On this page

Project: Tic-Tac-Toe

This module has no new statement to learn. It has one program to build: a game of Tic-Tac-Toe that two people can play by clicking, and then a computer that plays against you.

Almost everything in it comes from earlier modules. Module 13 gave you the board. Module 11 gave you the loop that searches. Module 7 gave you the habit of testing a function before trusting it. Two things are new: a type for an answer that may not exist, and a way for the lab to call your function when the picture is clicked. The rest of the lesson is about how to arrange what you already know.

A game is a few small functions

A game looks like one big thing. Written as one big main it would be a hundred lines that nobody could test. So the first step is not to write code. It is to ask what questions the program has to answer, and to give each question a function.

Decomposition is the name for this: splitting a program into functions that each answer one question, or do one job. For Tic-Tac-Toe the list is short.

Function The question it answers, or the job it does
winner Does one mark hold a whole line, and which mark?
status_of Is the game won, drawn, or still being played?
cell_at Which cell is at this point of the picture?
take_turn Play one turn in this cell, if the rules allow it.
draw Show this board.
choose_move Which cell should the computer take?

Every one of them takes the state of the game, or a part of it, as a parameter. Module 13 ended with a struct that holds this state, and this module uses it unchanged. The program below is that model, with one small function added that answers one small question: what does this board look like as text?

#include <iostream>
#include <string>
#include <vector>

enum class Mark { Empty, X, O };

struct Board {
    std::vector<Mark> cells;
    Mark turn;
};

Board new_board() {
    Board board{};
    for (int i = 0; i < 9; ++i) {
        board.cells.push_back(Mark::Empty);
    }
    board.turn = Mark::X;
    return board;
}

bool place(Board& board, int position) {
    if (position < 0 || position > 8) {
        return false;
    }
    if (board.cells[position] != Mark::Empty) {
        return false;
    }
    board.cells[position] = board.turn;
    if (board.turn == Mark::X) {
        board.turn = Mark::O;
    } else {
        board.turn = Mark::X;
    }
    return true;
}

std::string mark_text(Mark mark) {
    switch (mark) {
        case Mark::X:
            return "X";
        case Mark::O:
            return "O";
        case Mark::Empty:
            return ".";
    }
    return "?";
}

void print_board(const Board& board) {
    for (int i = 0; i < 9; ++i) {
        std::cout << mark_text(board.cells[i]);
        if (i % 3 == 2) {
            std::cout << "\n";
        }
    }
}

int main() {
    Board board = new_board();
    place(board, 4);
    place(board, 0);
    place(board, 8);
    print_board(board);
    std::cout << "Next: " << mark_text(board.turn) << "\n";
}
O..
.X.
..X
Next: O

The nine cells are numbered 0 to 8, row by row.

0 1 2
3 4 5
6 7 8

place is the function from module 13. It refuses a position outside 0 to 8 and a cell that is taken. Otherwise it puts the mark whose turn it is into the cell and passes the turn to the other mark.

mark_text is the name function of module 13: a Mark cannot be printed, so a function turns it into text. print_board goes over the nine positions and ends the line after positions 2, 5 and 8, which are the ones where i % 3 is 2.

Notice what the first lines of the functions say. place takes a Board&, because its job is to change the caller's board. print_board takes a const Board&, because it only looks. A reader can tell which functions are able to change the game without reading a single body. In the whole project only one function will take a Board&.

Small functions have a second advantage. Each one can be given a board built by hand and its answer compared with the answer you expect. A function that answers one question has one kind of test.

A value, or nothing

Some questions do not always have an answer. "Which mark has won?" has none while the game is still going. "Which cell was clicked?" has none when the click was beside the board.

Module 11 met this problem in searching, and solved it with a special number: a search returned a position, or -1 for "not found". That works for positions, because -1 is never a position. It does not work when every number is a possible answer.

The library has a type for exactly this. A std::optional<int> holds an int, or nothing. It comes from the header <optional>.

#include <iostream>
#include <optional>
#include <vector>

// Returns: the first temperature lower than limit, or nothing if there is none.
std::optional<int> first_below(const std::vector<int>& temps, int limit) {
    for (int t : temps) {
        if (t < limit) {
            return t;
        }
    }
    return std::nullopt;
}

