From e382cf95f2a44a8ed9a99f087fea406bc2e92e9d Mon Sep 17 00:00:00 2001 From: ramollia <@> Date: Wed, 15 Oct 2025 00:14:39 +0200 Subject: [PATCH] gui refactor --- .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/resourceBundles.xml | 41 -- app/src/main/java/org/toop/app/App.java | 102 ++--- .../java/org/toop/app/GameInformation.java | 43 +- app/src/main/java/org/toop/app/Server.java | 188 ++++++++ .../java/org/toop/app/canvas/GameCanvas.java | 54 +-- .../org/toop/app/canvas/TicTacToeCanvas.java | 4 +- .../java/org/toop/app/game/TicTacToeGame.java | 220 ++++++++++ .../java/org/toop/app/layer/Container.java | 11 - .../main/java/org/toop/app/layer/Layer.java | 81 ---- .../java/org/toop/app/layer/NodeBuilder.java | 131 ------ .../main/java/org/toop/app/layer/Popup.java | 19 - .../layer/containers/HorizontalContainer.java | 60 --- .../layer/containers/VerticalContainer.java | 60 --- .../toop/app/layer/layers/ConnectedLayer.java | 182 -------- .../toop/app/layer/layers/CreditsPopup.java | 72 ---- .../org/toop/app/layer/layers/MainLayer.java | 51 --- .../app/layer/layers/MultiplayerLayer.java | 176 -------- .../toop/app/layer/layers/OptionsPopup.java | 194 --------- .../org/toop/app/layer/layers/QuitPopup.java | 42 -- .../layer/layers/game/GameFinishedPopup.java | 52 --- .../app/layer/layers/game/TicTacToeLayer.java | 320 -------------- app/src/main/java/org/toop/app/view/View.java | 400 ++++++++++++++++++ .../java/org/toop/app/view/ViewStack.java | 105 +++++ .../toop/app/view/views/ChallengeView.java | 118 ++++++ .../org/toop/app/view/views/CreditsView.java | 101 +++++ .../org/toop/app/view/views/ErrorView.java | 45 ++ .../org/toop/app/view/views/GameView.java | 135 ++++++ .../app/view/views/LocalMultiplayerView.java | 158 +++++++ .../org/toop/app/view/views/LocalView.java | 43 ++ .../org/toop/app/view/views/MainView.java | 48 +++ .../org/toop/app/view/views/OnlineView.java | 83 ++++ .../org/toop/app/view/views/OptionsView.java | 249 +++++++++++ .../org/toop/app/view/views/QuitView.java | 40 ++ .../app/view/views/SendChallengeView.java | 118 ++++++ .../org/toop/app/view/views/ServerView.java | 80 ++++ .../main/java/org/toop/local/AppSettings.java | 19 +- .../localization/localization_ar.properties | 104 +++-- .../localization/localization_de.properties | 78 ++-- .../localization/localization_en.properties | 76 ++-- .../localization/localization_es.properties | 80 ++-- .../localization/localization_fr.properties | 84 ++-- .../localization/localization_hi.properties | 112 +++-- .../localization/localization_it.properties | 78 ++-- .../localization/localization_ja.properties | 96 +++-- .../localization/localization_ko.properties | 110 +++-- .../localization/localization_nl.properties | 80 ++-- .../localization/localization_ru.properties | 90 ++-- .../localization/localization_zh.properties | 92 ++-- .../main/resources/assets/style/dark-hc.css | 215 ---------- app/src/main/resources/assets/style/dark.css | 222 ++++------ .../main/resources/assets/style/general.css | 41 ++ .../resources/assets/style/high-contrast.css | 139 ++++++ app/src/main/resources/assets/style/large.css | 57 ++- .../main/resources/assets/style/light-hc.css | 215 ---------- app/src/main/resources/assets/style/light.css | 222 ++++------ .../main/resources/assets/style/medium.css | 53 ++- app/src/main/resources/assets/style/small.css | 53 ++- 58 files changed, 3385 insertions(+), 2762 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/resourceBundles.xml create mode 100644 app/src/main/java/org/toop/app/Server.java create mode 100644 app/src/main/java/org/toop/app/game/TicTacToeGame.java delete mode 100644 app/src/main/java/org/toop/app/layer/Container.java delete mode 100644 app/src/main/java/org/toop/app/layer/Layer.java delete mode 100644 app/src/main/java/org/toop/app/layer/NodeBuilder.java delete mode 100644 app/src/main/java/org/toop/app/layer/Popup.java delete mode 100644 app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java delete mode 100644 app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/CreditsPopup.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/MainLayer.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/MultiplayerLayer.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/OptionsPopup.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/QuitPopup.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/game/GameFinishedPopup.java delete mode 100644 app/src/main/java/org/toop/app/layer/layers/game/TicTacToeLayer.java create mode 100644 app/src/main/java/org/toop/app/view/View.java create mode 100644 app/src/main/java/org/toop/app/view/ViewStack.java create mode 100644 app/src/main/java/org/toop/app/view/views/ChallengeView.java create mode 100644 app/src/main/java/org/toop/app/view/views/CreditsView.java create mode 100644 app/src/main/java/org/toop/app/view/views/ErrorView.java create mode 100644 app/src/main/java/org/toop/app/view/views/GameView.java create mode 100644 app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java create mode 100644 app/src/main/java/org/toop/app/view/views/LocalView.java create mode 100644 app/src/main/java/org/toop/app/view/views/MainView.java create mode 100644 app/src/main/java/org/toop/app/view/views/OnlineView.java create mode 100644 app/src/main/java/org/toop/app/view/views/OptionsView.java create mode 100644 app/src/main/java/org/toop/app/view/views/QuitView.java create mode 100644 app/src/main/java/org/toop/app/view/views/SendChallengeView.java create mode 100644 app/src/main/java/org/toop/app/view/views/ServerView.java delete mode 100644 app/src/main/resources/assets/style/dark-hc.css create mode 100644 app/src/main/resources/assets/style/general.css create mode 100644 app/src/main/resources/assets/style/high-contrast.css delete mode 100644 app/src/main/resources/assets/style/light-hc.css diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/resourceBundles.xml b/.idea/resourceBundles.xml deleted file mode 100644 index af8f6fc..0000000 --- a/.idea/resourceBundles.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - localization - - - - - - - - - - - - - - - localization - - - \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 38a4873..96980b1 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,29 +1,24 @@ package org.toop.app; -import javafx.application.Platform; -import org.toop.app.layer.Layer; -import org.toop.app.layer.layers.MainLayer; -import org.toop.app.layer.layers.QuitPopup; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.MainView; +import org.toop.app.view.views.QuitView; import org.toop.framework.asset.ResourceManager; import org.toop.framework.asset.resources.CssAsset; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.local.AppContext; +import org.toop.local.AppSettings; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import org.toop.local.AppSettings; - -import java.util.Stack; public final class App extends Application { private static Stage stage; private static Scene scene; - private static StackPane root; - private static Stack stack; private static int height; private static int width; @@ -37,17 +32,15 @@ public final class App extends Application { public void start(Stage stage) throws Exception { final StackPane root = new StackPane(); final Scene scene = new Scene(root); + ViewStack.setup(scene); - stage.setTitle(AppContext.getString("appTitle")); + stage.setTitle(AppContext.getString("app-title")); stage.setWidth(1080); stage.setHeight(720); stage.setOnCloseRequest(event -> { event.consume(); - - if (!isQuitting) { - quitPopup(); - } + startQuit(); }); stage.setScene(scene); @@ -57,78 +50,40 @@ public final class App extends Application { App.stage = stage; App.scene = scene; - App.root = root; - App.stack = new Stack<>(); - - App.width = (int) stage.getWidth(); - App.height = (int) stage.getHeight(); + App.width = (int)stage.getWidth(); + App.height = (int)stage.getHeight(); App.isQuitting = false; - final AppSettings settings = new AppSettings(); - settings.applySettings(); - + AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - activate(new MainLayer()); + + ViewStack.push(new MainView()); } - public static void activate(Layer layer) { - Platform.runLater(() -> { - popAll(); - push(layer); - }); + public static void startQuit() { + if (isQuitting) { + return; + } + + ViewStack.push(new QuitView()); + isQuitting = true; } - public static void push(Layer layer) { - Platform.runLater(() -> { - root.getChildren().addLast(layer.getLayer()); - stack.push(layer); - }); - } - - public static void pop() { - Platform.runLater(() -> { - root.getChildren().removeLast(); - stack.pop(); - - isQuitting = false; - }); - } - - public static void popAll() { - Platform.runLater(() -> { - final int childrenCount = root.getChildren().size(); - - for (int i = 0; i < childrenCount; i++) { - try { - root.getChildren().removeLast(); - } catch (Exception e) { - IO.println(e); - } - } - - stack.removeAllElements(); - }); - } - - public static void quitPopup() { - Platform.runLater(() -> { - push(new QuitPopup()); - isQuitting = true; - }); + public static void stopQuit() { + ViewStack.pop(); + isQuitting = false; } public static void quit() { + ViewStack.cleanup(); stage.close(); } - public static void reloadAll() { - stage.setTitle(AppContext.getString("appTitle")); - - for (final Layer layer : stack) { - layer.reload(); - } + public static void reload() { + stage.setTitle(AppContext.getString("app-title")); + ViewStack.reload(); } public static void setFullscreen(boolean fullscreen) { @@ -137,7 +92,7 @@ public final class App extends Application { width = (int) stage.getWidth(); height = (int) stage.getHeight(); - reloadAll(); + reload(); } public static void setStyle(String theme, String layoutSize) { @@ -147,10 +102,11 @@ public final class App extends Application { scene.getStylesheets().removeLast(); } + scene.getStylesheets().add(ResourceManager.get("general.css").getUrl()); scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - reloadAll(); + reload(); } public static int getWidth() { diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java index fd7c56e..0a847fb 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -1,6 +1,41 @@ package org.toop.app; -public record GameInformation(String[] playerName, boolean[] isPlayerHuman, - int[] computerDifficulty, int[] computerThinkTime, - boolean isConnectionLocal, String serverIP, String serverPort) { -} +public class GameInformation { + public enum Type { + TICTACTOE, + REVERSI; + + public static int playerCount(Type type) { + return switch (type) { + case TICTACTOE -> 2; + case REVERSI -> 2; + }; + } + + public static int maxDepth(Type type) { + return switch (type) { + case TICTACTOE -> 5; + case REVERSI -> 0; // Todo + }; + } + } + + public static class Player { + public String name = ""; + public boolean isHuman = true; + public int computerDifficulty = 0; + public int computerThinkTime = 1; + } + + public final Type type; + public final Player[] players; + + public GameInformation(Type type) { + this.type = type; + players = new Player[Type.playerCount(type)]; + + for (int i = 0; i < players.length; i++) { + players[i] = new Player(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java new file mode 100644 index 0000000..f3ec468 --- /dev/null +++ b/app/src/main/java/org/toop/app/Server.java @@ -0,0 +1,188 @@ +package org.toop.app; + +import org.toop.app.game.TicTacToeGame; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.ChallengeView; +import org.toop.app.view.views.ErrorView; +import org.toop.app.view.views.SendChallengeView; +import org.toop.app.view.views.ServerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.local.AppContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class Server { + private String user = ""; + + private long clientId = -1; + private List onlinePlayers = new CopyOnWriteArrayList(); + + private ServerView view; + + private boolean isPolling = true; + + 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; + } + + return null; + } + + public Server(String ip, String port, String user) { + if (ip.split("\\.").length < 4) { + ViewStack.push(new ErrorView("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address"))); + return; + } + + int parsedPort = -1; + + try { + parsedPort = Integer.parseInt(port); + } catch (NumberFormatException _) { + ViewStack.push(new ErrorView("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port"))); + return; + } + + if (user.isEmpty()) { + ViewStack.push(new ErrorView(AppContext.getString("invalid-username"))); + return; + } + + new EventFlow() + .addPostEvent(NetworkEvents.StartClient.class, ip, parsedPort) + .onResponse(NetworkEvents.StartClientResponse.class, e -> { + this.user = user; + clientId = e.clientId(); + + new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent(); + + view = new ServerView(user, this::sendChallenge, this::disconnect); + ViewStack.push(view); + + startPopulateThread(); + }).postEvent(); + + new EventFlow().listen(this::handleReceivedChallenge); + } + + private void populatePlayerList() { + new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> { + if (e.clientId() == clientId) { + onlinePlayers = new ArrayList(List.of(e.playerlist())); + onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + + view.update(onlinePlayers); + } + }); + + final EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)); + + while (isPolling) { + sendGetPlayerList.postEvent(); + + try { + Thread.sleep(5000); + } catch (InterruptedException _) {} + } + } + + private void sendChallenge(String opponent) { + ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { + new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)) + .listen(NetworkEvents.GameMatchResponse.class, e -> { + if (e.clientId() == clientId) { + isPolling = false; + onlinePlayers.clear(); + + final GameInformation.Type type = gameToType(gameType); + 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 = opponent; + + new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame); + } + }).postEvent(); + })); + } + + private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { + 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("\"")); + + final String finalGameType = gameType; + + ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { + final int challengeId = Integer.parseInt(response.challengeId().substring(18, response.challengeId().length() - 2)); + 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(); + + final GameInformation.Type type = gameToType(finalGameType); + 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); + break; + case REVERSI: + break; + } + } + }); + })); + } + + private void disconnect() { + // Todo + } + + private void forfeitGame() { + // Todo + } + + private void exitGame() { + forfeitGame(); + + ViewStack.push(view); + startPopulateThread(); + } + + private void startPopulateThread() { + isPolling = true; + + final Thread populateThread = new Thread(this::populatePlayerList); + populateThread.setDaemon(false); + populateThread.start(); + } + + public List getGamesList() { + final List list = new ArrayList(); + list.add("tic-tac-toe"); // Todo: get games list from server and check if the game is supported + + return list; + } +} \ 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 5d4b56d..b742be7 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -16,8 +16,8 @@ public abstract class GameCanvas { protected final Color color; - protected int width; - protected int height; + protected final int width; + protected final int height; protected final int rows; protected final int columns; @@ -28,6 +28,9 @@ public abstract class GameCanvas { protected final Cell[] cells; protected GameCanvas(Color color, int width, int height, int rows, int columns, int gapSize, boolean edges, Consumer onCellClicked) { + width += gapSize * 2; + height += gapSize * 2; + canvas = new Canvas(width, height); graphics = canvas.getGraphicsContext2D(); @@ -44,14 +47,14 @@ public abstract class GameCanvas { cells = new Cell[rows * columns]; - final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows; - final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns; + final float cellWidth = ((float) width - gapSize * rows) / rows; + final float cellHeight = ((float) height - gapSize * columns) / columns; for (int y = 0; y < columns; y++) { - final float startY = y * cellHeight + y * gapSize; + final float startY = gapSize + y * cellHeight + y * gapSize; for (int x = 0; x < rows; x++) { - final float startX = x * cellWidth + x * gapSize; + final float startX = gapSize + x * cellWidth + x * gapSize; cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight); } } @@ -61,11 +64,11 @@ public abstract class GameCanvas { return; } - final int column = (int) ((event.getX() / width) * rows); - final int row = (int) ((event.getY() / height) * columns); + final int column = (int)((event.getX() / this.width) * rows); + final int row = (int)((event.getY() / this.height) * columns); event.consume(); - onCellClicked.accept(row * rows + column); + onCellClicked.accept(column + row * rows); }); render(); @@ -79,44 +82,33 @@ public abstract class GameCanvas { graphics.setFill(color); for (int x = 1; x < rows; x++) { - graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height); + graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height + gapSize); } for (int y = 1; y < columns; y++) { - graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize); + graphics.fillRect(0, cells[y * rows].y() - gapSize, width + gapSize, gapSize); } if (edges) { - graphics.fillRect(-gapSize, 0, gapSize, height); - graphics.fillRect(0, -gapSize, width, gapSize); + graphics.fillRect(0, 0, gapSize, height + gapSize); + graphics.fillRect(0, 0, width + gapSize, gapSize); - graphics.fillRect(width - gapSize, 0, gapSize, height); - graphics.fillRect(0, height - gapSize, width, gapSize); + graphics.fillRect(width + gapSize, 0, gapSize, height + gapSize * 2); + graphics.fillRect(0, height + gapSize, width + gapSize * 2, gapSize); } } - public void draw(Color color, int cell) { - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + 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() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; + final float width = cells[cell].width(); + final float height = cells[cell].height(); graphics.setFill(color); graphics.fillRect(x, y, width, height); } - public void resize(int width, int height) { - canvas.setWidth(width); - canvas.setHeight(height); - - this.width = width; - this.height = height; - - clear(); - render(); - } - 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 0f7cbb9..06d0b31 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -4,9 +4,9 @@ import javafx.scene.paint.Color; import java.util.function.Consumer; -public class TicTacToeCanvas extends GameCanvas { +public final class TicTacToeCanvas extends GameCanvas { public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 3, 3, 10, false, onCellClicked); + super(color, 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/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java new file mode 100644 index 0000000..07f589a --- /dev/null +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -0,0 +1,220 @@ +package org.toop.app.game; + +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.TicTacToeCanvas; +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.Game; +import org.toop.game.tictactoe.TicTacToe; +import org.toop.game.tictactoe.TicTacToeAI; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public final class TicTacToeGame { + private final GameInformation information; + + private final int myTurn; + private final BlockingQueue moveQueue; + + private final TicTacToe game; + private final TicTacToeAI ai; + + private final GameView view; + private final TicTacToeCanvas canvas; + + public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit) { + this.information = information; + + this.myTurn = myTurn; + moveQueue = new LinkedBlockingQueue(); + + game = new TicTacToe(); + ai = new TicTacToeAI(); + + if (onForfeit == null || onExit == null) { + view = new GameView(null, () -> { + ViewStack.push(new LocalMultiplayerView(information)); + }); + } else { + view = new GameView(onForfeit, onExit); + } + + canvas = new TicTacToeCanvas(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, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell, 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); + } + } + + private void localGameThread() { + boolean isRunning = true; + + while (isRunning) { + 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 = 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); + + 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 = false; + } + } + } + + private void onMoveResponse(NetworkEvents.GameMoveResponse response) { + 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.RED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.BLUE, move.position()); + } + + setGameLabels(game.getCurrentTurn() == myTurn); + } + + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + 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) { + view.updateChat("anon", msg.message()); + } + + 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/Container.java b/app/src/main/java/org/toop/app/layer/Container.java deleted file mode 100644 index 409c0eb..0000000 --- a/app/src/main/java/org/toop/app/layer/Container.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.toop.app.layer; - -import javafx.scene.Node; -import javafx.scene.layout.Region; - -public abstract class Container { - public abstract Region getContainer(); - - public abstract void addNodes(Node... nodes); - public abstract void addContainer(Container container, boolean fill); -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/Layer.java b/app/src/main/java/org/toop/app/layer/Layer.java deleted file mode 100644 index 35034c9..0000000 --- a/app/src/main/java/org/toop/app/layer/Layer.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.toop.app.layer; - -import org.toop.app.App; -import org.toop.app.canvas.GameCanvas; - -import javafx.geometry.Pos; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; - -public abstract class Layer { - protected StackPane layer; - protected Region background; - - protected Layer(String... backgroundStyles) { - layer = new StackPane(); - - background = new Region(); - background.getStyleClass().addAll(backgroundStyles); - background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE); - - layer.getChildren().addLast(background); - } - - protected void addContainer(Container container, Pos position, int xOffset, int yOffset, int widthPercent, int heightPercent) { - StackPane.setAlignment(container.getContainer(), position); - - final double widthUnit = App.getWidth() / 100.0; - final double heightUnit = App.getHeight() / 100.0; - - if (widthPercent > 0) { - container.getContainer().setMaxWidth(widthPercent * widthUnit); - } else { - container.getContainer().setMaxWidth(Region.USE_PREF_SIZE); - } - - if (heightPercent > 0) { - container.getContainer().setMaxHeight(heightPercent * heightUnit); - } else { - container.getContainer().setMaxHeight(Region.USE_PREF_SIZE); - } - - container.getContainer().setTranslateX(xOffset * widthUnit); - container.getContainer().setTranslateY(yOffset * heightUnit); - - layer.getChildren().addLast(container.getContainer()); - } - - protected void addGameCanvas(GameCanvas canvas, Pos position, int xOffset, int yOffset) { - StackPane.setAlignment(canvas.getCanvas(), position); - - final double widthUnit = App.getWidth() / 100.0; - final double heightUnit = App.getHeight() / 100.0; - - canvas.getCanvas().setTranslateX(xOffset * widthUnit); - canvas.getCanvas().setTranslateY(yOffset * heightUnit); - - layer.getChildren().addLast(canvas.getCanvas()); - } - - protected void pop() { - if (layer.getChildren().size() <= 1) { - return; - } - - layer.getChildren().removeLast(); - } - - protected void popAll() { - final int containers = layer.getChildren().size(); - - for (int i = 1; i < containers; i++) { - layer.getChildren().removeLast(); - } - } - - public StackPane getLayer() { - return layer; - } - - public abstract void reload(); -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/NodeBuilder.java b/app/src/main/java/org/toop/app/layer/NodeBuilder.java deleted file mode 100644 index a0f2996..0000000 --- a/app/src/main/java/org/toop/app/layer/NodeBuilder.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.toop.app.layer; - -import org.toop.framework.audio.events.AudioEvents; -import org.toop.framework.eventbus.EventFlow; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.geometry.Orientation; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.text.Text; - -import java.util.function.Consumer; - -public final class NodeBuilder { - public static void addCss(Node node, String... cssClasses) { - node.getStyleClass().addAll(cssClasses); - } - - public static void setCss(Node node, String... cssClasses) { - node.getStyleClass().removeAll(); - node.getStyleClass().addAll(cssClasses); - } - - public static Text header(String x) { - final Text element = new Text(x); - setCss(element, "text-primary", "text-header"); - - return element; - } - - public static Text text(String x) { - final Text element = new Text(x); - setCss(element, "text-secondary", "text-normal"); - - return element; - } - - public static Label button(String x, Runnable runnable) { - final Label element = new Label(x); - setCss(element, "button", "text-normal"); - - element.setOnMouseClicked(_ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - runnable.run(); - }); - - return element; - } - - public static Label toggle(String x1, String x2, boolean toggled, Consumer consumer) { - final Label element = new Label(toggled ? x2 : x1); - setCss(element, "toggle", "text-normal"); - - final BooleanProperty checked = new SimpleBooleanProperty(toggled); - - element.setOnMouseClicked(_ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - checked.set(!checked.get()); - - if (checked.get()) { - element.setText(x1); - } else { - element.setText(x2); - } - - consumer.accept(checked.get()); - }); - - return element; - } - - public static Slider slider(int max, int initial, Consumer consumer) { - final Slider element = new Slider(0, max, initial); - setCss(element, "bg-slider-track"); - - element.setMinorTickCount(0); - element.setMajorTickUnit(1); - element.setBlockIncrement(1); - - element.setSnapToTicks(true); - element.setShowTickLabels(true); - - element.setOnMouseClicked(_ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - }); - - element.valueProperty().addListener((_, _, newValue) -> { - consumer.accept(newValue.intValue()); - }); - - return element; - } - - public static TextField input(String x, Consumer consumer) { - final TextField element = new TextField(x); - setCss(element, "input", "text-normal"); - - element.setOnMouseClicked(_ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - }); - - element.textProperty().addListener((_, _, newValue) -> { - consumer.accept(newValue); - }); - - return element; - } - - public static ChoiceBox choiceBox(Consumer consumer) { - final ChoiceBox element = new ChoiceBox<>(); - setCss(element, "choice-box", "text-normal"); - - element.setOnMouseClicked(_ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - }); - - element.valueProperty().addListener((_, _, newValue) -> { - consumer.accept(newValue); - }); - - return element; - } - - public static Separator separator() { - final Separator element = new Separator(Orientation.HORIZONTAL); - setCss(element, "separator"); - - return element; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/Popup.java b/app/src/main/java/org/toop/app/layer/Popup.java deleted file mode 100644 index 6a54bec..0000000 --- a/app/src/main/java/org/toop/app/layer/Popup.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.toop.app.layer; - -import org.toop.app.App; - -public abstract class Popup extends Layer { - protected Popup(boolean popOnBackground, String... backgroundStyles) { - super(backgroundStyles); - - if (popOnBackground) { - background.setOnMouseClicked(_ -> { - App.pop(); - }); - } - } - - protected Popup(boolean popOnBackground) { - this(popOnBackground, "bg-popup"); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java b/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java deleted file mode 100644 index b3f00a5..0000000 --- a/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.toop.app.layer.containers; - -import org.toop.app.layer.Container; - -import javafx.collections.ObservableList; -import javafx.scene.Node; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; - -public final class HorizontalContainer extends Container { - private final HBox container; - - public HorizontalContainer(int spacing, String... cssClasses) { - container = new HBox(spacing); - container.getStyleClass().addAll(cssClasses); - } - - public HorizontalContainer(int spacing) { - this(spacing, "container"); - } - - @Override - public Region getContainer() { - return container; - } - - @Override - public void addNodes(Node... nodes) { - container.getChildren().addAll(nodes); - } - - @Override - public void addContainer(Container container, boolean fill) { - if (fill) { - container.getContainer().setMinSize(0, 0); - container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - HBox.setHgrow(container.getContainer(), Priority.ALWAYS); - } else { - container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - } - - this.container.getChildren().add(container.getContainer()); - - if (fill) { - balanceChildWidths(); - } - } - - private void balanceChildWidths() { - final ObservableList children = container.getChildren(); - final double widthPerChild = container.getWidth() / children.size(); - - for (final Node child : children) { - if (child instanceof Region) { - ((Region) child).setPrefWidth(widthPerChild); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java b/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java deleted file mode 100644 index a8fb74d..0000000 --- a/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.toop.app.layer.containers; - -import org.toop.app.layer.Container; - -import javafx.collections.ObservableList; -import javafx.scene.Node; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; - -public final class VerticalContainer extends Container { - private final VBox container; - - public VerticalContainer(int spacing, String... cssClasses) { - container = new VBox(spacing); - container.getStyleClass().addAll(cssClasses); - } - - public VerticalContainer(int spacing) { - this(spacing, "container"); - } - - @Override - public Region getContainer() { - return container; - } - - @Override - public void addNodes(Node... nodes) { - container.getChildren().addAll(nodes); - } - - @Override - public void addContainer(Container container, boolean fill) { - if (fill) { - container.getContainer().setMinSize(0, 0); - container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - VBox.setVgrow(container.getContainer(), Priority.ALWAYS); - } else { - container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - } - - this.container.getChildren().add(container.getContainer()); - - if (fill) { - balanceChildHeights(); - } - } - - private void balanceChildHeights() { - final ObservableList children = container.getChildren(); - final double heightPerChild = container.getHeight() / children.size(); - - for (final Node child : children) { - if (child instanceof Region) { - ((Region) child).setPrefHeight(heightPerChild); - } - } - } -} \ 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 f9bf08e..0000000 --- a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.toop.app.layer.layers; - -import javafx.application.Platform; -import org.toop.app.App; -import org.toop.app.GameInformation; -import org.toop.app.layer.Container; -import org.toop.app.layer.Layer; -import org.toop.app.layer.NodeBuilder; -import org.toop.app.layer.Popup; -import org.toop.app.layer.containers.HorizontalContainer; -import org.toop.app.layer.containers.VerticalContainer; -import org.toop.app.layer.layers.game.TicTacToeLayer; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.events.NetworkEvents; - -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.ListView; -import org.toop.local.AppContext; - -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; - -public final class ConnectedLayer extends Layer { - private static Timer pollTimer = new Timer(); - - private static class ChallengePopup extends Popup { - private final GameInformation information; - - private final String challenger; - private final String game; - - private final long clientID; - private final int challengeID; - - public ChallengePopup(GameInformation information, String challenger, String game, long clientID, String challengeID) { - super(false, "bg-popup"); - - this.information = information; - - this.challenger = challenger; - this.game = game; - - this.clientID = clientID; - this.challengeID = Integer.parseInt(challengeID.substring(18, challengeID.length() - 2)); - - reload(); - } - - @Override - public void reload() { - popAll(); - - final var challengeText = NodeBuilder.header(AppContext.getString("challengeText")); - final var challengerNameText = NodeBuilder.header(challenger); - - final var gameText = NodeBuilder.text(AppContext.getString("gameIsText")); - final var gameNameText = NodeBuilder.text(game); - - final var acceptButton = NodeBuilder.button(AppContext.getString("accept"), () -> { - pollTimer.cancel(); - - new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientID, challengeID)).postEvent(); - App.activate(new TicTacToeLayer(information, clientID)); - }); - - final var denyButton = NodeBuilder.button(AppContext.getString("deny"), () -> { - App.pop(); - }); - - final Container controlContainer = new HorizontalContainer(30); - controlContainer.addNodes(acceptButton, denyButton); - - final Container mainContainer = new VerticalContainer(30); - mainContainer.addNodes(challengeText, challengerNameText); - mainContainer.addNodes(gameText, gameNameText); - - mainContainer.addContainer(controlContainer, false); - - addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30); - } - } - - GameInformation information; - long clientId; - String user; - List onlinePlayers = new CopyOnWriteArrayList<>(); - - public ConnectedLayer(GameInformation information) { - super("bg-primary"); - - this.information = information; - - new EventFlow() - .addPostEvent(NetworkEvents.StartClient.class, information.serverIP(), Integer.parseInt(information.serverPort())) - .onResponse(NetworkEvents.StartClientResponse.class, e -> { - clientId = e.clientId(); - user = information.playerName()[0].replaceAll("\\s+", ""); - - new EventFlow().addPostEvent(new NetworkEvents.SendLogin(this.clientId, this.user)).postEvent(); - - Thread popThread = new Thread(this::populatePlayerList); - popThread.setDaemon(false); - popThread.start(); - }).postEvent(); - - new EventFlow().listen(this::handleReceivedChallenge); - - reload(); - } - - private void populatePlayerList() { - EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(this.clientId)); - new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> { - if (e.clientId() == this.clientId) { - List playerList = new java.util.ArrayList<>(List.of(e.playerlist())); // TODO: Garbage, but works - playerList.removeIf(name -> name.equalsIgnoreCase(user)); - if (this.onlinePlayers != playerList) { - this.onlinePlayers.clear(); - this.onlinePlayers.addAll(playerList); - } - } - }); - - TimerTask task = new TimerTask() { - public void run() { - sendGetPlayerList.postEvent(); - Platform.runLater(() -> reload()); - } - }; - - pollTimer.schedule(task, 0L, 5000L); // TODO: Block app exit, fix later - } - - private void sendChallenge(String oppUsername, String gameType) { - final AtomicInteger challengeId = new AtomicInteger(-1); - - if (onlinePlayers.contains(oppUsername)) { - new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(this.clientId, oppUsername, gameType)) - .listen(NetworkEvents.ChallengeResponse.class, e -> { - challengeId.set(Integer.parseInt(e.challengeId().substring(18, e.challengeId().length() - 2))); - }) - .listen(NetworkEvents.GameMatchResponse.class, e -> { - if (e.clientId() == this.clientId) { - pollTimer.cancel(); - App.activate(new TicTacToeLayer(information, this.clientId, e)); - } - }, false).postEvent(); - // ^ - // | - // | - // | - } - } - - private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { - App.push(new ChallengePopup(information, response.challengerName(), response.gameType(), clientId, response.challengeId())); - } - - @Override - public void reload() { - popAll(); - - ListView