thecodingidiot.com

Who Wants to Be a Game Developer? GraphicalThe Display

The Display

render_frame switches on g->state and calls the matching draw function. The ladder panel is not a state — it is drawn alongside every state except STATE_TITLE by calling draw_ladder in each case branch:

void    render_frame(game_t *g)
{
    SDL_RenderClear(g->ren);      /* clear back buffer to black */
    draw_background(g);
    switch (g->state) {
        case STATE_TITLE:    draw_title(g);                    break; /* no ladder on title */
        case STATE_QUESTION: draw_question(g); draw_ladder(g); break;
        case STATE_CONFIRM:  draw_confirm(g);  draw_ladder(g); break;
        case STATE_CORRECT:  draw_correct(g);  draw_ladder(g); break;
        case STATE_WRONG:    draw_wrong(g);    draw_ladder(g); break;
        case STATE_WIN:      draw_win(g);      draw_ladder(g); break;
        case STATE_GAMEOVER: draw_gameover(g); draw_ladder(g); break;
    }
    SDL_RenderPresent(g->ren);    /* swap back buffer to screen */
}

All seven draw functions are static — internal to render.c. They must be defined or forward-declared before render_frame.


Layout constants

The screen is divided into fixed zones, established in gen_assets.sh and mirrored here as constants. Add them at the top of render.c:

#define PAD     16   /* left margin */
#define LINE_H  22   /* pixels per text row */
 
/* left panel zone tops */
#define Q_Y     10   /* question zone: y = 0–179 */
#define ANS_Y   188  /* answer zone:   y = 180–419 */
#define HELP_Y  424  /* help zone:     y = 420–599 */
 
/* right ladder panel */
#define LDDR_X  572  /* ladder left edge */
#define LDDR_Y  12   /* first ladder row */
#define LDDR_H  38   /* pixels per ladder row */

Q_Y, ANS_Y, and HELP_Y pin text to the top of each coloured zone. LDDR_X keeps ladder text inside the right panel. Every draw function uses these constants — pixel values never appear in the draw functions themselves.


draw_wrapped

Most text in the game is short enough to fit on one line. Question text is not. draw_wrapped breaks a string into lines that fit within a given pixel width, measuring each word with TTF_SizeUTF8 before committing it to the current line:

static int draw_wrapped(game_t *g, int x, int y, int max_w, const char *s)
{
    char    buf[512];
    char    line[512];
    char   *word;
    char   *saveptr;
    int     line_w;
    int     word_w;
    int     space_w;
    int     h;
 
    snprintf(buf, sizeof(buf), "%s", s);
    line[0] = '\0';
    line_w = 0;
    TTF_SizeUTF8(g->font, " ", &space_w, &h);    /* measure space once */
    word = strtok_r(buf, " ", &saveptr);
    while (word != NULL) {
        TTF_SizeUTF8(g->font, word, &word_w, &h);
        if (line_w > 0 && line_w + space_w + word_w > max_w) {
            draw_string(g, x, y, line);          /* line full: flush */
            y += LINE_H;
            line[0] = '\0';
            line_w = 0;
        }
        if (line_w > 0) {
            tci_strlcat(line, " ", sizeof(line));
            line_w += space_w;
        }
        tci_strlcat(line, word, sizeof(line));
        line_w += word_w;
        word = strtok_r(NULL, " ", &saveptr);
    }
    if (line[0] != '\0') {                       /* flush last line */
        draw_string(g, x, y, line);
        y += LINE_H;
    }
    return (y); /* caller gets the y position after the last line */
}

snprintf(buf, sizeof(buf), "%s", s) is the first call — it copies s into buf with a size limit. snprintf is printf for buffers: same format strings, same specifiers (%s, %d, %c), but the output goes into the first argument instead of the terminal. The second argument is the total buffer size; snprintf never writes past it and always null-terminates. f04/05 names it as the safer general-purpose choice over strcpy — this is where it first appears in practice. Run man 3 snprintf.

TTF_SizeUTF8 fills word_w with the pixel width of the string at the current font size — no rendering happens. The space width is measured once before the loop.

strtok splits a string in place by replacing delimiter characters with \0, returning a pointer to each token in turn. State between calls is stored in a hidden static variable — which means two interleaved strtok calls on different strings corrupt each other. strtok_r is the re-entrant variant: the caller supplies saveptr to hold that state, so the function is safe even when called from multiple places simultaneously. Run man 3 strtok and man 3 strtok_r.

tci_strlcat from c01/03 takes the total buffer size rather than the remaining bytes, so tci_strlcat(line, word, sizeof(line)) is all that is needed — no remaining-space arithmetic.

