Back to project page it.alcacoop.fourinaline.
The source code is released under:
GNU General Public License
If you think the Android project it.alcacoop.fourinaline listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * GameModel.java// w ww . j a v a2 s .c om * * Created: 2008/02/16 * * Copyright (C) 2008 Julien Aubin * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gojul.fourinaline.model; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; /** * The <code>GameModel</code> class contains the basic modelisation * of a 4 in a line game.<br/> * It supports a various number of lines, and a various number of cells.<br/> * It only supports two players. However it is optimized for the support of * alpha beta algorithm in order to improve the overall speed of an AI player. * * @author Julien Aubin */ public final class GameModel implements Serializable { /** * The serial version UID. */ final static long serialVersionUID = 1; /** * The <code>CellCoord</code> class represents a class * of cell coordinates. * * @author Julien Aubin */ public final static class CellCoord implements Serializable { /** * The serial version UID. */ final static long serialVersionUID = 1; /** * The row index. */ private int rowIndex; /** * The column index. */ private int colIndex; /** * Constructor. * @param row the row index. * @param col the column index. */ public CellCoord(final int row, final int col) { rowIndex = row; colIndex = col; } /** * Returns the row index. * @return the row index. */ public int getRowIndex() { return rowIndex; } /** * Returns the column index. * @return the column index. */ public int getColIndex() { return colIndex; } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (obj != null && obj instanceof CellCoord) { CellCoord test = (CellCoord) obj; return test.rowIndex == rowIndex && test.colIndex == colIndex; } else return false; } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return rowIndex + colIndex; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return new StringBuffer("(").append(rowIndex).append(", ").append(colIndex).append(")").toString(); } } /** * The <code>GameModelException</code> class is the exception * for all the game model errors. * * @author Julien Aubin * */ public final static class GameModelException extends RuntimeException { /** * The serial version UID. */ final static long serialVersionUID = 1; /** * Constructor. */ GameModelException() { super(); } /** * Constructor. * @param message the mesage to display. */ GameModelException(final String message) { super(message); } } /** * The <code>PlayerMark</code> symbol contains the mark * for a game player. * * @author Julien Aubin */ public final static class PlayerMark implements Serializable { /** * The serial version UID. */ final static long serialVersionUID = 1; /** * The list of players. */ private final static List<PlayerMark> players = new ArrayList<PlayerMark>(); /** * The iterator upon the list of players. */ private static Iterator<PlayerMark> itPlayers = null; /** * The mark value. */ private int markValue; /** * Constructor. * @param val the value of the player mark. * @throws IllegalArgumentException if <code>val</code> * is smaller or equal to 0 or greater or equal to <code>Character.MAX_VALUE - 'A'</code>. */ private PlayerMark(final int val) throws IllegalArgumentException { if (val <= Character.MIN_VALUE || val >= (Character.MAX_VALUE - 'A')) { throw new IllegalArgumentException(); } players.add(this); markValue = val; } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (obj != null && obj instanceof PlayerMark) return ((PlayerMark) obj).markValue == markValue; else return false; } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return markValue; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return String.valueOf(markValue); } /** * Return the unique key representation * of this mark. * @return the unique key representation * of this mark. */ public String toUniqueKey() { char c = (char) ('A' + markValue - 1); return String.valueOf(c); } /** * Returns the next player. * @return the next player. */ private final static PlayerMark getNextPlayer() { /* if (itPlayers == null || !itPlayers.hasNext()) itPlayers = players.iterator(); */ PlayerMark result = itPlayers.next(); return result; } /** * Returns an iterator over the player mark list. This iterator * cannot modify the player mark list. * @return an iterator over the player mark list. This iterator * cannot modify the player mark list. */ public final static Iterator<PlayerMark> getPlayerIterator() { return Collections.unmodifiableCollection(players).iterator(); } /** * Returns the mark that follows the player mark <code>playerMark</code>. * @param playerMark the player mark to consider. * @return the mark that follows the player mark <code>playerMark</code>. * @throws NullPointerException if <code>playerMark</code> is null. */ public final static PlayerMark getNextMark(final PlayerMark playerMark) throws NullPointerException { if (playerMark == null) throw new NullPointerException(); int index = players.indexOf(playerMark); index = (index + 1) % players.size(); return players.get(index); } /** * Returns the number of known player marks. * @return the number of known player marks. */ public final static int getNumberOfPlayerMarks() { return players.size(); } /** * The player A mark. */ //public final static PlayerMark PLAYER_A_MARK = new PlayerMark(1); /** * The player B mark. */ //public final static PlayerMark PLAYER_B_MARK = new PlayerMark(2); } /** * The game status class represents the current game status of the game. * * @author Julien Aubin */ public final static class GameStatus implements Serializable { /** * The serial version UID. */ final static long serialVersionUID = 1; /** * The game status text. */ private String text; /** * Constructor. * @param statusText the status text. */ private GameStatus(final String statusText) { text = statusText; } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (obj != null && obj instanceof GameStatus) return ((GameStatus) obj).text.equals(text); else return false; } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return text.hashCode(); } /** * @see java.lang.Object#toString() */ @Override public String toString() { return text; } /** * The continue status. */ public final static GameStatus CONTINUE_STATUS = new GameStatus("Continue"); /** * The tie status. */ public final static GameStatus TIE_STATUS = new GameStatus("Tie"); /** * The won status. */ public final static GameStatus WON_STATUS = new GameStatus("Won"); } /** * This class represents a step of the game. * * @author Julien Aubin */ private final static class PlayStep implements Serializable { /** * The class serial version UID. */ final static long serialVersionUID = 1L; /** * The played column index. */ private int colIndex; /** * The played status. */ private GameStatus currentStatus; /** * The current player mark. */ private PlayerMark currentMark; /** * Constructor. * @param col the played column index. * @param status the played status. * @param mark the played mark. * @throws NullPointerException if any of the method parameter is null. */ public PlayStep(final int col, final GameStatus status, final PlayerMark mark) throws NullPointerException { if (status == null || mark == null) throw new NullPointerException(); colIndex = col; currentStatus = status; currentMark = mark; } /** * Return played the column index. * @return the played column index. */ public int getColIndex() { return colIndex; } /** * Return the game status at the time of the start of the turn. * @return the game status at the time of the start of the turn. */ public GameStatus getGameStatus() { return currentStatus; } /** * Return the mark at the start of the turn. * @return the mark at the start of the turn. */ public PlayerMark getPlayerMark() { return currentMark; } } /** * The game tab, indexed by rows and then by column index. * The (0, 0) coordinate represent the (top, left) cell. */ private PlayerMark[][] gameTab; /** * The line length required in order to win. */ private int winLineLength; /** * The currentPlayer. */ private PlayerMark currentPlayer; /** * The game status. */ private GameStatus gameStatus; /** * The win line. */ private List<CellCoord> winLine; /** * The play history. */ private List<PlayStep> playHistory; /** * The set of all lines of the game model. */ private Set<List<CellCoord>> lines; /** * The map of line positions that are considered as * possible victory lines. This is a cache mechanism in order * to improve the performance, especially with AI algorithm.<br/> * It maps a cell to the list of lines to which it belongs.<br/> * This map is shared among all the instances of a gamemodel that * share the same parameters, since it's never written with a * game model own data.<br/> * However it's thread safe. */ private Map<CellCoord, Set<List<CellCoord>>> winLinesMap; public GameModel(final int rows, final int cols, final int winLength, int firstPlayer) { PlayerMark.players.clear(); if (firstPlayer==1) { new PlayerMark(1); new PlayerMark(2); } else { new PlayerMark(2); new PlayerMark(1); } PlayerMark.itPlayers = PlayerMark.players.iterator(); create(rows, cols, winLength); } /** * Constructor. */ public GameModel() { this(6, 7, 4, 1); } /** * Constructor. * @param rows the number of rows. * @param cols the number of columns. * @param winLength the number of cells to get in order to have * a winning line. * @throws IllegalArgumentException if any of the parameters is smaller * or equal to 0, or if the number of cells for a winning line is * greater than <code>Math.min(rows, cols)</code>, or if <code>winLength</code> * is smaller or equal to 2. */ public void create(final int rows, final int cols, final int winLength) throws IllegalArgumentException { if (rows <= 0 || cols <= 0) throw new IllegalArgumentException("Illegal dimenstions. Rows : " + rows + " - Columns : " + cols); if (winLength <= 2 || winLength > Math.min(rows, cols)) throw new IllegalArgumentException("Illegal length of line in order to win. Rows : " + rows + " - Columns : " + cols + " - Length to win : " + winLength); gameTab = new PlayerMark[rows][cols]; winLineLength = winLength; currentPlayer = PlayerMark.getNextPlayer(); gameStatus = GameStatus.CONTINUE_STATUS; winLinesMap = new ConcurrentHashMap<CellCoord, Set<List<CellCoord>>>(); winLine = null; playHistory = new LinkedList<PlayStep>(); lines = new HashSet<List<CellCoord>>(); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { lines.addAll(getAllLines(i, j)); } } } /** * Constructor. * @param gameModel the game model to copy. * @throws NullPointerException if <code>gameModel</code> is null. */ public GameModel(final GameModel gameModel) throws NullPointerException { if (gameModel == null) throw new NullPointerException(); winLineLength = gameModel.winLineLength; currentPlayer = gameModel.currentPlayer; gameStatus = gameModel.gameStatus; // Here it is safe to copy the win line map - nothing particular // to a given instance of a game model is written here. winLinesMap = gameModel.winLinesMap; lines = gameModel.lines; // Here it is safe to copy the win line, since it is readonly. winLine = gameModel.winLine; gameTab = new PlayerMark[gameModel.gameTab.length][]; for (int i = 0; i < gameModel.gameTab.length; i++) { gameTab[i] = new PlayerMark[gameModel.gameTab[i].length]; for (int j = 0; j < gameModel.gameTab[i].length; j++) gameTab[i][j] = gameModel.gameTab[i][j]; } playHistory = new LinkedList<PlayStep>(gameModel.playHistory); } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (obj != null && obj instanceof GameModel) { // No test performed winLinesMap since it should be // the same for all the instances of GameModel which // have the same dimension. GameModel model = (GameModel) obj; boolean result = winLineLength == model.winLineLength && currentPlayer.equals(model.currentPlayer) && gameStatus.equals(model.gameStatus); if (result) { result = gameTab.length == model.gameTab.length && gameTab[0].length == model.gameTab[0].length; if (result) { for (int i = 0; i < gameTab.length && result; i++) { for (int j = 0; j < gameTab[i].length && result; j++) { if ((gameTab[i][j] == null && model.gameTab[i][j] != null) || (gameTab[i][j] != null && !gameTab[i][j].equals(model.gameTab[i][j]))) result = false; } } } } return result; } else return false; } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return winLinesMap.hashCode(); } /** * Returns the current player. * @return the current player. */ public PlayerMark getCurrentPlayer() { return currentPlayer; } /** * Returns the game status. * @return the game status. */ public GameStatus getGameStatus() { return gameStatus; } /** * Returns the number of cells to align in order to win. * @return the number of cells to align in order to win. */ public int getWinLineLength() { return winLineLength; } /** * Returns the row count of this game model. * @return the row count of this game model. */ public int getRowCount() { return gameTab.length; } /** * Returns the column count of this game model. * @return the column count of this game model. */ public int getColCount() { return gameTab[0].length; } /** * Returns the mark of the cell at coordinates <code>rowIndex, colIndex</code>. * @param rowIndex the row index. * @param colIndex the column index. * @return the mark of the cell at coordinates <code>rowIndex, colIndex</code>. * @throws ArrayIndexOutOfBoundsException if <code>rowIndex</code> is strictly * smaller than 0 or greater or equal to the number of rows, or if <code>colIndex</code> * is strictly smaller than 0 or greater or equal to the number of columns. */ public PlayerMark getCell(final int rowIndex, final int colIndex) throws ArrayIndexOutOfBoundsException { if (isOutOfBounds(rowIndex, colIndex)) throw new ArrayIndexOutOfBoundsException(new CellCoord(rowIndex, colIndex).toString()); return gameTab[rowIndex][colIndex]; } /** * Returns the mark of the cell at coordinates represented by <code>cellCoord</code>. * @param cellCoord the cell coordinates. * @return the mark of the cell at coordinates represented by <code>cellCoord</code>. * @throws NullPointerException if <code>cellCoord</code> is null. * @throws ArrayIndexOutOfBoundsException if <code>cellCoord</code> is out of the * game tab bounds. */ public PlayerMark getCell(final CellCoord cellCoord) throws NullPointerException, ArrayIndexOutOfBoundsException { if (cellCoord == null) throw new NullPointerException(); return getCell(cellCoord.getRowIndex(), cellCoord.getColIndex()); } /** * @see java.lang.Object#toString() */ @Override public String toString() { final String LINE_SEP = System.getProperty("line.separator"); StringBuilder sbContent = new StringBuilder(); for (int i = 0; i < gameTab.length; i++) { for (int j = 0; j < gameTab[i].length; j++) { String mark = null; if (gameTab[i][j] != null) mark = gameTab[i][j].toString(); else mark = "0"; sbContent.append(mark); if (j < gameTab[i].length - 1) sbContent.append(" "); else sbContent.append(LINE_SEP); } } return sbContent.toString(); } /** * Return the unique string representation * of this game model.<br/> * The string representation is provided * under a compressed form. * @return the unique string representation * of this game model. */ public String toUniqueKey() { StringBuilder result = new StringBuilder(); int nbEmptyCells = 0; for (int i = 0; i < gameTab.length; i++) { for (int j = 0; j < gameTab[i].length; j++) { if (gameTab[i][j] != null) { if (nbEmptyCells > 0) result.append(nbEmptyCells); nbEmptyCells = 0; result.append(gameTab[i][j].toUniqueKey()); } else { nbEmptyCells++; } } } if (nbEmptyCells > 0) result.append(nbEmptyCells); return result.toString(); } /** * Returns the set of playable columns. * @return the list of playable columns. */ public Collection<Integer> getListOfPlayableColumns() { Set<Integer> result = new TreeSet<Integer>(); if (!gameStatus.equals(GameStatus.CONTINUE_STATUS)) return result; for (int i = 0; i < gameTab[0].length; i++) if (gameTab[0][i] == null) result.add(Integer.valueOf(i)); return result; } /** * Perform a play at the column number <code>colIndex</code>, with * player mark <code>playerMark</code>.<br/> * Gives the turn to the next player, and updates the game status * if necessary. * @param colIndex the column index where the player wants to play. * @param playerMark the player mark. * @throws GameModelException if this is not the player turn, if * the game is over or if the column number <code>colIndex</code> * is not playable. */ public void play(final int colIndex, final PlayerMark playerMark) throws GameModelException { Collection<Integer> playableColumns = getListOfPlayableColumns(); if (!playableColumns.contains(Integer.valueOf(colIndex))) throw new GameModelException("The column number " + colIndex + " is not included in the list of playable columns " + playableColumns); if (!playerMark.equals(currentPlayer)) throw new GameModelException("This is not the turn of player " + playerMark + ". Current turn : " + currentPlayer); gameTab[getFreeRowIndexForColumn(colIndex)][colIndex] = playerMark; playHistory.add(new PlayStep(colIndex, gameStatus, currentPlayer)); updateGameStatus(colIndex); if (gameStatus.equals(GameStatus.CONTINUE_STATUS)) { currentPlayer = PlayerMark.getNextMark(currentPlayer); } } /** * Cancel the last play in the play history. * @throws GameModelException if there's no more more * play to remove. */ public void cancelLastPlay() throws GameModelException { if (playHistory.isEmpty()) throw new GameModelException(); PlayStep lastStep = playHistory.remove(playHistory.size() - 1); gameStatus = lastStep.getGameStatus(); currentPlayer = lastStep.getPlayerMark(); int colIndex = lastStep.getColIndex(); gameTab[getFreeRowIndexForColumn(colIndex) + 1][colIndex] = null; } /** * Returns the first free row index for the column which has for * index <code>colIndex</code>, i.e. the row index of the first * free cell of the column, or -1 if there's not such a free cell. * @param colIndex the column index to test. * @return the first free row index for the column which has for * index <code>colIndex</code>, i.e. the row index of the first * free cell of the column, or -1 if there's not such a free cell. * @throws ArrayIndexOutOfBoundsException if <code>colIndex</code> * is strictly smaller than 0, or greater or equal to the number of * columns of the game tab. */ public int getFreeRowIndexForColumn(final int colIndex) throws ArrayIndexOutOfBoundsException { if (colIndex < 0 || colIndex >= gameTab[0].length) throw new ArrayIndexOutOfBoundsException(colIndex); int rowResult = -1; boolean occupiedCellFound = false; for (int i = 0; i < gameTab.length && !occupiedCellFound; i++) { if (gameTab[i][colIndex] != null) { occupiedCellFound = true; rowResult = i - 1; } } if (!occupiedCellFound) rowResult = gameTab.length - 1; return rowResult; } /** * Returns the set of horizontal lines that cover the * cell at coordinates <code>row, col</code>. * @param row the row index. * @param col the column index. * @return the set of horizonal lines that cover the cell * at coordinates <code>row, col</code>. */ private Set<List<CellCoord>> getHorizontalLines(final int row, final int col) { // Computes the min, max columns that contain the column number col. // We must ensure that he column number col is included in all the lines, // which explains the "+1" int minColIndex = Math.max(0, col - winLineLength + 1); int maxColIndex = Math.min(col, gameTab[0].length - winLineLength); Set<List<CellCoord>> result = new HashSet<List<CellCoord>>(); // Here we use a <= symbol because the extreme safe coordinates // are already computed. for (int i = minColIndex; i <= maxColIndex; i++) { List<CellCoord> line = new ArrayList<CellCoord>(); for (int j = 0; j < winLineLength; j++) line.add(new CellCoord(row, i + j)); result.add(Collections.unmodifiableList(line)); } return result; } /** * Returns the set of vertical lines that cover the cell * at coordinates <code>row, col</code>. * @param row the row index. * @param col the column index. * @return the set of vertical lines that cover the cell * at coordinates <code>row, col</code>. */ private Set<List<CellCoord>> getVerticalLines(final int row, final int col) { // Computes the min, max rows that contain the row number row. // We must ensure that he row number row is included in all the columns, // which explains the "+1" int minRowIndex = Math.max(0, row - winLineLength + 1); int maxRowIndex = Math.min(row, gameTab.length - winLineLength); // Here we use a <= symbol because the extreme safe coordinates // are already computed. Set<List<CellCoord>> result = new HashSet<List<CellCoord>>(); for (int i = minRowIndex; i <= maxRowIndex; i++) { List<CellCoord> line = new ArrayList<CellCoord>(); for (int j = 0; j < winLineLength; j++) line.add(new CellCoord(i + j, col)); result.add(Collections.unmodifiableList(line)); } return result; } /** * Returns true if the coordinate <code>row, col</code> is out of bounds, * false elsewhere. * @param row the row index. * @param col the column index. * @return true if the coordinate <code>row, col</code> is out of bounds, * false elsewhere. */ private boolean isOutOfBounds(final int row, final int col) { return row < 0 || row >= gameTab.length || col < 0 || col >= gameTab[0].length; } /** * Returns the set of down diagonals that cover the cell * at coodinates <code>row, col</code>. Down diagonals * go from (top, left) to (bottom, right) coordinates. * @param row the row index. * @param col the column index. * @return the set of down diagonals that cover the cell * at coodinates <code>row, col</code>. */ private Set<List<CellCoord>> getDownDiagonals(final int row, final int col) { // The algorithem is pretty simple : just consists in computing all // the diagonals of line winLineLength that would contain the cell of coords (row, col) // in an infinite grid, and then churning out all the ones that // contain out of bounds cells. // Computes the extremities of the diagonals that contain the cell // of coord (row, col) int minColIndex = col - winLineLength + 1; int minRowIndex = row - winLineLength + 1; Set<List<CellCoord>> result = new HashSet<List<CellCoord>>(); // Here we use the <= and >= symbol because all the possible // diagonals contain the cell of coords (row, col) for (int rowIndex = minRowIndex, colIndex = minColIndex; rowIndex <= row && colIndex <= col; rowIndex++, colIndex++) { List<CellCoord> line = new ArrayList<CellCoord>(); boolean outOfBoundsDetected = false; // Computing diagonals is much more tricky since we evolve // simulataneously on two different dimensions. // That's why we must always detect if we're out of bounds or not. for (int i = 0; i < winLineLength && !outOfBoundsDetected; i++) { int cellRow = rowIndex + i; int cellCol = colIndex + i; if (!isOutOfBounds(cellRow, cellCol)) line.add(new CellCoord(cellRow, cellCol)); else outOfBoundsDetected = true; } if (!outOfBoundsDetected) result.add(Collections.unmodifiableList(line)); } return result; } /** * Returns the set of up diagonals that cover the cell * at coodinates <code>row, col</code>. Up diagonals * go from (bottom, left) to (top, right) coordinates. * @param row the row index. * @param col the column index. * @return the set of down diagonals that cover the cell * at coodinates <code>row, col</code>. */ private Set<List<CellCoord>> getUpDiagonals(final int row, final int col) { // The algorithem is pretty simple : just consists in computing all // the diagonals of line winLineLength that would contain the cell of coords (row, col) // in an infinite grid, and then churning out all the ones that // contain out of bounds cells. // Computes the extremities of the diagonals that contain the cell // of coord (row, col) int maxColIndex = col + winLineLength - 1; int minRowIndex = row - winLineLength + 1; Set<List<CellCoord>> result = new HashSet<List<CellCoord>>(); // Here we use the <= and >= symbol because all the possible // diagonals contain the cell of coords (row, col) for (int rowIndex = minRowIndex, colIndex = maxColIndex; rowIndex <= row && colIndex >= col; rowIndex++, colIndex--) { List<CellCoord> line = new ArrayList<CellCoord>(); boolean outOfBoundsDetected = false; // Computing diagonals is much more tricky since we evolve // simulataneously on two different dimensions. // That's why we must always detect if we're out of bounds or not. for (int i = 0; i < winLineLength && !outOfBoundsDetected; i++) { int cellRow = rowIndex + i; int cellCol = colIndex - i; if (!isOutOfBounds(cellRow, cellCol)) line.add(new CellCoord(cellRow, cellCol)); else outOfBoundsDetected = true; } if (!outOfBoundsDetected) result.add(Collections.unmodifiableList(line)); } return result; } /** * Returns the list of all the lines of this game * model. Each line appears only once in the returned * set. * @return the list of all the lines of this game * model. */ public Set<List<CellCoord>> getAllLines() { return Collections.unmodifiableSet(lines); } /** * Returns the list of all the values for the line <code>line</code>. * @param line the line for which we want to get all the values. * @return the list of all the values for the line <code>line</code>. * @throws NullPointerException if <code>line</code> is null. * @throws ArrayIndexOutOfBoundsException if any of the coordinates * pointed out by <code>line</code> is null. */ public List<PlayerMark> getValuesOfLine(final List<CellCoord> line) throws NullPointerException, ArrayIndexOutOfBoundsException { if (line == null) throw new NullPointerException(); // It's very easy to know if a line is valid, since // we know all the possible lines of the game. if (!lines.contains(line)) throw new ArrayIndexOutOfBoundsException(); List<PlayerMark> result = new ArrayList<PlayerMark>(line.size()); for (CellCoord coord: line) { result.add(gameTab[coord.getRowIndex()][coord.getColIndex()]); } return result; } /** * Returns the list of all the possible lines for coordinates <code>row, col</code>. * @param row the row index. * @param col the column index. * @return the list of all the possible lines that are possible * for coordinates <code>row, col</code>. * @throws ArrayIndexOutOfBoundsException if <code>row</code> is strictly * smaller than 0 or greater or equal to the number of rows, or if <code>col</code> * is strictly smaller than 0 or greater or equal to the number of columns. */ public Collection<List<CellCoord>> getAllLines(final int row, final int col) throws ArrayIndexOutOfBoundsException { if (isOutOfBounds(row, col)) throw new ArrayIndexOutOfBoundsException(new CellCoord(row, col).toString()); return getAllLines(new CellCoord(row, col)); } /** * Returns the list of all the possible lines for coordinates <code>row, col</code>. * @param cellCoord the coordinates to consider. * @return the list of all the possible lines for coordinates <code>row, col</code>. * @throws NullPointerException if <code>cellCoord</code> is null. * @throws ArrayIndexOutOfBoundsException if <code>cellCoord</code> is out of bounds. */ public Collection<List<CellCoord>> getAllLines(final CellCoord cellCoord) throws NullPointerException, ArrayIndexOutOfBoundsException { if (cellCoord == null) throw new NullPointerException(); Set<List<CellCoord>> result = winLinesMap.get(cellCoord); if (result == null) { int row = cellCoord.rowIndex; int col = cellCoord.colIndex; result = getHorizontalLines(row, col); result.addAll(getVerticalLines(row, col)); result.addAll(getDownDiagonals(row, col)); result.addAll(getUpDiagonals(row, col)); winLinesMap.put(cellCoord, Collections.unmodifiableSet(result)); } return result; } /** * Returns the winning line, sorted so that the cell * which has for index <code>i</code> in the line is contiguous * to the cell which has for index <code>i + 1</code>. * @return the winning line, or null if the game is not won. */ public List<CellCoord> getWinLine() { return winLine; } /** * Returns true if the game is won, false otherwise. * @param colIndex the index of the last played column. * @return true if the game is won, false otherwise. */ private boolean isGameWon(final int colIndex) { // Here we just have to determine if the game // has been won around the last cell played. The // other ones have not been updated, so they're not // interesting for us. boolean isWon = false; Set<List<CellCoord>> encounteredLines = new HashSet<List<CellCoord>>(); int rowIndex = getFreeRowIndexForColumn(colIndex) + 1; PlayerMark markTest = gameTab[rowIndex][colIndex]; // We only consider occupied cells here. if (markTest != null) { Collection<List<CellCoord>> lines = getAllLines(rowIndex, colIndex); Iterator<List<CellCoord>> it = lines.iterator(); while (it.hasNext() && !isWon) { List<CellCoord> line = it.next(); if (!encounteredLines.contains(line)) { encounteredLines.add(line); boolean isAllEqual = true; Iterator<CellCoord> itCoord = line.iterator(); // A line is considered as correct if all its // marks are not null and equal to each other. while (itCoord.hasNext() && isAllEqual) { PlayerMark mark = getCell(itCoord.next()); if (mark != null) isAllEqual = mark.equals(markTest); else isAllEqual = false; } isWon = isAllEqual; if (isWon) winLine = Collections.unmodifiableList(line); } } } return isWon; } /** * Updates the game status. * @param colIndex the index of the last played column. */ private void updateGameStatus(final int colIndex) { if (isGameWon(colIndex)) gameStatus = GameStatus.WON_STATUS; else { // In case nothing is playable, the game is tie if (getListOfPlayableColumns().isEmpty()) gameStatus = GameStatus.TIE_STATUS; } } }