thecodingidiot.com

Who Wants to Be a Game Developer?The Spec

The Spec

Before writing a function, draw the states. A game is a state machine. Every box in the diagram below is a function. Every arrow is a decision. The two terminal nodes — WIN and game over — are where the program exits. The loop back to "Level 1" is the main game.

The diagram is not decoration — it is the spec in visual form. Read each node and ask what it needs to do its job:

  • load_questions → a file path; returns a question array and a count
  • Display prize ladder → the current level and last safe level reached
  • Display question → the question struct and a hidden[] array
  • Correct? → the player's answer index compared to q->answer
  • Drop to safe level → a safe_level index into PRIZES[]

That reading produces the data structures. Nothing below is invented — it is all read out of the diagram.


The prize ladder

The game revolves around a fifteen-level ladder. Each correct answer moves the player up one level; a wrong answer drops them — but not necessarily to zero. Two of the fifteen levels are safe: clearing them locks in a floor prize the player keeps regardless of what happens next.

The code needs to answer two questions at any level index: what prize does it represent, and is it a safe level? Two parallel constant arrays capture both. Add them to display.c — they are used only by the display functions:

static 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"
};
 
static const int SAFE[LEVELS] = {
    0, 0, 0, 0, 1,
    0, 0, 0, 0, 1,
    0, 0, 0, 0, 0
};

PRIZES[0] is level 1 (£100). PRIZES[14] is level 15 (£1,000,000). SAFE[4] and SAFE[9] are the two safe levels. The index is always one less than the level number the player sees.


question_t

Before writing any display or game logic, ask what a question actually is. There is a piece of text — the question itself, a string. There are four answer options — also strings, so an array of four strings. One of those four is correct, identified by its position — an integer index, 0 through 3. And there is an optional hint for the Phone-a-Friend lifeline — another string, which may be absent.

That analysis produces the struct directly. game.h already declares it:

typedef struct {
    char    *text;
    char    *opts[4];   /* A, B, C, D */
    int      answer;    /* 0–3: index of correct option */
    char    *hint;      /* Phone-a-Friend text */
} question_t;

text is the question string. opts[0] through opts[3] are the four option strings. answer is the index of the correct one — 0 for A, 1 for B, 2 for C, 3 for D. hint is the text the game shows when the Phone-a-Friend lifeline is used.

All four pointers are heap-allocated and owned by the struct. The loader allocates them; free_questions releases them.


The question file format

Seven pipe-separated fields per line:

question text|option A|option B|option C|option D|answer_index|hint

answer_index is a single digit: 0 for A, 1 for B, 2 for C, 3 for D. hint is optional — the field may be absent, in which case the game generates "The answer is X." at runtime.

A concrete example:

Which command lists files and directories in a Unix terminal?|ls|dir|list|show|0|The answer is A: ls.

The | separator was chosen because it rarely appears in question text. When it does — a question about shell pipelines, say — double it:

What does the shell operator || do?|redirects output|pipes output|splits input|forks a process|1|The answer is B: pipes output.

|| produces a single | in the displayed field, the same way %% produces a single % in tci_printf. A lone | is always a field boundary; || is always a literal pipe in the content. The parser collapses || to a placeholder before splitting, then restores it in each field afterwards.


Lifelines and game state

The game loop maintains four pieces of state across questions: which lifelines are still available, the current level, the last safe level passed, and which options the 50:50 has removed. All of these are locals in game_loop — they belong to a single run and are discarded when the game ends.

Three flags track lifeline availability. Each starts at 1 (available) and is set to 0 when spent:

int  lifeline_5050     = 1;
int  lifeline_phone    = 1;
int  lifeline_audience = 1;

level is the current question index, 0-indexed, so PRIZES[level] always gives the prize at stake.

safe_level records the last safe level the player cleared, also as an index into PRIZES. It starts at -1, which means no safe level has been reached yet — a wrong answer at this point would leave the player with nothing:

int  level      = 0;
int  safe_level = -1;

safe_level is updated at the start of each level, before the question is shown:

if (SAFE[level])
    safe_level = level;

On a wrong answer, the banked prize follows directly. This expression is the ternary operator — C's shorthand for a two-branch conditional that produces a value:

safe_level >= 0 ? PRIZES[safe_level] : "£0"

Read it as: if safe_level >= 0, the result is PRIZES[safe_level]; otherwise the result is "£0". The general form is:

condition ? value_if_true : value_if_false

The equivalent written as an if/else would be:

const char *prize;
 
if (safe_level >= 0)
    prize = PRIZES[safe_level];
else
    prize = "£0";

The ternary is not faster — the compiler generates the same code. The reason to reach for it is that it is an expression, not a statement: it can sit inside a function call argument, an assignment, or a return, without a temporary variable.

Use it when both branches are short and the intent reads clearly inline. When the branches are complex, the if/else form is easier to follow.

The 50:50 lifeline needs its own piece of state — which options have been removed for the current question. A four-element array covers this, reset to all-zeros at the start of each new level:

int  hidden[4] = {0, 0, 0, 0};

hidden[i] = 1 means option i is not displayed. The display function checks each entry before printing. The correct answer is never hidden; exactly two of the three wrong answers are.


The spec is the contract

Every function in the next four pages is a clause in this contract. load_questions fills question_t structs from a file that matches the format above. display_ladder uses PRIZES and SAFE. The game loop uses safe_level, level, and the lifeline flags. None of that is decided in the implementation — it is decided here, before the first function body is open.

Before asking whether to write a spec, ask what you are actually trying to do. A quick prototype to test an idea is a different goal from a project you want to ship. Prototypes are disposable — the point is to learn something fast, not to produce correct code. Projects are not.

If you are building something you intend to finish, writing code before you understand the problem does not speed you up. It defers the understanding until after you have made decisions that depend on it.

The temptation is to skip this page and start typing. Vibe coding works: something compiles within the hour, and that feels like progress. The problem surfaces later, when the something that compiles is not the something you needed.

A function written before you knew what calls it gets rewritten when you find out. A struct designed on instinct gets refactored when the display logic needs a field that isn't there. Each fix is small in isolation. Together they cost more time than the spec would have.

The question is not whether vibe coding produces results. It does. The question is whether the total time to a correct, finished program is shorter. The early hours feel productive because code is accumulating. The later hours are expensive because the wrong code has to be understood, untangled, and replaced.

Write the spec first. Then write the code. When a function disagrees with the spec, the spec wins.