Chapter 17 — Fixed-Size Arrays: Tic-Tac-Toe Referee
You will build a stateless Tic-Tac-Toe referee — a small library of pure functions that inspect a game board and answer questions like "who won?" and "is this a draw?" — plus two utility functions that demonstrate the two most important C-style-array lessons.
The board is represented as a 2D std::array:
using Board = std::array<std::array<char, 3>, 3>;Each cell holds 'X', 'O', or ' ' (empty). All board-inspecting functions
receive the board by const reference (notes 17.3) — a cornerstone rule for
passing fixed-size arrays without unnecessary copies.
The exercise is split into two independent parts:
Part A — std::array 2D (tasks 1–5). You write helper functions that detect
wins in rows, columns, and diagonals, then combine them into winner() and
isDraw(). The test suite feeds hand-built boards for every win type — row, column,
main diagonal, anti-diagonal — plus the draw and in-progress cases, so there is no
place for a lucky coincidence to pass the grader.
Part B — C-style arrays + decay + pointer arithmetic (tasks 6–7). Two focused
utility functions. sumScores demonstrates array decay (notes 17.8): the
const int scores[] parameter silently becomes const int*, so sizeof(scores)
inside the function yields the pointer size, not the array size. firstNegative
demonstrates pointer arithmetic with a half-open range [begin, end) (notes
17.9) — the same conceptual model that powers std::vector::begin()/end() and every
standard-library algorithm you will meet in Chapter 18.
Why Tic-Tac-Toe? Because std::array<std::array<char,3>,3> is small enough to be
readable yet large enough to practice every 2D traversal pattern — and the game
logic is simple enough that you can verify the output mentally. The real lesson is
in the shapes: how to pass a 2D array by const ref, how to index it, and what
happens when you cross the boundary from std::array into C-style territory.
Your tasks
Count a mark (
countMark). Walk every cell ofboardwith nested range-for loops and count how many cells equalch. Constraints: passboardbyconst Board&; useconst auto& rowfor the outer loop variable so you do not copy each inner array.Check if full (
isFull). Returntrueif no' 'cell remains. Delegate tocountMark— one call, one comparison. No loops needed here.Detect a winning row or column (
rowWinner,colWinner). For each rowr, check whetherboard[r][0] == board[r][1] == board[r][2]and that the cell is not' '. Similarly for each columnc. Return the winning character or' '. Use index-basedforloops (r = 0..2 or c = 0..2).Detect a winning diagonal (
diagWinner). Check the main diagonal(0,0)→(1,1)→(2,2)and the anti-diagonal(0,2)→(1,1)→(2,0). Return the winner or' '. No loops needed — just three-cell comparisons.Combine the helpers (
winner,isDraw).winner()callsrowWinner,colWinner, anddiagWinnerin sequence, returning the first non-' 'result.isDraw()returnstruewhenwinner()is' 'andisFull()istrue.Sum a C-style array (
sumScores). Sumscores[0..count-1]with a plain index loop. The key lesson is in the decay:const int scores[]in the parameter list is identical toconst int* scoresafter the compiler processes it — the length is completely lost. That is why the caller passescountseparately. Do not usesizeof(scores)inside the body.Walk a half-open range (
firstNegative). Walk[begin, end)withfor (const int* p { begin }; p != end; ++p)and return the first pointer where*p < 0, orendif none. Constraint: advance with++p, notp[i]orp + i— the goal is to feel pointer arithmetic directly.
Success criteria
countMark(kEmpty, 'X') == 0— empty board has no X markscountMark(kDraw, 'X') + countMark(kDraw, 'O') + countMark(kDraw, ' ') == 9— counts must cover every cellisFull(kDraw) == true— all 9 cells occupiedrowWinner(kEmpty) == ' '— all-empty rows do not count as winsrowWinner(kXRow2) == 'X'— detects the bottom row, not just the topdiagWinner(kXMainDiag) == 'X'anddiagWinner(kOAntiDiag) == 'O'— both diagonals detectedwinner(kDraw) == ' '— a draw board has no winnerisDraw(kXRow0) == false— a won board is not a draw even when fullsumScores(negs, 4) == 10— negative values in the array must be summed correctlyfirstNegative(data, data) == data— empty range must return end immediately (no crash)firstNegative(allPositive, end) == end— "not found" must return the end sentinel
Concepts practiced
std::array<T,N>basics — construction, indexing,size()(notes 17.1, 17.2)- Passing
std::arrayby const reference — the right default for read-only array parameters (notes 17.3) - Nested
std::array—std::array<std::array<char,3>,3>as a 2D board; row/column indexing (notes 17.13) - Aggregate initialization with nested braces —
Board {{ {row}, {row}, {row} }}(notes 17.4) usingtype aliases —Boardmakes the 2D type readable (ch 10)- C-style array decay —
const int scores[]parameter is reallyconst int*; sizeof gives pointer size (notes 17.8) - Pointer arithmetic —
++p,*p, half-open[begin, end)range convention (notes 17.9) - Nested range-for loops for 2D traversal (reused from ch 16 range-for)
- Index-based
forloops for row/column wins (reused from ch 8) - Const references —
const Board&,const int*(reused from ch 12) - Type aliases —
using Board = ...(reused from ch 10) - CS6340 bridge: LLVM's
BasicBlock::begin()/end()follows the same half-open pointer/iterator convention asfirstNegative.
Constraints
- Allowed:
std::array, C-style arrays, range-for loops, index-based for loops,constreferences, type aliases (using),if/else,return, and all features from chapters ≤ 17. - Forbidden:
<algorithm>or anystd::algorithm/iterator object (Chapter 18 — not taught yet),new[]/delete[](Chapter 19), lambdas (Chapter 20). - Required idioms:
- All board parameters must be passed by
const Board&(never by value). firstNegativemust advance with++p, not index arithmetic.sumScoresmust use thecountparameter (not sizeof) for the loop bound.
- All board parameters must be passed by
Build & run locally
make # compile-check starter/tictactoe.cpp (warning-clean)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # run the grader against the reference (to see what passing looks like)
make clean # remove build artifactsmake test is the grader — it supplies its own main and calls every function
across many fixed inputs.
Hints
Task 1 — nested range-for and const auto&
int count { 0 };
for (const auto& row : board) // const auto& avoids copying the inner array
for (char cell : row)
if (cell == ch)
++count;
return count;The outer const auto& is important: without it, each row would be a copy of
std::array<char,3>, a pointless allocation. Always bind the outer loop variable
by reference when looping over an std::array of arrays (notes 17.3 / 17.13).
Task 3 — index loop for row/column wins
For rows:
for (int r { 0 }; r < 3; ++r) {
char first { board[static_cast<std::size_t>(r)][0] };
if (first != ' '
&& first == board[static_cast<std::size_t>(r)][1]
&& first == board[static_cast<std::size_t>(r)][2])
return first;
}
return ' ';Checking first != ' ' prevents an all-empty row from being mistakenly called
a winner. The same pattern applies to columns — just transpose the indices.
Task 4 — diagonal comparisons (no loops needed)
char mid { board[1][1] }; // centre cell shared by both diagonals
// main diagonal: (0,0) – (1,1) – (2,2)
if (mid != ' ' && board[0][0] == mid && board[2][2] == mid)
return mid;
// anti-diagonal: (0,2) – (1,1) – (2,0)
if (mid != ' ' && board[0][2] == mid && board[2][0] == mid)
return mid;
return ' ';Checking mid != ' ' short-circuits immediately if the centre is empty.
Task 5 — winner/isDraw orchestration
char winner(const Board& board) {
char w { rowWinner(board) }; if (w != ' ') return w;
w = colWinner(board); if (w != ' ') return w;
w = diagWinner(board); if (w != ' ') return w;
return ' ';
}
bool isDraw(const Board& board) {
return winner(board) == ' ' && isFull(board);
}Task 6 — sumScores: plain index loop on a decayed pointer
int total { 0 };
for (int i { 0 }; i < count; ++i)
total += scores[i];
return total;scores[i] is valid even though scores is a pointer — subscripting a pointer is
identical to *(scores + i) (notes 17.9). Remember: count is the ONLY
reliable source of length here; sizeof(scores) yields the pointer size, not the
array length.
Task 7 — firstNegative: pointer arithmetic with ++p
for (const int* p { begin }; p != end; ++p) {
if (*p < 0)
return p;
}
return end;++p advances by one int (the compiler scales by sizeof(int) for you — notes
17.9). *p dereferences to read the element. Returning end when no negative is
found is the standard "not found" convention for half-open ranges — the caller
checks result != end.
Stretch goals
- Add
printBoard(const Board&)that renders the board tostd::coutwith ASCII borders (uses only ch ≤ 17 features). - Make
winner()returnstd::optional<char>so the absence of a winner is explicit rather than encoded as' '(notes ch 12std::optional). - Replace the
'X'/'O'/' 'charwith a scopedenum class Cell { X, O, Empty }and update all functions — prevents the caller from passing arbitrary characters (Chapter 13). - Write a flat-board variant
using FlatBoard = std::array<char, 9>with an index mappingidx(r, c) = r*3 + c, as described in notes 17.13 ("Flattening"), and add a second set of helper functions for it. - In a separate
main.cpp, write a simple two-player console loop that lets two humans play; usewinner()andisDraw()from this library to decide when to stop (uses only ch ≤ 17 features).
// Chapter 17 — Fixed-Size Arrays · Project: Tic-Tac-Toe Referee (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the seven TASK blocks below. Each maps 1:1 to a task in the README
// and to a declaration in ../tictactoe.h. The bodies currently return WRONG
// placeholders so the file compiles immediately — that is why `make test` is
// RED right now. Your job is to turn it GREEN by implementing the real logic.
//
// 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
//
// ── Scope reminder ────────────────────────────────────────────────────────────
// Only chapter ≤ 17 features are needed:
// std::array, C-style arrays, range-for loops (ch 16), index loops (ch 8),
// const references (ch 12), function templates are NOT required here —
// the Board typedef hides the type complexity.
// FORBIDDEN: <algorithm>, new[], any C++20 feature.
#include "../tictactoe.h"
// ─────────────────────────────────────────────────────────────────────────────
// PART A — std::array 2D (tasks 1–5)
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 1: count a mark across the whole board ─────────────────────────────
// countMark — walk every cell of `board` and count how many equal `ch`.
//
// KEY TERM: std::array<std::array<char,3>,3> is a 2D "array of arrays" (notes
// 17.13). board[r] is the r-th row (an std::array<char,3>); board[r][c] is one
// cell. Walk rows with an outer range-for, then columns with an inner range-for.
//
// Constraint: pass board by const reference — never copy a 2D array (notes 17.3).
// The signature already specifies `const Board&`; keep it.
//
// >>> YOUR CODE HERE <<<
//
int countMark(const Board& board, char ch)
{
(void)board; (void)ch;
return 0; // placeholder — always reports 0 marks (wrong for non-empty boards)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: check whether the board is full ─────────────────────────────────
// isFull — return true when no ' ' (empty) cell remains.
//
// Hint: delegate to countMark. One call, one comparison — no loops needed here.
//
// >>> YOUR CODE HERE <<<
//
bool isFull(const Board& board)
{
(void)board;
return false; // placeholder — always reports not full (wrong for full boards)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: scan for a winning row or column ────────────────────────────────
// rowWinner / colWinner — detect a complete row or column.
//
// KEY TERM: a *row* win means board[r][0] == board[r][1] == board[r][2] AND that
// character is not ' ' (an all-empty row does NOT count as a win). Similarly for
// columns: board[0][c] == board[1][c] == board[2][c].
//
// Use an outer loop over r (or c) and compare the three cells directly.
// Return the winning character, or ' ' if no row (or column) is complete.
//
// >>> YOUR CODE HERE <<<
//
char rowWinner(const Board& board)
{
(void)board;
return ' '; // placeholder — reports no row winner (wrong when a row is won)
}
// >>> YOUR CODE HERE <<<
//
char colWinner(const Board& board)
{
(void)board;
return ' '; // placeholder — reports no column winner
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: check the two diagonals ─────────────────────────────────────────
// diagWinner — return the winning character if the main diagonal or the
// anti-diagonal is complete, or ' ' if neither is.
//
// KEY TERM: main diagonal — (0,0),(1,1),(2,2). Anti-diagonal — (0,2),(1,1),(2,0).
// No loops needed; just compare three cells each time.
//
// >>> YOUR CODE HERE <<<
//
char diagWinner(const Board& board)
{
(void)board;
return ' '; // placeholder — always reports no diagonal winner
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: combine the helpers ─────────────────────────────────────────────
// winner — ask each helper in turn; return the first non-' ' result, else ' '.
// isDraw — no winner AND board is full.
//
// Keep the logic in the helpers — winner() and isDraw() are just orchestrators.
//
// >>> YOUR CODE HERE <<<
//
char winner(const Board& board)
{
(void)board;
return ' '; // placeholder — always reports no winner
}
bool isDraw(const Board& board)
{
(void)board;
return false; // placeholder — always reports not a draw
}
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// PART B — C-style arrays + decay + pointer arithmetic (tasks 6–7)
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: sum a C-style array (and observe decay) ─────────────────────────
// sumScores — sum scores[0..count-1].
//
// KEY TERM: ARRAY DECAY (notes 17.8). The parameter `const int scores[]` is
// SYNTACTIC SUGAR for `const int* scores` — the compiler converts it for you.
// Inside this function body, `scores` is a POINTER; sizeof(scores) yields the
// pointer size (8 bytes on most 64-bit platforms), NOT the array size. The
// README shows this in the "decay lesson" section.
//
// Use a plain index loop `for (int i = 0; i < count; ++i)`.
// Do NOT call sizeof(scores) — just use count.
//
// >>> YOUR CODE HERE <<<
//
int sumScores(const int scores[], int count)
{
(void)scores; (void)count;
return 0; // placeholder — always returns 0 (wrong for non-empty arrays)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 7: walk a half-open range with pointer arithmetic ──────────────────
// firstNegative — return a pointer to the first negative element in [begin,end),
// or `end` if none.
//
// KEY TERM: POINTER ARITHMETIC (notes 17.9). The half-open range [begin,end)
// convention: `begin` points at the first element, `end` points ONE PAST the last.
// Walk with `for (const int* p { begin }; p != end; ++p)` and test `*p < 0`.
//
// Return the pointer where the first negative lives, or `end` if the range has
// no negative element. The caller checks `result != end` to know if one was found.
//
// Constraint: use `++p` (not p[i] or p+i) to advance — the 17.9 lesson.
//
// >>> YOUR CODE HERE <<<
//
const int* firstNegative(const int* begin, const int* end)
{
(void)begin; (void)end;
return end; // placeholder — pretends no negative was found (wrong when one exists)
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// Chapter 17 — Fixed-Size Arrays · Project: Tic-Tac-Toe Referee (REFERENCE SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation of ../tictactoe.h.
// Peek only after you have taken a real swing at starter/tictactoe.cpp — the
// learning is in wiring up the array traversals yourself, then comparing.
//
// Everything here uses only chapter ≤ 17 features. No <algorithm>, no new[].
#include "../tictactoe.h"
// ─────────────────────────────────────────────────────────────────────────────
// PART A — std::array 2D (tasks 1–5)
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 1: count a mark across the whole board ─────────────────────────────
// We walk EVERY cell with nested range-for loops (notes 17.13). The outer loop
// binds each row by const reference (`const auto& row`) — without the `&`, each
// inner std::array<char,3> would be COPIED on every iteration (notes 17.3).
// `const auto&` matches the inner-array type automatically (ch 10 auto).
int countMark(const Board& board, char ch)
{
int count { 0 };
for (const auto& row : board) // outer: each row is std::array<char,3>
for (char cell : row) // inner: each char in that row
if (cell == ch)
++count;
return count;
}
// ─── TASK 2: check whether the board is full ─────────────────────────────────
// Delegate entirely to countMark — if zero empty cells remain, the board is full.
// This keeps all the traversal logic in one place (countMark) and keeps isFull
// as a one-liner that reads like plain English.
bool isFull(const Board& board)
{
return countMark(board, ' ') == 0;
}
// ─── TASK 3: scan for a winning row or column ────────────────────────────────
// rowWinner: for each row r, check if all three cells are equal AND non-empty.
// board[r][0] == board[r][1] == board[r][2] != ' '
//
// We use a plain index loop so we can address board[r][0], board[r][1], board[r][2]
// explicitly. Range-for works equally well; index loops match notes 17.2's
// "index-based for loop" discussion.
char rowWinner(const Board& board)
{
for (int r { 0 }; r < 3; ++r)
{
char first { board[static_cast<std::size_t>(r)][0] }; // left-most cell in row r
if (first != ' '
&& first == board[static_cast<std::size_t>(r)][1]
&& first == board[static_cast<std::size_t>(r)][2])
{
return first; // this row belongs to `first`
}
}
return ' '; // no row is complete
}
// colWinner: same idea, but the "line" runs down column c.
// board[0][c] == board[1][c] == board[2][c] != ' '
char colWinner(const Board& board)
{
for (int c { 0 }; c < 3; ++c)
{
char first { board[0][static_cast<std::size_t>(c)] }; // top cell of column c
if (first != ' '
&& first == board[1][static_cast<std::size_t>(c)]
&& first == board[2][static_cast<std::size_t>(c)])
{
return first; // this column belongs to `first`
}
}
return ' ';
}
// ─── TASK 4: check the two diagonals ─────────────────────────────────────────
// Main diagonal: (0,0) → (1,1) → (2,2). The shared middle cell is board[1][1].
// Anti-diagonal: (0,2) → (1,1) → (2,0).
//
// No loops needed — just compare the three cells for each diagonal. If the
// shared middle is ' ', neither diagonal can be a winning line, so we can short-
// circuit on that check first (optional optimization — correctness doesn't depend
// on it).
char diagWinner(const Board& board)
{
// Main diagonal ─────────────────────────────────────────────────────────
char mid { board[1][1] }; // centre cell is shared by both diagonals
if (mid != ' ' && board[0][0] == mid && board[2][2] == mid)
return mid; // main diagonal won by `mid`
// Anti-diagonal ─────────────────────────────────────────────────────────
if (mid != ' ' && board[0][2] == mid && board[2][0] == mid)
return mid; // anti-diagonal won by `mid`
return ' ';
}
// ─── TASK 5: combine the helpers ─────────────────────────────────────────────
// winner: call each helper in sequence; return the first non-' ' result.
// All the interesting logic lives in the helper functions above — winner() is
// just the entry point the grader (and a real game loop) would call.
char winner(const Board& board)
{
char w { ' ' };
w = rowWinner(board);
if (w != ' ') return w;
w = colWinner(board);
if (w != ' ') return w;
w = diagWinner(board);
if (w != ' ') return w;
return ' '; // no winner in any direction
}
// isDraw: no winner AND no empty cells (board is full).
// We never call isFull first and winner second — that would traverse the board
// twice unnecessarily. winner() is cheaper to call first because it can return
// early on the first winning line found.
bool isDraw(const Board& board)
{
return winner(board) == ' ' && isFull(board);
}
// ─────────────────────────────────────────────────────────────────────────────
// PART B — C-style arrays + decay + pointer arithmetic (tasks 6–7)
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: sum a C-style array (and observe decay) ─────────────────────────
// KEY TERM: ARRAY DECAY (notes 17.8).
// `const int scores[]` in a parameter list is EXACTLY the same as
// `const int* scores` — the compiler rewrites it. Therefore sizeof(scores)
// inside this function is sizeof(const int*) ≈ 8, NOT sizeof(the-caller's-array).
// That is the decay lesson the README explains in full.
//
// We sum with a plain index loop. The caller passes `count` because the pointer
// alone carries NO length information after decay (notes 17.8 "decay loses length
// information").
int sumScores(const int scores[], int count)
{
int total { 0 };
for (int i { 0 }; i < count; ++i)
total += scores[i];
return total;
}
// ─── TASK 7: walk a half-open range with pointer arithmetic ──────────────────
// KEY TERM: POINTER ARITHMETIC + half-open range [begin, end) (notes 17.9).
// `++p` advances p by ONE ELEMENT (not one byte — the compiler scales the
// addition by sizeof(int) automatically). We stop when p == end (the one-past-
// last sentinel), never dereferencing end. This pattern is EXACTLY how
// standard-library iterators (ch 18) work under the hood; getting comfortable
// with it here pays off immediately next chapter.
//
// CS6340 tie-in: LLVM's instruction iterators and BasicBlock::begin()/end()
// follow exactly this half-open range protocol. Knowing it at the pointer level
// makes the higher-level API feel familiar, not magic.
const int* firstNegative(const int* begin, const int* end)
{
for (const int* p { begin }; p != end; ++p) // ++p: move one int forward
{
if (*p < 0) // dereference p to inspect the element
return p; // first negative found — return its address
}
return end; // sentinel: no negative in [begin, end)
}
// Chapter 17 — Fixed-Size Arrays · Project: Tic-Tac-Toe Referee (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// A tiny no-framework unit-test harness (same style as drills/CLAUDE.md spec).
// Includes ../tictactoe.h and calls every function across MANY inputs — fully
// deterministic because none of the graded functions do I/O. Each failing CHECK
// prints its expression and line number. Any failure → non-zero exit → `make test`
// is RED.
//
// The Makefile links this file against starter/tictactoe.cpp (your code) for
// `make test`, and against solution/tictactoe.cpp for `make test-solution`.
//
// Boards are built with nested aggregate init: Board { { row0 }, { row1 }, ... }
// The outer braces are for the Board (std::array<...,3>); the inner braces for
// each std::array<char,3> row. See notes 17.4 and 17.13 for why.
#include <iostream>
#include "../tictactoe.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)
// ─────────────────────────────────────────────────────────────────────────────
// Helper boards — hand-built for exact test cases
// ─────────────────────────────────────────────────────────────────────────────
// Empty board — all spaces
static const Board kEmpty {{
{{ ' ', ' ', ' ' }},
{{ ' ', ' ', ' ' }},
{{ ' ', ' ', ' ' }},
}};
// Full board, no winner (a draw). X goes first so there are 5 X and 4 O.
// X O X
// X X O
// O X O
// No row, column, or diagonal is complete — verified exhaustively.
static const Board kDraw {{
{{ 'X', 'O', 'X' }},
{{ 'X', 'X', 'O' }},
{{ 'O', 'X', 'O' }},
}};
// X wins row 0 (top row: X X X). '.' marks an empty cell below.
// X X X
// O O .
// . . .
static const Board kXRow0 {{
{{ 'X', 'X', 'X' }},
{{ 'O', 'O', ' ' }},
{{ ' ', ' ', ' ' }},
}};
// X wins row 2 (bottom row). '.' marks an empty cell below.
// O O .
// . O .
// X X X
static const Board kXRow2 {{
{{ 'O', 'O', ' ' }},
{{ ' ', 'O', ' ' }},
{{ 'X', 'X', 'X' }},
}};
// O wins column 1 (middle column). '.' marks an empty cell below.
// X O X
// X O .
// . O .
static const Board kOCol1 {{
{{ 'X', 'O', 'X' }},
{{ 'X', 'O', ' ' }},
{{ ' ', 'O', ' ' }},
}};
// O wins column 2 (right column). '.' marks an empty cell below.
// X X O
// X . O
// . X O
static const Board kOCol2 {{
{{ 'X', 'X', 'O' }},
{{ 'X', ' ', 'O' }},
{{ ' ', 'X', 'O' }},
}};
// X wins main diagonal (0,0)→(1,1)→(2,2). '.' marks an empty cell below.
// X O .
// O X .
// . . X
static const Board kXMainDiag {{
{{ 'X', 'O', ' ' }},
{{ 'O', 'X', ' ' }},
{{ ' ', ' ', 'X' }},
}};
// O wins anti-diagonal (0,2)→(1,1)→(2,0). '.' marks an empty cell below.
// X X O
// . O .
// O . X
static const Board kOAntiDiag {{
{{ 'X', 'X', 'O' }},
{{ ' ', 'O', ' ' }},
{{ 'O', ' ', 'X' }},
}};
// In-progress — no winner, not full. '.' marks an empty cell below.
// X O .
// . X .
// O . .
static const Board kInProgress {{
{{ 'X', 'O', ' ' }},
{{ ' ', 'X', ' ' }},
{{ 'O', ' ', ' ' }},
}};
int main()
{
// ── Task 1: countMark ────────────────────────────────────────────────────
// Empty board: all 9 cells are ' ', none are X or O.
CHECK(countMark(kEmpty, ' ') == 9);
CHECK(countMark(kEmpty, 'X') == 0);
CHECK(countMark(kEmpty, 'O') == 0);
// Draw board: 5 X (X goes first), 4 O, 0 empty — board is completely full.
CHECK(countMark(kDraw, 'X') == 5);
CHECK(countMark(kDraw, 'O') == 4);
CHECK(countMark(kDraw, ' ') == 0);
// In-progress board: 2 X, 2 O, 5 empty.
CHECK(countMark(kInProgress, 'X') == 2);
CHECK(countMark(kInProgress, 'O') == 2);
CHECK(countMark(kInProgress, ' ') == 5);
// Edge: counts should sum to 9 for any valid board.
CHECK(countMark(kXRow0, 'X') + countMark(kXRow0, 'O') + countMark(kXRow0, ' ') == 9);
// ── Task 2: isFull ───────────────────────────────────────────────────────
CHECK(!isFull(kEmpty)); // empty board is not full
CHECK( isFull(kDraw)); // draw board is completely full
CHECK(!isFull(kXRow0)); // has empty cells
CHECK(!isFull(kInProgress)); // has empty cells
// ── Task 3: rowWinner / colWinner ────────────────────────────────────────
// Row winners
CHECK(rowWinner(kXRow0) == 'X'); // top row is all X
CHECK(rowWinner(kXRow2) == 'X'); // bottom row is all X
CHECK(rowWinner(kOCol1) == ' '); // no row winner on this board
CHECK(rowWinner(kEmpty) == ' '); // empty rows never win
CHECK(rowWinner(kDraw) == ' '); // no complete row in the draw board
CHECK(rowWinner(kInProgress) == ' '); // in-progress has no row winner
// Column winners
CHECK(colWinner(kOCol1) == 'O'); // middle column is all O
CHECK(colWinner(kOCol2) == 'O'); // right column is all O
CHECK(colWinner(kXRow0) == ' '); // no column winner here
CHECK(colWinner(kEmpty) == ' '); // empty columns never win
CHECK(colWinner(kDraw) == ' '); // no complete column in draw board
// ── Task 4: diagWinner ───────────────────────────────────────────────────
CHECK(diagWinner(kXMainDiag) == 'X'); // main diagonal X wins
CHECK(diagWinner(kOAntiDiag) == 'O'); // anti-diagonal O wins
CHECK(diagWinner(kXRow0) == ' '); // row win, not diagonal
CHECK(diagWinner(kOCol1) == ' '); // column win, not diagonal
CHECK(diagWinner(kEmpty) == ' '); // empty diagonals don't win
CHECK(diagWinner(kDraw) == ' '); // draw board has no diagonal winner
// ── Task 5: winner / isDraw ──────────────────────────────────────────────
// Each win type detected correctly by winner()
CHECK(winner(kXRow0) == 'X'); // row win
CHECK(winner(kXRow2) == 'X'); // row win (different row)
CHECK(winner(kOCol1) == 'O'); // column win
CHECK(winner(kOCol2) == 'O'); // column win (different column)
CHECK(winner(kXMainDiag) == 'X'); // main-diagonal win
CHECK(winner(kOAntiDiag) == 'O'); // anti-diagonal win
CHECK(winner(kDraw) == ' '); // no winner on draw board
CHECK(winner(kEmpty) == ' '); // empty board has no winner
CHECK(winner(kInProgress) == ' '); // in-progress has no winner yet
// isDraw: only true when board is full AND no one has won.
CHECK( isDraw(kDraw)); // the classic draw scenario
CHECK(!isDraw(kEmpty)); // empty: not full, not a draw
CHECK(!isDraw(kXRow0)); // X wins -> not a draw
CHECK(!isDraw(kOCol1)); // O wins -> not a draw
CHECK(!isDraw(kInProgress)); // not full yet -> not a draw
// ── Task 6: sumScores (C-style array + decay lesson) ─────────────────────
// Standard cases
{
int scores5[] { 10, 20, 30, 40, 50 };
CHECK(sumScores(scores5, 5) == 150);
int scores3[] { 1, 2, 3 };
CHECK(sumScores(scores3, 3) == 6);
}
// Edge: zero-element slice of an array (count = 0)
{
int arr[] { 99, 100, 101 };
CHECK(sumScores(arr, 0) == 0); // sum of zero elements = 0
}
// Edge: single element
{
int single[] { 42 };
CHECK(sumScores(single, 1) == 42);
}
// Edge: negative values in the array
{
int negs[] { -5, 10, -3, 8 };
CHECK(sumScores(negs, 4) == 10); // -5 + 10 + -3 + 8 = 10
}
// Edge: all zeros
{
int zeros[] { 0, 0, 0 };
CHECK(sumScores(zeros, 3) == 0);
}
// ── Task 7: firstNegative (pointer arithmetic + begin/end) ───────────────
// Basic case: negative in the middle
{
int data[] { 3, 7, -2, 5 };
const int* begin { data };
const int* end { data + 4 };
const int* result { firstNegative(begin, end) };
CHECK(result != end); // found something
CHECK(*result == -2); // found the right one
CHECK(result == &data[2]); // at the right address
}
// First element is negative
{
int data[] { -1, 2, 3 };
const int* result { firstNegative(data, data + 3) };
CHECK(result == data); // the very first element
CHECK(*result == -1);
}
// Last element is negative
{
int data[] { 1, 2, 3, -9 };
const int* result { firstNegative(data, data + 4) };
CHECK(result == &data[3]); // the last element
CHECK(*result == -9);
}
// No negatives: should return end
{
int data[] { 1, 2, 3, 4 };
const int* result { firstNegative(data, data + 4) };
CHECK(result == data + 4); // returned the end sentinel
}
// Edge: empty range (begin == end) — should return end immediately
{
int data[] { -5, -10 };
const int* result { firstNegative(data, data) }; // empty range
CHECK(result == data); // end of empty range is begin
}
// Edge: all negative
{
int data[] { -3, -1, -7 };
const int* result { firstNegative(data, data + 3) };
CHECK(result == data); // first element, which is the first negative
CHECK(*result == -3);
}
// Edge: only one element, positive
{
int data[] { 5 };
CHECK(firstNegative(data, data + 1) == data + 1); // not found -> end
}
// Edge: only one element, negative
{
int data[] { -5 };
CHECK(firstNegative(data, data + 1) == data); // found at [0]
}
// ── Final report ─────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS ✅ all referee checks passed.\n";
else
std::cerr << "\nFAIL ❌ " << fails << " check(s) failed"
" \xe2\x80\x94 fix the TASK blocks in tictactoe.cpp.\n";
return fails ? 1 : 0;
}
# Chapter 17 — Fixed-Size Arrays · Tic-Tac-Toe Referee · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Layout:
# tictactoe.h — declarations (provided, complete, do NOT edit)
# starter/tictactoe.cpp — learner's implementations (7 TASK blocks to fill in)
# solution/tictactoe.cpp — reference implementation
# tests/tests.cpp — grader (includes ../tictactoe.h; links against one .cpp)
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 starter (warning-clean .o, no link)
# Also builds a trivial demo so `make run` works without a grader.
build:
$(CXX) $(CXXFLAGS) -c starter/tictactoe.cpp -o starter/tictactoe.o
@echo "OK \xe2\x9c\x85 starter/tictactoe.cpp compiles. Now run: make test"
# run — smoke-test: just confirm the starter .o rebuilt cleanly (no separate main).
run: build
@echo "(No interactive driver for this exercise — run: make test)"
# test — grade the LEARNER's code: link the grader against starter/tictactoe.cpp.
# RED until the TASK blocks are filled in; GREEN once they are.
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/tictactoe.cpp -o tests/run
@./tests/run || echo "FAIL \xe2\x9d\x8c fill in the TASK blocks in starter/tictactoe.cpp until every check passes."
# solution — run the grader against the REFERENCE solution (shows what passing looks like).
solution: tests/tests.cpp solution/tictactoe.cpp
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/tictactoe.cpp -o solution/run
./solution/run
# test-solution — proof the lab is solvable: the reference MUST pass every check.
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/tictactoe.cpp -o tests/run
@./tests/run
clean:
rm -f starter/tictactoe.o tests/run solution/run
rm -rf tests/run.dSYM solution/run.dSYM
// Chapter 17 — Fixed-Size Arrays · Project: Tic-Tac-Toe Referee
// ─────────────────────────────────────────────────────────────────────────────
// This header is the CONTRACT between you and the grader. It declares all the
// functions the learner must implement. DO NOT EDIT THIS FILE — tests/tests.cpp
// includes it, and so do BOTH starter/tictactoe.cpp (yours) and
// solution/tictactoe.cpp (the reference). Change a signature and nothing links.
//
// THE BIG IDEA OF THIS LAB: there are TWO independent exercises here.
//
// Part A — std::array 2D (tasks 1–5)
// ─────────────────────────────────────────────────────────────────────────
// Build a STATELESS TIC-TAC-TOE REFEREE on top of a 3×3 board type alias:
//
// using Board = std::array<std::array<char, 3>, 3>;
//
// The Board is always passed by CONST REFERENCE (notes 17.3). That keeps the
// calling convention efficient and clean — no copies, no mutation. Each helper
// isolates one geometric pattern (row, column, diagonal) so the top-level
// winner() just calls them in sequence. The tests feed hand-built boards so
// the grader never touches stdin.
//
// Part B — C-style arrays + decay + pointer arithmetic (tasks 6–7)
// ─────────────────────────────────────────────────────────────────────────
// Two small utility functions that DEMONSTRATE the two main C-array lessons:
// • sumScores() — array DECAY: the parameter looks like an array, but the
// compiler silently converts it to a pointer. sizeof inside the function
// gives the POINTER size, not the array size. The README explains why.
// • firstNegative() — POINTER ARITHMETIC: walk a half-open range [begin,end)
// with ++p, dereferencing only when needed. This pattern is the conceptual
// ancestor of iterators (ch 18) and many standard-library algorithms.
//
// Header guard (ch 2): stops this file being pasted in twice per build.
#ifndef TICTACTOE_H
#define TICTACTOE_H
#include <array> // std::array (notes 17.1)
// ─── Board type alias (provided — do NOT change) ─────────────────────────────
// std::array<std::array<char,3>,3>:
// • Outer array: 3 rows (notes 17.13)
// • Inner array: 3 columns each
// • Element type: char ('X', 'O', or ' ' for empty)
//
// Board[row][col] indexing follows the 2D-array convention from notes 17.12 / 17.13.
// The alias keeps function signatures readable.
using Board = std::array<std::array<char, 3>, 3>;
// ─── PART A: std::array 2D functions ─────────────────────────────────────────
// ─── TASK 1 ──────────────────────────────────────────────────────────────────
// countMark — count how many cells in `board` contain character `ch`.
//
// countMark(board, 'X') -> number of X cells (0..9)
// countMark(board, 'O') -> number of O cells (0..9)
// countMark(board, ' ') -> number of empty cells (0..9)
//
// Constraints: pass `board` by CONST REFERENCE (notes 17.3 — never copy a 2D
// array by value). Use nested range-for loops (notes 17.13).
int countMark(const Board& board, char ch);
// ─── TASK 2 ──────────────────────────────────────────────────────────────────
// isFull — return true if every cell is occupied (no ' ' remains).
//
// Hint: delegate to countMark — one call, one comparison.
bool isFull(const Board& board);
// ─── TASK 3 ──────────────────────────────────────────────────────────────────
// rowWinner / colWinner — detect a winning line.
//
// rowWinner(board) returns the character that owns an entire row, or ' ' if
// no row is complete.
//
// colWinner(board) returns the character that owns an entire column, or ' '.
//
// A row r is won by character ch when board[r][0] == board[r][1] == board[r][2] == ch
// AND ch != ' '.
//
// A column c is won when board[0][c] == board[1][c] == board[2][c] == ch AND ch != ' '.
//
// Constraints: pass `board` by const reference. Use index-based for loops (i, j).
// Return ' ' (the empty-cell sentinel) if no winner is found.
char rowWinner(const Board& board);
char colWinner(const Board& board);
// ─── TASK 4 ──────────────────────────────────────────────────────────────────
// diagWinner — detect a winning diagonal.
//
// Main diagonal: board[0][0], board[1][1], board[2][2]
// Anti-diagonal: board[0][2], board[1][1], board[2][0]
//
// Return the winning character, or ' ' if neither diagonal is complete.
// Constraints: pass `board` by const reference. No loops needed — just
// compare the three cells directly.
char diagWinner(const Board& board);
// ─── TASK 5 ──────────────────────────────────────────────────────────────────
// winner — combine the helpers above.
//
// Return 'X' or 'O' if either player has won (any row, column, or diagonal).
// Return ' ' otherwise (no winner yet — game in progress or draw).
//
// Call rowWinner, colWinner, diagWinner in sequence. Return the first non-' '
// result you get, or ' ' if none of them find a winner.
char winner(const Board& board);
// isDraw — return true when no one has won AND the board is full.
//
// Use winner() and isFull() — keep the logic in those helpers.
bool isDraw(const Board& board);
// ─── PART B: C-style arrays + decay + pointer arithmetic ─────────────────────
// ─── TASK 6 ──────────────────────────────────────────────────────────────────
// sumScores — sum a C-style int array.
//
// sumScores(scores, count) returns the sum of scores[0..count-1].
//
// The parameter `const int scores[]` DECAYS to `const int*` at the call site
// (notes 17.8). Inside this function, `scores` IS a pointer — sizeof(scores)
// gives the POINTER size (typically 8 bytes on 64-bit), not the array size.
// The README walks through why; the code here just uses the pointer + count
// to sum the elements with a loop (avoid sizeof inside this body).
//
// Use a range-based index loop (for int i = 0; i < count; ++i).
int sumScores(const int scores[], int count);
// ─── TASK 7 ──────────────────────────────────────────────────────────────────
// firstNegative — find the first negative element via POINTER ARITHMETIC.
//
// firstNegative(begin, end) walks the half-open range [begin, end) using
// ++p (not an index), dereferencing with *p to inspect each element.
// Return a pointer to the first element where *p < 0, or `end` if none.
//
// This is the half-open begin/end pattern from notes 17.9, and the conceptual
// ancestor of every standard-library iterator-range algorithm (ch 18).
// Constraint: walk with ++p, NOT p[i] or p + i.
const int* firstNegative(const int* begin, const int* end);
#endif // TICTACTOE_H
make test locally
(see “Build & run locally” above).