The Number-Guessing Engine
The number-guessing game is this chapter's own capstone — so let's build it the
way a real codebase would: split the engine (the pure decision logic) from
the randomness and I/O (the dice roll and the keyboard). You'll write the
engine as six small pure functions — judge a guess, label the hint, sum some
digits, build a countdown, validate input, and play a whole bounded round — each
one driven by a different Chapter-8 control-flow tool. A ready-made main.cpp
then wraps your engine with a Mersenne-Twister RNG and std::cin/std::cout so
you can actually play (make run).
Why the split? Because <random> and std::cin make code non-deterministic
— you can't unit-test "did the engine roll a 7?". So we keep the randomness and
the keyboard in main (ungraded) and keep the logic in a deterministic engine
the grader can pin to exact answers. That's the whole lesson, and it's exactly
how you'd make a fuzzer's decision logic testable in CS6340: the mutation/branch
choices are unit-tested; the random target launch is not.
Your tasks
- Judge a guess (
if/elsechain).judgeGuess(secret, guess)returns a three-way direction code —-1too low,+1too high,0correct — using oneif/else if/elsechain. The sign is the signal the player reads to know which way to move. No loops. - Label the hint (
switch).hintText(code)maps-1 → "too low",0 → "correct",+1 → "too high", and anything else→ "invalid"via thedefaultcase. Use aswitch(not if/else here — practicingswitchis the task), with abreakafter each case so you don't fall through. - Digit sum (loop +
%//).sumOfDigits(n)adds the decimal digits with awhile/forloop:n % 10grabs the last digit,n /= 10drops it. Use the magnitude, sosumOfDigits(-123) == sumOfDigits(123).sumOfDigits(0) == 0. - Countdown (
whilethat builds a string).countdownString(start)returns"3..2..1..liftoff"for3, counting down with awhileloop. The".."separator goes only between counts (none before the first, none after the last number); astart <= 0runs the body zero times and yields just"liftoff". Return the string — don't print it. - Validate input (
do while).promptsUntilValid(typed, low, high)is a deterministic stand-in for a re-prompt loop: the "typed" values are handed in as a string of digits. Walk them with ado { … } while (…)loop and return the 1-based count of values consumed until one lands in[low, high](or the total, if none do). Ado whilefits because you must read at least one value before you can test it. - Play a round (
for+break+continue).playRound(secret, guesses, maxAttempts)plays one bounded round; each char ofguessesis one numeric guess. Loop withfor. A guess of0is a "misclick" sentinel —continuewithout counting it. Otherwise it's a counted attempt; if it's correct (judge it!),breakand return the 1-based counted attempt that won; once themaxAttemptsbudget is spent, stop. Return0for a loss. This pulls the whole chapter together.
Success criteria
judgeGuess(-5, -9)/(-5, -1)— the chain must order negatives correctlyhintText(2)andhintText(-2)— thedefaultcase must catch out-of-range codessumOfDigits(-123) == 6— must use the magnitude, not a negative remaindersumOfDigits(2147483647) == 46— the digit loop onINT_MAX(no overflow)countdownString(0) == "liftoff"— thewhilebody must run zero timescountdownString(3) == "3..2..1..liftoff"— separators only between countspromptsUntilValid("7", 1, 6) == 1—do whilestill consumes the one valuepromptsUntilValid("999", 1, 6) == 3— none valid → return the total consumedplayRound(5, "405", 3) == 2— the0sentinel is skipped (continue), not countedplayRound(9, "123459", 5) == 0vs(…, 6) == 6— the budget must cut the win off
Concepts practiced
if/else if/elsechains for mutually-exclusive cases (8.2)switchwithcaselabels,default, andbreak(no fallthrough) (8.5, 8.6)whileloops — counting, and building up a result across iterations (8.8)do whileloops — the run-at-least-once shape for "read, then validate" (8.9)forloops for bounded iteration, withbreak(stop early) andcontinue(skip this one) (8.10, 8.11)- The modulo / divide digit-peel idiom
n % 10,n /= 10(8.8, reuses%from 6.2) - Seeing
<random>in action — engine + distribution + seed once (8.13, 8.14) — inmainonly, deliberately outside the graded, deterministic engine - Reused from earlier chapters: functions / headers / header guard (Ch 2),
int&static_cast&if(Ch 4),std::string/std::string_viewand indexing a string (Ch 5)
Constraints
- Allowed:
if/else,switch/case/default/break,while,do while,for,break/continue,%and/,static_cast,std::string(+=,std::to_string),std::string_viewindexing (sv[i],sv.size()), and the function/header machinery already in the files. - Forbidden (not taught yet):
<random>inside the engine (it belongs inmain, and would make your logic non-deterministic),std::cin/std::coutinside the engine, arrays /std::vector/ pointers (Ch 12+),enum(Ch 13),goto(8.7 — you havebreak/continue/return; use them). The engine must stay pure: arguments in, value out, no globals, no side effects. - Idioms required by the notes: give every loop a believable exit story
(8.8); prefer half-open counting (
< size, not<= size-1) (8.10); usebreakto preventswitchfallthrough (8.5); ado whileonly where the body must run once before the test (8.9).
Build & run locally
make # compile-check starter/engine.cpp AND build the playable driver
make run # PLAY the game against your engine (uses <random> + your logic)
make test # grade your engine -> RED until the TASK blocks are filled in
make solution # play the game against the reference engine
make clean # remove build artifactsmake test is the grader (it supplies its own main and calls your engine
across many fixed inputs). make run is the game — fun once your engine works,
but the grade comes from make test.
Hints
Task 1 — a chain, not three separate ifs
if (guess < secret) return -1;
else if (guess > secret) return +1;
else return 0;A chain picks exactly one branch. Three independent ifs would also work here
because the cases happen to be exclusive, but the chain says "these are
alternatives" out loud — and it's the construct the task is practicing.
Task 2 — switch shape, and why break matters
switch (code)
{
case -1: return "too low"; // a return ends the case…
case 0: return "correct";
case 1: return "too high";
default: return "invalid";
}Each case here ends in return, so it can't fall through. If you instead
assign a label and want to read it after the switch, you must break
after each case — otherwise execution falls into the next one (8.6). Either style
is fine; just don't forget the default.
Task 3 — peel digits with % and /
if (n < 0) n = -n; // work on |n|
int sum { 0 };
while (n > 0) { sum += n % 10; n /= 10; }
return sum;n % 10 is the last digit; n /= 10 drops it. When n reaches 0 the condition
is false and the body stops — that's the loop's exit story. (Handle the negative
first so you never take % of a negative and get a negative digit.)
Task 4 — separators go between, not around
std::string out {};
int n { start };
while (n >= 1)
{
if (n != start) out += ".."; // skip the "before-first" separator
out += std::to_string(n);
--n;
}
if (!out.empty()) out += ".."; // one more before the finale
out += "liftoff";
return out;std::to_string(3) gives "3". The two guards (n != start, and !out.empty())
are what keep the ".." strictly between tokens.
Task 5 — why a do-while, and where it stops
int count { 0 };
int value { 0 };
do {
value = typed[static_cast<std::size_t>(count)] - '0';
++count;
} while ((value < low || value > high) && count < static_cast<int>(typed.size()));
return count;The body must run once before there's a value to test — that's the do-while
signature. The second half of the condition, count < size, is the exit story
that stops you reading past the end when nothing is ever valid.
Task 6 — for + continue (skip) + break (stop)
int attempts { 0 };
for (std::size_t i { 0 }; i < guesses.size(); ++i)
{
int guess { guesses[i] - '0' };
if (guess == 0) continue; // sentinel: skip, don't count
++attempts;
if (attempts > maxAttempts) break; // budget spent
if (judgeGuess(secret, guess) == 0) return attempts; // correct!
}
return 0; // losscontinue jumps to the next character without spending an attempt; break (or
the return) leaves the loop. Note the ++attempts happens before the budget
check, so a guess judged on attempt maxAttempts still counts, but the one after
it is cut off.
Stretch goals
- Make
playRoundaccept a real range of guesses 1–99 instead of single digits — which needs a parsed list of numbers (arrays /std::vector, Chapter 16) instead of a string of digit chars. - Replace the
-1/0/+1code injudgeGuesswith a scopedenum classHint { TooLow, Correct, TooHigh }and switch on it (Chapter 13). - In
main, log the seed before each game (std::mt19937’s seed) so a fun or buggy round can be replayed exactly — the reproducibility trick from notes 8.14, and a real fuzzing habit. - Add full input validation to the driver loop with
std::cin.fail()/clear()/ignore()so non-numeric input can't wedge the game (Chapter 9). - Turn
promptsUntilValidinto an actualstd::cindo whileinmainand feed the engine the first valid value — keeping the count logic unit-tested.
// Chapter 8 — Control Flow · Project: The Number-Guessing Engine (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the six TASK blocks below. Each maps 1:1 to a task in the README and
// to a declaration in ../engine.h. The bodies currently return PLACEHOLDERS so
// the file compiles immediately — that's why `make test` is RED right now. Your
// job is to turn it GREEN by wiring up the real control flow.
//
// make build compile your code (should already work)
// make test grade it (RED until you fill these in)
// make solution run the grader against the reference if you get stuck
//
// Everything here is PURE: arguments in, value out. No <random>, no std::cin/cout
// — that machinery lives in main.cpp and is NOT graded. Keep it that way so your
// engine stays deterministic and testable.
#include "../engine.h"
// ─── TASK 1: judge a guess with an if / else if / else chain ─────────────────
// Return -1 if guess < secret (too low), +1 if guess > secret (too high), or 0
// if they're equal (correct). The three cases are mutually exclusive, so use ONE
// if / else if / else chain (not three independent `if`s). No loops here.
//
// >>> YOUR CODE HERE <<<
//
int judgeGuess(int /*secret*/, int /*guess*/)
{
return 0; // placeholder — always says "correct" (wrong for most inputs)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: turn a code into a label with a SWITCH ──────────────────────────
// switch on `code`: -1 -> "too low", 0 -> "correct", +1 -> "too high",
// anything else -> "invalid" (use `default`). Put a `break` after each case so
// you don't fall through into the next one. Please use `switch` here, not if/else
// — practicing the switch statement is the point of this task.
//
// >>> YOUR CODE HERE <<<
//
std::string_view hintText(int /*code*/)
{
return "invalid"; // placeholder
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: digit sum with a loop, % and / ──────────────────────────────────
// Add up the decimal digits of `n` using a `while` (or `for`) loop. Peel the
// last digit with `n % 10`, then drop it with `n /= 10`, until nothing is left.
// Use the MAGNITUDE so negatives match: sumOfDigits(-123) == sumOfDigits(123).
// sumOfDigits(0) must be 0 (the loop body should run zero times).
//
// >>> YOUR CODE HERE <<<
//
int sumOfDigits(int /*n*/)
{
return 0; // placeholder — correct only for n == 0
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: build a countdown STRING with a while loop ──────────────────────
// Count DOWN from `start` to 1, appending ".." only BETWEEN numbers, then append
// "liftoff": countdownString(3) == "3..2..1..liftoff", countdownString(0) ==
// "liftoff". Build into a std::string (std::to_string(int) makes "3" from 3, and
// `out += "..";` appends). Return the string — do NOT print it.
//
// >>> YOUR CODE HERE <<<
//
std::string countdownString(int /*start*/)
{
return "liftoff"; // placeholder — only correct when start <= 0
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: do-while validation loop ────────────────────────────────────────
// Walk the "typed" digit characters left-to-right with a `do { … } while (…)`
// loop. Convert each char to its value with `ch - '0'`. Count how many you
// consume until one lands IN RANGE [low, high]; return that 1-based count. If
// none are valid, return the total number consumed. A do-while fits because you
// must read at least ONE value before you can test it. Stop at the end of the
// string so you never read past it. `typed` is non-empty and all '0'..'9'.
//
// >>> YOUR CODE HERE <<<
//
int promptsUntilValid(std::string_view /*typed*/, int /*low*/, int /*high*/)
{
return 1; // placeholder — pretends the first prompt is always valid
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: play one bounded round (for + if + break + continue) ────────────
// Loop over the guess characters of `guesses` with a `for` loop. For each:
// • Convert the char to its value: `guesses[i] - '0'`.
// • If that value is 0, it's a "misclick" SENTINEL: `continue` WITHOUT counting.
// • Otherwise it's a counted attempt (++attempts). If you've now exceeded
// `maxAttempts`, `break` (the budget is spent — a loss).
// • Judge it with judgeGuess; if the result is 0 (correct), return the 1-based
// count of the COUNTED attempt that won.
// Return 0 if the player never guesses `secret` within the budget.
// Index a string_view with `guesses[i]`; its length is `guesses.size()` (a
// std::size_t — fine to compare an `i` of the same type against it).
//
// >>> YOUR CODE HERE <<<
//
int playRound(int /*secret*/, std::string_view /*guesses*/, int /*maxAttempts*/)
{
return 0; // placeholder — always reports a loss
}
// ─────────────────────────────────────────────────────────────────────────────
// Chapter 8 — Control Flow · Project: The Number-Guessing Engine (DRIVER / main)
// ─────────────────────────────────────────────────────────────────────────────
// This is the PLAYABLE game that wraps your pure engine. It is NOT graded and you
// do NOT need to edit it — it's here so you can actually PLAY once your engine
// works (`make run`), and so you can see the chapter's <random> tools in action.
//
// Why is all of this OUT of engine.cpp? Because <random> and std::cin make code
// non-deterministic — you can't unit-test "did the RNG pick 42?". So we keep the
// randomness and the keyboard HERE, in main, and keep the decision logic in the
// pure engine the grader hammers. That separation is the whole lesson (and it's
// exactly how you'd make a fuzzer's logic testable in CS6340).
//
// make run build the starter engine + this driver, then play
//
// NOTE: until you implement starter/engine.cpp, the game "works" but plays badly
// (every guess is judged "correct"); finish the engine and it becomes a real game.
#include <iostream> // std::cin, std::cout (kept OUT of the engine on purpose)
#include <random> // std::mt19937, std::uniform_int_distribution (8.13 / 8.14)
#include "../engine.h"
int main()
{
// ── Set up the random number generator (Chapter 8.13–8.14) ───────────────
// An ENGINE produces raw pseudo-random bits; a DISTRIBUTION maps them into a
// useful range. Seed the engine ONCE (std::random_device gives a varying
// seed) and then reuse it — re-seeding inside a loop is the classic bug that
// makes every "random" number come out the same.
std::random_device rd;
std::mt19937 rng { rd() }; // seed once, reuse forever
std::uniform_int_distribution<int> secretDist { 1, 9 }; // pick a secret 1..9
std::uniform_int_distribution<int> guessDist { 1, 9 };
const int secret { secretDist(rng) }; // the number to guess (hidden)
const int maxAttempts { 5 };
std::cout << "Guessing engine online. I'm thinking of a number from 1 to 9.\n"
<< "Type a guess (1-9) each round, or 'r' to let me guess randomly,\n"
<< "or 'q' to quit.\n";
int attempts { 0 };
while (attempts < maxAttempts)
{
std::cout << "\nAttempt " << (attempts + 1) << " of " << maxAttempts << " > ";
char command {};
if (!(std::cin >> command)) // EOF / closed input -> leave the loop
break;
int guess { 0 };
if (command == 'q')
{
std::cout << "Bye! The number was " << secret << ".\n";
return 0;
}
else if (command == 'r')
{
guess = guessDist(rng); // let the RNG pick this guess
std::cout << " (engine rolls " << guess << ")\n";
}
else if (command >= '1' && command <= '9')
{
guess = command - '0'; // the typed digit as a number
}
else
{
std::cout << " Please enter 1-9, 'r', or 'q'.\n";
continue; // don't spend an attempt
}
++attempts;
// Everything below routes through the PURE engine — no game logic lives
// in main itself. judgeGuess decides the direction; hintText labels it.
const int verdict { judgeGuess(secret, guess) };
std::cout << " " << guess << " is " << hintText(verdict) << ".\n";
if (verdict == 0)
{
std::cout << "You got it in " << attempts << " attempt(s)! "
<< "(digit sum of the secret is " << sumOfDigits(secret) << ")\n";
return 0;
}
}
// Out of attempts: a tiny countdown flourish, again from the pure engine.
std::cout << "\nOut of attempts. " << countdownString(3) << "!\n"
<< "The number was " << secret << ".\n";
return 0;
}
Try the lab first — the learning is in the attempt.
// Chapter 8 — Control Flow · Project: The Number-Guessing Engine (REFERENCE SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation of ../engine.h. Peek only
// after you've taken a real swing at starter/engine.cpp — the learning is in
// wiring the control flow yourself, then comparing.
//
// Everything here is PURE: arguments in, value out. No <random>, no std::cin/cout.
#include "../engine.h"
// ─── TASK 1: if / else if / else chain ───────────────────────────────────────
// The three cases are mutually exclusive, so a CHAIN (not three separate ifs) is
// the right tool: the first branch that matches wins and the rest are skipped.
// The returned sign is the "which way to move" signal the player reads.
int judgeGuess(int secret, int guess)
{
if (guess < secret)
return -1; // guess is below the secret -> too low
else if (guess > secret)
return +1; // guess is above the secret -> too high
else
return 0; // guess == secret -> correct
}
// ─── TASK 2: switch statement ────────────────────────────────────────────────
// One controlling expression (`code`) compared against discrete values — the
// textbook case for `switch`. We assign the label in each case and `break` so
// execution doesn't fall through into the next case; `default` catches anything
// that isn't a valid three-way code. (Assigning + breaking, rather than an early
// return, lets you SEE the `break` doing its job.)
std::string_view hintText(int code)
{
std::string_view label {};
switch (code)
{
case -1:
label = "too low";
break; // stop here — do NOT fall into case 0
case 0:
label = "correct";
break;
case 1:
label = "too high";
break;
default:
label = "invalid"; // not a value judgeGuess can produce
break;
}
return label;
}
// ─── TASK 3: a counting loop with % and / ────────────────────────────────────
// Peel digits off the right: `n % 10` is the last digit, `n / 10` drops it.
// We work on the MAGNITUDE so negatives match their positive twin; a single
// negation up front avoids a negative remainder from confusing the sum.
int sumOfDigits(int n)
{
if (n < 0)
n = -n; // digit sum is defined on |n|
int sum { 0 };
while (n > 0) // when n is 0 the body never runs -> sum stays 0
{
sum += n % 10; // add the current last digit
n /= 10; // shift right by one decimal place
}
return sum;
}
// ─── TASK 4: while loop that BUILDS a string ─────────────────────────────────
// Count down from `start` to 1, appending the ".." separator only BETWEEN
// numbers. We special-case the very first append (no leading ".."); every later
// number gets a ".." in front of it. After the loop, tack on "liftoff".
std::string countdownString(int start)
{
std::string out {};
int n { start };
while (n >= 1) // start <= 0 -> body never runs -> just "liftoff"
{
if (n != start)
out += ".."; // separator goes between counts, not before the first
out += std::to_string(n);
--n; // count DOWN; this is the loop's exit story
}
if (!out.empty())
out += ".."; // separator between the last number and the finale
out += "liftoff";
return out;
}
// ─── TASK 5: do-while validation loop ────────────────────────────────────────
// A do-while runs the body at least once THEN tests — exactly how "prompt, then
// validate" works. We always consume one "typed" digit before we can judge it.
// `count` is the 1-based number of the digit we're currently looking at.
int promptsUntilValid(std::string_view typed, int low, int high)
{
int count { 0 };
int value { 0 };
do
{
// Read the next "typed" value: the digit char at position `count`, turned
// into its int value. (count is 0-based as an index, but we bump it to a
// 1-based prompt counter on the same line.)
char ch { typed[static_cast<std::size_t>(count)] };
value = ch - '0'; // '0'..'9' -> 0..9
++count; // we have now consumed one more prompt
}
while ((value < low || value > high) // keep going while out of range…
&& count < static_cast<int>(typed.size())); // …but never run past the end
return count;
}
// ─── TASK 6: for loop + if + break + continue (the capstone) ─────────────────
// Walk the guess characters. The sentinel 0 is a "misclick": skip it WITHOUT
// counting (continue). Every other digit is a counted attempt — judge it; a 0
// from judgeGuess means a correct guess, so break and report which counted
// attempt won. Stop once the attempt budget is spent (a loss -> return 0).
int playRound(int secret, std::string_view guesses, int maxAttempts)
{
int attempts { 0 };
for (std::size_t i { 0 }; i < guesses.size(); ++i)
{
int guess { guesses[i] - '0' }; // digit char -> numeric guess
if (guess == 0)
continue; // sentinel: skip, do NOT count it
++attempts; // a real, counted attempt
if (attempts > maxAttempts)
break; // budget exhausted before this guess
if (judgeGuess(secret, guess) == 0)
return attempts; // correct -> the winning attempt number
}
return 0; // never guessed it within the budget
}
Try the lab first — the learning is in the attempt.
// Chapter 8 — Control Flow · Project: The Number-Guessing Engine (DRIVER / main · reference)
// ─────────────────────────────────────────────────────────────────────────────
// This is the PLAYABLE game that wraps your pure engine. It is NOT graded and you
// do NOT need to edit it — it's here so you can actually PLAY once your engine
// works (`make run`), and so you can see the chapter's <random> tools in action.
//
// Why is all of this OUT of engine.cpp? Because <random> and std::cin make code
// non-deterministic — you can't unit-test "did the RNG pick 42?". So we keep the
// randomness and the keyboard HERE, in main, and keep the decision logic in the
// pure engine the grader hammers. That separation is the whole lesson (and it's
// exactly how you'd make a fuzzer's logic testable in CS6340).
//
// make run build the starter engine + this driver, then play
//
// NOTE: until you implement starter/engine.cpp, the game "works" but plays badly
// (every guess is judged "correct"); finish the engine and it becomes a real game.
#include <iostream> // std::cin, std::cout (kept OUT of the engine on purpose)
#include <random> // std::mt19937, std::uniform_int_distribution (8.13 / 8.14)
#include "../engine.h"
int main()
{
// ── Set up the random number generator (Chapter 8.13–8.14) ───────────────
// An ENGINE produces raw pseudo-random bits; a DISTRIBUTION maps them into a
// useful range. Seed the engine ONCE (std::random_device gives a varying
// seed) and then reuse it — re-seeding inside a loop is the classic bug that
// makes every "random" number come out the same.
std::random_device rd;
std::mt19937 rng { rd() }; // seed once, reuse forever
std::uniform_int_distribution<int> secretDist { 1, 9 }; // pick a secret 1..9
std::uniform_int_distribution<int> guessDist { 1, 9 };
const int secret { secretDist(rng) }; // the number to guess (hidden)
const int maxAttempts { 5 };
std::cout << "Guessing engine online. I'm thinking of a number from 1 to 9.\n"
<< "Type a guess (1-9) each round, or 'r' to let me guess randomly,\n"
<< "or 'q' to quit.\n";
int attempts { 0 };
while (attempts < maxAttempts)
{
std::cout << "\nAttempt " << (attempts + 1) << " of " << maxAttempts << " > ";
char command {};
if (!(std::cin >> command)) // EOF / closed input -> leave the loop
break;
int guess { 0 };
if (command == 'q')
{
std::cout << "Bye! The number was " << secret << ".\n";
return 0;
}
else if (command == 'r')
{
guess = guessDist(rng); // let the RNG pick this guess
std::cout << " (engine rolls " << guess << ")\n";
}
else if (command >= '1' && command <= '9')
{
guess = command - '0'; // the typed digit as a number
}
else
{
std::cout << " Please enter 1-9, 'r', or 'q'.\n";
continue; // don't spend an attempt
}
++attempts;
// Everything below routes through the PURE engine — no game logic lives
// in main itself. judgeGuess decides the direction; hintText labels it.
const int verdict { judgeGuess(secret, guess) };
std::cout << " " << guess << " is " << hintText(verdict) << ".\n";
if (verdict == 0)
{
std::cout << "You got it in " << attempts << " attempt(s)! "
<< "(digit sum of the secret is " << sumOfDigits(secret) << ")\n";
return 0;
}
}
// Out of attempts: a tiny countdown flourish, again from the pure engine.
std::cout << "\nOut of attempts. " << countdownString(3) << "!\n"
<< "The number was " << secret << ".\n";
return 0;
}
// Chapter 8 — Control Flow · Project: The Number-Guessing Engine (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// A tiny no-framework unit-test harness (same style as the drills/CLAUDE.md
// spec). It includes ../engine.h and calls each engine function across MANY
// inputs — fully deterministic, because the engine has no <random> and no I/O.
// Each CHECK that fails prints its expression and line number. Any failure ->
// non-zero exit -> `make test` is RED.
//
// The Makefile links this file against starter/engine.cpp (your code) for
// `make test`, and against solution/engine.cpp for `make test-solution`.
#include <iostream>
#include <string>
#include "../engine.h"
static int fails = 0;
// CHECK: assert a boolean condition; on failure, report what and where.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Task 1: judgeGuess — the if/else chain (sign = direction) ────────────
CHECK(judgeGuess(50, 30) == -1); // 30 < 50 -> too low
CHECK(judgeGuess(50, 80) == +1); // 80 > 50 -> too high
CHECK(judgeGuess(50, 50) == 0); // equal -> correct
CHECK(judgeGuess(50, 49) == -1); // boundary just below
CHECK(judgeGuess(50, 51) == +1); // boundary just above
CHECK(judgeGuess(0, 0) == 0); // edge: secret 0
CHECK(judgeGuess(-5, -9) == -1); // edge: negatives still order correctly
CHECK(judgeGuess(-5, -1) == +1);
// ── Task 2: hintText — the switch (incl. default) ────────────────────────
CHECK(hintText(-1) == "too low");
CHECK(hintText(0) == "correct");
CHECK(hintText(1) == "too high");
CHECK(hintText(2) == "invalid"); // edge: default catches out-of-range…
CHECK(hintText(-2) == "invalid"); // …on both sides
CHECK(hintText(99) == "invalid");
// The engine round-trips: judging then labelling should read naturally.
CHECK(hintText(judgeGuess(7, 4)) == "too low");
CHECK(hintText(judgeGuess(7, 7)) == "correct");
// ── Task 3: sumOfDigits — counting loop with % and / ─────────────────────
CHECK(sumOfDigits(0) == 0); // edge: loop runs zero times
CHECK(sumOfDigits(5) == 5); // single digit
CHECK(sumOfDigits(123) == 6); // 1+2+3
CHECK(sumOfDigits(99) == 18);
CHECK(sumOfDigits(1000) == 1); // zeros contribute nothing
CHECK(sumOfDigits(-123) == 6); // edge: negative uses |n|, same as 123
CHECK(sumOfDigits(-7) == 7); // edge: single negative digit
CHECK(sumOfDigits(2147483647) == 46); // edge: INT_MAX, no overflow in the sum
// ── Task 4: countdownString — while loop that builds a string ────────────
CHECK(countdownString(3) == "3..2..1..liftoff");
CHECK(countdownString(1) == "1..liftoff");
CHECK(countdownString(2) == "2..1..liftoff");
CHECK(countdownString(0) == "liftoff"); // edge: body runs zero times
CHECK(countdownString(-4) == "liftoff"); // edge: negative start, still just finale
CHECK(countdownString(5) == "5..4..3..2..1..liftoff");
// ── Task 5: promptsUntilValid — do-while validation loop ─────────────────
CHECK(promptsUntilValid("4", 1, 6) == 1); // valid on the first prompt
CHECK(promptsUntilValid("093", 1, 6) == 3); // 0 bad, 9 bad, 3 good -> 3
CHECK(promptsUntilValid("80261", 1, 6) == 3);// 8 bad, 0 bad, 2 good -> 3
CHECK(promptsUntilValid("7", 1, 6) == 1); // edge: only value, invalid -> still 1 consumed
CHECK(promptsUntilValid("999", 1, 6) == 3); // edge: none valid -> total consumed (3)
CHECK(promptsUntilValid("16", 1, 6) == 1); // first already valid -> stop at 1 (do-while body once)
CHECK(promptsUntilValid("5", 5, 5) == 1); // edge: single-value range, hit it
// ── Task 6: playRound — the capstone (for + if + break + continue) ───────
CHECK(playRound(5, "375", 3) == 3); // guesses 3,7,5 -> 3rd attempt wins
CHECK(playRound(5, "5", 3) == 1); // first counted attempt wins
CHECK(playRound(5, "12", 3) == 0); // never guesses 5 within budget -> loss
CHECK(playRound(5, "405", 3) == 2); // edge: 0 is a skipped sentinel; 4 misses, 5 wins on attempt 2
CHECK(playRound(5, "0005", 3) == 1); // edge: three skips, then 5 wins on counted attempt 1
CHECK(playRound(5, "125", 2) == 0); // edge: budget 2 spent before the winning 5 is judged
CHECK(playRound(9, "123459", 6) == 6);// six real guesses; the winning 9 is the 6th counted attempt
CHECK(playRound(9, "123459", 5) == 0);// same guesses, budget 5 -> 9 is out of budget -> loss
CHECK(playRound(3, "13", 5) == 2); // 1 misses, 3 wins on the 2nd counted attempt
if (!fails)
std::cout << "PASS ✅ all engine checks passed.\n";
else
std::cerr << "\nFAIL ❌ " << fails << " check(s) failed — fix the TASK blocks in engine.cpp.\n";
return fails ? 1 : 0;
}
// Chapter 8 — Control Flow · Project: The Number-Guessing Engine
// ─────────────────────────────────────────────────────────────────────────────
// This header is the CONTRACT between you and the grader. It declares the six
// PURE functions that make up the guessing-game engine. DO NOT EDIT THIS FILE —
// tests/tests.cpp includes it, and so do BOTH starter/engine.cpp (yours) and
// solution/engine.cpp (the reference). Change a signature and nothing links.
//
// THE BIG IDEA OF THIS LAB: separate the *logic* from the *randomness and I/O*.
// • The engine below is PURE: every function looks only at its arguments and
// returns a value — no std::cin, no std::cout, NO <random>. That makes it
// fully DETERMINISTIC, so the grader can pin down exact answers.
// • The dice-rolling, the seeding, the "type a guess" prompt — all of that
// lives in main() (see starter/main.cpp), which WRAPS this engine. The notes
// call this out: keep the Mersenne-Twister RNG and the I/O OUT of the logic
// you want to test. (CS6340: the same split lets you unit-test a fuzzer's
// decision logic without actually launching the random target.)
//
// Each function exercises a different Chapter-8 control-flow tool: an if/else
// chain, a switch, a while loop, a do-while validation loop, and a for loop with
// break/continue. Your whole job is to wire up that control flow correctly.
//
// Header guard (Chapter 2): stops this file being pasted in twice per build.
#ifndef ENGINE_H
#define ENGINE_H
#include <string> // std::string — owned, mutable text (Chapter 5)
#include <string_view> // std::string_view — cheap, non-owning view (Chapter 5)
// ─── TASK 1 ──────────────────────────────────────────────────────────────────
// judgeGuess — the heart of the game. Compare a guess against the secret and
// report the DIRECTION as a three-way result code (think of a comparator, or
// strcmp): the SIGN of the answer tells the player which way to move.
//
// guess < secret -> return -1 ("your guess is too LOW, go higher")
// guess > secret -> return +1 ("your guess is too HIGH, go lower")
// guess == secret -> return 0 ("CORRECT")
//
// Implement it with an if / else if / else CHAIN (the cases are mutually
// exclusive — exactly one branch should run). No loops here.
int judgeGuess(int secret, int guess);
// ─── TASK 2 ──────────────────────────────────────────────────────────────────
// hintText — turn a three-way code from judgeGuess into a human label, using a
// SWITCH statement (this is the switch task — please don't use if/else here):
// -1 -> "too low"
// 0 -> "correct"
// +1 -> "too high"
// Any other value -> "invalid" via the `default` case. Remember `break` after
// each case so you don't fall through into the next one.
std::string_view hintText(int code);
// ─── TASK 3 ──────────────────────────────────────────────────────────────────
// sumOfDigits — add up the decimal digits of `n` with a LOOP (`while` or `for`),
// peeling one digit at a time with `% 10` (the last digit) and `/ 10` (drop it).
// The digit sum is defined on the MAGNITUDE, so a negative n uses its absolute
// value: sumOfDigits(-123) == 6, same as sumOfDigits(123). sumOfDigits(0) == 0.
// (Assume n > INT_MIN — that one value's magnitude doesn't fit in an int.)
// (This is the chapter's "loop-driven numeric tool" — pure counting, no I/O.)
int sumOfDigits(int n);
// ─── TASK 4 ──────────────────────────────────────────────────────────────────
// countdownString — build the launch countdown as a STRING using a `while` loop,
// counting DOWN from `start` to 1, then appending the finale:
// countdownString(3) == "3..2..1..liftoff"
// countdownString(1) == "1..liftoff"
// countdownString(0) == "liftoff" // loop body runs zero times
// Note there is NO ".." before "liftoff" and NONE after the final number — the
// separator ".." goes only BETWEEN counts. (Returning a string, not printing,
// is what makes this testable — main() can std::cout it later.)
std::string countdownString(int start);
// ─── TASK 5 ──────────────────────────────────────────────────────────────────
// promptsUntilValid — a DETERMINISTIC stand-in for a do-while input loop. The
// notes' do-while example keeps re-prompting until the user types an in-range
// value; here the "typed" values are handed to you up front as a string of
// single digits (e.g. "0093" means the user typed 0, then 0, then 9, then 3),
// so the grader can test it without std::cin.
//
// Walk the digits left-to-right with a `do { … } while (…)` loop. Count how many
// you consume until you hit one that is IN RANGE [low, high]; return that 1-based
// COUNT (the digit's value, not its index). A do-while is the right shape here:
// you always read at least ONE value before you can judge it.
// promptsUntilValid("7", 1, 6) == 1 // first read (7) is out of range… but
// // it is the ONLY one, so 1 prompt
// promptsUntilValid("093", 1, 6) == 3 // 0 bad, 9 bad, 3 good -> 3rd prompt
// promptsUntilValid("4", 1, 6) == 1 // valid on the very first prompt
// If NONE are valid, you'll have consumed them all: return that total count.
// Assume `typed` is non-empty and contains only the characters '0'..'9'.
int promptsUntilValid(std::string_view typed, int low, int high);
// ─── TASK 6 ──────────────────────────────────────────────────────────────────
// playRound — the capstone, and the chapter's own number-guessing capstone in
// pure form. Play one bounded round against `secret`. The player's guesses are
// the digit characters of `guesses` (each char '0'..'9' is one numeric guess).
// You get at most `maxAttempts` *counted* attempts.
//
// Loop over the guesses with a `for` loop. For each character:
// • Convert it to its int value (e.g. '7' -> 7).
// • If that value is OUTSIDE [0, 9]… it can't happen here (digits only), but
// the real rule you must implement is: a guess equal to the SENTINEL 0 means
// "skip / misclick" — do NOT count it and `continue` to the next character.
// • Otherwise it's a real attempt: judge it with judgeGuess. If it's correct,
// `break` out and return the 1-based number of the COUNTED attempt that won.
// • Stop once you've used `maxAttempts` counted attempts (a loss).
// Return the winning attempt number (1-based), or 0 if the player never guessed
// `secret` within the budget. This pulls together for + if + break + continue.
// playRound(5, "375", 3) == 3 // guesses 3,7,5 -> 3rd attempt wins
// playRound(5, "12", 3) == 0 // never guessed 5 -> loss
// playRound(5, "059", 3) == 0 // the 0 is skipped (not counted); 5,9 miss
// playRound(5, "5", 3) == 1 // first counted attempt wins
int playRound(int secret, std::string_view guesses, int maxAttempts);
#endif // ENGINE_H
# Chapter 8 — Control Flow · The Number-Guessing Engine · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# The learner implements ../engine.h in starter/engine.cpp (the PURE, graded
# logic). The grader (tests/tests.cpp) supplies main() and calls every engine
# function across many deterministic inputs — so the learner never writes a test
# loop. A second main, starter/main.cpp, is the PLAYABLE driver that wraps the
# engine with <random> + std::cin/cout; it is NOT graded (`make run` plays it).
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# build — compile-check the learner's engine (warning-clean object file, no link)
# AND build the playable driver. `make test` does the full grader build+run.
build:
$(CXX) $(CXXFLAGS) -c starter/engine.cpp -o starter/engine.o
$(CXX) $(CXXFLAGS) starter/main.cpp starter/engine.cpp -o starter/play
@echo "OK ✅ starter/engine.cpp compiles. Now run: make test (or: make run to play)"
# run — play the game against YOUR engine (the driver supplies main()).
run: build
./starter/play
# test — grade the LEARNER's code: link the grader against starter/engine.cpp.
# RED until the TASK blocks are filled in; GREEN once they're correct.
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/engine.cpp -o tests/run
@./tests/run
# solution — play the game against the REFERENCE engine.
solution: solution/play
./solution/play
solution/play: solution/main.cpp solution/engine.cpp
$(CXX) $(CXXFLAGS) solution/main.cpp solution/engine.cpp -o solution/play
# test-solution — proof the lab is solvable: the reference MUST pass every check.
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/engine.cpp -o tests/run
@./tests/run
clean:
rm -f starter/engine.o starter/play solution/play tests/run
rm -rf tests/run.dSYM starter/play.dSYM solution/play.dSYM
make test locally
(see “Build & run locally” above).