g01b builds on g01a.
Clone the companion repo — it carries everything needed, including the
libtci/ subdirectory with all library source:
git clone https://github.com/thecodingidiot-com/g01b-the-developer-graphical.git g01b-practice
cd g01b-practiceload.c and questions.txt are already present. Every other source
file is new or rewritten from g01a.
Why libtci ships as source
A compiled static archive (libtci.a) is architecture-specific — an
x86_64 Linux build cannot be used on ARM or macOS. Committing a binary
to a source repository also makes it opaque: there is no way to read,
audit, or rebuild it from the repo alone.
Shipping the source files in libtci/ solves both problems. make re
at the top level descends into libtci/ and builds libtci.a from
scratch using your own compiler and flags. The result is always correct
for your machine, and every function is readable alongside the game
code that calls it.
The limitation is worth naming: thecodingidiot.com declares
environment: "linux" for all c-tier and games chapters. The
curriculum does not support macOS, Windows, or ARM targets. For a
single-platform curriculum, architecture portability is not a concern
— but the principle of shipping source over binaries remains the right
default regardless.
Build the library once before anything else:
make -C libtci relibtci.a appears inside libtci/ with no warnings. Subsequent
make re calls at the top level rebuild the library only if its
sources have changed.
Install SDL2_image and SDL2_ttf
c04 installed SDL2. g01b adds two extensions: SDL2_image for loading PNG background files, and SDL2_ttf for rendering text from a TrueType font:
sudo apt install libsdl2-image-dev libsdl2-ttf-devVerify both are installed:
dpkg -l libsdl2-image-dev libsdl2-ttf-devBoth lines should start with ii.
Generate the assets
The game needs three 800×600 background PNGs and the Px437 IBM EGA 8×14 TrueType font (CC BY-SA 4.0, VileR / int10h.org). Both are in the companion repo. Clone it alongside your practice directory:
git clone https://github.com/thecodingidiot-com/g01b-the-developer-graphical.git g01b-companionGenerate the backgrounds from your practice directory:
cd ~/g01b-practice
bash ~/g01b-companion/gen_assets.shThe script creates assets/ in the current directory and writes
three PNGs into it — one per background colour, each divided into
four zones: question (top-left), answer (middle-left), help
(bottom-left), and ladder panel (right). Running the script from
the practice directory ensures the files land there, not inside the
companion repo clone.



