← thecodex.expert · The Codex Family of Knowledge
Tier 0 · Absolute Beginner · C Project

Number Guessing Game

Your very first C program with a memory and a decision: the computer picks a secret number, and you guess until you get it. You will learn how a program asks a question, checks an answer, and repeats — and how to split that logic so it can actually be tested.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a tiny game: the computer thinks of a number between 1 and 100, and the player keeps guessing. After each guess the computer says “too high” or “too low” until the player gets it right. Simple — but it teaches the three things every program does: ask, decide, and repeat.

Where this shows up in real life: every time an app checks your password, validates a form, or keeps asking until you give a valid answer, it is doing exactly this — take input, compare it to something, and loop until the condition is met. Learn it here in 15 lines, and you have learned the heartbeat of almost every program.

2 How to Think About It

Before writing any code, picture the game as a loop with a decision inside it. You do not need to know C yet — you just need to know the shape of what happens:

The plan — in plain English
1. Pick a secret number once, at the start. → 2. Ask the player to guess. → 3. Compare the guess to the secret: lower, higher, or equal? → 4. If not equal, go back to step 2. If equal, celebrate and stop. That is the whole program. Everything below is just saying this in C.

Too low

Too high

Correct!

Computer picks a secret number 1-100

Ask the player to guess

Is the guess right?

Say 'too low'

Say 'too high'

Show how many guesses it took

Game over

3 The Build — explained part by part

Here is the complete game, split into a header (NumberGuessingGame.h), the testable logic (NumberGuessingGame.c), and a tiny main.c that wires it to real stdin/stdout. Read each part’s plain-English note below it.

CNumberGuessingGame.h / NumberGuessingGame.c / main.c
#ifndef NUMBER_GUESSING_GAME_H
#define NUMBER_GUESSING_GAME_H
#include <stdio.h>

/* Plays one round: reads guesses (one per line) from `in`, writes prompts and
 * feedback to `out`. Returns the number of guesses it took to win, or -1 if
 * `in` ran out of input before a correct guess. */
int play_game(int secret, FILE *in, FILE *out);

#endif

#include "NumberGuessingGame.h"
#include <stdlib.h>
#include <string.h>

int play_game(int secret, FILE *in, FILE *out) {
    char line[64];
    int guesses = 0;
    fprintf(out, "I'm thinking of a number between 1 and 100.\n");
    while (fgets(line, sizeof(line), in) != NULL) {
        int guess = atoi(line);
        guesses++;
        if (guess == secret) {
            fprintf(out, "Correct! You guessed it in %d tries.\n", guesses);
            return guesses;
        } else if (guess < secret) {
            fprintf(out, "Too low.\n");
        } else {
            fprintf(out, "Too high.\n");
        }
    }
    return -1; /* input ran out without a correct guess */
}

#include "NumberGuessingGame.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    srand((unsigned)time(NULL));
    int secret = 1 + rand() % 100;
    int result = play_game(secret, stdin, stdout);
    return result >= 0 ? 0 : 1;
}
⚠ No in-browser playground here
C compiles to a real, native binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once GCC or Clang is installed.
What each part does — in plain words
int play_game(int secret, FILE *in, FILE *out) — the whole game logic takes its input and output as FILE * parameters instead of reaching for stdin/stdout directly. That is exactly what lets the tests below feed it fake input and capture its output through fmemopen, a real glibc function that turns an in-memory buffer into a genuine FILE * — no temporary files, no real terminal involved.

The header/source split — NumberGuessingGame.h declares the function; NumberGuessingGame.c defines it. This is C’s answer to Java’s single-file classes: the header is the contract, and any other .c file (the real main.c, or the test file) can #include it and link against the compiled logic without ever seeing its source.

while (fgets(line, sizeof(line), in) != NULL) — C has no dedicated infinite-loop-with-early-exit idiom the way some languages do, so looping until a read fails (end of input) is the standard C pattern for “keep going until there is nothing left to read.”

atoi(line) — the simplest way to parse a guess, chosen deliberately for a beginner project: unlike strtol, it gives no way to detect a malformed input (it just returns 0), which is disclosed plainly as a real limitation rather than hidden.
Common mistakes — and how to avoid them
✗ Calling rand() % 100 inside the loop instead of once before it — the secret would change every guess and the game could never be won.
✓ Pick the secret once, before play_game starts looping (see main.c).
✗ Writing if (guess = secret) by mistake instead of == — a classic C footgun, since assignment is itself a valid expression that silently evaluates to the assigned value.
✓ Compile with -Wall (as this project does): GCC specifically warns “suggest parentheses around assignment used as truth value” for exactly this mistake.
✗ Forgetting to seed rand() with srand(time(NULL)) — without it, the “random” secret is identical on every single run of the program.
✓ Call srand() exactly once, at the very start of main, before the first rand() call.

4 Test & Prove Each Part

How do we know the game works without playing it by hand? We write small tests — each one checks one rule and proves it. C ships no built-in test runner at all (unlike Python’s unittest, Go’s testing, or Java’s JUnit), and this sandbox cannot reach a package registry to pull one in — so this project uses a small, genuinely common real-world practice for solo C utilities: assert()-based checks in a tiny hand-written harness, covered in the course’s Testing lesson as one real option alongside Unity and Check.