draw_wrapped returns the y position after the last line written. The caller uses this to place the next element immediately below the wrapped text.


draw_ladder

The ladder renders into the right panel. The current level is marked with >, safe levels with *, already-passed levels with +:

static void draw_ladder(game_t *g)
{
    char    buf[48];
    int     i;
    int     y;
    char    marker;
 
    for (i = LEVELS - 1; i >= 0; i--) {
        y = LDDR_Y + (LEVELS - 1 - i) * LDDR_H; /* top row = highest prize */
        if (i == g->level)
            marker = '>';        /* current question */
        else if (SAFE[i])
            marker = '*';        /* safe level */
        else if (i < g->level)
            marker = '+';        /* already passed */
        else
            marker = ' ';
        snprintf(buf, sizeof(buf), "%c %s", marker, PRIZES[i]);
        draw_string(g, LDDR_X, y, buf);
    }
}

LDDR_H = 38 spreads 15 rows across 570 pixels of panel height, leaving a small margin at top and bottom. The loop runs top to bottom on screen — highest prize at the top — so the index runs backwards: i = LEVELS - 1 down to 0.


draw_lifeline_bar

The lifeline bar shows available lifelines and is called from draw_question. Each available lifeline contributes its label; spent lifelines are omitted. snprintf builds the whole string in one call:

static void draw_lifeline_bar(game_t *g, int y)
{
    char    buf[128];
 
    snprintf(buf, sizeof(buf), "%s%s%s[W] Walk away",
        (g->lifelines & 1) ? "[1] 50:50  "    : "",  /* bit 0 */
        (g->lifelines & 2) ? "[2] Phone  "    : "",  /* bit 1 */
        (g->lifelines & 4) ? "[3] Audience  " : ""); /* bit 2 */
    draw_string(g, PAD, y, buf);
}

Each conditional expression contributes either the label string or an empty string. snprintf concatenates whatever three expressions produce. [W] Walk away is always present.


draw_audience

The audience result shows a bar chart of percentages, one row per option. One # character per five percentage points:

static void draw_audience(game_t *g, int y)
{
    const char  *letters;
    char         buf[64];
    int          i;
    int          j;
    int          pos;
 
    letters = "ABCD";
    draw_string(g, PAD, y, "Ask the Audience:");
    y += LINE_H;
    for (i = 0; i < 4; i++) {
        pos = 0;
        buf[pos++] = letters[i];  /* "A: " prefix */
        buf[pos++] = ':';
        buf[pos++] = ' ';
        for (j = 0; j < g->audience[i] / 5 && pos < 55; j++)
            buf[pos++] = '#';     /* one '#' per 5 percentage points */
        buf[pos++] = ' ';
        if (g->audience[i] >= 100) {
            buf[pos++] = '1'; buf[pos++] = '0'; buf[pos++] = '0'; /* "100" */
        } else if (g->audience[i] >= 10) {
            buf[pos++] = '0' + g->audience[i] / 10; /* tens digit */
            buf[pos++] = '0' + g->audience[i] % 10; /* units digit */
        } else {
            buf[pos++] = '0' + g->audience[i];      /* single digit */
        }
        buf[pos++] = '%';
        buf[pos] = '\0';          /* terminate before draw_string */
        draw_string(g, PAD, y + i * LINE_H, buf);
    }
}

snprintf has no format specifier for "repeat this character N times". Building the # bar with it would require a separate loop to construct a # string first, then a second snprintf to assemble the final row. Writing buf character by character handles the bar, the space, and the percentage in one pass — no intermediate buffer needed.

The integer-to-string conversion uses the same '0' + digit trick from c02's tci_printf: subtracting the character '0' (value 48) from a digit character gives the numeric value; adding it back converts a digit value to the character. The three branches handle 100, two- digit, and one-digit percentages respectively.


draw_title

static void draw_title(game_t *g)
{
    (void)g;
    draw_string(g, PAD, Q_Y, "WHO WANTS TO BE A GAME DEVELOPER?");
    draw_string(g, PAD, ANS_Y, "Press Enter to start.");
    draw_string(g, PAD, ANS_Y + LINE_H, "Press Escape to quit.");
}

The title screen uses no game state — (void)g suppresses the unused parameter warning. Text lands in the question and answer zones.


draw_question

The question screen is the most complex: prize level, wrapped question text, visible answer options, the lifeline bar, and optionally the phone hint or audience chart:

