├── .gitignore ├── README.md ├── src ├── main │ └── java │ │ └── ttsu │ │ └── game │ │ ├── ai │ │ ├── heuristic │ │ │ ├── StateEvaluator.java │ │ │ └── tictactoe │ │ │ │ └── TicTacToeEvaluator.java │ │ ├── GameIntelligenceAgent.java │ │ └── MinimaxAgent.java │ │ ├── DiscreteGameState.java │ │ ├── tictactoe │ │ ├── TicTacToeMain.java │ │ ├── TicTacToeBoardPrinter.java │ │ ├── TicTacToeGameRunner.java │ │ ├── GameBoard.java │ │ └── TicTacToeGameState.java │ │ └── Position.java └── test │ └── java │ └── ttsu │ └── game │ ├── tictactoe │ ├── TicTacToeBoardPrinterTest.java │ ├── GameBoardTest.java │ ├── TicTacToeGameRunnerTest.java │ └── TicTacToeGameStateTest.java │ └── ai │ ├── heuristic │ └── tictactoe │ │ └── TicTacToeEvaluatorTest.java │ └── tictactoe │ └── TicTacToeMinimaxAgentTest.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .metadata 3 | .settings 4 | .classpath 5 | .project 6 | bin 7 | 8 | # Maven 9 | target 10 | 11 | # Gradle 12 | build 13 | .gradle 14 | .gradletasknamecache 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple TicTacToe Game # 2 | by Tim Tsu 3 | 4 | ## Getting started ## 5 | ### Prerequisites ### 6 | * JRE 1.6 7 | * Maven 2 or Gradle 8 | 9 | ### Build and Run ### 10 | * Maven 11 | mvn package 12 | java -jar target/tictactoe-1.0.jar 13 | * Gradle 14 | gradle installApp 15 | ./build/install/tictactoe-java/bin/tictactoe-java -------------------------------------------------------------------------------- /src/main/java/ttsu/game/ai/heuristic/StateEvaluator.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.ai.heuristic; 2 | 3 | import ttsu.game.DiscreteGameState; 4 | 5 | /** 6 | * An evaluator that examines a {@link DiscreteGameState} and calculates a simple heuristic score. 7 | * 8 | * @author Tim Tsu 9 | * 10 | * @param the type of game state that this evaluator examines 11 | */ 12 | public interface StateEvaluator { 13 | 14 | /** 15 | * Computes the heuristic score for a given game state. 16 | * 17 | * @param state the {@link DiscreteGameState} to evaluate 18 | * @return an integer score 19 | */ 20 | int evaluate(T state); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/DiscreteGameState.java: -------------------------------------------------------------------------------- 1 | package ttsu.game; 2 | 3 | import java.util.List; 4 | 5 | public interface DiscreteGameState { 6 | 7 | /** 8 | * Gets a list of available next states from the current game state. 9 | * 10 | * @return a {@link List} of available {@link DiscreteGameState}s; an empty list when there are no 11 | * available states. This will never be null. 12 | */ 13 | List availableStates(); 14 | 15 | /** 16 | * Gets whether this game state represents a terminal state. 17 | * 18 | * @return true if the game is over; false otherwise. 19 | */ 20 | boolean isOver(); 21 | 22 | // TODO: consider getCurrentPlayer so it doesn't have to be passed into the 23 | // minimax evaluation. 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/tictactoe/TicTacToeMain.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import java.util.Scanner; 4 | 5 | import ttsu.game.ai.GameIntelligenceAgent; 6 | import ttsu.game.ai.MinimaxAgent; 7 | import ttsu.game.ai.heuristic.tictactoe.TicTacToeEvaluator; 8 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 9 | 10 | public class TicTacToeMain { 11 | 12 | /** 13 | * @param args 14 | */ 15 | public static void main(String[] args) { 16 | TicTacToeEvaluator evaluator = new TicTacToeEvaluator(Player.O); 17 | GameIntelligenceAgent agent = 18 | new MinimaxAgent(evaluator); 19 | Scanner scanner = new Scanner(System.in); 20 | TicTacToeGameRunner game = new TicTacToeGameRunner(agent, scanner, System.out); 21 | 22 | game.run(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/tictactoe/TicTacToeBoardPrinter.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import java.io.PrintStream; 4 | 5 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 6 | 7 | /** 8 | * Prints a TicTacToe game board to the console. 9 | * 10 | * @author Tim Tsu 11 | * 12 | */ 13 | public class TicTacToeBoardPrinter { 14 | 15 | private PrintStream printStream; 16 | 17 | public TicTacToeBoardPrinter(PrintStream printStream) { 18 | this.printStream = printStream; 19 | } 20 | 21 | /** 22 | * Prints the TicTacToe game board. 23 | * 24 | * @param board the {@link GameBoard} to print; cannot be null 25 | */ 26 | public void printGameBoard(GameBoard board) { 27 | printRow(0, board); 28 | printStream.println("-+-+-"); 29 | printRow(1, board); 30 | printStream.println("-+-+-"); 31 | printRow(2, board); 32 | } 33 | 34 | private void printRow(int row, GameBoard board) { 35 | printStream.printf("%s|%s|%s\n", markToString(board.getMark(row, 0)), 36 | markToString(board.getMark(row, 1)), markToString(board.getMark(row, 2))); 37 | } 38 | 39 | private static String markToString(Player player) { 40 | return player == null ? " " : player.toString(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/ai/GameIntelligenceAgent.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.ai; 2 | 3 | import ttsu.game.DiscreteGameState; 4 | 5 | /** 6 | * A game agent for discrete game states that evaluates the next game state from the current game 7 | * state. 8 | * 9 | * @author Tim Tsu 10 | * 11 | * @param the type of {@link DiscreteGameState} for the game that this agent plays 12 | */ 13 | public interface GameIntelligenceAgent { 14 | 15 | /** 16 | * Returns the game state representing the game after this agent makes its move. 17 | * 18 | * @param currentState the current state of the game 19 | * @return the next game state; or null if there are no more states available 20 | */ 21 | T evaluateNextState(T currentState); 22 | 23 | /** 24 | * Returns the game state representing the game after this agent makes its move. Limits the 25 | * evaluation to a specific search depth. 26 | * 27 | * @param currentState the current state of the game 28 | * @param depth the limit to the number of future game states used to evaluate the next move 29 | * @return the next game state; or null if there are no more states available 30 | */ 31 | T evaluateNextState(T currentState, int depth); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/Position.java: -------------------------------------------------------------------------------- 1 | package ttsu.game; 2 | 3 | /** 4 | * A position on a 2D game board. Positions are represented as a row and column beginning at the top 5 | * left corner. 6 | * 7 | * @author Tim Tsu 8 | * 9 | */ 10 | public final class Position { 11 | private final int row; 12 | private final int col; 13 | 14 | /** 15 | * Creates a position at the given row and column. 16 | * 17 | * @param row 18 | * @param col 19 | */ 20 | public Position(int row, int col) { 21 | this.row = row; 22 | this.col = col; 23 | } 24 | 25 | public int getRow() { 26 | return row; 27 | } 28 | 29 | public int getCol() { 30 | return col; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object obj) { 35 | if (this == obj) { 36 | return true; 37 | } 38 | if (!(obj instanceof Position)) { 39 | return false; 40 | } 41 | Position other = (Position) obj; 42 | return row == other.row && col == other.col; 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return row * 3 + col; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | StringBuilder builder = new StringBuilder("Position: "); 53 | return builder.append('(').append(row).append(',').append(col).append(')').toString(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | com.timtsu 6 | tictactoe 7 | 1.0 8 | 9 | 10 | junit 11 | junit 12 | 4.10 13 | test 14 | 15 | 16 | org.mockito 17 | mockito-core 18 | 1.8.5 19 | test 20 | 21 | 22 | org.easytesting 23 | fest-assert 24 | 1.4 25 | test 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 2.3.2 34 | 35 | 1.6 36 | 1.6 37 | 38 | 39 | 40 | org.apache.maven.plugins 41 | maven-jar-plugin 42 | 2.3.2 43 | 44 | 45 | 46 | ttsu.game.tictactoe.TicTacToeMain 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/ai/heuristic/tictactoe/TicTacToeEvaluator.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.ai.heuristic.tictactoe; 2 | 3 | import static ttsu.game.tictactoe.TicTacToeGameState.Player.opponentOf; 4 | import ttsu.game.ai.heuristic.StateEvaluator; 5 | import ttsu.game.tictactoe.TicTacToeGameState; 6 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 7 | 8 | /** 9 | * A {@link StateEvaluator} for a {@link TicTacToeGameState} game state, relative to a particular 10 | * player. 11 | *

