Hi everyone. I finished Tic Tac Toe for the second time, this time using classes.
I would like your opinions on the game design, things I'm not doing well, etc.
The game constists of 3 base classes and 2 derived ones.
Those are:
TicTacToe: the game
Board: represents a board
GenericPlayer: a generic player (abstract class)
HumanPlayer and
ComputerPlayer: these are derived from generic player.
<br/>
TicTacToe.h
#ifndef TIC_TAC_TOE_H
#define TIC_TAC_TOE_H
#include <cassert> // used for debugging
#include <iostream>
#include <string>
#include <ctime> // used to a random number to decide which player chooses piece first
//constants needed
#include "constants.h"
//forward class declarations
class Board;
class GenericPlayer;
class HumanPlayer;
class ComputerPlayer;
class TicTacToe
{
public:
TicTacToe(int numberOfPlayers = 1);
~TicTacToe();
void Play(); // play a game of TicTacToe
void Instructions();
bool Tie() const; // true if it's a tie
char Win() const; // returns the winning piece if there's a winner, or empty if there isn't a winner
private:
void SetPieces(); // sets the pieces, e.g. X for p1
Board *board;
GenericPlayer *p1, *p2;
};
#endif //TIC_TAC_TOE_H
TicTacToe.cpp
#include "TicTacToe.h"
#include "Board.h"
#include "Player.h"
TicTacToe::TicTacToe(int numberOfPlayers)
{
board = new Board();
assert(numberOfPlayers == 1 || numberOfPlayers == 2);
if(numberOfPlayers == 1)
{
p1 = new HumanPlayer();
p2 = new ComputerPlayer();
dynamic_cast <ComputerPlayer*> (p2)->SetBoard(board);
}
else
{
p1 = new HumanPlayer();
p2 = new HumanPlayer();
}
std::cout << "\n";
}
TicTacToe::~TicTacToe()
{
//free the memory allocated by the GenericPlayer pointers
delete p1;
delete p2;
//and the board
delete board;
}
void TicTacToe::Instructions()
{
std::cout << "Welcome to Tic Tac Toe!!\n\n";
board->SetField(0, '1');
board->SetField(1, '2');
board->SetField(2, '3');
board->SetField(3, '4');
board->SetField(4, '5');
board->SetField(5, '6');
board->SetField(6, '7');
board->SetField(7, '8');
board->SetField(8, '9');
std::cout << "You enter a value from 1 to 9 to indicate where you want to play, as illustratedbelow:\n\n";
std::cout << *board;
board->Clear();
}
void TicTacToe::Play()
{
SetPieces();
GenericPlayer *whoPlays = 0;
if(p1->GetPiece() == X)
whoPlays = p1;
else
whoPlays = p2;
//THE GAME LOOP
int choice;
while(Win() == EMPTY && !Tie())
{
// if it's a human player that's playing, display the text
if(dynamic_cast <HumanPlayer*> (whoPlays))
std::cout << whoPlays->GetName() << ", it's your turn. [" << whoPlays->GetPiece() << "] 1-9: ";
// loop until the player who plays chooses a field which is empty
do
{
choice = whoPlays->makeMove();
if(board->GetField(choice) != EMPTY)
std::cout << "Hey, you can't place it there..!\n";
} while(board->GetField(choice) != EMPTY);
std::cout << whoPlays->GetName() << " chooses " << choice + 1 << ".\n\n";
board->SetField(choice, whoPlays->GetPiece());
std::cout << *board;
// it's the turn of the other player
if(whoPlays == p1)
whoPlays = p2;
else
whoPlays = p1;
}
if(Win() == p1->GetPiece())
std::cout << p1->GetName() << " wins! Sorry " << p2->GetName() << ", better luck next time...\n";
if(Win() == p2->GetPiece())
std::cout << p2->GetName() << " wins! Sorry " << p1->GetName() << ", better luck next time...\n";
}
//returns true if it's a tie
bool TicTacToe::Tie() const
{
//if the fields that do not contain EMPTY are ROWS * COLUMNS and no one has won, it's a tie
if(board->NumFieldsNotChar(EMPTY) == ROWS * COLUMNS)
{
std::cout << "It's a tie!!\n";
return true;
}
return false;
}
char TicTacToe::Win() const
{
for(int i = Board::HORIZONTAL; i <= Board::DIAGONAL; ++i)
{
// i here needs to be casted to Board::DIRECTION
if(board->Consecutive3((Board::DIRECTION)i, p1->GetPiece()))
return p1->GetPiece();
if(board->Consecutive3((Board::DIRECTION)i, p2->GetPiece()))
return p2->GetPiece();
}
return EMPTY;
}
void TicTacToe::SetPieces()
{
srand((unsigned)time(0));
int p1First = rand() % 2; // if 1, p1 plays first. if 0, p2 plays first (gets X, which goes first)
if(p1First)
{
std::cout << p1->GetName() << " plays first. [X]\n\n";
p1->SetPiece(X);
p2->SetPiece(O);
}
else
{
std::cout << p2->GetName() << " plays first. [X]\n\n";
p1->SetPiece(O);
p2->SetPiece(X);
}
}
This is the board class.
Board.h
#ifndef BOARD_H
#define BOARD_H
#include <iostream>
#include <cassert>
#include "constants.h"
#include "functions.h"
class Board
{
public:
Board();
void Clear(); // clear the table (set all fields to EMPTY, defined in constants.h
friend std::ostream &operator <<(std::ostream &os, const Board &b);
//set a field to c
void SetField(int row, int column, char c);
void SetField(int index, char c);
char GetField(int row, int column) const;
char GetField(int index) const;
//returns the number of fields that do not contain c
int NumFieldsNotChar(char c);
enum DIRECTION {HORIZONTAL, VERTICAL, DIAGONAL};
// returns true if c is found 3 times horizontal, vertical or diagonal
//e.g. Consecutive3(Board::HORIZONTAL, 'X')
// O X O
// X O X
// X X X
// it will return true
bool Consecutive3(DIRECTION direction, char c);
private:
char table[COLUMNS][ROWS];
};
#endif //BOARD_H
Board.cpp
#include "Board.h"
Board::Board()
{
Clear();
}
void Board::Clear()
{
for(int i = 0; i < ROWS; ++i)
for(int j = 0; j < COLUMNS; ++j)
table[j] = EMPTY;
}
std::ostream &operator << (std::ostream &os, const Board &b)
{
for(int i = 0; i < ROWS; ++i)
{
for(int j = 0; j < COLUMNS; ++j)
{
std::cout << b.table[j];
if(j != ROWS - 1)
std::cout << " | ";
}
std::cout << "\n";
if(i != ROWS - 1)
{
for(int k = 0; k < ROWS; ++k)
{
if(k != ROWS - 1)
std::cout << "-----";
}
}
std::cout << "\n";
}
return os;
}
void Board::SetField(int row, int column, char c)
{
boundCheck(0, ROWS, row);
boundCheck(0, ROWS, column);
table[row][column] = c;
}
void Board::SetField(int index, char c)
{
boundCheck(0, ROWS * ROWS, index);
int i, j;
convert1Dto2D(index, i, j);
table[j] = c;
}
char Board::GetField(int row, int column) const
{
boundCheck(0, ROWS, row);
boundCheck(0, ROWS, column);
return table[row][column];
}
char Board::GetField(int index) const
{
boundCheck(0, ROWS * ROWS, index);
int i, j;
convert1Dto2D(index, i, j);
return table[j];
}
int Board::NumFieldsNotChar(char c)
{
int count = 0;
for(int i = 0; i < ROWS; ++i)
for(int j = 0; j < COLUMNS; ++j)
if(table[j] != c)
++count;
return count;
}
bool Board::Consecutive3(Board::DIRECTION direction, char c)
{
switch(direction)
{
case Board::HORIZONTAL:
for(int i = 0; i < ROWS; ++i)
{
if(GetField(i, 0) == GetField(i, 1) && GetField(i, 0) == GetField(i, 2) )
if(GetField(i, 0) == c)
return true;
}
case Board::VERTICAL:
for(int i = 0; i < COLUMNS; ++i)
{
if(GetField(0, i) == GetField(1, i) && GetField(0, i) == GetField(2, i) )
if(GetField(0, i) == c)
return true;
}
case Board::DIAGONAL:
if(GetField(0, 0) == GetField(1, 1) && GetField(0, 0) == GetField(2, 2) )
if(GetField(0, 0) == c)
return true;
if(GetField(0, 2) == GetField(1, 1) && GetField(0, 2) == GetField(2, 0) )
if(GetField(0, 2) == c)
return true;
}
// horizontal
return false;
}
The player classes:
Player.h
#ifndef PLAYER_H
#define PLAYER_H
#include <iostream> // used to get HumanPlayer name (getline() )
#include <string> // used to get HumanPlayer name
#include <ctime> // used for random input from ComputerPlayer
#include "constants.h"
#include "functions.h"
class Board; // used by ComputerPlayer
class GenericPlayer // abstract class
{
public:
virtual int makeMove() = 0;
const std::string & GetName() const;
void SetPiece(char c);
char GetPiece() const;
protected:
char piece; // the piece a player has, X or O
std::string name;
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
class HumanPlayer : public GenericPlayer
{
public:
//constructors
HumanPlayer(const std::string &_name);
HumanPlayer();
virtual int makeMove();
};
////////////////////////////////////////////////////////////////////////////////////////////////////
class ComputerPlayer : public GenericPlayer
{
public:
ComputerPlayer();
virtual int makeMove();
void SetBoard(const Board *const board);
private:
const Board *pBoard;
};
#endif //PLAYER_H
Player.cpp
#include "TicTacToe.h"
#include "Board.h"
#include "Player.h"
/******************************** GenericPlayer ***********************************************************/
void GenericPlayer::SetPiece(char c)
{
piece = c;
}
char GenericPlayer::GetPiece() const
{
return piece;
}
const std::string & GenericPlayer::GetName() const
{
return name;
}
/************************************* HumanPlayer ********************************************************/
HumanPlayer::HumanPlayer(const std::string &_name)
{
name = _name;
}
HumanPlayer::HumanPlayer()
{
std::cout << "Enter name: ";
std::getline(std::cin, name);
}
int HumanPlayer::makeMove()
{
int temp;
do
{
temp = getInt();
if(temp <= 0 || temp > ROWS * COLUMNS)
std::cout << temp << " is not between 1 and 9. Enter again: ";
}while(temp <=0 || temp > ROWS * COLUMNS);
return --temp;
}
/**************************************** ComputerPlayer **************************************************/
ComputerPlayer::ComputerPlayer(): pBoard(0)
{
name = "Computer";
}
int ComputerPlayer::makeMove()
{
srand((unsigned)time(0));
Board temp = *pBoard;
char myPiece = GetPiece();
char otherPiece;
if(myPiece == X)
otherPiece = O;
else
otherPiece = X;
// check for win
for(int i = 0; i < ROWS * COLUMNS; ++i)
{
if(temp.GetField(i) == EMPTY)
temp.SetField(i, myPiece);
else
continue;
//check if this position will make the computer win
for(int j = Board::HORIZONTAL; j <= Board::DIAGONAL; ++j)
if(temp.Consecutive3((Board::DIRECTION)j, myPiece))
return i;
//if not, undo the move
temp.SetField(i, EMPTY);
}
// check to stop the player from winning
for(int i = 0; i < ROWS * COLUMNS; ++i)
{
if(temp.GetField(i) == EMPTY)
temp.SetField(i, otherPiece);
else
continue;
//check if this position will make the player win and block it
for(int j = Board::HORIZONTAL; j <= Board::DIAGONAL; ++j)
if(temp.Consecutive3((Board::DIRECTION)j, otherPiece))
return i;
temp.SetField(i, EMPTY);
}
//else return a random number
int r;
do
{
r = rand() % (ROWS * COLUMNS);
} while(temp.GetField(r) != EMPTY);
return r;
}
void ComputerPlayer::SetBoard(const Board *const board)
{
pBoard = board;
}
Helper functions:
functions.h
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#include <iostream>
#include <string>
#include <sstream>
#include <cassert> // used by boundCheck
#include "constants.h"
//throws a runtime error if val isn't between boundStart and boundEnd, boundEnd NOT included
void boundCheck(int boundStart, int boundEnd, int val);
//converts an index e.g. 6 to two values used in a 2D array, 6 is (2,0)
void convert1Dto2D(int index, int &row, int &column);
//this converts (2,0) to 6
void convert2Dto1D(int row, int column, int &index);
//read a string and return an int
int getInt();
#endif
functions.cpp
#include "functions.h"
void boundCheck(int boundStart, int boundEnd, int val)
{
assert(val >= boundStart && val < boundEnd);
}
void convert1Dto2D(int index, int &row, int &column)
{
row = int(index / ROWS);
column = index - row * ROWS;
}
void convert2Dto1D(int row, int column, int &index)
{
index = (row * ROWS) + column;
}
int getInt()
{
std::string temp;
std::getline(std::cin, temp);
std::stringstream ss(temp);
int i;
ss >> i;
return i;
}
and
constants.h (global constants)
#ifndef CONSTANTS_H
#define CONSTANTS_H
const int COLUMNS = 3;
const int ROWS = 3;
const char EMPTY = ' ';
const char X = 'X';
const char O = 'O';
#endif
main.cpp
#include <iostream>
#include "functions.h"
#include "TicTacToe.h"
using namespace std;
int getNumberOfPlayers();
int main()
{
cout << "\tTic Tac Toe\tby Minas Mina\n\n";
TicTacToe game(getNumberOfPlayers());
game.Instructions();
game.Play();
cin.get();
return 0;
}
int getNumberOfPlayers()
{
cout << "How many players? 1/2: ";
int numPlayers;
do
{
numPlayers = getInt();
if(numPlayers != 1 && numPlayers != 2)
cout << "1 or 2, not " << numPlayers << "!\n";
}while(numPlayers != 1 && numPlayers != 2);
return numPlayers;
}
[/souurce]
Sorry for the long post..
[Edited by - sheep19 on December 15, 2008 9:38:33 AM]