DPontes / kata-tictactoe

Kata with the TicTacToe exercise

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TicTacToe kata

© 2019 Jacob Mossberg

Contents

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 returns true.

    • Add new code to src/tictactoe.hpp and src/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.
  • tests contains the tests. Initially it contains a dummy test.

    • You should add new tests to tests/test_tictactoe.cpp

Prerequisites

  • CMake 3.5 or higher
  • C++ compiler that support C++17

Run tests

The following commands have been verified on Ubuntu 18.04.

  1. Clone repository

     $ cd git
     $ git clone https://github.com/jmossberg/tictactoe-kata-cpp.git
     $ cd tictactoe-kata-cpp
    
  2. Build

     $ mkdir build
     $ cd build
     $ cmake ..
     $ make
    
  3. List available tests

     $ tests/tictactoe_tests -l
    
  4. Run tests

     $ tests/tictactoe_tests
    

Run console game

  1. Complete the clone and build steps in the previous section.

  2. 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)

  1. Complete the clone and build steps in the previous section.

  2. List available tests

     $ solution/tests/tictactoe_tests_solution -l
    
  3. Run tests

     $ solution/tests/tictactoe_tests_solution        
    

Run console game (solution proposal)

  1. Complete the clone and build steps in the previous section.

  2. 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:

  1. 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>>;
    
  2. Add a play() method to update the internal representation of the board when adding a player symbol to a specific position on the board.

  3. 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

About

Kata with the TicTacToe exercise

License:MIT License


Languages

Language:C++ 92.4%Language:CMake 7.6%