Example Sample Logbook 3
Example Sample Logbook 3
Alex Bugby
1
Contents
Introduction ............................................................................................................................................ 3
Initial Setup ............................................................................................................................................. 3
Rules of Tic-Tac-Toe ................................................................................................................................ 3
Requirements Analysis ............................................................................................................................ 3
Behaviour Driven Development (Gherkin Specifications) ...................................................................... 5
Data Models .......................................................................................................................................... 11
Token Model ..................................................................................................................................... 11
Grid Model ........................................................................................................................................ 12
Turn Model........................................................................................................................................ 13
Input Model ...................................................................................................................................... 13
Player Model ..................................................................................................................................... 14
Axiomatic Definitions and Functions .................................................................................................... 15
Player Name ...................................................................................................................................... 15
Statistics ............................................................................................................................................ 15
Token................................................................................................................................................. 16
Grid.................................................................................................................................................... 16
Parsing inputs.................................................................................................................................... 16
Initialisation of the grid ..................................................................................................................... 17
Argument conversion and Data Transformation Graphs.................................................................. 17
Evaluating a Winner .......................................................................................................................... 19
T2 Implementation ............................................................................................................................... 20
Creating the Token Type ................................................................................................................... 20
The Grid............................................................................................................................................. 21
Defining Cell .................................................................................................................................. 21
Creating the Coord Type ............................................................................................................... 21
Defining Grid ................................................................................................................................. 21
Initialising the Grid ........................................................................................................................ 22
Rendering the Grid ........................................................................................................................ 22
Player Choosing a Grid Location ....................................................................................................... 23
Player Class ....................................................................................................................................... 26
Token Input ....................................................................................................................................... 27
Name Input ....................................................................................................................................... 28
A Note on Statistics ........................................................................................................................... 28
Menu Choice ..................................................................................................................................... 28
Game Loop ........................................................................................................................................ 30
Introduction
This logbook covers the design and software implementation of a Tic-Tac-Toe game using declarative
programming, interacted with by the user using a command-line interface (CLI). Within this
document, design paradigms will be shown thorough the use of gherkin specifications, set theory,
type definitions, and axiomatic definitions. Along with graph theory explanations, and justification of
behaviours. The system will be created in C++ following the specifications previously set. Manual and
automated testing will be undertaken to ensure the requirements laid out are met.
Initial Setup
The development environment consists of a Linux desktop Ubuntu installation, using Visual Studio
Code as the Integrated Development Environment. Where C++ will be the language used, using the
GNU Compiler Collection (GCC) to convert the program to an executable format. Alternative choices
of Python or F# could be used as the code base. However, due to having experience in Python
previously, I chose to work with C++ to broaden my understanding of other languages further.
The software projects aims to be a simple Tic-Tac-Toe game using a Command Line Interface (CLI) to
allow up to two players play the game simultaneously, whether that be two human players or one
human player and a computer opponent.
Rules of Tic-Tac-Toe
• The rules of Tic-Tac-Toe are as follows:
• The game is played on a 3x3 grid totalling 9 squares.
• One player chooses X or O as their token, where the other player receives the remaining
option. For example: If player one chooses X, then player two will be O.
• Players take it in turns to place their token in a square within the grid.
• A player cannot put a token in a square already occupied by a token.
• The first player to get 3 of their tokens in a row vertically, horizontally, or diagonally is
declared the winner.
• If no winner is decided before the 9 squares are filled with a token, then the game is a draw.
Requirements Analysis
The system needs to fulfil the following criteria:
From the above requirements, user input through a command line will be a necessary component.
To facilitate this, the user will be inputting string values into the command line to be parsed by the
program, wherein they will be converted to the appropriate types before being passed to their
relevant functions.
This however presents an issue, due to the program performing input and output (I/O) operations
the functions will be fundamentally impure in several cases due to not being able to have complete
precision over the input and output tuples behaviour. However, they are still able to be totalised in
most cases as it is possible to define when and where a function will return a value.
Boolean checks will need to be undertaken to evaluate that the win criteria has been met, it is
possible to assign this as a state in the program. However, Boolean values are very easy to handle
and will be the approach taken.
X O O
X O
X O X
X X O
O X
Data Models
• Input
o User Input (Standard Input)
• Output Message
o Menus
o Selected Name
o Game Grid Updates
o Win
o Loss
o Draw
o Statistics of a player
• Error
o Error Values
▪ Invalid argument
o Error Message
o Exit Code
• Exit Code
Token Model
The modelling of the game will begin with the model of the playing pieces, referred to as ‘Tokens’.
The Tokens used to play with are selected by the player when starting a valid game type. The Token
type contains two values of X and O, and a player is only allowed to choose values from this
definition. By defining Token as being a set of these two values, it becomes easier to check validity of
inputs, as the valid values are effectively constrained. As an example, say that Token was defined as
the character values of ‘X’ and ‘O’; in this case Token is now effectively a subset of all possible
Characters, which could cause issues in cases where the input being evaluated is not part of the
subset. A user inputting the value ‘U’ would have to be checked against for example.
With the above in mind, an expression of a Token could be defined as being a set of X and O:
#𝑇𝑜𝑘𝑒𝑛 = 2
Grid Model
The grid for the tic-tac-toe game as defined is a 3x3 square, diagrammatically presented below when
printed to standard output:
Figure 1: A Representation of the Playing Grid as printed to standard output to the user
The grid may also be presented with numeric values instead of characters. It is possible to make the
grid larger, or smaller, as requirements define. So, a malleable approach should be taken to allow for
re-use of code once implemented. For this purpose, each of the sub squares of the grid will be
denoted as cells. Where each cell is comprised of a Token or nothing (indicating an empty cell), to
represent this an option type can be used, the reason a None value is required is for the initial state
of the board, before any Tokens are placed. By having the values in each grid square location being
initially set to none, it is easy to check whether these values have changed later.
To correctly map these cells to a grid, a coordinate value will be used. A coordinate will be a set of
values from A – I. Paired to a cell with a dictionary data structure as key value pairs representing a
tile, where the coordinate is the key, and the cell the value. This is an effective way of allowing for
comparison of values. Note that the map is an ordered map, not unordered. This is due to values
needing to be presented to the user at some point in a specific order in a grid to make the interface
readable.
Turn Model
Since a game will involve two players, who take turns in order, it is viable to represent the players as
a sequence. Where the player at the head of the sequence is considered the active player. This will
also allow for modifications in the future, for example, perhaps a game would require 3 or 4 players.
Using a sequence allows for the number of players to be changed. The player sequence is
represented by the following:
Input Model
Players needs to be able to provide input to the system to select from presented options. An
example of an input would be the player passing in a string of X or O to select the Token they would
like to play as. However, any inputs will be component parts of pre-defined functions, and therefore
do not need a custom type defined for them. Any conversion can be done after inputs are validated.
For the sake of preciseness this can be defined as being a sequence of characters:
For the menu options, it is possible to define the options as values of a custom type that the user can
select from:
Player Model
A player is comprised of a name, and a set of statistics, the name component of a player will be a
sequence of Characters, further defined in the Axiomatic Definition of Player Name.
The statistics component contains a record of all wins, losses, and draws of the player. The wins,
losses, and draws will be stored as natural numbered integers, each up to a score of 1000 as
negative values are not required. This can be represented as a 32-bit unsigned integer. The equation
for which is stated below:
For each of the win, loss, and draw types a constraint needs to be placed on the 32-bit unsigned
integer so that the values are only valid up to the integer value of 1000. Represented below:
The Players game statistics therefore can be represented as a cartesian product of one of each wins,
losses and draws. For any one player, a player will have one triple of statistics. Represented below:
With the above declarations, a player object can be defined as being a cartesian product of a Token,
a name, and a single set of statistics.
Whereas every player in the system can be stored in a sequence to be referenced later.
Player Name
From the criteria set in the requirements analysis, the system needs to allow for a player to set their
name, this will be done through a function called “playerName” which takes no parameters and will
prompt the player to enter a name which is a sequence of characters and will then return this
sequence as name. The implementation of this function will be partial, as there cannot be a mapped
matching pair to every possible input due to the variation of names that any user can have.
The name of a player also needs to be checked, as each name must be unique to be used as an
identifier for that player. Therefore, a player cannot enter and use a name that is already in the
system. The check is a pure function that returns a Boolean value based on whether the name exists
in the database (true) or if it does not (false).
Statistics
Furthermore, each player has a set of statistics associated to them based on their play history.
Initially these values will be set to 0, however each win, loss, or draw by the specified player will
increment the value of the relevant parameter by 1. The statistics will be a collection of all these
types together. The incrementing functions will be named “addWin”, “addLoss”, and “addDraw”
respectively.
𝑎𝑑𝑑𝑊𝑖𝑛 = 𝑈𝑖𝑛𝑡 → 𝑈𝑖𝑛𝑡
𝑙𝑒𝑡 𝑎𝑑𝑑𝑊𝑖𝑛 = 𝜆𝑥 . 𝑥 + 1
𝑎𝑑𝑑𝐿𝑜𝑠𝑠 = 𝑈𝑖𝑛𝑡 → 𝑈𝑖𝑛𝑡
𝑙𝑒𝑡 𝑎𝑑𝑑𝐿𝑜𝑠𝑠 = 𝜆𝑥 | 𝑥 ≥ 0 . 𝑥 + 1
𝑎𝑑𝑑𝐷𝑟𝑎𝑤 = 𝑈𝑖𝑛𝑡 → 𝑈𝑖𝑛𝑡
𝑙𝑒𝑡 𝑎𝑑𝑑𝐷𝑟𝑎𝑤 = 𝜆𝑥 | 𝑥 ≥ 0 . 𝑥 + 1
It is also possible to get and set these values, below is an axiomatic definition for the win value.
Token
Players are also able to select a Token, this will be accomplished via a function called
“getPlayerTokenInput” where the player Token initially is set to void and is set to either X or O after
the player chooses from available options.
𝑔𝑒𝑡𝑃𝑙𝑎𝑦𝑒𝑟𝑇𝑜𝑘𝑒𝑛𝐼𝑛𝑝𝑢𝑡: 𝑣𝑜𝑖𝑑 → 𝑇𝑜𝑘𝑒𝑛
𝑙𝑒𝑡 𝑔𝑒𝑡𝑃𝑙𝑎𝑦𝑒𝑟𝑇𝑜𝑘𝑒𝑛𝐼𝑛𝑝𝑢𝑡 = {(𝑣𝑜𝑖𝑑, 𝑋),(void, O)}
When a player has selected a Token, the available Token values available to any other proceeding
player are all the Token values that are not a Token value that has already been assigned. This is
implemented in the “assignTokenPlayer2” function.
𝑎𝑠𝑠𝑖𝑔𝑛𝑇𝑜𝑘𝑒𝑛𝑃𝑙𝑎𝑦𝑒𝑟2: 𝑇𝑜𝑘𝑒𝑛 → 𝑇𝑜𝑘𝑒𝑛
𝑙𝑒𝑡 𝑎𝑠𝑠𝑖𝑔𝑛𝑇𝑜𝑘𝑒𝑛𝑃𝑙𝑎𝑦𝑒𝑟2 = 𝜆𝑥 ⋅ ∉ 𝑔𝑒𝑡𝑃𝑙𝑎𝑦𝑒𝑟𝑇𝑜𝑘𝑒𝑛𝐼𝑛𝑝𝑢𝑡
Grid
A coordinate can be selected by the user through the argument x, provided the given argument is
convertible to one of the values that is a member of the coordinate type.
However, each tile must not already have a Token inside of it. To check this, the values within the
tile dictionary can be checked. In the event that the tile value is set to a Token then the player
should not be allowed to place their Token in that tile.
𝑐ℎ𝑒𝑐𝑘𝑂𝑐𝑐𝑢𝑝𝑖𝑒𝑑𝑇𝑖𝑙𝑒: 𝑡𝑖𝑙𝑒 → 𝑏𝑜𝑜𝑙
𝑙𝑒𝑡 𝑐ℎ𝑒𝑐𝑘𝑂𝑐𝑐𝑢𝑝𝑖𝑒𝑑𝑇𝑖𝑙𝑒 = {(𝑣𝑜𝑖𝑑, 𝑓𝑎𝑙𝑠𝑒}, (𝑋, 𝑡𝑟𝑢𝑒), (𝑂, 𝑡𝑟𝑢𝑒)}
Parsing inputs
Inputs taken from a user need to be validated so that they can be converted to the appropriate
types. This can be defined as the following:
𝑝𝑎𝑟𝑠𝑒𝑀𝑒𝑛𝑢𝐼𝑛𝑝𝑢𝑡: 𝑣𝑜𝑖𝑑 → 𝑜𝑝𝑡𝑖𝑜𝑛𝑎𝑙 < 𝑀𝑒𝑛𝑢𝐶ℎ𝑜𝑖𝑐𝑒 >
𝑙𝑒𝑡 𝑝𝑎𝑟𝑠𝑒𝑀𝑒𝑛𝑢𝐼𝑛𝑝𝑢𝑡 = {(𝑣𝑜𝑖𝑑, 𝑆𝑖𝑛𝑔𝑙𝑒𝑃𝑙𝑎𝑦𝑒𝑟), (𝑣𝑜𝑖𝑑, 𝑀𝑢𝑙𝑡𝑖𝑝𝑙𝑎𝑦𝑒𝑟), (𝑣𝑜𝑖𝑑, 𝑆𝑡𝑎𝑡𝑖𝑠𝑡𝑖𝑐𝑠), (𝑣𝑜𝑖𝑑, 𝐸𝑥𝑖𝑡)}
𝑔𝑒𝑡𝑃𝑙𝑎𝑦𝑒𝑟𝑀𝑒𝑛𝑢𝐼𝑛𝑝𝑢𝑡: 𝑣𝑜𝑖𝑑 → 𝑀𝑒𝑛𝑢𝐶ℎ𝑜𝑖𝑐𝑒
Below is a data transformation graph of the above pipeline, using the getPlayerTokenInput()
axiomatic definition.
Using this Token type value, it is possible to pass it as a parameter to another function to
automatically assign the other possible value to the second player without requiring user input.
Figure 3: The Data Transformation Graph to get one Token Value from Another
The method for conversion from a Token to a String for standard output to present the user requires
a Token value to be provided, which is then converted to a string representation of that value by
matching it to a Token constructor.
The menu input works in a similar manner, converting the input to an optional menu type, before
retrieving the value and converting to a MenuChoice.
As the grid cells are enumerated types, they can directly interpret integer values converted
from user character inputs. For example, the Coord B is equivalent to 1. So, when placing a
Token, a user enters a numerical value which is mapped to a coordinate.
Evaluating a Winner
A winner is declared when the following rule is met:
A row, column, or diagonal all contain the same Token Value such that the total number of Token
values are equivalent to the square root of the total number of grid spaces.
In the event of a 3x3 grid where all 9 nodes are filled with a Token and the rule stated above does
not apply then the game is considered a draw.
With the above rules, there are 8 total possible combinations of nodes in a 3x3 grid that are
equivalent to a win state for a player, as follows:
{{A,B,C} , {A, E, I} , {A, D, G}, {B, E, H}, {C, E, G}, {C, F, I}, {D, E, F}, {G, H, I}}
The following undirected graph is a representation of a win state for the player assigned the Token
X.
In this representation the following subset of Coord which equates to a win condition has been met:
{A,E,I}
In a draw state it can be seen that all the None values have been replaced with a Token value of
either X or O. However, none of the sets defined as being win conditions have been met, thus a draw
state has been met.
T2 Implementation
Using the previous specifications, implementation of the program can begin. When looking at
functions, there are two types that can be followed, pure or impure. Pure functions are functions
that do not modify the program state outside of their own scope. Whereas impure functions do
modify the program state. Along with this, there are totalised and non-totalised functions. Where
totalised functions have every possible input, value mapped to a valid output, where non-totalised
functions do not. With this in mind, it would be preferable to make pure, totalised functions where
possible.
struct X {
bool operator==(const X& other) const {
return true;
}
};
struct O {
bool operator==(const O& other) const {
return true;
}
};
The decision to use structs and variants was eventually reached after first creating the Token as an
Enumerated type. Though still functionally usable; since Enumerated types map any values to an
integer value it could lead to issues with error handling. This has an upside, where it is possible to
use switch cases. However, Regex is more comprehensive than switch cases and works with structs
too.
The Grid
Defining Cell
When it comes to creating a cell, it is possible to use an optional type. An optional type allocates a
third, “empty” value, designated as nullopt in the case of C++.
enum coord {
A, B, C, D, E, F, G, H, I
};
Figure 11: Creation of the coord Enumerated Type
Defining Grid
using Grid = std::map<coord, cell>;
Figure 12: A map containing a coordinate as a key, and a cell as a value
It is possible to create a value as being the nullopt option from Cell, but it seemed redundant to use
a typedef for something that is only used in this section of the code.
//check the what the value is in that map (grid) coordinate and print
out the value based on that
//if there's no X or O, then print the index value
//actually really helped me understand how to access values!
void renderBoard(const std::map<coord, cell>& grid) {
int counter = 0;
for (const auto& [key, value] : grid) {
counter++;
std::cout << "|";
std::cout << /*'[' << key << "] = " <<*/ " " <<
(value.has_value()? tokenToString(value.value()) :
std::to_string(key+1)) << " ";
if (counter % 3 == 0) {
std::cout << "|";
if (counter != 9) {
std::cout << "\n-------------\n";
}
}
}
std::cout << "\n";
};
Figure 14: Implementation of the Render of the Board to Standard Output
If the value entered is valid, then the grid key matching that value is checked. If the value is currently
nullopt, then the value is changed to that players Token. Otherwise the player is asked to choose
another location.
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
continue;
}
coord c = static_cast<coord>(input - 1);
if (grid[c]) {
std::cout << "That location already contains a token.
Please choose a different location.\n";
} else {
grid[c] = tkn;
break;
}
}
};
Figure 17: Implementation of the placeToken Function
Within the game types, there is an option to play a single player game, which pits the player against
a CPU opponent. The CPU opponent should only be able to enter Tokens into cells that do not
already contain a Token. This check can be done by adding all the cells that have nullopt as a value to
a list, and generating a random value within this range.
Previously, implementation of this was accomplished by just allowing the function to perform
random number generation between 1 and 9. However, this led to issues where the CPU would take
a long time to reach a valid value when the majority of cells on the grid were already occupied.
Causing a delay.
Player Class
But to do any of this, a player must be created. The player itself is an object created from the
following class template:
class Player{
private:
Token tk;
std::string name;
Statistics statistics;
public:
Player(Token tk, std::string name, Statistics statistics ) {
this->tk = tk;
this->name = name;
this->statistics = statistics;
}
Player() = default;
public:
Token getToken() {
return tk;
}
public:
void setToken(Token t) {
tk = t;
}
public:
std::string getName() {
return name;
}
public:
void setName(std::string n) {
name = n;
}
public:
Statistics getStatistics() {
return statistics;
}
};
Figure 19: Class Definition of Player
This template specifies that a Player object is comprised of a Token, a name, and a set of statistics.
The first of these two parameters (Token and name) are gathered via standard input requests.
Whereas statistics are initially created as a default set of values all equating to 0. In this case, due to
the amount of data within a player object, a class definition is an effective way to handle the data.
Token Input
For the input request for the Token, the input is taken as a string, and is converted to an optional
Token via regex pattern matching. In the event the player does not enter a valid value, the system
will repeatedly ask for input until a valid value is entered. This is totalised function that handles any
input it is given and converts it to either a Token or prints to standard error.
Token getPlayerTokenInput() {
// 1. Prompt the user for token input
// 2. read the string input
// 3. attempt to parse the string input
// 4. if successfully parsed to token
// 4a. then return the Token value
// 4b1. otherwise print to stderr an error message "ERROR: Invalid
token, cannot parse"
// 4b2. goto 1
std::string input;
std::cout << "Please select a valid token (O or X): ";
std::cin >> input;
std::optional<Token> tkn = parsePlayerToken(input);
if (tkn.has_value()){
return tkn.value();
} else {
std::cerr << "ERROR: Invalid token value, please try again." <<
std::endl;
return getPlayerTokenInput();
}
}
Figure 20: Implementation of obtaining the Player Input for a Token
The parsing component is undertaken in the “parsePlayerToken” function, a totalised, pure function
taking the parameterised string input from the user and returning the matching struct value, or
nullopt if no valid match is found.
However, only one of the players gets to select their Token. The other player must be given the
remaining Token as two players cannot have the same Token at any one time.
In this totalised, pure function the argument “Token t” above is the Token assigned to player 1. This
simple check just returns the opposite struct to the input argument, where the input argument is of
the variant Token. For example, if player 1 selected X as their Token, the above function would
return O.
Name Input
Players can also have a name. Similar to the getPlayerTokenInput function, this function reads a
string given from a standard input prompt and returns that name. Below is the function
implementing this.
std::string getPlayerNameInput() {
std::string playerName;
std::cout << "Please enter your username: ";
std::cin >> playerName;
return playerName;
Figure 23: Returning a player name from a string input
A Note on Statistics
The statistics portion of the program has been discontinued, due to it not being part of the
specification, and was initially probed as a supplementary component. Learning C++ and Functional
Programming at the same time caused more issues than expected, and it was more important to get
the base program working rather than trying to add additional unnecessary features.
Menu Choice
When moving to the main of the program. The first thing the program will do is ask for the user to
choose options from a menu. For this, another enumerated type was created to cover the possible
options that the user could enter into the system.
The parseMenuInput function is a totalised, pure function that uses an optional type, similar to the
way that parsePlayerToken function was implemented. In this case, the user input is matched via
regex to numerical string values. Where each value is mapped to the MenuChoice type.
std::optional<MenuChoice> parseMenuInput(std::string s) {
const std::regex oneRePattern("(1|Singleplayer|singleplayer)");
const std::regex twoRePattern("(2|Multiplayer|multiplayer)");
const std::regex threeRePattern("(3|Exit|exit)");
//const std::regex fourRePattern("4");
if (std::regex_match(s, oneRePattern)){
return Singleplayer;
} else if (std::regex_match(s, twoRePattern)) {
return Multiplayer;
} else if (std::regex_match(s, threeRePattern)) {
return Exit;
}
return std::nullopt;
}
Figure 25: Implementation of the parseMenuInput Function
The getPlayerMenuInput function returns a value of the MenuChoice type, given a valid input has
been provided by the user. An impure, yet totalised function of which the input is checked with the
“has_value” method after the input has been parsed by the previously defined “parseMenuInput”
function. If no valid input has been given, an error message is printed to standard output and the
getPlayerMenuInput function is called again.
MenuChoice getPlayerMenuInput() {
std::string input;
std::cout << "Please select a Menu Option:" << std::endl;
std::cout << "1. Singleplayer" << std::endl;
std::cout << "2. Multiplayer" << std::endl;
std::cout << "3. View Score" << std::endl;
std::cout << "4. Exit" << std::endl;
std::cin >> input;
std::optional<MenuChoice> mChoice = parseMenuInput(input);
if (mChoice.has_value()){
return mChoice.value();
} else {
std::cerr << "ERROR: Invalid menu choice, please try again." <<
std::endl;
return getPlayerMenuInput();
}
};
Figure 26: Implementation of the getPlayerMenuInput Function
Game Loop
Finally, to implement how the game is played in practice, a function referred to as “gameLoop” was
created.
}
Figure 27: The Implementation of the gameLoop Function
This function takes a previously initialised map created with the “initialiseGrid” function, as well as
the current player, which is the 0 index value of the players list. Which is initialised as a standard
vector as follows:
//list of players
std::vector<Player> players;
Figure 28: Implementation of the Player Sequence
To change the active player, the changeCurrentPlayer function is called, which takes in an argument
of a list of players and swaps the header with the next player in line.
//list of players, that can handle any number of players, each turn the
next player in the list from the previous becomes current, until the
end of the list.
void changeCurrentPlayer(std::vector<Player>& players) {
Player finalPlayer = players.back();
players.pop_back();
players.insert(players.begin(), finalPlayer);
std::cout << "Current Player is: " << players.at(0).getName() << ",
With Token: " << (tokenToString(players.at(0).getToken())) << "\n";
}
Figure 29: Implementation of Changing the Active Player in the System
There are several approaches to evaluating a win condition, one such method would be the use of
the “Magic Square” formula. Where the “sum of the 𝑛 numbers in any horizontal, vertical,
or main diagonal line is always the same number” (Weisstein, 2023.) In the case of a 3x3 grid, the
magic sum would be 15. And, for example, for a 5x5 grid the magic sum would be 65. This is also
referred to as the magic constant, which can be mathematically represented as the following:
However, though this method is mathematically sound. The implementation is rather difficult.
Another method is using “bit-boarding”, where everything can be stored as a binary integer and
evaluated in a single step, creating a time complexity of O(1). While incredibly efficient, this
approach was not discovered until late into the implementation of the system and would require
refactoring of a large part of the code to make it functional.
Therefore, a different approach was undertaken, where checkWinState takes three arguments, the
first being the current player (or player at index 0 of the players list), the previously initialised grid,
and the number of matching values required for a win condition to occur. In the case of a default
game of tic-tac-toe with a 3x3 grid, the matching values required are equivalent to 3, and the value
to match against is the Token of the player argument given to the function, acquired via the
getToken method of the player Class. Each column, row, or diagonal of this board have win
conditions if any 3 matching values are found in them individually. An easy way to evaluate this is to
initialise each of these as 0 and increment them when a match is found.
The pure function loops over each key within the grid map, and if a value paired to that key in the
map matches the given Token, then the previously initialised values are incremented by 1. If this
incrementation reaches the value of 3 then the loop ends, and the function returns True. Note that
this function contains nested loops, to correctly iterate to simulate columns, rows, and diagonals.
This does trade complexity of implementation for computational time, resulting in a time complexity
of O(𝑛2 ).
return true;
} else {
return false;
}
};
Figure 31: Implementation of the Function to Evaluate whether a Win Condition has been met
Testing
Both manual and automated tests must be undertaken to ensure features work as expected based on the gherkin specifications and other planning
components. The manual testing will use the software implementation to evaluate whether expected outputs are returned based on specific user inputs,
while the automated testing will use the Catch2 suite for unit tests and behavioural testing. Catch2 is being used as it is very simple to set up, only requiring
the importing of a header file containing the definitions, and a test case file that can access the Catch2 header file. Other options require the installation
and use of other external libraries and can be difficult to get working properly within an environment that has already been set-up without them.
Manual Testing
Test Case ID 1 Passed/Failed
Software Feature User Selects Singleplayer from the Menu Options
Steps Expected Actual Result
Run Program Tic-Tac-Toe program starts successfully and
prompts for a menu choice in the form of user
input.
In the prompt “Please Select a A valid input of “1” is entered by the user, the
Menu Option” Enter: 1 system validates this input and prompts for a
username.
35
In the prompt “Please Select a An invalid input of “5” is entered by the user,
Menu Option” Enter: 1 the system validates this input and prints an
error message “ERROR: Invalid menu choice ‘5’
please try again” to standard error, and
prompts the user to select a menu option.
In the prompt “Please Select A valid input of “1” is entered by the user,
a Menu Option” Enter: 1 the system validates this input and prompts
for a username.
In the prompt “Please enter A valid input of “Alex” is entered by the user,
your username: “ Enter: Alex the system validates this input and prompts
for a Token.
In the prompt “Please select A valid input of “O” is entered by the user,
a valid Token (O or X): “ the system validates this input and prints
Enter: O that “the singleplayer game has begun”, that
“the current player is Alex” and the playing
grid to standard output and prompts for a
location input.
In the prompt “Please Select a A valid input of “1” is entered by the user, the
Menu Option” Enter: 1 system validates this input and prompts for a
username.
In the prompt “Please enter A valid input of “Alex” is entered by the user,
your username: “ Enter: Alex the system validates this input and prompts for
a token.
In the prompt “Please select a An invalid input of “y” is entered by the user,
valid Token (O or X): “ Enter: O the system validates this input and prints an
error message “ERROR: Invalid token value,
please try again.” And prompts the user for a
token.
In the prompt “Please select A valid input of “O” is entered by the user,
a valid Token (O or X): “ the system validates this input and prints
Enter: O the playing grid to standard output and
prompts for a location input.
In the prompt “Enter the A valid input of “1” is entered by the user,
location (1-9) where you the system validates this input and places
want to place your token: “ the users token in the grid at the location
Enter: 1 and prints the updated board to standard
output.
In the prompt “Please Select A valid input of “1” is entered by the user,
a Menu Option” Enter: 1 the system validates this input and
prompts for a username.
In the prompt “Please select A valid input of “O” is entered by the user,
a valid Token (O or X): “ the system validates this input and prints
Enter: O the playing grid to standard output and
prompts for a location input.
In the prompt “Enter the A valid input of “5” is entered by the user,
location (1-9) where you the system validates this input and places
want to place your token: “ the users token in the grid at the location
Enter: 1 and prints the updated board to standard
output. The system prints that the game is
a draw. The system waits for user input to
exit the program.
Automated Testing
The automated testing is comprised of both unit tests, and behaviour driven development (BDD)
style tests. The automated testing mainly covers areas where user input could cause issues with the
system and ensuring that the system handles these inputs correctly. Not all cases will be covered in
this section, in-depth test cases can be found in Appendix A.
Unit Tests
The layout of unit testing in Catch2 requires each unit test to be labelled as TEST_CASE with a
descriptor. Where assertions can be of various definitions, such as REQUIRES, REQUIRES FALSE,
CHECK_THROWS and so on.
Checking the validity of the Token the player has chosen can be checked by asserting the value
returned from the parsePlayerToken function is of a valid Token type. Here the test case is described
as an assertion of whether the Token is valid, with the string in square brackets being what is
expected as a result of the test case. In this event, a valid token should be returned.
It is also important to test that invalid inputs return the correct values. In the case of a Token, if the
user inputs anything that cannot be matched to X or O, then a null value is returned instead.
The same Unit tests are undertaken for Menu Choice and when selecting a location on the grid to
place a Token, as they are similar in structure they will not be listed here and can instead be found in
Appendix A.
When initialising the grid, the map structure must contain all the values of coordinate as the keys,
and all values as an optional null value, as a cell (or square) of the grid should be able to contain null,
X, or O as its value. By asserting the grid size, and looping through the grid, the keys and values of
the newly created grid can be checked one by one. The unit test should return as true.
Furthermore, it is possible to check if the win state is functioning correctly by running an assertion
on the checkWinState() function. By creating a new grid, and manually setting the values of the map
of Keys A, B, and C to the same Token, and creating a new Player object who also has been assigned
that Token.
A similar method can be followed for the draw state, though since the checkDrawState checks
against the number of turns taken no player data needs to be passed to the function. By setting
every value in the grid to a valid Token, where no win condition subsets are met, the draw state
should evaluate as true.
SCENARIO( "Selecting the Game Type to be Played (User chooses to start a new
2-player game)", "[gameType]" ) {
GIVEN( "the human player has specified the game type they would like to
play as a valid string “1” from the game-type menu" ) {
std::optional<MenuChoice> mChoice = parseMenuInput("2");
WHEN( "player menu choice is confirmed" ) {
if (mChoice.has_value()){
REQUIRE(mChoice.value() == Multiplayer);
}
THEN( "the game begins and prompts the player to select their
token via stdout" ) {
}
}
}
}
Figure 38: BDD Scenario of a User Selecting a Game Type to be Played
The setup for each of these scenarios requires that certain values be set manually, so that input is
not required for them to run. This can be seen above in the example line
“std::optional<MenuChoice> mChoice = parseMenuInput(“2);”, where in the actual program
getPlayerMenuInput would be the function that is called to obtain the “2” value.
For another example of a BDD Scenario, the user selects an invalid Token to play with. Again, being
like the Gherkin specification, by evaluating if the string given can be parsed as a Token the scenario
can be automated and be made to return an error message.
GIVEN( "the human player has specified the Token they would like to play
with as an invalid string 'y'") {
Token t;
std::string userInput = "y";
WHEN( "player menu choice is confirmed" ) {
REQUIRE_FALSE( parsePlayerToken(userInput) == t);
}
THEN( "the system prints ERROR: Invalid token value, please try
again. to standard out, and the user is prompted to pick a valid token." )
{
}
}
}
Figure 39: BDD Scenario of a User Selecting an Invalid Token
A final example that will be shown is a user adding a Token to a valid grid location. Here, objects
must be first initialised, such as the grid, the token to check against, and the player. When the
playerGridChoiceValue function is called, if the casted coordinate value is nullopt then the new
Token value is assigned to it.
GIVEN( "the human player has specified a valid coordinate choice '1'") {
Grid g = initializeGrid();
Token tkn = X();
Player player1(tkn, "Alex", Statistics(0,0,0));
int input = playerGridChoiceValue(1);
coord c = static_cast<coord>(input - 1);
WHEN( "player menu choice is confirmed" ) {
for (const auto& [key, value] : g) {
if (g[c] == std::nullopt) {
g[c] = tkn;
}
REQUIRE (g[A] == tkn);
}
THEN( "the system prints the updated board, and prompts the next
player for their turn." ) {
}
}
}
}
Figure 40: BDD Scenario of Adding a Token to an Empty Grid Square
This shows that all tests validated and returned the desires result over 95 assertions across 19
different test cases.
Version control is a system that keeps a record of “changes to a file or set of files over time”(Chacon
and Straub, 2022) so that specific versions can be recalled later in time. For example, if a merge
request occurred that inadvertently broke functionality within the program it would be possible to
revert to a previous version of the repository before these changes are made. However, there are
many approaches to version control. Where something simple like copying files to another directory
may be a common approach; automation of this approach through tools like git are preferable as the
former method is “incredibly error prone”(Chacon and Straub, 2022.) When working as a team,
version control systems like Git are incredibly useful to track changes to a codebase. When using
software such as GitHub, “version control identifies the problem areas so that team members can
quickly revert changes to a previous version, compare changes, or identify who committed the
problem code through the revision history” (GitLab, 2023.)
Git facilitates several functionalities within its toolset. One such functionality is “clone”. Cloning a
repository will place the data in a new directory, while simultaneously creating “remote-tracking
branches for each branch in the cloned repository (visible using git branch --remotes)” (Git, 2022.)
This is quite often the first step when attempting to modify a repository, as It allows for changes to
be main without affecting the master/main repository/branch and the files therein. Allowing for
code to be simultaneously edited while still having a previous version still being live.
Once changes have been made, it is possible to “commit” these changes to the main branch, a
commit is effectively a save of the version of the codebase at that point in time. Each commit can be
used as a point to go back to at a later stage of the product in the event the user wishes to make a
change, or encounters a bug at some point in the future after a commit.
Push and pull are somewhat similar terms in Git but have a distinct difference in their use. A push
would take code create on a local machine and send it to the Git repository. A pull would update the
local machine with the version stored in Git. When performing a pull request what is effectively
happening is a team member asking for other team members to take the changes made and review
them before merging them to the main repository.
Finally, merging is used to combine changes together within a single branch. However, merging will
cause a conflict if the file being updated has been modified outside of the current merge. For
example, if one team member tries to merge a change to a file, but that file has been removed by
another team member during the interim then a merge conflict will occur until the issue is resolved.
If a version control system like git was used for the Tic-Tac-Toe project it would allow for storage of
different stages of the program as it progressed. Allowing for easy resolution of issues where a bug
was introduced, or data loss occurred. A very basic form of version control is saving files in different
folders manually at regular intervals, but using a software-based solution both saves time and
ensures consistent file backups. Moving forward, it would be beneficial to use software solutions
such as GitHub to allow for easier management of program versions. Giving access to the project
from anywhere from online storage, easily cloned in to an Integrated Development Environment.
https://github.com/torbjoernk/CppKoans
The commit history for CppKoans can be easily accessed by clicking the commits symbol when
navigating GitHub, underlined in red in figure X.
Commits are listed in order, with the most recent commit being at the top of the list. By scrolling
down, or selecting older, it is possible to see the oldest commit made to the repository. In the case
of the CppKoans repository, it can be seen that the oldest commit was titled “initial commit” by the
user torbjoernk on the 20th April 2012.
Within this commit, the README.md file was modified, to give a description of what the repository
is about. As well as the .gitignore file, which “specifies intentionally untracked files that Git should
ignore” [https://git-scm.com/docs/gitignore]. It can be seen that all filesnames ending in .slo, .lo, .o,
.so, .lai, .la, and .a are requested to be ignored by git. Any files that end with the former will not be
listed in documented changes going forward.
https://github.com/torbjoernk/CppKoans/commit/edc687c7361293fb4fdbe182aaf8cafa993a5d56
There are more substantial changes made to the repository. Torbjoernk made this commit on the
26th April 2012 to the main branch. Within this commit several koans were defined to handle other
types that had not already been covered by the codebase already. As can be seen in the
koan03_further_types.hpp file, this koan handles string values.
This change also modifies other files within the codebase to accommodate the new koan, such as
including the koan_further_types.hpp (header) file in the all_koans.hpp file, and increasing the
number of tests in the koan02_character_types.hpp file to allow for the inclusion of the further
types koan.
Within the issue tracker for the project, which can be navigated to via the project navigation bar it
can be seen that there are three issues, only one of which has been closed. Looking at the open and
closed issues, it can be seen that the repository owner has not responded to any issues raised for the
entirety of the project lifespan. This is likely in part to the current README.md stating not to open
tickets on GitHub and instead post to the Cucumber discussion group.
The closed issue, submitted by the user SuperCool83 on the 1st of August 2018 is an issue with
getting cmake to install correctly. Which is unrelated to the project scope. The user who submitted
the issue closed it themselves on the 3rd August 2018, managing to resolve their issue within a Linux
environment.
In terms of Pull requests it can be seen that there have been six in total across the lifespan of the
repository, of which 3 are still open, and 3 have been closed. The pull request submitted by the user
mewmew on the 16th September 2015 was merged on the 18th November 2015 by torbjoaernk into
the main repository. This pull request refers to a “typo” being made in the Koan_01 variable, this
syntax was modified by the pull requester to change the type from an integer to a float.
Figure 47: The Modified Code as Shown in the Pull Request by user mewmew
APPENDIX A
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
#include "game.h"
#include "playingGrid.h"
//reference material
//https://github.com/catchorg/Catch2/blob/v2.x/docs/tutorial.md#top
//Check grid initial state is valid (all keys are of Coord, all values are
nullopt)
TEST_CASE("Grid initialisation is valid for 3x3" "[Grid]") {
Grid g = initializeGrid();
REQUIRE (g.size() == 9);
for (const auto& [key, value] : g) {
REQUIRE (key >= A);
REQUIRE (key <= I);
REQUIRE (value == std::nullopt);
}
}
//Check user can place a Token in a grid coordinate with a nullopt value
TEST_CASE("Adding a Token to an empty Grid Square" "[Grid]") {
Grid g = initializeGrid();
Token tkn = X();
Player player1(tkn, "Alex", Statistics(0,0,0));
int input = playerGridChoiceValue(1);
coord c = static_cast<coord>(input - 1);
for (const auto& [key, value] : g) {
if (g[c] == std::nullopt) {
g[c] = tkn;
}
REQUIRE (g[A] == tkn);
}
GIVEN( "the human player has specified the game type they would like to
play as a valid string “2” from the game-type menu" ) {
std::optional<MenuChoice> mChoice = parseMenuInput("2");
WHEN( "player menu choice is confirmed" ) {
if (mChoice.has_value()){
REQUIRE(mChoice.value() == Multiplayer);
}
THEN( "the system prompts the player to select their token via
stdout" ) {
}
}
}
GIVEN( "the human player has specified the game type they would like to
play as an invalid string “5” from the game-type menu" ) {
std::optional<MenuChoice> mChoice = parseMenuInput("5");
WHEN( "player menu choice is confirmed" ) {
REQUIRE(mChoice == std::nullopt);
}
}
}
}
GIVEN( "the human player has specified the Token they would like to play
with as a valid string 'x'") {
Token t;
std::string userInput = "x";
WHEN( "player token choice is confirmed" ) {
REQUIRE( parsePlayerToken(userInput) == t);
}
THEN( "the game either prompts the next user for their name, or
the game begins" ) {
}
}
}
GIVEN( "the human player has specified the Token they would like to play
with as an invalid string 'y'") {
Token t;
std::string userInput = "y";
WHEN( "player token choice is confirmed" ) {
REQUIRE_FALSE( parsePlayerToken(userInput) == t);
}
THEN( "the system prints ERROR: Invalid token value, please try
again. to standard out, and the user is prompted to pick a valid token." )
{
}
}
}
GIVEN( "the human player has specified a valid coordinate choice '1'") {
Grid g = initializeGrid();
Token tkn = X();
Player player1(tkn, "Alex", Statistics(0,0,0));
int input = playerGridChoiceValue(1);
coord c = static_cast<coord>(input - 1);
WHEN( "player grid choice is confirmed" ) {
for (const auto& [key, value] : g) {
if (g[c] == std::nullopt) {
g[c] = tkn;
}
REQUIRE (g[A] == tkn);
}
THEN( "the system prints the updated board, and prompts the next
player for their turn." ) {
}
}
}
}
GIVEN( "the human player has specified a valid coordinate choice '1'") {
Grid g = initializeGrid();
Token tkn = X();
Player player1(tkn, "Alex", Statistics(0,0,0));
int input = playerGridChoiceValue(1);
coord c = static_cast<coord>(input - 1);
WHEN( "player grid choice is confirmed" ) {
for (const auto& [key, value] : g) {
if (g[c] != std::nullopt) {
REQUIRE_FALSE(g[A] == tkn);
}
}
//BDD user attempts to add a Token to an invalid grid square value Scenario
SCENARIO( "Adding a Token to an invalid Grid Square", "[Token]" ) {
GIVEN( "the human player has specified an invalid coordinate choice '10'")
{
int input = 10;
WHEN( "player grid choice is confirmed" ) {
CHECK_THROWS(playerGridChoiceValue(10));
THEN( "the system prints 'That location already contains a token.
Please choose a different location.' to standard out, and prompts the user to
choose again." ) {
}
}
}
}
References
Meza, F. , 2018. The Value of Code Documentation. Available at:
https://www.olioapps.com/blog/the-value-of-code-documentation [Accessed: 17/04/2023]
Chacon, S. and Straub, B. (2022) About Version Control. Available at: https://git-
scm.com/book/en/v2/Getting-Started-About-Version-Control [Accessed: 17/05/2023]