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.
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:
3 The Build — explained part by part
Here is the complete game, split into a header (NumberGuessingGame.hpp), the testable logic (NumberGuessingGame.cpp), and a tiny main.cpp that wires it to real std::cin/std::cout. Read each part’s plain-English note below it.
#pragma once
#include <istream>
#include <ostream>
// Plays one full game of number-guessing against `secret`, reading guesses
// from `in` and writing feedback to `out`. Returns the number of guesses it
// took to win, or -1 if the input ran out before a correct guess. Taking
// std::istream&/std::ostream& instead of touching std::cin/std::cout
// directly is what lets the tests below feed it a std::istringstream and
// capture output in a std::ostringstream -- no real terminal involved.
int play_game(int secret, std::istream &in, std::ostream &out);
#include "NumberGuessingGame.hpp"
#include <string>
int play_game(int secret, std::istream &in, std::ostream &out) {
std::string line;
int guesses = 0;
while (std::getline(in, line)) {
int guess;
try {
guess = std::stoi(line);
} catch (const std::exception &) {
continue; // not a whole number; ignore the line rather than crash
}
guesses++;
if (guess < secret) {
out << "Too low.\n";
} else if (guess > secret) {
out << "Too high.\n";
} else {
out << "Correct! You guessed it in " << guesses << " tries.\n";
return guesses;
}
}
return -1; // ran out of input without a correct guess
}
#include "NumberGuessingGame.hpp"
#include <iostream>
#include <random>
int main() {
std::cout << "I'm thinking of a number between 1 and 100.\n";
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(1, 100);
int secret = dist(gen);
play_game(secret, std::cin, std::cout);
return 0;
}
std::cin/std::cout directly. That is exactly what lets the tests below feed it a std::istringstream and capture its output in a std::ostringstream — no temporary files, no real terminal involved, and no separate library needed the way C’s fmemopen workaround was.The header/source split —
NumberGuessingGame.hpp declares the function; NumberGuessingGame.cpp defines it. Even though C++ classes can bundle declaration and definition together, a plain free function still benefits from the same C-style split: any other .cpp file (the real main.cpp, or the test file) can #include the header and link against the compiled logic without ever seeing its source.try { guess = std::stoi(line); } catch (const std::exception &) { continue; } —
std::stoi throws std::invalid_argument on text that is not a number, instead of C’s atoi, which silently returns 0 for garbage input. Catching the exception and skipping the line is how this project chooses to be more honest than the C version about a malformed guess.std::random_device rd; std::mt19937 gen(rd()); — modern C++’s standard way to get a real random number:
random_device is a genuine, non-deterministic entropy source on this platform (confirmed by checking its .entropy() is non-zero), used once to seed a fast mt19937 generator for repeated draws.
std::mt19937 (or worse, calling std::random_device) inside the loop for every guess — reseeding constantly is wasteful and can even reduce randomness quality on some platforms.main does here.atoi out of old C habit to parse the guess — it gives no way to detect a malformed input, silently returning 0.std::stoi inside a try/catch (as this project does), which throws a catchable exception instead of guessing.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. Unlike C, this sandbox cannot reach a package registry to fetch Catch2 or GoogleTest either, so this project uses the same small, genuinely common practice C’s projects on this site use: plain assert() checks in a tiny hand-written harness — a perfectly defensible choice for a small header-only utility, and covered as one real option in the course’s Testing lesson alongside Catch2 and GoogleTest.
#include "NumberGuessingGame.hpp"
#include <cassert>
#include <iostream>
#include <sstream>
#include <string>
#define RUN(name) do { name(); std::cout << "PASS: " << #name << "\n"; } while (0)
static void guessing_the_secret_on_the_first_try_counts_as_one_guess() {
std::istringstream in("42\n");
std::ostringstream out;
int guesses = play_game(42, in, out);
assert(guesses == 1);
}
static void the_game_narrows_down_with_a_mix_of_low_and_high_guesses() {
std::istringstream in("10\n90\n50\n");
std::ostringstream out;
int guesses = play_game(50, in, out);
assert(guesses == 3);
}
static void running_out_of_input_without_a_correct_guess_is_reported() {
std::istringstream in("10\n20\n");
std::ostringstream out;
int guesses = play_game(99, in, out);
assert(guesses == -1);
}
static void feedback_text_is_exact() {
std::istringstream in("10\n90\n50\n");
std::ostringstream out;
play_game(50, in, out);
std::string text = out.str();
assert(text.find("Too low.") != std::string::npos);
assert(text.find("Too high.") != std::string::npos);
assert(text.find("Correct! You guessed it in 3 tries.") != std::string::npos);
}
int main() {
RUN(guessing_the_secret_on_the_first_try_counts_as_one_guess);
RUN(the_game_narrows_down_with_a_mix_of_low_and_high_guesses);
RUN(running_out_of_input_without_a_correct_guess_is_reported);
RUN(feedback_text_is_exact);
std::cout << "All tests passed.\n";
return 0;
}
Compile and run with g++ -std=c++20 -o test_run NumberGuessingGame.cpp test_NumberGuessingGame.cpp && ./test_run. Note that main.cpp 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.cpp.
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.
What it expects
A number like 42, typed and then Enter pressed.What it returns
"Too low."
"Too high."
"Correct! You guessed it in N tries."6 Run It & Automate It
Save the code as NumberGuessingGame.hpp / NumberGuessingGame.cpp / main.cpp and compile it with g++ — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.
g++ -std=c++20 -o guess main.cpp NumberGuessingGame.cpp && ./guessThen 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.
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.#include <string> — std::stoi lives there, not in <iostream>.out << statement; a silently empty branch means a guess vanishes with no feedback.// 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 'g++ --version'
// compile with strict warnings on
sh 'g++ -std=c++20 -Wall -Wextra -o app *.cpp'
}
}
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.' }
}
}
- Limit the number of guesses. Print “Out of guesses!” after, say, 7 tries. (Teaches: a counter compared against a constant.)
- Add a difficulty setting. Let the player choose the range (1–10, 1–1000). (Teaches: passing extra parameters through
play_game.) - Use Catch2 or GoogleTest. If you have network access, replace the hand-written harness and compare the experience. (Teaches: what a real test framework buys you — fixtures, matchers, readable failure output.)
std::stoi with exception handling beats C’s silent-failure atoi, and how to seed <random> properly for real randomness. Related: Standard Library, Exceptions.