thecodingidiot.com

Who Wants to Be a Game Developer?The Question File

The Question File

The question file is a plain text file — one question per line, seven fields separated by |. load_questions opens it, reads it line by line using tci_getline, and parses each line using tciu_split. The result is a heap-allocated array of question_t * pointers.

This is precisely why those two functions were worth writing.


Opening the file

load_questions takes a file path and a pointer to an int where it will store the count. It returns a question_t ** — an array of pointers to individually allocated question structs:

question_t  **load_questions(const char *path, int *count)
{
    int         fd;
    char        *line;
    char        *nl;
    char        **fields;
    question_t  **questions;
    question_t  *q;
    int         capacity;
    int         i;
 
    fd = open(path, O_RDONLY);
    if (fd < 0) {
        tci_printf("error: cannot open '%s'\n", path);
        return (NULL);
    }
    questions = NULL;
    *count = 0;
    capacity = 0;

open is the same syscall used in c03/01. O_RDONLY is in <fcntl.h>, which game.h already includes.


Handling the || escape

tciu_split splits on every |, unconditionally. The file format handles literal pipes with || — the same character doubled, the same way tci_printf handles %%. A preprocessing step collapses || to a safe placeholder before the split, and a second step restores it afterwards.

Add both helpers as static functions above load_questions:

static void encode_pipes(char *s)
{
    char *r;
    char *w;
 
    r = s;
    w = s;
    while (*r) {
        if (r[0] == '|' && r[1] == '|') {
            *w++ = '\x01';  /* C0 control byte — cannot appear in valid UTF-8 text */
            r += 2;
        } else {
            *w++ = *r++;
        }
    }
    *w = '\0';              /* loop exits before the null terminator is copied */
}
 
static void decode_pipes(char **fields)
{
    int   i;
    char *p;
 
    for (i = 0; fields[i]; i++) {
        for (p = fields[i]; *p; p++) {
            if (*p == '\x01')
                *p = '|';
        }
    }
}

encode_pipes is a compaction loop: r reads ahead, w writes behind. When r sees ||, w stores the sentinel byte '\x01' and r skips two characters; otherwise both advance together. '\x01' is a control byte that cannot appear in a UTF-8[1] question file. decode_pipes walks every field after splitting and restores each '\x01' to '|'.


Reading and parsing

The read loop calls tci_getline on every iteration. Each call returns one line including the trailing '\n', or NULL at EOF:

    while ((line = tci_getline(fd)) != NULL) {
        nl = tci_strchr(line, '\n');
        if (nl)
            *nl = '\0';
        encode_pipes(line);
        fields = tciu_split(line, '|');
        free(line);         /* tciu_split copied every field; line is no longer needed */
        if (!fields || !fields[0] || !fields[4]) {
            if (fields)
                free(fields);
            continue;
        }
        decode_pipes(fields);

The tci_strchr call strips the trailing newline before splitting. encode_pipes must run before tciu_split — once the line is split, field boundaries are already set. The guard on fields[4] skips any line that does not have at least five fields. decode_pipes runs immediately after validation, so every string that reaches question_t already has '|' where the file had ||.

free(line) releases the string returned by tci_getline. It was allocated inside the function; we own it from the moment it is returned.


Growing the array

The array of question pointers grows dynamically — the same doubling pattern from f05/01:

        if (*count == capacity) {
            capacity = capacity ? capacity * 2 : 16;
            questions = realloc(questions, capacity * sizeof(question_t *));
        }

realloc(NULL, n) is equivalent to malloc(n), so the first iteration works correctly with questions initialised to NULL.

Read man 3 realloc — it documents the return value on failure and the exact behaviour when the pointer or size is zero. Any function from the standard library that you use for the first time is worth a man lookup before you assume you understand it.


Filling question_t

Once the array has room, allocate the struct and copy each field:

        q = tci_calloc(1, sizeof(question_t));
        q->text       = tci_strdup(fields[0]);
        q->opts[0]    = tci_strdup(fields[1]);
        q->opts[1]    = tci_strdup(fields[2]);
        q->opts[2]    = tci_strdup(fields[3]);
        q->opts[3]    = tci_strdup(fields[4]);
        q->answer     = fields[5] ? fields[5][0] - '0' : 0;  /* '0'==48, '1'==49 … */
        q->hint       = (fields[6] && fields[6][0])          /* absent or empty → NULL */
                        ? tci_strdup(fields[6])
                        : NULL;
        questions[(*count)++] = q;

tci_calloc zeroes the struct, so any field not explicitly set starts as NULL or 0. fields[5][0] - '0' converts the single-digit answer character to an integer index. If no hint field is present or it is empty, q->hint stays NULL — the game loop generates one at runtime.


Freeing the split result

tciu_split returns a NULL-terminated char **. Each string in the array is a separate allocation. Free them all, then free the array:

        i = 0;
        while (fields[i])
            free(fields[i++]);
        free(fields);
    }
    close(fd);
    return (questions);
}

free_questions

Every allocation made in load_questions is released here:

void    free_questions(question_t **questions, int count)
{
    int  i;
    int  j;
 
    if (!questions)
        return;
    for (i = 0; i < count; i++) {
        free(questions[i]->text);
        for (j = 0; j < 4; j++)
            free(questions[i]->opts[j]);
        free(questions[i]->hint);
        free(questions[i]);
    }
    free(questions);
}

free(NULL) is defined as a no-op in C99, so the free(questions[i]->hint) call is safe even when hint was never set.


Wire it into main

Replace the main.c stub with a version that loads questions and confirms the count:

#include "game.h"
 
int main(int argc, char **argv)
{
    question_t  **questions;
    int          count;
 
    if (argc < 2) {
        tci_printf("usage: %s <questions.txt>\n", argv[0]);
        return (1);
    }
    questions = load_questions(argv[1], &count);
    if (!questions)
        return (1);
    tci_printf("loaded %d questions\n", count);
    free_questions(questions, count);
    return (0);
}

Create a minimal questions.txt with one line and test:

Which command lists files and directories in a Unix terminal?|ls|dir|list|show|0|The answer is A: ls.
make re
./game questions.txt
loaded 1 questions

The loader works. The full question file comes on the last page.

Footnotes

  1. UTF-8 - Wikipedia