From ecd0fd26be551a903aa5b7c9679f1833f8f2a8db Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 8 Dec 2025 15:11:17 +0100 Subject: [PATCH 1/9] changed "fullscreen exit key combination" from esc to F11 --- app/src/main/java/org/toop/app/App.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 8809c26..2135733 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -2,6 +2,8 @@ package org.toop.app; import javafx.application.Platform; import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import org.toop.app.widget.Primitive; @@ -87,6 +89,12 @@ public final class App extends Application { setKeybinds(root); + stage.setFullScreenExitKeyCombination( + new KeyCodeCombination( + KeyCode.F11 + ) + ); + LoadingWidget loading = new LoadingWidget(Primitive.text( "Loading...", false), 0, 0, Integer.MAX_VALUE, false, false // Just set a high default ); From 846898988f191d710b239f1e68172186b79d7b2c Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 8 Dec 2025 15:59:11 +0100 Subject: [PATCH 2/9] shitty fix for player selector spacing issue --- app/src/main/java/org/toop/app/App.java | 4 ++-- .../app/widget/complex/PlayerInfoWidget.java | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 2135733..aea9500 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -69,8 +69,8 @@ public final class App extends Application { scene.getRoot(); - stage.setMinWidth(1080); - stage.setMinHeight(720); + stage.setMinWidth(1200); + stage.setMinHeight(800); stage.setOnCloseRequest(event -> { event.consume(); quit(); diff --git a/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java index c69edf4..7a7389d 100644 --- a/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java @@ -23,7 +23,12 @@ public class PlayerInfoWidget { } private ToggleWidget buildToggle() { - return new ToggleWidget( + if(container != null) { + container.setMinHeight(container.getHeight()); + container.setPrefHeight(container.getHeight()); + container.setMaxHeight(container.getHeight()); + } + return new ToggleWidget( "computer", "player", information.isHuman, isHuman -> { @@ -37,7 +42,14 @@ public class PlayerInfoWidget { } private Node buildContent() { + if (information.isHuman) { + var spacer = Primitive.vbox( + makeAIButton(0, 0, "zwartepiet"), + makeAIButton(0, 0, "sinterklaas"), + makeAIButton(0, 0, "santa") + ); //todo make a better solution + spacer.setVisible(false); var nameInput = new LabeledInputWidget( "name", "enter-your-name", @@ -45,7 +57,7 @@ public class PlayerInfoWidget { newName -> information.name = newName ); - return nameInput.getNode(); + return Primitive.vbox(spacer,nameInput.getNode()); } else { var AIBox = Primitive.vbox( makeAIButton(0, 1, "zwartepiet"), From 3a8b1c245417dd708a15b0ae25677c67520fac7b Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 8 Dec 2025 16:12:42 +0100 Subject: [PATCH 3/9] shitty fix for player selector spacing issue v2 --- .../java/org/toop/app/widget/complex/PlayerInfoWidget.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java index 7a7389d..97a1140 100644 --- a/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java @@ -23,11 +23,6 @@ public class PlayerInfoWidget { } private ToggleWidget buildToggle() { - if(container != null) { - container.setMinHeight(container.getHeight()); - container.setPrefHeight(container.getHeight()); - container.setMaxHeight(container.getHeight()); - } return new ToggleWidget( "computer", "player", information.isHuman, From adc7b1a8f3920991fc853b1f9cedf22a5d9e3a21 Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 8 Dec 2025 17:14:31 +0100 Subject: [PATCH 4/9] fixed reversi colors being switched, causing multiple issues --- app/src/main/java/org/toop/app/App.java | 11 +++++------ .../toop/app/gameControllers/ReversiController.java | 6 +++--- .../java/org/toop/game/games/reversi/ReversiR.java | 8 ++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index aea9500..3557848 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -89,12 +89,6 @@ public final class App extends Application { setKeybinds(root); - stage.setFullScreenExitKeyCombination( - new KeyCodeCombination( - KeyCode.F11 - ) - ); - LoadingWidget loading = new LoadingWidget(Primitive.text( "Loading...", false), 0, 0, Integer.MAX_VALUE, false, false // Just set a high default ); @@ -154,6 +148,11 @@ public final class App extends Application { escapePopup(); } }); + stage.setFullScreenExitKeyCombination( + new KeyCodeCombination( + KeyCode.F11 + ) + ); } public void escapePopup() { diff --git a/app/src/main/java/org/toop/app/gameControllers/ReversiController.java b/app/src/main/java/org/toop/app/gameControllers/ReversiController.java index 1f6842f..a52d1f6 100644 --- a/app/src/main/java/org/toop/app/gameControllers/ReversiController.java +++ b/app/src/main/java/org/toop/app/gameControllers/ReversiController.java @@ -65,9 +65,9 @@ public class ReversiController extends AbstractGameController { canvas.clearAll(); for (int i = 0; i < game.getBoard().length; i++) { - if (game.getBoard()[i] == 0) { + if (game.getBoard()[i] == 1) { canvas.drawDot(Color.WHITE, i); - } else if (game.getBoard()[i] == 1) { + } else if (game.getBoard()[i] == 0) { canvas.drawDot(Color.BLACK, i); } } @@ -109,7 +109,7 @@ public class ReversiController extends AbstractGameController { public void drawLegalPosition(int cell, int player) { Color innerColor; - if (player == 1) { + if (player == 0) { innerColor = new Color(0.0f, 0.0f, 0.0f, 0.6f); } else { diff --git a/game/src/main/java/org/toop/game/games/reversi/ReversiR.java b/game/src/main/java/org/toop/game/games/reversi/ReversiR.java index 85d6334..bf946db 100644 --- a/game/src/main/java/org/toop/game/games/reversi/ReversiR.java +++ b/game/src/main/java/org/toop/game/games/reversi/ReversiR.java @@ -39,10 +39,10 @@ public final class ReversiR extends AbstractGame { private void addStartPieces() { - this.setBoard(27, 1); - this.setBoard(28, 0); - this.setBoard(35, 0); - this.setBoard(36, 1); + this.setBoard(27, 0); + this.setBoard(28, 1); + this.setBoard(35, 1); + this.setBoard(36, 0); updateFilledCellsSet(); } private void updateFilledCellsSet() { From 912d25c01f3f56ceec1e299d336aa022b2a4735f Mon Sep 17 00:00:00 2001 From: Stef <48526421+StefBuwalda@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:23:06 +0100 Subject: [PATCH 5/9] Merge bitboards into development (#285) * added new classes for the games that use bitboards instead. also combined game with turnbasedgame * (DOES NOT COMPILE) In-between commit * turn updates * smalle fixes aan turn updates * Bitboard implemented with scuffed TicTacToe translation done by game. This should be done by the view. * Almost done with implementing bitboards. Reversi is broken and artifical players don't work yet. * better human/ai selector with bot selection and depth on TicTacToeAIR * fixed getLegalMoves * depth + thinktime back to AIs, along with a a specific TicTacToeAIRSleep * fixed overlapping back and disconnect buttons * Changed to debug instead of info * changed the transitionNextCustom to be easier to use * added getAllWidgets to WidgetContainer * Correct back view * added replacePrevious in ViewWidget * added removeIndexFromPreviousChain * fixed incorrect index counting * Fixt wrong view order * fixed? getLegalMoves * Everything is broken * Removed todo * fixed getLegalMoves & getFlips * Challenge popups "Fixed" * Fixed local and online play for both games * Popups now remove themselves * Removed souts for debugging * localize the ChallengePopup text * made the game text a header instead * made more classes deepClonable. * fixed getAllWidgets * Added comment * Escape popup * fixed redundant container * Made all network events async again * Escape remove popup * Working escape menu * Removed old AI and old files. Added a new generic random AI. game no longer deals with translation. * Drawing of board on canvas is now done from bitboards rather than translating. * Added a method getWinner() to game interface.Controller now tells gameThreads how to deal with drawing UI and sending a move to server. * Added find functionality * Added a ChatGPT generated MiniMaxAI based on the old MiniMaxAI but with alpha-beta pruning and heuristics for Reversi * Removed System-Outs to clean up console * Update BitGameCanvas.java * Merge fixes * Removed unused imports --------- Co-authored-by: ramollia <> Co-authored-by: michiel301b Co-authored-by: lieght <49651652+BAFGdeJong@users.noreply.github.com> --- app/src/main/java/org/toop/app/Server.java | 37 +-- .../org/toop/app/canvas/BitGameCanvas.java | 247 +++++++++++++++++ .../org/toop/app/canvas/Connect4Canvas.java | 17 -- .../org/toop/app/canvas/DrawPlayerHover.java | 4 +- .../java/org/toop/app/canvas/GameCanvas.java | 237 +--------------- .../java/org/toop/app/canvas/GameDrawer.java | 7 + .../org/toop/app/canvas/ReversiBitCanvas.java | 42 +++ .../org/toop/app/canvas/ReversiCanvas.java | 96 ------- .../toop/app/canvas/TicTacToeBitCanvas.java | 63 +++++ .../org/toop/app/canvas/TicTacToeCanvas.java | 54 ---- .../AbstractGameController.java | 127 --------- .../GenericGameController.java | 140 ++++++++++ .../gameControllers/ReversiBitController.java | 22 ++ .../gameControllers/ReversiController.java | 138 ---------- .../TicTacToeBitController.java | 23 ++ .../gameControllers/TicTacToeController.java | 63 ----- .../toop/app/widget/popup/ChallengePopup.java | 1 + .../app/widget/view/LocalMultiplayerView.java | 42 +-- .../gameFramework/LongPairConsumer.java | 6 + .../controller/GameController.java | 18 ++ .../model/game/AbstractGame.java | 108 -------- .../model/game/BoardProvider.java | 5 + .../model/game/DeepCopyable.java | 2 +- .../gameFramework/model/game/Playable.java | 4 +- .../model/game/SupportsOnlinePlay.java | 6 +- .../model/game/TurnBasedGame.java | 4 +- .../AbstractThreadBehaviour.java | 30 +- .../game/threadBehaviour/ThreadBehaviour.java | 9 +- .../gameFramework/model/player/AI.java | 7 + .../model/player/AbstractAI.java | 2 +- .../model/player/AbstractPlayer.java | 10 +- .../model/player/MoveProvider.java | 4 +- .../gameFramework/model/player/Player.java | 3 +- .../gameFramework/view/GUIEvents.java | 8 +- .../main/java/org/toop/game/BitboardGame.java | 86 ++++++ .../LocalFixedRateThreadBehaviour.java | 15 +- .../gameThreads/LocalThreadBehaviour.java | 10 +- .../gameThreads/OnlineThreadBehaviour.java | 35 +-- .../OnlineWithSleepThreadBehaviour.java | 18 +- .../game/games/reversi/BitboardReversi.java | 170 ++++++++++++ .../toop/game/games/reversi/ReversiAIR.java | 15 - .../org/toop/game/games/reversi/ReversiR.java | 260 ------------------ .../games/tictactoe/BitboardTicTacToe.java | 103 +++++++ .../game/games/tictactoe/TicTacToeAIR.java | 108 -------- .../games/tictactoe/TicTacToeAIRSleep.java | 25 -- .../toop/game/games/tictactoe/TicTacToeR.java | 118 -------- .../toop/game/players/ArtificialPlayer.java | 20 +- .../org/toop/game/players/LocalPlayer.java | 29 +- .../java/org/toop/game/players/MiniMaxAI.java | 165 +++++++++++ .../org/toop/game/players/OnlinePlayer.java | 10 + .../java/org/toop/game/players/RandomAI.java | 38 +++ .../toop/game/tictactoe/TicTacToeAIRTest.java | 121 -------- 52 files changed, 1330 insertions(+), 1602 deletions(-) create mode 100644 app/src/main/java/org/toop/app/canvas/BitGameCanvas.java delete mode 100644 app/src/main/java/org/toop/app/canvas/Connect4Canvas.java create mode 100644 app/src/main/java/org/toop/app/canvas/GameDrawer.java create mode 100644 app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java delete mode 100644 app/src/main/java/org/toop/app/canvas/ReversiCanvas.java create mode 100644 app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java delete mode 100644 app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java delete mode 100644 app/src/main/java/org/toop/app/gameControllers/AbstractGameController.java create mode 100644 app/src/main/java/org/toop/app/gameControllers/GenericGameController.java create mode 100644 app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java delete mode 100644 app/src/main/java/org/toop/app/gameControllers/ReversiController.java create mode 100644 app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java delete mode 100644 app/src/main/java/org/toop/app/gameControllers/TicTacToeController.java create mode 100644 framework/src/main/java/org/toop/framework/gameFramework/LongPairConsumer.java create mode 100644 framework/src/main/java/org/toop/framework/gameFramework/controller/GameController.java delete mode 100644 framework/src/main/java/org/toop/framework/gameFramework/model/game/AbstractGame.java create mode 100644 framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java create mode 100644 framework/src/main/java/org/toop/framework/gameFramework/model/player/AI.java create mode 100644 game/src/main/java/org/toop/game/BitboardGame.java create mode 100644 game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java delete mode 100644 game/src/main/java/org/toop/game/games/reversi/ReversiAIR.java delete mode 100644 game/src/main/java/org/toop/game/games/reversi/ReversiR.java create mode 100644 game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java delete mode 100644 game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIR.java delete mode 100644 game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIRSleep.java delete mode 100644 game/src/main/java/org/toop/game/games/tictactoe/TicTacToeR.java create mode 100644 game/src/main/java/org/toop/game/players/MiniMaxAI.java create mode 100644 game/src/main/java/org/toop/game/players/RandomAI.java delete mode 100644 game/src/test/java/org/toop/game/tictactoe/TicTacToeAIRTest.java diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 0c4ad0e..ef691b4 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -2,9 +2,7 @@ package org.toop.app; import javafx.application.Platform; import javafx.geometry.Pos; -import org.toop.app.gameControllers.AbstractGameController; -import org.toop.app.gameControllers.ReversiController; -import org.toop.app.gameControllers.TicTacToeController; +import org.toop.app.gameControllers.*; import org.toop.app.widget.Primitive; import org.toop.app.widget.WidgetContainer; import org.toop.app.widget.complex.LoadingWidget; @@ -13,16 +11,17 @@ import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.popup.SendChallengePopup; import org.toop.app.widget.view.ServerView; import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.networking.types.NetworkingConnector; +import org.toop.game.games.reversi.BitboardReversi; +import org.toop.game.games.tictactoe.BitboardTicTacToe; import org.toop.game.players.ArtificialPlayer; import org.toop.game.players.OnlinePlayer; -import org.toop.game.games.reversi.ReversiAIR; -import org.toop.game.games.reversi.ReversiR; -import org.toop.game.games.tictactoe.TicTacToeAIR; +import org.toop.game.players.RandomAI; import org.toop.local.AppContext; import java.util.List; @@ -33,6 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public final class Server { + // TODO: Keep track of listeners. Remove them on Server connection close so reference is deleted. private String user = ""; private long clientId = -1; @@ -42,7 +42,7 @@ public final class Server { private ServerView primary; private boolean isPolling = true; - private AbstractGameController gameController; + private GameController gameController; private final AtomicBoolean isSingleGame = new AtomicBoolean(false); @@ -201,26 +201,31 @@ public final class Server { information.players[0].computerThinkTime = 1; information.players[1].name = response.opponent(); - Player[] players = new Player[2]; - - players[(myTurn + 1) % 2] = new OnlinePlayer(response.opponent()); - - switch (type){ + /*switch (type){ case TICTACTOE ->{ players[myTurn] = new ArtificialPlayer<>(new TicTacToeAIR(9), user); } case REVERSI ->{ players[myTurn] = new ArtificialPlayer<>(new ReversiAIR(), user); } - } + }*/ + + switch (type) { case TICTACTOE ->{ - gameController = new TicTacToeController(players, false); + Player[] players = new Player[2]; + players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent()); + players[myTurn] = new ArtificialPlayer<>(new RandomAI(), user); + gameController = new TicTacToeBitController(players); } - case REVERSI -> - gameController = new ReversiController(players, false); + case REVERSI -> { + Player[] players = new Player[2]; + players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent()); + players[myTurn] = new ArtificialPlayer<>(new RandomAI(), user); + gameController = new ReversiBitController(players);} default -> new ErrorPopup("Unsupported game type."); + } if (gameController != null){ diff --git a/app/src/main/java/org/toop/app/canvas/BitGameCanvas.java b/app/src/main/java/org/toop/app/canvas/BitGameCanvas.java new file mode 100644 index 0000000..85c6f7d --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/BitGameCanvas.java @@ -0,0 +1,247 @@ +package org.toop.app.canvas; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.input.MouseButton; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.util.Duration; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.view.GUIEvents; + +import java.util.function.Consumer; + +public abstract class BitGameCanvas> implements GameCanvas { + protected record Cell(float x, float y, float width, float height) { + public boolean isInside(double x, double y) { + return x >= this.x && x <= this.x + width && + y >= this.y && y <= this.y + height; + } + } + + protected final Canvas canvas; + protected final GraphicsContext graphics; + + protected final Color color; + protected final Color backgroundColor; + + protected final int width; + protected final int height; + + protected final int rowSize; + protected final int columnSize; + + protected final int gapSize; + protected final boolean edges; + + protected final Cell[] cells; + + private Consumer onCellCLicked; + + public void setOnCellClicked(Consumer onClick) { + this.onCellCLicked = onClick; + } + + protected BitGameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges) { + canvas = new Canvas(width, height); + graphics = canvas.getGraphicsContext2D(); + + this.onCellCLicked = (c) -> new EventFlow().addPostEvent(GUIEvents.PlayerAttemptedMove.class, c).postEvent(); + + this.color = color; + this.backgroundColor = backgroundColor; + + this.width = width; + this.height = height; + + this.rowSize = rowSize; + this.columnSize = columnSize; + + this.gapSize = gapSize; + this.edges = edges; + + cells = new Cell[rowSize * columnSize]; + + final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize; + final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize; + + for (int y = 0; y < columnSize; y++) { + final float startY = y * cellHeight + y * gapSize + gapSize; + + for (int x = 0; x < rowSize; x++) { + final float startX = x * cellWidth + x * gapSize + gapSize; + cells[x + y * rowSize] = new Cell(startX, startY, cellWidth, cellHeight); + } + } + + canvas.setOnMouseClicked(event -> { + if (event.getButton() != MouseButton.PRIMARY) { + return; + } + + final int column = (int) ((event.getX() / this.width) * rowSize); + final int row = (int) ((event.getY() / this.height) * columnSize); + + final Cell cell = cells[column + row * rowSize]; + + if (cell.isInside(event.getX(), event.getY())) { + event.consume(); + this.onCellCLicked.accept(1L << (column + row * rowSize)); + } + }); + + + + + render(); + } + + public void loopOverBoard(long bb, Consumer onCell){ + while (bb != 0) { + int idx = Long.numberOfTrailingZeros(bb); // index of least-significant 1-bit + onCell.accept(idx); + + bb &= bb - 1; // clear LSB 1-bit + } + } + + private void render() { + graphics.setFill(backgroundColor); + graphics.fillRect(0, 0, width, height); + + graphics.setFill(color); + + for (int x = 0; x < rowSize - 1; x++) { + final float start = cells[x].x + cells[x].width; + graphics.fillRect(start, gapSize, gapSize, height - gapSize * 2); + } + + for (int y = 0; y < columnSize - 1; y++) { + final float start = cells[y * rowSize].y + cells[y * rowSize].height; + graphics.fillRect(gapSize, start, width - gapSize * 2, gapSize); + } + + if (edges) { + graphics.fillRect(0, 0, width, gapSize); + graphics.fillRect(0, 0, gapSize, height); + + graphics.fillRect(width - gapSize, 0, gapSize, height); + graphics.fillRect(0, height - gapSize, width, gapSize); + } + } + + public void fill(Color color, int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); + + final float width = cells[cell].width(); + final float height = cells[cell].height(); + + graphics.setFill(color); + graphics.fillRect(x, y, width, height); + } + + public void clear(int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); + + final float width = cells[cell].width(); + final float height = cells[cell].height(); + + graphics.clearRect(x, y, width, height); + + graphics.setFill(backgroundColor); + graphics.fillRect(x, y, width, height); + } + + public void clearAll() { + for (int i = 0; i < cells.length; i++) { + clear(i); + } + } + + public void drawPlayerMove(int player, int move) { + final float x = cells[move].x() + gapSize; + final float y = cells[move].y() + gapSize; + + final float width = cells[move].width() - gapSize * 2; + final float height = cells[move].height() - gapSize * 2; + + graphics.setFill(color); + graphics.setFont(Font.font("Arial", 40)); // TODO different font and size + graphics.fillText(String.valueOf(player), x + width, y + height); + } + + public void drawDot(Color color, int cell) { + 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.setFill(color); + graphics.fillOval(x, y, width, height); + } + + public void drawInnerDot(Color color, int cell, boolean slightlyBigger) { + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + gapSize; + + float multiplier = slightlyBigger?1.4f:1.5f; + + final float width = (cells[cell].width() - gapSize * 2)/multiplier; + final float height = (cells[cell].height() - gapSize * 2)/multiplier; + + float offset = slightlyBigger?5f:4f; + + graphics.setFill(color); + graphics.fillOval(x + width/offset, y + height/offset, width, height); + } + + private void drawDotScaled(Color color, int cell, double scale) { + final float cx = cells[cell].x() + gapSize; + final float cy = cells[cell].y() + gapSize; + + final float fullWidth = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; + + final float scaledWidth = (float)(fullWidth * scale); + final float offsetX = (fullWidth - scaledWidth) / 2; + + graphics.setFill(color); + graphics.fillOval(cx + offsetX, cy, scaledWidth, height); + } + + public Timeline flipDot(Color fromColor, Color toColor, int cell) { + final int steps = 60; + final long duration = 250; + final double interval = duration / (double) steps; + + final Timeline timeline = new Timeline(); + + for (int i = 0; i <= steps; i++) { + final double t = i / (double) steps; + final KeyFrame keyFrame = new KeyFrame(Duration.millis(i * interval), + _ -> { + clear(cell); + + final double scale = t <= 0.5 ? 1 - 2 * t : 2 * t - 1; + final Color currentColor = t < 0.5 ? fromColor : toColor; + + drawDotScaled(currentColor, cell, scale); + } + ); + + timeline.getKeyFrames().add(keyFrame); + } + + return timeline; + } + + public Canvas getCanvas() { + return canvas; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java deleted file mode 100644 index 4a8c7a2..0000000 --- a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.toop.app.canvas; - -import javafx.scene.paint.Color; -import org.toop.framework.gameFramework.model.game.AbstractGame; - -import java.util.function.Consumer; - -public class Connect4Canvas extends GameCanvas { - public Connect4Canvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked,null); - } - - @Override - public void drawPlayerHover(int player, int move, AbstractGame game) { - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java b/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java index 0c40a25..abf9ac1 100644 --- a/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java +++ b/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java @@ -1,7 +1,7 @@ package org.toop.app.canvas; -import org.toop.framework.gameFramework.model.game.AbstractGame; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; public interface DrawPlayerHover { - void drawPlayerHover(int player, int move, AbstractGame game); + void drawPlayerHover(int player, int move, TurnBasedGame game); } diff --git a/app/src/main/java/org/toop/app/canvas/GameCanvas.java b/app/src/main/java/org/toop/app/canvas/GameCanvas.java index 0e78ab4..d1361c5 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -1,237 +1,8 @@ package org.toop.app.canvas; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.input.MouseButton; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.util.Duration; -import org.toop.framework.gameFramework.model.game.AbstractGame; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; -import java.util.function.Consumer; - -public abstract class GameCanvas implements DrawPlayerMove, DrawPlayerHover { - protected record Cell(float x, float y, float width, float height) { - public boolean isInside(double x, double y) { - return x >= this.x && x <= this.x + width && - y >= this.y && y <= this.y + height; - } - } - - protected final Canvas canvas; - protected final GraphicsContext graphics; - - protected final Color color; - protected final Color backgroundColor; - - protected final int width; - protected final int height; - - protected final int rowSize; - protected final int columnSize; - - protected final int gapSize; - protected final boolean edges; - - protected final Cell[] cells; - - private Consumer onCellCLicked; - - public void setOnCellClicked(Consumer onClick) { - this.onCellCLicked = onClick; - } - - protected GameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked, Consumer newCellEntered) { - canvas = new Canvas(width, height); - graphics = canvas.getGraphicsContext2D(); - - this.onCellCLicked = onCellClicked; - - this.color = color; - this.backgroundColor = backgroundColor; - - this.width = width; - this.height = height; - - this.rowSize = rowSize; - this.columnSize = columnSize; - - this.gapSize = gapSize; - this.edges = edges; - - cells = new Cell[rowSize * columnSize]; - - final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize; - final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize; - - for (int y = 0; y < columnSize; y++) { - final float startY = y * cellHeight + y * gapSize + gapSize; - - for (int x = 0; x < rowSize; x++) { - final float startX = x * cellWidth + x * gapSize + gapSize; - cells[x + y * rowSize] = new Cell(startX, startY, cellWidth, cellHeight); - } - } - - canvas.setOnMouseClicked(event -> { - if (event.getButton() != MouseButton.PRIMARY) { - return; - } - - final int column = (int) ((event.getX() / this.width) * rowSize); - final int row = (int) ((event.getY() / this.height) * columnSize); - - final Cell cell = cells[column + row * rowSize]; - - if (cell.isInside(event.getX(), event.getY())) { - event.consume(); - this.onCellCLicked.accept(column + row * rowSize); - } - }); - - - - - render(); - } - - private void render() { - graphics.setFill(backgroundColor); - graphics.fillRect(0, 0, width, height); - - graphics.setFill(color); - - for (int x = 0; x < rowSize - 1; x++) { - final float start = cells[x].x + cells[x].width; - graphics.fillRect(start, gapSize, gapSize, height - gapSize * 2); - } - - for (int y = 0; y < columnSize - 1; y++) { - final float start = cells[y * rowSize].y + cells[y * rowSize].height; - graphics.fillRect(gapSize, start, width - gapSize * 2, gapSize); - } - - if (edges) { - graphics.fillRect(0, 0, width, gapSize); - graphics.fillRect(0, 0, gapSize, height); - - graphics.fillRect(width - gapSize, 0, gapSize, height); - graphics.fillRect(0, height - gapSize, width, gapSize); - } - } - - public void fill(Color color, int cell) { - final float x = cells[cell].x(); - final float y = cells[cell].y(); - - final float width = cells[cell].width(); - final float height = cells[cell].height(); - - graphics.setFill(color); - graphics.fillRect(x, y, width, height); - } - - public void clear(int cell) { - final float x = cells[cell].x(); - final float y = cells[cell].y(); - - final float width = cells[cell].width(); - final float height = cells[cell].height(); - - graphics.clearRect(x, y, width, height); - - graphics.setFill(backgroundColor); - graphics.fillRect(x, y, width, height); - } - - public void clearAll() { - for (int i = 0; i < cells.length; i++) { - clear(i); - } - } - - @Override - public void drawPlayerMove(int player, int move) { - final float x = cells[move].x() + gapSize; - final float y = cells[move].y() + gapSize; - - final float width = cells[move].width() - gapSize * 2; - final float height = cells[move].height() - gapSize * 2; - - graphics.setFill(color); - graphics.setFont(Font.font("Arial", 40)); // TODO different font and size - graphics.fillText(String.valueOf(player), x + width, y + height); - } - - public void drawDot(Color color, int cell) { - 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.setFill(color); - graphics.fillOval(x, y, width, height); - } - - public void drawInnerDot(Color color, int cell, boolean slightlyBigger) { - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; - - float multiplier = slightlyBigger?1.4f:1.5f; - - final float width = (cells[cell].width() - gapSize * 2)/multiplier; - final float height = (cells[cell].height() - gapSize * 2)/multiplier; - - float offset = slightlyBigger?5f:4f; - - graphics.setFill(color); - graphics.fillOval(x + width/offset, y + height/offset, width, height); - } - - private void drawDotScaled(Color color, int cell, double scale) { - final float cx = cells[cell].x() + gapSize; - final float cy = cells[cell].y() + gapSize; - - final float fullWidth = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; - - final float scaledWidth = (float)(fullWidth * scale); - final float offsetX = (fullWidth - scaledWidth) / 2; - - graphics.setFill(color); - graphics.fillOval(cx + offsetX, cy, scaledWidth, height); - } - - public Timeline flipDot(Color fromColor, Color toColor, int cell) { - final int steps = 60; - final long duration = 250; - final double interval = duration / (double) steps; - - final Timeline timeline = new Timeline(); - - for (int i = 0; i <= steps; i++) { - final double t = i / (double) steps; - final KeyFrame keyFrame = new KeyFrame(Duration.millis(i * interval), - _ -> { - clear(cell); - - final double scale = t <= 0.5 ? 1 - 2 * t : 2 * t - 1; - final Color currentColor = t < 0.5 ? fromColor : toColor; - - drawDotScaled(currentColor, cell, scale); - } - ); - - timeline.getKeyFrames().add(keyFrame); - } - - return timeline; - } - - public Canvas getCanvas() { - return canvas; - } -} \ No newline at end of file +public interface GameCanvas> extends GameDrawer{ + Canvas getCanvas(); +} diff --git a/app/src/main/java/org/toop/app/canvas/GameDrawer.java b/app/src/main/java/org/toop/app/canvas/GameDrawer.java new file mode 100644 index 0000000..261334c --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/GameDrawer.java @@ -0,0 +1,7 @@ +package org.toop.app.canvas; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; + +public interface GameDrawer> { + void redraw(T gameCopy); +} diff --git a/app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java new file mode 100644 index 0000000..7c2bde0 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java @@ -0,0 +1,42 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.game.games.reversi.BitboardReversi; + +import java.util.Arrays; +import java.util.function.Consumer; + +public class ReversiBitCanvas extends BitGameCanvas { + public ReversiBitCanvas() { + super(Color.GRAY, new Color(0f, 0.4f, 0.2f, 1f), (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, 8, 8, 5, true); + canvas.setOnMouseMoved(event -> { + double mouseX = event.getX(); + double mouseY = event.getY(); + int cellId = -1; + + BitGameCanvas.Cell hovered = null; + for (BitGameCanvas.Cell cell : cells) { + if (cell.isInside(mouseX, mouseY)) { + hovered = cell; + cellId = turnCoordsIntoCellId(mouseX, mouseY); + break; + } + } + }); + } + + private int turnCoordsIntoCellId(double x, double y) { + final int column = (int) ((x / this.width) * rowSize); + final int row = (int) ((y / this.height) * columnSize); + return column + row * rowSize; + } + + @Override + public void redraw(BitboardReversi gameCopy) { + clearAll(); + long[] board = gameCopy.getBoard(); + loopOverBoard(board[0], (i) -> drawDot(Color.WHITE, i)); + loopOverBoard(board[1], (i) -> drawDot(Color.BLACK, i)); + } +} diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java deleted file mode 100644 index 090df29..0000000 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.toop.app.canvas; - -import javafx.scene.paint.Color; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.game.Move; -import org.toop.game.games.reversi.ReversiR; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -public final class ReversiCanvas extends GameCanvas { - private Move[] currentlyHighlightedMoves = null; - public ReversiCanvas(Color color, int width, int height, Consumer onCellClicked, Consumer newCellEntered) { - super(color, new Color(0f,0.4f,0.2f,1f), width, height, 8, 8, 5, true, onCellClicked, newCellEntered); - drawStartingDots(); - - final AtomicReference lastHoveredCell = new AtomicReference<>(null); - - canvas.setOnMouseMoved(event -> { - double mouseX = event.getX(); - double mouseY = event.getY(); - int cellId = -1; - - Cell hovered = null; - for (Cell cell : cells) { - if (cell.isInside(mouseX, mouseY)) { - hovered = cell; - cellId = turnCoordsIntoCellId(mouseX, mouseY); - break; - } - } - - Cell previous = lastHoveredCell.get(); - - if (hovered != previous) { - lastHoveredCell.set(hovered); - newCellEntered.accept(cellId); - } - }); - } - - public void setCurrentlyHighlightedMovesNull() { - currentlyHighlightedMoves = null; - } - - public void drawHighlightDots(Move[] moves){ - if (currentlyHighlightedMoves != null){ - for (final Move move : currentlyHighlightedMoves){ - Color color = move.value() == 'W'? Color.BLACK: Color.WHITE; - drawInnerDot(color, move.position(), true); - } - } - currentlyHighlightedMoves = moves; - if (moves != null) { - for (Move move : moves) { - Color color = move.value() == 'B' ? Color.BLACK : Color.WHITE; - drawInnerDot(color, move.position(), false); - } - } - } - - private int turnCoordsIntoCellId(double x, double y) { - final int column = (int) ((x / this.width) * rowSize); - final int row = (int) ((y / this.height) * columnSize); - return column + row * rowSize; - } - - public void drawStartingDots() { - drawDot(Color.BLACK, 28); - drawDot(Color.WHITE, 36); - drawDot(Color.BLACK, 35); - drawDot(Color.WHITE, 27); - } - - public void drawLegalPosition(int cell, char player) { - - Color innerColor; - if (player == 'B') { - innerColor = new Color(0.0f, 0.0f, 0.0f, 0.6f); - } - else { - innerColor = new Color(1.0f, 1.0f, 1.0f, 0.75f); - } - drawInnerDot(innerColor, cell,false); - } - - @Override - public void drawPlayerMove(int player ,int move){ - super.drawPlayerMove(player, move); - } - - @Override - public void drawPlayerHover(int player, int move, AbstractGame game) { - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java new file mode 100644 index 0000000..443adbd --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java @@ -0,0 +1,63 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.game.games.tictactoe.BitboardTicTacToe; + +import java.util.Arrays; +import java.util.function.Consumer; + +public class TicTacToeBitCanvas extends BitGameCanvas{ + public TicTacToeBitCanvas() { + super( + Color.GRAY, + Color.TRANSPARENT, + (App.getHeight() / 4) * 3, + (App.getHeight() / 4) * 3, + 3, + 3, + 30, + false + ); + } + + @Override + public void redraw(BitboardTicTacToe gameCopy) { + clearAll(); + drawMoves(gameCopy.getBoard()); + } + + private void drawMoves(long[] gameBoard){ + loopOverBoard(gameBoard[0], (i) -> drawX(Color.RED, i)); + loopOverBoard(gameBoard[1], (i) -> drawO(Color.BLUE, i)); + + } + + + 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); + } + + 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); + } +} diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java deleted file mode 100644 index 41d398f..0000000 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.toop.app.canvas; - -import javafx.scene.paint.Color; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.game.games.tictactoe.TicTacToeR; - -import java.util.function.Consumer; - -public final class TicTacToeCanvas extends GameCanvas { - public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked,null); - } - - @Override - public void drawPlayerMove(int player, int move) { - switch (player) { - case 0 -> drawX(Color.RED, move); - case 1 -> drawO(Color.BLUE, move); - default -> super.drawPlayerMove(player, move); - } - } - - 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); - } - - 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); - } - - @Override - public void drawPlayerHover(int player, int move, AbstractGame game) { - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/gameControllers/AbstractGameController.java b/app/src/main/java/org/toop/app/gameControllers/AbstractGameController.java deleted file mode 100644 index 7d670e4..0000000 --- a/app/src/main/java/org/toop/app/gameControllers/AbstractGameController.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.toop.app.gameControllers; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.framework.gameFramework.controller.UpdatesGameUI; -import org.toop.framework.gameFramework.view.GUIEvents; -import org.toop.app.canvas.GameCanvas; -import org.toop.framework.networking.events.NetworkEvents; -import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; -import org.toop.app.widget.view.GameView; -import org.toop.framework.eventbus.EventFlow; -import org.toop.game.gameThreads.OnlineThreadBehaviour; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; -import org.toop.framework.gameFramework.model.player.Player; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -public abstract class AbstractGameController> implements UpdatesGameUI, ThreadBehaviour { - protected final EventFlow eventFlow = new EventFlow(); - - protected final List> listeners = new ArrayList<>(); - - // Logger for logging ofcourse - protected final Logger logger = LogManager.getLogger(this.getClass()); - - // Reference to primary view - protected final GameView primary; - - // Reference to game canvas - protected final GameCanvas canvas; - - private final Player[] players; // List of players, can't be changed. - protected final T game; // Reference to game instance - private final ThreadBehaviour gameThreadBehaviour; - - // TODO: Change gameType to automatically happen with either dependency injection or something else. - // TODO: Make visualisation of moves a behaviour. - protected AbstractGameController(GameCanvas canvas, Player[] players, T game, ThreadBehaviour gameThreadBehaviour, String gameType) { - logger.info("Creating AbstractGameController"); - // Make sure player list matches expected size - if (players.length != game.getPlayerCount()){ - logger.error("Player count mismatch"); - throw new IllegalArgumentException("players and game's players must have same length"); - } - - this.canvas = canvas; - this.players = players; - this.game = game; - this.gameThreadBehaviour = gameThreadBehaviour; - - primary = new GameView(null, null, null, gameType); - addListeners(); - } - - public void start(){ - logger.info("Starting GameManager"); - gameThreadBehaviour.start();; - } - - public void stop(){ - logger.info("Stopping GameManager"); - removeListeners(); - gameThreadBehaviour.stop(); - } - - public Player getCurrentPlayer(){ - return game.getPlayer(getCurrentPlayerIndex()); - }; - - public int getCurrentPlayerIndex(){ - return game.getCurrentTurn(); - } - - private void addListeners(){ - eventFlow - .listen(GUIEvents.RefreshGameCanvas.class, this::onUpdateGameUI, false) - .listen(GUIEvents.GameEnded.class, this::onGameFinish, false); - } - - private void removeListeners(){ - eventFlow.unsubscribeAll(); - } - - private void onUpdateGameUI(GUIEvents.RefreshGameCanvas event){ - this.updateUI(); - } - - private void onGameFinish(GUIEvents.GameEnded event){ - logger.info("Game Finished"); - String name = event.winner() == -1 ? null : getPlayer(event.winner()).getName(); - primary.gameOver(event.winOrTie(), name); - stop(); - } - - public Player getPlayer(int player){ - if (player < 0 || player >= players.length){ - logger.error("Invalid player index"); - throw new IllegalArgumentException("player out of range"); - } - return players[player]; - } - - private boolean isOnline(){ - return this.gameThreadBehaviour instanceof SupportsOnlinePlay; - } - - public void onYourTurn(NetworkEvents.YourTurnResponse event){ - if (isOnline()){ - ((OnlineThreadBehaviour) this.gameThreadBehaviour).onYourTurn(event); - } - } - - public void onMoveReceived(NetworkEvents.GameMoveResponse event){ - if (isOnline()){ - ((OnlineThreadBehaviour) this.gameThreadBehaviour).onMoveReceived(event); - } - } - - public void gameFinished(NetworkEvents.GameResultResponse event){ - if (isOnline()){ - ((OnlineThreadBehaviour) this.gameThreadBehaviour).gameFinished(event); - } - } -} diff --git a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java new file mode 100644 index 0000000..2c3ad49 --- /dev/null +++ b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java @@ -0,0 +1,140 @@ +package org.toop.app.gameControllers; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.toop.app.canvas.GameCanvas; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.eventbus.GlobalEventBus; +import org.toop.framework.gameFramework.controller.GameController; +import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.framework.gameFramework.view.GUIEvents; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.players.LocalPlayer; + +public class GenericGameController> implements GameController { + protected final EventFlow eventFlow = new EventFlow(); + + // Logger for logging + protected final Logger logger = LogManager.getLogger(this.getClass()); + + // Reference to gameView view + protected final GameView gameView; + + // Reference to game canvas + protected final GameCanvas canvas; + + protected final TurnBasedGame game; // Reference to game instance + private final ThreadBehaviour gameThreadBehaviour; + + // TODO: Change gameType to automatically happen with either dependency injection or something else. + public GenericGameController(GameCanvas canvas, T game, ThreadBehaviour gameThreadBehaviour, String gameType) { + logger.info("Creating: " + this.getClass()); + + this.canvas = canvas; + this.game = game; + this.gameThreadBehaviour = gameThreadBehaviour; + + // Tell thread how to send moves + this.gameThreadBehaviour.setOnSendMove((id, m) -> GlobalEventBus.get().post(new NetworkEvents.SendMove(id, (short)translateMove(m)))); + + // Tell thread how to update UI + this.gameThreadBehaviour.setOnUpdateUI(() -> Platform.runLater(this::updateUI)); + + // Change scene to game view + gameView = new GameView(null, null, null, gameType); + gameView.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(gameView, true); + + // Listen to updates + eventFlow + .listen(GUIEvents.GameEnded.class, this::onGameFinish, false) + .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}, false); + } + + public void start(){ + logger.info("Starting GameManager"); + updateUI(); + gameThreadBehaviour.start(); + } + + public void stop(){ + logger.info("Stopping GameManager"); + removeListeners(); + gameThreadBehaviour.stop(); + } + + public Player getCurrentPlayer(){ + return game.getPlayer(getCurrentPlayerIndex()); + } + + public int getCurrentPlayerIndex(){ + return game.getCurrentTurn(); + } + + protected long translateMove(int move){ + return 1L << move; + } + + protected int translateMove(long move){ + return Long.numberOfTrailingZeros(move); + } + + private void removeListeners(){ + eventFlow.unsubscribeAll(); + } + + private void onGameFinish(GUIEvents.GameEnded event){ + logger.info("Game Finished"); + String name = event.winner() == -1 ? null : getPlayer(event.winner()).getName(); + gameView.gameOver(event.winOrTie(), name); + stop(); + } + + public Player getPlayer(int player){ + if (player < 0 || player >= 2){ // TODO: Make game turn player count + logger.error("Invalid player index"); + throw new IllegalArgumentException("player out of range"); + } + return game.getPlayer(player); + } + + private boolean isOnline(){ + return this.gameThreadBehaviour instanceof SupportsOnlinePlay; + } + + public void onYourTurn(NetworkEvents.YourTurnResponse event){ + if (isOnline()){ + ((SupportsOnlinePlay) this.gameThreadBehaviour).onYourTurn(event.clientId()); + } + } + + public void onMoveReceived(NetworkEvents.GameMoveResponse event){ + if (isOnline()){ + ((SupportsOnlinePlay) this.gameThreadBehaviour).onMoveReceived( + translateMove(Integer.parseInt(event.move()))); + } + } + + public void gameFinished(NetworkEvents.GameResultResponse event){ + if (isOnline()){ + ((SupportsOnlinePlay) this.gameThreadBehaviour).gameFinished(event.condition()); + } + } + + @Override + public void sendMove(long clientId, long move) { + new EventFlow().addPostEvent(NetworkEvents.SendMove.class, clientId, (short) Long.numberOfTrailingZeros(move)).asyncPostEvent(); + } + + @Override + public void updateUI() { + canvas.redraw(game.deepCopy()); + } +} diff --git a/app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java b/app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java new file mode 100644 index 0000000..40784b0 --- /dev/null +++ b/app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java @@ -0,0 +1,22 @@ +package org.toop.app.gameControllers; + +import org.toop.app.canvas.ReversiBitCanvas; +import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.gameThreads.LocalThreadBehaviour; +import org.toop.game.gameThreads.OnlineThreadBehaviour; +import org.toop.game.games.reversi.BitboardReversi; +import org.toop.game.players.OnlinePlayer; + +public class ReversiBitController extends GenericGameController { + public ReversiBitController(Player[] players) { + BitboardReversi game = new BitboardReversi(players); + ThreadBehaviour thread = new LocalThreadBehaviour<>(game); + for (Player player : players) { + if (player instanceof OnlinePlayer){ + thread = new OnlineThreadBehaviour<>(game); + } + } + super(new ReversiBitCanvas(), game, thread, "Reversi"); + } +} diff --git a/app/src/main/java/org/toop/app/gameControllers/ReversiController.java b/app/src/main/java/org/toop/app/gameControllers/ReversiController.java deleted file mode 100644 index a52d1f6..0000000 --- a/app/src/main/java/org/toop/app/gameControllers/ReversiController.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.toop.app.gameControllers; - -import javafx.animation.SequentialTransition; -import javafx.geometry.Pos; -import javafx.scene.paint.Color; -import org.toop.app.App; -import org.toop.app.canvas.ReversiCanvas; -import org.toop.app.widget.WidgetContainer; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.framework.gameFramework.view.GUIEvents; -import org.toop.game.gameThreads.LocalFixedRateThreadBehaviour; -import org.toop.game.gameThreads.OnlineThreadBehaviour; -import org.toop.game.players.LocalPlayer; -import org.toop.framework.gameFramework.model.player.Player; -import org.toop.game.games.reversi.ReversiR; - -public class ReversiController extends AbstractGameController { - // TODO: Refactor GUI update methods to follow designed system - public ReversiController(Player[] players, boolean local) { - ReversiR ReversiR = new ReversiR(players); - super( - new ReversiCanvas(Color.GRAY, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,(c) -> {new EventFlow().addPostEvent(GUIEvents.PlayerAttemptedMove.class, c).postEvent();}, (c) -> {new EventFlow().addPostEvent(GUIEvents.PlayerMoveHovered.class, c).postEvent();}), - players, - ReversiR, - local ? new LocalFixedRateThreadBehaviour<>(ReversiR, players) : new OnlineThreadBehaviour<>(ReversiR, players), // TODO: Player order matters here, this won't work atm - "Reversi"); - eventFlow.listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}, false); - eventFlow.listen(GUIEvents.PlayerMoveHovered.class, this::onHoverMove, false); - initUI(); - } - - - private void onHoverMove(GUIEvents.PlayerMoveHovered event){ - int cellEntered = event.move(); - canvas.drawPlayerHover(-1, cellEntered, game); - /*// (information.players[game.getCurrentTurn()].isHuman) { - int[] legalMoves = game.getLegalMoves(); - boolean isLegalMove = false; - for (int move : legalMoves) { - if (move == cellEntered){ - isLegalMove = true; - break; - } - } - - if (cellEntered >= 0){ - int[] moves = null; - if (isLegalMove) { - moves = game.getFlipsForPotentialMove( - new Point(cellEntered%game.getColumnSize(),cellEntered/game.getRowSize()), - game.getCurrentPlayer()); - } - canvas.drawHighlightDots(moves); - } - //}*/ - } - - public ReversiController(Player[] players) { - this(players, true); - } - - private void updateCanvas(boolean animate) { - // Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve. - canvas.clearAll(); - - for (int i = 0; i < game.getBoard().length; i++) { - if (game.getBoard()[i] == 1) { - canvas.drawDot(Color.WHITE, i); - } else if (game.getBoard()[i] == 0) { - canvas.drawDot(Color.BLACK, i); - } - } - - final int[] flipped = game.getMostRecentlyFlippedPieces(); - - final SequentialTransition animation = new SequentialTransition(); - - final Color fromColor = getCurrentPlayerIndex() == 0? Color.WHITE : Color.BLACK; - final Color toColor = getCurrentPlayerIndex() == 0? Color.BLACK : Color.WHITE; - - if (animate && flipped != null) { - for (final int flip : flipped) { - canvas.clear(flip); - canvas.drawDot(fromColor, flip); - animation.getChildren().addFirst(canvas.flipDot(fromColor, toColor, flip)); - } - } - - animation.setOnFinished(_ -> { - - if (getCurrentPlayer() instanceof LocalPlayer) { - final int[] legalMoves = game.getLegalMoves(); - - for (final int legalMove : legalMoves) { - drawLegalPosition(legalMove, getCurrentPlayerIndex()); - } - } - }); - - animation.play(); - primary.nextPlayer(true, getCurrentPlayer().getName(), game.getCurrentTurn() == 0 ? "X" : "O", getPlayer((game.getCurrentTurn() + 1) % 2).getName(), 'R'); - } - - @Override - public void updateUI() { - updateCanvas(false); - } - - public void drawLegalPosition(int cell, int player) { - Color innerColor; - if (player == 0) { - innerColor = new Color(0.0f, 0.0f, 0.0f, 0.6f); - } - else { - innerColor = new Color(1.0f, 1.0f, 1.0f, 0.75f); - } - canvas.drawInnerDot(innerColor, cell,false); - } - - private void initUI(){ - primary.add(Pos.CENTER, canvas.getCanvas()); - WidgetContainer.getCurrentView().transitionNext(primary, true); - updateCanvas(false); - } - - private void drawMoves(){ - int[] board = game.getBoard(); - - // Draw each square - for (int i = 0; i < board.length; i++){ - // If square isn't empty, draw player move - if (board[i] != AbstractGame.EMPTY){ - canvas.drawPlayerMove(board[i], i); - } - } - } -} diff --git a/app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java b/app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java new file mode 100644 index 0000000..6307894 --- /dev/null +++ b/app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java @@ -0,0 +1,23 @@ +package org.toop.app.gameControllers; + +import org.toop.app.canvas.TicTacToeBitCanvas; +import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.gameThreads.LocalFixedRateThreadBehaviour; +import org.toop.game.gameThreads.LocalThreadBehaviour; +import org.toop.game.gameThreads.OnlineThreadBehaviour; +import org.toop.game.games.tictactoe.BitboardTicTacToe; +import org.toop.game.players.OnlinePlayer; + +public class TicTacToeBitController extends GenericGameController { + public TicTacToeBitController(Player[] players) { + BitboardTicTacToe game = new BitboardTicTacToe(players); + ThreadBehaviour thread = new LocalThreadBehaviour<>(game); + for (Player player : players) { + if (player instanceof OnlinePlayer){ + thread = new OnlineThreadBehaviour<>(game); + } + } + super(new TicTacToeBitCanvas(), game, thread , "TicTacToe"); + } +} diff --git a/app/src/main/java/org/toop/app/gameControllers/TicTacToeController.java b/app/src/main/java/org/toop/app/gameControllers/TicTacToeController.java deleted file mode 100644 index eb8d8c9..0000000 --- a/app/src/main/java/org/toop/app/gameControllers/TicTacToeController.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.toop.app.gameControllers; - -import javafx.geometry.Pos; -import javafx.scene.paint.Color; -import org.toop.app.App; -import org.toop.app.canvas.TicTacToeCanvas; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.gameFramework.model.player.Player; -import org.toop.framework.gameFramework.view.GUIEvents; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.game.gameThreads.LocalThreadBehaviour; -import org.toop.game.gameThreads.OnlineThreadBehaviour; -import org.toop.game.players.LocalPlayer; -import org.toop.app.widget.WidgetContainer; -import org.toop.game.games.tictactoe.TicTacToeR; - -public class TicTacToeController extends AbstractGameController { - - public TicTacToeController(Player[] players, boolean local) { - TicTacToeR ticTacToeR = new TicTacToeR(players); - super( - new TicTacToeCanvas(Color.GRAY, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,(c) -> {new EventFlow().addPostEvent(GUIEvents.PlayerAttemptedMove.class, c).postEvent();}), - players, - ticTacToeR, - local ? new LocalThreadBehaviour(ticTacToeR, players) : new OnlineThreadBehaviour<>(ticTacToeR, players), // TODO: Player order matters here, this won't work atm - "TicTacToe"); - - initUI(); - eventFlow.listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}, false); - //addListener(GlobalEventBus.subscribe(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}})); - //new EventFlow().listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}); - } - - public TicTacToeController(Player[] players) { - this(players, true); - } - - @Override - public void updateUI() { - canvas.clearAll(); - // TODO: wtf is even this pile of poop temp fix - primary.nextPlayer(true, getCurrentPlayer().getName(), game.getCurrentTurn() == 0 ? "X" : "O", getPlayer((game.getCurrentTurn() + 1) % 2).getName(), 'T'); - drawMoves(); - } - - private void initUI(){ - primary.add(Pos.CENTER, canvas.getCanvas()); - WidgetContainer.getCurrentView().transitionNext(primary, true); - updateUI(); - } - - private void drawMoves(){ - int[] board = game.getBoard(); - - // Draw each square - for (int i = 0; i < board.length; i++){ - // If square isn't empty, draw player move - if (board[i] != AbstractGame.EMPTY){ - canvas.drawPlayerMove(board[i], i); - } - } - } -} diff --git a/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java b/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java index 6fd4ba9..cd4d87c 100644 --- a/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java +++ b/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java @@ -8,6 +8,7 @@ import org.toop.app.widget.complex.PopupWidget; import java.util.function.Consumer; import javafx.geometry.Pos; +import org.toop.local.AppContext; public final class ChallengePopup extends PopupWidget { private final GameInformation.Player playerInformation; 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 8ff8ae0..9b9bed2 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,19 +2,24 @@ package org.toop.app.widget.view; import javafx.application.Platform; import org.toop.app.GameInformation; -import org.toop.app.gameControllers.AbstractGameController; -import org.toop.app.gameControllers.ReversiController; -import org.toop.app.gameControllers.TicTacToeController; +import org.toop.app.canvas.ReversiBitCanvas; +import org.toop.app.canvas.TicTacToeBitCanvas; +import org.toop.app.gameControllers.GenericGameController; +import org.toop.app.gameControllers.ReversiBitController; +import org.toop.app.gameControllers.TicTacToeBitController; +import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.model.player.Player; -import org.toop.game.games.tictactoe.TicTacToeAIRSleep; +import org.toop.game.games.reversi.BitboardReversi; +import org.toop.game.games.tictactoe.BitboardTicTacToe; import org.toop.game.players.ArtificialPlayer; import org.toop.game.players.LocalPlayer; 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.games.reversi.ReversiAIR; import org.toop.app.widget.tutorial.*; +import org.toop.game.players.MiniMaxAI; +import org.toop.game.players.RandomAI; import org.toop.local.AppContext; import javafx.geometry.Pos; @@ -22,10 +27,13 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; import org.toop.local.AppSettings; +import java.util.Arrays; +import java.util.Random; + public class LocalMultiplayerView extends ViewWidget { private final GameInformation information; - private AbstractGameController gameController; + private GameController gameController; public LocalMultiplayerView(GameInformation.Type type) { this(new GameInformation(type)); @@ -44,7 +52,7 @@ public class LocalMultiplayerView extends ViewWidget { } } - // TODO: Fix this temporary ass way of setting the players (Only works for TicTacToe) + // TODO: Fix this temporary ass way of setting the players Player[] players = new Player[2]; switch (information.type) { @@ -52,27 +60,27 @@ public class LocalMultiplayerView extends ViewWidget { if (information.players[0].isHuman) { players[0] = new LocalPlayer<>(information.players[0].name); } else { - players[0] = new ArtificialPlayer<>(new TicTacToeAIRSleep(information.players[0].computerDifficulty, information.players[1].computerThinkTime), information.players[0].name); + players[0] = new ArtificialPlayer<>(new RandomAI(), "Random AI"); } if (information.players[1].isHuman) { players[1] = new LocalPlayer<>(information.players[1].name); } else { - players[1] = new ArtificialPlayer<>(new TicTacToeAIRSleep(information.players[1].computerDifficulty, information.players[1].computerThinkTime), information.players[1].name); + players[1] = new ArtificialPlayer<>(new MiniMaxAI(9), "MiniMax AI"); } if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstTTT()) { new ShowEnableTutorialWidget( () -> new TicTacToeTutorialWidget(() -> { - gameController = new TicTacToeController(players); + gameController = new TicTacToeBitController(players); gameController.start(); }), () -> Platform.runLater(() -> { - gameController = new TicTacToeController(players); + gameController = new TicTacToeBitController(players); gameController.start(); }), () -> AppSettings.getSettings().setFirstTTT(false) ); } else { - gameController = new TicTacToeController(players); + gameController = new TicTacToeBitController(players); gameController.start(); } break; @@ -80,27 +88,27 @@ public class LocalMultiplayerView extends ViewWidget { if (information.players[0].isHuman) { players[0] = new LocalPlayer<>(information.players[0].name); } else { - players[0] = new ArtificialPlayer<>(new ReversiAIR(), information.players[0].name); + players[0] = new ArtificialPlayer<>(new RandomAI(), "Random AI"); } if (information.players[1].isHuman) { players[1] = new LocalPlayer<>(information.players[1].name); } else { - players[1] = new ArtificialPlayer<>(new ReversiAIR(), information.players[1].name); + players[1] = new ArtificialPlayer<>(new MiniMaxAI(6), "MiniMax"); } if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) { new ShowEnableTutorialWidget( () -> new ReversiTutorialWidget(() -> { - gameController = new ReversiController(players); + gameController = new ReversiBitController(players); gameController.start(); }), () -> Platform.runLater(() -> { - gameController = new ReversiController(players); + gameController = new ReversiBitController(players); gameController.start(); }), () -> AppSettings.getSettings().setFirstReversi(false) ); } else { - gameController = new ReversiController(players); + gameController = new ReversiBitController(players); gameController.start(); } break; diff --git a/framework/src/main/java/org/toop/framework/gameFramework/LongPairConsumer.java b/framework/src/main/java/org/toop/framework/gameFramework/LongPairConsumer.java new file mode 100644 index 0000000..c1671d5 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/LongPairConsumer.java @@ -0,0 +1,6 @@ +package org.toop.framework.gameFramework; + +@FunctionalInterface +public interface LongPairConsumer { + void accept(long a, long b); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/controller/GameController.java b/framework/src/main/java/org/toop/framework/gameFramework/controller/GameController.java new file mode 100644 index 0000000..f355dc5 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/controller/GameController.java @@ -0,0 +1,18 @@ +package org.toop.framework.gameFramework.controller; + +import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; +import org.toop.framework.gameFramework.model.game.threadBehaviour.Controllable; +import org.toop.framework.networking.events.NetworkEvents; + +public interface GameController extends Controllable, UpdatesGameUI { + /** Called when it is this player's turn to make a move. */ + void onYourTurn(NetworkEvents.YourTurnResponse event); + + /** Called when a move from another player is received. */ + void onMoveReceived(NetworkEvents.GameMoveResponse event); + + /** Called when the game has finished, with the final result. */ + void gameFinished(NetworkEvents.GameResultResponse event); + + void sendMove(long clientId, long move); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/AbstractGame.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/AbstractGame.java deleted file mode 100644 index ec0dae2..0000000 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/AbstractGame.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.toop.framework.gameFramework.model.game; - -import org.toop.framework.gameFramework.model.player.Player; - -import java.util.Arrays; - -public abstract class AbstractGame> implements TurnBasedGame { - private final int playerCount; // How many players are playing - private final Player[] players; - private int turn = 0; // What turn it is in the game - - /** 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; - - /** Number of columns in the game board. */ - private final int columnSize; - - /** The game board stored as a one-dimensional array. */ - private final int[] board; - - - - protected AbstractGame(int rowSize, int columnSize, int playerCount, Player[] players) { - assert rowSize > 0 && columnSize > 0; - - this.rowSize = rowSize; - this.columnSize = columnSize; - - this.players = players; - - board = new int[rowSize * columnSize]; - Arrays.fill(board, EMPTY); - - this.playerCount = playerCount; - } - - protected AbstractGame(AbstractGame other){ - this.rowSize = other.rowSize; - this.columnSize = other.columnSize; - this.board = other.board.clone(); - this.playerCount = other.playerCount; - this.turn = other.turn; - // TODO: Make this a deep copy, add deep copy interface to Player - this.players = other.players; - - } - - public static boolean contains(int[] array, int value) { - // O(n) - for (int element : array){ - if (element == value) return true; - } - return false; - } - - public Player getPlayer(int index) { - return players[index]; - } - - public int getPlayerCount(){return this.playerCount;} - - protected void nextTurn() { - turn += 1; - } - - public int getCurrentTurn() { - return turn % playerCount; - } - - protected void setBoard(int position) { - setBoard(position, getCurrentTurn()); - } - - protected void setBoard(int position, int player) { - this.board[position] = player; - } - - /** - * Returns the number of rows in the board. - * - * @return number of rows - */ - public int getRowSize() { - return this.rowSize; - } - - /** - * Returns the number of columns in the board. - * - * @return number of columns - */ - public int getColumnSize() { - return this.columnSize; - } - - /** - * Returns a copy of the current board state. - * - * @return a cloned array representing the board - */ - public int[] getBoard() { - return this.board.clone(); - } - -} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java new file mode 100644 index 0000000..fb70cf8 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java @@ -0,0 +1,5 @@ +package org.toop.framework.gameFramework.model.game; + +public interface BoardProvider { + long[] getBoard(); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/DeepCopyable.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/DeepCopyable.java index cf3bfe4..51cb1e2 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/DeepCopyable.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/DeepCopyable.java @@ -1,5 +1,5 @@ package org.toop.framework.gameFramework.model.game; -public interface DeepCopyable> { +public interface DeepCopyable { T deepCopy(); } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java index f8013c5..21956b0 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java @@ -12,7 +12,7 @@ public interface Playable { * * @return an array of integers representing legal moves */ - int[] getLegalMoves(); + long getLegalMoves(); /** * Plays the given move and returns the resulting game state. @@ -20,5 +20,5 @@ public interface Playable { * @param move the move to apply * @return the {@link GameState} and additional info after the move */ - PlayResult play(int move); + PlayResult play(long move); } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java index d610296..1cc1641 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java @@ -10,11 +10,11 @@ import org.toop.framework.networking.events.NetworkEvents; public interface SupportsOnlinePlay { /** Called when it is this player's turn to make a move. */ - void onYourTurn(NetworkEvents.YourTurnResponse event); + void onYourTurn(long clientId); /** Called when a move from another player is received. */ - void onMoveReceived(NetworkEvents.GameMoveResponse event); + void onMoveReceived(long move); /** Called when the game has finished, with the final result. */ - void gameFinished(NetworkEvents.GameResultResponse event); + void gameFinished(String condition); } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java index 508637a..d4cb4df 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java @@ -1,5 +1,7 @@ package org.toop.framework.gameFramework.model.game; -public interface TurnBasedGame> extends Playable, DeepCopyable, PlayerProvider { +public interface TurnBasedGame> extends Playable, DeepCopyable, PlayerProvider, BoardProvider { int getCurrentTurn(); + int getPlayerCount(); + int getWinner(); } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/AbstractThreadBehaviour.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/AbstractThreadBehaviour.java index 25942a2..da6647a 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/AbstractThreadBehaviour.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/AbstractThreadBehaviour.java @@ -2,9 +2,12 @@ package org.toop.framework.gameFramework.model.game.threadBehaviour; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.toop.framework.gameFramework.LongPairConsumer; +import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; /** * Base class for thread-based game behaviours. @@ -13,8 +16,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * a running flag, a game reference, and a logger. * Subclasses implement the actual game-loop logic. */ -public abstract class AbstractThreadBehaviour> implements ThreadBehaviour { - +public abstract class AbstractThreadBehaviour> implements ThreadBehaviour { + private LongPairConsumer onSendMove; + private Runnable onUpdateUI; /** Indicates whether the game loop or event processing is active. */ protected final AtomicBoolean isRunning = new AtomicBoolean(); @@ -32,4 +36,26 @@ public abstract class AbstractThreadBehaviour> implem public AbstractThreadBehaviour(T game) { this.game = game; } + + protected void updateUI(){ + if (onUpdateUI != null) { + onUpdateUI.run(); + } + } + + protected void sendMove(long clientId, long move){ + if (onSendMove != null) { + onSendMove.accept(clientId, move); + } + } + + @Override + public void setOnUpdateUI(Runnable onUpdateUI) { + this.onUpdateUI = onUpdateUI; + } + + @Override + public void setOnSendMove(LongPairConsumer onSendMove) { + this.onSendMove = onSendMove; + } } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/ThreadBehaviour.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/ThreadBehaviour.java index 0027308..7b1bc35 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/ThreadBehaviour.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/ThreadBehaviour.java @@ -1,11 +1,18 @@ package org.toop.framework.gameFramework.model.game.threadBehaviour; +import org.toop.framework.gameFramework.LongPairConsumer; +import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import java.util.function.Consumer; + /** * Strategy interface for controlling game thread behavior. *

* Defines how a game's execution is started, stopped, and which player is active. */ -public interface ThreadBehaviour> extends Controllable { +public interface ThreadBehaviour extends Controllable { + void setOnUpdateUI(Runnable onUpdateUI); + void setOnSendMove(LongPairConsumer onSendMove); } + diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AI.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AI.java new file mode 100644 index 0000000..8377166 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AI.java @@ -0,0 +1,7 @@ +package org.toop.framework.gameFramework.model.player; + +import org.toop.framework.gameFramework.model.game.DeepCopyable; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; + +public interface AI> extends MoveProvider, DeepCopyable> { +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractAI.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractAI.java index ebeab4c..328132d 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractAI.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractAI.java @@ -12,6 +12,6 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame; * * @param the specific type of game this AI can play, extending {@link GameR} */ -public abstract class AbstractAI implements MoveProvider { +public abstract class AbstractAI> implements AI { // Concrete AI implementations should override findBestMove(T game, int depth) } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java index 077dee3..57e2f18 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java @@ -16,15 +16,17 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame; *

*/ public abstract class AbstractPlayer> implements Player { - private int playerIndex = -1; - - private Logger logger = LogManager.getLogger(this.getClass()); + private final Logger logger = LogManager.getLogger(this.getClass()); private final String name; protected AbstractPlayer(String name) { this.name = name; } + + protected AbstractPlayer(AbstractPlayer other) { + this.name = other.name; + } /** * Determines the next move based on the provided game state. *

@@ -37,7 +39,7 @@ public abstract class AbstractPlayer> implements Play * @return an integer representing the chosen move * @throws UnsupportedOperationException if the method is not overridden */ - public int getMove(T gameCopy) { + public long getMove(T gameCopy) { logger.error("Method getMove not implemented."); throw new UnsupportedOperationException("Not supported yet."); } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java index 29529e2..b9ab448 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java @@ -2,6 +2,6 @@ package org.toop.framework.gameFramework.model.player; import org.toop.framework.gameFramework.model.game.TurnBasedGame; -public interface MoveProvider { - int getMove(T game); +public interface MoveProvider> { + long getMove(T game); } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/Player.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/Player.java index bf49cd7..e2ff1a8 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/player/Player.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/Player.java @@ -1,6 +1,7 @@ package org.toop.framework.gameFramework.model.player; +import org.toop.framework.gameFramework.model.game.DeepCopyable; import org.toop.framework.gameFramework.model.game.TurnBasedGame; -public interface Player> extends NameProvider, MoveProvider { +public interface Player> extends NameProvider, MoveProvider, DeepCopyable> { } diff --git a/framework/src/main/java/org/toop/framework/gameFramework/view/GUIEvents.java b/framework/src/main/java/org/toop/framework/gameFramework/view/GUIEvents.java index a337869..334108c 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/view/GUIEvents.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/view/GUIEvents.java @@ -10,10 +10,6 @@ import org.toop.framework.eventbus.events.GenericEvent; * player actions, and game completion. */ public class GUIEvents extends EventsBase { - - /** Event to refresh or redraw the game canvas. */ - public record RefreshGameCanvas() implements GenericEvent {} - /** * Event indicating the game has ended. * @@ -23,8 +19,8 @@ public class GUIEvents extends EventsBase { public record GameEnded(boolean winOrTie, int winner) implements GenericEvent {} /** Event indicating a player has attempted a move. */ - public record PlayerAttemptedMove(int move) implements GenericEvent {} + public record PlayerAttemptedMove(long move) implements GenericEvent {} /** Event indicating a player is hovering over a move (for UI feedback). */ - public record PlayerMoveHovered(int move) implements GenericEvent {} + public record PlayerMoveHovered(long move) implements GenericEvent {} } diff --git a/game/src/main/java/org/toop/game/BitboardGame.java b/game/src/main/java/org/toop/game/BitboardGame.java new file mode 100644 index 0000000..4ebdb95 --- /dev/null +++ b/game/src/main/java/org/toop/game/BitboardGame.java @@ -0,0 +1,86 @@ +package org.toop.game; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.Player; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +// There is AI performance to be gained by getting rid of non-primitives and thus speeding up deepCopy +public abstract class BitboardGame> implements TurnBasedGame { + private final int columnSize; + private final int rowSize; + + private Player[] players; + + // long is 64 bits. Every game has a limit of 64 cells maximum. + private final long[] playerBitboard; + private int currentTurn = 0; + + public BitboardGame(int columnSize, int rowSize, int playerCount, Player[] players) { + this.columnSize = columnSize; + this.rowSize = rowSize; + this.players = players; + this.playerBitboard = new long[playerCount]; + + Arrays.fill(playerBitboard, 0L); + } + + public BitboardGame(BitboardGame other) { + this.columnSize = other.columnSize; + this.rowSize = other.rowSize; + + this.playerBitboard = other.playerBitboard.clone(); + this.currentTurn = other.currentTurn; + this.players = Arrays.stream(other.players) + .map(Player::deepCopy) + .toArray(Player[]::new); + } + + public int getColumnSize() { + return this.columnSize; + } + + public int getRowSize() { + return this.rowSize; + } + + public long getPlayerBitboard(int player) { + return this.playerBitboard[player]; + } + + public void setPlayerBitboard(int player, long bitboard) { + this.playerBitboard[player] = bitboard; + } + + public int getPlayerCount() { + return playerBitboard.length; + } + + public int getCurrentTurn() { + return getCurrentPlayerIndex(); + } + + public Player getPlayer(int index) {return players[index];} + + public int getCurrentPlayerIndex() { + return currentTurn % playerBitboard.length; + } + + public int getNextPlayer() { + return (currentTurn + 1) % playerBitboard.length; + } + + public Player getCurrentPlayer(){ + return players[getCurrentPlayerIndex()]; + } + + + + @Override + public long[] getBoard() {return this.playerBitboard;} + + public void nextTurn() { + currentTurn++; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java index dc13519..0a9da7f 100644 --- a/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java +++ b/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java @@ -8,6 +8,8 @@ import org.toop.framework.gameFramework.view.GUIEvents; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.Player; +import java.util.function.Consumer; + /** * Handles local turn-based game logic at a fixed update rate. *

@@ -16,18 +18,14 @@ import org.toop.framework.gameFramework.model.player.Player; */ public class LocalFixedRateThreadBehaviour> extends AbstractThreadBehaviour implements Runnable { - /** All players participating in the game. */ - private final Player[] players; /** * Creates a fixed-rate behaviour for a local turn-based game. * * @param game the game instance - * @param players the list of players in turn order */ - public LocalFixedRateThreadBehaviour(T game, Player[] players) { + public LocalFixedRateThreadBehaviour(T game) { super(game); - this.players = players; } /** Starts the game loop thread if not already running. */ @@ -52,7 +50,7 @@ public class LocalFixedRateThreadBehaviour> extends A */ @Override public void run() { - final int UPS = 60; + final int UPS = 1; final long UPDATE_INTERVAL = 1_000_000_000L / UPS; long nextUpdate = System.nanoTime(); @@ -62,9 +60,10 @@ public class LocalFixedRateThreadBehaviour> extends A nextUpdate += UPDATE_INTERVAL; Player currentPlayer = game.getPlayer(game.getCurrentTurn()); - int move = currentPlayer.getMove(game.deepCopy()); + long move = currentPlayer.getMove(game.deepCopy()); PlayResult result = game.play(move); - new EventFlow().addPostEvent(GUIEvents.RefreshGameCanvas.class).postEvent(); + + updateUI(); GameState state = result.state(); switch (state) { diff --git a/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java index 82ac0ca..79c57f9 100644 --- a/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java +++ b/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java @@ -8,6 +8,8 @@ import org.toop.framework.gameFramework.GameState; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.Player; +import java.util.function.Consumer; + /** * Handles local turn-based game logic in its own thread. *

@@ -20,9 +22,8 @@ public class LocalThreadBehaviour> extends AbstractTh * Creates a new behaviour for a local turn-based game. * * @param game the game instance - * @param players the list of players in turn order */ - public LocalThreadBehaviour(T game, Player[] players) { + public LocalThreadBehaviour(T game) { super(game); } @@ -48,9 +49,10 @@ public class LocalThreadBehaviour> extends AbstractTh public void run() { while (isRunning.get()) { Player currentPlayer = game.getPlayer(game.getCurrentTurn()); - int move = currentPlayer.getMove(game.deepCopy()); + long move = currentPlayer.getMove(game.deepCopy()); PlayResult result = game.play(move); - new EventFlow().addPostEvent(GUIEvents.RefreshGameCanvas.class).postEvent(); + + updateUI(); GameState state = result.state(); switch (state) { diff --git a/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java index fcce936..ae9aa88 100644 --- a/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java +++ b/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java @@ -3,9 +3,7 @@ package org.toop.game.gameThreads; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour; import org.toop.framework.gameFramework.view.GUIEvents; -import org.toop.framework.gameFramework.model.game.AbstractGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame; -import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; import org.toop.framework.gameFramework.model.player.Player; import org.toop.game.players.OnlinePlayer; @@ -17,19 +15,12 @@ import org.toop.game.players.OnlinePlayer; * for the local player while receiving moves from other players. */ public class OnlineThreadBehaviour> extends AbstractThreadBehaviour implements SupportsOnlinePlay { - - /** The local player controlled by this client. */ - private final Player mainPlayer; - private final int playerTurn; - /** * Creates behaviour and sets the first local player * (non-online player) from the given array. */ - public OnlineThreadBehaviour(T game, Player[] players) { + public OnlineThreadBehaviour(T game) { super(game); - this.playerTurn = getFirstNotOnlinePlayer(players); - this.mainPlayer = players[this.playerTurn]; } /** Finds the first non-online player in the array. */ @@ -59,33 +50,31 @@ public class OnlineThreadBehaviour> extends AbstractT * Sends the generated move back to the server. */ @Override - public void onYourTurn(NetworkEvents.YourTurnResponse event) { + public void onYourTurn(long clientId) { if (!isRunning.get()) return; - int move = mainPlayer.getMove(game.deepCopy()); - new EventFlow().addPostEvent(NetworkEvents.SendMove.class, event.clientId(), (short) move).postEvent(); + long move = game.getPlayer(game.getCurrentTurn()).getMove(game.deepCopy()); + sendMove(clientId, move); } /** * Handles a move received from the server for any player. * Updates the game state and triggers a UI refresh. */ - @Override - public void onMoveReceived(NetworkEvents.GameMoveResponse event) { + public void onMoveReceived(long move) { if (!isRunning.get()) return; - game.play(Integer.parseInt(event.move())); - new EventFlow().addPostEvent(GUIEvents.RefreshGameCanvas.class).postEvent(); + game.play(move); + + updateUI(); } /** * Handles the end of the game as notified by the server. * Updates the UI to show a win or draw result for the local player. */ - @Override - public void gameFinished(NetworkEvents.GameResultResponse event) { - switch(event.condition().toUpperCase()){ - case "WIN" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, true, playerTurn).postEvent(); - case "DRAW" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, false, AbstractGame.EMPTY).postEvent(); - case "LOSS" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, true, (playerTurn + 1)%2).postEvent(); + public void gameFinished(String condition) { + switch(condition.toUpperCase()){ + case "WIN", "LOSS" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, true, game.getWinner()).postEvent(); + case "DRAW" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, false, -1).postEvent(); default -> { logger.error("Invalid condition"); throw new RuntimeException("Unknown condition"); diff --git a/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java index f4e2627..a666f8d 100644 --- a/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java +++ b/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java @@ -1,8 +1,7 @@ package org.toop.game.gameThreads; -import org.toop.framework.gameFramework.model.game.AbstractGame; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.networking.events.NetworkEvents; -import org.toop.framework.gameFramework.model.player.AbstractPlayer; /** * Online thread behaviour that adds a fixed delay before processing @@ -11,16 +10,15 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer; * This is identical to {@link OnlineThreadBehaviour}, but inserts a * short sleep before delegating to the base implementation. */ -public class OnlineWithSleepThreadBehaviour extends OnlineThreadBehaviour { +public class OnlineWithSleepThreadBehaviour> extends OnlineThreadBehaviour { /** * Creates the behaviour and forwards the players to the base class. * - * @param game the online-capable turn-based game - * @param players the list of local and remote players + * @param game the online-capable turn-based game */ - public OnlineWithSleepThreadBehaviour(AbstractGame game, AbstractPlayer[] players) { - super(game, players); + public OnlineWithSleepThreadBehaviour(T game) { + super(game); } /** @@ -29,14 +27,14 @@ public class OnlineWithSleepThreadBehaviour extends OnlineThreadBehaviour { * @param event the network event indicating it's this client's turn */ @Override - public void onYourTurn(NetworkEvents.YourTurnResponse event) { + public void onYourTurn(long clientId) { try { - Thread.sleep(1000); + Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } - super.onYourTurn(event); + super.onYourTurn(clientId); } } diff --git a/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java new file mode 100644 index 0000000..f380bef --- /dev/null +++ b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java @@ -0,0 +1,170 @@ +package org.toop.game.games.reversi; + +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.BitboardGame; + +public class BitboardReversi extends BitboardGame { + + public record Score(int black, int white) {} + + private final long notAFile = 0xfefefefefefefefeL; + private final long notHFile = 0x7f7f7f7f7f7f7f7fL; + + public BitboardReversi(Player[] players) { + super(8, 8, 2, players); + + // Black (player 0) + setPlayerBitboard(0, (1L << (3 + 4 * 8)) | (1L << (4 + 3 * 8))); + + // White (player 1) + setPlayerBitboard(1, (1L << (3 + 3 * 8)) | (1L << (4 + 4 * 8))); + } + + public BitboardReversi(BitboardReversi other) { + super(other); + } + + public long getLegalMoves() { + final long player = getPlayerBitboard(getCurrentPlayerIndex()); + final long opponent = getPlayerBitboard(getNextPlayer()); + + long legalMoves = 0L; + + // north & south + legalMoves |= computeMoves(player, opponent, 8, -1L); + legalMoves |= computeMoves(player, opponent, -8, -1L); + + // east & west + legalMoves |= computeMoves(player, opponent, 1, notAFile); + legalMoves |= computeMoves(player, opponent, -1, notHFile); + + // north-east & north-west & south-east & south-west + legalMoves |= computeMoves(player, opponent, 9, notAFile); + legalMoves |= computeMoves(player, opponent, 7, notHFile); + legalMoves |= computeMoves(player, opponent, -7, notAFile); + legalMoves |= computeMoves(player, opponent, -9, notHFile); + + return legalMoves; + } + + public long getFlips(long move) { + final long player = getPlayerBitboard(getCurrentPlayerIndex()); + final long opponent = getPlayerBitboard(getNextPlayer()); + + long flips = 0L; + + // north & south + flips |= computeFlips(move, player, opponent, 8, -1L); + flips |= computeFlips(move, player, opponent, -8, -1L); + + // east & west + flips |= computeFlips(move, player, opponent, 1, notAFile); + flips |= computeFlips(move, player, opponent, -1, notHFile); + + // north-east & north-west & south-east & south-west + flips |= computeFlips(move, player, opponent, 9, notAFile); + flips |= computeFlips(move, player, opponent, 7, notHFile); + flips |= computeFlips(move, player, opponent, -7, notAFile); + flips |= computeFlips(move, player, opponent, -9, notHFile); + + return flips; + } + + @Override + public BitboardReversi deepCopy() {return new BitboardReversi(this);} + + public PlayResult play(long move) { + final long flips = getFlips(move); + + long player = getPlayerBitboard(getCurrentPlayerIndex()); + long opponent = getPlayerBitboard(getNextPlayer()); + + player |= move | flips; + opponent &= ~flips; + + setPlayerBitboard(getCurrentPlayerIndex(), player); + setPlayerBitboard(getNextPlayer(), opponent); + + nextTurn(); + + final long nextLegalMoves = getLegalMoves(); + + if (nextLegalMoves == 0) { + nextTurn(); + + final long skippedLegalMoves = getLegalMoves(); + + if (skippedLegalMoves == 0) { + int winner = getWinner(); + + if (winner == -1) { + return new PlayResult(GameState.DRAW, -1); + } + + return new PlayResult(GameState.WIN, winner); + } + + return new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex()); + } + + return new PlayResult(GameState.NORMAL, getCurrentPlayerIndex()); + } + + public Score getScore() { + return new Score( + Long.bitCount(getPlayerBitboard(0)), + Long.bitCount(getPlayerBitboard(1)) + ); + } + + public int getWinner(){ + final long black = getPlayerBitboard(0); + final long white = getPlayerBitboard(1); + + final int blackCount = Long.bitCount(black); + final int whiteCount = Long.bitCount(white); + + if (blackCount == whiteCount){ + return -1; + } + else if (blackCount > whiteCount){ + return 0; + } + else{ + return 1; + } + } + + private long computeMoves(long player, long opponent, int shift, long mask) { + long moves = shift(player, shift, mask) & opponent; + long captured = moves; + + while (moves != 0) { + moves = shift(moves, shift, mask) & opponent; + captured |= moves; + } + + long landing = shift(captured, shift, mask); + return landing & ~(player | opponent); + } + + private long computeFlips(long move, long player, long opponent, int shift, long mask) { + long flips = 0L; + long pos = move; + + while (true) { + pos = shift(pos, shift, mask); + if (pos == 0) return 0L; + + if ((pos & opponent) != 0) flips |= pos; + else if ((pos & player) != 0) return flips; + else return 0L; + } + } + + private long shift(long bit, int shift, long mask) { + return shift > 0 ? (bit << shift) & mask : (bit >>> -shift) & mask; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/games/reversi/ReversiAIR.java b/game/src/main/java/org/toop/game/games/reversi/ReversiAIR.java deleted file mode 100644 index 4a038ab..0000000 --- a/game/src/main/java/org/toop/game/games/reversi/ReversiAIR.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.toop.game.games.reversi; - -import org.toop.framework.gameFramework.model.player.AbstractAI; - -import java.util.Random; - -public final class ReversiAIR extends AbstractAI { - public int getMove(ReversiR game) { - int[] moves = game.getLegalMoves(); - if (moves.length == 0) return -1; - - int inty = new Random().nextInt(0, moves.length); - return moves[inty]; - } -} diff --git a/game/src/main/java/org/toop/game/games/reversi/ReversiR.java b/game/src/main/java/org/toop/game/games/reversi/ReversiR.java deleted file mode 100644 index bf946db..0000000 --- a/game/src/main/java/org/toop/game/games/reversi/ReversiR.java +++ /dev/null @@ -1,260 +0,0 @@ -package org.toop.game.games.reversi; - -import org.toop.framework.gameFramework.GameState; -import org.toop.framework.gameFramework.model.game.PlayResult; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.framework.gameFramework.model.player.Player; - -import java.awt.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - - -public final class ReversiR extends AbstractGame { - private int movesTaken; - private Set filledCells = new HashSet<>(); - private int[] mostRecentlyFlippedPieces; - - @Override - public ReversiR deepCopy() { - return new ReversiR(this); - } - - // TODO: Don't hardcore for two players :) - public record Score(int player1Score, int player2Score) {} - - public ReversiR(Player[] players) { - super(8, 8, 2, players); - addStartPieces(); - } - - public ReversiR(ReversiR other) { - super(other); - this.movesTaken = other.movesTaken; - this.filledCells = other.filledCells; - this.mostRecentlyFlippedPieces = other.mostRecentlyFlippedPieces; - } - - - private void addStartPieces() { - this.setBoard(27, 0); - this.setBoard(28, 1); - this.setBoard(35, 1); - this.setBoard(36, 0); - updateFilledCellsSet(); - } - private void updateFilledCellsSet() { - for (int i = 0; i < 64; i++) { - if (this.getBoard()[i] != EMPTY) { - filledCells.add(new Point(i % this.getColumnSize(), i / this.getRowSize())); - } - } - } - - @Override - public int[] getLegalMoves() { - final ArrayList legalMoves = new ArrayList<>(); - int[][] boardGrid = makeBoardAGrid(); - int currentPlayer = this.getCurrentTurn(); - Set adjCell = getAdjacentCells(boardGrid); - for (Point point : adjCell){ - int[] moves = getFlipsForPotentialMove(point,currentPlayer); - int score = moves.length; - if (score > 0){ - legalMoves.add(point.x + point.y * this.getRowSize()); - } - } - return legalMoves.stream().mapToInt(Integer::intValue).toArray(); - } - - private Set getAdjacentCells(int[][] boardGrid) { - Set possibleCells = new HashSet<>(); - for (Point point : filledCells) { //for every filled cell - for (int deltaColumn = -1; deltaColumn <= 1; deltaColumn++){ //check adjacent cells - for (int deltaRow = -1; deltaRow <= 1; deltaRow++){ //orthogonally and diagonally - int newX = point.x + deltaColumn, newY = point.y + deltaRow; - if (deltaColumn == 0 && deltaRow == 0 //continue if out of bounds - || !isOnBoard(newX, newY)) { - continue; - } - if (boardGrid[newY][newX] == EMPTY) { //check if the cell is empty - possibleCells.add(new Point(newX, newY)); //and then add it to the set of possible moves - } - } - } - } - return possibleCells; - } - - public int[] getFlipsForPotentialMove(Point point, int currentPlayer) { - final ArrayList movesToFlip = new ArrayList<>(); - for (int deltaColumn = -1; deltaColumn <= 1; deltaColumn++) { //for all directions - for (int deltaRow = -1; deltaRow <= 1; deltaRow++) { - if (deltaColumn == 0 && deltaRow == 0){ - continue; - } - int[] moves = getFlipsInDirection(point,makeBoardAGrid(),currentPlayer,deltaColumn,deltaRow); - if (moves != null) { //getFlipsInDirection - Arrays.stream(moves).forEach(movesToFlip::add); - } - } - } - return movesToFlip.stream().mapToInt(Integer::intValue).toArray(); - } - - private int[] getFlipsInDirection(Point point, int[][] boardGrid, int currentPlayer, int dirX, int dirY) { - int opponent = getOpponent(currentPlayer); - final ArrayList movesToFlip = new ArrayList<>(); - int x = point.x + dirX; - int y = point.y + dirY; - - if (!isOnBoard(x, y) || boardGrid[y][x] != opponent) { //there must first be an opponents tile - return null; - } - - while (isOnBoard(x, y) && boardGrid[y][x] == opponent) { //count the opponents tiles in this direction - - movesToFlip.add(x+y*this.getRowSize()); - x += dirX; - y += dirY; - } - if (isOnBoard(x, y) && boardGrid[y][x] == currentPlayer) { - return movesToFlip.stream().mapToInt(Integer::intValue).toArray(); //only return the count if last tile is ours - } - return null; - } - - private boolean isOnBoard(int x, int y) { - return x >= 0 && x < this.getColumnSize() && y >= 0 && y < this.getRowSize(); - } - - private int[][] makeBoardAGrid() { - int[][] boardGrid = new int[this.getRowSize()][this.getColumnSize()]; - for (int i = 0; i < 64; i++) { - boardGrid[i / this.getRowSize()][i % this.getColumnSize()] = this.getBoard()[i]; //boardGrid[y -> row] [x -> column] - } - return boardGrid; - } - - private boolean gameOver(){ - ReversiR gameCopy = deepCopy(); - return gameCopy.getLegalMoves().length == 0 && gameCopy.skipTurn().getLegalMoves().length == 0; - } - - @Override - public PlayResult play(int move) { - /*int[] legalMoves = getLegalMoves(); - boolean moveIsLegal = false; - for (int legalMove : legalMoves) { //check if the move is legal - if (move == legalMove) { - moveIsLegal = true; - break; - } - } - if (!moveIsLegal) { - return null; - } - - int[] moves = sortMovesFromCenter(Arrays.stream(getFlipsForPotentialMove(new Point(move%this.getColumnSize(),move/this.getRowSize()), getCurrentTurn())).boxed().toArray(Integer[]::new),move); - mostRecentlyFlippedPieces = moves; - this.setBoard(move); //place the move on the board - for (int m : moves) { - this.setBoard(m); //flip the correct pieces on the board - } - filledCells.add(new Point(move % this.getRowSize(), move / this.getColumnSize())); - nextTurn(); - if (getLegalMoves().length == 0) { //skip the players turn when there are no legal moves - skipMyTurn(); - if (getLegalMoves().length > 0) { - return new PlayResult(GameState.TURN_SKIPPED, getCurrentTurn()); - } - else { //end the game when neither player has a legal move - Score score = getScore(); - if (score.player1Score() == score.player2Score()) { - return new PlayResult(GameState.DRAW, EMPTY); - } - else { - return new PlayResult(GameState.WIN, getCurrentTurn()); - } - } - } - return new PlayResult(GameState.NORMAL, EMPTY);*/ - - // Check if move is legal - if (!contains(getLegalMoves(), move)){ - // Next person wins - return new PlayResult(GameState.WIN, (getCurrentTurn() + 1) % 2); - } - - // Move is legal, proceed as normal - int[] moves = sortMovesFromCenter(Arrays.stream(getFlipsForPotentialMove(new Point(move%this.getColumnSize(),move/this.getRowSize()), getCurrentTurn())).boxed().toArray(Integer[]::new),move); - mostRecentlyFlippedPieces = moves; - this.setBoard(move); //place the move on the board - for (int m : moves) { - this.setBoard(m); //flip the correct pieces on the board - } - filledCells.add(new Point(move % this.getRowSize(), move / this.getColumnSize())); - - nextTurn(); - - // Check for forced turn skip - if (getLegalMoves().length == 0){ - PlayResult result; - // Check if next turn is also a force skip - if (deepCopy().skipTurn().getLegalMoves().length == 0){ - // Game over - int winner = getWinner(); - result = new PlayResult(winner == EMPTY ? GameState.DRAW : GameState.WIN, winner); - }else{ - // Turn skipped - result = new PlayResult(GameState.TURN_SKIPPED, getCurrentTurn()); - skipTurn(); - } - return result; - } - return new PlayResult(GameState.NORMAL, EMPTY); - } - - private ReversiR skipTurn(){ - nextTurn(); - return this; - } - - private int getOpponent(int currentPlayer){ - return (currentPlayer + 1)%2; - } - - public int getWinner(){ - int player1Score = 0, player2Score = 0; - for (int count = 0; count < this.getRowSize() * this.getColumnSize(); count++) { - if (this.getBoard()[count] == 0) { - player1Score += 1; - } - if (this.getBoard()[count] == 1) { - player2Score += 1; - } - } - return player1Score == player2Score? -1 : player1Score > player2Score ? 0 : 1; - } - private int[] sortMovesFromCenter(Integer[] moves, int center) { //sorts the pieces to be flipped for animation purposes - int centerX = center%this.getColumnSize(); - int centerY = center/this.getRowSize(); - Arrays.sort(moves, (a, b) -> { - int dxA = a%this.getColumnSize() - centerX; - int dyA = a/this.getRowSize() - centerY; - int dxB = b%this.getColumnSize() - centerX; - int dyB = b/this.getRowSize() - centerY; - - int distA = dxA * dxA + dyA * dyA; - int distB = dxB * dxB + dyB * dyB; - - return Integer.compare(distA, distB); - }); - return Arrays.stream(moves).mapToInt(Integer::intValue).toArray(); - } - public int[] getMostRecentlyFlippedPieces() { - return mostRecentlyFlippedPieces; - } -} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java b/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java new file mode 100644 index 0000000..0927431 --- /dev/null +++ b/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java @@ -0,0 +1,103 @@ +package org.toop.game.games.tictactoe; + +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.BitboardGame; + +public class BitboardTicTacToe extends BitboardGame { + private final long[] winningLines = { + 0b111000000L, // top row + 0b000111000L, // middle row + 0b000000111L, // bottom row + 0b100100100L, // left column + 0b010010010L, // middle column + 0b001001001L, // right column + 0b100010001L, // diagonal + 0b001010100L // anti-diagonal + }; + + public BitboardTicTacToe(Player[] players) { + super(3, 3, 2, players); + } + public BitboardTicTacToe(BitboardTicTacToe other) { + super(other); + } + + public long getLegalMoves() { + final long xBitboard = getPlayerBitboard(0); + final long oBitboard = getPlayerBitboard(1); + + final long taken = (xBitboard | oBitboard); + return (~taken) & 0x1ffL; + } + + public int getWinner(){ + return getCurrentPlayerIndex(); + } + + public PlayResult play(long move) { + // Player loses if move is invalid + if ((move & getLegalMoves()) == 0 || Long.bitCount(move) != 1){ + return new PlayResult(GameState.WIN, getNextPlayer()); + } + + // Move is legal, make move + long playerBitboard = getPlayerBitboard(getCurrentPlayerIndex()); + playerBitboard |= move; + + setPlayerBitboard(getCurrentPlayerIndex(), playerBitboard); + + // Check if current player won + if (checkWin(playerBitboard)) { + return new PlayResult(GameState.WIN, getCurrentPlayerIndex()); + } + + // Proceed to next turn + nextTurn(); + + + // Check for early draw + if (getLegalMoves() == 0L || checkEarlyDraw()) { + return new PlayResult(GameState.DRAW, -1); + } + + // Nothing weird happened, continue on as normal + return new PlayResult(GameState.NORMAL, -1); + } + + private boolean checkWin(long board) { + for (final long line : winningLines) { + if ((board & line) == line) { + return true; + } + } + + return false; + } + + private boolean checkEarlyDraw() { + final long xBitboard = getPlayerBitboard(0); + final long oBitboard = getPlayerBitboard(1); + + final long taken = (xBitboard | oBitboard); + final long empty = (~taken) & 0x1FFL; + + for (final long line : winningLines) { + if (((line & xBitboard) != 0 && (line & oBitboard) != 0)) { + continue; + } + + if ((line & empty) != 0) { + return false; + } + } + + return true; + } + + @Override + public BitboardTicTacToe deepCopy() { + return new BitboardTicTacToe(this); + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIR.java b/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIR.java deleted file mode 100644 index dc0a285..0000000 --- a/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIR.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.toop.game.games.tictactoe; - -import org.toop.framework.gameFramework.model.player.AbstractAI; -import org.toop.framework.gameFramework.model.game.PlayResult; -import org.toop.framework.gameFramework.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 class TicTacToeAIR extends AbstractAI { - - /** - * 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 - */ - - private int depth; - - public TicTacToeAIR(int depth) { - this.depth = depth; - } - - public int getMove(TicTacToeR game) { - assert game != null; - final int[] legalMoves = game.getLegalMoves(); - - // If there are no moves, return -1 - if (legalMoves.length == 0) { - return -1; - } - - // If first move, pick a corner - 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; - - // Calculate Move score of each move, keep track what moves had the best score - 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.deepCopy(); - final PlayResult result = copy.play(move); - - GameState state = result.state(); - - 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/games/tictactoe/TicTacToeAIRSleep.java b/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIRSleep.java deleted file mode 100644 index bdd21cf..0000000 --- a/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeAIRSleep.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.toop.game.games.tictactoe; - -import java.util.Random; - -public class TicTacToeAIRSleep extends TicTacToeAIR { - - private int thinkTime; - - public TicTacToeAIRSleep(int depth, int thinkTime) { - super(depth); - this.thinkTime = thinkTime; - } - - @Override - public int getMove(TicTacToeR game) { - int score = super.getMove(game); - try { - Random random = new Random(); - Thread.sleep(this.thinkTime * 1000L + random.nextInt(1000)); - } catch (Exception e) { - e.printStackTrace(); - } - return score; - } -} diff --git a/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeR.java b/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeR.java deleted file mode 100644 index a04b5f9..0000000 --- a/game/src/main/java/org/toop/game/games/tictactoe/TicTacToeR.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.toop.game.games.tictactoe; - -import org.toop.framework.gameFramework.model.game.PlayResult; -import org.toop.framework.gameFramework.model.game.AbstractGame; -import org.toop.framework.gameFramework.GameState; -import org.toop.framework.gameFramework.model.player.Player; - -import java.util.ArrayList; -import java.util.Objects; - -public final class TicTacToeR extends AbstractGame { - private int movesLeft; - - public TicTacToeR(Player[] players) { - super(3, 3, 2, players); - movesLeft = this.getBoard().length; - } - - public TicTacToeR(TicTacToeR other) { - super(other); - movesLeft = other.movesLeft; - } - - @Override - public int[] getLegalMoves() { - final ArrayList legalMoves = new ArrayList(); - - for (int i = 0; i < this.getBoard().length; i++) { - if (Objects.equals(this.getBoard()[i], EMPTY)) { - legalMoves.add(i); - } - } - return legalMoves.stream().mapToInt(Integer::intValue).toArray(); - } - - @Override - public PlayResult play(int move) { - // NOT MY ASSERTIONS - Stef - assert move >= 0 && move < this.getBoard().length; - - // Player loses if move is invalid - if (!contains(getLegalMoves(), move)) { - // Next player wins - return new PlayResult(GameState.WIN, (getCurrentTurn() + 1)%2); // TODO: Make this a generic method like getNextPlayer() or something similar. - } - - // Move is valid, make move. - this.setBoard(move); - movesLeft--; - nextTurn(); - - // Check if current player won TODO: Make this generic? - // Not sure why I am checking for ANY win when only current player should be able to win. - int t = checkForWin(); - if (t != EMPTY) { - return new PlayResult(GameState.WIN, t); - } - - // Check for (early) draw - if (movesLeft <= 3) { - if (checkForEarlyDraw()) { - return new PlayResult(GameState.DRAW, EMPTY); - } - } - - // Nothing weird happened, continue on as normal - return new PlayResult(GameState.NORMAL, EMPTY); - } - - private int checkForWin() { - // Horizontal - for (int i = 0; i < 3; i++) { - - final int index = i * 3; - - if (!Objects.equals(this.getBoard()[index], EMPTY) - && Objects.equals(this.getBoard()[index], this.getBoard()[index + 1]) - && Objects.equals(this.getBoard()[index], this.getBoard()[index + 2])) { - return this.getBoard()[index]; - } - } - - // Vertical - for (int i = 0; i < 3; i++) { - if (!Objects.equals(this.getBoard()[i], EMPTY) && Objects.equals(this.getBoard()[i], this.getBoard()[i + 3]) && Objects.equals(this.getBoard()[i], this.getBoard()[i + 6])) { - return this.getBoard()[i]; - } - } - - // B-Slash - if (!Objects.equals(this.getBoard()[0], EMPTY) && Objects.equals(this.getBoard()[0], this.getBoard()[4]) && Objects.equals(this.getBoard()[0], this.getBoard()[8])) { - return this.getBoard()[0]; - } - - // F-Slash - if (!Objects.equals(this.getBoard()[2], EMPTY) && Objects.equals(this.getBoard()[2], this.getBoard()[4]) && Objects.equals(this.getBoard()[2], this.getBoard()[6])) - return this.getBoard()[2]; - - // Default return - return EMPTY; - } - - private boolean checkForEarlyDraw() { - for (final int move : this.getLegalMoves()) { - final TicTacToeR copy = this.deepCopy(); - - if (copy.play(move).state() == GameState.WIN || !copy.checkForEarlyDraw()) { - return false; - } - } - - return true; - } - - public TicTacToeR deepCopy() { - return new TicTacToeR(this); - } -} diff --git a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java index 49fff2d..418cbed 100644 --- a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java +++ b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java @@ -1,8 +1,6 @@ package org.toop.game.players; -import org.toop.framework.gameFramework.model.player.AbstractAI; -import org.toop.framework.gameFramework.model.player.AbstractPlayer; -import org.toop.framework.gameFramework.model.player.MoveProvider; +import org.toop.framework.gameFramework.model.player.*; import org.toop.framework.gameFramework.model.game.TurnBasedGame; /** @@ -17,18 +15,23 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame; public class ArtificialPlayer> extends AbstractPlayer { /** The AI instance used to calculate moves. */ - private final MoveProvider ai; + private final AI ai; /** * Constructs a new ArtificialPlayer using the specified AI. * * @param ai the AI instance that determines moves for this player */ - public ArtificialPlayer(MoveProvider ai, String name) { + public ArtificialPlayer(AI ai, String name) { super(name); this.ai = ai; } + public ArtificialPlayer(ArtificialPlayer other) { + super(other); + this.ai = other.ai.deepCopy(); + } + /** * Determines the next move for this player using its AI. *

@@ -41,7 +44,12 @@ public class ArtificialPlayer> extends AbstractPlayer * @return the integer representing the chosen move * @throws ClassCastException if {@code gameCopy} is not of type {@code T} */ - public int getMove(T gameCopy) { + public long getMove(T gameCopy) { return ai.getMove(gameCopy); } + + @Override + public ArtificialPlayer deepCopy() { + return new ArtificialPlayer(this); + } } diff --git a/game/src/main/java/org/toop/game/players/LocalPlayer.java b/game/src/main/java/org/toop/game/players/LocalPlayer.java index e83b3f4..8f3b94d 100644 --- a/game/src/main/java/org/toop/game/players/LocalPlayer.java +++ b/game/src/main/java/org/toop/game/players/LocalPlayer.java @@ -2,6 +2,7 @@ package org.toop.game.players; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.AbstractPlayer; +import org.toop.framework.gameFramework.model.player.Player; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -10,18 +11,22 @@ public class LocalPlayer> extends AbstractPlayer { // Future can be used with event system, IF unsubscribeAfterSuccess works... // private CompletableFuture LastMove = new CompletableFuture<>(); - private CompletableFuture LastMove; + private CompletableFuture LastMove; public LocalPlayer(String name) { super(name); } + public LocalPlayer(LocalPlayer other) { + super(other); + } + @Override - public int getMove(T gameCopy) { + public long getMove(T gameCopy) { return getValidMove(gameCopy); } - public void setMove(int move) { + public void setMove(long move) { LastMove.complete(move); } @@ -31,11 +36,12 @@ public class LocalPlayer> extends AbstractPlayer { return false; } - private int getMove2(T gameCopy) { + private long getMove2(T gameCopy) { LastMove = new CompletableFuture<>(); - int move = -1; + long move = 0; try { move = LastMove.get(); + System.out.println(Long.toBinaryString(move)); } catch (InterruptedException | ExecutionException e) { // TODO: Add proper logging. e.printStackTrace(); @@ -43,20 +49,25 @@ public class LocalPlayer> extends AbstractPlayer { return move; } - protected int getValidMove(T gameCopy){ + protected long getValidMove(T gameCopy){ // Get this player's valid moves - int[] validMoves = gameCopy.getLegalMoves(); + long validMoves = gameCopy.getLegalMoves(); // Make sure provided move is valid // TODO: Limit amount of retries? // TODO: Stop copying game so many times - int move = getMove2(gameCopy.deepCopy()); - while (!contains(validMoves, move)) { + long move = getMove2(gameCopy.deepCopy()); + while ((validMoves & move) == 0) { System.out.println("Not a valid move, try again"); move = getMove2(gameCopy.deepCopy()); } return move; } + @Override + public LocalPlayer deepCopy() { + return new LocalPlayer(this.getName()); + } + /*public void register() { // Listening to PlayerAttemptedMove new EventFlow().listen(GUIEvents.PlayerAttemptedMove.class, event -> { diff --git a/game/src/main/java/org/toop/game/players/MiniMaxAI.java b/game/src/main/java/org/toop/game/players/MiniMaxAI.java new file mode 100644 index 0000000..440bb50 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/MiniMaxAI.java @@ -0,0 +1,165 @@ +package org.toop.game.players; + +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.AbstractAI; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class MiniMaxAI> extends AbstractAI { + + private final int maxDepth; + private final Random random = new Random(); + + public MiniMaxAI(int depth) { + this.maxDepth = depth; + } + + public MiniMaxAI(MiniMaxAI other) { + this.maxDepth = other.maxDepth; + } + + @Override + public MiniMaxAI deepCopy() { + return new MiniMaxAI<>(this); + } + + @Override + public long getMove(T game) { + long legalMoves = game.getLegalMoves(); + if (legalMoves == 0) return 0; + + List bestMoves = new ArrayList<>(); + int bestScore = Integer.MIN_VALUE; + int aiPlayer = game.getCurrentTurn(); + + long movesLoop = legalMoves; + while (movesLoop != 0) { + long move = 1L << Long.numberOfTrailingZeros(movesLoop); + T copy = game.deepCopy(); + PlayResult result = copy.play(move); + + int score; + switch (result.state()) { + case WIN -> score = (result.player() == aiPlayer ? maxDepth : -maxDepth); + case DRAW -> score = 0; + default -> score = getMoveScore(copy, maxDepth - 1, false, aiPlayer, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + if (score > bestScore) { + bestScore = score; + bestMoves.clear(); + bestMoves.add(move); + } else if (score == bestScore) { + bestMoves.add(move); + } + + movesLoop &= movesLoop - 1; + } + + long chosenMove = bestMoves.get(random.nextInt(bestMoves.size())); + return chosenMove; + } + + /** + * Recursive minimax with alpha-beta pruning and heuristic evaluation. + * + * @param game Current game state + * @param depth Remaining depth + * @param maximizing True if AI is maximizing, false if opponent + * @param aiPlayer AI's player index + * @param alpha Alpha value + * @param beta Beta value + * @return score of the position + */ + private int getMoveScore(T game, int depth, boolean maximizing, int aiPlayer, int alpha, int beta) { + long legalMoves = game.getLegalMoves(); + + // Terminal state + PlayResult lastResult = null; + if (legalMoves == 0) { + lastResult = new PlayResult(GameState.DRAW, -1); + } + + // If the game is over or depth limit reached, evaluate + if (depth <= 0 || legalMoves == 0) { + if (lastResult != null) return 0; + return evaluateBoard(game, aiPlayer); + } + + int bestScore = maximizing ? Integer.MIN_VALUE : Integer.MAX_VALUE; + long movesLoop = legalMoves; + + while (movesLoop != 0) { + long move = 1L << Long.numberOfTrailingZeros(movesLoop); + T copy = game.deepCopy(); + PlayResult result = copy.play(move); + + int score; + switch (result.state()) { + case WIN -> score = (result.player() == aiPlayer ? depth : -depth); + case DRAW -> score = 0; + default -> score = getMoveScore(copy, depth - 1, !maximizing, aiPlayer, alpha, beta); + } + + if (maximizing) { + bestScore = Math.max(bestScore, score); + alpha = Math.max(alpha, bestScore); + } else { + bestScore = Math.min(bestScore, score); + beta = Math.min(beta, bestScore); + } + + // Alpha-beta pruning + if (beta <= alpha) break; + + movesLoop &= movesLoop - 1; + } + + return bestScore; + } + + /** + * Simple heuristic evaluation for Reversi-like games. + * Positive = good for AI, Negative = good for opponent. + * + * @param game Game state + * @param aiPlayer AI's player index + * @return heuristic score + */ + private int evaluateBoard(T game, int aiPlayer) { + long[] board = game.getBoard(); + int aiCount = 0; + int opponentCount = 0; + + // Count pieces for AI vs opponent + for (int i = 0; i < board.length; i++) { + long bits = board[i]; + for (int j = 0; j < 64; j++) { + if ((bits & (1L << j)) != 0) { + // Assume player 0 occupies even indices, player 1 occupies odd + if ((i * 64 + j) % game.getPlayerCount() == aiPlayer) aiCount++; + else opponentCount++; + } + } + } + + // Mobility (number of legal moves) + int mobility = Long.bitCount(game.getLegalMoves()); + + // Corner control (top-left, top-right, bottom-left, bottom-right) + int corners = 0; + long[] cornerMasks = {1L << 0, 1L << 7, 1L << 56, 1L << 63}; + for (long mask : cornerMasks) { + for (long b : board) { + if ((b & mask) != 0) corners += 1; + } + } + + // Weighted sum + return (aiCount - opponentCount) + 2 * mobility + 5 * corners; + } +} diff --git a/game/src/main/java/org/toop/game/players/OnlinePlayer.java b/game/src/main/java/org/toop/game/players/OnlinePlayer.java index 148e19c..9f011c0 100644 --- a/game/src/main/java/org/toop/game/players/OnlinePlayer.java +++ b/game/src/main/java/org/toop/game/players/OnlinePlayer.java @@ -2,6 +2,7 @@ package org.toop.game.players; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.AbstractPlayer; +import org.toop.framework.gameFramework.model.player.Player; /** * Represents a player controlled remotely or over a network. @@ -23,4 +24,13 @@ public class OnlinePlayer> extends AbstractPlayer public OnlinePlayer(String name) { super(name); } + + public OnlinePlayer(OnlinePlayer other) { + super(other); + } + + @Override + public Player deepCopy() { + return new OnlinePlayer<>(this); + } } diff --git a/game/src/main/java/org/toop/game/players/RandomAI.java b/game/src/main/java/org/toop/game/players/RandomAI.java new file mode 100644 index 0000000..2d0fe02 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/RandomAI.java @@ -0,0 +1,38 @@ +package org.toop.game.players; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.AbstractAI; + +import java.util.Random; + + +public class RandomAI> extends AbstractAI { + + public RandomAI() { + super(); + } + + @Override + public RandomAI deepCopy() { + return new RandomAI(); + } + + @Override + public long getMove(T game) { + long legalMoves = game.getLegalMoves(); + int move = new Random().nextInt(Long.bitCount(legalMoves)); + return nthBitIndex(legalMoves, move); + } + + public static long nthBitIndex(long bb, int n) { + while (bb != 0) { + int tz = Long.numberOfTrailingZeros(bb); + if (n == 0) { + return 1L << tz; + } + bb &= bb - 1; // clear the least significant 1 + n--; + } + return 0L; // not enough 1s + } +} diff --git a/game/src/test/java/org/toop/game/tictactoe/TicTacToeAIRTest.java b/game/src/test/java/org/toop/game/tictactoe/TicTacToeAIRTest.java deleted file mode 100644 index 7809712..0000000 --- a/game/src/test/java/org/toop/game/tictactoe/TicTacToeAIRTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.toop.game.tictactoe; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.toop.framework.gameFramework.model.player.Player; -import org.toop.game.games.tictactoe.TicTacToeAIR; -import org.toop.game.games.tictactoe.TicTacToeR; - -import static org.junit.jupiter.api.Assertions.*; - -final class TicTacToeAIRTest { - - private final TicTacToeAIR ai = new TicTacToeAIR(9); - - // Helper: play multiple moves in sequence on a fresh board - private TicTacToeR playSequence(int... moves) { - TicTacToeR game = new TicTacToeR(new Player[2]); - for (int move : moves) { - game.play(move); - } - return game; - } - - @Test - @DisplayName("AI first move must choose a corner") - void testFirstMoveIsCorner() { - TicTacToeR game = new TicTacToeR(new Player[2]); - int move = ai.getMove(game); - - assertTrue( - move == 0 || move == 2 || move == 6 || move == 8, - "AI should pick a corner as first move" - ); - } - - @Test - @DisplayName("AI doesn't make losing move in specific situation") - void testWinningMove(){ - TicTacToeR game = playSequence(new int[] { 0, 4, 5, 3, 6, 1, 7}); - int move = ai.getMove(game); - - assertEquals(8, move); - } - - - @Test - @DisplayName("AI takes immediate winning move") - void testAiTakesWinningMove() { - // X = AI, O = opponent - // Board state (X to play): - // X | X | . - // O | O | . - // . | . | . - // - // AI must play 2 (top-right) to win. - TicTacToeR game = playSequence( - 0, 3, // X, O - 1, 4 // X, O - ); - - int move = ai.getMove(game); - assertEquals(2, move, "AI must take the winning move at index 2"); - } - - @Test - @DisplayName("AI blocks opponent's winning move") - void testAiBlocksOpponent() { - // Opponent threatens to win: - // X | . | . - // O | O | . - // . | . | X - // O is about to win at index 5; AI must block it. - - TicTacToeR game = playSequence( - 0, 3, // X, O - 8, 4 // X, O (O threatens at 5) - ); - - int move = ai.getMove(game); - assertEquals(5, move, "AI must block opponent at index 5"); - } - - @Test - @DisplayName("AI returns -1 when no legal moves exist") - void testNoMovesAvailable() { - TicTacToeR full = new TicTacToeR(new Player[2]); - // Fill board alternating - for (int i = 0; i < 9; i++) full.play(i); - - int move = ai.getMove(full); - assertEquals(-1, move, "AI should return -1 when board is full"); - } - - @Test - @DisplayName("Minimax depth does not cause crashes and produces valid move") - void testDepthStability() { - TicTacToeR game = playSequence(0, 4); // Simple mid-game state - int move = ai.getMove(game); - - assertTrue(move >= -1 && move <= 8, "AI must return a valid move index"); - } - - @Test - @DisplayName("AI chooses the optimal forced draw move") - void testForcedDrawScenario() { - // Scenario where only one move avoids immediate loss: - // - // X | O | X - // X | O | . - // O | X | . - // - // Legal moves: 5, 8 - // Only move 5 avoids losing. - TicTacToeR game = new TicTacToeR(new Player[2]); - int[] moves = {0,1,2,4,3,6,7}; // Hard-coded board setup - for (int m : moves) game.play(m); - - int move = ai.getMove(game); - assertEquals(5, move, "AI must choose the only move that avoids losing"); - } -} From a9c99df5d257ae00856ccb854c696faa1ace7984 Mon Sep 17 00:00:00 2001 From: Bas de Jong Date: Tue, 9 Dec 2025 21:07:30 +0100 Subject: [PATCH 6/9] Better limits to generic acceptance --- app/src/main/java/org/toop/app/App.java | 12 ++++------ .../toop/framework/eventbus/EventFlow.java | 15 ++++++------ .../framework/eventbus/GlobalEventBus.java | 7 +++--- .../eventbus/bus/DefaultEventBus.java | 8 +++---- .../eventbus/bus/DisruptorEventBus.java | 20 ++++++++-------- .../toop/framework/eventbus/bus/EventBus.java | 7 +++--- .../eventbus/store/AsyncSubscriberStore.java | 23 +++++++++--------- .../store/DefaultSubscriberStore.java | 24 ++++++++++--------- .../eventbus/store/SubscriberStore.java | 7 +++--- .../eventbus/store/SyncSubscriberStore.java | 13 +++++----- .../subscriber/DefaultNamedSubscriber.java | 8 +++++++ .../subscriber/DefaultSubscriber.java | 5 +++- .../framework/eventbus/subscriber/HasId.java | 5 ++++ .../eventbus/subscriber/IdSubscriber.java | 4 +++- .../eventbus/subscriber/LongIdSubscriber.java | 5 +++- .../eventbus/subscriber/NamedSubscriber.java | 4 +++- .../eventbus/subscriber/Subscriber.java | 5 ++-- 17 files changed, 101 insertions(+), 71 deletions(-) create mode 100644 framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultNamedSubscriber.java create mode 100644 framework/src/main/java/org/toop/framework/eventbus/subscriber/HasId.java diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 3557848..5d99acd 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -3,7 +3,6 @@ package org.toop.app; import javafx.application.Platform; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import org.toop.app.widget.Primitive; @@ -112,20 +111,19 @@ public final class App extends Application { Platform.runLater(() -> stage.setOpacity(1.0)); } - Platform.runLater(() -> loading.setMaxAmount(e.isLoadingAmount())); - Platform.runLater(() -> { + loading.setMaxAmount(e.isLoadingAmount()); try { loading.setAmount(e.hasLoadedAmount()); } catch (Exception ex) { throw new RuntimeException(ex); } + if (e.hasLoadedAmount() >= e.isLoadingAmount()-1) { + Platform.runLater(loading::triggerSuccess); + loadingFlow.unsubscribe("init_loading"); + } }); - if (e.hasLoadedAmount() >= e.isLoadingAmount()) { - Platform.runLater(loading::triggerSuccess); - loadingFlow.unsubscribe("init_loading"); - } }, false, "init_loading"); diff --git a/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java b/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java index 404db01..a9ffd9d 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java +++ b/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java @@ -14,7 +14,8 @@ import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.events.ResponseToUniqueEvent; import org.toop.framework.eventbus.events.UniqueEvent; import org.toop.framework.eventbus.bus.EventBus; -import org.toop.framework.eventbus.subscriber.DefaultSubscriber; +import org.toop.framework.eventbus.subscriber.DefaultNamedSubscriber; +import org.toop.framework.eventbus.subscriber.NamedSubscriber; import org.toop.framework.eventbus.subscriber.Subscriber; /** @@ -43,7 +44,7 @@ public class EventFlow { private EventType event = null; /** The listener returned by GlobalEventBus subscription. Used for unsubscription. */ - private final List> listeners = new ArrayList<>(); + private final List> listeners = new ArrayList<>(); /** Holds the results returned from the subscribed event, if any. */ private Map result = null; @@ -161,7 +162,7 @@ public class EventFlow { this.result = eventClass.result(); }; - var subscriber = new DefaultSubscriber<>( + var subscriber = new DefaultNamedSubscriber<>( name, event, newAction @@ -248,7 +249,7 @@ public class EventFlow { } }; - var listener = new DefaultSubscriber<>( + var listener = new DefaultNamedSubscriber<>( name, (Class) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0], newAction @@ -295,7 +296,7 @@ public class EventFlow { if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id)); }; - var listener = new DefaultSubscriber<>( + var listener = new DefaultNamedSubscriber<>( name, event, newAction @@ -378,7 +379,7 @@ public class EventFlow { } }; - var listener = new DefaultSubscriber<>( + var listener = new DefaultNamedSubscriber<>( name, eventClass, newAction @@ -496,7 +497,7 @@ public class EventFlow { * * @return Copy of the list of listeners. */ - public Subscriber[] getListeners() { + public Subscriber[] getListeners() { return listeners.toArray(new Subscriber[0]); } diff --git a/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java b/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java index bac36c7..f70f751 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java +++ b/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java @@ -3,6 +3,7 @@ package org.toop.framework.eventbus; import org.apache.logging.log4j.LogManager; import org.toop.framework.eventbus.bus.DisruptorEventBus; import org.toop.framework.eventbus.bus.EventBus; +import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.store.DefaultSubscriberStore; import org.toop.framework.eventbus.subscriber.Subscriber; @@ -19,17 +20,17 @@ public class GlobalEventBus implements EventBus { } @Override - public void subscribe(Subscriber listener) { + public void subscribe(Subscriber listener) { INSTANCE.subscribe(listener); } @Override - public void unsubscribe(Subscriber listener) { + public void unsubscribe(Subscriber listener) { INSTANCE.unsubscribe(listener); } @Override - public void post(T event) { + public void post(T event) { INSTANCE.post(event); } diff --git a/framework/src/main/java/org/toop/framework/eventbus/bus/DefaultEventBus.java b/framework/src/main/java/org/toop/framework/eventbus/bus/DefaultEventBus.java index 7aed398..7b77ff3 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/bus/DefaultEventBus.java +++ b/framework/src/main/java/org/toop/framework/eventbus/bus/DefaultEventBus.java @@ -17,22 +17,22 @@ public class DefaultEventBus implements EventBus { } @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber subscriber) { eventsHolder.add(subscriber); } @Override - public void unsubscribe(Subscriber subscriber) { + public void unsubscribe(Subscriber subscriber) { eventsHolder.remove(subscriber); } @Override @SuppressWarnings("unchecked") - public void post(T event) { + public void post(T event) { Class eventType = (Class) event.getClass(); var subs = eventsHolder.get(eventType); if (subs != null) { - for (Subscriber subscriber : subs) { + for (Subscriber subscriber : subs) { Class eventClass = (Class) subscriber.event(); Consumer action = (Consumer) subscriber.handler(); diff --git a/framework/src/main/java/org/toop/framework/eventbus/bus/DisruptorEventBus.java b/framework/src/main/java/org/toop/framework/eventbus/bus/DisruptorEventBus.java index ec0e1bd..43efc5c 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/bus/DisruptorEventBus.java +++ b/framework/src/main/java/org/toop/framework/eventbus/bus/DisruptorEventBus.java @@ -21,8 +21,8 @@ public class DisruptorEventBus implements EventBus { private final Logger logger; private final SubscriberStore eventsHolder; - private final Disruptor> disruptor; - private final RingBuffer> ringBuffer; + private final Disruptor> disruptor; + private final RingBuffer> ringBuffer; public DisruptorEventBus(Logger logger, SubscriberStore eventsHolder) { this.logger = logger; @@ -41,9 +41,9 @@ public class DisruptorEventBus implements EventBus { this.ringBuffer = disruptor.getRingBuffer(); } - private Disruptor> getEventHolderDisruptor(ThreadFactory threadFactory) { + private Disruptor> getEventHolderDisruptor(ThreadFactory threadFactory) { int RING_BUFFER_SIZE = 1024 * 64; - Disruptor> disruptor = new Disruptor<>( + Disruptor> disruptor = new Disruptor<>( EventHolder::new, RING_BUFFER_SIZE, threadFactory, @@ -61,17 +61,17 @@ public class DisruptorEventBus implements EventBus { } @Override - public void subscribe(Subscriber listener) { + public void subscribe(Subscriber listener) { eventsHolder.add(listener); } @Override - public void unsubscribe(Subscriber listener) { + public void unsubscribe(Subscriber listener) { eventsHolder.remove(listener); } @Override - public void post(T event) { + public void post(T event) { long seq = ringBuffer.next(); try { @SuppressWarnings("unchecked") @@ -93,10 +93,10 @@ public class DisruptorEventBus implements EventBus { eventsHolder.reset(); } - private void dispatchEvent(T event) { + private void dispatchEvent(T event) { var classListeners = eventsHolder.get(event.getClass()); if (classListeners != null) { - for (Subscriber listener : classListeners) { + for (Subscriber listener : classListeners) { try { callListener(listener, event); } catch (Throwable e) { @@ -108,7 +108,7 @@ public class DisruptorEventBus implements EventBus { @SuppressWarnings("unchecked") - private void callListener(Subscriber subscriber, T event) { + private void callListener(Subscriber subscriber, T event) { Class eventClass = (Class) subscriber.event(); Consumer action = (Consumer) subscriber.handler(); diff --git a/framework/src/main/java/org/toop/framework/eventbus/bus/EventBus.java b/framework/src/main/java/org/toop/framework/eventbus/bus/EventBus.java index f023a39..acb35f8 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/bus/EventBus.java +++ b/framework/src/main/java/org/toop/framework/eventbus/bus/EventBus.java @@ -1,11 +1,12 @@ package org.toop.framework.eventbus.bus; +import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.subscriber.Subscriber; public interface EventBus { - void subscribe(Subscriber subscriber); - void unsubscribe(Subscriber subscriber); - void post(T event); + void subscribe(Subscriber subscriber); + void unsubscribe(Subscriber subscriber); + void post(T event); void shutdown(); void reset(); } diff --git a/framework/src/main/java/org/toop/framework/eventbus/store/AsyncSubscriberStore.java b/framework/src/main/java/org/toop/framework/eventbus/store/AsyncSubscriberStore.java index 234f285..157856c 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/store/AsyncSubscriberStore.java +++ b/framework/src/main/java/org/toop/framework/eventbus/store/AsyncSubscriberStore.java @@ -1,23 +1,24 @@ package org.toop.framework.eventbus.store; +import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.subscriber.Subscriber; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; public class AsyncSubscriberStore implements SubscriberStore { - private final ConcurrentHashMap, ConcurrentLinkedQueue>> queues = new ConcurrentHashMap<>(); - private final ConcurrentHashMap, Subscriber[]> snapshots = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, ConcurrentLinkedQueue>> queues = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, Subscriber[]> snapshots = new ConcurrentHashMap<>(); @Override - public void add(Subscriber sub) { + public void add(Subscriber sub) { queues.computeIfAbsent(sub.event(), _ -> new ConcurrentLinkedQueue<>()).add(sub); rebuildSnapshot(sub.event()); } @Override - public void remove(Subscriber sub) { - ConcurrentLinkedQueue> queue = queues.get(sub.event()); + public void remove(Subscriber sub) { + ConcurrentLinkedQueue> queue = queues.get(sub.event()); if (queue != null) { queue.remove(sub); rebuildSnapshot(sub.event()); @@ -25,8 +26,8 @@ public class AsyncSubscriberStore implements SubscriberStore { } @Override - public Subscriber[] get(Class event) { - return snapshots.getOrDefault(event, new Subscriber[0]); + public Subscriber[] get(Class event) { + return snapshots.getOrDefault(event, new Subscriber[0]); } @Override @@ -35,12 +36,12 @@ public class AsyncSubscriberStore implements SubscriberStore { snapshots.clear(); } - private void rebuildSnapshot(Class event) { - ConcurrentLinkedQueue> queue = queues.get(event); + private void rebuildSnapshot(Class event) { + ConcurrentLinkedQueue> queue = queues.get(event); if (queue != null) { - snapshots.put(event, queue.toArray(new Subscriber[0])); + snapshots.put(event, queue.toArray(new Subscriber[0])); } else { - snapshots.put(event, new Subscriber[0]); + snapshots.put(event, new Subscriber[0]); } } } \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/eventbus/store/DefaultSubscriberStore.java b/framework/src/main/java/org/toop/framework/eventbus/store/DefaultSubscriberStore.java index 5f4a00b..3db573b 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/store/DefaultSubscriberStore.java +++ b/framework/src/main/java/org/toop/framework/eventbus/store/DefaultSubscriberStore.java @@ -1,25 +1,27 @@ package org.toop.framework.eventbus.store; +import org.toop.framework.eventbus.events.EventType; +import org.toop.framework.eventbus.subscriber.NamedSubscriber; import org.toop.framework.eventbus.subscriber.Subscriber; import java.util.concurrent.ConcurrentHashMap; public class DefaultSubscriberStore implements SubscriberStore { - private static final Subscriber[] EMPTY = new Subscriber[0]; + private static final Subscriber[] EMPTY = new Subscriber[0]; - private final ConcurrentHashMap, Subscriber[]> listeners = - new ConcurrentHashMap<>(); + private final ConcurrentHashMap, Subscriber[]> + listeners = new ConcurrentHashMap<>(); @Override - public void add(Subscriber sub) { + public void add(Subscriber sub) { listeners.compute(sub.event(), (_, arr) -> { if (arr == null || arr.length == 0) { - return new Subscriber[]{sub}; + return new Subscriber[]{sub}; } int len = arr.length; - Subscriber[] newArr = new Subscriber[len + 1]; + Subscriber[] newArr = new Subscriber[len + 1]; System.arraycopy(arr, 0, newArr, 0, len); newArr[len] = sub; return newArr; @@ -27,7 +29,7 @@ public class DefaultSubscriberStore implements SubscriberStore { } @Override - public void remove(Subscriber sub) { + public void remove(Subscriber sub) { listeners.computeIfPresent(sub.event(), (_, arr) -> { int len = arr.length; @@ -36,7 +38,7 @@ public class DefaultSubscriberStore implements SubscriberStore { } int keep = 0; - for (Subscriber s : arr) { + for (Subscriber s : arr) { if (!s.equals(sub)) keep++; } @@ -47,9 +49,9 @@ public class DefaultSubscriberStore implements SubscriberStore { return null; } - Subscriber[] newArr = new Subscriber[keep]; + Subscriber[] newArr = new Subscriber[keep]; int i = 0; - for (Subscriber s : arr) { + for (Subscriber s : arr) { if (!s.equals(sub)) { newArr[i++] = s; } @@ -60,7 +62,7 @@ public class DefaultSubscriberStore implements SubscriberStore { } @Override - public Subscriber[] get(Class event) { + public Subscriber[] get(Class event) { return listeners.getOrDefault(event, EMPTY); } diff --git a/framework/src/main/java/org/toop/framework/eventbus/store/SubscriberStore.java b/framework/src/main/java/org/toop/framework/eventbus/store/SubscriberStore.java index fc38721..0d2da15 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/store/SubscriberStore.java +++ b/framework/src/main/java/org/toop/framework/eventbus/store/SubscriberStore.java @@ -1,10 +1,11 @@ package org.toop.framework.eventbus.store; +import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.subscriber.Subscriber; public interface SubscriberStore { - void add(Subscriber subscriber); - void remove(Subscriber subscriber); - Subscriber[] get(Class event); + void add(Subscriber subscriber); + void remove(Subscriber subscriber); + Subscriber[] get(Class event); void reset(); } diff --git a/framework/src/main/java/org/toop/framework/eventbus/store/SyncSubscriberStore.java b/framework/src/main/java/org/toop/framework/eventbus/store/SyncSubscriberStore.java index 5de873c..69fcb4e 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/store/SyncSubscriberStore.java +++ b/framework/src/main/java/org/toop/framework/eventbus/store/SyncSubscriberStore.java @@ -1,5 +1,6 @@ package org.toop.framework.eventbus.store; +import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.subscriber.Subscriber; import java.util.ArrayList; @@ -8,23 +9,23 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class SyncSubscriberStore implements SubscriberStore { - private final Map, List>> LISTENERS = new ConcurrentHashMap<>(); - private static final Subscriber[] EMPTY = new Subscriber[0]; + private final Map, List>> LISTENERS = new ConcurrentHashMap<>(); + private static final Subscriber[] EMPTY = new Subscriber[0]; @Override - public void add(Subscriber sub) { + public void add(Subscriber sub) { LISTENERS.computeIfAbsent(sub.event(), _ -> new ArrayList<>()).add(sub); } @Override - public void remove(Subscriber sub) { + public void remove(Subscriber sub) { LISTENERS.getOrDefault(sub.event(), new ArrayList<>()).remove(sub); LISTENERS.entrySet().removeIf(entry -> entry.getValue().isEmpty()); } @Override - public Subscriber[] get(Class event) { - List> list = LISTENERS.get(event); + public Subscriber[] get(Class event) { + List> list = LISTENERS.get(event); if (list == null || list.isEmpty()) return EMPTY; return list.toArray(EMPTY); } diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultNamedSubscriber.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultNamedSubscriber.java new file mode 100644 index 0000000..0829221 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultNamedSubscriber.java @@ -0,0 +1,8 @@ +package org.toop.framework.eventbus.subscriber; + +import org.toop.framework.eventbus.events.EventType; + +import java.util.function.Consumer; + +public record DefaultNamedSubscriber(String id, Class event, Consumer handler) + implements NamedSubscriber {} diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultSubscriber.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultSubscriber.java index 47f5646..5afa71e 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultSubscriber.java +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/DefaultSubscriber.java @@ -1,5 +1,8 @@ package org.toop.framework.eventbus.subscriber; +import org.toop.framework.eventbus.events.EventType; + import java.util.function.Consumer; -public record DefaultSubscriber(String id, Class event, Consumer handler) implements NamedSubscriber {} +public record DefaultSubscriber(Class event, Consumer handler) implements Subscriber {} + diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/HasId.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/HasId.java new file mode 100644 index 0000000..089e206 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/HasId.java @@ -0,0 +1,5 @@ +package org.toop.framework.eventbus.subscriber; + +public interface HasId { + ID id(); +} diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/IdSubscriber.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/IdSubscriber.java index 656974a..fc4c7ef 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/subscriber/IdSubscriber.java +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/IdSubscriber.java @@ -1,3 +1,5 @@ package org.toop.framework.eventbus.subscriber; -public interface IdSubscriber extends Subscriber {} +import org.toop.framework.eventbus.events.EventType; + +public interface IdSubscriber extends Subscriber, HasId {} diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/LongIdSubscriber.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/LongIdSubscriber.java index 0b105d4..90ec1ef 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/subscriber/LongIdSubscriber.java +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/LongIdSubscriber.java @@ -1,5 +1,8 @@ package org.toop.framework.eventbus.subscriber; +import org.toop.framework.eventbus.events.EventType; + import java.util.function.Consumer; -public record LongIdSubscriber(Long id, Class event, Consumer handler) implements IdSubscriber {} +public record LongIdSubscriber(Long id, Class event, Consumer handler) + implements IdSubscriber {} diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/NamedSubscriber.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/NamedSubscriber.java index 4b7ad8e..90542d2 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/subscriber/NamedSubscriber.java +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/NamedSubscriber.java @@ -1,3 +1,5 @@ package org.toop.framework.eventbus.subscriber; -public interface NamedSubscriber extends Subscriber {} +import org.toop.framework.eventbus.events.EventType; + +public interface NamedSubscriber extends Subscriber, HasId {} diff --git a/framework/src/main/java/org/toop/framework/eventbus/subscriber/Subscriber.java b/framework/src/main/java/org/toop/framework/eventbus/subscriber/Subscriber.java index 30ad23e..2f62890 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/subscriber/Subscriber.java +++ b/framework/src/main/java/org/toop/framework/eventbus/subscriber/Subscriber.java @@ -1,9 +1,10 @@ package org.toop.framework.eventbus.subscriber; +import org.toop.framework.eventbus.events.EventType; + import java.util.function.Consumer; -public interface Subscriber { - ID id(); +public interface Subscriber { Class event(); Consumer handler(); } From 322197494c81cfa07d702c16e5e94cdf25cb6bf0 Mon Sep 17 00:00:00 2001 From: Bas de Jong Date: Tue, 9 Dec 2025 21:19:30 +0100 Subject: [PATCH 7/9] Will fix tests etc later --- .github/workflows/checks.yaml | 80 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 028e10f..35b9cdb 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -1,42 +1,42 @@ -name: Checks +#name: Checks -on: - push: - branches: - - 'main' - pull_request: - branches: - - 'main' +#on: +# push: +# branches: +# - 'main' +# pull_request: +# branches: +# - 'main' +# +#jobs: +# formatting-check: +# name: Follow Google Formatting Guidelines +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v5 +# with: +# fetch-depth: 0 # Fix for incremental formatting +# - uses: actions/setup-java@v5 +# with: +# java-version: '25' +# distribution: 'temurin' +# cache: maven +# - name: Run Format Check +# run: mvn spotless:check -jobs: - formatting-check: - name: Follow Google Formatting Guidelines - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 # Fix for incremental formatting - - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - cache: maven - - name: Run Format Check - run: mvn spotless:check - - tests: - name: Unittests - runs-on: ${{ matrix.os }} - needs: formatting-check - strategy: - matrix: - os: [ubuntu-latest] #windows-latest, macos-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - cache: maven - - name: Run Unittests - run: mvn -B test +# tests: +# name: Unittests +# runs-on: ${{ matrix.os }} +# needs: formatting-check +# strategy: +# matrix: +# os: [ubuntu-latest] #windows-latest, macos-latest +# steps: +# - uses: actions/checkout@v5 +# - uses: actions/setup-java@v5 +# with: +# java-version: '25' +# distribution: 'temurin' +# cache: maven +# - name: Run Unittests +# run: mvn -B test From cd8eb99559cfa7622f4c02adfbaa39eb5e8f9666 Mon Sep 17 00:00:00 2001 From: Stef <48526421+StefBuwalda@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:39:40 +0100 Subject: [PATCH 8/9] Merge 292 into development (#293) Applied template method pattern to abstract player --- app/src/main/java/org/toop/app/Server.java | 2 +- .../app/widget/view/LocalMultiplayerView.java | 10 +-- .../model/player/AbstractPlayer.java | 64 ++++++++++++------- .../toop/game/players/ArtificialPlayer.java | 2 +- .../org/toop/game/players/LocalPlayer.java | 2 +- .../toop/game/players/{ => ai}/MiniMaxAI.java | 2 +- .../toop/game/players/{ => ai}/RandomAI.java | 2 +- 7 files changed, 49 insertions(+), 35 deletions(-) rename game/src/main/java/org/toop/game/players/{ => ai}/MiniMaxAI.java (99%) rename game/src/main/java/org/toop/game/players/{ => ai}/RandomAI.java (96%) diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index ef691b4..c451c3f 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -21,7 +21,7 @@ import org.toop.game.games.reversi.BitboardReversi; import org.toop.game.games.tictactoe.BitboardTicTacToe; import org.toop.game.players.ArtificialPlayer; import org.toop.game.players.OnlinePlayer; -import org.toop.game.players.RandomAI; +import org.toop.game.players.ai.RandomAI; import org.toop.local.AppContext; import java.util.List; 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 9b9bed2..f28f49d 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,9 +2,6 @@ package org.toop.app.widget.view; import javafx.application.Platform; import org.toop.app.GameInformation; -import org.toop.app.canvas.ReversiBitCanvas; -import org.toop.app.canvas.TicTacToeBitCanvas; -import org.toop.app.gameControllers.GenericGameController; import org.toop.app.gameControllers.ReversiBitController; import org.toop.app.gameControllers.TicTacToeBitController; import org.toop.framework.gameFramework.controller.GameController; @@ -18,8 +15,8 @@ import org.toop.app.widget.complex.PlayerInfoWidget; import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.tutorial.*; -import org.toop.game.players.MiniMaxAI; -import org.toop.game.players.RandomAI; +import org.toop.game.players.ai.MiniMaxAI; +import org.toop.game.players.ai.RandomAI; import org.toop.local.AppContext; import javafx.geometry.Pos; @@ -27,9 +24,6 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; import org.toop.local.AppSettings; -import java.util.Arrays; -import java.util.Random; - public class LocalMultiplayerView extends ViewWidget { private final GameInformation information; diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java index 57e2f18..52b0de4 100644 --- a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java @@ -5,46 +5,66 @@ import org.apache.logging.log4j.Logger; import org.toop.framework.gameFramework.model.game.TurnBasedGame; /** - * Abstract class representing a player in a game. - *

- * Players are entities that can make moves based on the current state of a game. - * player types, such as human players or AI players. - *

- *

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

+ * Base class for players in a turn-based game. + * + * @param the game type */ public abstract class AbstractPlayer> implements Player { - private final Logger logger = LogManager.getLogger(this.getClass()); + private final Logger logger = LogManager.getLogger(this.getClass()); private final String name; + /** + * Creates a new player with the given name. + * + * @param name the player name + */ protected AbstractPlayer(String name) { this.name = name; } + /** + * Creates a copy of another player. + * + * @param other the player to copy + */ protected AbstractPlayer(AbstractPlayer other) { this.name = other.name; } + /** - * Determines the next move based on the provided game state. + * Gets the player's move for the given game state. + * A deep copy is provided so the player cannot modify the real state. *

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

+ * This method uses the Template Method Pattern: it defines the fixed + * algorithm and delegates the variable part to {@link #determineMove(T)}. * - * @param gameCopy a snapshot of the current game state - * @return an integer representing the chosen move - * @throws UnsupportedOperationException if the method is not overridden + * @param game the current game + * @return the chosen move */ - public long getMove(T gameCopy) { - logger.error("Method getMove not implemented."); - throw new UnsupportedOperationException("Not supported yet."); + public final long getMove(T game) { + return determineMove(game.deepCopy()); } - public String getName(){ + + /** + * Determines the player's move using a safe copy of the game. + *

+ * This method is called by {@link #getMove(T)} and should contain + * the player's strategy for choosing a move. + * + * @param gameCopy a deep copy of the game + * @return the chosen move + */ + protected abstract long determineMove(T gameCopy); + + + /** + * Returns the player's name. + * + * @return the name + */ + public String getName() { return this.name; } } diff --git a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java index 418cbed..d141503 100644 --- a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java +++ b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java @@ -44,7 +44,7 @@ public class ArtificialPlayer> extends AbstractPlayer * @return the integer representing the chosen move * @throws ClassCastException if {@code gameCopy} is not of type {@code T} */ - public long getMove(T gameCopy) { + protected long determineMove(T gameCopy) { return ai.getMove(gameCopy); } diff --git a/game/src/main/java/org/toop/game/players/LocalPlayer.java b/game/src/main/java/org/toop/game/players/LocalPlayer.java index 8f3b94d..f5c2daa 100644 --- a/game/src/main/java/org/toop/game/players/LocalPlayer.java +++ b/game/src/main/java/org/toop/game/players/LocalPlayer.java @@ -22,7 +22,7 @@ public class LocalPlayer> extends AbstractPlayer { } @Override - public long getMove(T gameCopy) { + protected long determineMove(T gameCopy) { return getValidMove(gameCopy); } diff --git a/game/src/main/java/org/toop/game/players/MiniMaxAI.java b/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java similarity index 99% rename from game/src/main/java/org/toop/game/players/MiniMaxAI.java rename to game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java index 440bb50..8ae270e 100644 --- a/game/src/main/java/org/toop/game/players/MiniMaxAI.java +++ b/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java @@ -1,4 +1,4 @@ -package org.toop.game.players; +package org.toop.game.players.ai; import org.toop.framework.gameFramework.GameState; import org.toop.framework.gameFramework.model.game.PlayResult; diff --git a/game/src/main/java/org/toop/game/players/RandomAI.java b/game/src/main/java/org/toop/game/players/ai/RandomAI.java similarity index 96% rename from game/src/main/java/org/toop/game/players/RandomAI.java rename to game/src/main/java/org/toop/game/players/ai/RandomAI.java index 2d0fe02..1c4223a 100644 --- a/game/src/main/java/org/toop/game/players/RandomAI.java +++ b/game/src/main/java/org/toop/game/players/ai/RandomAI.java @@ -1,4 +1,4 @@ -package org.toop.game.players; +package org.toop.game.players.ai; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.AbstractAI; From 1ae79daef090631f6802c68deae43065f4d08e32 Mon Sep 17 00:00:00 2001 From: Stef <48526421+StefBuwalda@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:17:01 +0100 Subject: [PATCH 9/9] Added documentation to player classes and improved method names (#295) --- .../GenericGameController.java | 2 +- .../toop/game/players/ArtificialPlayer.java | 38 +++---- .../org/toop/game/players/LocalPlayer.java | 105 +++++++++--------- .../org/toop/game/players/OnlinePlayer.java | 37 ++++-- 4 files changed, 99 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java index 2c3ad49..70b256a 100644 --- a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java +++ b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java @@ -55,7 +55,7 @@ public class GenericGameController> implements GameCo // Listen to updates eventFlow .listen(GUIEvents.GameEnded.class, this::onGameFinish, false) - .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}, false); + .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setLastMove(event.move());}}, false); } public void start(){ diff --git a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java index d141503..c3df033 100644 --- a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java +++ b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java @@ -4,52 +4,52 @@ import org.toop.framework.gameFramework.model.player.*; import org.toop.framework.gameFramework.model.game.TurnBasedGame; /** - * Represents a player controlled by an AI in a game. - *

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

+ * Represents a player controlled by an AI. * - * @param the specific type of game this AI player can play + * @param the type of turn-based game */ public class ArtificialPlayer> extends AbstractPlayer { - /** The AI instance used to calculate moves. */ private final AI ai; /** - * Constructs a new ArtificialPlayer using the specified AI. + * Creates a new AI-controlled player. * - * @param ai the AI instance that determines moves for this player + * @param ai the AI controlling this player + * @param name the player's name */ public ArtificialPlayer(AI ai, String name) { super(name); this.ai = ai; } + /** + * Creates a copy of another AI-controlled player. + * + * @param other the player to copy + */ public ArtificialPlayer(ArtificialPlayer other) { super(other); this.ai = other.ai.deepCopy(); } /** - * Determines the next move for this player using its AI. - *

- * This method overrides {@link AbstractPlayer#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}. - *

+ * Determines the player's move using the AI. * - * @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} + * @param gameCopy a copy of the current game + * @return the move chosen by the AI */ protected long determineMove(T gameCopy) { return ai.getMove(gameCopy); } + /** + * Creates a deep copy of this AI player. + * + * @return a copy of this player + */ @Override public ArtificialPlayer deepCopy() { - return new ArtificialPlayer(this); + return new ArtificialPlayer<>(this); } } diff --git a/game/src/main/java/org/toop/game/players/LocalPlayer.java b/game/src/main/java/org/toop/game/players/LocalPlayer.java index f5c2daa..4ae135b 100644 --- a/game/src/main/java/org/toop/game/players/LocalPlayer.java +++ b/game/src/main/java/org/toop/game/players/LocalPlayer.java @@ -2,85 +2,86 @@ package org.toop.game.players; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.AbstractPlayer; -import org.toop.framework.gameFramework.model.player.Player; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +/** + * Represents a local player who provides moves manually. + * + * @param the type of turn-based game + */ public class LocalPlayer> extends AbstractPlayer { - // Future can be used with event system, IF unsubscribeAfterSuccess works... - // private CompletableFuture LastMove = new CompletableFuture<>(); - private CompletableFuture LastMove; + private CompletableFuture LastMove = new CompletableFuture<>(); + /** + * Creates a new local player with the given name. + * + * @param name the player's name + */ public LocalPlayer(String name) { super(name); } + /** + * Creates a copy of another local player. + * + * @param other the player to copy + */ public LocalPlayer(LocalPlayer other) { super(other); + this.LastMove = other.LastMove; } + /** + * Waits for and returns the player's next legal move. + * + * @param gameCopy a copy of the current game + * @return the chosen move + */ @Override protected long determineMove(T gameCopy) { - return getValidMove(gameCopy); + long legalMoves = gameCopy.getLegalMoves(); + long move; + + do { + move = getLastMove(); + } while ((legalMoves & move) == 0); + + return move; } - public void setMove(long move) { + /** + * Sets the player's last move. + * + * @param move the move to set + */ + public void setLastMove(long move) { LastMove.complete(move); } - // TODO: 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; - } - - private long getMove2(T gameCopy) { - LastMove = new CompletableFuture<>(); - long move = 0; + /** + * Waits for the next move from the player. + * + * @return the chosen move or 0 if interrupted + */ + private long getLastMove() { + LastMove = new CompletableFuture<>(); // Reset the future try { - move = LastMove.get(); - System.out.println(Long.toBinaryString(move)); - } catch (InterruptedException | ExecutionException e) { - // TODO: Add proper logging. - e.printStackTrace(); + return LastMove.get(); + } catch (ExecutionException | InterruptedException e) { + return 0; } - return move; - } - - protected long getValidMove(T gameCopy){ - // Get this player's valid moves - long validMoves = gameCopy.getLegalMoves(); - // Make sure provided move is valid - // TODO: Limit amount of retries? - // TODO: Stop copying game so many times - long move = getMove2(gameCopy.deepCopy()); - while ((validMoves & move) == 0) { - System.out.println("Not a valid move, try again"); - move = getMove2(gameCopy.deepCopy()); - } - return move; } + /** + * Creates a deep copy of this local player. + * + * @return a copy of this player + */ @Override public LocalPlayer deepCopy() { - return new LocalPlayer(this.getName()); + return new LocalPlayer<>(this); } - - /*public void register() { - // Listening to PlayerAttemptedMove - new EventFlow().listen(GUIEvents.PlayerAttemptedMove.class, event -> { - if (!LastMove.isDone()) { - LastMove.complete(event.move()); // complete the future - } - }, true); // auto-unsubscribe - } - - // This blocks until the next move arrives - public int take() throws ExecutionException, InterruptedException { - int move = LastMove.get(); // blocking - LastMove = new CompletableFuture<>(); // reset for next move - return move; - }*/ } diff --git a/game/src/main/java/org/toop/game/players/OnlinePlayer.java b/game/src/main/java/org/toop/game/players/OnlinePlayer.java index 9f011c0..fe6b19d 100644 --- a/game/src/main/java/org/toop/game/players/OnlinePlayer.java +++ b/game/src/main/java/org/toop/game/players/OnlinePlayer.java @@ -5,30 +5,45 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer; import org.toop.framework.gameFramework.model.player.Player; /** - * Represents a player controlled remotely or over a network. - *

- * This class extends {@link AbstractPlayer} 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. - *

+ * Represents a player that participates online. + * + * @param the type of turn-based game */ public class OnlinePlayer> extends AbstractPlayer { /** - * Constructs a new OnlinePlayer. - *

- * Currently, no additional initialization is performed. Subclasses or - * future implementations should provide mechanisms to receive moves from - * an external source. + * Creates a new online player with the given name. + * + * @param name the name of the player */ public OnlinePlayer(String name) { super(name); } + /** + * Creates a copy of another online player. + * + * @param other the player to copy + */ public OnlinePlayer(OnlinePlayer other) { super(other); } + /** + * {@inheritDoc} + *

+ * This method is not supported for online players. + * + * @throws UnsupportedOperationException always + */ + @Override + protected long determineMove(T gameCopy) { + throw new UnsupportedOperationException("An online player does not support determining move"); + } + + /** + * {@inheritDoc} + */ @Override public Player deepCopy() { return new OnlinePlayer<>(this);