thecodingidiot.com

Who Wants to Be a Game Developer? GraphicalPlay It

Play It

Build and run the complete game:

make re
./game questions.txt

The title screen appears on a dark navy background. Press Enter to start. Answer all fifteen questions correctly to reach the win screen. Press Escape at any point to quit.


The question file

Questions are read at startup from a plain text file passed as the first argument. Each line is one question in pipe-separated format:

text|A|B|C|D|answer|hint

answer is the zero-indexed position of the correct option: 0 for A, 1 for B, 2 for C, 3 for D. hint is optional — if omitted, the phone lifeline falls back to generating "The answer is X." from q->answer. Lines beginning with # are comments and are skipped.

One field in the bank contains a pipe character as part of its content — the question about the shell operator |. A literal | inside a field would be misread as a field separator, so the format uses || as an escape sequence: || in any field is decoded to a single |. load.c handles this in next_field.


load.c

load.c has no SDL2 dependency — it was written to be testable without a display. The file format, load_questions, free_questions, the realloc doubling pattern, and free(NULL) were all covered in g01a/02 and have not changed. The g01b version is a structural rewrite: field extraction and struct allocation are split into two named functions (next_field and parse_line) rather than inlined inside load_questions. The libtci calls stay the same — tci_getline, tci_calloc, tci_strdup, tci_atoi, tci_strchr, tci_printf — because g01b sits before c07, where the curriculum switches to libc deliberately.

The one part worth examining closely is how || escaping is handled. g01a used a two-pass approach: encode_pipes replaced || with a sentinel byte '\x01' before splitting, then decode_pipes restored it afterwards. g01b handles it in a single pass inside next_field:

static char *next_field(char **p)
{
    char    *start;
    char    *dst;
 
    if (!*p || !**p)
        return (NULL);
    start = *p;
    dst = start;
    while (**p) {
        if (**p == '|') {
            if (*(*p + 1) == '|') { /* || → literal | */
                *dst++ = '|';
                *p += 2;
            } else {                /* single | → field separator */
                *dst = '\0';
                (*p)++;
                return (start);
            }
        } else {
            *dst++ = **p;
            (*p)++;
        }
    }
    *dst = '\0';                    /* end of string: last field */
    return (start);
}

The function extracts one field and advances *p past its separator. Decoding is immediate: when **p is | and the next character is also |, a single | is written to dst and *p skips two characters. Otherwise a single | closes the field. Because || shrinks to |, dst trails *p whenever an escape is decoded — the output is written back into the same buffer in place, and it is never longer than the input.

The g01a sentinel approach required two full passes over the string and a third pass over each field after splitting. next_field does everything in one loop with no intermediate state.


The question bank

The game ships with 20 questions — the 15 from g01a plus 5 covering c04. The Fisher-Yates shuffle in main.c picks 15 at random for each run.