static void draw_question(game_t *g)
{
    question_t  *q;
    const char  *letters;
    char         buf[256];
    int          y;
    int          i;
 
    q = g->questions[g->level];
    letters = "ABCD";
    draw_string(g, PAD, Q_Y, PRIZES[g->level]);               /* prize at top */
    draw_wrapped(g, PAD, Q_Y + LINE_H, 560 - PAD * 2, q->text);
    y = ANS_Y;
    for (i = 0; i < 4; i++) {
        if (g->hidden[i])
            continue;                                         /* 50:50 hides two */
        snprintf(buf, sizeof(buf), "%c. %s", letters[i], q->opts[i]);
        draw_string(g, PAD, y, buf);
        y += LINE_H + 4;
    }
    y = HELP_Y + PAD;
    draw_lifeline_bar(g, y);
    y += LINE_H;
    if (g->phone_active) {
        if (q->hint)
            y = draw_wrapped(g, PAD, y, 560 - PAD * 2, q->hint); /* custom hint */
        else {
            snprintf(buf, sizeof(buf), "The answer is %c.", letters[q->answer]);
            draw_string(g, PAD, y, buf);                     /* generic fallback */
            y += LINE_H;
        }
    }
    if (g->audience[0] || g->audience[1] || g->audience[2] || g->audience[3])
        draw_audience(g, y);                                  /* any vote cast */
}

The function works through three zones in order.

Question zone. PRIZES[g->level] lands at Q_Y. The question text starts one row below at Q_Y + LINE_H, passed to draw_wrapped because questions can exceed the left panel width. The return value of draw_wrapped is discarded here — the answer zone begins at a fixed ANS_Y, not relative to however many lines the question needed.

Answer zone. y starts at ANS_Y and advances with each visible option. The +4 in y += LINE_H + 4 adds a small gap between options, giving them visual breathing room beyond the bare line height. Options hidden by 50:50 are skipped; y only advances for the ones that are drawn, so the remaining options close the gap without leaving blank rows.

Help zone. y resets to HELP_Y + PAD — a fixed boundary, not a continuation from wherever the answer loop left off. The lifeline bar is drawn first. Then y advances by LINE_H before the optional elements, so they always start one row below the bar.

The phone hint and audience chart are independent: both can appear simultaneously if the player used both lifelines. The phone block captures draw_wrapped's return into y so that if the hint spans multiple lines, the audience chart starts below all of them. The audience guard checks that at least one percentage is non-zero — the g->audience array is zeroed in game_init and only filled after the audience lifeline is used.

560 - PAD * 2 is the max width for both draw_wrapped calls: 560 is the left edge of the divider between the two panels, and subtracting both margins keeps text clear of both edges.


draw_confirm

The confirm screen repeats the question and options, then asks for confirmation in the help zone:

static void draw_confirm(game_t *g)
{
    question_t  *q;
    const char  *letters;
    char         buf[512];
    int          y;
    int          i;
 
    q = g->questions[g->level];
    letters = "ABCD";
    draw_string(g, PAD, Q_Y, PRIZES[g->level]);
    draw_wrapped(g, PAD, Q_Y + LINE_H, 560 - PAD * 2, q->text);
    y = ANS_Y;
    for (i = 0; i < 4; i++) {
        if (g->hidden[i])
            continue;
        snprintf(buf, sizeof(buf), "%c. %s", letters[i], q->opts[i]);
        draw_string(g, PAD, y, buf);
        y += LINE_H + 4;
    }
    draw_string(g, PAD, HELP_Y + PAD, "Is that your final answer?");
    snprintf(buf, sizeof(buf),
        "Answer: %c  --  Enter to confirm, Backspace to go back.",
        g->pending);              /* pending: letter chosen in STATE_QUESTION */
    draw_string(g, PAD, HELP_Y + PAD + LINE_H, buf);
}

The confirm screen reuses the question and answer layout from draw_question unchanged — same prize, same wrapped text, same answer loop with the same 50:50 skip. The player must see exactly what they saw when they made their choice. The help zone carries the only new content: the confirmation prompt and the pending answer.

g->pending was set in handle_question when the player pressed A–D. It holds the letter as a char ('A''D'), formatted directly into the prompt with %c. The player sees their letter; pressing Enter commits it, Backspace discards it and returns to STATE_QUESTION.

Notice that draw_confirm has no lifeline bar and no phone or audience block. The player cannot use a lifeline after selecting an answer but before confirming — those keys are only handled in STATE_QUESTION.


draw_correct and draw_wrong

Both screens reveal the correct answer after the player has confirmed. The question text is shown again — not the options — so the player can read the answer in context. draw_wrapped is used because the question may wrap, and its return value is captured into y so the reveal lines always follow immediately below the last line, regardless of how many lines the question needed:

