From 144de5100e9809cb710a2eb333b234b08e3ba03f Mon Sep 17 00:00:00 2001 From: Stef Date: Thu, 27 Nov 2025 16:58:54 +0100 Subject: [PATCH] Revert "merge widgets with development" This reverts commit 38681c5db03e3e170c90ec3ec6795b6331b157df. --- .idea/inspectionProfiles/Project_Default.xml | 584 +----------------- app/src/main/java/org/toop/app/Server.java | 12 +- .../org/toop/app/canvas/Connect4Canvas.java | 2 +- .../java/org/toop/app/canvas/GameCanvas.java | 22 +- .../org/toop/app/canvas/ReversiCanvas.java | 95 +-- .../org/toop/app/canvas/TicTacToeCanvas.java | 2 +- .../org/toop/app/game/BaseGameThread.java | 14 +- .../java/org/toop/app/game/Connect4Game.java | 275 +++++++++ .../org/toop/app/game/Connect4GameThread.java | 188 ------ .../java/org/toop/app/game/ReversiGame.java | 327 ++++++++++ .../org/toop/app/game/ReversiGameThread.java | 241 -------- .../java/org/toop/app/game/TicTacToeGame.java | 250 ++++++++ .../toop/app/game/TicTacToeGameThread.java | 61 +- app/src/main/java/org/toop/app/view/View.java | 400 ++++++++++++ .../java/org/toop/app/view/ViewStack.java | 105 ++++ .../toop/app/view/displays/SongDisplay.java | 129 ++++ .../toop/app/view/views/ChallengeView.java | 127 ++++ .../org/toop/app/view/views/CreditsView.java | 110 ++++ .../org/toop/app/view/views/ErrorView.java | 45 ++ .../org/toop/app/view/views/GameView.java | 184 ++++++ .../app/view/views/LocalMultiplayerView.java | 171 +++++ .../org/toop/app/view/views/LocalView.java | 57 ++ .../org/toop/app/view/views/MainView.java | 55 ++ .../org/toop/app/view/views/OnlineView.java | 91 +++ .../org/toop/app/view/views/OptionsView.java | 258 ++++++++ .../org/toop/app/view/views/QuitView.java | 40 ++ .../app/view/views/SendChallengeView.java | 119 ++++ .../org/toop/app/view/views/ServerView.java | 89 +++ .../app/widget/view/LocalMultiplayerView.java | 8 +- .../main/java/org/toop/local/AppContext.java | 3 - 30 files changed, 2942 insertions(+), 1122 deletions(-) create mode 100644 app/src/main/java/org/toop/app/game/Connect4Game.java delete mode 100644 app/src/main/java/org/toop/app/game/Connect4GameThread.java create mode 100644 app/src/main/java/org/toop/app/game/ReversiGame.java delete mode 100644 app/src/main/java/org/toop/app/game/ReversiGameThread.java create mode 100644 app/src/main/java/org/toop/app/game/TicTacToeGame.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/displays/SongDisplay.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 diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 1448990..655cfae 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,591 +1,9 @@ \ 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 index 9df2e2d..00f91cc 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,8 +1,8 @@ package org.toop.app; -import org.toop.app.game.Connect4GameThread; -import org.toop.app.game.ReversiGameThread; -import org.toop.app.game.TicTacToeGameThread; +import org.toop.app.game.Connect4Game; +import org.toop.app.game.ReversiGame; +import org.toop.app.game.TicTacToeGame; import org.toop.app.widget.WidgetContainer; import org.toop.app.widget.popup.ChallengePopup; import org.toop.app.widget.popup.ErrorPopup; @@ -131,11 +131,11 @@ public final class Server { switch (type) { case TICTACTOE -> - new TicTacToeGameThread(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); + new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case REVERSI -> - new ReversiGameThread(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); + new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case CONNECT4 -> - new Connect4GameThread(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); + new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); default -> new ErrorPopup("Unsupported game type."); } } 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 913bb62..c17dafd 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, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked); + super(color, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked,null); } } \ 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 7342f5b..f8f6d70 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -35,9 +35,8 @@ public abstract class GameCanvas { protected final boolean edges; protected final Cell[] cells; - protected Cell currentCell; - protected GameCanvas(Color color, Color backgroundColor, 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, Consumer newCellEntered) { canvas = new Canvas(width, height); graphics = canvas.getGraphicsContext2D(); @@ -54,7 +53,6 @@ public abstract class GameCanvas { this.edges = edges; cells = new Cell[rowSize * columnSize]; - currentCell = null; final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize; final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize; @@ -84,24 +82,12 @@ public abstract class GameCanvas { } }); + + + render(); } - public void setOnCellEntered(Consumer onCellEntered) { - canvas.setOnMouseMoved(event -> { - final int column = (int) ((event.getX() / this.width) * rowSize); - final int row = (int) ((event.getY() / this.height) * columnSize); - - final Cell cell = cells[column + row * rowSize]; - - if (currentCell != cell && cell.isInside(event.getX(), event.getY())) { - event.consume(); - currentCell = cell; - onCellEntered.accept(column + row * rowSize); - } - }); - } - private void render() { graphics.setFill(backgroundColor); graphics.fillRect(0, 0, width, height); 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 53d0b7d..c1b24f7 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -7,38 +7,61 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public final class ReversiCanvas extends GameCanvas { - private Move[] currentlyHighlightedMoves = null; - - public ReversiCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, new Color(0f, 0.4f, 0.2f, 1f), width, height, 8, 8, 5, true, onCellClicked); + private Move[] currentlyHighlightedMoves = null; + public ReversiCanvas(Color color, int width, int height, Consumer onCellClicked, Consumer newCellEntered) { + super(color, new Color(0f,0.4f,0.2f,1f), width, height, 8, 8, 5, true, onCellClicked, newCellEntered); drawStartingDots(); + + final AtomicReference lastHoveredCell = new AtomicReference<>(null); + + canvas.setOnMouseMoved(event -> { + double mouseX = event.getX(); + double mouseY = event.getY(); + int cellId = -1; + + Cell hovered = null; + for (Cell cell : cells) { + if (cell.isInside(mouseX, mouseY)) { + hovered = cell; + cellId = turnCoordsIntoCellId(mouseX, mouseY); + break; + } + } + + Cell previous = lastHoveredCell.get(); + + if (hovered != previous) { + lastHoveredCell.set(hovered); + newCellEntered.accept(cellId); + } + }); } - public void setCurrentlyHighlightedMovesNull() { - currentlyHighlightedMoves = null; - } + public void setCurrentlyHighlightedMovesNull() { + currentlyHighlightedMoves = null; + } - public void drawHighlightDots(Move[] moves) { - if (currentlyHighlightedMoves != null) { - for (final Move move : currentlyHighlightedMoves) { - Color color = move.value() == 'W' ? Color.BLACK : Color.WHITE; - drawInnerDot(color, move.position(), true); - } - } - currentlyHighlightedMoves = moves; - if (moves != null) { - for (Move move : moves) { - Color color = move.value() == 'B' ? Color.BLACK : Color.WHITE; - drawInnerDot(color, move.position(), false); - } - } - } + public void drawHighlightDots(Move[] moves){ + if (currentlyHighlightedMoves != null){ + for (final Move move : currentlyHighlightedMoves){ + Color color = move.value() == 'W'? Color.BLACK: Color.WHITE; + drawInnerDot(color, move.position(), true); + } + } + currentlyHighlightedMoves = moves; + if (moves != null) { + for (Move move : moves) { + Color color = move.value() == 'B' ? Color.BLACK : Color.WHITE; + drawInnerDot(color, move.position(), false); + } + } + } - private int turnCoordsIntoCellId(double x, double y) { - final int column = (int) ((x / this.width) * rowSize); - final int row = (int) ((y / this.height) * columnSize); - return column + row * rowSize; - } + private int turnCoordsIntoCellId(double x, double y) { + final int column = (int) ((x / this.width) * rowSize); + final int row = (int) ((y / this.height) * columnSize); + return column + row * rowSize; + } public void drawStartingDots() { drawDot(Color.BLACK, 28); @@ -48,14 +71,16 @@ public final class ReversiCanvas extends GameCanvas { } public void drawLegalPosition(int cell, char player) { - Color innerColor; - if (player == 'B') { - innerColor = new Color(0.0f, 0.0f, 0.0f, 0.6f); - } else { - innerColor = new Color(1.0f, 1.0f, 1.0f, 0.75f); - } - - drawInnerDot(innerColor, cell, false); + Color innerColor; + if (player == 'B') { + innerColor = new Color(0.0f, 0.0f, 0.0f, 0.6f); + } + else { + innerColor = new Color(1.0f, 1.0f, 1.0f, 0.75f); + } + drawInnerDot(innerColor, cell,false); + public void drawLegalPosition(Color color, int cell) { + drawDot(new Color(color.getRed(), color.getGreen(), color.getBlue(), 0.25), cell); } } \ No newline at end of file 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 d7ccbc8..890eb39 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, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked); + super(color, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked,null); } public void drawX(Color color, int cell) { diff --git a/app/src/main/java/org/toop/app/game/BaseGameThread.java b/app/src/main/java/org/toop/app/game/BaseGameThread.java index 11b9076..f9178da 100644 --- a/app/src/main/java/org/toop/app/game/BaseGameThread.java +++ b/app/src/main/java/org/toop/app/game/BaseGameThread.java @@ -6,7 +6,6 @@ import org.toop.app.widget.view.GameView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.events.NetworkEvents; import org.toop.game.Game; -import org.toop.game.records.Move; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -19,7 +18,7 @@ public abstract class BaseGameThread { protected final GameInformation information; protected final int myTurn; protected final Runnable onGameOver; - protected final BlockingQueue moveQueue; + protected final BlockingQueue moveQueue; protected final TGame game; protected final TAI ai; @@ -28,7 +27,6 @@ public abstract class BaseGameThread { protected final TCanvas canvas; protected final AtomicBoolean isRunning = new AtomicBoolean(true); - protected final AtomicBoolean isPaused = new AtomicBoolean(false); protected BaseGameThread( GameInformation information, @@ -78,7 +76,7 @@ public abstract class BaseGameThread { } private void onCellClicked(int cell) { - if (!isRunning.get() || isPaused.get()) return; + if (!isRunning.get()) return; final int currentTurn = getCurrentTurn(); if (!information.players[currentTurn].isHuman) return; @@ -86,9 +84,8 @@ public abstract class BaseGameThread { final char value = getSymbolForTurn(currentTurn); try { - moveQueue.put(new Move(cell, value)); - } catch (InterruptedException _) { - } + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} } protected void gameOver() { @@ -113,13 +110,10 @@ public abstract class BaseGameThread { protected abstract void addCanvasToPrimary(); protected abstract int getCurrentTurn(); - protected abstract char getSymbolForTurn(int turn); - protected abstract String getNameForTurn(int turn); protected abstract void onMoveResponse(NetworkEvents.GameMoveResponse response); - protected abstract void onYourTurnResponse(NetworkEvents.YourTurnResponse response); protected abstract void localGameThread(); 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..76bd13e --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -0,0 +1,275 @@ +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.enumerators.GameState; +import org.toop.game.records.Move; + +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 Runnable onGameOver; + 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 final AtomicBoolean isRunning; + + 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(); + + + 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 Move(cell%columnSize, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'X' : 'O'; + + try { + moveQueue.put(new 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(); + setGameLabels(information.players[0].isHuman); + } 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, null); + } + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "RED" : "BLUE"; + final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount(); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + currentValue, + information.players[nextTurn].name); + + Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Move wants = moveQueue.take(); + final Move[] legalMoves = game.getLegalMoves(); + + for (final 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 GameState 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 != GameState.NORMAL) { + if (state == GameState.WIN) { + view.gameOver(true, information.players[currentTurn].name); + } else if (state == GameState.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 Move move = new Move(Integer.parseInt(response.move()), playerChar); + final GameState state = game.play(move); + + if (state != GameState.NORMAL) { + if (state == GameState.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 == GameState.DRAW) { + view.gameOver(false, ""); + gameOver(); + } + } + + if (move.value() == 'X') { + canvas.drawDot(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawDot(Color.ROYALBLUE, move.position()); + } + + updateCanvas(); + 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; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final 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.clearAll(); + + for (int i = 0; i < game.getBoard().length; i++) { + if (game.getBoard()[i] == 'X') { + canvas.drawDot(Color.RED, i); + } else if (game.getBoard()[i] == 'O') { + canvas.drawDot(Color.BLUE, i); + } + } + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "RED" : "BLUE"; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + currentValue, + information.players[isMe? 1 : 0].name); + } +} diff --git a/app/src/main/java/org/toop/app/game/Connect4GameThread.java b/app/src/main/java/org/toop/app/game/Connect4GameThread.java deleted file mode 100644 index a0d4174..0000000 --- a/app/src/main/java/org/toop/app/game/Connect4GameThread.java +++ /dev/null @@ -1,188 +0,0 @@ -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.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.enumerators.GameState; -import org.toop.game.records.Move; - -import java.util.function.Consumer; - -public final class Connect4GameThread extends BaseGameThread { - private static final int COLS = 7; - - public Connect4GameThread( - GameInformation info, - int myTurn, - Runnable onForfeit, - Runnable onExit, - Consumer onMessage, - Runnable onGameOver - ) { - super( - info, - myTurn, - onForfeit, - onExit, - onMessage, - onGameOver, - Connect4::new, - Connect4AI::new, - clickHandler -> new Connect4Canvas( - Color.GRAY, - (App.getHeight() / 4) * 3, - (App.getHeight() / 4) * 3, - cell -> clickHandler.accept(cell % COLS) - ) - ); - } - - public Connect4GameThread(GameInformation info) { - this(info, 0, null, null, null, null); - } - - @Override - protected void addCanvasToPrimary() { - primary.add(Pos.CENTER, canvas.getCanvas()); - } - - @Override - protected int getCurrentTurn() { - return game.getCurrentTurn(); - } - - @Override - protected char getSymbolForTurn(int turn) { - return turn == 0 ? 'X' : 'O'; - } - - @Override - protected String getNameForTurn(int turn) { - return turn == 0 ? "RED" : "BLUE"; - } - - private void drawMove(Move move) { - if (move.value() == 'X') - canvas.drawDot(Color.RED, move.position()); - else - canvas.drawDot(Color.BLUE, move.position()); - } - - @Override - protected void onMoveResponse(NetworkEvents.GameMoveResponse response) { - if (!isRunning.get()) return; - - char symbol = - response.player().equalsIgnoreCase(information.players[0].name) - ? (myTurn == 0 ? 'X' : 'O') - : (myTurn == 0 ? 'O' : 'X'); - - final Move move = new Move(Integer.parseInt(response.move()), symbol); - final GameState state = game.play(move); - - drawMove(move); - updateCanvas(); - - if (state != GameState.NORMAL) { - if (state == GameState.WIN) { - boolean p0 = response.player().equalsIgnoreCase(information.players[0].name); - primary.gameOver(p0, information.players[p0 ? 0 : 1].name); - gameOver(); - } else if (state == GameState.DRAW) { - primary.gameOver(false, ""); - gameOver(); - } - } - - setGameLabels(game.getCurrentTurn() == myTurn); - } - - @Override - protected void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { - if (!isRunning.get()) return; - - moveQueue.clear(); - int col = -1; - - if (information.players[0].isHuman) { - try { - col = moveQueue.take().position(); - } catch (InterruptedException _) { - } - } else { - final Move move = ai.findBestMove(game, information.players[0].computerDifficulty); - assert move != null; - col = move.position(); - } - - new EventFlow() - .addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short) col)) - .postEvent(); - } - - @Override - protected void localGameThread() { - while (isRunning.get()) { - final int current = game.getCurrentTurn(); - setGameLabels(current == myTurn); - - Move move = null; - - if (information.players[current].isHuman) { - try { - final Move wants = moveQueue.take(); - for (final Move legal : game.getLegalMoves()) { - if (legal.position() == wants.position() && - legal.value() == wants.value()) { - move = wants; - break; - } - } - } catch (InterruptedException _) { - } - } else { - final long start = System.currentTimeMillis(); - move = ai.findBestMove(game, information.players[current].computerDifficulty); - - if (information.players[current].computerThinkTime > 0) { - long elapsed = System.currentTimeMillis() - start; - long sleep = information.players[current].computerThinkTime * 1000L - elapsed; - try { - Thread.sleep((long) (sleep * Math.random())); - } catch (InterruptedException _) { - } - } - } - - if (move == null) continue; - - GameState state = game.play(move); - drawMove(move); - updateCanvas(); - - if (state != GameState.NORMAL) { - if (state == GameState.WIN) - primary.gameOver(information.players[current].isHuman, information.players[current].name); - else if (state == GameState.DRAW) - primary.gameOver(false, ""); - isRunning.set(false); - } - } - } - - private void updateCanvas() { - canvas.clearAll(); - - for (int i = 0; i < game.getBoard().length; i++) { - char c = game.getBoard()[i]; - if (c == 'X') canvas.drawDot(Color.RED, i); - else if (c == 'O') canvas.drawDot(Color.BLUE, i); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java new file mode 100644 index 0000000..a65805e --- /dev/null +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -0,0 +1,327 @@ +package org.toop.app.game; + +import javafx.animation.SequentialTransition; +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.ReversiCanvas; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.enumerators.GameState; +import org.toop.game.records.Move; +import org.toop.game.reversi.Reversi; +import org.toop.game.reversi.ReversiAI; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; + +import java.awt.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class ReversiGame { + private final GameInformation information; + + private final int myTurn; + private final Runnable onGameOver; + private final BlockingQueue moveQueue; + private Runnable onGameOver; + private final BlockingQueue moveQueue; + + private final Reversi game; + private final ReversiAI ai; + + private final GameView primary; + private final ReversiCanvas canvas; + + private final AtomicBoolean isRunning; + private final AtomicBoolean isPaused; + + public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { + this.information = information; + + this.myTurn = myTurn; + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue<>(); + + game = new Reversi(); + ai = new ReversiAI(); + + isRunning = new AtomicBoolean(true); + isPaused = new AtomicBoolean(false); + + if (onForfeit == null || onExit == null) { + primary = new GameView(null, () -> { + isRunning.set(false); + WidgetContainer.getCurrentView().transitionPrevious(); + }, null); + } else { + primary = new GameView(onForfeit, () -> { + isRunning.set(false); + onExit.run(); + }, onMessage); + } + + canvas = new ReversiCanvas(Color.BLACK, + (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? 'B' : 'W'; + + try { + moveQueue.put(new Move(cell, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'B' : 'W'; + + try { + moveQueue.put(new Move(cell, value)); + } catch (InterruptedException _) {} + } + } + },this::highlightCells); + + + + primary.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(primary); + + if (onForfeit == null || onExit == null) { + new Thread(this::localGameThread).start(); + setGameLabels(information.players[0].isHuman); + } else { + new EventFlow() + .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse); + + setGameLabels(myTurn == 0); + } + + updateCanvas(false); + } + + public ReversiGame(GameInformation information) { + this(information, 0, null, null, null,null); + } + + private void localGameThread() { + while (isRunning.get()) { + if (isPaused.get()) { + try { + Thread.sleep(200); + } catch (InterruptedException _) {} + + continue; + } + + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; + final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount(); + + primary.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + currentValue, + information.players[nextTurn].name); + + Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Move wants = moveQueue.take(); + final Move[] legalMoves = game.getLegalMoves(); + + for (final 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; + } + + canvas.setCurrentlyHighlightedMovesNull(); + final GameState state = game.play(move); + updateCanvas(true); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + primary.gameOver(true, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + primary.gameOver(false, ""); + if (state != GameState.NORMAL) { + if (state == GameState.WIN) { + view.gameOver(true, information.players[currentTurn].name); + } else if (state == GameState.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? 'B' : 'W'; + } else { + playerChar = myTurn == 0? 'W' : 'B'; + } + + final Move move = new Move(Integer.parseInt(response.move()), playerChar); + final GameState state = game.play(move); + + if (state != GameState.NORMAL) { + if (state == GameState.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + primary.gameOver(true, information.players[0].name); + gameOver(); + } else { + primary.gameOver(false, information.players[1].name); + gameOver(); + } + } else if (state == GameState.DRAW) { + view.gameOver(false, ""); + game.play(move); + } + } + + updateCanvas(false); + 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; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final 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 updateCanvas(boolean animate) { + // Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve. + canvas.clearAll(); + + for (int i = 0; i < game.getBoard().length; i++) { + if (game.getBoard()[i] == 'B') { + canvas.drawDot(Color.BLACK, i); + } else if (game.getBoard()[i] == 'W') { + canvas.drawDot(Color.WHITE, i); + } + } + + final Move[] flipped = game.getMostRecentlyFlippedPieces(); + + final SequentialTransition animation = new SequentialTransition(); + isPaused.set(true); + + final Color fromColor = game.getCurrentPlayer() == 'W'? Color.WHITE : Color.BLACK; + final Color toColor = game.getCurrentPlayer() == 'W'? Color.BLACK : Color.WHITE; + + if (animate && flipped != null) { + for (final Move flip : flipped) { + canvas.clear(flip.position()); + canvas.drawDot(fromColor, flip.position()); + animation.getChildren().addFirst(canvas.flipDot(fromColor, toColor, flip.position())); + } + } + + animation.setOnFinished(_ -> { + isPaused.set(false); + + final Move[] legalMoves = game.getLegalMoves(); + + for (final Move legalMove : legalMoves) { + canvas.drawLegalPosition(legalMove.position(), game.getCurrentPlayer()); + for (final Game.Move legalMove : legalMoves) { + canvas.drawLegalPosition(fromColor, legalMove.position()); + } + }); + + animation.play(); + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; + + primary.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + currentValue, + information.players[isMe? 1 : 0].name); + } + + private void highlightCells(int cellEntered) { + Move[] legalMoves = game.getLegalMoves(); + boolean isLegalMove = false; + for (Move move : legalMoves) { + if (move.position() == cellEntered){ + isLegalMove = true; + break; + } + } + + if (cellEntered >= 0){ + Move[] moves = null; + if (isLegalMove) { + moves = game.getFlipsForPotentialMove( + new Point(cellEntered%game.getColumnSize(),cellEntered/game.getRowSize()), + game.getCurrentPlayer()); + } + canvas.drawHighlightDots(moves); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/ReversiGameThread.java b/app/src/main/java/org/toop/app/game/ReversiGameThread.java deleted file mode 100644 index 05c1706..0000000 --- a/app/src/main/java/org/toop/app/game/ReversiGameThread.java +++ /dev/null @@ -1,241 +0,0 @@ -package org.toop.app.game; - -import javafx.animation.SequentialTransition; -import javafx.geometry.Pos; -import javafx.scene.paint.Color; -import org.toop.app.App; -import org.toop.app.GameInformation; -import org.toop.app.canvas.ReversiCanvas; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.events.NetworkEvents; -import org.toop.game.enumerators.GameState; -import org.toop.game.records.Move; -import org.toop.game.reversi.Reversi; -import org.toop.game.reversi.ReversiAI; - -import java.awt.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -public final class ReversiGameThread extends BaseGameThread { - public ReversiGameThread(GameInformation info, int myTurn, - Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { - super(info, myTurn, onForfeit, onExit, onMessage, onGameOver, - Reversi::new, - ReversiAI::new, - clickHandler -> new ReversiCanvas( - Color.BLACK, - (App.getHeight() / 4) * 3, - (App.getHeight() / 4) * 3, - clickHandler - ) - ); - - canvas.setOnCellEntered(this::highlightCells); - } - - public ReversiGameThread(GameInformation info) { - this(info, 0, null, null, null, null); - } - - @Override - protected void addCanvasToPrimary() { - primary.add(Pos.CENTER, canvas.getCanvas()); - } - - @Override - protected int getCurrentTurn() { - return game.getCurrentTurn(); - } - - @Override - protected char getSymbolForTurn(int turn) { - return turn == 0 ? 'B' : 'W'; - } - - @Override - protected String getNameForTurn(int turn) { - return turn == 0 ? "BLACK" : "WHITE"; - } - - private void drawMove(Move move) { - if (move.value() == 'B') canvas.drawDot(Color.BLACK, move.position()); - else canvas.drawDot(Color.WHITE, move.position()); - } - - @Override - protected void onMoveResponse(NetworkEvents.GameMoveResponse response) { - if (!isRunning.get()) return; - - char playerChar = - response.player().equalsIgnoreCase(information.players[0].name) - ? (myTurn == 0 ? 'B' : 'W') - : (myTurn == 0 ? 'W' : 'B'); - - final Move move = new Move(Integer.parseInt(response.move()), playerChar); - final GameState state = game.play(move); - - updateCanvas(true); - - if (state != GameState.NORMAL) { - if (state == GameState.WIN) { - boolean p0 = response.player().equalsIgnoreCase(information.players[0].name); - primary.gameOver(p0, information.players[p0 ? 0 : 1].name); - gameOver(); - } else if (state == GameState.DRAW) { - primary.gameOver(false, ""); - gameOver(); - } - } - - setGameLabels(game.getCurrentTurn() == myTurn); - } - - @Override - protected void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { - if (!isRunning.get()) return; - - moveQueue.clear(); - int position = -1; - - if (information.players[0].isHuman) { - try { - position = moveQueue.take().position(); - } catch (InterruptedException _) { - } - } else { - final 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(); - } - - @Override - protected void localGameThread() { - while (isRunning.get()) { - if (isPaused.get()) { - try { - Thread.sleep(200); - } catch (InterruptedException _) {} - - continue; - } - - final int currentTurn = game.getCurrentTurn(); - setGameLabels(currentTurn == myTurn); - - Move move = null; - - if (information.players[currentTurn].isHuman) { - try { - final Move wants = moveQueue.take(); - final Move[] legalMoves = game.getLegalMoves(); - - for (final Move legalMove : legalMoves) { - if (legalMove.position() == wants.position() && - legalMove.value() == wants.value()) { - move = wants; - break; - } - } - } catch (InterruptedException _) { - } - - } else { - long start = System.currentTimeMillis(); - move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); - - if (information.players[currentTurn].computerThinkTime > 0) { - long elapsed = System.currentTimeMillis() - start; - long sleep = information.players[currentTurn].computerThinkTime * 1000L - elapsed; - - try { - Thread.sleep((long) (sleep * Math.random())); - } catch (InterruptedException _) { - } - } - } - - if (move == null) continue; - - GameState state = game.play(move); - updateCanvas(true); - - if (state != GameState.NORMAL) { - if (state == GameState.WIN) { - primary.gameOver(information.players[currentTurn].isHuman, - information.players[currentTurn].name); - } else if (state == GameState.DRAW) { - primary.gameOver(false, ""); - } - - isRunning.set(false); - } - } - } - - private void updateCanvas(boolean animate) { - canvas.clearAll(); - - for (int i = 0; i < game.getBoard().length; i++) { - char c = game.getBoard()[i]; - if (c == 'B') canvas.drawDot(Color.BLACK, i); - else if (c == 'W') canvas.drawDot(Color.WHITE, i); - } - - final Move[] flipped = game.getMostRecentlyFlippedPieces(); - - final SequentialTransition anim = new SequentialTransition(); - isPaused.set(true); - - final Color from = game.getCurrentPlayer() == 'W' ? Color.WHITE : Color.BLACK; - final Color to = game.getCurrentPlayer() == 'W' ? Color.BLACK : Color.WHITE; - - if (animate && flipped != null) { - for (final Move flip : flipped) { - canvas.clear(flip.position()); - canvas.drawDot(from, flip.position()); - anim.getChildren().addFirst(canvas.flipDot(from, to, flip.position())); - } - } - - anim.setOnFinished(_ -> { - isPaused.set(false); - - for (final Move m : game.getLegalMoves()) { - canvas.drawLegalPosition(m.position(), game.getCurrentPlayer()); - } - }); - - anim.play(); - } - - private void highlightCells(int cell) { - Move[] legal = game.getLegalMoves(); - boolean isLegal = false; - - for (Move m : legal) { - if (m.position() == cell) { - isLegal = true; - break; - } - } - - if (cell >= 0) { - Move[] flips = null; - - if (isLegal) { - flips = game.getFlipsForPotentialMove( - new Point(cell % game.getColumnSize(), cell / game.getRowSize()), - game.getCurrentPlayer() - ); - } - - canvas.drawHighlightDots(flips); - } - } -} \ 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 new file mode 100644 index 0000000..84fc237 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -0,0 +1,250 @@ +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.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.enumerators.GameState; +import org.toop.game.records.Move; +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; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class TicTacToeGame { + private final GameInformation information; + + private final int myTurn; + private Runnable onGameOver; + private final BlockingQueue moveQueue; + + private final TicTacToe game; + private final TicTacToeAI ai; + + private final GameView primary; + private final TicTacToeCanvas canvas; + + private final AtomicBoolean isRunning; + + public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { + this.information = information; + + this.myTurn = myTurn; + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue(); + + game = new TicTacToe(); + ai = new TicTacToeAI(); + + isRunning = new AtomicBoolean(true); + + if (onForfeit == null || onExit == null) { + primary = new GameView(null, () -> { + isRunning.set(false); + WidgetContainer.getCurrentView().transitionPrevious(); + }, null); + } else { + primary = new GameView(onForfeit, () -> { + isRunning.set(false); + onExit.run(); + }, onMessage); + } + + 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 Move(cell, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'X' : 'O'; + + try { + moveQueue.put(new Move(cell, value)); + } catch (InterruptedException _) {} + } + } + }); + + primary.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(primary); + + 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); + + setGameLabels(myTurn == 0); + } + } + + public TicTacToeGame(GameInformation information) { + this(information, 0, null, null, null, null); + } + + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "X" : "O"; + final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount(); + + primary.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + currentValue, + information.players[nextTurn].name); + + Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Move wants = moveQueue.take(); + final Move[] legalMoves = game.getLegalMoves(); + + for (final 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 GameState 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 != GameState.NORMAL) { + if (state == GameState.WIN) { + primary.gameOver(true, information.players[currentTurn].name); + } else if (state == GameState.DRAW) { + primary.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 Move move = new Move(Integer.parseInt(response.move()), playerChar); + final GameState state = game.play(move); + + if (state != GameState.NORMAL) { + if (state == GameState.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + primary.gameOver(true, information.players[0].name); + gameOver(); + } else { + primary.gameOver(false, information.players[1].name); + gameOver(); + } + } else if (state == GameState.DRAW) { + if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over. + primary.gameOver(false, ""); + gameOver(); + } + } + } + + 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 gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + if (!isRunning.get()) { + return; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final Move 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 setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "X" : "O"; + + primary.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + currentValue, + information.players[isMe? 1 : 0].name); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java b/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java index cf0db79..a71bf07 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java @@ -1,19 +1,19 @@ 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.TicTacToeCanvas; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.events.NetworkEvents; -import org.toop.game.enumerators.GameState; -import org.toop.game.records.Move; +import org.toop.game.Game; import org.toop.game.tictactoe.TicTacToe; import org.toop.game.tictactoe.TicTacToeAI; import java.util.function.Consumer; +import javafx.geometry.Pos; +import javafx.scene.paint.Color; + public final class TicTacToeGameThread extends BaseGameThread { public TicTacToeGameThread(GameInformation info, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { super(info, myTurn, onForfeit, onExit, onMessage, onGameOver, @@ -47,7 +47,7 @@ public final class TicTacToeGameThread extends BaseGameThread nodeMap; + + protected View(boolean mainView, String cssClass) { + this.mainView = mainView; + + view = new StackPane(); + view.getStyleClass().add(cssClass); + + nodeMap = new HashMap(); + } + + public void add(Pos position, Node node) { + assert node != null; + + StackPane.setAlignment(node, position); + view.getChildren().add(node); + } + + protected Region hspacer() { + final Region hspacer = new Region(); + hspacer.getStyleClass().add("hspacer"); + + return hspacer; + } + + protected Region vspacer() { + final Region vspacer = new Region(); + vspacer.getStyleClass().add("vspacer"); + + return vspacer; + } + + protected ScrollPane fit(String identifier, String cssClass, Node node) { + assert node != null; + + final ScrollPane fit = new ScrollPane(node); + fit.getStyleClass().add(cssClass); + + fit.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + fit.setFitToWidth(true); + fit.setFitToHeight(true); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, fit); + } + + return fit; + } + + protected ScrollPane fit(String identifier, Node node) { + return fit(identifier, "fit", node); + } + + protected ScrollPane fit(Node node) { + return fit("", node); + } + + protected HBox hbox(String identifier, String cssClass, Node... nodes) { + assert !nodeMap.containsKey(identifier); + + final HBox hbox = new HBox(); + hbox.getStyleClass().add(cssClass); + hbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + for (final Node node : nodes) { + if (node != null) { + hbox.getChildren().add(node); + } + } + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, hbox); + } + + return hbox; + } + + protected HBox hbox(String identifier, Node... nodes) { + return hbox(identifier, "container", nodes); + } + + protected HBox hbox(Node... nodes) { + return hbox("", nodes); + } + + protected HBox hboxFill(String identifier, String cssClass, Node... nodes) { + final HBox hbox = hbox(identifier, cssClass, nodes); + + for (final Node node : hbox.getChildren()) { + if (node instanceof Region) { + ((Region)node).setMaxHeight(Double.MAX_VALUE); + } + } + + return hbox; + } + + protected HBox hboxFill(String identifier, Node... nodes) { + final HBox hbox = hbox(identifier, nodes); + + for (final Node node : hbox.getChildren()) { + if (node instanceof Region) { + ((Region)node).setMaxHeight(Double.MAX_VALUE); + } + } + + return hbox; + } + + protected HBox hboxFill(Node... nodes) { + final HBox hbox = hbox(nodes); + + for (final Node node : hbox.getChildren()) { + if (node instanceof Region) { + ((Region)node).setMaxHeight(Double.MAX_VALUE); + } + } + + return hbox; + } + + protected VBox vbox(String identifier, String cssClass, Node... nodes) { + assert !nodeMap.containsKey(identifier); + + final VBox vbox = new VBox(); + vbox.getStyleClass().add(cssClass); + vbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + for (final Node node : nodes) { + if (node != null) { + vbox.getChildren().add(node); + } + } + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, vbox); + } + + return vbox; + } + + protected VBox vbox(String identifier, Node... nodes) { + return vbox(identifier, "container", nodes); + } + + protected VBox vbox(Node... nodes) { + return vbox("", nodes); + } + + protected VBox vboxFill(String identifier, String cssClass, Node... nodes) { + final VBox vbox = vbox(identifier, cssClass, nodes); + + for (final Node node : vbox.getChildren()) { + if (node instanceof Region) { + ((Region)node).setMaxWidth(Double.MAX_VALUE); + } + } + + return vbox; + } + + protected VBox vboxFill(String identifier, Node... nodes) { + final VBox vbox = vbox(identifier, nodes); + + for (final Node node : vbox.getChildren()) { + if (node instanceof Region) { + ((Region)node).setMaxWidth(Double.MAX_VALUE); + } + } + + return vbox; + } + + protected VBox vboxFill(Node... nodes) { + final VBox vbox = vbox(nodes); + + for (final Node node : vbox.getChildren()) { + if (node instanceof Region) { + ((Region)node).setMaxWidth(Double.MAX_VALUE); + } + } + + return vbox; + } + + protected Separator separator(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final Separator separator = new Separator(); + separator.getStyleClass().add(cssClass); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, separator); + } + + return separator; + } + + protected Separator separator(String identifier) { + return separator(identifier, "separator"); + } + + protected Separator separator() { + return separator(""); + } + + protected Text header(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final Text header = new Text(); + header.getStyleClass().add(cssClass); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, header); + } + + return header; + } + + protected Text header(String identifier) { + return header(identifier, "header"); + } + + protected Text header() { + return header(""); + } + + protected Text text(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final Text text = new Text(); + text.getStyleClass().add(cssClass); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, text); + } + + return text; + } + + protected Text text(String identifier) { + return text(identifier, "text"); + } + + protected Text text() { + return text(""); + } + + protected Button button(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final Button button = new Button(); + button.getStyleClass().add(cssClass); + + button.setOnMouseClicked(_ -> { + new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); + }); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, button); + } + + return button; + } + + protected Button button(String identifier) { + return button(identifier, "button"); + } + + protected Button button() { + return button(""); + } + + protected Slider slider(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final Slider slider = new Slider(); + slider.getStyleClass().add(cssClass); + + slider.setMinorTickCount(0); + slider.setMajorTickUnit(1); + slider.setBlockIncrement(1); + + slider.setSnapToTicks(true); + slider.setShowTickLabels(true); + + slider.setOnMouseClicked(_ -> { + new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); + }); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, slider); + } + + return slider; + } + + protected Slider slider(String identifier) { + return slider(identifier, "slider"); + } + + protected Slider slider() { + return slider(""); + } + + protected TextField input(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final TextField input = new TextField(); + input.getStyleClass().add(cssClass); + + input.setOnMouseClicked(_ -> { + new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); + }); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, input); + } + + return input; + } + + protected TextField input(String identifier) { + return input(identifier, "input"); + } + + protected TextField input() { + return input(""); + } + + protected ComboBox combobox(String identifier, String cssClass) { + assert !nodeMap.containsKey(identifier); + + final ComboBox combobox = new ComboBox(); + combobox.getStyleClass().add(cssClass); + + combobox.setOnMouseClicked(_ -> { + new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); + }); + + if (!identifier.isEmpty()) { + nodeMap.put(identifier, combobox); + } + + return combobox; + } + + protected ComboBox combobox(String identifier) { + return combobox(identifier, "combo-box"); + } + + protected ComboBox combobox() { + return combobox(""); + } + + @SuppressWarnings("unchecked") + protected T get(String identifier) { + assert nodeMap.containsKey(identifier); + return (T) nodeMap.get(identifier); + } + + protected void clear() { + view.getChildren().clear(); + nodeMap.clear(); + } + + public boolean isMainView() { return mainView; } + public Region getView() { return view; } + + public abstract void setup(); + public void cleanup() { + clear(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/ViewStack.java b/app/src/main/java/org/toop/app/view/ViewStack.java new file mode 100644 index 0000000..20d0ff3 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/ViewStack.java @@ -0,0 +1,105 @@ +package org.toop.app.view; + +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; + +import java.util.Stack; + +public final class ViewStack { + private static boolean setup = false; + + private static StackPane root; + + private static View active; + private static Stack stack; + + public static void setup(Scene scene) { + assert scene != null; + + if (setup) { + return; + } + + root = new StackPane(); + + active = null; + stack = new Stack(); + + scene.setRoot(root); + + setup = true; + } + + public static void cleanup() { + assert setup; + + final var count = stack.size(); + + for (int i = 0; i < count; i++) { + pop(); + } + + if (active != null) { + active.cleanup(); + } + + setup = false; + } + + public static void reload() { + assert setup; + + for (final var view : stack) { + view.cleanup(); + } + + if (active != null) { + active.cleanup(); + active.setup(); + } + + for (final var view : stack) { + view.setup(); + } + } + + public static void push(View view) { + assert setup; + assert view != null; + + if (view.isMainView()) { + Platform.runLater(() -> { + if (active != null) { + root.getChildren().removeFirst(); + active.cleanup(); + } + + root.getChildren().addFirst(view.getView()); + view.setup(); + + active = view; + }); + } else { + Platform.runLater(() -> { + stack.push(view); + root.getChildren().addLast(view.getView()); + view.setup(); + }); + } + } + + public static void pop() { + assert setup; + + if (stack.isEmpty()) { + return; + } + + Platform.runLater(() -> { + final var last = stack.pop(); + root.getChildren().removeLast(); + last.cleanup(); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/displays/SongDisplay.java b/app/src/main/java/org/toop/app/view/displays/SongDisplay.java new file mode 100644 index 0000000..f21e2aa --- /dev/null +++ b/app/src/main/java/org/toop/app/view/displays/SongDisplay.java @@ -0,0 +1,129 @@ +package org.toop.app.view.displays; + +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import org.toop.app.widget.Widget; +import org.toop.framework.audio.AudioEventListener; +import org.toop.framework.audio.events.AudioEvents; +import org.toop.framework.eventbus.EventFlow; +import javafx.geometry.Pos; +import javafx.scene.text.Text; +import org.toop.framework.eventbus.GlobalEventBus; + +public class SongDisplay extends VBox implements Widget { + + private final Text songTitle; + private final ProgressBar progressBar; + private final Text progressText; + private boolean paused = false; + + public SongDisplay() { + new EventFlow() + .listen(this::updateTheSong); + + setAlignment(Pos.CENTER); + getStyleClass().add("song-display"); + + songTitle = new Text("song playing"); + songTitle.getStyleClass().add("song-title"); + + progressBar = new ProgressBar(0); + progressBar.getStyleClass().add("progress-bar"); + + progressText = new Text("0:00/0:00"); + progressText.getStyleClass().add("progress-text"); + + Button skipButton = new Button(">>"); + Button pauseButton = new Button("⏸"); + Button previousButton = new Button("<<"); + + skipButton.getStyleClass().setAll("skip-button"); + pauseButton.getStyleClass().setAll("pause-button"); + previousButton.getStyleClass().setAll("previous-button"); + + skipButton.setOnAction( event -> { + GlobalEventBus.post(new AudioEvents.SkipMusic()); + paused = false; + pauseButton.setText(getPlayString(paused)); + }); + + pauseButton.setOnAction(event -> { + GlobalEventBus.post(new AudioEvents.PauseMusic()); + paused = !paused; + pauseButton.setText(getPlayString(paused)); + }); + + previousButton.setOnAction( event -> { + GlobalEventBus.post(new AudioEvents.PreviousMusic()); + paused = false; + pauseButton.setText(getPlayString(paused)); + }); + + HBox control = new HBox(10, previousButton, pauseButton, skipButton); + control.setAlignment(Pos.CENTER); + control.getStyleClass().add("controls"); + + getChildren().addAll(songTitle, progressBar, progressText, control); + } + + private void updateTheSong(AudioEvents.PlayingMusic event) { + Platform.runLater(() -> { + String text = event.name(); + text = text.substring(0, text.length() - 4); + songTitle.setText(text); + double currentPos = event.currentPosition(); + double duration = event.duration(); + if (currentPos / duration > 0.05) { + double progress = currentPos / duration; + progressBar.setProgress(progress); + } + else if (currentPos / duration < 0.05) { + progressBar.setProgress(0.05); + } + progressText.setText(getTimeString(event.currentPosition(), event.duration())); + }); + } + + private String getTimeString(long position, long duration) { + long positionMinutes = position / 60; + long durationMinutes = duration / 60; + long positionSeconds = position % 60; + long durationSeconds = duration % 60; + String positionSecondsStr = String.valueOf(positionSeconds); + String durationSecondsStr = String.valueOf(durationSeconds); + + if (positionSeconds < 10) { + positionSecondsStr = "0" + positionSeconds; + } + if (durationSeconds < 10) { + durationSecondsStr = "0" + durationSeconds; + } + + String time = positionMinutes + ":" + positionSecondsStr + " / " + durationMinutes + ":" + durationSecondsStr; + return time; + } + + private String getPlayString(boolean paused) { + if (paused) { + return "▶"; + } + else { + return "⏸"; + } + } + + @Override + public Node getNode() { + return this; + } +} + + + + + 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 new file mode 100644 index 0000000..36184f6 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/ChallengeView.java @@ -0,0 +1,127 @@ +package org.toop.app.view.views; + +import org.toop.app.GameInformation; +import org.toop.app.Server; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Slider; +import javafx.scene.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public final class ChallengeView extends View { + private final GameInformation.Player playerInformation; + + private final String challenger; + private final String game; + + private final Consumer onAccept; + + public ChallengeView(String challenger, String game, Consumer onAccept) { + super(false, "bg-popup"); + + playerInformation = new GameInformation.Player(); + + this.challenger = challenger; + this.game = game; + + this.onAccept = onAccept; + } + + @Override + public void setup() { + final Text challengeText = text(); + challengeText.setText(AppContext.getString("you-were-challenged-by")); + + final Text challengerHeader = header(); + challengerHeader.setText(challenger); + + final Text gameText = text(); + gameText.setText(AppContext.getString("to-a-game-of") + " " + game); + + final Button acceptButton = button(); + acceptButton.setText(AppContext.getString("accept")); + acceptButton.setOnAction(_ -> { + onAccept.accept(playerInformation); + }); + + final Button denyButton = button(); + denyButton.setText(AppContext.getString("deny")); + denyButton.setOnAction(_ -> { + ViewStack.pop(); + }); + + final List nodes = new ArrayList<>(); + + if (playerInformation.isHuman) { + final Button playerToggle = button(); + playerToggle.setText(AppContext.getString("player")); + playerToggle.setOnAction(_ -> { + playerInformation.isHuman = false; + cleanup(); + setup(); + }); + + nodes.add(vbox(playerToggle)); + } else { + final Button computerToggle = button(); + computerToggle.setText(AppContext.getString("computer")); + computerToggle.setOnAction(_ -> { + playerInformation.isHuman = true; + cleanup(); + setup(); + }); + + nodes.add(vbox(computerToggle)); + + final Text computerDifficultyText = text(); + computerDifficultyText.setText(AppContext.getString("computer-difficulty")); + + final Slider computerDifficultySlider = slider(); + computerDifficultySlider.setMin(0); + computerDifficultySlider.setMax(Server.gameToType(game).getMaxDepth()); + computerDifficultySlider.setValue(playerInformation.computerDifficulty); + computerDifficultySlider.valueProperty().addListener((_, _, newValue) -> { + playerInformation.computerDifficulty = newValue.intValue(); + }); + + nodes.add(vbox(computerDifficultyText, computerDifficultySlider)); + } + + final SongDisplay songdisplay = new SongDisplay(); + + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.CENTER, + fit(hboxFill( + vboxFill( + challengeText, + challengerHeader, + gameText, + separator(), + + hboxFill( + acceptButton, + denyButton + ) + ), + + vboxFill( + nodes.toArray(new Node[0]) + ) + )) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/CreditsView.java b/app/src/main/java/org/toop/app/view/views/CreditsView.java new file mode 100644 index 0000000..3f02ef8 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/CreditsView.java @@ -0,0 +1,110 @@ +package org.toop.app.view.views; + +import org.toop.app.App; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.local.AppContext; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.util.Duration; + +public final class CreditsView extends View { + public CreditsView() { + super(false, "bg-primary"); + } + + @Override + public void setup() { + final Text scrumMasterHeader = header(); + scrumMasterHeader.setText(AppContext.getString("scrum-master") + ": Stef"); + + final Text productOwnerHeader = header(); + productOwnerHeader.setText(AppContext.getString("product-owner") + ": Omar"); + + final Text mergeCommanderHeader = header(); + mergeCommanderHeader.setText(AppContext.getString("merge-commander") + ": Bas"); + + final Text localizationHeader = header(); + localizationHeader.setText(AppContext.getString("localization") + ": Ticho"); + + final Text aiHeader = header(); + aiHeader.setText(AppContext.getString("ai") + ": Michiel"); + + final Text developersHeader = header(); + developersHeader.setText(AppContext.getString("developers") + ": Michiel, Bas, Stef, Omar, Ticho"); + + final Text moralSupportHeader = header(); + moralSupportHeader.setText(AppContext.getString("moral-support") + ": Wesley"); + + final Text openglHeader = header(); + openglHeader.setText(AppContext.getString("opengl") + ": Omar"); + + final SongDisplay songdisplay = new SongDisplay(); + + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.CENTER, + fit("credits-fit", vboxFill("credits-container", "credits-container", + vbox("credits-spacer-top", ""), + + scrumMasterHeader, + productOwnerHeader, + mergeCommanderHeader, + localizationHeader, + aiHeader, + developersHeader, + moralSupportHeader, + openglHeader, + + vbox("credits-spacer-bottom", "") + )) + ); + + final Button backButton = button(); + backButton.setText(AppContext.getString("back")); + backButton.setOnAction(_ -> { ViewStack.pop(); }); + + add(Pos.BOTTOM_LEFT, + vboxFill( + backButton + ) + ); + + playCredits(100, 20); + } + + private void playCredits(int lineHeight, int length) { + final ScrollPane creditsFit = get("credits-fit"); + creditsFit.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + creditsFit.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + + final VBox creditsContainer = get("credits-container"); + creditsContainer.setSpacing(lineHeight); + + final VBox creditsSpacerTop = get("credits-spacer-top"); + creditsSpacerTop.setMinHeight(App.getHeight() - lineHeight); + + final VBox creditsSpacerBottom = get("credits-spacer-bottom"); + creditsSpacerBottom.setMinHeight(App.getHeight() - lineHeight); + + final Timeline timeline = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(creditsFit.vvalueProperty(), 0.0)), + new KeyFrame(Duration.seconds(length), new KeyValue(creditsFit.vvalueProperty(), 1.0)) + ); + + timeline.setCycleCount(Timeline.INDEFINITE); + timeline.play(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/ErrorView.java b/app/src/main/java/org/toop/app/view/views/ErrorView.java new file mode 100644 index 0000000..5dc85fa --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/ErrorView.java @@ -0,0 +1,45 @@ +package org.toop.app.view.views; + +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.text.Text; + +public final class ErrorView extends View { + private final String error; + + public ErrorView(String error) { + super(false, "bg-popup"); + this.error = error; + } + + @Override + public void setup() { + final Text errorHeader = header(); + errorHeader.setText(AppContext.getString("error")); + + final Text errorText = text(); + errorText.setText(error); + + final Button okButton = button(); + okButton.setText(AppContext.getString("ok")); + okButton.setOnAction(_ -> { ViewStack.pop(); }); + + add(Pos.CENTER, + vboxFill( + errorHeader, + separator(), + + vspacer(), + errorText, + vspacer(), + + separator(), + okButton + ) + ); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..df69240 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/GameView.java @@ -0,0 +1,184 @@ +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; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.text.Text; + +import java.util.function.Consumer; + +public final class GameView extends View { + private static class GameOverView extends View { + private final boolean iWon; + private final String winner; + + public GameOverView(boolean iWon, String winner) { + super(false, "bg-popup"); + + this.iWon = iWon; + this.winner = winner; + } + + @Override + public void setup() { + final Text gameOverHeader = header(); + gameOverHeader.setText(AppContext.getString("game-over")); + + final Button okButton = button(); + okButton.setText(AppContext.getString("ok")); + okButton.setOnAction(_ -> { ViewStack.pop(); }); + + Text gameOverText = text(); + + if (winner.isEmpty()) { + gameOverText.setText(AppContext.getString("the-game-ended-in-a-draw")); + } else { + if (iWon) { + gameOverText.setText(AppContext.getString("you-win") + " " + winner); + } else { + gameOverText.setText(AppContext.getString("you-lost-against") + " " + winner); + } + } + + add(Pos.CENTER, + fit(vboxFill( + gameOverHeader, + separator(), + + vspacer(), + gameOverText, + vspacer(), + + separator(), + okButton + )) + ); + } + } + + private final Button forfeitButton; + private final Button exitButton; + + private final Text currentPlayerHeader; + private final Text currentMoveHeader; + + private final Text nextPlayerHeader; + + private final ListView chatListView; + private final TextField chatInput; + + public GameView(Runnable onForfeit, Runnable onExit, Consumer onMessage) { + assert onExit != null; + + super(true, "bg-primary"); + + if (onForfeit != null) { + forfeitButton = button(); + forfeitButton.setText(AppContext.getString("forfeit")); + forfeitButton.setOnAction(_ -> onForfeit.run()); + } else { + forfeitButton = null; + } + + final SongDisplay songdisplay = new SongDisplay(); + + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + if (onMessage != null) { + chatListView = new ListView(); + + chatInput = input(); + chatInput.setOnAction(_ -> { + onMessage.accept(chatInput.getText()); + chatInput.setText(""); + }); + } else { + chatListView = null; + chatInput = null; + } + + exitButton = button(); + exitButton.setText(AppContext.getString("exit")); + exitButton.setOnAction(_ -> onExit.run()); + + currentPlayerHeader = header("", "current-player"); + currentMoveHeader = header(); + + nextPlayerHeader = header(); + } + + @Override + public void setup() { + add(Pos.TOP_RIGHT, + fit(vboxFill( + currentPlayerHeader, + + hboxFill( + separator(), + currentMoveHeader, + separator() + ), + + nextPlayerHeader + )) + ); + + add(Pos.BOTTOM_LEFT, + vboxFill( + forfeitButton, + exitButton + ) + ); + + if (chatListView != null) { + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + chatListView, + chatInput + ) + )); + } + } + + public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) { + Platform.runLater(() -> { + currentPlayerHeader.setText(currentPlayer); + currentMoveHeader.setText(currentMove); + + nextPlayerHeader.setText(nextPlayer); + + if (isMe) { + currentPlayerHeader.getStyleClass().add("my-turn"); + } else { + currentPlayerHeader.getStyleClass().remove("my-turn"); + } + }); + + } + + public void updateChat(String message) { + if (chatListView == null) { + return; + } + + final Text messageText = text(); + messageText.setText(message); + + chatListView.getItems().add(messageText); + } + + public void gameOver(boolean iWon, String winner) { + ViewStack.push(new GameOverView(iWon, winner)); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..c44d784 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java @@ -0,0 +1,171 @@ +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.TicTacToeGameThread; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Slider; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import java.util.ArrayList; +import java.util.List; + +public final class LocalMultiplayerView extends View { + private final GameInformation information; + + public LocalMultiplayerView(GameInformation information) { + super(true, "bg-primary"); + this.information = information; + } + + public LocalMultiplayerView(GameInformation.Type type) { + this(new GameInformation(type)); + } + + @Override + public void setup() { + final Button playButton = button(); + playButton.setText(AppContext.getString("play")); + playButton.setOnAction(_ -> { + for (final GameInformation.Player player : information.players) { + if (player.name.isEmpty()) { + ViewStack.push(new ErrorView(AppContext.getString("please-enter-your-name"))); + return; + } + } + + switch (information.type) { + case TICTACTOE: new TicTacToeGameThread(information); break; + case REVERSI: new ReversiGame(information); break; + case CONNECT4: new Connect4Game(information); break; + // case BATTLESHIP: new BattleshipGame(information); break; + } + }); + + final SongDisplay songdisplay = new SongDisplay(); + + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.CENTER, + fit(vboxFill( + hbox( + setupPlayers() + ), + + separator(), + playButton + )) + ); + + final Button backButton = button(); + backButton.setText(AppContext.getString("back")); + backButton.setOnAction(_ -> { ViewStack.push(new MainView()); }); + + add(Pos.BOTTOM_LEFT, + vboxFill( + backButton + ) + ); + } + + private VBox[] setupPlayers() { + final VBox[] playerBoxes = new VBox[information.type.getPlayerCount()]; + + for (int i = 0; i < playerBoxes.length; i++) { + final int index = i; + + List nodes = new ArrayList<>(); + + final Text playerHeader = header(); + playerHeader.setText(AppContext.getString("player") + " #" + (i + 1)); + + nodes.add(playerHeader); + nodes.add(separator()); + + final Text nameText = text(); + nameText.setText(AppContext.getString("name")); + + if (information.players[i].isHuman) { + final Button playerToggle = button(); + playerToggle.setText(AppContext.getString("player")); + playerToggle.setOnAction(_ -> { + information.players[index].isHuman = false; + cleanup(); + setup(); + }); + + nodes.add(vboxFill(playerToggle)); + + final TextField playerNameInput = input(); + playerNameInput.setPromptText(AppContext.getString("enter-your-name")); + playerNameInput.setText(information.players[i].name); + playerNameInput.textProperty().addListener((_, _, newValue) -> { + information.players[index].name = newValue; + }); + + nodes.add(vboxFill(nameText, playerNameInput)); + } else { + final Button computerToggle = button(); + computerToggle.setText(AppContext.getString("computer")); + computerToggle.setOnAction(_ -> { + information.players[index].isHuman = true; + cleanup(); + setup(); + }); + + nodes.add(vboxFill(computerToggle)); + + information.players[i].name = "Pism Bot V" + i; + + final Text computerNameText = text(); + computerNameText.setText(information.players[index].name); + + nodes.add(vboxFill(nameText, computerNameText)); + + final Text computerDifficultyText = text(); + computerDifficultyText.setText(AppContext.getString("computer-difficulty")); + + final Slider computerDifficultySlider = slider(); + computerDifficultySlider.setMin(0); + computerDifficultySlider.setMax(information.type.getMaxDepth()); + computerDifficultySlider.setValue(information.players[i].computerDifficulty); + computerDifficultySlider.valueProperty().addListener((_, _, newValue) -> { + information.players[index].computerDifficulty = newValue.intValue(); + }); + + nodes.add(vboxFill(computerDifficultyText, computerDifficultySlider)); + + final Text computerThinkTimeText = text(); + computerThinkTimeText.setText(AppContext.getString("computer-think-time")); + + final Slider computerThinkTimeSlider = slider(); + computerThinkTimeSlider.setMin(0); + computerThinkTimeSlider.setMax(5); + computerThinkTimeSlider.setValue(information.players[i].computerThinkTime); + computerThinkTimeSlider.valueProperty().addListener((_, _, newValue) -> { + information.players[index].computerThinkTime = newValue.intValue(); + }); + + nodes.add(vboxFill(computerThinkTimeText, computerThinkTimeSlider)); + } + + playerBoxes[i] = vboxFill(nodes.toArray(new Node[0])); + } + + return playerBoxes; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..c4a1c4b --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/LocalView.java @@ -0,0 +1,57 @@ +package org.toop.app.view.views; + +import org.toop.app.GameInformation; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; + +public final class LocalView extends View { + public LocalView() { + super(true, "bg-primary"); + } + + @Override + public void setup() { + final Button ticTacToeButton = button(); + ticTacToeButton.setText(AppContext.getString("tic-tac-toe")); + ticTacToeButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.TICTACTOE)); }); + + final Button reversiButton = button(); + reversiButton.setText(AppContext.getString("reversi")); + reversiButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.REVERSI)); }); + + 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, + connect4Button + )) + ); + + final Button backButton = button(); + backButton.setText(AppContext.getString("back")); + backButton.setOnAction(_ -> { ViewStack.push(new MainView()); }); + + final SongDisplay songdisplay = new SongDisplay(); + + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.BOTTOM_LEFT, + vboxFill( + backButton + ) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/MainView.java b/app/src/main/java/org/toop/app/view/views/MainView.java new file mode 100644 index 0000000..10fa0fc --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/MainView.java @@ -0,0 +1,55 @@ +package org.toop.app.view.views; + +import org.toop.app.App; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.local.AppContext; +import org.toop.app.view.displays.SongDisplay; +import javafx.geometry.Pos; +import javafx.scene.control.Button; + +public final class MainView extends View { + public MainView() { + super(true, "bg-primary"); + } + + @Override + public void setup() { + final Button localButton = button(); + localButton.setText(AppContext.getString("local")); + localButton.setOnAction(_ -> { ViewStack.push(new LocalView()); }); + + final Button onlineButton = button(); + onlineButton.setText(AppContext.getString("online")); + onlineButton.setOnAction(_ -> { ViewStack.push(new OnlineView()); }); + + final Button creditsButton = button(); + creditsButton.setText(AppContext.getString("credits")); + creditsButton.setOnAction(_ -> { ViewStack.push(new CreditsView()); }); + + final Button optionsButton = button(); + optionsButton.setText(AppContext.getString("options")); + optionsButton.setOnAction(_ -> { ViewStack.push(new OptionsView()); }); + + final Button quitButton = button(); + quitButton.setText(AppContext.getString("quit")); + quitButton.setOnAction(_ -> { App.startQuit(); }); + + final SongDisplay songdisplay = new SongDisplay(); + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.CENTER, + fit(vboxFill( + localButton, + onlineButton, + creditsButton, + optionsButton, + quitButton + )) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/OnlineView.java b/app/src/main/java/org/toop/app/view/views/OnlineView.java new file mode 100644 index 0000000..3d7750d --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/OnlineView.java @@ -0,0 +1,91 @@ +package org.toop.app.view.views; + +import org.toop.app.Server; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.text.Text; + +public class OnlineView extends View { + public OnlineView() { + super(true, "bg-primary"); + } + + @Override + public void setup() { + final Text serverInformationHeader = header(); + serverInformationHeader.setText(AppContext.getString("server-information")); + + final Text serverIPText = text(); + serverIPText.setText(AppContext.getString("ip-address")); + + final TextField serverIPInput = input(); + serverIPInput.setPromptText(AppContext.getString("enter-the-server-ip")); + + final Text serverPortText = text(); + serverPortText.setText(AppContext.getString("port")); + + final TextField serverPortInput = input(); + serverPortInput.setPromptText(AppContext.getString("enter-the-server-port")); + + final Text playerNameText = text(); + playerNameText.setText(AppContext.getString("player-name")); + + final TextField playerNameInput = input(); + playerNameInput.setPromptText(AppContext.getString("enter-your-name")); + + final Button connectButton = button(); + connectButton.setText(AppContext.getString("connect")); + connectButton.setOnAction(_ -> { + new Server(serverIPInput.getText(), serverPortInput.getText(), playerNameInput.getText()); + }); + + final SongDisplay songdisplay = new SongDisplay(); + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.CENTER, + fit(vboxFill( + serverInformationHeader, + separator(), + + vboxFill( + serverIPText, + serverIPInput + ), + + vboxFill( + serverPortText, + serverPortInput + ), + + vboxFill( + playerNameText, + playerNameInput + ), + + vboxFill( + connectButton + ) + )) + ); + + final Button backButton = button(); + backButton.setText(AppContext.getString("back")); + backButton.setOnAction(_ -> { ViewStack.push(new MainView()); }); + + add(Pos.BOTTOM_LEFT, + vboxFill( + backButton + ) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/OptionsView.java b/app/src/main/java/org/toop/app/view/views/OptionsView.java new file mode 100644 index 0000000..08cdfa0 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/OptionsView.java @@ -0,0 +1,258 @@ +package org.toop.app.view.views; + +import org.toop.app.App; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.framework.audio.VolumeControl; +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.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Slider; +import javafx.scene.text.Text; +import javafx.util.StringConverter; + +import java.util.Locale; + +public final class OptionsView extends View { + public OptionsView() { + super(false, "bg-secondary"); + } + + @Override + public void setup() { + final Text generalHeader = header(); + generalHeader.setText(AppContext.getString("general")); + + final Text volumeHeader = header(); + volumeHeader.setText(AppContext.getString("volume")); + + final Text styleHeader = header(); + styleHeader.setText(AppContext.getString("style")); + + add(Pos.CENTER, + fit(hboxFill( + vboxFill( + generalHeader, + separator(), + + vboxFill( + text("language-text"), + combobox("language-combobox") + ), + + vboxFill( + button("fullscreen-button") + ) + ), + + vboxFill( + volumeHeader, + separator(), + + vboxFill( + text("master-volume-text"), + slider("master-volume-slider") + ), + + vboxFill( + text("effects-volume-text"), + slider("effects-volume-slider") + ), + + vboxFill( + text("music-volume-text"), + slider("music-volume-slider") + ) + ), + + vboxFill( + styleHeader, + separator(), + + vboxFill( + text("theme-text"), + combobox("theme-combobox") + ), + + vboxFill( + text("layout-text"), + combobox("layout-combobox") + ) + ) + )) + ); + + setupLanguageOption(); + setupMasterVolumeOption(); + setupEffectsVolumeOption(); + setupMusicVolumeOption(); + setupThemeOption(); + setupLayoutOption(); + setupFullscreenOption(); + + final Button backButton = button(); + backButton.setText(AppContext.getString("back")); + backButton.setOnAction(_ -> { ViewStack.pop(); }); + + final SongDisplay songdisplay = new SongDisplay(); + + + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + songdisplay + ))); + + add(Pos.BOTTOM_LEFT, + vboxFill( + backButton + ) + ); + } + + private void setupLanguageOption() { + final Text languageText = get("language-text"); + languageText.setText(AppContext.getString("language")); + + final ComboBox languageCombobox = get("language-combobox"); + languageCombobox.getItems().addAll(AppContext.getLocalization().getAvailableLocales()); + languageCombobox.setValue(AppContext.getLocale()); + + languageCombobox.getSelectionModel().selectedItemProperty().addListener((_, _, newValue) -> { + AppSettings.getSettings().setLocale(newValue.toString()); + AppContext.setLocale(newValue); + }); + + languageCombobox.setConverter(new StringConverter<>() { + @Override + public String toString(Locale locale) { + return AppContext.getString(locale.getDisplayName().toLowerCase()); + } + + @Override + public Locale fromString(String s) { + return null; + } + }); + } + + private void setupMasterVolumeOption() { + final Text masterVolumeText = get("master-volume-text"); + masterVolumeText.setText(AppContext.getString("master-volume")); + + final Slider masterVolumeSlider = get("master-volume-slider"); + masterVolumeSlider.setMin(0); + masterVolumeSlider.setMax(100); + masterVolumeSlider.setValue(AppSettings.getSettings().getVolume()); + + masterVolumeSlider.valueProperty().addListener((_, _, newValue) -> { + AppSettings.getSettings().setVolume(newValue.intValue()); + new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue(), VolumeControl.MASTERVOLUME)).asyncPostEvent(); + }); + } + + private void setupEffectsVolumeOption() { + final Text effectsVolumeText = get("effects-volume-text"); + effectsVolumeText.setText(AppContext.getString("effects-volume")); + + final Slider effectsVolumeSlider = get("effects-volume-slider"); + effectsVolumeSlider.setMin(0); + effectsVolumeSlider.setMax(100); + effectsVolumeSlider.setValue(AppSettings.getSettings().getFxVolume()); + + effectsVolumeSlider.valueProperty().addListener((_, _, newValue) -> { + AppSettings.getSettings().setFxVolume(newValue.intValue()); + new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue(), VolumeControl.FX)).asyncPostEvent(); + }); + } + + private void setupMusicVolumeOption() { + final Text musicVolumeText = get("music-volume-text"); + musicVolumeText.setText(AppContext.getString("music-volume")); + + final Slider musicVolumeSlider = get("music-volume-slider"); + musicVolumeSlider.setMin(0); + musicVolumeSlider.setMax(100); + musicVolumeSlider.setValue(AppSettings.getSettings().getMusicVolume()); + + musicVolumeSlider.valueProperty().addListener((_, _, newValue) -> { + AppSettings.getSettings().setMusicVolume(newValue.intValue()); + new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue(), VolumeControl.MUSIC)).asyncPostEvent(); + }); + } + + private void setupThemeOption() { + final Text themeText = get("theme-text"); + themeText.setText(AppContext.getString("theme")); + + final ComboBox themeCombobox = get("theme-combobox"); + themeCombobox.getItems().addAll("dark", "light", "high-contrast"); + themeCombobox.setValue(AppSettings.getSettings().getTheme()); + + themeCombobox.getSelectionModel().selectedItemProperty().addListener((_, _, newValue) -> { + AppSettings.getSettings().setTheme(newValue); + App.setStyle(newValue, AppSettings.getSettings().getLayoutSize()); + }); + + themeCombobox.setConverter(new StringConverter<>() { + @Override + public String toString(String theme) { + return AppContext.getString(theme); + } + + @Override + public String fromString(String s) { + return null; + } + }); + } + + private void setupLayoutOption() { + final Text layoutText = get("layout-text"); + layoutText.setText(AppContext.getString("layout-size")); + + final ComboBox layoutCombobox = get("layout-combobox"); + layoutCombobox.getItems().addAll("small", "medium", "large"); + layoutCombobox.setValue(AppSettings.getSettings().getLayoutSize()); + + layoutCombobox.getSelectionModel().selectedItemProperty().addListener((_, _, newValue) -> { + AppSettings.getSettings().setLayoutSize(newValue); + App.setStyle(AppSettings.getSettings().getTheme(), newValue); + }); + + layoutCombobox.setConverter(new StringConverter<>() { + @Override + public String toString(String layout) { + return AppContext.getString(layout); + } + + @Override + public String fromString(String s) { + return null; + } + }); + } + + private void setupFullscreenOption() { + final Button fullscreenButton = get("fullscreen-button"); + + if (AppSettings.getSettings().getFullscreen()) { + fullscreenButton.setText(AppContext.getString("windowed")); + fullscreenButton.setOnAction(_ -> { + AppSettings.getSettings().setFullscreen(false); + App.setFullscreen(false); + }); + } else { + fullscreenButton.setText(AppContext.getString("fullscreen")); + fullscreenButton.setOnAction(_ -> { + AppSettings.getSettings().setFullscreen(true); + App.setFullscreen(true); + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/QuitView.java b/app/src/main/java/org/toop/app/view/views/QuitView.java new file mode 100644 index 0000000..c2f37eb --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/QuitView.java @@ -0,0 +1,40 @@ +package org.toop.app.view.views; + +import org.toop.app.App; +import org.toop.app.view.View; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.text.Text; + +public final class QuitView extends View { + public QuitView() { + super(false, "bg-popup"); + } + + @Override + public void setup() { + final Text sureHeader = header(); + sureHeader.setText(AppContext.getString("are-you-sure")); + + final Button yesButton = button(); + yesButton.setText(AppContext.getString("yes")); + yesButton.setOnAction(_ -> { App.quit(); }); + + final Button noButton = button(); + noButton.setText(AppContext.getString("no")); + noButton.setOnAction(_ -> { App.stopQuit(); }); + + add(Pos.CENTER, + fit(vbox( + sureHeader, + + hbox( + yesButton, + noButton + ) + )) + ); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..d72560c --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java @@ -0,0 +1,119 @@ +package org.toop.app.view.views; + +import org.toop.app.GameInformation; +import org.toop.app.Server; +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Slider; +import javafx.scene.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +public final class SendChallengeView extends View { + private final Server server; + private final String opponent; + private final BiConsumer onSend; + + private final GameInformation.Player playerInformation; + + public SendChallengeView(Server server, String opponent, BiConsumer onSend) { + super(false, "bg-popup"); + + this.server = server; + this.opponent = opponent; + this.onSend = onSend; + + playerInformation = new GameInformation.Player(); + } + + @Override + public void setup() { + final Text challengeText = text(); + challengeText.setText(AppContext.getString("challenge")); + + final Text opponentHeader = header(); + opponentHeader.setText(opponent); + + final Text gameText = text(); + gameText.setText(AppContext.getString("to-a-game-of")); + + final ComboBox gamesCombobox = combobox(); + gamesCombobox.getItems().addAll(server.getGameList()); + gamesCombobox.setValue(gamesCombobox.getItems().getFirst()); + + final Button sendButton = button(); + sendButton.setText(AppContext.getString("send")); + sendButton.setOnAction(_ -> { onSend.accept(playerInformation, gamesCombobox.getValue()); }); + + final Button cancelButton = button(); + cancelButton.setText(AppContext.getString("cancel")); + cancelButton.setOnAction(_ -> { + ViewStack.pop(); }); + + final List nodes = new ArrayList<>(); + + if (playerInformation.isHuman) { + final Button playerToggle = button(); + playerToggle.setText(AppContext.getString("player")); + playerToggle.setOnAction(_ -> { + playerInformation.isHuman = false; + cleanup(); + setup(); + }); + + nodes.add(vbox(playerToggle)); + } else { + final Button computerToggle = button(); + computerToggle.setText(AppContext.getString("computer")); + computerToggle.setOnAction(_ -> { + playerInformation.isHuman = true; + cleanup(); + setup(); + }); + + nodes.add(vbox(computerToggle)); + + final Text computerDifficultyText = text(); + computerDifficultyText.setText(AppContext.getString("computer-difficulty")); + + final Slider computerDifficultySlider = slider(); + computerDifficultySlider.setMin(0); + computerDifficultySlider.setMax(Server.gameToType(gamesCombobox.getValue()).getMaxDepth()); + computerDifficultySlider.setValue(playerInformation.computerDifficulty); + computerDifficultySlider.valueProperty().addListener((_, _, newValue) -> { + playerInformation.computerDifficulty = newValue.intValue(); + }); + + nodes.add(vbox(computerDifficultyText, computerDifficultySlider)); + } + + add(Pos.CENTER, + fit(hboxFill( + vboxFill( + challengeText, + opponentHeader, + gameText, + gamesCombobox, + separator(), + + hboxFill( + sendButton, + cancelButton + ) + ), + + vboxFill( + nodes.toArray(new Node[0]) + ) + )) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/ServerView.java b/app/src/main/java/org/toop/app/view/views/ServerView.java new file mode 100644 index 0000000..c819c14 --- /dev/null +++ b/app/src/main/java/org/toop/app/view/views/ServerView.java @@ -0,0 +1,89 @@ +package org.toop.app.view.views; + +import org.toop.app.view.View; +import org.toop.app.view.ViewStack; +import org.toop.app.view.displays.SongDisplay; +import org.toop.local.AppContext; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.text.Text; + +import java.util.List; +import java.util.function.Consumer; + +public final class ServerView extends View { + private final String user; + private final Consumer onPlayerClicked; + private final Runnable onDisconnect; + + private ListView