int main() {
    std::vector<int> week = {4, 2, -1, 3, -6};

    std::optional<int> frost = first_below(week, 0);
    if (frost) {
        std::cout << "First frost: " << *frost << " degrees\n";
        std::cout << "Five degrees warmer: " << *frost + 5 << "\n";
    } else {
        std::cout << "No frost\n";
    }

    std::optional<int> hard_frost = first_below(week, -10);
    if (hard_frost) {
        std::cout << "First hard frost: " << *hard_frost << " degrees\n";
    } else {
        std::cout << "No hard frost\n";
    }
}
First frost: -1 degrees
Five degrees warmer: 4
No hard frost

There are four things to learn, and the program uses each of them.

Written Meaning
return t; In a function whose return type is std::optional<int>, returning an int gives back an optional that holds it
return std::nullopt; gives back an optional that holds nothing. std::nullopt is the library's name for "nothing"
if (frost) true when frost holds a value, false when it holds nothing. It does not look at what the value is
*frost the value inside. Here that is an int, and it can be used wherever an int can

The first answer in this program is -1. A function that returned -1 for "nothing found" could not have told you so: the caller would not know whether -1 was a temperature or a failure. With std::optional the two cases cannot be mixed up, because "nothing" is not one of the numbers.

Three rules go with it.

  • An optional is not the value. frost + 5 does not compile, and neither does int t = frost;. You have to write *frost, and that makes you think about whether there is a value at all.
  • Write *frost only where you know there is a value, which in practice means inside if (frost) { ... }. Reading the value of an optional that holds nothing is not defined, like reading outside a vector with []. The compiler does not stop you.
  • if (frost) asks whether there is a value. It is still true when the value is 0, and when the value is false.

The type in the angle brackets can be any type, as for a vector. This project uses std::optional<Mark> for "the mark that has won, or nothing" and std::optional<int> for "the cell at this point, or nothing".

Winning lines as data

A player wins by holding all three cells of a line. There are eight lines: three rows, three columns and two diagonals.

One way to look for a winner is eight if statements, each naming three cells. That is twenty-four positions typed into conditions, and one wrong digit among them makes a game that misses one win. Nobody would see it by reading.

The other way is to notice that the eight tests are the same test with different numbers. Then the numbers can be data, and the test can be written once. The eight lines fit in a grid of positions, as in module 11: eight rows, and each row holds the three positions of one line.

#include <iostream>
#include <vector>

