In order to implement a Tic Tac Toe Rule Engine we shall define the following entities:
-Model, which shall be used as a base for the rule engine
-
ModelOptions, which shall be used for containing the options for creating a model
-
Direction, which shall be used to simplify the winning condition check.
-
GameStatus, which shall be used as the return result after a winning condition check.
-
MoveStatus, which shall be used as the return result after a player makes a move.
-
Player, which shall be used for containing the information about the players.
We shall consider that the Tic Tac Toe game takes place on a n x m board having 2 or more players, where n can be equal m.
For the model we shall define the following the attributes:
-Number of players
-
The width and height of the game table
-
The number of symbols needed for a player to win
The model must allow the following operations:
-Put symbol, which shall allow a player to insert his symbol at a certain position on the table if that position was not occupied by another player.
-
Winning condition check, which needs to verify the table and signal if a player managed to have enough symbols placed in a line/diagonal configuration.
The implementation for the
Model is:
package tictactoe.ruleengine;
import java.awt.Point;
/**
*
* @author bogdan
*/
public class Model
{
/**
* A reference to the options used for creating the model
*/
private ModelOptions options;
/**
* The game table
*/
private int[][] table;
/**
* Creates new Model object
* @param options The options used for creating the model
*/
public Model(ModelOptions options)
{
this.options = options;
this.table = new int[options.getHeight()][options.getWidth()];
clearTable();
}
/**
* Initializes the game table with the default values
*/
private void clearTable()
{
int i,j;
int height = options.getHeight();
int width = options.getWidth();
for(i = 0; i<height; i++)
{
for(j = 0; j<width; j++)
{
table[i][j] = Player.NO_PLAYER;
}
}
}
/**
* Checks if the coordinates are within the game table bounds
* @param x The X coordinate of the point
* @param y The Y coordinate of the point
* @return True if the point is within bounds, false otherwise
*/
private boolean isWithinBounds(int x, int y)
{
return ((x>=0) && (x<options.getHeight())
&& (y>=0) && (y<options.getWidth()));
}
/**
* Verifies if P(i,j) is situated on a line or diagonal with enough symbols
* in order for the player to win.
* @param i The X coordinate of the point
* @param j The Y coordinate of the point
* @param symbol The type of symbol who is counted
* @return True - if a player won
* False - No one one.
*/
private boolean checkSquare(int i, int j, int symbol)
{
int k;
int counter;
int x, y;
boolean hasWon = false;
Direction dir;
int symbolsToWin = options.getSymbolsToWin();
//Checks in every direction if there are enough symbols in order
//for a player to win
for(k = Direction.FIRST_SYMBOL; k< Direction.LAST_SYMBOL; k++)
{
counter = 1;
x = i;
y = j;
while(counter!=symbolsToWin)
{
dir = Direction.getDirection(k);
x += dir.getXOffset();
y += dir.getYOffset();
if((isWithinBounds(x, y)==true)&& (table[x][y]==symbol) &&
(counter<symbolsToWin))
{
counter++;
}
else
{
break;
}
}
if(counter==symbolsToWin)
{
hasWon = true;
break;
}
}
return hasWon;
}
/**
* Verifies each square to see if any player won
* @return
* GameStatus.GAME_OVER - The game is over, no one won (if there no more
* free squares)
* GameStatus.GAME_PENDING - No one won yet
* Otherwise returns a GameStatus object initialized with the symbol of
* the winning player.
*/
public GameStatus checkWinningConditions()
{
int i,j;
int result = Player.NO_CHANGE;
boolean hasWon;
int emptySymbolCounter = 0;
int height = options.getHeight();
int width = options.getWidth();
//Checks for every square if winning conditions are achieved
for(i=0; (i<height && result==-1); i++)
{
for(j=0; (j<width && result==-1); j++)
{
if(table[i][j]!=Player.NO_PLAYER)
{
hasWon = checkSquare(i,j,table[i][j]);
if(hasWon==true)
{
result = table[i][j];
}
}
else
{
emptySymbolCounter++;
}
}
}
if((result==-1) && (emptySymbolCounter==0))
{
result = Player.NO_PLAYER;
}
return new GameStatus(result);
}
/**
* Inserts a symbol on the game table
* @param x The X coordinate at which the symbol is inserted
* @param y The Y coordinate at which the symbol is inserted
* @param symbol The value of the symbol who will be inserted
* @return
* MoveStatus.IsValid - if the insertion is valid
* MoveStatus.AlreadyOccupied -
*/
public MoveStatus putSymbol(int x, int y, int symbol)
{
int i;
boolean found = false;
MoveStatus status =MoveStatus.None;
int[] playerSymbols = Player.getAllPlayerSymbols();
for(i = 0; i< playerSymbols.length; i++)
{
if(symbol == playerSymbols[i])
{
if(isWithinBounds(x, y)==true)
{
if(table[x][y]==Player.NO_PLAYER)
{
table[x][y] = symbol;
status = MoveStatus.Valid;
}
else
{
status = MoveStatus.AlreadyOccupied;
}
}
else
{
status = MoveStatus.OutOfBounds;
}
found=true;
break;
}
}
if(found==false)
{
status = MoveStatus.UnknownSymbol;
}
return status;
}
/**
* Returns a string representation of the table
* @return A string representation of the table
*/
@Override
public String toString()
{
int i,j;
int height = options.getHeight();
int width = options.getWidth();
StringBuilder builder = new StringBuilder();
for(i = 0; i< height; i++)
{
for(j = 0; j<width; j++)
{
builder.append(table[i][j]);
builder.append(" ");
}
builder.append("\n");
}
return builder.toString();
}
/**
* Returns the symbol at the point P(i,j)
* @param i The X coordinate of the point
* @param j The Y coordinate of the point
* @return The symbol at the point P(i,j) if i and j are within the table
* bounds. Otherwise returns Integer.MAX_VALUE;
*/
public int getSymbolAt(int i, int j)
throws IllegalArgumentException
{
if(isWithinBounds(i,j)==true)
{
return table[i][j];
}
else
{
return Integer.MAX_VALUE;
}
}
}
The implementation for the
ModelOptions is:
package tictactoe;
import java.util.ArrayList;
/**
*
* @author dystopiancode
*/
public class ModelOptions
{
/**
* A reference to an ArrayList containing the players.
*/
private ArrayList<Player> players;
/**
* The width of the game table
*/
private int width;
/**
* The height of the game table
*/
private int height;
/**
* The number of symbols that need to be in a line or a row in order for
* a player to win
*/
private int symbolsToWin;
/**
* Creates a new ModelOptions object
* @param players A reference to an ArrayList containing the players.
* @param height The height of the game table
* @param width The width of the game table
* @param symbolsToWin The number of symbols that need to be in a line or
* a row in order for a player to win
* @throws IllegalArgumentException
*/
public ModelOptions(ArrayList<Player> players,
int height,
int width,
int symbolsToWin)
throws IllegalArgumentException
{
if((symbolsToWin<=height) && (symbolsToWin<=width))
{
this.players = players;
this.width = width;
this.height = height;
this.symbolsToWin = symbolsToWin;
}
else
{
throw new IllegalArgumentException("Size of table too small");
}
}
/**
* Returns a reference to an ArrayList containing the players
* @return A reference to the players Arraylist
*/
public ArrayList<Player> getPlayers()
{
return players;
}
/**
* Returns the number of symbols needed for winning
* @return The number of symbols needed for winning
*/
public int getSymbolToWin()
{
return symbolsToWin;
}
/**
* Returns the height of the game table
* @return The height of the game table
*/
public int getHeight()
{
return height;
}
/**
* Returns the width of the game table
* @return The width of the game table
*/
public int getWidth()
{
return width;
}
}
The implementation for
Direction is:
package tictactoe;
/**
* @author dystopiancode
*/
public enum Direction
{
NONE(0,0,0),
NORTH(1,-1,0),
SOUTH(2,1,0),
EAST(3,0,-1),
WEST(4,0,1),
NORTH_EAST(5,-1,-1),
NORTH_WEST(6,-1,1),
SOUTH_EAST(7,1,-1),
SOUTH_WEST(8,1,1);
/**
* The symbol for no direction
*/
public static final int NONE_SYMBOL = 0;
/**
* The symbol for the first direction
*/
public static final int FIRST_SYMBOL = 1;
/**
* The symbol for the last direction
*/
public static final int LAST_SYMBOL = 8;
/**
* The symbol used for representing a direction
*/
private int _symbol;
/**
* The x axis offset introduced by changing the location according to
* the direction.
*/
private int _xOffset;
/**
* The y axis offset introduced by changing the location according to
* the direction.
*/
private int _yOffset;
/**
* Creates a Direction object
* @param symbol The symbol used for representing the direction
* @param xOffset The x axis offset introduced by changing the location
* according to the direction.
* @param yOffset The y axis offset introduced by changing the location
* according to the direction.
*/
private Direction(int symbol, int xOffset, int yOffset)
{
_symbol = symbol;
_xOffset = xOffset;
_yOffset = yOffset;
}
/**
* Return the x axis offset
* @return The x axis offset
*/
public int getXOffset()
{
return _xOffset;
}
/**
* Return the y axis offset
* @return The y axis offset
*/
public int getYOffset()
{
return _yOffset;
}
/**
* Return the symbol of the direction
* @return The symbol
*/
public int getSymbol()
{
return _symbol;
}
/**
* Creates a direction according to a symbol
* @param symbol The symbol used for creating the direction
* @return The direction who has the indicated symbol
*/
public static Direction getDirection(int symbol)
{
switch(symbol)
{
case 1:
return Direction.NORTH;
case 2:
return Direction.SOUTH;
case 3:
return Direction.EAST;
case 4:
return Direction.WEST;
case 5:
return Direction.NORTH_EAST;
case 6:
return Direction.NORTH_WEST;
case 7:
return Direction.SOUTH_EAST;
case 8:
return Direction.SOUTH_WEST;
default:
return Direction.NONE;
}
}
}
The implementation for
GameStatus is:
package tictactoe;
/**
*
* @author dystopiancode
*/
public class GameStatus
{
/**
* The game is over, no on won
*/
public static GameStatus GAME_OVER = new GameStatus(Player.NO_PLAYER);
/**
* The game is still running
*/
public static GameStatus GAME_PENDING = new GameStatus(Player.NO_CHANGE);
/**
* The symbol encapsulated by the GameStatus
*/
private int symbol;
/**
* Creates a new GameStatus object
* @param symbol The symbol representing the status (usually the symbol
* of the player who won the game).
*/
public GameStatus(int symbol)
{
this.symbol = symbol;
}
/**
* Returns the symbol of the winning player, if any.
* @return The symbol of the winning player
*/
public int getSymbol()
{
return symbol;
}
/**
* Verifies if the GameStatus objects are equal
* @param status The object who is going to be compared with the current
* object
* @return True if the object are equal, false otherwise
*/
public boolean equals(GameStatus status)
{
return (symbol==status.symbol);
}
}
The implementation for
MoveStatus is:
package tictactoe;
/**
*
* @author dystopiancode
*/
public enum MoveStatus
{
None(0),
Valid(1),
AlreadyOccupied(2),
OutOfBounds(3),
UnknownSymbol(4);
/**
* A integer representing the code of a status resulted after a player
* made a move.
*/
private int statusCode;
/**
* Private constructor for the enumeration
* @param statusCode The code of the current status
*/
private MoveStatus(int statusCode)
{
this.statusCode = statusCode;
}
/**
* Checks if two MoveStatus objects are equal
* @param status The object to which the current object will be compared
* @return True if the objects are equal, otherwise false
*/
public boolean equals(MoveStatus status)
{
return (this.statusCode == status.statusCode);
}
}
The implementation for
Player is:
package tictactoe;
/**
*
* @author dystopiancode
*/
public class Player
{
/**
*
*/
public static final int NO_CHANGE = -1;
public static final int NO_PLAYER = 0;
/**
* Static counter-like variable used for assigning a symbol for each new
* player.
*/
private static int symbolCounter = 1;
/**
* The name of the player
*/
private String name;
/**
* The symbol used for representing a a player on the game table
*/
private int symbol;
/**
* Createa a new Player object
* @param name The name of the player
*/
public Player(String name)
{
this.name = name;
symbol = symbolCounter;
symbolCounter++;
}
/**
* Returns the player's symbol
* @return The Player's symbol
*/
public int getSymbol()
{
return symbol;
}
/**
* Returns the player's name
* @return The player's name
*/
public String getName()
{
return name;
}
/**
* Creates a vector containing all the symbols used by the existing players
* @return A vector containing all the symbols used by the existing players
*/
public static int[] getAllPlayerSymbols()
{
int[] symbols = new int[symbolCounter];
for(int i = 0; i<symbolCounter; i++)
{
symbols[i] = i;
}
return symbols;
}
}