So, I was bored and wanted to re-visit a programming language I haven't touched in years, C++. I am a bit rusty with my C++, but eventually I managed to implement a simple, command-line
Tic Tac Toe (or Noughts and Crosses for us Commonwealth folk).
I deliberately avoided cooking up yet another Minimax TicTacToe (and I must admit I'm not very strong in recursion techniques). I just implemented some common-sense rules and came up with this code:
// Simple command line TicTacToe game
// (C) 2011 V.Harishankar
// Released under the GNU GPL v3
#include <iostream>
#include <cstdlib>
#include <ctime>
class TicTacToeGame
{
protected:
enum Turn { PLAYER = 0, AI = 1 };
// Represents the current turn
Turn current_turn;
// Character (O or X for player)
char player_char, ai_char;
// Represents the board
char board[3][3];
// Clear the board
void initialize_board ();
// Display the board
void print_board ();
// Chekc if all slots are filled
bool all_slots_filled ();
// Find a winning position for given player (O or X) and fill the
// row and column parameter
void find_winning_position (char noughtorcross, int &row, int &col);
// Check if the specified player has won (O or X)
bool check_if_won (char ch);
// Human player move
void player_move ();
// Computer player move
void ai_move ();
// Game loop
void play_game ();
public:
// Constructor
TicTacToeGame ()
{
// Note that these values are flipped when the game is initialized
// for the first time
current_turn = PLAYER;
player_char = 'X';
ai_char = 'O';
main_menu ();
}
// Main game menu loop
void main_menu ();
};
void TicTacToeGame::player_move ()
{
int row, col;
// Get the row and column from human player
while (1)
{
std::cout << "Enter row for " << player_char << " [1..3]: ";
std::cin >> row;
std::cout << "Enter col for " << player_char << " [1..3]: ";
std::cin >> col;
row -= 1;
col -= 1;
// Row and column must be valid
if ((row >= 3 || row < 0) || (col >= 3 || col < 0))
std::cout << "Invalid row/column.\n";
else
{
// if it is a blank this is a valid move
if (board[row][col] == ' ')
break;
else
std::cout << "Cell is occupied.\n";
}
}
// Set the player move
board[row][col] = player_char;
}
void TicTacToeGame::find_winning_position (char noughtorcross, int &row, int &col)
{
// check if specified character (nought or cross) can win along any row
int i, j, chcount, emptycol, emptyrow;
// Loop through rows
for (i = 0; i < 3; i ++)
{
// Chcount -number of either O or X along the row
chcount = 0;
emptycol = -1;
// Loop through columns
for (j = 0; j < 3; j ++)
{
// If the specified player is occupying the cell, add to chcount
if (board[i][j] == noughtorcross)
chcount ++;
// If specified cell is blank mark the column as empty
else if (board[i][j] == ' ')
emptycol = j;
}
// If there are two of the specified player in the row and there is an
// empty cell, then the empty cell is the winning position
if (chcount == 2 && emptycol != -1)
{
row = i;
col = emptycol;
return;
}
}
// The same logic as above applies for columns
// check if specified character can win along any column
for (j = 0; j < 3; j ++)
{
chcount = 0;
emptyrow = -1;
for (i = 0; i < 3; i ++)
{
if (board[i][j] == noughtorcross)
chcount ++;
else if (board[i][j] == ' ')
emptyrow = i;
}
if (chcount == 2 && emptyrow != -1)
{
row = emptyrow;
col = j;
return;
}
}
// Check if specified character can win along diagonal top-left to bottom-right
chcount = 0;
emptyrow = -1;
emptycol = -1;
// Loop through the diagonal
for (i = 0; i < 3; i ++)
{
if (board[i][i] == noughtorcross)
chcount ++;
else if (board[i][i] == ' ')
emptyrow = emptycol = i;
}
if (chcount == 2 && emptyrow != -1 && emptycol != -1)
{
row = emptyrow;
col = emptycol;
return;
}
// Check if specified character can win along diagonal top-right to bottom-left
chcount = 0;
emptyrow = -1;
emptycol = -1;
// Loop through the diagonal
for (i = 0; i < 3; i ++)
{
if (board[i][2-i] == noughtorcross)
chcount ++;
else if (board[i][2-i] == ' ')
{
emptyrow = i;
emptycol = 2 - i;
}
}
if (chcount == 2 && emptyrow != -1 && emptycol != -1)
{
row = emptyrow;
col = emptycol;
return;
}
}
void TicTacToeGame::ai_move ()
{
// Check if AI can win in this move
int row=-1, col=-1;
// See if AI can win in this move
find_winning_position (ai_char, row, col);
// if AI can win, play the winning move
if (row != -1 && col != -1)
{
board[row][col] = ai_char;
return;
}
row = col = -1;
// see if player can win next move and block
// if possible
find_winning_position (player_char, row, col);
if (row != -1 && col != -1)
{
board[row][col] = ai_char;
return;
}
// check if the centre can be played
if (board[1][1] == ' ')
{
board[1][1] = ai_char;
return;
}
// Corner positions
int corners[4][2] = {{0, 0}, {0, 2}, {2, 0}, {2, 2}};
// If any one or more corners are blank
if (board[0][0] == ' ' || board[0][2] == ' ' || board[2][0] == ' ' || board[2][2] == ' ')
{
// randomly select corner
while (1)
{
int corner = rand () % 4;
row = corners[corner][0];
col = corners[corner][1];
if (board[row][col] == ' ')
{
board[row][col] = ai_char;
return;
}
}
}
// Edge positions
int edges[4][2] = {{0, 1}, {1, 0}, {1, 2}, {2, 1}};
// if any one or more edges are blank
if (board[0][1] == ' ' || board[1][0] == ' ' || board[1][2] == ' ' || board[2][1] == ' ')
{
// randomly select edge
while (1)
{
int edge = rand () % 4;
row = edges[edge][0];
col = edges[edge][1];
if (board[row][col] == ' ')
{
board[row][col] = ai_char;
return;
}
}
}
}
bool TicTacToeGame::check_if_won (char ch)
{
bool win = false;
// Check across
int i;
for (i = 0; i < 3; i ++)
if (board[i][0] == ch && board[i][1] == ch && board[i][2] == ch)
{
win = true;
break;
}
// Check down
for (i = 0; i < 3; i ++)
if (board[0][i] == ch && board[1][i] == ch && board[2][i] == ch)
{
win = true;
break;
}
// check diagonals
if (board[0][0] == ch && board[1][1] == ch && board[2][2] == ch)
win = true;
if (board[0][2] == ch && board[1][1] == ch && board[2][0] == ch)
win = true;
return win;
}
bool TicTacToeGame::all_slots_filled ()
{
bool filled = true;
for (int i=0; i < 3; i++)
{
for (int j=0; j < 3; j++)
if (board[i][j] == ' ')
filled = false;
if (filled == false)
break;
}
return filled;
}
void TicTacToeGame::print_board ()
{
std::cout << "\n";
for (int i=0; i < 3; i ++)
{
for (int j=0; j < 3; j++)
{
std::cout << board[i][j] << "\t";
if (j < 2)
std::cout << "|\t";
}
if (i < 2)
std::cout << "\n---------------------------------\n";
}
std::cout << "\n";
}
void TicTacToeGame::play_game ()
{
// Initialize the board first
initialize_board ();
// If player char was X then make it O
if (player_char == 'X')
{
player_char = 'O';
ai_char = 'X';
current_turn = AI;
}
else
{
ai_char = 'O';
player_char = 'X';
current_turn = PLAYER;
}
// So long as gameover is false, keep playing
bool gameover = false;
while (! gameover)
{
print_board ();
// if turn is that of the player
if (current_turn == PLAYER)
{
player_move ();
current_turn = AI;
}
else
{
std::cout << "\nComputer played: \n";
ai_move ();
current_turn = PLAYER;
}
// check for winner nought
if (check_if_won ('O'))
{
print_board ();
std::cout << "\nNoughts won! Game over...\n";
break;
}
else if (check_if_won ('X'))
{
print_board ();
std::cout << "\nCrosses won! Game over...\n";
break;
}
else if (all_slots_filled ())
{
print_board ();
std::cout << "\nGame drawn.\n";
break;
}
}
}
void TicTacToeGame::initialize_board ()
{
for (int i = 0; i < 3; i++)
for (int j=0; j < 3; j++)
board[i][j] = ' ';
}
void TicTacToeGame::main_menu ()
{
while (1)
{
std::cout << "\n -------------------------";
std::cout << "\n Welcome to TicTacToe game";
std::cout << "\n -------------------------";
std::cout << "\n 1. Play";
std::cout << "\n 2. Exit";
char ch;
std::cout << "\n Your choice: ";
std::cin >> ch;
switch (ch)
{
case '1': play_game (); break;
case '2': return;
}
}
}
int main ()
{
// initialize randomizer
srand (time (NULL));
TicTacToeGame game;
return 0;
}
Yes, it's not the prettiest code and it's probably more verbose than you would expect, but the logic is relatively simple to follow as it uses game-specific rules with a little randomization. A lot of Tic Tac Toe tutorials prefer the more sophisticated and general Minimax algorithm (which is really useful for more complex decision algorithms based on a zero-sum formula), but I found few examples with just a normal "AI".
Note that the above logic is not perfect. The AI is relatively dumb in that it doesn't start off with a goal of winning, trying instead to pick up desirable positions (centre, corners, then the other squares), but will win or block the human player from winning if it sees the chance. In actuality, this approximation doesn't affect the game all that much because of the limited scope of Tic Tac Toe board and the duration of gameplay. So far, I've not been able to beat it yet, but I am not sure whether it is an unbeatable strategy.
You can take the above code, save it in a file called
tictactoe.cpp, compile it with your favourite C++ compiler and run the resultant executable from the command line to test the program.
Your feedback is much appreciated.
2 comment(s)
Leave a comment »What's your take on Santhosh Pandit?
Comment by Vinu Varghese (visitor) on Mon, 24 Oct 2011 @ 10:36 IST #
Comment by Hari (blog owner) on Mon, 24 Oct 2011 @ 14:46 IST #