Copy the font:
cp ~/g01b-companion/assets/font.ttf assets/The Makefile
CC = gcc
CFLAGS = -Wall -Wextra -Werror -D_REENTRANT -I libtci
LDFLAGS = $(shell sdl2-config --libs) -lSDL2_image -lSDL2_ttf
SRCS = main.c load.c game.c render.c font.c
OBJS = $(SRCS:.c=.o)
LIBS = libtci/libtci.a
NAME = game
.PHONY: all clean fclean re
all: $(NAME)
$(NAME): $(OBJS) $(LIBS)
$(CC) $(OBJS) $(LIBS) $(LDFLAGS) -o $(NAME)
$(LIBS):
$(MAKE) -C libtci
%.o: %.c game.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
$(MAKE) -C libtci clean
rm -f $(OBJS)
fclean: clean
$(MAKE) -C libtci fclean
rm -f $(NAME)
re: fclean all-D_REENTRANT is the flag SDL2 requires — it enables thread-safe
versions of certain library functions (what threads are and why they
need safe library functions is covered in
c08).
sdl2-config --cflags would produce the same flag, but it also
injects -I/usr/include/SDL2 into the include path. Combined with
#include <SDL2/SDL.h> in the source that becomes a double-nested
lookup /usr/include/SDL2/SDL2/SDL.h which does not exist. Using
-D_REENTRANT directly avoids the conflict. sdl2-config --libs
on the linker side has no such problem and is kept.
sdl2-config is installed with libsdl2-dev. LDFLAGS adds
-lSDL2_image and -lSDL2_ttf after the SDL2 base libraries.
The pattern rule %.o: %.c game.h rebuilds every object when
the header changes.
game.h
game.h is the contract between all five source files. Replace
your g01a version with this:
#ifndef GAME_H
# define GAME_H
# include <SDL2/SDL.h>
# include <SDL2/SDL_image.h>
# include <SDL2/SDL_ttf.h>
# include <fcntl.h>
# include <unistd.h>
# include <stdlib.h>
# include <time.h>
# include "libtci.h"
# define LEVELS 15
# define WIN_W 800
# define WIN_H 600
typedef struct s_question
{
char *text;
char *opts[4];
int answer;
char *hint;
} question_t;
typedef enum e_state
{
STATE_TITLE,
STATE_QUESTION,
STATE_CONFIRM,
STATE_CORRECT,
STATE_WRONG,
STATE_WIN,
STATE_GAMEOVER
} game_state_t;
typedef struct s_game
{
SDL_Window *win;
SDL_Renderer *ren;
SDL_Texture *bg_studio;
SDL_Texture *bg_correct;
SDL_Texture *bg_wrong;
TTF_Font *font;
game_state_t state;
question_t **questions;
int count;
int level;
int safe_level;
int lifelines;
int phone_active;
int hidden[4];
int audience[4];
char pending;
} game_t;
extern const char *PRIZES[LEVELS];
extern const int SAFE[LEVELS];
/* load.c */
question_t **load_questions(const char *path, int *count);
void free_questions(question_t **questions, int count);
/* game.c */
void game_init(game_t *g, question_t **questions, int count);
void game_free(game_t *g);
void evaluate_answer(game_t *g);
void handle_lifeline(game_t *g, int lifeline);
void next_question(game_t *g);
/* font.c */
int font_load(game_t *g, const char *path, int ptsize);
void font_free(game_t *g);
void draw_string(game_t *g, int x, int y, const char *s);
/* render.c */
int render_init(game_t *g);
void render_free(game_t *g);
void render_frame(game_t *g);
/* main.c */
void handle_event(game_t *g, SDL_Event *ev, int *running);
#endifThe SDL2 headers are included once here. Every file that includes
game.h sees the SDL2 types — SDL_Window *, SDL_Renderer *,
TTF_Font * — without including the headers again. load.c and
game.c include game.h but call no SDL2 functions. Only
render.c, font.c, and main.c call into the SDL2 API. The
platform separation rule is about what each file calls, not what
it sees.
phone_active is a flag set when the Phone-a-Friend lifeline is
used and cleared when the next question begins. It controls whether
the hint is displayed. Without it, the hint from question N would
persist to question N+1 because the lifeline bit stays cleared for
the rest of the game.
PRIZES and SAFE are declared extern here and defined in
game.c. They are game data — the prize ladder and the safe-level
markers — not display data.
game.c
game.c carries the logic from g01a forward with three changes:
PRIZES and SAFE become non-static globals (they are declared
extern in game.h), phone_active is initialised and reset,
and next_question no longer switches to a dedicated ladder state
— the ladder is always visible as a panel in the render layer.
#include "game.h"
const char *PRIZES[LEVELS] = {
"£100", "£200", "£300", "£500", "£1,000",
"£2,000", "£4,000", "£8,000", "£16,000", "£32,000",
"£64,000", "£125,000", "£250,000", "£500,000", "£1,000,000"
};
const int SAFE[LEVELS] = {
0, 0, 0, 0, 1,
0, 0, 0, 0, 1,
0, 0, 0, 0, 0
};
void game_init(game_t *g, question_t **questions, int count)
{
tci_bzero(g->hidden, sizeof(g->hidden));
tci_bzero(g->audience, sizeof(g->audience));
g->state = STATE_TITLE;
g->questions = questions;
g->count = count;
g->level = 0;
g->safe_level = -1;
g->lifelines = 7;
g->phone_active = 0;
g->pending = 0;
}
void game_free(game_t *g)
{
(void)g;
}
void evaluate_answer(game_t *g)
{
question_t *q;
q = g->questions[g->level];
if (g->pending - 'A' == q->answer)
g->state = STATE_CORRECT;
else
g->state = STATE_WRONG;
}
void handle_lifeline(game_t *g, int lifeline)
{
question_t *q;
int bit;
int removed;
int i;
q = g->questions[g->level];
bit = 1 << (lifeline - 1);
if (!(g->lifelines & bit))
return;
g->lifelines &= ~bit;
if (lifeline == 1) {
removed = 0;
for (i = 0; i < 4 && removed < 2; i++) {
if (i == q->answer)
continue;
g->hidden[i] = 1;
removed++;
}
} else if (lifeline == 2) {
g->phone_active = 1;
} else if (lifeline == 3) {
int correct;
int spread;
correct = 55 + (rand() % 30);
g->audience[q->answer] = correct;
spread = 100 - correct;
for (i = 0; i < 4; i++) {
if (i == q->answer)
continue;
if (g->hidden[i]) {
g->audience[i] = 0;
} else {
int portion = spread / 3;
g->audience[i] = portion;
spread -= portion;
}
}
}
}
void next_question(game_t *g)
{
g->level++;
if (g->level >= LEVELS) {
g->state = STATE_WIN;
return;
}
tci_bzero(g->hidden, sizeof(g->hidden));
tci_bzero(g->audience, sizeof(g->audience));
g->phone_active = 0;
g->pending = 0;
if (SAFE[g->level])
g->safe_level = g->level;
g->state = STATE_QUESTION;
}lifelines is initialised to 7 — binary 111, one bit per
lifeline. handle_lifeline receives a lifeline number (1, 2, or 3),
derives the bit with 1 << (lifeline - 1), checks it is still set,
clears it, and applies the effect. A second call with the same
lifeline number returns immediately because the bit is already clear.
The audience percentages are weighted: the correct answer is assigned a random value between 55 and 84, the remainder is distributed among the wrong answers proportionally. The visible bars are a hint, not a guarantee.
Stubs: font.c and render.c
Create these two files. They will be filled in on the pages that follow. The stubs compile cleanly and produce a working window.
font.c:
#include "game.h"
int font_load(game_t *g, const char *path, int ptsize)
{
(void)g;
(void)path;
(void)ptsize;
return (0);
}
void font_free(game_t *g)
{
(void)g;
}
void draw_string(game_t *g, int x, int y, const char *s)
{
(void)g;
(void)x;
(void)y;
(void)s;
}render.c:
#include "game.h"
int render_init(game_t *g)
{
(void)g;
return (0);
}
void render_free(game_t *g)
{
(void)g;
}
void render_frame(game_t *g)
{
SDL_RenderClear(g->ren);
SDL_RenderPresent(g->ren);
}main.c
main.c owns the SDL2 lifecycle and the event loop. It also defines
handle_event — the function that translates SDL key events into
game state changes.
#include "game.h"
static void shuffle(question_t **questions, int count)
{
int i;
int j;
question_t *tmp;
for (i = count - 1; i > 0; i--) {
j = rand() % (i + 1);
tmp = questions[i];
questions[i] = questions[j];
questions[j] = tmp;
}
}
void handle_event(game_t *g, SDL_Event *ev, int *running)
{
if (ev->type == SDL_QUIT) {
*running = 0;
return;
}
if (ev->type == SDL_KEYDOWN && ev->key.keysym.sym == SDLK_ESCAPE)
*running = 0;
(void)g;
}
int main(int argc, char *argv[])
{
game_t g;
question_t **questions;
int count;
SDL_Event ev;
int running;
if (argc < 2) {
tci_printf("usage: %s questions.txt\n", argv[0]);
return (1);
}
srand((unsigned int)time(NULL));
questions = load_questions(argv[1], &count);
if (!questions || count < LEVELS) {
tci_printf("need at least %d questions, got %d\n", LEVELS, count);
free_questions(questions, count);
return (1);
}
shuffle(questions, count);
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init: %s", SDL_GetError());
free_questions(questions, count);
return (1);
}
if (IMG_Init(IMG_INIT_PNG) == 0) {
SDL_Log("IMG_Init: %s", IMG_GetError());
SDL_Quit();
free_questions(questions, count);
return (1);
}
if (TTF_Init() != 0) {
SDL_Log("TTF_Init: %s", TTF_GetError());
IMG_Quit();
SDL_Quit();
free_questions(questions, count);
return (1);
}
tci_bzero(&g, sizeof(g));
g.win = SDL_CreateWindow("Who Wants to Be a Game Developer?",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WIN_W, WIN_H, 0);
if (!g.win) {
SDL_Log("SDL_CreateWindow: %s", SDL_GetError());
TTF_Quit();
IMG_Quit();
SDL_Quit();
free_questions(questions, count);
return (1);
}
g.ren = SDL_CreateRenderer(g.win, -1, SDL_RENDERER_ACCELERATED);
if (!g.ren) {
SDL_Log("SDL_CreateRenderer: %s", SDL_GetError());
SDL_DestroyWindow(g.win);
TTF_Quit();
IMG_Quit();
SDL_Quit();
free_questions(questions, count);
return (1);
}
if (font_load(&g, "assets/font.ttf", 16) != 0) {
SDL_DestroyRenderer(g.ren);
SDL_DestroyWindow(g.win);
TTF_Quit();
IMG_Quit();
SDL_Quit();
free_questions(questions, count);
return (1);
}
if (render_init(&g) != 0) {
font_free(&g);
SDL_DestroyRenderer(g.ren);
SDL_DestroyWindow(g.win);
TTF_Quit();
IMG_Quit();
SDL_Quit();
free_questions(questions, count);
return (1);
}
game_init(&g, questions, count);
running = 1;
while (running) {
while (SDL_PollEvent(&ev))
handle_event(&g, &ev, &running);
render_frame(&g);
SDL_Delay(16);
}
render_free(&g);
game_free(&g);
font_free(&g);
SDL_DestroyRenderer(g.ren);
SDL_DestroyWindow(g.win);
TTF_Quit();
IMG_Quit();
SDL_Quit();
free_questions(questions, count);
return (0);
}The init sequence follows the same pattern as
c04: each
subsystem is initialised in order, and if any step fails, every
subsystem already open is closed before returning. The cleanup at
the end mirrors the init in reverse — render_free before
font_free before the renderer and window before the SDL
subsystems.
SDL_Delay(16) caps the loop at roughly 60 frames per second.
The game renders the same frame every 16 ms until a key is pressed
— SDL_PollEvent returns 0 when the event queue is empty and
control falls through to render_frame. The frame budget is not
tight; the dominant cost is TTF_RenderUTF8_Solid per string,
which is covered on the next page.
handle_event at this stage only handles quit and escape. The
full key dispatch — A through D for answers, 1 through 3 for
lifelines, Space and Enter to advance — is built in
The Input.
tci_bzero(&g, sizeof(g)) zeroes the entire struct before the
first field is written. This ensures every pointer is NULL and
every integer is 0 before SDL2 handles are assigned, which makes
the cleanup sequence safe even if initialisation fails partway
through.
Confirm the window opens
make re
./game questions.txtA black window should appear. Press Escape to quit. The window is
empty because render_frame only clears to black — the background
PNG is loaded in The Background, and text appears in The
Font. A clean build with an opening window confirms that the
Makefile, the SDL2 init chain, and the stub files are all correct.
If make fails with a missing header error, confirm that
libsdl2-image-dev and libsdl2-ttf-dev are installed. If the
window does not open, the error message from SDL_Log or tci_printf will name the
failing step.