diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fc355c9..d801bf4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,97 +6,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -104,9 +15,9 @@ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 72be14a..97dd9e8 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java index 63349e2..f2f618e 100644 --- a/app/src/main/java/org/toop/Main.java +++ b/app/src/main/java/org/toop/Main.java @@ -1,16 +1,16 @@ package org.toop; +import org.toop.app.gui.LocalServerSelector; import org.toop.framework.networking.NetworkingClientManager; import org.toop.framework.networking.NetworkingInitializationException; - public class Main { - public static void main(String[] args) { + static void main(String[] args) { initSystems(); + javax.swing.SwingUtilities.invokeLater(LocalServerSelector::new); } private static void initSystems() throws NetworkingInitializationException { new NetworkingClientManager(); } - } \ No newline at end of file diff --git a/app/src/main/java/org/toop/events/WindowEvents.java b/app/src/main/java/org/toop/events/WindowEvents.java index 279ea57..59c22ff 100644 --- a/app/src/main/java/org/toop/events/WindowEvents.java +++ b/app/src/main/java/org/toop/events/WindowEvents.java @@ -4,22 +4,9 @@ import org.toop.framework.eventbus.events.EventWithoutSnowflake; import org.toop.framework.eventbus.events.EventsBase; public class WindowEvents extends EventsBase { - /** Triggers when a cell is clicked in one of the game boards. */ public record CellClicked(int cell) implements EventWithoutSnowflake {} /** Triggers when the window wants to quit. */ public record OnQuitRequested() implements EventWithoutSnowflake {} - - /** Triggers when the window is resized. */ -// public record OnResize(Window.Size size) implements EventWithoutSnowflake {} - - /** Triggers when the mouse is moved within the window. */ - public record OnMouseMove(int x, int y) implements EventWithoutSnowflake {} - - /** Triggers when the mouse is clicked within the window. */ - public record OnMouseClick(int button) implements EventWithoutSnowflake {} - - /** Triggers when the mouse is released within the window. */ - public record OnMouseRelease(int button) implements EventWithoutSnowflake {} } \ No newline at end of file diff --git a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java b/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java index f0903b2..f361d8c 100644 --- a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java +++ b/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java @@ -6,7 +6,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.events.NetworkEvents; -import org.toop.game.GameBase; +import org.toop.game.Game; +import org.toop.game.tictactoe.TicTacToe; +import org.toop.game.tictactoe.TicTacToeAI; import org.toop.tictactoe.gui.UIGameBoard; import org.toop.framework.networking.NetworkingGameClientHandler; @@ -24,8 +26,8 @@ public class LocalTicTacToe { // TODO: Implement runnable private final ExecutorService executor = Executors.newFixedThreadPool(3); private final BlockingQueue receivedQueue = new LinkedBlockingQueue<>(); - private final BlockingQueue moveQueuePlayerA = new LinkedBlockingQueue<>(); - private final BlockingQueue moveQueuePlayerB = new LinkedBlockingQueue<>(); + private final BlockingQueue moveQueuePlayerA = new LinkedBlockingQueue<>(); + private final BlockingQueue moveQueuePlayerB = new LinkedBlockingQueue<>(); private Object receivedMessageListener = null; @@ -34,8 +36,8 @@ public class LocalTicTacToe { // TODO: Implement runnable private String connectionId = null; private String serverId = null; - private boolean isAiPlayer[] = new boolean[2]; - private TicTacToeAI[] aiPlayers = new TicTacToeAI[2]; + private boolean[] isAiPlayer = new boolean[2]; + private TicTacToeAI ai = new TicTacToeAI(); private TicTacToe ticTacToe; private UIGameBoard ui; @@ -80,15 +82,6 @@ public class LocalTicTacToe { // TODO: Implement runnable private LocalTicTacToe(boolean[] aiFlags) { this.isAiPlayer = aiFlags; // store who is AI - - for (int i = 0; i < aiFlags.length && i < this.aiPlayers.length; i++) { - if (aiFlags[i]) { - this.aiPlayers[i] = new TicTacToeAI(); // create AI for that player - } else { - this.aiPlayers[i] = null; // not an AI player - } - } - this.isLocal = true; //this.executor.submit(this::localGameThread); } @@ -139,17 +132,17 @@ public class LocalTicTacToe { // TODO: Implement runnable this.ticTacToe = new TicTacToe("X", "O"); while (running) { try { - GameBase.State state; + Game.State state; if (!isAiPlayer[0]) { state = this.ticTacToe.play(this.moveQueuePlayerA.take()); } else { - int bestMove = aiPlayers[0].findBestMove(this.ticTacToe); - state = this.ticTacToe.play(bestMove); - if (state != GameBase.State.INVALID) { - ui.setCell(bestMove, "X"); - } + Game.Move bestMove = ai.findBestMove(this.ticTacToe, 9); + assert bestMove != null; + + state = this.ticTacToe.play(bestMove); + ui.setCell(bestMove.position(), "X"); } - if (state == GameBase.State.WIN || state == GameBase.State.DRAW) { + if (state == Game.State.WIN || state == Game.State.DRAW) { ui.setState(state, "X"); running = false; } @@ -157,13 +150,12 @@ public class LocalTicTacToe { // TODO: Implement runnable if (!isAiPlayer[1]) { state = this.ticTacToe.play(this.moveQueuePlayerB.take()); } else { - int bestMove = aiPlayers[1].findBestMove(this.ticTacToe); - state = this.ticTacToe.play(bestMove); - if (state != GameBase.State.INVALID) { - ui.setCell(bestMove, "O"); - } + Game.Move bestMove = ai.findBestMove(this.ticTacToe, 9); + assert bestMove != null; + state = this.ticTacToe.play(bestMove); + ui.setCell(bestMove.position(), "O"); } - if (state == GameBase.State.WIN || state == GameBase.State.DRAW) { + if (state == Game.State.WIN || state == Game.State.DRAW) { ui.setState(state, "O"); running = false; } @@ -187,7 +179,8 @@ public class LocalTicTacToe { // TODO: Implement runnable } public char[] getCurrentBoard() { - return ticTacToe.getGrid(); + //return ticTacToe.getGrid(); + return new char[2]; } /** End the current game. */ @@ -203,13 +196,13 @@ public class LocalTicTacToe { // TODO: Implement runnable () -> { try { if (this.playersTurn == 0 && !isAiPlayer[0]) { - this.moveQueuePlayerA.put(moveIndex); + this.moveQueuePlayerA.put(new Game.Move(moveIndex, 'X')); logger.info( "Adding player's {}, move: {} to queue A", this.playersTurn, moveIndex); } else if (this.playersTurn == 1 && !isAiPlayer[1]) { - this.moveQueuePlayerB.put(moveIndex); + this.moveQueuePlayerB.put(new Game.Move(moveIndex, 'O')); logger.info( "Adding player's {}, move: {} to queue B", this.playersTurn, diff --git a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java b/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java index aa9ee5c..a8c0027 100644 --- a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java +++ b/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java @@ -2,13 +2,14 @@ package org.toop.tictactoe.gui; import java.awt.*; import java.awt.event.ActionEvent; +import java.util.Locale; import javax.swing.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.toop.app.gui.LocalGameSelector; import org.toop.app.gui.RemoteGameSelector; +import org.toop.game.Game; import org.toop.tictactoe.LocalTicTacToe; -import org.toop.game.GameBase; public class UIGameBoard { private static final int TICTACTOE_SIZE = 3; @@ -44,6 +45,7 @@ public class UIGameBoard { // Back button backToMainMenuButton = new JButton("Back to Main Menu"); + tttPanel.add(backToMainMenuButton, BorderLayout.SOUTH); backToMainMenuButton.addActionListener( _ -> { @@ -117,13 +119,13 @@ public class UIGameBoard { cells[index].setText(move); } - public void setState(GameBase.State state, String playerMove) { + public void setState(Game.State state, String playerMove) { Color color; - if (state == GameBase.State.WIN && playerMove.equals(currentPlayer)) { + if (state == Game.State.WIN && playerMove.equals(currentPlayer)) { color = new Color(160, 220, 160); - } else if (state == GameBase.State.WIN) { + } else if (state == Game.State.WIN) { color = new Color(220, 160, 160); - } else if (state == GameBase.State.DRAW) { + } else if (state == Game.State.DRAW) { color = new Color(220, 220, 160); } else { color = new Color(220, 220, 220); @@ -131,7 +133,7 @@ public class UIGameBoard { for (JButton cell : cells) { cell.setBackground(color); } - if (state == GameBase.State.DRAW || state == GameBase.State.WIN) { + if (state == Game.State.DRAW || state == Game.State.WIN) { gameOver = true; } } diff --git a/game/pom.xml b/game/pom.xml index 3297948..c82a815 100644 --- a/game/pom.xml +++ b/game/pom.xml @@ -13,6 +13,50 @@ + + org.junit + junit-bom + 5.13.4 + pom + import + + + org.junit.jupiter + junit-jupiter-api + 5.13.4 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.13.4 + test + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + org.mockito + mockito-core + 5.19.0 + test + + + org.mockito + mockito-junit-jupiter + 5.19.0 + test + + org.apache.logging.log4j log4j-api diff --git a/game/src/main/java/org/toop/game/AI.java b/game/src/main/java/org/toop/game/AI.java new file mode 100644 index 0000000..0506b10 --- /dev/null +++ b/game/src/main/java/org/toop/game/AI.java @@ -0,0 +1,5 @@ +package org.toop.game; + +public abstract class AI { + public abstract Game.Move findBestMove(T game, int depth); +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/Game.java b/game/src/main/java/org/toop/game/Game.java new file mode 100644 index 0000000..b37bd73 --- /dev/null +++ b/game/src/main/java/org/toop/game/Game.java @@ -0,0 +1,57 @@ +package org.toop.game; + +import java.util.Arrays; + +public abstract class Game { + public enum State { + NORMAL, LOSE, DRAW, WIN, + } + + public record Move(int position, char value) {} + + public static final char EMPTY = (char)0; + + protected final int rowSize; + protected final int columnSize; + protected final char[] board; + + protected final Player[] players; + protected int currentPlayer; + + protected Game(int rowSize, int columnSize, Player... players) { + assert rowSize > 0 && columnSize > 0; + assert players.length >= 1; + + this.rowSize = rowSize; + this.columnSize = columnSize; + + board = new char[rowSize * columnSize]; + Arrays.fill(board, EMPTY); + + this.players = players; + currentPlayer = 0; + } + + protected Game(Game other) { + rowSize = other.rowSize; + columnSize = other.columnSize; + board = Arrays.copyOf(other.board, other.board.length); + + players = Arrays.copyOf(other.players, other.players.length); + currentPlayer = other.currentPlayer; + } + + public int getRowSize() { return rowSize; } + public int getColumnSize() { return columnSize; } + public char[] getBoard() { return board; } + + public Player[] getPlayers() { return players; } + public Player getCurrentPlayer() { return players[currentPlayer]; } + + protected void nextPlayer() { + currentPlayer = (currentPlayer + 1) % players.length; + } + + public abstract Move[] getLegalMoves(); + public abstract State play(Move move); +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/GameBase.java b/game/src/main/java/org/toop/game/GameBase.java deleted file mode 100644 index c6de8c3..0000000 --- a/game/src/main/java/org/toop/game/GameBase.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.toop.game; - -// Todo: refactor -public abstract class GameBase { - public enum State { - INVALID, - - NORMAL, - DRAW, - WIN, - } - - public static char EMPTY = '-'; - - protected int size; - public char[] grid; - - protected Player[] players; - public int currentPlayer; - - public GameBase(int size, Player player1, Player player2) { - this.size = size; - grid = new char[size * size]; - - for (int i = 0; i < grid.length; i++) { - grid[i] = EMPTY; - } - - players = new Player[2]; - players[0] = player1; - players[1] = player2; - - currentPlayer = 0; - } - - public boolean isInside(int index) { - return index >= 0 && index < size * size; - } - - public int getSize() { - return size; - } - - public char[] getGrid() { - return grid; - } - - public Player[] getPlayers() { - return players; - } - - public Player getCurrentPlayer() { - return players[currentPlayer]; - } - - public abstract State play(int index); -} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/Player.java b/game/src/main/java/org/toop/game/Player.java index 8f71c40..2dc4a2f 100644 --- a/game/src/main/java/org/toop/game/Player.java +++ b/game/src/main/java/org/toop/game/Player.java @@ -1,20 +1,3 @@ package org.toop.game; -// Todo: refactor -public class Player { - String name; - char symbol; - - public Player(String name, char symbol) { - this.name = name; - this.symbol = symbol; - } - - public String getName() { - return this.name; - } - - public char getSymbol() { - return this.symbol; - } -} \ No newline at end of file +public record Player(String name, char... values) {} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToe.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToe.java new file mode 100644 index 0000000..4b39df2 --- /dev/null +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToe.java @@ -0,0 +1,84 @@ +package org.toop.game.tictactoe; + +import org.toop.game.Game; +import org.toop.game.Player; + +import java.util.ArrayList; + +public final class TicTacToe extends Game { + private int movesLeft; + + public TicTacToe(String player1, String player2) { + super(3, 3, new Player(player1, 'X'), new Player(player2, 'O')); + movesLeft = board.length; + } + + public TicTacToe(TicTacToe other) { + super(other); + movesLeft = other.movesLeft; + } + + @Override + public Move[] getLegalMoves() { + final ArrayList legalMoves = new ArrayList<>(); + + for (int i = 0; i < board.length; i++) { + if (board[i] == EMPTY) { + legalMoves.add(new Move(i, getCurrentPlayer().values()[0])); + } + } + + return legalMoves.toArray(new Move[0]); + } + + @Override + public State play(Move move) { + assert move != null; + assert move.position() >= 0 && move.position() < board.length; + assert move.value() == getCurrentPlayer().values()[0]; + + board[move.position()] = move.value(); + movesLeft--; + + if (checkForWin()) { + return State.WIN; + } + + if (movesLeft <= 0) { + return State.DRAW; + } + + nextPlayer(); + return State.NORMAL; + } + + private boolean checkForWin() { + // Horizontal + for (int i = 0; i < 3; i++) { + final int index = i * 3; + + if (board[index] != EMPTY + && board[index] == board[index + 1] + && board[index] == board[index + 2]) { + return true; + } + } + + // Vertical + for (int i = 0; i < 3; i++) { + if (board[i] != EMPTY + && board[i] == board[i + 3] + && board[i] == board[i + 6]) { + return true; + } + } + + // B-Slash + if (board[0] != EMPTY && board[0] == board[4] && board[0] == board[8]) { + return true; + } + + // F-Slash + return board[2] != EMPTY && board[2] == board[4] && board[2] == board[6]; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java new file mode 100644 index 0000000..afc61b8 --- /dev/null +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java @@ -0,0 +1,68 @@ +package org.toop.game.tictactoe; + +import org.toop.game.AI; +import org.toop.game.Game; + +public final class TicTacToeAI extends AI { + @Override + public Game.Move findBestMove(TicTacToe game, int depth) { + assert game != null; + assert depth >= 0; + + final Game.Move[] legalMoves = game.getLegalMoves(); + + if (legalMoves.length <= 0) { + return null; + } + + if (legalMoves.length == 9) { + return switch ((int)(Math.random() * 4)) { + case 1 -> legalMoves[2]; + case 2 -> legalMoves[6]; + case 3 -> legalMoves[8]; + default -> legalMoves[0]; + }; + } + + int bestScore = -depth; + Game.Move bestMove = null; + + for (final Game.Move move : legalMoves) { + final int score = getMoveScore(game, depth, move, true); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + + return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; + } + + private int getMoveScore(TicTacToe game, int depth, Game.Move move, boolean maximizing) { + final TicTacToe copy = new TicTacToe(game); + final Game.State state = copy.play(move); + + switch (state) { + case Game.State.DRAW: return 0; + case Game.State.WIN: return maximizing? depth + 1 : -depth - 1; + } + + if (depth <= 0) { + return 0; + } + + final Game.Move[] legalMoves = copy.getLegalMoves(); + int score = maximizing? depth + 1 : -depth - 1; + + for (final Game.Move next : legalMoves) { + if (maximizing) { + score = Math.min(score, getMoveScore(copy, depth - 1, next, false)); + } else { + score = Math.max(score, getMoveScore(copy, depth - 1, next, true)); + } + } + + return score; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/tictactoe/TicTacToe.java b/game/src/main/java/org/toop/tictactoe/TicTacToe.java deleted file mode 100644 index 70d1598..0000000 --- a/game/src/main/java/org/toop/tictactoe/TicTacToe.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.toop.tictactoe; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.game.GameBase; -import org.toop.game.Player; - -// Todo: refactor -public class TicTacToe extends GameBase { - - protected static final Logger logger = LogManager.getLogger(TicTacToe.class); - - public Thread gameThread; - public String gameId; - - public int movesLeft; - - public TicTacToe(String player1, String player2) { - super(3, new Player(player1, 'X'), new Player(player2, 'O')); - movesLeft = size * size; - } - - /** - * Used for the server. - * - * @param player1 - * @param player2 - * @param gameId - */ - public TicTacToe(String player1, String player2, String gameId) { - super(3, new Player(player1, 'X'), new Player(player2, 'O')); - this.gameId = gameId; - movesLeft = size * size; - } - - @Override - public State play(int index) { - if (!validateMove(index)) { - return State.INVALID; - } - - grid[index] = getCurrentPlayer().getSymbol(); - movesLeft--; - - if (checkWin()) { - return State.WIN; - } - - if (movesLeft <= 0) { - return State.DRAW; - } - - currentPlayer = (currentPlayer + 1) % players.length; - return State.NORMAL; - } - - public boolean validateMove(int index) { - return movesLeft > 0 && isInside(index) && grid[index] == EMPTY; - } - - public boolean checkWin() { - // Horizontal - for (int i = 0; i < 3; i++) { - final int index = i * 3; - - if (grid[index] != EMPTY - && grid[index] == grid[index + 1] - && grid[index] == grid[index + 2]) { - return true; - } - } - - // Vertical - for (int i = 0; i < 3; i++) { - int index = i; - - if (grid[index] != EMPTY - && grid[index] == grid[index + 3] - && grid[index] == grid[index + 6]) { - return true; - } - } - - // B-Slash - if (grid[0] != EMPTY && grid[0] == grid[4] && grid[0] == grid[8]) { - return true; - } - - // F-Slash - if (grid[2] != EMPTY && grid[2] == grid[4] && grid[2] == grid[6]) { - return true; - } - - return false; - } - - /** For AI use only. */ - public void decrementMovesLeft() { - movesLeft--; - } - - /** This method copies the board, mainly for AI use. */ - public TicTacToe copyBoard() { - TicTacToe clone = new TicTacToe(players[0].getName(), players[1].getName()); - System.arraycopy(this.grid, 0, clone.grid, 0, this.grid.length); - clone.movesLeft = this.movesLeft; - clone.currentPlayer = this.currentPlayer; - return clone; - } -} diff --git a/game/src/main/java/org/toop/tictactoe/TicTacToeAI.java b/game/src/main/java/org/toop/tictactoe/TicTacToeAI.java deleted file mode 100644 index 3eb475d..0000000 --- a/game/src/main/java/org/toop/tictactoe/TicTacToeAI.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.toop.tictactoe; - -import org.toop.game.GameBase; - -// Todo: refactor -public class TicTacToeAI { - /** - * This method tries to find the best move by seeing if it can set a winning move, if not, it - * will do a minimax. - */ - public int findBestMove(TicTacToe game) { - int bestVal = -100; // set bestVal to something impossible - int bestMove = 10; // set bestMove to something impossible - - int winningMove = -5; - - boolean empty = true; - for (char cell : game.grid) { - if (!(cell == GameBase.EMPTY)) { - empty = false; - break; - } - } - - if (empty) { // start in a random corner - return switch ((int) (Math.random() * 4)) { - case 1 -> 2; - case 2 -> 6; - case 3 -> 8; - default -> 0; - }; - } - - // simulate all possible moves on the field - for (int i = 0; i < game.grid.length; i++) { - - if (game.validateMove(i)) { // check if the move is legal here - TicTacToe copyGame = game.copyBoard(); // make a copy of the game - GameBase.State result = copyGame.play(i); // play a move on the copy board - - int thisMoveValue; - - if (result == GameBase.State.WIN) { - return i; // just return right away if you can win on the next move - } - - for (int index = 0; index < game.grid.length; index++) { - if (game.validateMove(index)) { - TicTacToe opponentCopy = copyGame.copyBoard(); - GameBase.State opponentResult = opponentCopy.play(index); - if (opponentResult == GameBase.State.WIN) { - winningMove = index; - } - } - } - - thisMoveValue = - doMinimax(copyGame, game.movesLeft, false); // else look at other moves - if (thisMoveValue - > bestVal) { // if better move than the current best, change the move - bestVal = thisMoveValue; - bestMove = i; - } - } - } - if (winningMove > -5) { - return winningMove; - } - return bestMove; // return the best move when we've done everything - } - - /** - * This method simulates all the possible future moves in the game through a copy in search of - * the best move. - */ - public int doMinimax(TicTacToe game, int depth, boolean maximizing) { - boolean state = game.checkWin(); // check for a win (base case stuff) - - if (state) { - if (maximizing) { - // it's the maximizing players turn and someone has won. this is not good, so return - // a negative value - return -10 + depth; - } else { - // it is the turn of the AI and it has won! this is good for us, so return a - // positive value above 0 - return 10 - depth; - } - } else { - boolean empty = false; - for (char cell : - game.grid) { // else, look at draw conditions. we check per cell if it's empty - // or not - if (cell == GameBase.EMPTY) { - empty = true; // if a thing is empty, set to true - break; // break the loop - } - } - if (!empty - || depth == 0) { // if the grid is full or the depth is 0 (both meaning game is - // over) return 0 for draw - return 0; - } - } - - int bestVal; // set the value to the highest possible - if (maximizing) { // it's the maximizing players turn, the AI - bestVal = -100; - for (int i = 0; i < game.grid.length; i++) { // loop through the grid - if (game.validateMove(i)) { - TicTacToe copyGame = game.copyBoard(); - copyGame.play(i); // play the move on a copy board - int value = - doMinimax(copyGame, depth - 1, false); // keep going with the minimax - bestVal = - Math.max( - bestVal, - value); // select the best value for the maximizing player (the - // AI) - } - } - } else { // it's the minimizing players turn, the player - bestVal = 100; - for (int i = 0; i < game.grid.length; i++) { // loop through the grid - if (game.validateMove(i)) { - TicTacToe copyGame = game.copyBoard(); - copyGame.play(i); // play the move on a copy board - int value = doMinimax(copyGame, depth - 1, true); // keep miniMaxing - bestVal = - Math.min( - bestVal, - value); // select the lowest score for the minimizing player, - // they want to make it hard for us - } - } - } - return bestVal; - } -} \ No newline at end of file diff --git a/game/src/test/java/org/toop/game/PlayerTest.java b/game/src/test/java/org/toop/game/PlayerTest.java new file mode 100644 index 0000000..3a3f14b --- /dev/null +++ b/game/src/test/java/org/toop/game/PlayerTest.java @@ -0,0 +1,48 @@ +package org.toop.game; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PlayerTest { + private Player playerA; + private Player playerB; + private Player playerC; + + @BeforeEach + void setup() { + playerA = new Player("test A", 'x', 'Z', 'i'); + playerB = new Player("test B", 'O', (char)12, (char)-34, 's'); + playerC = new Player("test C", (char)9, '9', (char)-9, '0', 'X', 'O'); + } + + @Test + void testNameGetter_returnsTrueForValidName() { + assertEquals("test A", playerA.name()); + assertEquals("test B", playerB.name()); + assertEquals("test C", playerC.name()); + } + + @Test + void testValuesGetter_returnsTrueForValidValues() { + final char[] valuesA = playerA.values(); + assertEquals('x', valuesA[0]); + assertEquals('Z', valuesA[1]); + assertEquals('i', valuesA[2]); + + final char[] valuesB = playerB.values(); + assertEquals('O', valuesB[0]); + assertEquals(12, valuesB[1]); + assertEquals((char)-34, valuesB[2]); + assertEquals('s', valuesB[3]); + + final char[] valuesC = playerC.values(); + assertEquals((char)9, valuesC[0]); + assertEquals('9', valuesC[1]); + assertEquals((char)-9, valuesC[2]); + assertEquals('0', valuesC[3]); + assertEquals('X', valuesC[4]); + assertEquals('O', valuesC[5]); + } +} \ No newline at end of file diff --git a/game/src/test/java/org/toop/game/tictactoe/TicTacToeAITest.java b/game/src/test/java/org/toop/game/tictactoe/TicTacToeAITest.java new file mode 100644 index 0000000..a320631 --- /dev/null +++ b/game/src/test/java/org/toop/game/tictactoe/TicTacToeAITest.java @@ -0,0 +1,83 @@ +package org.toop.game.tictactoe; + +import org.toop.game.Game; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TicTacToeAITest { + private TicTacToe game; + private TicTacToeAI ai; + + @BeforeEach + void setup() { + game = new TicTacToe("AI", "AI"); + ai = new TicTacToeAI(); + } + + @Test + void testBestMove_returnWinningMoveWithDepth1() { + // X X - + // O O - + // - - - + game.play(new Game.Move(0, 'X')); + game.play(new Game.Move(3, 'O')); + game.play(new Game.Move(1, 'X')); + game.play(new Game.Move(4, 'O')); + + final Game.Move move = ai.findBestMove(game, 1); + + assertNotNull(move); + assertEquals('X', move.value()); + assertEquals(2, move.position()); + } + + @Test + void testBestMove_blockOpponentWinDepth1() { + // - - - + // O - - + // X X - + game.play(new Game.Move(6, 'X')); + game.play(new Game.Move(3, 'O')); + game.play(new Game.Move(7, 'X')); + + final Game.Move move = ai.findBestMove(game, 1); + + assertNotNull(move); + assertEquals('O', move.value()); + assertEquals(8, move.position()); + } + + @Test + void testBestMove_preferCornerOnEmpty() { + final Game.Move move = ai.findBestMove(game, 0); + + assertNotNull(move); + assertEquals('X', move.value()); + assertTrue(Set.of(0, 2, 6, 8).contains(move.position())); + } + + @Test + void testBestMove_findBestMoveDraw() { + // O X - + // - O X + // X O X + game.play(new Game.Move(1, 'X')); + game.play(new Game.Move(0, 'O')); + game.play(new Game.Move(5, 'X')); + game.play(new Game.Move(4, 'O')); + game.play(new Game.Move(6, 'X')); + game.play(new Game.Move(7, 'O')); + game.play(new Game.Move(8, 'X')); + + final Game.Move move = ai.findBestMove(game, game.getLegalMoves().length); + + assertNotNull(move); + assertEquals('O', move.value()); + assertEquals(2, move.position()); + } +} \ No newline at end of file diff --git a/game/src/test/java/org/toop/tictactoe/GameBaseTest.java b/game/src/test/java/org/toop/tictactoe/GameBaseTest.java deleted file mode 100644 index ff025c4..0000000 --- a/game/src/test/java/org/toop/tictactoe/GameBaseTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package java.org.toop.tictactoe; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.toop.game.GameBase; -import org.toop.game.Player; - -class GameBaseTest { - - private static class TestGame extends GameBase { - public TestGame(int size, Player p1, Player p2) { - super(size, p1, p2); - } - - @Override - public State play(int index) { - if (!isInside(index)) return State.INVALID; - grid[index] = getCurrentPlayer().getSymbol(); - // Just alternate players for testing - currentPlayer = (currentPlayer + 1) % 2; - return State.NORMAL; - } - } - - private GameBase game; - private Player player1; - private Player player2; - - @BeforeEach - void setUp() { - player1 = new Player("A", 'X'); - player2 = new Player("B", 'O'); - game = new TestGame(3, player1, player2); - } - - @Test - void testConstructor_initializesGridAndPlayers() { - assertEquals(3, game.getSize()); - assertEquals(9, game.getGrid().length); - - for (char c : game.getGrid()) { - assertEquals(GameBase.EMPTY, c); - } - - assertEquals(player1, game.getPlayers()[0]); - assertEquals(player2, game.getPlayers()[1]); - assertEquals(player1, game.getCurrentPlayer()); - } - - @Test - void testIsInside_returnsTrueForValidIndices() { - for (int i = 0; i < 9; i++) { - assertTrue(game.isInside(i)); - } - } - - @Test - void testIsInside_returnsFalseForInvalidIndices() { - assertFalse(game.isInside(-1)); - assertFalse(game.isInside(9)); - assertFalse(game.isInside(100)); - } - - @Test - void testPlay_alternatesPlayersAndMarksGrid() { - // First move - assertEquals(GameBase.State.NORMAL, game.play(0)); - assertEquals('X', game.getGrid()[0]); - assertEquals(player2, game.getCurrentPlayer()); - - // Second move - assertEquals(GameBase.State.NORMAL, game.play(1)); - assertEquals('O', game.getGrid()[1]); - assertEquals(player1, game.getCurrentPlayer()); - } - - @Test - void testPlay_invalidIndexReturnsInvalid() { - assertEquals(GameBase.State.INVALID, game.play(-1)); - assertEquals(GameBase.State.INVALID, game.play(9)); - } -} diff --git a/game/src/test/java/org/toop/tictactoe/PlayerTest.java b/game/src/test/java/org/toop/tictactoe/PlayerTest.java deleted file mode 100644 index ca6151a..0000000 --- a/game/src/test/java/org/toop/tictactoe/PlayerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.toop.game.tictactoe; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.toop.game.Player; - -class PlayerTest { - - private Player playerA; - private Player playerB; - - @BeforeEach - void setup() { - playerA = new Player("testA", 'X'); - playerB = new Player("testB", 'O'); - } - - @Test - void testNameGetter_returnsTrueForValidName() { - assertEquals("testA", playerA.getName()); - assertEquals("testB", playerB.getName()); - } - - @Test - void testSymbolGetter_returnsTrueForValidSymbol() { - assertEquals('X', playerA.getSymbol()); - assertEquals('O', playerB.getSymbol()); - } -} diff --git a/game/src/test/java/org/toop/tictactoe/ai/MinMaxTicTacToeTest.java b/game/src/test/java/org/toop/tictactoe/ai/MinMaxTicTacToeTest.java deleted file mode 100644 index 9cb083f..0000000 --- a/game/src/test/java/org/toop/tictactoe/ai/MinMaxTicTacToeTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.toop.game.tictactoe.ai; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Set; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.toop.game.GameBase; -import org.toop.tictactoe.TicTacToe; - -/** Unit tests for MinMaxTicTacToe AI. */ -public class MinMaxTicTacToeTest { - - private MinMaxTicTacToe ai; - private TicTacToe game; - - @BeforeEach // called before every test is done to make it work - void setUp() { - ai = new MinMaxTicTacToe(); - game = new TicTacToe("AI", "Human"); - } - - @Test - void testBestMoveWinningMoveAvailable() { - // Setup board where AI can win immediately - // X = AI, O = player - // X | X | . - // O | O | . - // . | . | . - game.grid = - new char[] { - 'X', - 'X', - GameBase.EMPTY, - 'O', - 'O', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - game.movesLeft = 4; - - int bestMove = ai.findBestMove(game); - - // Ai is expected to place at index 2 to win - assertEquals(2, bestMove); - } - - @Test - void testBestMoveBlocksOpponentWin() { - // Setup board where player could win next turn - // O | O | . - // X | . | . - // . | . | . - game.grid = - new char[] { - 'O', - 'O', - GameBase.EMPTY, - 'X', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - - int bestMove = ai.findBestMove(game); - - // AI block at index 2 to continue the game - assertEquals(2, bestMove); - } - - @Test - void testBestMoveCornerPreferredOnEmptyBoard() { - // On empty board, center (index 4) is strongest - int bestMove = ai.findBestMove(game); - - assertTrue(Set.of(0, 2, 6, 8).contains(bestMove)); - } - - @Test - void testDoMinimaxScoresWinPositive() { - // Simulate a game state where AI has already won - TicTacToe copy = game.copyBoard(); - copy.grid = - new char[] { - 'X', - 'X', - 'X', - 'O', - 'O', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - - int score = ai.doMinimax(copy, 5, false); - - assertTrue(score > 0, "AI win should yield positive score"); - } - - @Test - void testDoMinimaxScoresLossNegative() { - // Simulate a game state where human has already won - TicTacToe copy = game.copyBoard(); - copy.grid = - new char[] { - 'O', - 'O', - 'O', - 'X', - 'X', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - - int score = ai.doMinimax(copy, 5, true); - - assertTrue(score < 0, "Human win should yield negative score"); - } - - @Test - void testDoMinimaxDrawReturnsZero() { - // Simulate a draw position - TicTacToe copy = game.copyBoard(); - copy.grid = - new char[] { - 'X', 'O', 'X', - 'X', 'O', 'O', - 'O', 'X', 'X' - }; - - int score = ai.doMinimax(copy, 0, true); - - assertEquals(0, score, "Draw should return 0 score"); - } -}