TicTacToe kata
© 2019 Jacob Mossberg
Contents
- Introduction
- Folder structure
- Prerequisites
- Run tests
- Run console game
- Solution proposal
- Visual Studio 2019
- Coding kata
Introduction
The goal of this kata is to develop a class named TicTacToe
that let us play the Tic-tac-toe game. Wikipedia has the following description of the game:
Tic-tac-toe (American English), noughts and crosses (British English), or Xs and Os is a paper-and-pencil game for two players, X and O, who take turns marking the spaces in a 3×3 grid. The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game.
The players are typically denoted with X
and O
on the game board. A game of Tic-tac-toe can proceed like shown below.
We start with an empty board:
---
---
---
Player X
puts a marker in the upper left corner:
X--
---
---
Player O
puts a marker in the upper right corner:
X-O
---
---
After some more iterations the game board looks like this:
XXO
-XX
-OO
Player O
makes a winning move by putting a marker in the lower left corner:
XXO
-XX
OOO
Player O wins!
Folder structure
-
3rdparty
contains the Catch2 test frame work -
solution
contains one possible solution to the kata. You can take a peek if you get stuck. -
src
contains the code. Initially it contains a dummy function that alwas returnstrue
.- Add new code to
src/tictactoe.hpp
andsrc/tictactoe.cpp
. - The final console application is run from
src/main_src.cpp.
. You must add code to this file in the last step of the kata.
- Add new code to
-
tests
contains the tests. Initially it contains a dummy test.- You should add new tests to
tests/test_tictactoe.cpp
- You should add new tests to
Prerequisites
- CMake 3.5 or higher
- C++ compiler that support C++17
Run tests
The following commands have been verified on Ubuntu 18.04.
-
Clone repository
$ cd git $ git clone https://github.com/jmossberg/tictactoe-kata-cpp.git $ cd tictactoe-kata-cpp
-
Build
$ mkdir build $ cd build $ cmake .. $ make
-
List available tests
$ tests/tictactoe_tests -l
-
Run tests
$ tests/tictactoe_tests
Run console game
-
Complete the clone and build steps in the previous section.
-
Run console game
$ cd git/tictactoe-kata-cpp/build $ src/tictactoe
Solution proposal
There is a folder named solution
that contains one possible solution for the kata.
Run tests (solution proposal)
-
Complete the clone and build steps in the previous section.
-
List available tests
$ solution/tests/tictactoe_tests_solution -l
-
Run tests
$ solution/tests/tictactoe_tests_solution
Run console game (solution proposal)
-
Complete the clone and build steps in the previous section.
-
Run console game
$ cd git/tictactoe-kata-cpp/build $ solution/src/tictactoe_solution
Visual Studio 2019
The git repostiory has been successfully opened in Visual Studio 2019 using the built in CMake support.
Coding kata
Now we can start with the kata itself. Follow the steps below one by one to develop first develop the helper class Position
and then the TicTacToe
class incrementally. Take a peek in the solution
folder if you get stuck.
Step 1
Let's start by developing a helper class named Position
that can be used to specify a position on the game board using x- and y-coordinates.
012 y-axis
0 ---
1 ---
2 ---
x-axis
Putting a X
mark at [x=1, y=0]
:
012 y-axis
0 -0-
1 X--
2 ---
x-axis
Add the following test to test_tictactoe.cpp
:
TEST_CASE("test_can_create_position_class_instance_with_zero_zero", "[tictactoe]") {
auto p = tictactoe::Position(0, 0);
REQUIRE(p.to_string() == "[0,0]");
}
Run the tests in test_tictactoe.cpp
to make sure the new test is failing.
Step 2
Now let's make test_can_create_position_class_instance_with_zero_zero
pass by adding a a class named Position
to tictactoe.hpp
within the tictactoe
namespace.
class Position {
public:
Position(int x, int y);
std::string to_string() const;
};
You must also include <string>
in tictactoe.hpp
.
And then add the following definitions to tictactoe.cpp
within the tictactoe
namespace:
Position::Position(int x, int y) {}
std::string Position::to_string() const {
return std::string{"[0,0]"};
}
Step 3
Add a new test to verify that the Position
class accepts other coordinate values than 0, 0
.
Code:
auto p = tictactoe::Position(1,2)
std::cout << p.to_string() << std::endl;
Output:
[1,2]
Make sure the test fail when you run it and then go ahead and fix the implementation of Position
to make the test pass.
Step 4
Let's continue and start working on the class TicTacToe
. We want the TicTacToe
class to maintain an updated view of the game board that can be retrieved by running the to_string()
method on an instance of the class.
Add a test to verify that we can create an instance of TicTacToe
and that it returns the following string representation of an empty board:
TEST_CASE("test_create_tictactoe_with_empty_board", "[tictactoe]") {
auto t = tictactoe::TicTacToe();
std::string expected_board;
expected_board += "---\n";
expected_board += "---\n";
expected_board += "---\n";
REQUIRE(t.to_string() == expected_board);
}
Make sure the test fails when you run it and then go ahead and create an initial implementation of TicTacToe
to make the test pass.
Add the following to tictactoe.hpp
:
class TicTacToe {
public:
std::string to_string() const;
};
And the following to tictactoe.cpp
:
std::string TicTacToe::to_string() const {
std::string board;
board += "---\n";
board += "---\n";
board += "---\n";
return board;
}
The test should now pass.
Step 5
The TicTacToe
class shall implement a play
method which accepts an argument set to X
or O
and also a position argument. Add a test to verify they following behavior:
TEST_CASE("test_play_o_marker_uppper_left", "[tictactoe]") {
auto t = tictactoe::TicTacToe();
t.play(std::string{"O"}, tictactoe::Position(0, 0));
std::string expected_board;
expected_board += "O--\n";
expected_board += "---\n";
expected_board += "---\n";
REQUIRE(t.to_string() == expected_board);
}
You will have to change multiple things to make this test pass:
-
Come up with an internal representation of the board in this step. One alternative is to use a vector of vector of char:
std::vector<std::vector<char>>;
You could then create a type alias to save on the amount of text you need to write:
using GameBoard = std::vector<std::vector<char>>;
-
Add a
play()
method to update the internal representation of the board when adding a player symbol to a specific position on the board. -
Update the
to_string()
method to use the internal representation of the board when producing the string.
Step 6
It would be handy if the TicTactoe
class implements an overload of the play
method that accepts a string as input. This can be used later when running the Tic-tac-toe game in the console.
Add the following test:
TEST_CASE("test_play_x_marker_uppper_left_using_string_input", "[tictactoe]") {
auto t = tictactoe::TicTacToe();
t.play("X,0,0");
std::string expected_board;
expected_board += "X--\n";
expected_board += "---\n";
expected_board += "---\n";
REQUIRE(t.to_string() == expected_board);
}
Now add the corresponding implementation to make it pass.
Step 7
We want the TicTacToe
class to check if we have a winner across the board and print out the winner in its string representation. Add a test and corresponding implementation to produce the following behavior.
TEST_CASE("test_detect_winner_across", "[tictactoe]") {
tictactoe::TicTacToe t;
t.play(std::string{"O"}, tictactoe::Position(0, 0));
t.play(std::string{"X"}, tictactoe::Position(1, 0));
t.play(std::string{"O"}, tictactoe::Position(0, 1));
t.play(std::string{"X"}, tictactoe::Position(2, 0));
t.play(std::string{"O"}, tictactoe::Position(0, 2));
std::string expected_board;
expected_board += "OOO\n";
expected_board += "X--\n";
expected_board += "X--\n";
expected_board += "Player O wins!\n";
REQUIRE(t.to_string() == expected_board);
}
Step 8
We also want the TicTacToe
class to check if we have a winner vertically on the board and print out the winner in its string representation.
TEST_CASE("test_detect_winner_vertically", "[tictactoe]") {
tictactoe::TicTacToe t;
t.play(std::string{"X"}, tictactoe::Position(0, 2));
t.play(std::string{"O"}, tictactoe::Position(1, 1));
t.play(std::string{"X"}, tictactoe::Position(1, 2));
t.play(std::string{"O"}, tictactoe::Position(2, 1));
t.play(std::string{"X"}, tictactoe::Position(2, 2));
std::string expected_board;
expected_board += "--X\n";
expected_board += "-OX\n";
expected_board += "-OX\n";
expected_board += "Player X wins!\n";
REQUIRE(t.to_string() == expected_board);
}
Go ahead and add a test to fix the above!
Step 9
Finally TicTacToe
shall find winners both on left-to-right diagonal and right-to-left diagonal on the board and print out the winner in its string representation.
Add tests to check the diagonals and the corresponding implementation in TicTacToe
.
Step 10
Now let's add some error handling to TicTacToe
. We want to throw an exception if we try to play on the same position twice.
TEST_CASE("test_play_same_cell_twice_throws_exception", "[tictactoe]") {
tictactoe::TicTacToe t;
t.play(std::string{"O"}, tictactoe::Position(1, 1));
REQUIRE_THROWS_AS( t.play(std::string{"X"}, tictactoe::Position(1, 1)), tictactoe::CellNotEmpty );
}
Step 11
We also want to throw an exception if the same player tries to play two times in a row.
Step 12
The TicTacToe
class shall recognize a tie, i.e. when the board is full and there is no player with three marks in a row (up, down, across or diagonally).
TEST_CASE("test_detect_tie", "[tictactoe]") {
tictactoe::TicTacToe t;
t.play(std::string{'O'}, tictactoe::Position(0, 0));
t.play(std::string{'X'}, tictactoe::Position(0, 1));
t.play(std::string{'O'}, tictactoe::Position(0, 2));
t.play(std::string{'X'}, tictactoe::Position(1, 1));
t.play(std::string{'O'}, tictactoe::Position(1, 0));
t.play(std::string{'X'}, tictactoe::Position(1, 2));
t.play(std::string{'O'}, tictactoe::Position(2, 1));
t.play(std::string{'X'}, tictactoe::Position(2, 0));
t.play(std::string{'O'}, tictactoe::Position(2, 2));
std::string expected_board;
expected_board += "OXO\n";
expected_board += "OXX\n";
expected_board += "XOO\n";
expected_board += "Tie!\n";
REQUIRE(t.to_string() == expected_board);
}
Step 13
If you reached this far you can think about how to make use of your TicTacToe
class in an interactive console application that let you and another person play tic-tac-toe against each other.
Running the application could look like this:
tictactoe
Board
---
---
---
Enter move (e.g. X,0,1): X,0,0
Board
X--
---
---
Enter move (e.g. X,0,1): O,0,1
Board
XO-
---
---
Enter move (e.g. X,0,1): X,2,2
Board
XO-
---
--X
Enter move (e.g. X,0,1): O,1,1
Board
XO-
-O-
--X
Enter move (e.g. X,0,1): X,1,0
Board
XO-
XO-
--X
Enter move (e.g. X,0,1): O,2,1
Board
XO-
XO-
-OX
Player O wins!
The console needs to implement a loop that does the following:
- Print current board
- Exit game if current board contains the sub string "wins"
- Exit game if the current board contains the sub string "Tie"
- Let the user enter a move in the form of
<player>,<x>,<y>
, e.g. X,0,1 - Feed the move into the
TicTacToe
play method