Which command lists files and directories in a Unix terminal?|ls|dir|list|show|0|The answer is A: ls.
What does the shell operator || do?|redirects output|pipes stdout to stdin|splits input|forks a process|1|The answer is B: it pipes stdout of one command to stdin of the next.
Which C keyword exits a loop immediately?|exit|stop|break|return|2|The answer is C: break.
What does malloc return on failure?|0|an empty buffer|NULL|-1|2|The answer is C: NULL.
In C, what is the size of a char in bytes?|2|4|8|1|3|The answer is D: 1 byte.
What does the tci_strlen function return?|A pointer to the string|The number of bytes until the null terminator|The allocated size of the string|The ASCII value of the first character|1|The answer is B: the number of bytes until the null terminator.
Which file descriptor is standard output?|0|2|3|1|3|The answer is D: 1.
What does write() return on success?|0|The number of bytes written|A pointer to the buffer|1|1|The answer is B: the number of bytes written.
Which ANSI escape sequence resets terminal colour?|033[1m|033[0m|033[33m|033[2J|1|The answer is B: \033[0m.
In a Makefile, what does .PHONY declare?|A file that will never be created|A target that does not correspond to a file|A variable|A pattern rule|1|The answer is B: a target that does not correspond to a file.
What does tci_getline return at end-of-file?|An empty string|0|-1|NULL|3|The answer is D: NULL.
Which operator in C tests a single bit?|^|~|&|||2|The answer is C: bitwise AND (&).
What does the Fisher-Yates shuffle guarantee?|Alphabetical order|A uniform random permutation|The same sequence every run|Sorted descending order|1|The answer is B: a uniform random permutation.
What does realloc(NULL, n) do?|Returns NULL|Crashes|Behaves like malloc(n)|Returns a zero-filled buffer|2|The answer is C: it behaves exactly like malloc(n).
Which C standard is used throughout this curriculum?|C89|C11|C17|C99|3|The answer is D: C99.
In SDL2, what does SDL_UpdateTexture do?|Destroys the texture|Uploads a pixel buffer to the GPU|Creates a new texture|Clears the renderer|1|The answer is B: it uploads your pixel buffer to the GPU.
In the SDL2 pixel format ARGB8888, which byte position holds alpha?|Bits 0-7|Bits 8-15|Bits 16-23|Bits 24-31|3|The answer is D: bits 24-31 — the most significant byte.
What escape condition does the Mandelbrot iteration check?|re^2 + im^2 > 2|re^2 + im^2 >= 4|re^2 + im^2 > 4|re + im > 4|2|The answer is C: strictly greater than 4, not >= 4.
In HSV colour, what does H stand for?|Hardness|Hue|Highlight|Haze|1|The answer is B: Hue — position on the colour wheel from 0 to 360 degrees.
In a pixel buffer of width W, how is pixel (x, y) addressed?|x + y * H|x * W + y|y * W + x|x + y|2|The answer is C: y * W + x.

Question 12 uses || for the option | — bitwise OR is a wrong answer, but it contains the separator character. The next_field escape mechanism was added specifically to support this question.


The tester

The game renders to a window — it cannot be run in a pipe or a CI environment without a display. The logic, however, is entirely SDL2-free: load.c and game.c contain no SDL2 calls. test.sh exploits that separation.

bash test.sh

The script generates a headless version of game.h at /tmp/ with void * in place of every SDL2 type (SDL_Window *, SDL_Renderer *, etc.), then patches game.c and load.c with sed to include the headless header instead of the real one. The patched files compile without SDL2 headers or libraries.

The tests cover:

  • Question loading: 20 questions parsed correctly; first question text is non-empty
  • game_init: state is STATE_TITLE, level is 0, safe_level is -1, lifelines is 7
  • Safe-level advancement: next_question sets safe_level to 4 at level 5 and to 9 at level 10; state is STATE_QUESTION at both (the ladder is always visible — no separate state needed)
  • Lifeline bitfield: all three bits set at start; each lifeline clears its bit; a spent lifeline cannot be used again
  • evaluate_answer: correct pending letter → STATE_CORRECT; wrong pending letter → STATE_WRONG

Graphical output is not tested. The tester states this at the start:

g01b tester — logic only; graphical output is not covered

The separation between game.c and render.c is what makes the logic testable without a display. That separation is the lesson.


Companion repo

The reference solution, assets, and tester are at: github.com/thecodingidiot-com/g01b-the-developer-graphical

FileNotes
main.cSDL2 init, shuffle, event loop, cleanup
load.cQuestion parser; no SDL2 dependency
game.cGame logic; no SDL2 dependency
render.cAll draw_* functions; SDL2 only
font.cTTF font loading and text rendering; SDL2 only
Makefilesdl2-config --cflags --libs, -lSDL2_image, -lSDL2_ttf, $(MAKE) -C libtci
questions.txt20 questions
assets/font.ttfUbuntuMono-R (system font copy; swap for Px437 IBM EGA 8×14 from int10h.org for the retro look)
assets/bg_studio.png#0a0a2a 800×600
assets/bg_correct.png#0a2a0a 800×600
assets/bg_wrong.png#2a0a0a 800×600
libtci/Full libtci + libtciutil source; make -C libtci re builds the archives
gen_assets.shGenerates the three background PNGs and copies the font
test.shHeadless logic tester

up next

The Pipeline

The Pipeline