Foundations · Module 1 · Lesson 7 of 7
Project: Interactive Number Guessing Game
Build a small terminal game that combines input, validation, loops, conditionals, and functions without hiding the learning in a library.
By the end you can
- Break a small program into functions with clear responsibilities
- Validate numeric input before using it in comparisons
- Use a loop to keep a program running until a condition is met
- Trace the complete program before reading the finished implementation
01 · A question first
How can a program keep asking questions, reject bad input, count attempts, and stop at exactly the right moment? You will build each part of a number guessing game before seeing the complete program.
Make a guess, then step through the trace below to check it.
02 · Watch it run
target = 37;guess = 42;attempts = attempts + 1;if (guess > target) result = TOO_HIGH;- game 0target: 37(looked at in this step)
- game 1guess: unset
- game 2attempts: 0(looked at in this step)
- game 3result: waiting
The game chooses a target and starts with no attempts recorded.
03 · The idea
Start with the problem
The finished game is small, but it contains the same decisions found in larger command-line tools. It must keep state, accept input, reject invalid input, compare values, count work, and stop at a clear condition. The goal is not to paste a complete program. The goal is to make each decision visible before combining it.
The game will use a fixed target of 37. A player enters guesses until the guess is correct. The program prints whether a guess is too low or too high and reports the number of attempts at the end. Using a fixed target keeps the first version deterministic; randomness can be added later without changing the control flow.
Build it in milestones
Milestone 1: Write the decision table
Before writing code, describe the result of comparing guess with target:
| Condition | Result |
|---|---|
guess < target | Too low |
guess > target | Too high |
| Otherwise | Correct |
Turn the table into a function contract. A function named compare_guess should receive two integers and return a small result code. The caller should not need to know how the comparison is written.
Your first task is to write only the function and call it with 37, 42, and 12. Print the returned result for each call. Do not write the input loop yet.
Milestone 2: Add one valid guess
Create target, guess, and attempts in main. Set guess to a literal first. Increment attempts exactly once before calling compare_guess. Print the result.
Ask yourself where the increment belongs. If it is before the comparison, every accepted guess is counted even when it is correct. If it is after a continue, some guesses may disappear from the count. Small placement decisions like this are why tracing the state is useful.
Milestone 3: Repeat until correct
Replace the single guess with a while loop. The loop should:
- Ask for a guess.
- Validate that the input was a number.
- Count the accepted guess.
- Compare the guess with the target.
- Print a hint or stop.
The loop condition can be result != CORRECT, or the loop can return immediately when the answer is correct. Either is valid, but choose one stopping rule and make it obvious.
Milestone 4: Handle bad input
Input functions can fail. If scanf cannot convert the next token to an integer, it leaves the invalid characters in the input stream. A loop that does not remove those characters will fail forever.
For this beginner project, use fgets to read a complete line into a fixed-size character array and use sscanf to attempt the conversion. This gives the program a whole line to discard when conversion fails. It also avoids introducing dynamic allocation.
Test at least these inputs:
42, then30, then37hello- An empty line
- A number followed by extra text
Decide whether extra text should be rejected. The complete version rejects a line unless it contains exactly one integer and optional whitespace around it.
Trace before reading the answer
With target 37 and guesses 42, 30, and 37, the important state transitions are:
start: target=37, attempts=0
42: attempts=1, result=TOO_HIGH
30: attempts=2, result=TOO_LOW
37: attempts=3, result=CORRECT
end: print "Solved in 3 attempts"If your planned code cannot produce this table, stop and fix the state model before adding formatting. A program is easier to debug when its variables have one clear job each.
Show the complete code
Show complete C source
#include <stdio.h>
enum GuessResult {
TOO_LOW,
TOO_HIGH,
CORRECT
};
enum GuessResult compare_guess(int guess, int target) {
if (guess < target) {
return TOO_LOW;
}
if (guess > target) {
return TOO_HIGH;
}
return CORRECT;
}
int read_guess(int *guess) {
char line[64];
char extra;
printf("Enter a guess: ");
if (fgets(line, sizeof(line), stdin) == NULL) {
return 0;
}
if (sscanf(line, " %d %c", guess, &extra) != 1) {
return -1;
}
return 1;
}
int main(void) {
const int target = 37;
int attempts = 0;
enum GuessResult result = TOO_LOW;
puts("Guess the number between 1 and 100.");
while (result != CORRECT) {
int guess;
int input_status = read_guess(&guess);
if (input_status == 0) {
puts("\nInput ended before the game was solved.");
return 1;
}
if (input_status < 0) {
puts("Please enter one whole number.");
continue;
}
attempts++;
result = compare_guess(guess, target);
if (result == TOO_LOW) {
puts("Too low.");
} else if (result == TOO_HIGH) {
puts("Too high.");
}
}
printf("Correct. You solved it in %d attempt%s.\n",
attempts, attempts == 1 ? "" : "s");
return 0;
}What to change next
After the base version works, change one decision at a time:
- Pass the target as a command-line argument.
- Add a maximum of seven attempts.
- Add difficulty levels with different ranges.
- Replace the fixed target with a random value after learning the random-number API.
Do not add all four at once. Each change should have a new input table and a new stopping rule. That habit scales from beginner exercises to production systems.
Operational checklist
An interactive program is still a parser at a trust boundary. Read a complete line into a bounded buffer, distinguish end of file from a blank line, and parse the numeric token with an API that reports range errors and trailing characters. Do not rely on scanf return values alone when the remaining input can trap the next iteration in an unchanged invalid token. Reject values outside the game domain before performing arithmetic, and use a type wide enough for the chosen bounds.
Keep the game state separate from input and output so the rules can be tested without a terminal. A deterministic random source or injected seed makes the target reproducible in tests. Decide whether the secret is inclusive at either bound, whether repeated guesses consume an attempt, and how duplicate guesses are reported. The loop should have a clear termination condition for a correct guess, an attempt limit, EOF, and an explicit quit command. Every path should leave the stream in a known state and return a meaningful status to the operating system.
Test the smallest and largest valid guesses, values just outside each boundary, negative numbers, blank input, words mixed with digits, very long lines, repeated guesses, EOF before completion, and interrupted input. Run the same binary with redirected input as well as at a terminal. These cases turn a small game into a practical exercise in input validation, state transitions, portability, and resource-safe C program structure.
04 · Prove it
Question 1 of 3 · answer from memory
Which function should decide whether a guess is too low, too high, or correct?
Something wrong with this lesson? Report a problem