Build and run the complete game:
make re
./game questions.txtThe 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|hintanswer 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.shThe 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 isSTATE_TITLE, level is 0,safe_levelis -1, lifelines is 7- Safe-level advancement:
next_questionsetssafe_levelto 4 at level 5 and to 9 at level 10; state isSTATE_QUESTIONat 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 coveredThe 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
| File | Notes |
|---|---|
main.c | SDL2 init, shuffle, event loop, cleanup |
load.c | Question parser; no SDL2 dependency |
game.c | Game logic; no SDL2 dependency |
render.c | All draw_* functions; SDL2 only |
font.c | TTF font loading and text rendering; SDL2 only |
Makefile | sdl2-config --cflags --libs, -lSDL2_image, -lSDL2_ttf, $(MAKE) -C libtci |
questions.txt | 20 questions |
assets/font.ttf | UbuntuMono-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.sh | Generates the three background PNGs and copies the font |
test.sh | Headless logic tester |