Guessing the secret on the first try counts as 1 guess
The game correctly narrows down with a mix of too-low and too-high guesses
Running out of input without a correct guess is reported, not silently ignored
The feedback text says exactly "Too low.", "Too high.", and "Correct!" at the right moments
Ctest_NumberGuessingGame.c
#include "NumberGuessingGame.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>

#define RUN(name) do { name(); printf("PASS: %s\n", #name); } while (0)

static void correct_guess_on_first_try_returns_one(void) {
    char buf[16];
    FILE *out = fmemopen(buf, sizeof(buf), "w");
    FILE *in = fmemopen("42\n", 3, "r");
    int result = play_game(42, in, out);
    fclose(in); fclose(out);
    assert(result == 1);
}

static void counts_every_guess_before_the_correct_one(void) {
    char outbuf[256];
    FILE *out = fmemopen(outbuf, sizeof(outbuf), "w");
    FILE *in = fmemopen("10\n50\n42\n", 9, "r");
    int result = play_game(42, in, out);
    fclose(in); fclose(out);
    assert(result == 3);
}

static void running_out_of_guesses_returns_negative_one(void) {
    char outbuf[256];
    FILE *out = fmemopen(outbuf, sizeof(outbuf), "w");
    FILE *in = fmemopen("10\n20\n", 6, "r");
    int result = play_game(42, in, out);
    fclose(in); fclose(out);
    assert(result == -1);
}

static void feedback_says_too_low_and_too_high_correctly(void) {
    char outbuf[256] = {0};
    FILE *out = fmemopen(outbuf, sizeof(outbuf), "w");
    FILE *in = fmemopen("10\n90\n50\n", 9, "r");
    play_game(50, in, out);
    fclose(in); fclose(out);
    assert(strstr(outbuf, "Too low.") != NULL);
    assert(strstr(outbuf, "Too high.") != NULL);
    assert(strstr(outbuf, "Correct!") != NULL);
}

int main(void) {
    RUN(correct_guess_on_first_try_returns_one);
    RUN(counts_every_guess_before_the_correct_one);
    RUN(running_out_of_guesses_returns_negative_one);
    RUN(feedback_says_too_low_and_too_high_correctly);
    printf("All tests passed.\n");
    return 0;
}

Compile and run with gcc -D_GNU_SOURCE -o test_run NumberGuessingGame.c test_NumberGuessingGame.c && ./test_run. The -D_GNU_SOURCE flag is what exposes fmemopen on glibc — without it the test file will not compile. Note that main.c is deliberately left out of this compile line: it has its own main, which would collide with the test harness’s main in test_NumberGuessingGame.c.

5 The Interface

Even a tiny program has an interface — the way a person interacts with it. Here is its contract, documented plainly, the same way a professional would describe any tool.

INPUTYour guessa whole number 1–100, typed by the player
What it expects
A number like 42, typed and then Enter pressed.
OUTPUTFeedbackone of three replies
What it returns
"Too low."
"Too high."
"Correct! You guessed it in N tries."

6 Run It & Automate It

Save the code as NumberGuessingGame.h / NumberGuessingGame.c / main.c and compile it with gcc — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.

Run it locally
gcc -o guess main.c NumberGuessingGame.c && ./guess
Then type a number, press Enter, and follow the “too high / too low” hints until you win.

A CI tool like Jenkins runs the same compile-then-test-then-check-for-leaks steps automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
I'm thinking of a number between 1 and 100.
50
Too high.
25
Too low.
37
Too high.
31
Correct! You guessed it in 4 tries.
If it breaks — how to fix it
🚨 The "random" number is the same every time I run it.
You forgot to seed the generator. Call srand((unsigned)time(NULL)); once, before the first rand() call, as main.c does.
🚨 undefined reference to `play_game'
You compiled main.c alone without NumberGuessingGame.c. The header only declares the function; the linker needs the .c file that actually defines it, in the same compile command.
🚨 multiple definition of `main'
You compiled main.c together with test_NumberGuessingGame.c — both define main. Compile the real program from main.c + NumberGuessingGame.c, and the tests from NumberGuessingGame.c + test_NumberGuessingGame.c, never all three together.
GroovyJenkinsfile
// Jenkinsfile — compiles, tests, and checks for leaks on every change.
pipeline {
    agent any

    stages {
        stage('Get the code') {
            // download the latest code
            steps { checkout scm }
        }
        stage('Compile') {
            steps {
                // confirm a compiler is installed
                sh 'gcc --version'
                // compile with strict warnings on
                sh 'gcc -std=c17 -Wall -Wextra -o app *.c'
            }
        }
        stage('Run the tests') {
            steps {
                // prints PASS/FAIL, exits non-zero on failure
                sh './app'
            }
        }
        stage('Check for memory leaks') {
            steps {
                // fails the build on any leak or invalid access
                sh 'valgrind --error-exitcode=1 --leak-check=full ./app'
            }
        }
    }

    post {
        success { echo 'All tests passed, no leaks found.' }
        failure { echo 'A test or Valgrind check failed — see above.' }
    }
}
Try extending it
Add a maximum-guesses limit and report a loss message. Or track and print the best (fewest-guesses) game across multiple rounds using a static variable in main.c.
What you learned
Splitting logic into a header and a source file so it can be linked into both a real program and a test binary; using FILE * parameters instead of hardcoding stdin/stdout to make code testable; fmemopen for in-memory I/O in tests; and why seeding rand() matters.