const std::vector<std::vector<int>> win_lines = {
    {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
    {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
    {0, 4, 8}, {2, 4, 6}
};

// Needs: position is from 0 to 8.
// Returns: how many of the eight lines pass through that cell.
int lines_through(int position) {
    int total = 0;
    for (const std::vector<int>& line : win_lines) {
        for (int p : line) {
            if (p == position) {
                ++total;
            }
        }
    }
    return total;
}

int main() {
    std::cout << "Lines: " << win_lines.size() << "\n";
    std::cout << "Through the centre: " << lines_through(4) << "\n";
    std::cout << "Through a corner: " << lines_through(0) << "\n";
    std::cout << "Through an edge: " << lines_through(1) << "\n";
}
Lines: 8
Through the centre: 4
Through a corner: 3
Through an edge: 2

win_lines is declared outside every function, so it is a global variable. Module 6 said that those are avoided, and gave the reason: any line anywhere in the program may be the one that changed it. That reason does not apply here. The variable is const, so no line can change it. A const table that several functions read is the ordinary way to keep a program's fixed data.

Read the table against the numbered board. The first row of the table is the three rows of the board, the second is the three columns, and the third is the two diagonals. A table like this can be checked by eye in a few seconds, which eight conditions cannot.

for (const std::vector<int>& line : win_lines) visits the eight lines in turn. Each line is a vector of three positions, so line[0], line[1] and line[2] are the positions of its cells. lines_through uses them to answer a question about the board's shape. The centre lies on four lines, a corner on three and an edge on two, which is why players like the centre.

A function that looks for a winner has the same outer loop. For each line it asks about the three cells of the board at those three positions: board.cells[line[0]] and its two neighbours. You will write that function in the lab. One warning now: on an empty board, the three cells of every line are equal to each other.

How does the game stand?

Many programs have to say how far along something is: running, finished well, finished badly. The possibilities are a short fixed list, so an enum class holds them. The function that works out the answer asks a few questions about the state, and the order of the questions matters.

A quiz has five questions, and three right answers pass it.

#include <iostream>
#include <string>

struct Quiz {
    int asked = 0;
    int right = 0;
};

enum class Progress { Running, Passed, Failed };

Progress progress_of(const Quiz& quiz) {
    if (quiz.right >= 3) {
        return Progress::Passed;
    }
    if (quiz.asked == 5) {
        return Progress::Failed;
    }
    return Progress::Running;
}

std::string progress_name(Progress progress) {
    switch (progress) {
        case Progress::Running:
            return "running";
        case Progress::Passed:
            return "passed";
        case Progress::Failed:
            return "failed";
    }
    return "unknown";
}

int main() {
    std::cout << progress_name(progress_of(Quiz{2, 1})) << "\n";
    std::cout << progress_name(progress_of(Quiz{4, 3})) << "\n";
    std::cout << progress_name(progress_of(Quiz{5, 2})) << "\n";
    std::cout << progress_name(progress_of(Quiz{5, 3})) << "\n";
}
running
passed
failed
passed

Look at the last quiz: five questions asked, three right. Both if conditions are true for it. The quiz is over, and it is passed. The function gives the right answer because it asks about passing first. With the two if statements exchanged, a learner whose third right answer came on the last question would be told they had failed.

The rule is general. When two conditions can be true together, decide which one wins, and test that one first. return then leaves the function, and the later questions are asked only when the earlier answers were no.

Tic-Tac-Toe has the same trap. A game is drawn when the board is full and nobody has won. But the ninth mark can be the one that completes a line, and then the board is full and there is a winner. So the function that reports the status of a game asks its questions in this order: is there a winner? If not, is there an empty cell? If not, it is a draw.

Notice also that Progress is not stored in the Quiz. It is worked out from the two numbers whenever somebody asks. A stored copy could disagree with the numbers after one forgotten update. A value that is worked out cannot.

A function the lab calls

Every function so far ran because your program called it. A program that reacts to clicks needs the opposite: the program cannot know when a click will come, so the lab has to call a function of yours when it does.

on_click arranges that. You give it the name of a function, and the lab calls that function for every click on the picture, with the x and y of the click.

#include "scimigo.h"
#include <string>
using namespace scimigo;

int clicks = 0;

void number_the_click(int x, int y) {
    ++clicks;
    circle(x, y, 16, "gold");
    text(x - 6, y + 7, std::to_string(clicks), 20);
}

int main() {
    canvas(600, 400);
    rectangle(0, 0, 600, 400, "skyblue");
    text(20, 30, "Click anywhere", 20);
    on_click(number_the_click);
}
Click anywhere

Run this in the lab and click the picture a few times. Each click gets a gold disc with a number: 1, 2, 3.

number_the_click is a click handler: a function that is written to be called when a click happens. It must have this shape: it returns nothing, and it takes two ints. The lab fills them in with the position of the click, in the coordinates of module 1. You choose the name.

on_click(number_the_click); hands the function to the lab. Notice that there are no parentheses after number_the_click. With parentheses it would be a call. Without them it is only the function's name, and on_click remembers it. Nothing in this program calls number_the_click. The lab does.

Here is what happens, in order.

  1. main runs to its end: the background, the line of text, and on_click.
  2. The program waits.
  3. You click. The lab calls number_the_click with the position of the click.
  4. The program waits again.

The handler needs to know how many clicks came before, and it has nowhere to get that from. Its parameters are fixed: an x and a y, and the lab supplies both. Its local variables are made new on every call, as module 6 showed, so a local counter would be 1 every time. The count has to live in a variable that outlasts the call, and that is a variable declared outside the functions: int clicks = 0;.

This is the one place in this course where a global variable that changes is the right tool. The handler cannot be given the state as a parameter, because you are not the one who calls it. Keep the habit of module 6 everywhere else. In this project there will be exactly one such variable, and only the handler will name it. Every other function gets what it needs through its parameters.

One detail about the lab, in case you look closely. It does not keep your program running between clicks. On every click it runs the program again from the start, and then calls the handler once for each click so far, in order. The result is the same as if the program had been waiting: after the third click, the handler has been called three times and clicks is 3. You can write your program as if steps 1 to 4 above were the whole story. It does mean that anything main prints is printed again on each click.

State first, picture second

The last program drew straight from the handler. That only works for a picture that never has to change. In a game, a click may have to change something that is already on the screen, or be refused, or end the game. The handler cannot decide any of that by looking at the picture. It can only decide it from the state.

So a program that reacts to events is arranged in two halves that are kept apart.

  1. An event changes the state, and nothing else.
  2. The picture is drawn again, all of it, from the state.
#include "scimigo.h"
#include <string>
#include <vector>
using namespace scimigo;

void draw(const std::vector<int>& taps) {
    canvas(600, 400);
    int total = 0;
    int n = taps.size();
    for (int i = 0; i < n; ++i) {
        rectangle(100 + i * 100, 150, 96, 96, "gold");
        text(135 + i * 100, 212, std::to_string(taps[i]), 36);
        total += taps[i];
    }
    text(100, 300, "Total: " + std::to_string(total), 24);
}

std::vector<int> counters = {0, 0, 0, 0};

void handle_click(int x, int y) {
    if (x >= 100 && x < 500 && y >= 150 && y < 250) {
        int box = (x - 100) / 100;
        ++counters[box];
    }
    draw(counters);
}

int main() {
    draw(counters);
    on_click(handle_click);
}
0000Total: 0

Four boxes each show a number, and a click on a box adds one to its number. A line below shows the total.

The state is counters, four ints. handle_click does the first half. It works out which box was clicked and adds one to that element. It draws nothing itself. Then it calls draw.

draw does the second half. It starts with canvas(600, 400). Calling canvas again clears the picture, so every call of draw starts from an empty picture and draws the four boxes, the four numbers and the total from the vector it was given. It does not know that a click happened, or which box changed.

This arrangement has three consequences.

  • The picture cannot disagree with the state. There is no old number to erase and no way to forget to update the total. Whatever the vector holds is what is shown.
  • The state can answer questions. The total is worked out from the vector. A program that only drew numbers on the screen would have nothing to add up. In the game, "is this cell taken?" and "has somebody won?" are questions of this kind.
  • draw can be tested without clicking. It takes the state as a parameter, so it can be called with any vector. Only handle_click names the global variable.

The handler also shows how a click becomes a position in the state. The boxes start at x 100 and each is 100 wide, so (x - 100) / 100 is 0 for the first box, 1 for the second, and so on. The if before it matters. A click at x 40 is to the left of every box, and (40 - 100) / 100 is -60 / 100, which is 0 in int division. Without the if, that click would count for the first box.

Trying a move on a copy

A computer opponent has to think ahead by one move: if I put my mark here, have I won? To answer, it must put the mark there and look. But it must not put it on the real board, because it is only asking.

The answer is a copy. Module 6 said that a parameter passed by value is a copy that the function may change freely, and module 10 said to pass vectors by const reference to avoid that copy. Here the copy is the point.

#include <algorithm>
#include <iostream>
#include <vector>

// Needs: scores has at least two elements.
// Returns: the total of the best three scores if extra were added.
int best_three_with(std::vector<int> scores, int extra) {
    scores.push_back(extra);
    std::sort(scores.begin(), scores.end());
    int n = scores.size();
    return scores[n - 1] + scores[n - 2] + scores[n - 3];
}

int main() {
    std::vector<int> scores = {55, 80, 62, 71};
    std::cout << "With a 90: " << best_three_with(scores, 90) << "\n";
    std::cout << "With a 40: " << best_three_with(scores, 40) << "\n";
    std::cout << scores.size() << " scores, and the first is still " << scores[0] << "\n";
}
With a 90: 241
With a 40: 213
4 scores, and the first is still 55

best_three_with answers a "what if" question. It adds the extra score, sorts, and adds up the last three. All of that happens to its own copy, because the parameter is std::vector<int> scores with no &. The last line of output shows that the caller's vector still has four elements, in their old order. Each call starts from a fresh copy, so the 90 of the first call is not there during the second.

A struct is copied in the same way, with every member, including a vector inside it. A function with the parameter Board board can place a mark on its board and ask who has won, and the game's real board never changes.

When a copy is needed in the middle of a function, an ordinary declaration makes one.

std::vector<int> trial = scores;   // a copy; changing trial leaves scores alone
trial.push_back(90);

The two ways do the same thing. Choose the parameter when the whole function is one "what if". Choose the declaration when a loop needs a fresh copy on each turn.

That is the last piece. The computer opponent in the lab looks at each empty cell, asks on a copy whether a mark there would win, and takes the first cell where the answer is yes.

Your turn

Now try it yourself

Predict what a program will do, run it, and step through it line by line. The checks tell you when your own code works.

Loading the lab…

In this lab

  1. A value, or nothing

    first_even looks for the first even number in a vector. Some vectors have none, so its return type is std::optional<int>: an int, or nothing.

    report calls it and prints one line. main calls report for three vectors: {3, 8, 5}, {3, 5} and {7, 0, 4}. Work out the three lines before you run it.

  2. A function the lab calls

    Run this program and click the picture a few times. Every click adds a blue dot where you clicked. add_dot is the click handler: main hands it to the lab with on_click(add_dot);, and the lab calls it with the position of each click.

    Change the program so that every second dot is orange: the first dot blue, the second orange, the third blue, and so on.

    add_dot has to know how many clicks there have been, and a variable declared inside it starts again on every call. So:

    1. Above add_dot, outside the functions, declare int clicks = 0;.
    2. In add_dot, add one to clicks on every click.
    3. Choose the colour from clicks. An even number has the remainder 0 when divided by 2.

    Leave main, the radius 12 and the name add_dot as they are.

  3. State first, picture second

    This is a small Tic-Tac-Toe board that fills the whole 300 by 300 picture, so each cell is 100 wide and 100 high. Board, new_board and place are the ones from module 13.

    The state is the variable game, declared outside the functions. handle_click changes it with place and then calls draw(game), which clears the picture and draws all of it again.

    So that the clicks are the same for everyone, main makes three calls of handle_click itself, as the lab would for three clicks: at (150, 150), at (250, 50) and at (160, 140). Then it prints the nine cells, with a dot for an empty cell, and whose turn it is.

    Work out the four printed lines, and what is on the picture at the end, before you run it. After running it you can go on clicking.

  4. One question each

    The two-player game needs five jobs done.

    1. Say which mark holds a whole line, if any does.
    2. Say whether the game is won, drawn or still being played.
    3. Say which cell is at a point of the picture, if any is.
    4. Play one turn in a cell if the rules allow it, and say whether it was played.
    5. Show a board on the picture.

    Here are the first lines of the five functions, in another order. Their names have been replaced by letters, so only the return type and the parameters are left to go by. Status is an enum class with the values Playing, XWins, OWins and Draw.

    void a(const Board& board);
    std::optional<int> b(int x, int y);
    bool c(Board& board, int position);
    Status d(const Board& board);
    std::optional<Mark> e(const Board& board);
    

    There is nothing to run in this step. Read each first line as a promise: what the function is given, what it may change, and what it gives back.

  5. Find the winner

    This is the first of four steps that build one program. Each step's starter contains finished code for the steps before it, so you can do a step even if an earlier one gave you trouble. The parts marked Given are not to be rewritten.

    Given here: Mark, Board, new_board and place from module 13, mark_text, print_board, and main.

    Your part is two things.

    The table win_lines. It is a grid of positions with one row for each line that wins. The three rows of the board are in it. Add the three columns and the two diagonals, so that it has eight rows of three positions. The cells are numbered 0 to 8, row by row.

    std::optional<Mark> winner(const Board& board) returns the mark that holds all three cells of one of the lines, and nothing (std::nullopt) if no mark does.

    • Write one loop over win_lines. Do not write eight conditions.
    • A line whose three cells are all empty is not a win.
    • The function prints nothing and does not change the board.

    main builds a board with X on a diagonal, prints it, and prints what winner returns. With your function it prints Winner: X. The checks call winner with many other boards.

  6. Win, draw, or keep playing?

    The second step of the project. The starter contains a finished winner, so this step does not depend on yours.

    A game of Tic-Tac-Toe is in one of four states: still being played, won by X, won by O, or drawn. Write a type for that and the function that works it out.

    enum class Status has the values Playing, XWins, OWins and Draw, in that order. The checks use these names.

    Status status_of(const Board& board) asks three questions, in this order.

    1. Does a mark hold a line? Then the answer is Status::XWins or Status::OWins. Call winner to find out. Do not look at the lines again yourself.
    2. Otherwise, is any cell empty? Then the game goes on: Status::Playing.
    3. Otherwise the board is full and nobody has won: Status::Draw.

    The order matters. The ninth mark can be the one that completes a line, and a full board with a winner is a win.

    The starter does not compile until Status and status_of exist, because the given status_name and main below your part use them. main prints the status of a new board and of a board after five moves: playing, then X wins.

  7. Play the whole game

    The third step: two people can play by clicking. The starter contains a finished winner and status_of, and empty versions of everything you write here, so it runs and shows an empty picture.

    The picture is 600 by 400. The board's top-left corner is at (150, 30), and each cell owns a square 100 wide and 100 high. Cell 0 owns x from 150 up to, but not including, 250, and y from 30 up to, but not including, 130. The whole board ends just before x 450 and y 330.

    Write these, from the top of your part down.

    std::optional<int> cell_at(int x, int y) returns the position, 0 to 8, of the cell that owns the point, and nothing for a point that is not on the board. Test for "not on the board" before you divide.

    bool take_turn(Board& board, int position) plays one turn. If the game is over, which status_of can tell you, it changes nothing and returns false. Otherwise it leaves the turn to place and returns what place returns. place already refuses a taken cell and a bad position.

    std::string status_line(const Board& board) returns exactly one of X wins, O wins, Draw, X to move, O to move. The last two are for a game that is still being played, and name board.turn.

    void draw(const Board& board) draws everything from the board, in this order.

    • First the canvas, 600 by 400, which clears the picture. The starter has this line.
    • For each position, a cell: a rectangle 96 wide and 96 high in the colour skyblue, with its top-left corner at the top-left corner of the square the cell owns. The 4 pixels left over make the gaps.
    • In each cell that is not empty, its mark: the text that mark_text gives for the cell, in size 48, placed 30 to the right of the cell's corner and 68 below it. Draw no text in an empty cell.
    • The status line: the text that status_line gives for the board, in size 24, at (150, 370).

    handle_click finds the cell at the click. If there is one, it takes a turn on game there. Then it draws game, whether or not anything changed.

    main draws game and hands handle_click to the lab with on_click.

    game is the one variable outside the functions. Use it in handle_click and main only. Every other function works on the board it is given, and the checks call them with boards of their own.

  8. A computer opponent

    The last step: the computer plays O and answers every turn you take. The starter is the finished two-player game. Run it first and play a few turns against yourself.

    std::optional<int> choose_move(const Board& board, Mark mark) returns the cell that mark should take.

    1. If mark can win at once by taking an empty cell, it returns that cell. If several cells would win, it returns the lowest position.
    2. Otherwise it returns the lowest empty cell.
    3. If the board is full, it returns nothing.

    To find out whether a cell would win, try it on a copy of the board: put mark into that cell of the copy and ask winner about the copy. The caller's board must not change. A helper function with a Board parameter passed by value is one way to get the copy, and a declaration inside the loop that makes a copy is another. The lesson section Trying a move on a copy shows both.

    choose_move uses the mark it is given. It does not look at board.turn, and the checks call it for X as well as for O.

    handle_click: when the click was a turn that was really played (take_turn returned true), ask choose_move for the computer's cell, with game and the mark whose turn it now is, and take that turn too. A click that played nothing gets no answer. Then draw game as before.

    If your turn ended the game, take_turn refuses the computer's answer, which is what you want.

  9. Block the win

    The computer of the last step takes a win when it sees one, and otherwise the first empty cell. It never stops you from winning. Make it a better player.

    The starter is the finished game. would_win(board, position, mark) is given: it tries the move on a copy and says whether mark would then hold a line.

    Change choose_move(const Board& board, Mark mark) so that it decides in this order.

    1. If mark can win at once, take that cell.
    2. Otherwise, if the other mark could win at once on its next turn, take that cell first, to block it.
    3. Otherwise take the centre, cell 4, if it is empty.
    4. Otherwise take the lowest empty cell.
    5. If the board is full, return nothing.

    When several cells fit rule 1, or several fit rule 2, return the lowest of them.

    Rule 2 asks the same "what if" question as rule 1, for the other mark. You will need the other mark: X when mark is O, and O when mark is X. A small function for that keeps choose_move readable.

    The checks call choose_move for X as well as for O, and then play against the computer by clicking.

  10. The winning line

    When a game is won, show where: the three cells of the completed line turn gold. The starter is the finished two-player game.

    std::optional<std::vector<int>> winning_line(const Board& board) returns the three positions of a line that one mark holds, and nothing if no mark holds a line. It is winner with a different answer: winner says who, and this says where. The answer is the row of win_lines for that line, a vector of three positions. If a board has two completed lines, return the one that comes first in win_lines.

    The type reads from the inside out: a vector of ints, or nothing.

    draw asks winning_line once, before its loop. A cell whose position is one of the three is drawn with "gold" in place of "skyblue". Everything else stays: nine rectangles of the same size in the same places, the marks, the status line. When nobody has won, no cell is gold.

    To use the positions, take the vector out of the optional first, at a place where you have tested that it holds one, and store it in a std::vector<int> variable of its own. The three elements of that variable are ordinary ints.

  11. A new game

    When a game is over, the two-player game ignores every click. Let the players go on: once the game is over, the next click anywhere on the picture starts a new game.

    • The new game has an empty board.
    • The mark that went second in the game that has ended goes first in the new one. X opens the first game, O the second, X the third, and so on, whoever won.
    • The click that starts the new game does only that. It places no mark, even if it was on a cell.
    • While a game is being played, everything works as before.

    Only handle_click and the state need to change. draw already shows whatever board it is given, and status_line already says O to move for an empty board on which it is O's turn.

    The program has to remember which mark opened the game being played, and that is not in the Board. It is a second piece of state: a Mark declared outside the functions, beside game.

    A Board can be assigned like any struct: assigning what new_board returns to game replaces the finished game with an empty one, and the member turn of game can then be set.

  12. Tests for the game

    The game rests on two functions that answer questions about a board: winner and status_of. Module 7 said not to trust a function before it has been tested. Write the tests.

    The starter contains both functions and three helpers, so that one test case is one line. board_from builds a board from nine characters. wins(rows, mark) says whether winner reports that mark for the board. nobody_wins(rows) says whether winner reports nothing. Two pieces of text in quotes, written one after the other, are joined by the compiler into one, so a board can be written as three rows.

    Fill in test_winner and test_status with at least ten boards between them. For every board, decide the expected answer before you run anything, as in module 7.

    Then find out whether your tests are any good. Break winner on purpose: remove one line from win_lines, or delete the test for Mark::Empty. Run the program. An assert of yours should stop it. If the program still prints All tests passed, your tests have a gap. Add the case that catches the bug, and then repair the function.

    Nothing checks this one. When you have finished, go through the list below.

  13. Four in a row

    Plan, and then build, a second game with the method of this module.

    The game. The grid has 7 columns and 6 rows. Two players take turns. A player clicks a column, and their mark drops to the lowest free cell of that column. The first player with four marks next to each other in a line wins. The line can go across, down, or along either diagonal. If the grid fills up with no such line, the game is a draw.

    Plan first. Before you write any function, write the plan as comments at the top of the program.

    • The state. Module 11's grid, a vector of vectors, suits the cells. What else must the program remember?
    • The list of functions: the first line of each, and the one question it answers or the one job it does. Tic-Tac-Toe needed six. Expect about as many here, and expect some of them to be nearly the same.
    • For each function, whether it takes the state by const reference, by reference, or as a copy.

    Then build it, one function at a time, in an order that lets you test each function before the next one needs it. A function that answers a question can be tested with a grid built by hand, before any drawing exists.

    Two things are different from Tic-Tac-Toe, and both are worth thinking about before you start. A click picks a column, not a cell, so one function has to find the lowest free cell of a column, and that cell may not exist. And there are far too many lines of four to list in a table by hand. Look for another way to find them: from a cell, a line goes on in a direction, and a direction is a pair of numbers, a step in the row and a step in the column.

    Nothing checks this one. When you have finished, go through the list below.

Finished the free foundation?

Keep going with the advanced track

8 more modules. Every lesson is free to read; one purchase unlocks the guided labs.

  1. Objects, Lifetime & RAIIA move-only owning type, and a session that restores its settings on every exit
  2. Pointers and OwnershipA dispatch desk where every job has exactly one owner
  3. How std::vector Really WorksA vector that grows its own buffer, and a reception board that never shifts its cells
  4. STL: Containers & AlgorithmsA departure board that keeps the newest update for every stop, in order
  5. Interview Patterns in C++Search, window and maze solvers, and the longest batch that fits a limit
  6. Dynamic Programming & CapstoneMemoized and tabulated solutions that recover their choices, and an exact supply-pack planner
  7. Trees and Ordered SearchA search tree you grow, walk and prune, and a rescue log that finds the nearest boat
  8. Stacks, Heaps and Priority QueuesA skyline solver, a heap you repair by hand, and a merge of hundreds of sorted logs
Start with Objects, Lifetime & RAII