I see a number of things that would improve the code:
Use objects
C++ is an object-oriented language, and your code would benefit from being more object-oriented. When you have multiple functions that all take a grid
as an argument, this is a clear indication that the functions should probably be member functions and that grid
should be an object. Here's the class I came up with, based on your code. Note that the constructor takes an argument which defaults to 3. That's the size of one dimension of the square board, which can now be altered to be either the usual 3x3 grid (by default) or made any arbitrary size. More on that later.
class TicTacToe
{
private:
unsigned _dim;
unsigned free;
std::vector<char>grid;
// applies turn and returns true unless square is already occupied
bool Apply(char player, int position);
public:
TicTacToe(unsigned dim=3) : _dim(dim), free(_dim*_dim), grid(free,'-') {}
bool Full() const { return free==0; }
//Simple Display
void Display() const;
void Turn(char player);
bool Win(char player) const;
};
Separate input and output from core logic
Your Turn
function does three things: it gets input from the user, it validates the input and then it applies the turn to the grid. It's generally better design to separate things so that each function does just one or maybe two closely related things.
When I changed your code, the Turn
code is only responsible for getting the input, delegating the actual application of the move to a private member function named Apply
:
bool TicTacToe::Apply(char player, int position)
{
if (grid[position] != '-')
return false;
grid[position] = player;
--free;
return true;
}
Consider extensions and generalizations
While your Win
function is not incorrect as written, it is not flexible because all of the numbers are hard-coded. Consider that one might want a 4x4 or larger grid, in which it might be good to have a more general method for checking that doesn't have everything hard-coded.
bool TicTacToe::Win(char player) const
{
// check for row or column wins
for(unsigned i = 0; i < _dim; ++i){
bool rowwin = true;
bool colwin = true;
for (unsigned j=0; j < _dim; ++j) {
rowwin &= grid[i*_dim+j] == player;
colwin &= grid[j*_dim+i] == player;
}
if (colwin || rowwin)
return true;
}
// check for diagonal wins
bool diagwin = true;
for (unsigned i=0; i < _dim; ++i)
diagwin &= grid[i*_dim+i] == player;
if (diagwin)
return true;
diagwin = true;
for (unsigned i=0; i < _dim; ++i)
diagwin &= grid[i*_dim+(_dim-i-1)] == player;
return diagwin;
}
The same thing can also be applied to your Display
routine.
void TicTacToe::Display() const
{
//Creating a onscreen grid
std::cout << ' ';
for (unsigned i=1; i<=_dim; ++i)
std::cout << " " << i;
for(unsigned i = 0; i < _dim; i++){
std::cout << "\n" << static_cast<char>('A'+i) << " ";
for(unsigned j = 0; j < _dim; j++)
std::cout << " " << grid[i*_dim+j] << " ";
}
std::cout << "\n\n";
}
Recognize a draw
The code doesn't recognize when the game ends in a draw. An easy way to do so is to simply count how many free spaces are left. Note that this is only legitimately a tie if the last move hasn't actually been a winning move, so as written, this should only be called after we check for a win. This could be further improved by not relying on the calling function to perform this check.
bool Full() const { return free==0; }
Sanitize user input
User input is inherhently suspect. Make sure that you always validate user input before using it. In this case, consider that if the user enters a number instead of a letter, the code gets stuck in an infinite loop. Worse, the input is not range-checked and could attempt to write to memory far outside the grid
vector.
void TicTacToe::Turn(char player)
{
char row = 0;
char column = 0;
unsigned position = 0;
bool applied = false;
std::cout <<"\n" << player << ": Please play. \n";
while(!applied) {
std::cout << "Row(1,2,3,...): ";
std::cin >> row;
std::cout << player << ": Column(A,B,C,...): ";
std::cin >> column;
position = _dim*(column-'A')+(row-'1');
if (position < grid.size()) {
applied = Apply(player, position);
if (!applied)
std::cout << "Already Used. Try Again. \n";
} else {
std::cout << "Invalid position. Try again.\n";
}
}
std::cout << "\n\n";
}
Highlight loop termination conditions
When you have a loop such as while(true)
it implies that the loop never ends, but in the case of your loop in main
, the code actually ends when one player wins. In the improved, code, it also ends if there is a tie.
int main()
{
TicTacToe ttt;
const char players[2] = {'X', 'O'};
int player = 1;
bool win = false;
bool full = false;
while(!win && !full){
player = 1-player;
ttt.Display();
ttt.Turn(players[player]);
win = ttt.Win(players[player]);
full = ttt.Full();
}
ttt.Display();
if (win) {
std::cout << "\n" << players[player] << " is the Winner!\n";
} else {
std::cout << "\nTie game!\n";
}
}