Saturday, July 7, 2012

Implementing a Tic Tac Toe Rule Engine in Java

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;
    }
}

No comments:

Post a Comment

Got a question regarding something in the article? Leave me a comment and I will get back at you as soon as I can!

Related Posts Plugin for WordPress, Blogger...
Recommended Post Slide Out For Blogger