From f69df69a36c1663d63e906dd265e0d6c69b1f344 Mon Sep 17 00:00:00 2001 From: Stef Date: Fri, 28 Nov 2025 15:04:12 +0100 Subject: [PATCH] (RANDOM COMMIT) Hope it works --- .../org/toop/app/canvas/TicTacToeCanvas.java | 59 ++++++----- .../java/org/toop/app/game/MoveBehaviour.java | 8 -- .../main/java/org/toop/app/game/Player.java | 6 -- .../app/game/Players/ArtificialPlayer.java | 45 +++++++++ .../app/game/{ => Players}/LocalPlayer.java | 10 +- .../org/toop/app/game/Players/MakesMove.java | 23 +++++ .../toop/app/game/Players/OnlinePlayer.java | 24 +++++ .../org/toop/app/game/Players/Player.java | 35 +++++++ .../app/widget/view/LocalMultiplayerView.java | 9 +- game/pom.xml | 8 +- game/src/main/java/org/toop/game/AIR.java | 17 ++++ .../java/org/toop/game/GameController.java | 17 ++++ game/src/main/java/org/toop/game/GameR.java | 98 +++++++++++++++--- .../org/toop/game/TicTacToeController.java | 24 +++++ .../java/org/toop/game/TurnBasedGameR.java | 10 +- .../org/toop}/game/TurnBasedGameThread.java | 49 +++++---- .../java/org/toop/game/UpdatesGameUI.java | 5 + .../org/toop/game/interfaces/IAIMoveR.java | 26 +++++ .../org/toop/game/interfaces/IPlayableR.java | 25 ++++- .../org/toop/game/tictactoe/TicTacToeAIR.java | 99 +++++++++++++++++++ .../org/toop/game/tictactoe/TicTacToeR.java | 15 ++- 21 files changed, 524 insertions(+), 88 deletions(-) delete mode 100644 app/src/main/java/org/toop/app/game/MoveBehaviour.java delete mode 100644 app/src/main/java/org/toop/app/game/Player.java create mode 100644 app/src/main/java/org/toop/app/game/Players/ArtificialPlayer.java rename app/src/main/java/org/toop/app/game/{ => Players}/LocalPlayer.java (61%) create mode 100644 app/src/main/java/org/toop/app/game/Players/MakesMove.java create mode 100644 app/src/main/java/org/toop/app/game/Players/OnlinePlayer.java create mode 100644 app/src/main/java/org/toop/app/game/Players/Player.java create mode 100644 game/src/main/java/org/toop/game/AIR.java create mode 100644 game/src/main/java/org/toop/game/GameController.java create mode 100644 game/src/main/java/org/toop/game/TicTacToeController.java rename {app/src/main/java/org/toop/app => game/src/main/java/org/toop}/game/TurnBasedGameThread.java (65%) create mode 100644 game/src/main/java/org/toop/game/UpdatesGameUI.java create mode 100644 game/src/main/java/org/toop/game/interfaces/IAIMoveR.java create mode 100644 game/src/main/java/org/toop/game/tictactoe/TicTacToeAIR.java diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java index 890eb39..32f52c1 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -4,35 +4,48 @@ import javafx.scene.paint.Color; import java.util.function.Consumer; -public final class TicTacToeCanvas extends GameCanvas { +public final class TicTacToeCanvas extends GameCanvas implements Drawable { public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { super(color, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked,null); } - public void drawX(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); + public void drawPlayer(char a, Color color, int cell) { + graphics.setStroke(color); + graphics.setLineWidth(gapSize); - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; + drawChar(a, color, cell); + draw(); + } - final float width = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; +// public void drawX(Color color, int cell) { +// graphics.setStroke(color); +// graphics.setLineWidth(gapSize); +// +// final float x = cells[cell].x() + gapSize; +// final float y = cells[cell].y() + gapSize; +// +// final float width = cells[cell].width() - gapSize * 2; +// final float height = cells[cell].height() - gapSize * 2; +// +// graphics.strokeLine(x, y, x + width, y + height); +// graphics.strokeLine(x + width, y, x, y + height); +// } - graphics.strokeLine(x, y, x + width, y + height); - graphics.strokeLine(x + width, y, x, y + height); - } + @Override + public void draw() { - public void drawO(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); - - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; - - final float width = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; - - graphics.strokeOval(x, y, width, height); - } + } +// +// public void drawO(Color color, int cell) { +// graphics.setStroke(color); +// graphics.setLineWidth(gapSize); +// +// final float x = cells[cell].x() + gapSize; +// final float y = cells[cell].y() + gapSize; +// +// final float width = cells[cell].width() - gapSize * 2; +// final float height = cells[cell].height() - gapSize * 2; +// +// graphics.strokeOval(x, y, width, height); +// } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/MoveBehaviour.java b/app/src/main/java/org/toop/app/game/MoveBehaviour.java deleted file mode 100644 index aa8a7a7..0000000 --- a/app/src/main/java/org/toop/app/game/MoveBehaviour.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.toop.app.game; - -import org.toop.game.records.Move; - -public interface MoveBehaviour -{ - int getMove(); -} diff --git a/app/src/main/java/org/toop/app/game/Player.java b/app/src/main/java/org/toop/app/game/Player.java deleted file mode 100644 index 33d5689..0000000 --- a/app/src/main/java/org/toop/app/game/Player.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.toop.app.game; - -import org.toop.game.records.Move; - -public abstract class Player implements MoveBehaviour{ -} diff --git a/app/src/main/java/org/toop/app/game/Players/ArtificialPlayer.java b/app/src/main/java/org/toop/app/game/Players/ArtificialPlayer.java new file mode 100644 index 0000000..24bb4e5 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Players/ArtificialPlayer.java @@ -0,0 +1,45 @@ +package org.toop.app.game.Players; + +import org.toop.game.AIR; +import org.toop.game.GameR; + +/** + * Represents a player controlled by an AI in a game. + *

+ * This player uses an {@link AIR} instance to determine its moves. The generic + * parameter {@code T} specifies the type of {@link GameR} the AI can handle. + *

+ * + * @param the specific type of game this AI player can play + */ +public class ArtificialPlayer extends Player { + + /** The AI instance used to calculate moves. */ + private final AIR ai; + + /** + * Constructs a new ArtificialPlayer using the specified AI. + * + * @param ai the AI instance that determines moves for this player + */ + public ArtificialPlayer(AIR ai) { + this.ai = ai; + } + + /** + * Determines the next move for this player using its AI. + *

+ * This method overrides {@link Player#getMove(GameR)}. Because the AI is + * typed to {@code T}, a runtime cast is required. It is the caller's + * responsibility to ensure that {@code gameCopy} is of type {@code T}. + *

+ * + * @param gameCopy a copy of the current game state + * @return the integer representing the chosen move + * @throws ClassCastException if {@code gameCopy} is not of type {@code T} + */ + @Override + public int getMove(GameR gameCopy) { + return ai.findBestMove((T) gameCopy, 9); // TODO: Make depth configurable + } +} diff --git a/app/src/main/java/org/toop/app/game/LocalPlayer.java b/app/src/main/java/org/toop/app/game/Players/LocalPlayer.java similarity index 61% rename from app/src/main/java/org/toop/app/game/LocalPlayer.java rename to app/src/main/java/org/toop/app/game/Players/LocalPlayer.java index 6b11ac3..6b2e952 100644 --- a/app/src/main/java/org/toop/app/game/LocalPlayer.java +++ b/app/src/main/java/org/toop/app/game/Players/LocalPlayer.java @@ -1,15 +1,17 @@ -package org.toop.app.game; +package org.toop.app.game.Players; + +import org.toop.game.GameR; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -public class LocalPlayer extends Player{ - private BlockingQueue queue = new LinkedBlockingQueue(); +public class LocalPlayer extends Player { + private final BlockingQueue queue = new LinkedBlockingQueue(); public LocalPlayer() {} @Override - public int getMove() { + public int getMove(GameR gameCopy) { try { return queue.take(); }catch (InterruptedException e){ diff --git a/app/src/main/java/org/toop/app/game/Players/MakesMove.java b/app/src/main/java/org/toop/app/game/Players/MakesMove.java new file mode 100644 index 0000000..30147a3 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Players/MakesMove.java @@ -0,0 +1,23 @@ +package org.toop.app.game.Players; + +import org.toop.game.GameR; + +/** + * Interface representing an entity capable of making a move in a game. + *

+ * Any class implementing this interface should provide logic to determine + * the next move given a snapshot of the current game state. + *

+ */ +public interface MakesMove { + + /** + * Determines the next move based on the provided game state. + * + * @param gameCopy a copy or snapshot of the current game state + * (never null) + * @return an integer representing the chosen move. + * The interpretation of this value depends on the specific game. + */ + int getMove(GameR gameCopy); +} diff --git a/app/src/main/java/org/toop/app/game/Players/OnlinePlayer.java b/app/src/main/java/org/toop/app/game/Players/OnlinePlayer.java new file mode 100644 index 0000000..a1fc643 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Players/OnlinePlayer.java @@ -0,0 +1,24 @@ +package org.toop.app.game.Players; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Represents a player controlled remotely or over a network. + *

+ * This class extends {@link Player} and can be used to implement game logic + * where moves are provided by an external source (e.g., another user or a server). + * Currently, this class is a placeholder and does not implement move logic. + *

+ */ +public class OnlinePlayer extends Player { + + /** + * Constructs a new OnlinePlayer. + *

+ * Currently, no additional initialization is performed. Subclasses or + * future implementations should provide mechanisms to receive moves from + * an external source. + */ + public OnlinePlayer() {} +} diff --git a/app/src/main/java/org/toop/app/game/Players/Player.java b/app/src/main/java/org/toop/app/game/Players/Player.java new file mode 100644 index 0000000..e4b3720 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Players/Player.java @@ -0,0 +1,35 @@ +package org.toop.app.game.Players; + +import org.toop.game.GameR; + +/** + * Abstract class representing a player in a game. + *

+ * Players are entities that can make moves based on the current state of a game. + * This class implements {@link MakesMove} and serves as a base for concrete + * player types, such as human players or AI players. + *

+ *

+ * Subclasses should override the {@link #getMove(GameR)} method to provide + * specific move logic. + *

+ */ +public abstract class Player implements MakesMove { + + /** + * Determines the next move based on the provided game state. + *

+ * The default implementation throws an {@link UnsupportedOperationException}, + * indicating that concrete subclasses must override this method to provide + * actual move logic. + *

+ * + * @param gameCopy a snapshot of the current game state + * @return an integer representing the chosen move + * @throws UnsupportedOperationException if the method is not overridden + */ + @Override + public int getMove(GameR gameCopy) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java index 439b00b..5cd593f 100644 --- a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java +++ b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java @@ -2,12 +2,15 @@ package org.toop.app.widget.view; import org.toop.app.GameInformation; import org.toop.app.game.*; +import org.toop.app.game.Players.ArtificialPlayer; +import org.toop.app.game.Players.LocalPlayer; +import org.toop.app.game.Players.Player; import org.toop.app.widget.Primitive; import org.toop.app.widget.complex.PlayerInfoWidget; import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.popup.ErrorPopup; -import org.toop.game.tictactoe.TicTacToe; -import org.toop.game.tictactoe.TicTacToeAI; +import org.toop.game.TurnBasedGameThread; +import org.toop.game.tictactoe.TicTacToeAIR; import org.toop.game.tictactoe.TicTacToeR; import org.toop.local.AppContext; @@ -33,7 +36,7 @@ public class LocalMultiplayerView extends ViewWidget { } switch (information.type) { - case TICTACTOE -> new TurnBasedGameThread(new Player[]{new LocalPlayer(), new LocalPlayer()}, new TicTacToeR()); + case TICTACTOE -> new TurnBasedGameThread(new Player[]{new LocalPlayer(), new ArtificialPlayer<>(new TicTacToeAIR())}, new TicTacToeR()); case REVERSI -> new ReversiGame(information); case CONNECT4 -> new Connect4Game(information); // case BATTLESHIP -> new BattleshipGame(information); diff --git a/game/pom.xml b/game/pom.xml index 6785e1c..61bc2e4 100644 --- a/game/pom.xml +++ b/game/pom.xml @@ -99,8 +99,14 @@ error_prone_annotations 2.42.0 + + org.toop + app + 0.1 + compile + - + diff --git a/game/src/main/java/org/toop/game/AIR.java b/game/src/main/java/org/toop/game/AIR.java new file mode 100644 index 0000000..bc2f60b --- /dev/null +++ b/game/src/main/java/org/toop/game/AIR.java @@ -0,0 +1,17 @@ +package org.toop.game; + +import org.toop.game.interfaces.IAIMoveR; + +/** + * Abstract base class for AI implementations for games extending {@link GameR}. + *

+ * Provides a common superclass for specific AI algorithms. Concrete subclasses + * must implement the {@link #findBestMove(GameR, int)} method defined by + * {@link IAIMoveR} to determine the best move given a game state and a search depth. + *

+ * + * @param the specific type of game this AI can play, extending {@link GameR} + */ +public abstract class AIR implements IAIMoveR { + // Concrete AI implementations should override findBestMove(T game, int depth) +} diff --git a/game/src/main/java/org/toop/game/GameController.java b/game/src/main/java/org/toop/game/GameController.java new file mode 100644 index 0000000..484f59e --- /dev/null +++ b/game/src/main/java/org/toop/game/GameController.java @@ -0,0 +1,17 @@ +/*package org.toop.game; + +import org.toop.app.canvas.GameCanvas; +import org.toop.game.TurnBasedGameThread; +import org.toop.app.widget.view.GameView; + +public abstract class GameController implements UpdatesGameUI { + // TODO: Seperate this from game Thread + protected final GameView primary = new GameView(null, null, null); + protected final GameCanvas canvas; + protected final TurnBasedGameThread gameThread; + + protected GameController(GameCanvas canvas, TurnBasedGameThread gameThread) { + this.gameThread = gameThread; + this.canvas = canvas; + } +}*/ diff --git a/game/src/main/java/org/toop/game/GameR.java b/game/src/main/java/org/toop/game/GameR.java index e21f6a3..78d2ddb 100644 --- a/game/src/main/java/org/toop/game/GameR.java +++ b/game/src/main/java/org/toop/game/GameR.java @@ -1,41 +1,109 @@ package org.toop.game; -import org.toop.game.interfaces.IPlayable; import org.toop.game.interfaces.IPlayableR; -import org.toop.game.records.Move; import java.util.Arrays; -public abstract class GameR implements IPlayableR { +/** + * Abstract base class representing a general grid-based game. + *

+ * Provides the basic structure for games with a two-dimensional board stored as a + * one-dimensional array. Tracks the board state, row and column sizes, and provides + * helper methods for accessing and modifying the board. + *

+ *

+ * Concrete subclasses must implement the {@link #clone()} method and can extend this + * class with specific game rules, winning conditions, and move validation logic. + *

+ */ +public abstract class GameR implements IPlayableR, Cloneable { - public static final Integer EMPTY = null; // Constant + /** Constant representing an empty position on the board. */ + public static final int EMPTY = -1; + /** Number of rows in the game board. */ private final int rowSize; - private final int columnSize; - private final Integer[] board; + /** Number of columns in the game board. */ + private final int columnSize; + + /** The game board stored as a one-dimensional array. */ + private final int[] board; + + /** + * Constructs a new game board with the specified row and column size. + * + * @param rowSize number of rows (> 0) + * @param columnSize number of columns (> 0) + * @throws AssertionError if rowSize or columnSize is not positive + */ protected GameR(int rowSize, int columnSize) { assert rowSize > 0 && columnSize > 0; this.rowSize = rowSize; this.columnSize = columnSize; - board = new Integer[rowSize * columnSize]; + board = new int[rowSize * columnSize]; Arrays.fill(board, EMPTY); } - protected GameR(GameR other) { - rowSize = other.rowSize; - columnSize = other.columnSize; - board = Arrays.copyOf(other.board, other.board.length); + /** + * Copy constructor for creating a deep copy of another game instance. + * + * @param copy the game instance to copy + */ + protected GameR(GameR copy) { + this.rowSize = copy.rowSize; + this.columnSize = copy.columnSize; + this.board = copy.board.clone(); } - public int getRowSize() {return this.rowSize;} + /** + * Returns the number of rows in the board. + * + * @return number of rows + */ + public int getRowSize() { + return this.rowSize; + } - public int getColumnSize() {return this.columnSize;} + /** + * Returns the number of columns in the board. + * + * @return number of columns + */ + public int getColumnSize() { + return this.columnSize; + } - public Integer[] getBoard() {return this.board;} + /** + * Returns a copy of the current board state. + * + * @return a cloned array representing the board + */ + public int[] getBoard() { + return this.board.clone(); + } - protected void setBoard(int position, int player){this.board[position] = player;} + /** + * Sets the value of a specific position on the board. + * + * @param position the index in the board array + * @param player the value to set (e.g., player number) + */ + protected void setBoardPosition(int position, int player) { + this.board[position] = player; + } + /** + * Creates and returns a deep copy of this game instance. + *

+ * Subclasses must implement this method to ensure proper copying of any + * additional fields beyond the base board structure. + *

+ * + * @return a cloned instance of this game + */ + @Override + public abstract GameR clone(); } diff --git a/game/src/main/java/org/toop/game/TicTacToeController.java b/game/src/main/java/org/toop/game/TicTacToeController.java new file mode 100644 index 0000000..6143c05 --- /dev/null +++ b/game/src/main/java/org/toop/game/TicTacToeController.java @@ -0,0 +1,24 @@ +/*package org.toop.game; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.app.canvas.TicTacToeCanvas; +import org.toop.app.game.Players.LocalPlayer; +import org.toop.game.TurnBasedGameThread; +import org.toop.app.widget.WidgetContainer; + +public class TicTacToeController extends GameController { + + public TicTacToeController() { + super(new TicTacToeCanvas(Color.GRAY, + (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,(c) -> {if (players[game.getCurrentTurn()] instanceof LocalPlayer lp) {lp.enqueueMove(c);}}), new TurnBasedGameThread()); + primary.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(primary)); + } + + @Override + public void updateUI() { + + } +}*/ diff --git a/game/src/main/java/org/toop/game/TurnBasedGameR.java b/game/src/main/java/org/toop/game/TurnBasedGameR.java index 702a516..f3b65a8 100644 --- a/game/src/main/java/org/toop/game/TurnBasedGameR.java +++ b/game/src/main/java/org/toop/game/TurnBasedGameR.java @@ -9,10 +9,9 @@ public abstract class TurnBasedGameR extends GameR { this.playerCount = playerCount; } - protected TurnBasedGameR(TurnBasedGameR other) { + protected TurnBasedGameR(TurnBasedGameR other){ super(other); - playerCount = other.playerCount; - turn = other.turn; + this.playerCount = other.playerCount; } public int getPlayerCount(){return this.playerCount;} @@ -26,6 +25,9 @@ public abstract class TurnBasedGameR extends GameR { } protected void setBoard(int position) { - super.setBoard(position, getCurrentTurn()); + super.setBoardPosition(position, getCurrentTurn()); } + + @Override + public abstract TurnBasedGameR clone(); } diff --git a/app/src/main/java/org/toop/app/game/TurnBasedGameThread.java b/game/src/main/java/org/toop/game/TurnBasedGameThread.java similarity index 65% rename from app/src/main/java/org/toop/app/game/TurnBasedGameThread.java rename to game/src/main/java/org/toop/game/TurnBasedGameThread.java index 2a04fd0..a89ad71 100644 --- a/app/src/main/java/org/toop/app/game/TurnBasedGameThread.java +++ b/game/src/main/java/org/toop/game/TurnBasedGameThread.java @@ -1,16 +1,15 @@ -package org.toop.app.game; +package org.toop.game; import javafx.geometry.Pos; import javafx.scene.paint.Color; import org.toop.app.App; import org.toop.app.canvas.GameCanvas; import org.toop.app.canvas.TicTacToeCanvas; +import org.toop.app.game.Players.LocalPlayer; +import org.toop.app.game.Players.Player; import org.toop.app.widget.WidgetContainer; import org.toop.app.widget.view.GameView; -import org.toop.game.TurnBasedGameR; import org.toop.game.enumerators.GameState; -import org.toop.game.records.Move; -import org.toop.game.tictactoe.TicTacToe; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; @@ -20,12 +19,15 @@ public class TurnBasedGameThread implements Runnable { private final TurnBasedGameR game; // Reference to game instance private final AtomicBoolean isRunning = new AtomicBoolean(); + //private final GameController controller; - // TODO: Seperate this from game Thread - private final GameView primary = new GameView(null, null, null); - private final TicTacToeCanvas canvas; + protected final GameView primary = new GameView(null, null, null); + protected final GameCanvas canvas; public TurnBasedGameThread(Player[] players, TurnBasedGameR game) { + // Set reference to controller + //this.controller = controller; + // Make sure player list matches expected size if (players.length != game.getPlayerCount()){ throw new IllegalArgumentException("players and game's players must have same length"); @@ -45,10 +47,18 @@ public class TurnBasedGameThread implements Runnable { } + public Player[] getPlayers() { + return players; + } + // Move to UI shiz private void drawMove(int move) { - if (game.getCurrentTurn() == 1) canvas.drawX(Color.RED, move); - else canvas.drawO(Color.BLUE, move); + if (game.getCurrentTurn() == 1){ + canvas.drawChar('X', Color.RED, move); + } + else{ + canvas.drawChar('O', Color.RED, move); + } } public void run() { @@ -59,19 +69,18 @@ public class TurnBasedGameThread implements Runnable { // Get current player Player currentPlayer = players[game.getCurrentTurn()]; - + System.out.println(game.getCurrentTurn() + "'s turn"); // Get this player's valid moves - Integer[] validMoves = game.getLegalMoves(); + int[] validMoves = game.getLegalMoves(); // Get player's move, reask if Move is invalid // TODO: Limit amount of retries? - int move = currentPlayer.getMove(); - while (!Arrays.asList(validMoves).contains(move)) { - System.out.println("Invalid move");; - move = currentPlayer.getMove(); + int move = currentPlayer.getMove(game.clone()); + while (!contains(validMoves, move)) { + move = currentPlayer.getMove(game.clone()); } - // Make move + System.out.println(Arrays.toString(game.getBoard())); GameState state = game.play(move); drawMove(move); @@ -81,12 +90,16 @@ public class TurnBasedGameThread implements Runnable { } else if (state == GameState.DRAW) { // THere was a draw } + System.out.println(state); isRunning.set(false); } } } + - private void updateUI(){ - + // helper function, would like to replace to get rid of this method + public static boolean contains(int[] array, int value){ + for (int i : array) if (i == value) return true; + return false; } } diff --git a/game/src/main/java/org/toop/game/UpdatesGameUI.java b/game/src/main/java/org/toop/game/UpdatesGameUI.java new file mode 100644 index 0000000..89cd195 --- /dev/null +++ b/game/src/main/java/org/toop/game/UpdatesGameUI.java @@ -0,0 +1,5 @@ +package org.toop.game; + +public interface UpdatesGameUI { + void updateUI(); +} diff --git a/game/src/main/java/org/toop/game/interfaces/IAIMoveR.java b/game/src/main/java/org/toop/game/interfaces/IAIMoveR.java new file mode 100644 index 0000000..376b450 --- /dev/null +++ b/game/src/main/java/org/toop/game/interfaces/IAIMoveR.java @@ -0,0 +1,26 @@ +package org.toop.game.interfaces; + +import org.toop.game.GameR; + +/** + * Interface defining the behavior of an AI capable of selecting the best move + * in a game represented by {@link GameR}. + * + * @param the specific type of game this AI can play, extending {@link GameR} + */ +public interface IAIMoveR { + + /** + * Determines the best move for the given game state. + *

+ * Implementations of this method should analyze the provided game state and + * return the most optimal move for the current player. The analysis can + * consider future moves up to the specified depth. + *

+ * + * @param game the current game state to analyze + * @param depth the search depth or lookahead for evaluating moves + * @return an integer representing the chosen move + */ + int findBestMove(T game, int depth); +} diff --git a/game/src/main/java/org/toop/game/interfaces/IPlayableR.java b/game/src/main/java/org/toop/game/interfaces/IPlayableR.java index f360a4d..677617f 100644 --- a/game/src/main/java/org/toop/game/interfaces/IPlayableR.java +++ b/game/src/main/java/org/toop/game/interfaces/IPlayableR.java @@ -1,9 +1,30 @@ package org.toop.game.interfaces; import org.toop.game.enumerators.GameState; -import org.toop.game.records.Move; +/** + * Interface representing a playable game with rules for determining legal moves + * and executing moves. + *

+ * Any game class implementing this interface should provide methods to query + * the current legal moves and to apply a move to the game state, returning + * the resulting game state. + *

+ */ public interface IPlayableR { - Integer[] getLegalMoves(); + + /** + * Returns an array of legal moves that can currently be played in the game. + * + * @return an array of integers representing valid moves; may be empty if no moves are possible + */ + int[] getLegalMoves(); + + /** + * Applies a move to the game and returns the resulting state. + * + * @param move the move to play, represented as an integer + * @return the {@link GameState} after the move is played + */ GameState play(int move); } diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAIR.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAIR.java new file mode 100644 index 0000000..f9b2fb6 --- /dev/null +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAIR.java @@ -0,0 +1,99 @@ +package org.toop.game.tictactoe; + +import org.toop.game.AIR; +import org.toop.game.enumerators.GameState; + +/** + * AI implementation for playing Tic-Tac-Toe. + *

+ * This AI uses a recursive minimax-like strategy with a limited depth to + * evaluate moves. It attempts to maximize its chances of winning while + * minimizing the opponent's opportunities. Random moves are used in the + * opening or when no clear best move is found. + *

+ */ +public final class TicTacToeAIR extends AIR { + + /** + * Determines the best move for the given Tic-Tac-Toe game state. + *

+ * Uses a depth-limited recursive strategy to score each legal move and + * selects the move with the highest score. If no legal moves are available, + * returns -1. If multiple moves are equally good, picks one randomly. + *

+ * + * @param game the current Tic-Tac-Toe game state + * @param depth the depth of lookahead for evaluating moves (non-negative) + * @return the index of the best move, or -1 if no moves are available + */ + @Override + public int findBestMove(TicTacToeR game, int depth) { + assert game != null; + assert depth >= 0; + + final int[] legalMoves = game.getLegalMoves(); + + if (legalMoves.length == 0) { + return -1; + } + + if (legalMoves.length == 9) { + return switch ((int)(Math.random() * 4)) { + case 0 -> legalMoves[2]; + case 1 -> legalMoves[6]; + case 2 -> legalMoves[8]; + default -> legalMoves[0]; + }; + } + + int bestScore = -depth; + int bestMove = -1; + + for (final int move : legalMoves) { + final int score = getMoveScore(game, depth, move, true); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + + return bestMove != -1 ? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; + } + + /** + * Recursively evaluates the score of a potential move using a minimax-like approach. + * + * @param game the current Tic-Tac-Toe game state + * @param depth remaining depth to evaluate + * @param move the move to evaluate + * @param maximizing true if the AI is to maximize score, false if minimizing + * @return the score of the move + */ + private int getMoveScore(TicTacToeR game, int depth, int move, boolean maximizing) { + final TicTacToeR copy = game.clone(); + final GameState state = copy.play(move); + + switch (state) { + case DRAW: return 0; + case WIN: return maximizing ? depth + 1 : -depth - 1; + } + + if (depth <= 0) { + return 0; + } + + final int[] legalMoves = copy.getLegalMoves(); + int score = maximizing ? depth + 1 : -depth - 1; + + for (final int 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; + } +} diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToeR.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToeR.java index cc09e3c..9acabf4 100644 --- a/game/src/main/java/org/toop/game/tictactoe/TicTacToeR.java +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToeR.java @@ -1,11 +1,13 @@ package org.toop.game.tictactoe; +import org.toop.game.GameR; import org.toop.game.TurnBasedGame; import org.toop.game.TurnBasedGameR; import org.toop.game.enumerators.GameState; import org.toop.game.records.Move; import java.util.ArrayList; +import java.util.Arrays; import java.util.Objects; public final class TicTacToeR extends TurnBasedGameR { @@ -22,7 +24,7 @@ public final class TicTacToeR extends TurnBasedGameR { } @Override - public Integer[] getLegalMoves() { + public int[] getLegalMoves() { final ArrayList legalMoves = new ArrayList(); final char currentValue = getCurrentValue(); @@ -31,8 +33,8 @@ public final class TicTacToeR extends TurnBasedGameR { legalMoves.add(i); } } - - return legalMoves.toArray(new Integer[0]); + System.out.println(Arrays.toString(legalMoves.stream().mapToInt(Integer::intValue).toArray())); + return legalMoves.stream().mapToInt(Integer::intValue).toArray(); } @Override @@ -87,7 +89,7 @@ public final class TicTacToeR extends TurnBasedGameR { private boolean checkForEarlyDraw() { for (final int move : this.getLegalMoves()) { - final TicTacToeR copy = new TicTacToeR(this); + final TicTacToeR copy = this.clone(); if (copy.play(move) == GameState.WIN || !copy.checkForEarlyDraw()) { return false; @@ -100,4 +102,9 @@ public final class TicTacToeR extends TurnBasedGameR { private char getCurrentValue() { return this.getCurrentTurn() == 0 ? 'X' : 'O'; } + + @Override + public TicTacToeR clone() { + return new TicTacToeR(this); + } }