12 | * The score is calculated such that a winning state always has a higher score than a drawn state 13 | * and a drawn state always has a higher score than a losing state. A winning state in less moves 14 | * has a higher score than a winning state in more moves. 15 | *

16 | * 17 | * @author Tim Tsu 18 | * 19 | */ 20 | public class TicTacToeEvaluator implements StateEvaluator { 21 | 22 | private final Player player; 23 | 24 | /** 25 | * Crates a new {@link TicTacToeEvaluator} that scores winning states relative to a given player. 26 | * 27 | * @param player a TicTacToe {@link Player}; cannot be null 28 | */ 29 | public TicTacToeEvaluator(Player player) { 30 | if (player == null) { 31 | throw new IllegalArgumentException("player cannot be null"); 32 | } 33 | this.player = player; 34 | } 35 | 36 | @Override 37 | public int evaluate(TicTacToeGameState game) { 38 | if (game == null) { 39 | throw new IllegalArgumentException("cannot evaluate null game"); 40 | } 41 | if (game.hasWin(player)) { 42 | return game.availableStates().size() + 1; 43 | } else if (game.hasWin(opponentOf(player))) { 44 | return -1; 45 | } else { 46 | return 0; 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/ttsu/game/tictactoe/TicTacToeBoardPrinterTest.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import static org.mockito.Mockito.inOrder; 4 | import static ttsu.game.tictactoe.TicTacToeGameState.Player.O; 5 | import static ttsu.game.tictactoe.TicTacToeGameState.Player.X; 6 | 7 | import java.io.PrintStream; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.InOrder; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | 16 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class TicTacToeBoardPrinterTest { 20 | private TicTacToeBoardPrinter printer; 21 | @Mock 22 | private PrintStream printStream; 23 | 24 | @Before 25 | public void setup() { 26 | printer = new TicTacToeBoardPrinter(printStream); 27 | } 28 | 29 | @Test 30 | public void printGameBoardEmpty() { 31 | GameBoard board = new GameBoard(); 32 | 33 | printer.printGameBoard(board); 34 | 35 | InOrder inOrder = inOrder(printStream); 36 | inOrder.verify(printStream).printf("%s|%s|%s\n", " ", " ", " "); 37 | inOrder.verify(printStream).println("-+-+-"); 38 | inOrder.verify(printStream).printf("%s|%s|%s\n", " ", " ", " "); 39 | inOrder.verify(printStream).println("-+-+-"); 40 | inOrder.verify(printStream).printf("%s|%s|%s\n", " ", " ", " "); 41 | } 42 | 43 | @Test 44 | public void printGameBoard() { 45 | GameBoard board = new GameBoard(new Player[][] { {O, X, O}, {X, null, O}, {X, O, X}}); 46 | 47 | printer.printGameBoard(board); 48 | 49 | InOrder inOrder = inOrder(printStream); 50 | inOrder.verify(printStream).printf("%s|%s|%s\n", "O", "X", "O"); 51 | inOrder.verify(printStream).println("-+-+-"); 52 | inOrder.verify(printStream).printf("%s|%s|%s\n", "X", " ", "O"); 53 | inOrder.verify(printStream).println("-+-+-"); 54 | inOrder.verify(printStream).printf("%s|%s|%s\n", "X", "O", "X"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/ttsu/game/ai/heuristic/tictactoe/TicTacToeEvaluatorTest.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.ai.heuristic.tictactoe; 2 | 3 | import static org.fest.assertions.Assertions.assertThat; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.Before; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import org.junit.rules.ExpectedException; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | 16 | import ttsu.game.DiscreteGameState; 17 | import ttsu.game.tictactoe.TicTacToeGameState; 18 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class TicTacToeEvaluatorTest { 22 | 23 | private TicTacToeEvaluator evaluator; 24 | @Mock 25 | private TicTacToeGameState game; 26 | @Mock 27 | private List availableStates; 28 | 29 | @Rule 30 | public ExpectedException thrown = ExpectedException.none(); 31 | 32 | @Before 33 | public void setup() { 34 | evaluator = new TicTacToeEvaluator(Player.X); 35 | } 36 | 37 | // -- constructor 38 | 39 | @Test 40 | public void constructorNullPlayer() { 41 | thrown.expect(IllegalArgumentException.class); 42 | thrown.expectMessage("player cannot be null"); 43 | new TicTacToeEvaluator(null); 44 | } 45 | 46 | // -- evaluate 47 | 48 | @Test 49 | public void evaluateWin() { 50 | when(game.hasWin(Player.X)).thenReturn(true); 51 | assertThat(evaluator.evaluate(game)).isEqualTo(1); 52 | } 53 | 54 | @Test 55 | public void evaluateWinConsidersAvailableMoves() { 56 | when(game.hasWin(Player.X)).thenReturn(true); 57 | when(game.availableStates()).thenReturn(availableStates); 58 | when(availableStates.size()).thenReturn(5); 59 | assertThat(evaluator.evaluate(game)).isEqualTo(6); 60 | } 61 | 62 | @Test 63 | public void evaluateLoss() { 64 | when(game.hasWin(Player.O)).thenReturn(true); 65 | assertThat(evaluator.evaluate(game)).isEqualTo(-1); 66 | } 67 | 68 | @Test 69 | public void evaluateDraw() { 70 | assertThat(evaluator.evaluate(game)).isEqualTo(0); 71 | } 72 | 73 | @Test 74 | public void evaluateNull() { 75 | thrown.expect(IllegalArgumentException.class); 76 | thrown.expectMessage("cannot evaluate null game"); 77 | evaluator.evaluate(null); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/ttsu/game/tictactoe/GameBoardTest.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import static org.fest.assertions.Assertions.assertThat; 4 | 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.ExpectedException; 9 | 10 | import ttsu.game.Position; 11 | import ttsu.game.tictactoe.GameBoard; 12 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 13 | 14 | 15 | public class GameBoardTest { 16 | 17 | private GameBoard board; 18 | 19 | @Rule 20 | public ExpectedException thrown = ExpectedException.none(); 21 | 22 | @Before 23 | public void setup() { 24 | board = new GameBoard(); 25 | } 26 | 27 | // -- constructor 28 | 29 | @Test 30 | public void copyConstructor() { 31 | board.mark(0, 0, Player.X); 32 | GameBoard newBoard = new GameBoard(board); 33 | assertThat(newBoard.getMark(0, 0)).isEqualTo(Player.X); 34 | 35 | newBoard.mark(1, 1, Player.O); 36 | assertThat(board.getMark(1, 1)).isNotEqualTo(Player.X); 37 | } 38 | 39 | // -- getMark 40 | @Test 41 | public void getMarkUnmarked() { 42 | assertThat(board.getMark(0, 0)).isNull(); 43 | } 44 | 45 | @Test 46 | public void getMarkOffBoard() { 47 | thrown.expect(IllegalArgumentException.class); 48 | thrown.expectMessage("(3,0) is off the board"); 49 | board.getMark(3, 0); 50 | } 51 | 52 | @Test 53 | public void getMarkOffBoardNegative() { 54 | thrown.expect(IllegalArgumentException.class); 55 | thrown.expectMessage("(-1,0) is off the board"); 56 | board.getMark(-1, 0); 57 | } 58 | 59 | // -- mark 60 | 61 | @Test 62 | public void markOnBoard() { 63 | boolean success = board.mark(0, 0, Player.O); 64 | 65 | assertThat(success).isTrue(); 66 | assertThat(board.getMark(0, 0)).isEqualTo(Player.O); 67 | } 68 | 69 | @Test 70 | public void markTwice() { 71 | board.mark(0, 0, Player.O); 72 | boolean success = board.mark(0, 0, Player.X); 73 | 74 | assertThat(success).isFalse(); 75 | assertThat(board.getMark(0, 0)).isEqualTo(Player.O); 76 | } 77 | 78 | @Test 79 | public void markNull() { 80 | thrown.expect(IllegalArgumentException.class); 81 | thrown.expectMessage("cannot mark null player"); 82 | board.mark(0, 0, null); 83 | } 84 | 85 | @Test 86 | public void markOffBoard() { 87 | thrown.expect(IllegalArgumentException.class); 88 | thrown.expectMessage("(3,0) is off the board"); 89 | board.mark(3, 0, null); 90 | } 91 | 92 | // -- getOpenPositions 93 | 94 | @Test 95 | public void getOpenPositionsAll() { 96 | assertThat(board.getOpenPositions()).containsOnly(new Position(0, 0), new Position(0, 1), 97 | new Position(0, 2), new Position(1, 0), new Position(1, 1), new Position(1, 2), 98 | new Position(2, 0), new Position(2, 1), new Position(2, 2)); 99 | } 100 | 101 | @Test 102 | public void getOpenPositions() { 103 | board.mark(0, 0, Player.X); 104 | assertThat(board.getOpenPositions()).containsOnly(new Position(0, 1), new Position(0, 2), 105 | new Position(1, 0), new Position(1, 1), new Position(1, 2), new Position(2, 0), 106 | new Position(2, 1), new Position(2, 2)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/ttsu/game/tictactoe/TicTacToeGameRunnerTest.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import static org.fest.assertions.Assertions.assertThat; 4 | import static org.fest.util.Strings.join; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.io.PrintStream; 11 | import java.util.Scanner; 12 | 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mock; 16 | import org.mockito.Mockito; 17 | import org.mockito.runners.MockitoJUnitRunner; 18 | 19 | import ttsu.game.Position; 20 | import ttsu.game.ai.GameIntelligenceAgent; 21 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 22 | 23 | @RunWith(MockitoJUnitRunner.class) 24 | public class TicTacToeGameRunnerTest { 25 | 26 | String line = System.getProperty("line.separator"); 27 | 28 | @Mock 29 | private GameIntelligenceAgent agent; 30 | @Mock 31 | private PrintStream printStream; 32 | 33 | @Test 34 | public void moveHumanContinuesToAcceptInputUntilValid() { 35 | Scanner scanner = scannerWithInputs("", " 1, 1", "invalid", "-1,1", "3,1", "1,2,3", "0,0"); 36 | TicTacToeGameRunner runner = new TicTacToeGameRunner(agent, scanner, printStream); 37 | 38 | runner.moveHuman(); 39 | 40 | verify(printStream, times(6)).println(TicTacToeGameRunner.INSTRUCTION_TEXT); 41 | } 42 | 43 | @Test 44 | public void moveHumanErrorWhenOffBoard() { 45 | Scanner scanner = scannerWithInputs("-1,0", "3,3", "0,0"); 46 | TicTacToeGameRunner runner = new TicTacToeGameRunner(agent, scanner, printStream); 47 | 48 | runner.moveHuman(); 49 | 50 | verify(printStream).printf("(%d,%d) is not on the board. ", -1, 0); 51 | verify(printStream).printf("(%d,%d) is not on the board. ", 3, 3); 52 | verify(printStream, times(2)).println(TicTacToeGameRunner.INSTRUCTION_TEXT); 53 | } 54 | 55 | @Test 56 | public void moveHumanErrorWhenRepeatMove() { 57 | Scanner scanner = scannerWithInputs("1,1", "1,1", "0,0"); 58 | TicTacToeGameRunner runner = new TicTacToeGameRunner(agent, scanner, printStream); 59 | 60 | runner.moveHuman(); 61 | runner.moveHuman(); 62 | 63 | verify(printStream).printf("(%d,%d) has already been taken. ", 1, 1); 64 | verify(printStream).println(TicTacToeGameRunner.INSTRUCTION_TEXT); 65 | } 66 | 67 | @Test 68 | public void moveHumanSwitchesPlayers() { 69 | Scanner scanner = scannerWithInputs("1,1", "0,0"); 70 | TicTacToeGameRunner runner = new TicTacToeGameRunner(agent, scanner, printStream); 71 | 72 | assertThat(runner.getGame().getCurrentPlayer()).isEqualTo(Player.X); 73 | runner.moveHuman(); 74 | assertThat(runner.getGame().getCurrentPlayer()).isEqualTo(Player.O); 75 | } 76 | 77 | @Test 78 | public void moveComputerSwitchesPlayers() { 79 | TicTacToeGameRunner runner = new TicTacToeGameRunner(agent, new Scanner(""), printStream); 80 | TicTacToeGameState nextState = mock(TicTacToeGameState.class); 81 | when(nextState.getLastMove()).thenReturn(new Position(0, 0)); 82 | when(agent.evaluateNextState(Mockito.any(TicTacToeGameState.class))).thenReturn(nextState); 83 | 84 | assertThat(runner.getGame().getCurrentPlayer()).isEqualTo(Player.X); 85 | runner.moveComputer(); 86 | assertThat(runner.getGame().getCurrentPlayer()).isEqualTo(Player.O); 87 | } 88 | 89 | // -- helper -- 90 | 91 | private Scanner scannerWithInputs(String... inputs) { 92 | String joinedInputs = join(inputs).with("\n"); 93 | Scanner scanner = new Scanner(joinedInputs); 94 | return scanner; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/ai/MinimaxAgent.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.ai; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import ttsu.game.DiscreteGameState; 9 | import ttsu.game.ai.heuristic.StateEvaluator; 10 | 11 | /** 12 | * Implementation of {@link GameIntelligenceAgent} that evaluates the next optimal game state using 13 | * the minimax algorithm, with a-b pruning. 14 | * 15 | * @author Tim Tsu 16 | * 17 | * @param the type of {@link DiscreteGameState} to evaluate 18 | */ 19 | public class MinimaxAgent implements GameIntelligenceAgent { 20 | 21 | private static class Node { 22 | private S state; 23 | private int alpha; 24 | private int beta; 25 | private int value; 26 | private List> children; 27 | 28 | public Node(S state, int alpha, int beta) { 29 | this.alpha = alpha; 30 | this.beta = beta; 31 | this.state = state; 32 | } 33 | } 34 | 35 | private final StateEvaluator evaluator; 36 | 37 | /** 38 | * Creates a new instance of {@link MinimaxAgent} that uses the given {@link StateEvaluator} for 39 | * measuring the value of each game state. 40 | * 41 | * @param evaluator the {@link StateEvaluator} used to measure the value of each game state; 42 | * cannot be null 43 | */ 44 | public MinimaxAgent(StateEvaluator evaluator) { 45 | if (evaluator == null) { 46 | throw new IllegalArgumentException("evaluator cannot be null"); 47 | } 48 | this.evaluator = evaluator; 49 | } 50 | 51 | @Override 52 | public T evaluateNextState(T currentState) { 53 | return evaluateNextState(currentState, Integer.MAX_VALUE); 54 | } 55 | 56 | @Override 57 | public T evaluateNextState(T currentState, int depth) { 58 | if (currentState == null) { 59 | throw new IllegalArgumentException("initialState cannot be null"); 60 | } 61 | if (depth < 0) { 62 | throw new IllegalArgumentException("depth cannot be less than zero. depth=" + depth); 63 | } 64 | Node root = buildTree(currentState, depth); 65 | for (Node child : root.children) { 66 | if (child.value == root.value) { 67 | return child.state; 68 | } 69 | } 70 | return null; 71 | } 72 | 73 | @SuppressWarnings("unchecked") 74 | private Node buildTree(T state, boolean max, int alpha, int beta, int depth) { 75 | Node current = new Node(state, alpha, beta); 76 | current.value = max ? alpha : beta; 77 | if (depth == 0 || state.isOver()) { 78 | current.value = evaluator.evaluate(state); 79 | current.alpha = current.value; 80 | current.beta = current.value; 81 | current.state = state; 82 | current.children = Collections.emptyList(); 83 | return current; 84 | } else { 85 | ArrayList> children = new ArrayList>(); 86 | for (DiscreteGameState nextState : state.availableStates()) { 87 | Node child = buildTree((T) nextState, !max, current.alpha, current.beta, depth - 1); 88 | if (max && child.value > current.value) { 89 | current.value = child.value; 90 | current.alpha = child.value; 91 | } else if (!max && child.value < current.value) { 92 | current.value = child.value; 93 | current.beta = child.value; 94 | } 95 | // prune condition 96 | if (current.alpha >= current.beta) { 97 | break; 98 | } 99 | children.add(child); 100 | } 101 | current.children = children; 102 | } 103 | return current; 104 | } 105 | 106 | private Node buildTree(T state, int depth) { 107 | return buildTree(state, true, Integer.MIN_VALUE, Integer.MAX_VALUE, depth); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/ttsu/game/ai/tictactoe/TicTacToeMinimaxAgentTest.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.ai.tictactoe; 2 | 3 | import static org.fest.assertions.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | import static ttsu.game.tictactoe.TicTacToeGameState.Player.O; 7 | import static ttsu.game.tictactoe.TicTacToeGameState.Player.X; 8 | 9 | import org.fest.util.Collections; 10 | import org.junit.Before; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.junit.rules.ExpectedException; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mock; 16 | import org.mockito.runners.MockitoJUnitRunner; 17 | 18 | import ttsu.game.DiscreteGameState; 19 | import ttsu.game.ai.MinimaxAgent; 20 | import ttsu.game.ai.heuristic.tictactoe.TicTacToeEvaluator; 21 | import ttsu.game.tictactoe.GameBoard; 22 | import ttsu.game.tictactoe.TicTacToeGameState; 23 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class TicTacToeMinimaxAgentTest { 27 | 28 | private MinimaxAgent agent; 29 | 30 | @Mock 31 | private TicTacToeEvaluator evaluator; 32 | 33 | @Mock 34 | private TicTacToeGameState gameState; 35 | 36 | @Rule 37 | public ExpectedException thrown = ExpectedException.none(); 38 | 39 | @Before 40 | public void setup() { 41 | agent = new MinimaxAgent(evaluator); 42 | } 43 | 44 | @Test 45 | public void evaluateLeaf() { 46 | MinimaxAgent minimaxEval = new MinimaxAgent(evaluator); 47 | assertThat(minimaxEval.evaluateNextState(gameState, 0)).isNull(); 48 | } 49 | 50 | @Test 51 | public void evaluateNegativeDepth() { 52 | thrown.expect(IllegalArgumentException.class); 53 | thrown.expectMessage("depth cannot be less than zero. depth=-1"); 54 | agent.evaluateNextState(gameState, -1); 55 | } 56 | 57 | // choose winning move 58 | @Test 59 | public void preferWinningState() { 60 | TicTacToeGameState winState = mock(TicTacToeGameState.class); 61 | TicTacToeGameState drawState = mock(TicTacToeGameState.class); 62 | when(winState.isOver()).thenReturn(true); 63 | when(drawState.isOver()).thenReturn(true); 64 | when(evaluator.evaluate(winState)).thenReturn(100); 65 | when(evaluator.evaluate(drawState)).thenReturn(0); 66 | when(gameState.availableStates()).thenReturn( 67 | Collections.list(winState, drawState)); 68 | 69 | TicTacToeGameState actualState = agent.evaluateNextState(gameState); 70 | assertThat(actualState).isSameAs(winState); 71 | } 72 | 73 | // prevent loss. this is essentially the same as above... 74 | @Test 75 | public void preventLosingMove() { 76 | TicTacToeGameState loseState = mock(TicTacToeGameState.class); 77 | TicTacToeGameState drawState = mock(TicTacToeGameState.class); 78 | when(loseState.isOver()).thenReturn(true); 79 | when(drawState.isOver()).thenReturn(true); 80 | when(evaluator.evaluate(loseState)).thenReturn(-1); 81 | when(evaluator.evaluate(drawState)).thenReturn(0); 82 | when(gameState.availableStates()).thenReturn( 83 | Collections.list(loseState, drawState)); 84 | 85 | TicTacToeGameState actualState = agent.evaluateNextState(gameState); 86 | assertThat(actualState).isSameAs(drawState); 87 | } 88 | 89 | @Test 90 | public void preferEarlyWin() { 91 | TicTacToeGameState gameState = 92 | new TicTacToeGameState(new GameBoard(new Player[][] { {O, null, null}, {O, X, X}, 93 | {null, null, X}}), O); 94 | MinimaxAgent agent = 95 | new MinimaxAgent(new TicTacToeEvaluator(O)); 96 | TicTacToeGameState actualState = agent.evaluateNextState(gameState); 97 | assertThat(actualState.hasWin(O)).isTrue(); 98 | } 99 | 100 | // /game already over 101 | @Test 102 | public void evaluateGameAlreadyOver() { 103 | when(gameState.isOver()).thenReturn(true); 104 | assertThat(agent.evaluateNextState(gameState)).isNull(); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/tictactoe/TicTacToeGameRunner.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import java.io.PrintStream; 4 | import java.util.Scanner; 5 | 6 | import ttsu.game.Position; 7 | import ttsu.game.ai.GameIntelligenceAgent; 8 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 9 | 10 | /** 11 | * A class for configuring and running a TicTacToe game. 12 | * 13 | * @author Tim Tsu 14 | * 15 | */ 16 | public class TicTacToeGameRunner { 17 | 18 | /** 19 | * The instructions for valid human input. 20 | */ 21 | static final String INSTRUCTION_TEXT = 22 | "Enter ',' to play a position. For example, '0,2'."; 23 | 24 | private TicTacToeGameState game; 25 | 26 | private TicTacToeBoardPrinter boardPrinter; 27 | private GameIntelligenceAgent agent; 28 | private Scanner scanner; 29 | private PrintStream printStream; 30 | 31 | /** 32 | * Creates a new game runner. 33 | * 34 | * @param agent a {@link GameIntelligenceAgent} for choosing the computer opponent's moves 35 | * @param scanner a {@link Scanner} for collecting user input 36 | * @param printStream the {@link PrintStream} for displaying text to the user 37 | */ 38 | public TicTacToeGameRunner(GameIntelligenceAgent agent, Scanner scanner, 39 | PrintStream printStream) { 40 | this.game = new TicTacToeGameState(); 41 | this.boardPrinter = new TicTacToeBoardPrinter(printStream); 42 | this.agent = agent; 43 | this.scanner = scanner; 44 | this.printStream = printStream; 45 | } 46 | 47 | /** 48 | * Runs the TicTacToe game, alternating between human and computer moves until the game is over. 49 | */ 50 | public void run() { 51 | printInstructions(); 52 | while (!game.isOver()) { 53 | moveHuman(); 54 | moveComputer(); 55 | boardPrinter.printGameBoard(game.getGameBoard()); 56 | } 57 | printGameOver(); 58 | } 59 | 60 | /** 61 | * Gets the current game state. 62 | * 63 | * @return the game 64 | */ 65 | TicTacToeGameState getGame() { 66 | return game; 67 | } 68 | 69 | void moveComputer() { 70 | TicTacToeGameState nextState = agent.evaluateNextState(game); 71 | if (nextState == null) { 72 | return; 73 | } 74 | Position nextMove = nextState.getLastMove(); 75 | game.play(nextMove.getRow(), nextMove.getCol()); 76 | game.switchPlayer(); 77 | } 78 | 79 | void moveHuman() { 80 | Position userPosition; 81 | while (true) { 82 | do { 83 | printStream.print("Player X [row,col]: "); 84 | String input = scanner.nextLine(); 85 | userPosition = parseUserInput(input); 86 | } while (userPosition == null); 87 | 88 | try { 89 | if (game.play(userPosition.getRow(), userPosition.getCol())) { 90 | game.switchPlayer(); 91 | return; 92 | } else { 93 | printStream.printf("(%d,%d) has already been taken. ", userPosition.getRow(), 94 | userPosition.getCol()); 95 | printInstructions(); 96 | } 97 | } catch (IllegalArgumentException e) { 98 | printStream.printf("(%d,%d) is not on the board. ", userPosition.getRow(), 99 | userPosition.getCol()); 100 | printInstructions(); 101 | } 102 | } 103 | } 104 | 105 | private void printGameOver() { 106 | if (game.hasWin(Player.X)) { 107 | ((PrintStream) printStream).println("Player X won."); 108 | } else if (game.hasWin(Player.O)) { 109 | printStream.println("Player O won."); 110 | } else { 111 | printStream.println("Game ended in a draw."); 112 | } 113 | } 114 | 115 | private void printInstructions() { 116 | printStream.println(INSTRUCTION_TEXT); 117 | } 118 | 119 | private Position parseUserInput(String input) { 120 | String[] posInput = input.split(","); 121 | if (posInput.length != 2) { 122 | printInstructions(); 123 | return null; 124 | } 125 | int row, col; 126 | try { 127 | row = Integer.parseInt(posInput[0]); 128 | col = Integer.parseInt(posInput[1]); 129 | } catch (NumberFormatException e) { 130 | printInstructions(); 131 | return null; 132 | } 133 | return new Position(row, col); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/tictactoe/GameBoard.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import ttsu.game.Position; 8 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 9 | 10 | /** 11 | * Represents a TicTacToe game board. 12 | * 13 | * @author Tim Tsu 14 | * 15 | */ 16 | public class GameBoard { 17 | 18 | private static final int COLS = 3; 19 | private static final int ROWS = 3; 20 | 21 | private final Player[][] board; 22 | 23 | /** 24 | * Creates a new blank TicTacToe board. 25 | */ 26 | public GameBoard() { 27 | board = new Player[ROWS][COLS]; 28 | } 29 | 30 | /** 31 | * Create a game board from an array. 32 | * 33 | * @param board the {@link Player} array to use as a game board 34 | */ 35 | public GameBoard(Player[][] board) { 36 | if (board == null) { 37 | throw new IllegalArgumentException("board cannot be null"); 38 | } 39 | this.board = board; 40 | } 41 | 42 | /** 43 | * Create a deep copy of another game board. 44 | * 45 | * @param board the board to copy 46 | */ 47 | public GameBoard(GameBoard other) { 48 | board = new Player[ROWS][COLS]; 49 | for (int row = 0; row < ROWS; row++) { 50 | for (int col = 0; col < COLS; col++) { 51 | board[row][col] = other.board[row][col]; 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Marks a position on the game board for a given player. 58 | * 59 | * @param row the row of the position on the board to mark 60 | * @param col the column of the position on the board to mark 61 | * @param player the {@link Player} marking the board 62 | * @return true if the position was open to mark; false if the position 63 | * was already marked 64 | * @throws IllegalArgumentException if the given position is off the board or the player is 65 | * null 66 | */ 67 | public boolean mark(int row, int col, Player player) { 68 | validatePosition(row, col); 69 | if (player == null) { 70 | throw new IllegalArgumentException("cannot mark null player"); 71 | } 72 | if (board[row][col] != null) { 73 | return false; 74 | } else { 75 | board[row][col] = player; 76 | return true; 77 | } 78 | } 79 | 80 | /** 81 | * Gets the mark at the given board position. 82 | * 83 | * @param row the row of the position to inspect 84 | * @param col the column of the position to inspect 85 | * @return the {@link Player} that marked the given position, or null if position is 86 | * open 87 | */ 88 | public Player getMark(int row, int col) { 89 | validatePosition(row, col); 90 | return board[row][col]; 91 | } 92 | 93 | /** 94 | * Gets the list of open positions on the game board. 95 | * 96 | * @return a {@link List} of {@link Position}s; will never be null 97 | */ 98 | public List getOpenPositions() { 99 | ArrayList positions = new ArrayList(); 100 | for (int row = 0; row < ROWS; row++) { 101 | for (int col = 0; col < COLS; col++) { 102 | if (board[row][col] == null) { 103 | positions.add(new Position(row, col)); 104 | } 105 | } 106 | } 107 | return positions; 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | StringBuilder sb = new StringBuilder(); 113 | for (int row = 0; row < ROWS; row++) { 114 | for (int col = 0; col < COLS; col++) { 115 | Player p = board[row][col]; 116 | if (p != null) { 117 | sb.append(p); 118 | } else { 119 | sb.append(' '); 120 | } 121 | } 122 | sb.append('\n'); 123 | } 124 | return sb.toString(); 125 | } 126 | 127 | @Override 128 | public boolean equals(Object obj) { 129 | if (this == obj) { 130 | return true; 131 | } 132 | if (!(obj instanceof GameBoard)) { 133 | return false; 134 | } 135 | GameBoard other = (GameBoard) obj; 136 | for (int row = 0; row < ROWS; row++) { 137 | if (!Arrays.equals(board[row], other.board[row])) { 138 | return false; 139 | } 140 | } 141 | return true; 142 | } 143 | 144 | @Override 145 | public int hashCode() { 146 | final int prime = 31; 147 | int result = 1; 148 | for (int row = 0; row < ROWS; row++) { 149 | result = prime * result + Arrays.hashCode(board[row]); 150 | } 151 | return result; 152 | } 153 | 154 | private static void validatePosition(int row, int col) { 155 | if (row < 0 || row >= ROWS || col < 0 || col >= COLS) { 156 | throw new IllegalArgumentException("(" + row + "," + col + ") is off the board"); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/test/java/ttsu/game/tictactoe/TicTacToeGameStateTest.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import static org.fest.assertions.Assertions.assertThat; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.ExpectedException; 11 | 12 | import ttsu.game.DiscreteGameState; 13 | import ttsu.game.Position; 14 | import ttsu.game.tictactoe.TicTacToeGameState.Player; 15 | 16 | 17 | public class TicTacToeGameStateTest { 18 | 19 | private TicTacToeGameState game; 20 | 21 | @Rule 22 | public ExpectedException thrown = ExpectedException.none(); 23 | 24 | @Before 25 | public void setup() { 26 | game = new TicTacToeGameState(); 27 | } 28 | 29 | // -- constructor 30 | 31 | @Test 32 | public void startingPlayerIsX() { 33 | assertThat(new TicTacToeGameState().getCurrentPlayer()).isEqualTo(Player.X); 34 | } 35 | 36 | @Test 37 | public void copyConstructorDeepCopiesBoard() { 38 | game.play(0, 0); 39 | TicTacToeGameState copy = new TicTacToeGameState(game); 40 | assertThat(copy.getGameBoard()).isEqualTo(game.getGameBoard()).isNotSameAs(game.getGameBoard()); 41 | assertThat(copy.getLastMove()).isEqualTo(game.getLastMove()); 42 | assertThat(copy.getCurrentPlayer()).isEqualTo(game.getCurrentPlayer()); 43 | } 44 | 45 | // -- availableStates 46 | 47 | @Test 48 | public void getAvaliableStatesEmptyBoard() { 49 | TicTacToeGameState game = new TicTacToeGameState(); 50 | List states = game.availableStates(); 51 | assertThat(states).hasSize(9); 52 | } 53 | 54 | @Test 55 | public void getAvailableStatesLastOne() { 56 | TicTacToeGameState game = new TicTacToeGameState(); 57 | game.play(0, 0); 58 | game.play(0, 1); 59 | game.play(0, 2); 60 | game.play(1, 0); 61 | game.play(1, 1); 62 | game.play(1, 2); 63 | game.play(2, 0); 64 | game.play(2, 1); 65 | 66 | List states = game.availableStates(); 67 | assertThat(states).hasSize(1); 68 | TicTacToeGameState availableState = (TicTacToeGameState) states.get(0); 69 | assertThat(availableState.getCurrentPlayer()).isEqualTo( 70 | Player.opponentOf(game.getCurrentPlayer())); 71 | assertThat(availableState.getLastMove()).isEqualTo(new Position(2, 2)); 72 | } 73 | 74 | @Test 75 | public void getAvailableStatesCompleteBoard() { 76 | TicTacToeGameState game = new TicTacToeGameState(); 77 | game.play(0, 0); 78 | game.play(0, 1); 79 | game.play(0, 2); 80 | game.play(1, 0); 81 | game.play(1, 1); 82 | game.play(1, 2); 83 | game.play(2, 0); 84 | game.play(2, 1); 85 | game.play(2, 2); 86 | 87 | assertThat(game.availableStates()).isEmpty(); 88 | } 89 | 90 | // -- hasWin 91 | 92 | @Test 93 | public void hasWinRow() { 94 | game.play(0, 0); 95 | game.play(0, 1); 96 | game.play(0, 2); 97 | assertThat(game.hasWin(Player.X)); 98 | } 99 | 100 | @Test 101 | public void hasWinCol() { 102 | game.play(0, 0); 103 | game.play(1, 0); 104 | game.play(2, 0); 105 | assertThat(game.hasWin(Player.X)); 106 | } 107 | 108 | @Test 109 | public void hasWinDiagonal() { 110 | game.play(0, 0); 111 | game.play(1, 1); 112 | game.play(2, 2); 113 | assertThat(game.hasWin(Player.X)); 114 | } 115 | 116 | // -- isOver 117 | 118 | @Test 119 | public void isOverWin() { 120 | game.play(0, 0); 121 | game.play(0, 1); 122 | game.play(0, 2); 123 | assertThat(game.isOver()).isTrue(); 124 | } 125 | 126 | @Test 127 | public void isOverDraw() { 128 | // XOX 129 | // OXX 130 | // OXO 131 | game.play(0, 0); 132 | game.play(0, 2); 133 | game.play(1, 1); 134 | game.play(1, 2); 135 | game.play(2, 1); 136 | game.switchPlayer(); 137 | game.play(0, 1); 138 | game.play(1, 0); 139 | game.play(2, 0); 140 | game.play(2, 2); 141 | 142 | assertThat(game.isOver()).isTrue(); 143 | } 144 | 145 | // -- play 146 | 147 | @Test 148 | public void playOnBoard() { 149 | assertThat(game.play(0, 0)).isTrue(); 150 | assertThat(game.getLastMove()).isEqualTo(new Position(0, 0)); 151 | } 152 | 153 | @Test 154 | public void playOffBoard() { 155 | thrown.expect(IllegalArgumentException.class); 156 | thrown.expectMessage("(-1,0) is off the board"); 157 | game.play(-1, 0); 158 | } 159 | 160 | @Test 161 | public void playSameLocation() { 162 | assertThat(game.play(0, 0)).isTrue(); 163 | assertThat(game.play(0, 1)).isTrue(); 164 | // should not affect the last move 165 | assertThat(game.play(0, 0)).isFalse(); 166 | assertThat(game.getLastMove()).isEqualTo(new Position(0, 1)); 167 | } 168 | 169 | // -- switchPlayer 170 | 171 | @Test 172 | public void switchPlayer() { 173 | assertThat(game.getCurrentPlayer()).isEqualTo(Player.X); 174 | game.switchPlayer(); 175 | assertThat(game.getCurrentPlayer()).isEqualTo(Player.O); 176 | game.switchPlayer(); 177 | assertThat(game.getCurrentPlayer()).isEqualTo(Player.X); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/ttsu/game/tictactoe/TicTacToeGameState.java: -------------------------------------------------------------------------------- 1 | package ttsu.game.tictactoe; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import ttsu.game.DiscreteGameState; 7 | import ttsu.game.Position; 8 | 9 | /** 10 | * A {@link DiscreteGameState} representing the current state of a TicTacToe game. 11 | * 12 | * @author Tim Tsu 13 | * 14 | */ 15 | public class TicTacToeGameState implements DiscreteGameState { 16 | public static enum Player { 17 | O, X; 18 | public static Player opponentOf(Player player) { 19 | return player == X ? O : X; 20 | } 21 | } 22 | 23 | private final GameBoard board; 24 | private Player currentPlayer; 25 | private Position lastMove; 26 | 27 | /** 28 | * Creates the initial state of a new TicTacToe game. 29 | */ 30 | public TicTacToeGameState() { 31 | board = new GameBoard(); 32 | currentPlayer = Player.X; 33 | } 34 | 35 | /** 36 | * Creates a new instance of a TicTacToe game state with a given board layout and current player. 37 | * 38 | * @param board the current board state 39 | * @param currentPlayer the current player whose turn it is to make the next move 40 | */ 41 | public TicTacToeGameState(GameBoard board, Player currentPlayer) { 42 | if (board == null) { 43 | throw new IllegalArgumentException("board cannot be null"); 44 | } 45 | if (currentPlayer == null) { 46 | throw new IllegalArgumentException("currentPlayer cannot be null"); 47 | } 48 | this.board = board; 49 | this.currentPlayer = currentPlayer; 50 | } 51 | 52 | /** 53 | * Creates a deep copy of the given TicTacToe game state. 54 | * 55 | * @param other the TicTacToe game state to copy 56 | */ 57 | public TicTacToeGameState(TicTacToeGameState other) { 58 | this.board = new GameBoard(other.board); 59 | this.currentPlayer = other.getCurrentPlayer(); 60 | this.lastMove = other.lastMove; 61 | } 62 | 63 | @Override 64 | public List availableStates() { 65 | List availableMoves = board.getOpenPositions(); 66 | List availableStates = 67 | new ArrayList(availableMoves.size()); 68 | for (Position move : availableMoves) { 69 | TicTacToeGameState newState = new TicTacToeGameState(this); 70 | newState.play(move.getRow(), move.getCol()); 71 | newState.switchPlayer(); 72 | availableStates.add(newState); 73 | } 74 | return availableStates; 75 | } 76 | 77 | /** 78 | * Gets the current player whose turn it is to make the next move. 79 | * 80 | * @return the {@link Player} who gets to make the next move 81 | */ 82 | public Player getCurrentPlayer() { 83 | return currentPlayer; 84 | } 85 | 86 | /** 87 | * Gets the last position that was played on the TicTacToe board. 88 | * 89 | * @return a {@link Position} on the TicTacToe board, or null if no moves were taken yet. 90 | */ 91 | public Position getLastMove() { 92 | return lastMove; 93 | } 94 | 95 | /** 96 | * Returns whether the given player has a winning position on the TicTacToe board. 97 | * 98 | * @param player the player to check for a win 99 | * @return true if player has won; false otherwise 100 | */ 101 | public boolean hasWin(Player player) { 102 | for (int i = 0; i < 3; i++) { 103 | if (completesRow(player, i) || completesColumn(player, i)) { 104 | return true; 105 | } 106 | } 107 | return completesDiagonal(player); 108 | } 109 | 110 | @Override 111 | public boolean isOver() { 112 | return hasWin(Player.O) || hasWin(Player.X) || board.getOpenPositions().isEmpty(); 113 | } 114 | 115 | /** 116 | * Play a move in the given row and column of the TicTacToe board with the current player. 117 | * 118 | * @param row the row to mark 119 | * @param col the column to mark 120 | * @return true if this position was playable; false otherwise 121 | */ 122 | public boolean play(int row, int col) { 123 | if (board.mark(row, col, currentPlayer)) { 124 | lastMove = new Position(row, col); 125 | return true; 126 | } 127 | return false; 128 | 129 | } 130 | 131 | /** 132 | * Gets the game board. 133 | * 134 | * @return {@link GameBoard} for the current TicTacToe game; cannot be null 135 | */ 136 | public GameBoard getGameBoard() { 137 | return board; 138 | } 139 | 140 | /** 141 | * Switches the current player. 142 | */ 143 | public void switchPlayer() { 144 | currentPlayer = Player.opponentOf(currentPlayer); 145 | } 146 | 147 | private boolean completesColumn(Player player, int col) { 148 | Player col0 = board.getMark(0, col); 149 | Player col1 = board.getMark(1, col); 150 | Player col2 = board.getMark(2, col); 151 | return player == col0 && col0 == col1 && col1 == col2; 152 | } 153 | 154 | private boolean completesDiagonal(Player player) { 155 | Player center = board.getMark(1, 1); 156 | if (player != center) { 157 | return false; 158 | } 159 | return (board.getMark(0, 0) == center && center == board.getMark(2, 2)) 160 | || (board.getMark(0, 2) == center && center == board.getMark(2, 0)); 161 | } 162 | 163 | private boolean completesRow(Player player, int row) { 164 | Player row0 = board.getMark(row, 0); 165 | Player row1 = board.getMark(row, 1); 166 | Player row2 = board.getMark(row, 2); 167 | return player == row0 && row0 == row1 && row1 == row2; 168 | } 169 | } 170 | --------------------------------------------------------------------------------