From 0447f7b0fe0695c85a12ded36bd31d057586ad38 Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Sun, 19 Oct 2025 02:18:11 +0200 Subject: [PATCH 01/10] added method for sorting the flipped pieces by distance to Move.position --- .../java/org/toop/app/canvas/GameCanvas.java | 5 ++++ .../java/org/toop/game/reversi/Reversi.java | 27 ++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) 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 52d0521..bc4f1cd 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -83,6 +83,11 @@ public abstract class GameCanvas { graphics.clearRect(0, 0, width, height); } + public void clearCell(int cellIndex) { + Cell cell = cells[cellIndex]; + graphics.clearRect(cell.x, cell.y, cell.width, cell.height); + } + public void render() { graphics.setFill(color); diff --git a/game/src/main/java/org/toop/game/reversi/Reversi.java b/game/src/main/java/org/toop/game/reversi/Reversi.java index e378b45..6da3c73 100644 --- a/game/src/main/java/org/toop/game/reversi/Reversi.java +++ b/game/src/main/java/org/toop/game/reversi/Reversi.java @@ -15,6 +15,7 @@ public final class Reversi extends TurnBasedGame { private int movesTaken; public static final char FIRST_MOVE = 'B'; private Set filledCells = new HashSet<>(); + private Move[] mostRecentlyFlippedPieces; public Reversi() { super(8, 8, 2); @@ -138,14 +139,14 @@ public final class Reversi extends TurnBasedGame { } } if (moveIsLegal) { - Move[] moves = getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value()); + Move[] moves = sortMovesFromCenter(getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value()),move); + mostRecentlyFlippedPieces = moves; board[move.position()] = move.value(); IO.println(move.position() +" "+ move.value()); for (Move m : moves) { board[m.position()] = m.value(); } filledCells.add(new Point(move.position() % rowSize, move.position() / columnSize)); - //updateFilledCellsSet(); nextTurn(); if (getLegalMoves().length == 0) { skipMyTurn(); @@ -153,12 +154,12 @@ public final class Reversi extends TurnBasedGame { } return State.NORMAL; } - return null; } public void skipMyTurn(){ IO.println("TURN " + getCurrentPlayer() + " SKIPPED"); + //todo notify user that a turn has been skipped nextTurn(); } @@ -192,5 +193,25 @@ public final class Reversi extends TurnBasedGame { } return new Game.Score(player1Score, player2Score); } + public Move[] sortMovesFromCenter(Move[] moves, Move center) { + int centerX = center.position()%columnSize; + int centerY = center.position()/rowSize; + IO.println("pre "+Arrays.toString(moves)); + Arrays.sort(moves, (a, b) -> { + int dxA = a.position()%columnSize - centerX; + int dyA = a.position()/rowSize - centerY; + int dxB = b.position()%columnSize - centerX; + int dyB = b.position()/rowSize - centerY; + int distA = dxA * dxA + dyA * dyA; + int distB = dxB * dxB + dyB * dyB; + + return Integer.compare(distA, distB); + }); + IO.println("post "+Arrays.toString(moves)); + return moves; + } + public Move[] getMostRecentlyFlippedPieces() { + return mostRecentlyFlippedPieces; + } } \ No newline at end of file From df493a5ebac9ad39984999d0e68aadcb3e296ff3 Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Sun, 19 Oct 2025 13:06:46 +0200 Subject: [PATCH 02/10] new reversi test (both players no legal moves) --- .../java/org/toop/game/reversi/Reversi.java | 17 +++++++++-- .../org/toop/game/tictactoe/ReversiTest.java | 28 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/game/src/main/java/org/toop/game/reversi/Reversi.java b/game/src/main/java/org/toop/game/reversi/Reversi.java index 6da3c73..ea16075 100644 --- a/game/src/main/java/org/toop/game/reversi/Reversi.java +++ b/game/src/main/java/org/toop/game/reversi/Reversi.java @@ -150,7 +150,18 @@ public final class Reversi extends TurnBasedGame { nextTurn(); if (getLegalMoves().length == 0) { skipMyTurn(); - return State.MOVE_SKIPPED; + if (getLegalMoves().length > 0) { + return State.MOVE_SKIPPED; + } + else { + Score score = getScore(); + if (score.player1Score() == score.player2Score()) { + return State.DRAW; + } + else { + return State.WIN; + } + } } return State.NORMAL; } @@ -184,10 +195,10 @@ public final class Reversi extends TurnBasedGame { public Game.Score getScore(){ int player1Score = 0, player2Score = 0; for (int count = 0; count < rowSize * columnSize; count++) { - if (board[count] == 'W') { + if (board[count] == 'B') { player1Score += 1; } - if (board[count] == 'B') { + if (board[count] == 'W') { player2Score += 1; } } diff --git a/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java b/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java index f251b89..fa3b897 100644 --- a/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java +++ b/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java @@ -149,6 +149,34 @@ class ReversiTest { game.play(ai.findBestMove(game,5)); } + @Test + void testGameShouldEndIfNoValidMoves() { + //European Grand Prix Ghent 2017: Replay Hassan - Verstuyft J. (3-17) + game.play(new Game.Move(19, 'B')); + game.play(new Game.Move(20, 'W')); + game.play(new Game.Move(29, 'B')); + game.play(new Game.Move(22, 'W')); + game.play(new Game.Move(21, 'B')); + game.play(new Game.Move(34, 'W')); + game.play(new Game.Move(23, 'B')); + game.play(new Game.Move(13, 'W')); + game.play(new Game.Move(26, 'B')); + game.play(new Game.Move(18, 'W')); + game.play(new Game.Move(12, 'B')); + game.play(new Game.Move(4, 'W')); + game.play(new Game.Move(17, 'B')); + game.play(new Game.Move(31, 'W')); + Game.State stateTurn15 = game.play(new Game.Move(39, 'B')); + assertEquals(Game.State.NORMAL, stateTurn15); + Game.State stateTurn16 = game.play(new Game.Move(16, 'W')); + assertEquals(Game.State.WIN, stateTurn16); + Game.State stateTurn17 = game.play(new Game.Move(5, 'B')); + assertNull(stateTurn17); + Game.Score score = game.getScore(); + assertEquals(3, score.player1Score()); + assertEquals(17, score.player2Score()); + } + @Test void testAISelectsLegalMove() { Game.Move move = ai.findBestMove(game,4); From c3f764f33db0eff81ff34ab20f8c34fd46fb75f0 Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Sun, 19 Oct 2025 22:01:33 +0200 Subject: [PATCH 03/10] connect4 with minimax AI --- .../java/org/toop/app/GameInformation.java | 9 +- app/src/main/java/org/toop/app/Server.java | 9 +- .../org/toop/app/canvas/Connect4Canvas.java | 13 + .../java/org/toop/app/canvas/GameCanvas.java | 51 +++- .../org/toop/app/canvas/TicTacToeCanvas.java | 25 -- .../java/org/toop/app/game/Connect4Game.java | 263 ++++++++++++++++++ .../app/view/views/LocalMultiplayerView.java | 3 + .../org/toop/app/view/views/LocalView.java | 9 +- .../localization/localization_en.properties | 1 + .../localization/localization_nl.properties | 1 + .../java/org/toop/game/Connect4/Connect4.java | 114 ++++++++ .../org/toop/game/Connect4/Connect4AI.java | 63 +++++ 12 files changed, 520 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/toop/app/canvas/Connect4Canvas.java create mode 100644 app/src/main/java/org/toop/app/game/Connect4Game.java create mode 100644 game/src/main/java/org/toop/game/Connect4/Connect4.java create mode 100644 game/src/main/java/org/toop/game/Connect4/Connect4AI.java diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java index 5380003..bca254f 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -3,12 +3,17 @@ package org.toop.app; public class GameInformation { public enum Type { TICTACTOE, - REVERSI; + REVERSI, + CONNECT4, + BATTLESHIP; + public static int playerCount(Type type) { return switch (type) { case TICTACTOE -> 2; case REVERSI -> 2; + case CONNECT4 -> 2; + case BATTLESHIP -> 2; }; } @@ -16,6 +21,8 @@ public class GameInformation { return switch (type) { case TICTACTOE -> 5; // Todo. 5 seems to always draw or win. could increase to 9 but that might affect performance case REVERSI -> 10; // Todo. 10 is a guess. might be too slow or too bad. + case CONNECT4 -> 7; + case BATTLESHIP -> 5; }; } } diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 8d71c4a..2437b37 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,6 +1,7 @@ package org.toop.app; import com.google.common.util.concurrent.AbstractScheduledService; +import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.ViewStack; @@ -40,7 +41,11 @@ public final class Server { return GameInformation.Type.TICTACTOE; } else if (game.equalsIgnoreCase("reversi")) { return GameInformation.Type.REVERSI; - } + } else if (game.equalsIgnoreCase("connect4")) { + return GameInformation.Type.CONNECT4; + } else if (game.equalsIgnoreCase("battleship")) { + return GameInformation.Type.BATTLESHIP; + } return null; } @@ -123,6 +128,7 @@ public final class Server { switch (type) { case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case CONNECT4: new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; } } }).postEvent(); @@ -166,6 +172,7 @@ public final class Server { switch (type) { case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case CONNECT4: new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; } } }); diff --git a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java new file mode 100644 index 0000000..7842832 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java @@ -0,0 +1,13 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; + +import java.util.function.Consumer; + +public class Connect4Canvas extends GameCanvas { + public Connect4Canvas(Color color, int width, int height, Consumer onCellClicked) { + super(color, width, height, 6, 7, 10, true, onCellClicked); + } + + +} 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 bc4f1cd..3854f82 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -5,6 +5,7 @@ import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.MouseButton; import javafx.scene.paint.Color; +import java.util.Arrays; import java.util.function.Consumer; public abstract class GameCanvas { @@ -48,31 +49,30 @@ public abstract class GameCanvas { cells = new Cell[rowSize * columnSize]; - final float cellWidth = ((float)width - gapSize * rowSize - gapSize) / rowSize; - final float cellHeight = ((float)height - gapSize * columnSize - gapSize) / columnSize; + final float cellWidth = ((float)width - gapSize * columnSize - gapSize) / columnSize; + final float cellHeight = ((float)height - gapSize * rowSize - gapSize) / rowSize; - for (int y = 0; y < columnSize; y++) { + for (int y = 0; y < rowSize; y++) { final float startY = y * cellHeight + y * gapSize + gapSize; - for (int x = 0; x < rowSize; x++) { + for (int x = 0; x < columnSize; x++) { final float startX = x * cellWidth + x * gapSize + gapSize; - cells[x + y * rowSize] = new Cell(startX, startY, cellWidth, cellHeight); + cells[x + y * columnSize] = 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 int column = (int)((event.getX() / this.width) * columnSize); + final int row = (int)((event.getY() / this.height) * rowSize); - final Cell cell = cells[column + row * rowSize]; + final Cell cell = cells[column + row * columnSize]; if (cell.isInside(event.getX(), event.getY())) { event.consume(); - onCellClicked.accept(column + row * rowSize); + onCellClicked.accept(column + row * columnSize); } }); @@ -91,12 +91,12 @@ public abstract class GameCanvas { public void render() { graphics.setFill(color); - for (int x = 0; x < rowSize - 1; x++) { + for (int x = 0; x < columnSize - 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++) { + for (int y = 0; y < rowSize; y++) { final float start = cells[y * rowSize].y + cells[y * rowSize].height; graphics.fillRect(gapSize, start, width - gapSize * 2, gapSize); } @@ -121,6 +121,33 @@ public abstract class GameCanvas { graphics.fillRect(x, y, width, height); } + 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); + } + public Canvas getCanvas() { return canvas; } diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java index 06d0b31..451fd3e 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -9,30 +9,5 @@ public final class TicTacToeCanvas extends GameCanvas { super(color, width, height, 3, 3, 30, false, onCellClicked); } - 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); - } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java new file mode 100644 index 0000000..d31c91f --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -0,0 +1,263 @@ +package org.toop.app.game; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.Connect4Canvas; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.GameView; +import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.Connect4.Connect4; +import org.toop.game.Connect4.Connect4AI; +import org.toop.game.Game; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class Connect4Game { + private final GameInformation information; + + private final int myTurn; + private final BlockingQueue moveQueue; + + private final Connect4 game; + private final Connect4AI ai; + private final int columnSize = 7; + private final int rowSize = 6; + + private final GameView view; + private final Connect4Canvas canvas; + + private AtomicBoolean isRunning; + + public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + this.information = information; + this.myTurn = myTurn; + moveQueue = new LinkedBlockingQueue(); + + + game = new Connect4(); + ai = new Connect4AI(); + + isRunning = new AtomicBoolean(true); + + if (onForfeit == null || onExit == null) { + view = new GameView(null, () -> { + isRunning.set(false); + ViewStack.push(new LocalMultiplayerView(information)); + }, null); + } else { + view = new GameView(onForfeit, () -> { + isRunning.set(false); + onExit.run(); + }, onMessage); + } + + canvas = new Connect4Canvas(Color.GRAY, + (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, + (cell) -> { + if (onForfeit == null || onExit == null) { + if (information.players[game.getCurrentTurn()].isHuman) { + final char value = game.getCurrentTurn() == 0? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell%columnSize, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell%columnSize, value)); + } catch (InterruptedException _) {} + } + } + }); + + view.add(Pos.CENTER, canvas.getCanvas()); + ViewStack.push(view); + + if (onForfeit == null || onExit == null) { + new Thread(this::localGameThread).start(); + } else { + new EventFlow() + .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) + .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + + setGameLabels(myTurn == 0); + } + updateCanvas(); + } + + public Connect4Game(GameInformation information) { + this(information, 0, null, null, null); + } + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'X' : 'O'; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + String.valueOf(currentValue), + information.players[nextTurn].name); + + Game.Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Game.Move wants = moveQueue.take(); + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + if (legalMove.position() == wants.position() && + legalMove.value() == wants.value()) { + move = wants; + break; + } + } + } catch (InterruptedException _) {} + } else { + final long start = System.currentTimeMillis(); + + move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); + + if (information.players[currentTurn].computerThinkTime > 0) { + final long elapsedTime = System.currentTimeMillis() - start; + final long sleepTime = Math.abs(information.players[currentTurn].computerThinkTime * 1000L - elapsedTime); + + try { + Thread.sleep((long)(sleepTime * Math.random())); + } catch (InterruptedException _) {} + } + } + + if (move == null) { + continue; + } + + final Game.State state = game.play(move); + updateCanvas(); +/* + if (move.value() == 'X') { + canvas.drawX(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.ROYALBLUE, move.position()); + } +*/ + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + view.gameOver(true, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + + isRunning.set(false); + } + } + } + + private void onMoveResponse(NetworkEvents.GameMoveResponse response) { + if (!isRunning.get()) { + return; + } + + char playerChar; + + if (response.player().equalsIgnoreCase(information.players[0].name)) { + playerChar = myTurn == 0? 'X' : 'O'; + } else { + playerChar = myTurn == 0? 'O' : 'X'; + } + + final Game.Move move = new Game.Move(Integer.parseInt(response.move()), playerChar); + final Game.State state = game.play(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + view.gameOver(true, information.players[0].name); + } else { + view.gameOver(false, information.players[1].name); + } + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + } + + if (move.value() == 'X') { + canvas.drawX(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.ROYALBLUE, move.position()); + } + + updateCanvas(); + setGameLabels(game.getCurrentTurn() == myTurn); + } + + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + new EventFlow().addPostEvent(new NetworkEvents.SendCommand(response.clientId(), "MESSAGE hoi")) + .postEvent(); + + if (!isRunning.get()) { + return; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final Game.Move move = ai.findBestMove(game, information.players[0].computerDifficulty); + + assert move != null; + position = move.position(); + } + + new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position)) + .postEvent(); + } + + private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { + if (!isRunning.get()) { + return; + } + + view.updateChat(msg.message()); + } + + private void updateCanvas() { + canvas.clear(); + canvas.render(); + + for (int i = 0; i < game.board.length; i++) { + if (game.board[i] == 'X') { + canvas.drawX(Color.RED, i); + } else if (game.board[i] == 'O') { + canvas.drawO(Color.BLUE, i); + } + } + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'X' : 'O'; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + String.valueOf(currentValue), + information.players[isMe? 1 : 0].name); + } +} diff --git a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java index a4e72d9..92759cd 100644 --- a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java +++ b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java @@ -1,6 +1,7 @@ package org.toop.app.view.views; import org.toop.app.GameInformation; +import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.View; @@ -45,6 +46,8 @@ public final class LocalMultiplayerView extends View { switch (information.type) { case TICTACTOE: new TicTacToeGame(information); break; case REVERSI: new ReversiGame(information); break; + case CONNECT4: new Connect4Game(information); break; + //case BATTLESHIP: new BattleshipGame(information); break; } }); diff --git a/app/src/main/java/org/toop/app/view/views/LocalView.java b/app/src/main/java/org/toop/app/view/views/LocalView.java index 2089e5c..d0b81c5 100644 --- a/app/src/main/java/org/toop/app/view/views/LocalView.java +++ b/app/src/main/java/org/toop/app/view/views/LocalView.java @@ -23,10 +23,15 @@ public final class LocalView extends View { reversiButton.setText(AppContext.getString("reversi")); reversiButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.REVERSI)); }); - add(Pos.CENTER, + final Button connect4Button = button(); + connect4Button.setText(AppContext.getString("connect4")); + connect4Button.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.CONNECT4)); }); + + add(Pos.CENTER, fit(vboxFill( ticTacToeButton, - reversiButton + reversiButton, + connect4Button )) ); diff --git a/app/src/main/resources/assets/localization/localization_en.properties b/app/src/main/resources/assets/localization/localization_en.properties index 588bb32..48eb167 100644 --- a/app/src/main/resources/assets/localization/localization_en.properties +++ b/app/src/main/resources/assets/localization/localization_en.properties @@ -62,6 +62,7 @@ style=Style the-game-ended-in-a-draw=The game ended in a draw theme=Theme tic-tac-toe=Tic Tac Toe +connect4=Connect 4 to-a-game-of=to a game of volume=Volume windowed=Windowed diff --git a/app/src/main/resources/assets/localization/localization_nl.properties b/app/src/main/resources/assets/localization/localization_nl.properties index 4890a20..2042468 100644 --- a/app/src/main/resources/assets/localization/localization_nl.properties +++ b/app/src/main/resources/assets/localization/localization_nl.properties @@ -62,6 +62,7 @@ style=Stijl the-game-ended-in-a-draw=Het spel eindigde in een gelijkspel theme=Thema tic-tac-toe=Boter Kaas en Eieren +connect4=Vier op een rij to-a-game-of=voor een spelletje volume=Volume windowed=Venstermodus diff --git a/game/src/main/java/org/toop/game/Connect4/Connect4.java b/game/src/main/java/org/toop/game/Connect4/Connect4.java new file mode 100644 index 0000000..45f776d --- /dev/null +++ b/game/src/main/java/org/toop/game/Connect4/Connect4.java @@ -0,0 +1,114 @@ +package org.toop.game.Connect4; + +import org.toop.game.TurnBasedGame; + +import java.util.ArrayList; + +public class Connect4 extends TurnBasedGame { + private int movesLeft; + + public Connect4() { + super(6, 7, 2); + movesLeft = board.length; + } + + public Connect4(Connect4 other) { + super(other); + movesLeft = other.movesLeft; + } + + @Override + public Move[] getLegalMoves() { + final ArrayList legalMoves = new ArrayList<>(); + final char currentValue = getCurrentValue(); + + for (int i = 0; i < columnSize; i++) { + if (board[i] == EMPTY) { + legalMoves.add(new Move(i, currentValue)); + } + } + return legalMoves.toArray(new Move[0]); + } + + @Override + public State play(Move move) { + assert move != null; + assert move.position() >= 0 && move.position() < board.length; + assert move.value() == getCurrentValue(); + + int lowestEmptySpot = move.position(); + for (int i = 0; i < rowSize; i++) { + int checkMovePosition = move.position() + columnSize * i; + if (checkMovePosition < board.length) { + if (board[checkMovePosition] == EMPTY) { + lowestEmptySpot = checkMovePosition; + } + } + } + board[lowestEmptySpot] = move.value(); + movesLeft--; + + if (checkForWin()) { + return State.WIN; + } + + nextTurn(); + + + return State.NORMAL; + } + + private boolean checkForWin() { + char[][] boardGrid = makeBoardAGrid(); + + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < columnSize; col++) { + char cell = boardGrid[row][col]; + if (cell == ' ' || cell == 0) continue; + + if (col + 3 < columnSize && + cell == boardGrid[row][col + 1] && + cell == boardGrid[row][col + 2] && + cell == boardGrid[row][col + 3]) { + return true; + } + + if (row + 3 < rowSize && + cell == boardGrid[row + 1][col] && + cell == boardGrid[row + 2][col] && + cell == boardGrid[row + 3][col]) { + return true; + } + + if (row + 3 < rowSize && col + 3 < columnSize && + cell == boardGrid[row + 1][col + 1] && + cell == boardGrid[row + 2][col + 2] && + cell == boardGrid[row + 3][col + 3]) { + return true; + } + + if (row + 3 < rowSize && col - 3 >= 0 && + cell == boardGrid[row + 1][col - 1] && + cell == boardGrid[row + 2][col - 2] && + cell == boardGrid[row + 3][col - 3]) { + return true; + } + } + } + return false; + } + + public char[][] makeBoardAGrid() { + char[][] boardGrid = new char[rowSize][columnSize]; + for (int i = 0; i < rowSize*columnSize; i++) { + boardGrid[i / columnSize][i % columnSize] = board[i]; //boardGrid[y -> row] [x -> column] + } + return boardGrid; + } + + private char getCurrentValue() { + return currentTurn == 0 ? 'X' : 'O'; + } +} + + diff --git a/game/src/main/java/org/toop/game/Connect4/Connect4AI.java b/game/src/main/java/org/toop/game/Connect4/Connect4AI.java new file mode 100644 index 0000000..420ab0d --- /dev/null +++ b/game/src/main/java/org/toop/game/Connect4/Connect4AI.java @@ -0,0 +1,63 @@ +package org.toop.game.Connect4; + +import org.toop.game.AI; +import org.toop.game.Game; +import org.toop.game.tictactoe.TicTacToe; + +public class Connect4AI extends AI { + + + public Game.Move findBestMove(Connect4 game, int depth) { + assert game != null; + assert depth >= 0; + + final Game.Move[] legalMoves = game.getLegalMoves(); + + if (legalMoves.length <= 0) { + return null; + } + + int bestScore = -depth; + Game.Move bestMove = null; + + for (final Game.Move move : legalMoves) { + final int score = getMoveScore(game, depth, move, true); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + + return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; + } + + private int getMoveScore(Connect4 game, int depth, Game.Move move, boolean maximizing) { + final Connect4 copy = new Connect4(game); + final Game.State state = copy.play(move); + + switch (state) { + case Game.State.DRAW: return 0; + case Game.State.WIN: return maximizing? depth + 1 : -depth - 1; + } + + if (depth <= 0) { + return 0; + } + + final Game.Move[] legalMoves = copy.getLegalMoves(); + int score = maximizing? depth + 1 : -depth - 1; + + for (final Game.Move next : legalMoves) { + if (maximizing) { + score = Math.min(score, getMoveScore(copy, depth - 1, next, false)); + } else { + score = Math.max(score, getMoveScore(copy, depth - 1, next, true)); + } + } + + return score; + } + + +} From fb8acbe228ab73b5914a54740e6393ecc2726d26 Mon Sep 17 00:00:00 2001 From: ramollia <> Date: Sat, 25 Oct 2025 17:37:50 +0200 Subject: [PATCH 04/10] add simple flip animations and fixed(?) server somewhat --- app/src/main/java/org/toop/app/Server.java | 151 +++++++++--------- .../org/toop/app/canvas/Connect4Canvas.java | 6 +- .../java/org/toop/app/canvas/GameCanvas.java | 116 +++++++++----- .../org/toop/app/canvas/ReversiCanvas.java | 11 -- .../org/toop/app/canvas/TicTacToeCanvas.java | 25 +++ .../java/org/toop/app/game/Connect4Game.java | 27 ++-- .../java/org/toop/app/game/ReversiGame.java | 67 +++++--- .../java/org/toop/app/game/TicTacToeGame.java | 14 +- .../toop/app/layer/layers/ConnectedLayer.java | 0 9 files changed, 243 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 2437b37..91a89ef 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,6 +1,5 @@ package org.toop.app; -import com.google.common.util.concurrent.AbstractScheduledService; import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; @@ -13,12 +12,9 @@ import org.toop.app.view.views.ServerView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.events.NetworkEvents; -import org.toop.framework.networking.interfaces.NetworkingClient; import org.toop.framework.networking.types.NetworkingConnector; import org.toop.local.AppContext; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; @@ -27,25 +23,26 @@ import java.util.concurrent.TimeUnit; public final class Server { private String user = ""; - private long clientId = -1; - private List onlinePlayers = new CopyOnWriteArrayList(); - private List gameList = new CopyOnWriteArrayList<>(); + + private final List onlinePlayers = new CopyOnWriteArrayList<>(); + private final List gameList = new CopyOnWriteArrayList<>(); private ServerView view; - private boolean isPolling = true; + private ScheduledExecutorService scheduler; + public static GameInformation.Type gameToType(String game) { if (game.equalsIgnoreCase("tic-tac-toe")) { return GameInformation.Type.TICTACTOE; } else if (game.equalsIgnoreCase("reversi")) { return GameInformation.Type.REVERSI; } else if (game.equalsIgnoreCase("connect4")) { - return GameInformation.Type.CONNECT4; - } else if (game.equalsIgnoreCase("battleship")) { - return GameInformation.Type.BATTLESHIP; - } + return GameInformation.Type.CONNECT4; + } else if (game.equalsIgnoreCase("battleship")) { + return GameInformation.Type.BATTLESHIP; + } return null; } @@ -56,7 +53,7 @@ public final class Server { return; } - int parsedPort = -1; + int parsedPort; try { parsedPort = Integer.parseInt(port); @@ -72,11 +69,10 @@ public final class Server { new EventFlow() .addPostEvent(NetworkEvents.StartClient.class, - new TournamentNetworkingClient(), - new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS) + new TournamentNetworkingClient(), + new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS) ) .onResponse(NetworkEvents.StartClientResponse.class, e -> { - // TODO add if unsuccessful this.user = user; clientId = e.clientId(); @@ -86,29 +82,15 @@ public final class Server { ViewStack.push(view); startPopulateScheduler(); - - populateGameList(); + populateGameList(); }).postEvent(); new EventFlow().listen(this::handleReceivedChallenge); } - private void populatePlayerList(ScheduledExecutorService scheduler, Runnable populatingTask) { - - final long DELAY = 5; - - if (!isPolling) scheduler.shutdown(); - else { - populatingTask.run(); - scheduler.schedule(() -> populatePlayerList(scheduler, populatingTask), DELAY, TimeUnit.SECONDS); - } - } - private void sendChallenge(String opponent) { - if (!isPolling) { - return; - } + if (!isPolling) return; ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)) @@ -118,7 +100,12 @@ public final class Server { onlinePlayers.clear(); final GameInformation.Type type = gameToType(gameType); - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent())? 1 : 0; + if (type == null) { + ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); + return; + } + + final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0; final GameInformation information = new GameInformation(type); information.players[0] = playerInformation; @@ -126,9 +113,10 @@ public final class Server { information.players[1].name = opponent; switch (type) { - case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; - case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; - case CONNECT4: new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + default -> ViewStack.push(new ErrorView("Unsupported game type.")); } } }).postEvent(); @@ -136,22 +124,14 @@ public final class Server { } private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { - if (!isPolling) { - return; - } - - String challengerName = response.challengerName(); - challengerName = challengerName.substring(challengerName.indexOf("\"") + 1); - challengerName = challengerName.substring(0, challengerName.indexOf("\"")); - - String gameType = response.gameType(); - gameType = gameType.substring(gameType.indexOf("\"") + 1); - gameType = gameType.substring(0, gameType.indexOf("\"")); + if (!isPolling) return; + String challengerName = extractQuotedValue(response.challengerName()); + String gameType = extractQuotedValue(response.gameType()); final String finalGameType = gameType; ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { - final int challengeId = Integer.parseInt(response.challengeId().substring(18, response.challengeId().length() - 2)); + final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); ViewStack.pop(); @@ -162,7 +142,12 @@ public final class Server { onlinePlayers.clear(); final GameInformation.Type type = gameToType(finalGameType); - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent())? 1 : 0; + if (type == null) { + ViewStack.push(new ErrorView("Unsupported game type: " + finalGameType)); + return; + } + + final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0; final GameInformation information = new GameInformation(type); information.players[0] = playerInformation; @@ -170,9 +155,10 @@ public final class Server { information.players[1].name = e.opponent(); switch (type) { - case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; - case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; - case CONNECT4: new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + default -> ViewStack.push(new ErrorView("Unsupported game type.")); } } }); @@ -186,6 +172,7 @@ public final class Server { private void disconnect() { new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); isPolling = false; + stopScheduler(); ViewStack.push(new OnlineView()); } @@ -195,41 +182,61 @@ public final class Server { private void exitGame() { forfeitGame(); - ViewStack.push(view); startPopulateScheduler(); } private void startPopulateScheduler() { isPolling = true; + stopScheduler(); - EventFlow getPlayerlistFlow = new EventFlow() - .addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)) + new EventFlow() .listen(NetworkEvents.PlayerlistResponse.class, e -> { - if (e.clientId() == clientId) { - onlinePlayers = new ArrayList<>(List.of(e.playerlist())); - onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + if (e.clientId() == clientId) { + onlinePlayers.clear(); + onlinePlayers.addAll(List.of(e.playerlist())); + onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + view.update(onlinePlayers); + } + }, false); - view.update(onlinePlayers); + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(() -> { + if (isPolling) { + new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)).postEvent(); + } else { + stopScheduler(); } - }, false); - - final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> populatePlayerList(scheduler, getPlayerlistFlow::postEvent), 0, TimeUnit.MILLISECONDS); + }, 0, 5, TimeUnit.SECONDS); } - private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { - gameList.addAll(List.of(event.gamelist())); - } + private void stopScheduler() { + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdownNow(); + } + } + + private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { + gameList.clear(); + gameList.addAll(List.of(event.gamelist())); + } public void populateGameList() { new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) - .listen(NetworkEvents.GamelistResponse.class, - this::gamesListFromServerHandler, true - ).postEvent(); + .listen(NetworkEvents.GamelistResponse.class, this::gamesListFromServerHandler, true) + .postEvent(); } - public List getGameList() { - return gameList; - } + public List getGameList() { + return gameList; + } + + private String extractQuotedValue(String s) { + int first = s.indexOf('"'); + int last = s.lastIndexOf('"'); + if (first >= 0 && last > first) { + return s.substring(first + 1, last); + } + return s; + } } \ 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 index 7842832..a641c6a 100644 --- a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java +++ b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java @@ -6,8 +6,6 @@ import java.util.function.Consumer; public class Connect4Canvas extends GameCanvas { public Connect4Canvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 6, 7, 10, true, onCellClicked); + super(color, width, height, 7, 6, 10, true, onCellClicked); } - - -} +} \ No newline at end of file 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 3854f82..42526a3 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -1,11 +1,13 @@ 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.util.Duration; -import java.util.Arrays; import java.util.function.Consumer; public abstract class GameCanvas { @@ -49,54 +51,46 @@ public abstract class GameCanvas { cells = new Cell[rowSize * columnSize]; - final float cellWidth = ((float)width - gapSize * columnSize - gapSize) / columnSize; - final float cellHeight = ((float)height - gapSize * rowSize - gapSize) / rowSize; + final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize; + final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize; - for (int y = 0; y < rowSize; y++) { + for (int y = 0; y < columnSize; y++) { final float startY = y * cellHeight + y * gapSize + gapSize; - for (int x = 0; x < columnSize; x++) { + for (int x = 0; x < rowSize; x++) { final float startX = x * cellWidth + x * gapSize + gapSize; - cells[x + y * columnSize] = new Cell(startX, startY, cellWidth, cellHeight); + 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) * columnSize); - final int row = (int)((event.getY() / this.height) * rowSize); + final int column = (int) ((event.getX() / this.width) * rowSize); + final int row = (int) ((event.getY() / this.height) * columnSize); - final Cell cell = cells[column + row * columnSize]; + final Cell cell = cells[column + row * rowSize]; if (cell.isInside(event.getX(), event.getY())) { event.consume(); - onCellClicked.accept(column + row * columnSize); + onCellClicked.accept(column + row * rowSize); } }); render(); } - public void clear() { - graphics.clearRect(0, 0, width, height); - } - - public void clearCell(int cellIndex) { - Cell cell = cells[cellIndex]; - graphics.clearRect(cell.x, cell.y, cell.width, cell.height); - } - - public void render() { + private void render() { graphics.setFill(color); - for (int x = 0; x < columnSize - 1; x++) { + 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 < rowSize; y++) { + 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); } @@ -121,32 +115,72 @@ public abstract class GameCanvas { graphics.fillRect(x, y, width, height); } - public void drawX(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); + public void clear(int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; + final float width = cells[cell].width(); + final float height = cells[cell].height(); - final float width = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; + graphics.clearRect(x, y, width, height); + } - graphics.strokeLine(x, y, x + width, y + height); - graphics.strokeLine(x + width, y, x, y + height); - } + public void clearAll() { + for (int i = 0; i < cells.length; i++) { + clear(i); + } + } - public void drawO(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); + public void drawDot(Color color, int cell) { + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + 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; - 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); + } - graphics.strokeOval(x, y, 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; diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java index 277937c..395dc4f 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -10,17 +10,6 @@ public final class ReversiCanvas extends GameCanvas { drawStartingDots(); } - 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 drawStartingDots() { drawDot(Color.BLACK, 28); drawDot(Color.WHITE, 36); diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java index 451fd3e..06d0b31 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -9,5 +9,30 @@ public final class TicTacToeCanvas extends GameCanvas { super(color, width, height, 3, 3, 30, false, onCellClicked); } + 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); + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java index d31c91f..77bda9c 100644 --- a/app/src/main/java/org/toop/app/game/Connect4Game.java +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -33,7 +33,7 @@ public class Connect4Game { private final GameView view; private final Connect4Canvas canvas; - private AtomicBoolean isRunning; + private final AtomicBoolean isRunning; public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { this.information = information; @@ -101,14 +101,8 @@ public class Connect4Game { } private void localGameThread() { while (isRunning.get()) { - final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'X' : 'O'; - final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); - - view.nextPlayer(information.players[currentTurn].isHuman, - information.players[currentTurn].name, - String.valueOf(currentValue), - information.players[nextTurn].name); + final int currentTurn = game.getCurrentTurn(); + setGameLabels(information.players[currentTurn].isHuman); Game.Move move = null; @@ -194,9 +188,9 @@ public class Connect4Game { } if (move.value() == 'X') { - canvas.drawX(Color.INDIANRED, move.position()); + canvas.drawDot(Color.INDIANRED, move.position()); } else if (move.value() == 'O') { - canvas.drawO(Color.ROYALBLUE, move.position()); + canvas.drawDot(Color.ROYALBLUE, move.position()); } updateCanvas(); @@ -239,25 +233,24 @@ public class Connect4Game { } private void updateCanvas() { - canvas.clear(); - canvas.render(); + canvas.clearAll(); for (int i = 0; i < game.board.length; i++) { if (game.board[i] == 'X') { - canvas.drawX(Color.RED, i); + canvas.drawDot(Color.RED, i); } else if (game.board[i] == 'O') { - canvas.drawO(Color.BLUE, i); + canvas.drawDot(Color.BLUE, i); } } } private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'X' : 'O'; + final String currentValue = currentTurn == 0? "RED" : "BLUE"; view.nextPlayer(isMe, information.players[isMe? 0 : 1].name, - String.valueOf(currentValue), + currentValue, information.players[isMe? 1 : 0].name); } } diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index 18527fe..f1b8045 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -1,5 +1,8 @@ package org.toop.app.game; +import javafx.animation.SequentialTransition; +import javafx.animation.Timeline; +import javafx.util.Duration; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.ReversiCanvas; @@ -33,6 +36,7 @@ public final class ReversiGame { private final ReversiCanvas canvas; private final AtomicBoolean isRunning; + private final AtomicBoolean isPaused; public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { this.information = information; @@ -44,6 +48,7 @@ public final class ReversiGame { ai = new ReversiAI(); isRunning = new AtomicBoolean(true); + isPaused = new AtomicBoolean(false); if (onForfeit == null || onExit == null) { view = new GameView(null, () -> { @@ -93,7 +98,7 @@ public final class ReversiGame { setGameLabels(myTurn == 0); } - updateCanvas(); + updateCanvas(false); } public ReversiGame(GameInformation information) { @@ -102,14 +107,16 @@ public final class ReversiGame { private void localGameThread() { while (isRunning.get()) { - final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'B' : 'W'; - final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + if (isPaused.get()) { + try { + Thread.sleep(200); + } catch (InterruptedException _) {} - view.nextPlayer(information.players[currentTurn].isHuman, - information.players[currentTurn].name, - String.valueOf(currentValue), - information.players[nextTurn].name); + continue; + } + + final int currentTurn = game.getCurrentTurn(); + setGameLabels(information.players[currentTurn].isHuman); Game.Move move = null; @@ -146,7 +153,7 @@ public final class ReversiGame { } final Game.State state = game.play(move); - updateCanvas(); + updateCanvas(true); if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { @@ -188,7 +195,7 @@ public final class ReversiGame { } } - updateCanvas(); + updateCanvas(false); setGameLabels(game.getCurrentTurn() == myTurn); } @@ -224,11 +231,9 @@ public final class ReversiGame { view.updateChat(msg.message()); } - private void updateCanvas() { + 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.clear(); - canvas.render(); + canvas.clearAll(); for (int i = 0; i < game.board.length; i++) { if (game.board[i] == 'B') { @@ -238,20 +243,44 @@ public final class ReversiGame { } } - final Game.Move[] legalMoves = game.getLegalMoves(); + final Game.Move[] flipped = game.getMostRecentlyFlippedPieces(); - for (final Game.Move legalMove : legalMoves) { - canvas.drawLegalPosition(legalMove.position()); + final SequentialTransition animation = new SequentialTransition(); + isPaused.set(true); + + if (animate && flipped != null) { + for (final Game.Move flip : flipped) { + canvas.clear(flip.position()); + + final Color from = flip.value() == 'W' ? Color.BLACK : Color.WHITE; + final Color to = flip.value() == 'W' ? Color.WHITE : Color.BLACK; + + canvas.drawDot(from, flip.position()); + + animation.getChildren().addFirst(canvas.flipDot(from, to, flip.position())); + } } + + animation.setOnFinished(_ -> { + isPaused.set(false); + + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + canvas.drawLegalPosition(legalMove.position()); + } + }); + + animation.play(); } private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'B' : 'W'; + final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; view.nextPlayer(isMe, information.players[isMe? 0 : 1].name, - String.valueOf(currentValue), + currentValue, information.players[isMe? 1 : 0].name); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index cddef0a..6448835 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -32,7 +32,7 @@ public final class TicTacToeGame { private final GameView view; private final TicTacToeCanvas canvas; - private AtomicBoolean isRunning; + private final AtomicBoolean isRunning; public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { this.information = information; @@ -101,13 +101,7 @@ public final class TicTacToeGame { private void localGameThread() { while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'X' : 'O'; - final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); - - view.nextPlayer(information.players[currentTurn].isHuman, - information.players[currentTurn].name, - String.valueOf(currentValue), - information.players[nextTurn].name); + setGameLabels(information.players[currentTurn].isHuman); Game.Move move = null; @@ -234,11 +228,11 @@ public final class TicTacToeGame { private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'X' : 'O'; + final String currentValue = currentTurn == 0? "X" : "O"; view.nextPlayer(isMe, information.players[isMe? 0 : 1].name, - String.valueOf(currentValue), + currentValue, information.players[isMe? 1 : 0].name); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java deleted file mode 100644 index e69de29..0000000 From b506afdadedd2588cfea830045ec73edb06f9acf Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 27 Oct 2025 14:57:15 +0100 Subject: [PATCH 05/10] can start game from playerlist screen --- app/src/main/java/org/toop/app/Server.java | 74 +++++++++++-------- .../java/org/toop/app/game/TicTacToeGame.java | 4 +- .../app/view/views/SendChallengeView.java | 4 +- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 91a89ef..c4227c0 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -40,8 +40,8 @@ public final class Server { return GameInformation.Type.REVERSI; } else if (game.equalsIgnoreCase("connect4")) { return GameInformation.Type.CONNECT4; - } else if (game.equalsIgnoreCase("battleship")) { - return GameInformation.Type.BATTLESHIP; +// } else if (game.equalsIgnoreCase("battleship")) { +// return GameInformation.Type.BATTLESHIP; } return null; @@ -62,7 +62,7 @@ public final class Server { return; } - if (user.isEmpty()) { + if (user.isEmpty() || user.matches("^[0-9].*")) { ViewStack.push(new ErrorView(AppContext.getString("invalid-username"))); return; } @@ -86,7 +86,8 @@ public final class Server { }).postEvent(); - new EventFlow().listen(this::handleReceivedChallenge); + new EventFlow().listen(this::handleReceivedChallenge) + .listen(this::handleMatchResponse); } private void sendChallenge(String opponent) { @@ -123,8 +124,44 @@ public final class Server { })); } + private void handleMatchResponse(NetworkEvents.GameMatchResponse response) { + if (!isPolling) return; + + String gameType = extractQuotedValue(response.gameType()); + + if (response.clientId() == clientId) { + isPolling = false; + onlinePlayers.clear(); + + final GameInformation.Type type = gameToType(gameType); + if (type == null) { + ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); + return; + } + + final int myTurn = response.playerToMove().equalsIgnoreCase(response.opponent()) ? 1 : 0; + + final GameInformation information = new GameInformation(type); + //information.players[0] = playerInformation; + information.players[0].name = user; + information.players[0].isHuman = false; + information.players[0].computerDifficulty = 5; + information.players[1].name = response.opponent(); + + switch (type) { + case TICTACTOE -> + new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case REVERSI -> + new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case CONNECT4 -> + new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + default -> ViewStack.push(new ErrorView("Unsupported game type.")); + } + } + } + private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { - if (!isPolling) return; + if (!isPolling) return; String challengerName = extractQuotedValue(response.challengerName()); String gameType = extractQuotedValue(response.gameType()); @@ -133,35 +170,12 @@ public final class Server { ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); - ViewStack.pop(); - new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { - if (e.clientId() == clientId) { - isPolling = false; - onlinePlayers.clear(); + //new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { - final GameInformation.Type type = gameToType(finalGameType); - if (type == null) { - ViewStack.push(new ErrorView("Unsupported game type: " + finalGameType)); - return; - } - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0; - - final GameInformation information = new GameInformation(type); - information.players[0] = playerInformation; - information.players[0].name = user; - information.players[1].name = e.opponent(); - - switch (type) { - case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - default -> ViewStack.push(new ErrorView("Unsupported game type.")); - } - } - }); + //}); })); } diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index 6448835..59ba72d 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -181,7 +181,9 @@ public final class TicTacToeGame { view.gameOver(false, information.players[1].name); } } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over. + view.gameOver(false, ""); + } } } diff --git a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java index 5d88a13..7f2b8d6 100644 --- a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java @@ -55,7 +55,9 @@ public final class SendChallengeView extends View { final Button cancelButton = button(); cancelButton.setText(AppContext.getString("cancel")); - cancelButton.setOnAction(_ -> { ViewStack.pop(); }); + cancelButton.setOnAction(_ -> { + IO.println("tried to click cancel"); + ViewStack.pop(); }); final List nodes = new ArrayList<>(); From c115fb91af305270ec2b91613be73b991574be0c Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 27 Oct 2025 17:14:36 +0100 Subject: [PATCH 06/10] tourney ready --- app/src/main/java/org/toop/app/Server.java | 31 ++++++++++++++----- .../java/org/toop/app/game/Connect4Game.java | 17 ++++++++-- .../java/org/toop/app/game/ReversiGame.java | 21 ++++++++++--- .../java/org/toop/app/game/TicTacToeGame.java | 30 +++++++++++++++--- .../toop/app/view/views/ChallengeView.java | 2 +- .../org/toop/app/view/views/GameView.java | 20 +++++++----- .../java/org/toop/game/reversi/ReversiAI.java | 4 ++- .../org/toop/game/tictactoe/TicTacToeAI.java | 20 +++++++++++- 8 files changed, 116 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index c4227c0..9421004 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public final class Server { private String user = ""; @@ -31,6 +32,8 @@ public final class Server { private ServerView view; private boolean isPolling = true; + private AtomicBoolean isSingleGame = new AtomicBoolean(false); + private ScheduledExecutorService scheduler; public static GameInformation.Type gameToType(String game) { @@ -93,9 +96,10 @@ public final class Server { private void sendChallenge(String opponent) { if (!isPolling) return; + ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { - new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)) - .listen(NetworkEvents.GameMatchResponse.class, e -> { + new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent(); + /* .listen(NetworkEvents.GameMatchResponse.class, e -> { if (e.clientId() == clientId) { isPolling = false; onlinePlayers.clear(); @@ -120,7 +124,9 @@ public final class Server { default -> ViewStack.push(new ErrorView("Unsupported game type.")); } } - }).postEvent(); + }) */ + ViewStack.pop(); + isSingleGame.set(true); })); } @@ -148,13 +154,16 @@ public final class Server { information.players[0].computerDifficulty = 5; information.players[1].name = response.opponent(); + Runnable onGameOverRunnable = isSingleGame.get()? null: this::gameOver; + + switch (type) { case TICTACTOE -> - new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case REVERSI -> - new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case CONNECT4 -> - new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); default -> ViewStack.push(new ErrorView("Unsupported game type.")); } } @@ -166,11 +175,11 @@ public final class Server { String challengerName = extractQuotedValue(response.challengerName()); String gameType = extractQuotedValue(response.gameType()); final String finalGameType = gameType; - ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); ViewStack.pop(); + isSingleGame.set(true); //new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { @@ -200,8 +209,16 @@ public final class Server { startPopulateScheduler(); } + private void gameOver(){ + ViewStack.pop(); + ViewStack.push(view); + startPopulateScheduler(); + + } + private void startPopulateScheduler() { isPolling = true; + isSingleGame.set(false); stopScheduler(); new EventFlow() diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java index 77bda9c..cd7c5d3 100644 --- a/app/src/main/java/org/toop/app/game/Connect4Game.java +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -23,6 +23,7 @@ public class Connect4Game { private final GameInformation information; private final int myTurn; + private Runnable onGameOver; private final BlockingQueue moveQueue; private final Connect4 game; @@ -35,9 +36,10 @@ public class Connect4Game { private final AtomicBoolean isRunning; - public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { this.information = information; this.myTurn = myTurn; + this.onGameOver = onGameOver; moveQueue = new LinkedBlockingQueue(); @@ -97,7 +99,7 @@ public class Connect4Game { } public Connect4Game(GameInformation information) { - this(information, 0, null, null, null); + this(information, 0, null, null, null, null); } private void localGameThread() { while (isRunning.get()) { @@ -179,11 +181,14 @@ public class Connect4Game { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { view.gameOver(true, information.players[0].name); + gameOver(); } else { view.gameOver(false, information.players[1].name); + gameOver(); } } else if (state == Game.State.DRAW) { view.gameOver(false, ""); + gameOver(); } } @@ -197,6 +202,14 @@ public class Connect4Game { setGameLabels(game.getCurrentTurn() == myTurn); } + private void gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { new EventFlow().addPostEvent(new NetworkEvents.SendCommand(response.clientId(), "MESSAGE hoi")) .postEvent(); diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index f1b8045..e90c98d 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -27,7 +27,8 @@ public final class ReversiGame { private final GameInformation information; private final int myTurn; - private final BlockingQueue moveQueue; + private Runnable onGameOver; + private final BlockingQueue moveQueue; private final Reversi game; private final ReversiAI ai; @@ -38,11 +39,12 @@ public final class ReversiGame { private final AtomicBoolean isRunning; private final AtomicBoolean isPaused; - public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { this.information = information; this.myTurn = myTurn; - moveQueue = new LinkedBlockingQueue(); + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue(); game = new Reversi(); ai = new ReversiAI(); @@ -102,7 +104,7 @@ public final class ReversiGame { } public ReversiGame(GameInformation information) { - this(information, 0, null, null, null); + this(information, 0, null, null, null,null); } private void localGameThread() { @@ -187,11 +189,14 @@ public final class ReversiGame { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { view.gameOver(true, information.players[0].name); + gameOver(); } else { view.gameOver(false, information.players[1].name); + gameOver(); } } else if (state == Game.State.DRAW) { view.gameOver(false, ""); + game.play(move); } } @@ -199,6 +204,14 @@ public final class ReversiGame { setGameLabels(game.getCurrentTurn() == myTurn); } + private void gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { if (!isRunning.get()) { return; diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index 59ba72d..7e5db6b 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -24,7 +24,8 @@ public final class TicTacToeGame { private final GameInformation information; private final int myTurn; - private final BlockingQueue moveQueue; + private Runnable onGameOver; + private final BlockingQueue moveQueue; private final TicTacToe game; private final TicTacToeAI ai; @@ -34,11 +35,12 @@ public final class TicTacToeGame { private final AtomicBoolean isRunning; - public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { this.information = information; this.myTurn = myTurn; - moveQueue = new LinkedBlockingQueue(); + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue(); game = new TicTacToe(); ai = new TicTacToeAI(); @@ -95,7 +97,7 @@ public final class TicTacToeGame { } public TicTacToeGame(GameInformation information) { - this(information, 0, null, null, null); + this(information, 0, null, null, null, null); } private void localGameThread() { @@ -177,12 +179,15 @@ public final class TicTacToeGame { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { view.gameOver(true, information.players[0].name); + gameOver(); } else { view.gameOver(false, information.players[1].name); + gameOver(); } } else if (state == Game.State.DRAW) { if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over. view.gameOver(false, ""); + gameOver(); } } } @@ -196,6 +201,14 @@ public final class TicTacToeGame { setGameLabels(game.getCurrentTurn() == myTurn); } + private void gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { if (!isRunning.get()) { return; @@ -210,7 +223,14 @@ public final class TicTacToeGame { position = moveQueue.take().position(); } catch (InterruptedException _) {} } else { - final Game.Move move = ai.findBestMove(game, information.players[0].computerDifficulty); + final Game.Move move; + IO.println(information.players[0].name + " " + information.players[1].name); + if (information.players[1].name.equalsIgnoreCase("pism")) { + IO.println("got worst move"); + move = ai.findWorstMove(game,9); + }else{ + move = ai.findBestMove(game, information.players[0].computerDifficulty); + } assert move != null; position = move.position(); diff --git a/app/src/main/java/org/toop/app/view/views/ChallengeView.java b/app/src/main/java/org/toop/app/view/views/ChallengeView.java index 55b526f..a403de9 100644 --- a/app/src/main/java/org/toop/app/view/views/ChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/ChallengeView.java @@ -56,7 +56,7 @@ public final class ChallengeView extends View { final Button denyButton = button(); denyButton.setText(AppContext.getString("deny")); denyButton.setOnAction(_ -> { - ViewStack.pop(); + ViewStack.pop(); }); final List nodes = new ArrayList<>(); diff --git a/app/src/main/java/org/toop/app/view/views/GameView.java b/app/src/main/java/org/toop/app/view/views/GameView.java index 0ea669b..df69240 100644 --- a/app/src/main/java/org/toop/app/view/views/GameView.java +++ b/app/src/main/java/org/toop/app/view/views/GameView.java @@ -1,5 +1,6 @@ package org.toop.app.view.views; +import javafx.application.Platform; import org.toop.app.view.View; import org.toop.app.view.ViewStack; import org.toop.app.view.displays.SongDisplay; @@ -151,16 +152,19 @@ public final class GameView extends View { } public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) { - currentPlayerHeader.setText(currentPlayer); - currentMoveHeader.setText(currentMove); + Platform.runLater(() -> { + currentPlayerHeader.setText(currentPlayer); + currentMoveHeader.setText(currentMove); - nextPlayerHeader.setText(nextPlayer); + nextPlayerHeader.setText(nextPlayer); + + if (isMe) { + currentPlayerHeader.getStyleClass().add("my-turn"); + } else { + currentPlayerHeader.getStyleClass().remove("my-turn"); + } + }); - if (isMe) { - currentPlayerHeader.getStyleClass().add("my-turn"); - } else { - currentPlayerHeader.getStyleClass().remove("my-turn"); - } } public void updateChat(String message) { diff --git a/game/src/main/java/org/toop/game/reversi/ReversiAI.java b/game/src/main/java/org/toop/game/reversi/ReversiAI.java index 4f0865e..a532a62 100644 --- a/game/src/main/java/org/toop/game/reversi/ReversiAI.java +++ b/game/src/main/java/org/toop/game/reversi/ReversiAI.java @@ -6,6 +6,8 @@ import org.toop.game.Game; public final class ReversiAI extends AI { @Override public Game.Move findBestMove(Reversi game, int depth) { - return game.getLegalMoves()[0]; + Game.Move[] moves = game.getLegalMoves(); + int inty = (int)(Math.random() * moves.length-.5f); + return moves[inty]; } } diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java index afc61b8..e780976 100644 --- a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java @@ -11,7 +11,7 @@ public final class TicTacToeAI extends AI { final Game.Move[] legalMoves = game.getLegalMoves(); - if (legalMoves.length <= 0) { + if (legalMoves.length == 0) { return null; } @@ -38,6 +38,24 @@ public final class TicTacToeAI extends AI { return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; } + public Game.Move findWorstMove(TicTacToe game, int depth){ + + + Game.Move[] legalMoves = game.getLegalMoves(); + + int bestScore = -depth; + Game.Move bestMove = null; + + for (final Game.Move move : legalMoves) { + final int score = getMoveScore(game, depth, move, false); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + return bestMove; + } private int getMoveScore(TicTacToe game, int depth, Game.Move move, boolean maximizing) { final TicTacToe copy = new TicTacToe(game); From caa812217f592a44c99fe405896eff69c08da942 Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 27 Oct 2025 17:20:22 +0100 Subject: [PATCH 07/10] spam minder --- app/src/main/java/org/toop/app/game/Connect4Game.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java index cd7c5d3..50af89f 100644 --- a/app/src/main/java/org/toop/app/game/Connect4Game.java +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -211,8 +211,6 @@ public class Connect4Game { } private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { - new EventFlow().addPostEvent(new NetworkEvents.SendCommand(response.clientId(), "MESSAGE hoi")) - .postEvent(); if (!isRunning.get()) { return; From 1c2736ac757c4abd582b5c9ac83be74901ade8ac Mon Sep 17 00:00:00 2001 From: ramollia <> Date: Mon, 27 Oct 2025 17:21:04 +0100 Subject: [PATCH 08/10] fixed setgamelabels --- app/src/main/java/org/toop/app/game/Connect4Game.java | 9 ++++++++- app/src/main/java/org/toop/app/game/ReversiGame.java | 9 ++++++++- app/src/main/java/org/toop/app/game/TicTacToeGame.java | 8 +++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java index 77bda9c..a777858 100644 --- a/app/src/main/java/org/toop/app/game/Connect4Game.java +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -85,6 +85,7 @@ public class Connect4Game { if (onForfeit == null || onExit == null) { new Thread(this::localGameThread).start(); + setGameLabels(information.players[0].isHuman); } else { new EventFlow() .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) @@ -102,7 +103,13 @@ public class Connect4Game { private void localGameThread() { while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); - setGameLabels(information.players[currentTurn].isHuman); + final String currentValue = currentTurn == 0? "RED" : "BLUE"; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + currentValue, + information.players[nextTurn].name); Game.Move move = null; diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index f1b8045..e438dd2 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -89,6 +89,7 @@ public final class ReversiGame { if (onForfeit == null || onExit == null) { new Thread(this::localGameThread).start(); + setGameLabels(information.players[0].isHuman); } else { new EventFlow() .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) @@ -116,7 +117,13 @@ public final class ReversiGame { } final int currentTurn = game.getCurrentTurn(); - setGameLabels(information.players[currentTurn].isHuman); + final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + currentValue, + information.players[nextTurn].name); Game.Move move = null; diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index 6448835..2a0a24f 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -101,7 +101,13 @@ public final class TicTacToeGame { private void localGameThread() { while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); - setGameLabels(information.players[currentTurn].isHuman); + final String currentValue = currentTurn == 0? "X" : "O"; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + currentValue, + information.players[nextTurn].name); Game.Move move = null; From 6fb248bec45ec4aa5a4f8775b946127389e89cba Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Mon, 27 Oct 2025 17:21:50 +0100 Subject: [PATCH 09/10] spam minder v2 --- app/src/main/java/org/toop/app/game/TicTacToeGame.java | 2 -- .../main/java/org/toop/app/view/views/SendChallengeView.java | 1 - game/src/main/java/org/toop/game/reversi/Reversi.java | 3 --- 3 files changed, 6 deletions(-) diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index 7e5db6b..de91574 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -224,9 +224,7 @@ public final class TicTacToeGame { } catch (InterruptedException _) {} } else { final Game.Move move; - IO.println(information.players[0].name + " " + information.players[1].name); if (information.players[1].name.equalsIgnoreCase("pism")) { - IO.println("got worst move"); move = ai.findWorstMove(game,9); }else{ move = ai.findBestMove(game, information.players[0].computerDifficulty); diff --git a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java index 7f2b8d6..e7635f7 100644 --- a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java @@ -56,7 +56,6 @@ public final class SendChallengeView extends View { final Button cancelButton = button(); cancelButton.setText(AppContext.getString("cancel")); cancelButton.setOnAction(_ -> { - IO.println("tried to click cancel"); ViewStack.pop(); }); final List nodes = new ArrayList<>(); diff --git a/game/src/main/java/org/toop/game/reversi/Reversi.java b/game/src/main/java/org/toop/game/reversi/Reversi.java index ea16075..e86de86 100644 --- a/game/src/main/java/org/toop/game/reversi/Reversi.java +++ b/game/src/main/java/org/toop/game/reversi/Reversi.java @@ -142,7 +142,6 @@ public final class Reversi extends TurnBasedGame { Move[] moves = sortMovesFromCenter(getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value()),move); mostRecentlyFlippedPieces = moves; board[move.position()] = move.value(); - IO.println(move.position() +" "+ move.value()); for (Move m : moves) { board[m.position()] = m.value(); } @@ -207,7 +206,6 @@ public final class Reversi extends TurnBasedGame { public Move[] sortMovesFromCenter(Move[] moves, Move center) { int centerX = center.position()%columnSize; int centerY = center.position()/rowSize; - IO.println("pre "+Arrays.toString(moves)); Arrays.sort(moves, (a, b) -> { int dxA = a.position()%columnSize - centerX; int dyA = a.position()/rowSize - centerY; @@ -219,7 +217,6 @@ public final class Reversi extends TurnBasedGame { return Integer.compare(distA, distB); }); - IO.println("post "+Arrays.toString(moves)); return moves; } public Move[] getMostRecentlyFlippedPieces() { From 2cda94e4dba4b2b5a4f94ebdbe8e63e7a4813055 Mon Sep 17 00:00:00 2001 From: ramollia <> Date: Tue, 28 Oct 2025 09:13:09 +0100 Subject: [PATCH 10/10] canvas changes --- .../main/java/org/toop/app/canvas/Connect4Canvas.java | 2 +- app/src/main/java/org/toop/app/canvas/GameCanvas.java | 10 +++++++++- .../main/java/org/toop/app/canvas/ReversiCanvas.java | 2 +- .../main/java/org/toop/app/canvas/TicTacToeCanvas.java | 2 +- app/src/main/java/org/toop/app/game/ReversiGame.java | 4 +--- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java index a641c6a..913bb62 100644 --- a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java +++ b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java @@ -6,6 +6,6 @@ import java.util.function.Consumer; public class Connect4Canvas extends GameCanvas { public Connect4Canvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 7, 6, 10, true, onCellClicked); + super(color, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked); } } \ No newline at end of file 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 42526a3..1a002a6 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -22,6 +22,7 @@ public abstract class GameCanvas { protected final GraphicsContext graphics; protected final Color color; + protected final Color backgroundColor; protected final int width; protected final int height; @@ -34,11 +35,12 @@ public abstract class GameCanvas { protected final Cell[] cells; - protected GameCanvas(Color color, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked) { + protected GameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked) { canvas = new Canvas(width, height); graphics = canvas.getGraphicsContext2D(); this.color = color; + this.backgroundColor = backgroundColor; this.width = width; this.height = height; @@ -83,6 +85,9 @@ public abstract class GameCanvas { } private void render() { + graphics.setFill(backgroundColor); + graphics.fillRect(0, 0, width, height); + graphics.setFill(color); for (int x = 0; x < rowSize - 1; x++) { @@ -123,6 +128,9 @@ public abstract class GameCanvas { final float height = cells[cell].height(); graphics.clearRect(x, y, width, height); + + graphics.setFill(backgroundColor); + graphics.fillRect(x, y, width, height); } public void clearAll() { diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java index 395dc4f..8eec0ad 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -6,7 +6,7 @@ import java.util.function.Consumer; public final class ReversiCanvas extends GameCanvas { public ReversiCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 8, 8, 10, true, onCellClicked); + super(color, Color.GREEN, width, height, 8, 8, 5, true, onCellClicked); drawStartingDots(); } diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java index 06d0b31..d7ccbc8 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -6,7 +6,7 @@ import java.util.function.Consumer; public final class TicTacToeCanvas extends GameCanvas { public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 3, 3, 30, false, onCellClicked); + super(color, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked); } public void drawX(Color color, int cell) { diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index fa5c611..64f0b6c 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -1,8 +1,6 @@ package org.toop.app.game; import javafx.animation.SequentialTransition; -import javafx.animation.Timeline; -import javafx.util.Duration; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.ReversiCanvas; @@ -64,7 +62,7 @@ public final class ReversiGame { }, onMessage); } - canvas = new ReversiCanvas(Color.GRAY, + canvas = new ReversiCanvas(Color.BLACK, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, (cell) -> { if (onForfeit == null || onExit == null) {