static void draw_correct(game_t *g)
{
    question_t  *q;
    int          y;
 
    q = g->questions[g->level];
    draw_string(g, PAD, Q_Y, "Correct!");
    y = draw_wrapped(g, PAD, ANS_Y, 560 - PAD * 2, q->text);  /* y = row after question */
    draw_string(g, PAD, y + 4, "The answer was:");            /* +4: small gap */
    draw_string(g, PAD, y + 4 + LINE_H, q->opts[q->answer]);
    draw_string(g, PAD, HELP_Y + PAD, "Press Space to continue.");
}
 
static void draw_wrong(game_t *g)
{
    question_t  *q;
    const char  *prize;
    int          y;
 
    q = g->questions[g->level];
    draw_string(g, PAD, Q_Y, "Wrong!");
    y = draw_wrapped(g, PAD, ANS_Y, 560 - PAD * 2, q->text);   /* y = row after question */
    draw_string(g, PAD, y + 4, "The answer was:");             /* +4: small gap */
    draw_string(g, PAD, y + 4 + LINE_H, q->opts[q->answer]);
    prize = g->safe_level >= 0 ? PRIZES[g->safe_level] : "£0"; /* -1 if no safe reached */
    draw_string(g, PAD, HELP_Y + PAD, "You leave with:");
    draw_string(g, PAD, HELP_Y + PAD + LINE_H, prize);
    draw_string(g, PAD, HELP_Y + PAD + LINE_H * 2, "Press Escape to quit.");
}

In draw_correct, y positions the reveal two rows below the question: y + 4 for the label and y + 4 + LINE_H for the answer text itself. The help zone is used only for the continue prompt — there is no prize to display yet, because next_question has not been called. The level advances only after the player presses Space.

draw_wrong follows the same layout for the question reveal, then uses all three rows of the help zone: the "You leave with:" label, the prize string, and the quit instruction. g->safe_level holds the index of the last banked safe level, or -1 if the player never reached one. The ternary resolves this to a prize string or "£0" before any draw_string call — the draw call itself stays unconditional.

Both functions show the question text but not the answer options. The options are no longer relevant once the answer has been evaluated; the question text gives context for the reveal.


draw_win and draw_gameover

static void draw_win(game_t *g)
{
    (void)g;                     /* no game state needed */
    draw_string(g, PAD, Q_Y, "YOU HAVE JUST WON £1,000,000!");
    draw_string(g, PAD, ANS_Y, "Congratulations!");
    draw_string(g, PAD, HELP_Y + PAD, "Press Escape to quit.");
}
 
static void draw_gameover(game_t *g)
{
    const char  *prize;
 
    prize = g->safe_level >= 0 ? PRIZES[g->safe_level] : "£0"; /* -1 if no safe reached */
    draw_string(g, PAD, Q_Y, "You walk away with:");
    draw_string(g, PAD, ANS_Y, prize);
    draw_string(g, PAD, HELP_Y + PAD, "Press Escape to quit.");
}

draw_gameover handles both a deliberate walk-away and a walk-away forced by loss — in both cases g->state is STATE_GAMEOVER and safe_level holds the last banked prize, or -1 if none was reached.


draw_string vs draw_wrapped

Answer options use draw_string — a single line, no wrapping. Question text uses draw_wrapped. The distinction is a design decision, not a technical limitation, and it is worth stating explicitly.

If a question is long enough to wrap into the answer zone, the two regions will overlap and the layout breaks. The code has no guard against this. draw_wrapped does not know where the answer zone starts; it just keeps advancing y by LINE_H until the text is exhausted.

This is acceptable because the game is a controlled environment. The questions are not typed at runtime by the player — they come from a file the developer controls. The same applies to answer options: if an option is wider than the left panel, draw_string clips it silently. Neither path can be reached by the player as long as the question file is curated.

The discipline is not to eliminate every possible failure, but to identify every possible failure and confirm that the design prevents it from being reachable. Here, the prevention is authorial: keep questions short enough to fit.

A shipped game would bake the questions in at compile time, removing the file argument entirely and making the question set immutable. The file-based approach exists here because it makes the game easier to iterate during development.

If this were a system that accepted arbitrary user input — a text editor, a terminal emulator, a chat client — the overlap would be a bug. In a quiz game with a curated question bank, it is a documented constraint.


Test the display

Build and run:

make re
./game questions.txt

The window should show the title screen. To test a specific draw function before input handling is in place, temporarily force a state after game_init:

    game_init(&g, questions, count);
    g.state = STATE_QUESTION; /* force for testing */

Remove the override before the next page.