diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index c06f6ce..eb314d9 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,8 +1,9 @@ package org.toop.app; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.MainView; -import org.toop.app.view.views.QuitView; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.display.SongDisplay; +import org.toop.app.widget.popup.QuitPopup; +import org.toop.app.widget.view.MainView; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -11,6 +12,7 @@ import org.toop.local.AppContext; import org.toop.local.AppSettings; import javafx.application.Application; +import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; @@ -30,11 +32,12 @@ public final class App extends Application { @Override public void start(Stage stage) throws Exception { - final StackPane root = new StackPane(); + final StackPane root = WidgetContainer.setup(); final Scene scene = new Scene(root); - ViewStack.setup(scene); stage.setTitle(AppContext.getString("app-title")); + stage.titleProperty().bind(AppContext.bindToKey("app-title")); + stage.setWidth(1080); stage.setHeight(720); @@ -61,7 +64,8 @@ public final class App extends Application { AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - ViewStack.push(new MainView()); + WidgetContainer.add(Pos.CENTER, new MainView()); + WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay()); } public static void startQuit() { @@ -69,47 +73,32 @@ public final class App extends Application { return; } - ViewStack.push(new QuitView()); + WidgetContainer.add(Pos.CENTER, new QuitPopup()); isQuitting = true; } public static void stopQuit() { - ViewStack.pop(); isQuitting = false; } public static void quit() { - ViewStack.cleanup(); stage.close(); System.exit(0); // TODO: This is like dropping a nuke } - public static void reload() { - stage.setTitle(AppContext.getString("app-title")); - ViewStack.reload(); - } - public static void setFullscreen(boolean fullscreen) { stage.setFullScreen(fullscreen); - width = (int) stage.getWidth(); - height = (int) stage.getHeight(); - - reload(); + width = (int)stage.getWidth(); + height = (int)stage.getHeight(); } public static void setStyle(String theme, String layoutSize) { - final int stylesCount = scene.getStylesheets().size(); - - for (int i = 0; i < stylesCount; i++) { - scene.getStylesheets().removeLast(); - } + scene.getStylesheets().clear(); scene.getStylesheets().add(ResourceManager.get("general.css").getUrl()); scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - - reload(); } public static int getWidth() { diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java index bca254f..5a17c8f 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -2,29 +2,26 @@ package org.toop.app; public class GameInformation { public enum Type { - TICTACTOE, - REVERSI, - CONNECT4, - BATTLESHIP; + TICTACTOE(2, 5), + REVERSI(2, 10), + CONNECT4(2, 7), + BATTLESHIP(2, 5); + private final int playerCount; + private final int maxDepth; - public static int playerCount(Type type) { - return switch (type) { - case TICTACTOE -> 2; - case REVERSI -> 2; - case CONNECT4 -> 2; - case BATTLESHIP -> 2; - }; - } + Type(int playerCount, int maxDepth) { + this.playerCount = playerCount; + this.maxDepth = maxDepth; + } - public static int maxDepth(Type type) { - return switch (type) { - case TICTACTOE -> 5; // Todo. 5 seems to always draw or win. could increase to 9 but that might affect performance - case REVERSI -> 10; // Todo. 10 is a guess. might be too slow or too bad. - case CONNECT4 -> 7; - case BATTLESHIP -> 5; - }; - } + public int getPlayerCount() { + return playerCount; + } + + public int getMaxDepth() { + return maxDepth; + } } public static class Player { @@ -39,7 +36,7 @@ public class GameInformation { public GameInformation(Type type) { this.type = type; - players = new Player[Type.playerCount(type)]; + players = new Player[type.getPlayerCount()]; for (int i = 0; i < players.length; i++) { players[i] = new Player(); diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 4a1e93e..00f91cc 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -3,12 +3,11 @@ package org.toop.app; import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.ChallengeView; -import org.toop.app.view.views.ErrorView; -import org.toop.app.view.views.OnlineView; -import org.toop.app.view.views.SendChallengeView; -import org.toop.app.view.views.ServerView; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.popup.ChallengePopup; +import org.toop.app.widget.popup.ErrorPopup; +import org.toop.app.widget.popup.SendChallengePopup; +import org.toop.app.widget.view.ServerView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.events.NetworkEvents; @@ -29,10 +28,10 @@ public final class Server { private final List onlinePlayers = new CopyOnWriteArrayList<>(); private final List gameList = new CopyOnWriteArrayList<>(); - private ServerView view; + private ServerView primary; private boolean isPolling = true; - private AtomicBoolean isSingleGame = new AtomicBoolean(false); + private final AtomicBoolean isSingleGame = new AtomicBoolean(false); private ScheduledExecutorService scheduler; @@ -52,7 +51,7 @@ public final class Server { public Server(String ip, String port, String user) { if (ip.split("\\.").length < 4) { - ViewStack.push(new ErrorView("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address"))); + new ErrorPopup("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address")); return; } @@ -61,12 +60,12 @@ public final class Server { try { parsedPort = Integer.parseInt(port); } catch (NumberFormatException _) { - ViewStack.push(new ErrorView("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port"))); + new ErrorPopup("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port")); return; } if (user.isEmpty() || user.matches("^[0-9].*")) { - ViewStack.push(new ErrorView(AppContext.getString("invalid-username"))); + new ErrorPopup(AppContext.getString("invalid-username")); return; } @@ -81,8 +80,8 @@ public final class Server { new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent(); - view = new ServerView(user, this::sendChallenge, this::disconnect); - ViewStack.push(view); + primary = new ServerView(user, this::sendChallenge, this::disconnect); + WidgetContainer.getCurrentView().transitionNext(primary); startPopulateScheduler(); populateGameList(); @@ -96,38 +95,10 @@ public final class Server { private void sendChallenge(String opponent) { if (!isPolling) return; - - ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { + new SendChallengePopup(this, opponent, (playerInformation, gameType) -> { new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent(); - /* .listen(NetworkEvents.GameMatchResponse.class, e -> { - if (e.clientId() == clientId) { - isPolling = false; - onlinePlayers.clear(); - - final GameInformation.Type type = gameToType(gameType); - if (type == null) { - ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); - return; - } - - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0; - - final GameInformation information = new GameInformation(type); - information.players[0] = playerInformation; - information.players[0].name = user; - information.players[1].name = opponent; - - switch (type) { - case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - default -> ViewStack.push(new ErrorView("Unsupported game type.")); - } - } - }) */ - ViewStack.pop(); isSingleGame.set(true); - })); + }); } private void handleMatchResponse(NetworkEvents.GameMatchResponse response) { @@ -141,7 +112,7 @@ public final class Server { final GameInformation.Type type = gameToType(gameType); if (type == null) { - ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); + new ErrorPopup("Unsupported game type: " + gameType); return; } @@ -165,7 +136,7 @@ public final class Server { new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); - default -> ViewStack.push(new ErrorView("Unsupported game type.")); + default -> new ErrorPopup("Unsupported game type."); } } } @@ -176,17 +147,11 @@ public final class Server { String challengerName = extractQuotedValue(response.challengerName()); String gameType = extractQuotedValue(response.gameType()); final String finalGameType = gameType; - ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { + new ChallengePopup(challengerName, gameType, (playerInformation) -> { final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); - ViewStack.pop(); isSingleGame.set(true); - - //new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { - - - //}); - })); + }); } private void sendMessage(String message) { @@ -197,7 +162,7 @@ public final class Server { new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); isPolling = false; stopScheduler(); - ViewStack.push(new OnlineView()); + primary.transitionPrevious(); } private void forfeitGame() { @@ -206,15 +171,11 @@ public final class Server { private void exitGame() { forfeitGame(); - ViewStack.push(view); startPopulateScheduler(); } private void gameOver(){ - ViewStack.pop(); - ViewStack.push(view); startPopulateScheduler(); - } private void startPopulateScheduler() { @@ -228,7 +189,7 @@ public final class Server { onlinePlayers.clear(); onlinePlayers.addAll(List.of(e.playerlist())); onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); - view.update(onlinePlayers); + primary.update(onlinePlayers); } }, false); 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 b3f1628..c1b24f7 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -80,5 +80,7 @@ public final class ReversiCanvas extends GameCanvas { 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/game/BaseGameThread.java b/app/src/main/java/org/toop/app/game/BaseGameThread.java new file mode 100644 index 0000000..f9178da --- /dev/null +++ b/app/src/main/java/org/toop/app/game/BaseGameThread.java @@ -0,0 +1,120 @@ +package org.toop.app.game; + +import org.toop.app.GameInformation; +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.Game; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract class BaseGameThread { + protected final GameInformation information; + protected final int myTurn; + protected final Runnable onGameOver; + protected final BlockingQueue moveQueue; + + protected final TGame game; + protected final TAI ai; + + protected final GameView primary; + protected final TCanvas canvas; + + protected final AtomicBoolean isRunning = new AtomicBoolean(true); + + protected BaseGameThread( + GameInformation information, + int myTurn, + Runnable onForfeit, + Runnable onExit, + Consumer onMessage, + Runnable onGameOver, + Supplier gameSupplier, + Supplier aiSupplier, + Function, TCanvas> canvasFactory) { + + this.information = information; + this.myTurn = myTurn; + this.onGameOver = onGameOver; + this.moveQueue = new LinkedBlockingQueue<>(); + + this.game = gameSupplier.get(); + this.ai = aiSupplier.get(); + + 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); + } + + this.canvas = canvasFactory.apply(this::onCellClicked); + + addCanvasToPrimary(); + + 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); + } + + private void onCellClicked(int cell) { + if (!isRunning.get()) return; + + final int currentTurn = getCurrentTurn(); + if (!information.players[currentTurn].isHuman) return; + + final char value = getSymbolForTurn(currentTurn); + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + + protected void gameOver() { + if (onGameOver != null) { + isRunning.set(false); + onGameOver.run(); + } + } + + protected void setGameLabels(boolean isMe) { + final int currentTurn = getCurrentTurn(); + final String turnName = getNameForTurn(currentTurn); + + primary.nextPlayer( + isMe, + information.players[isMe ? 0 : 1].name, + turnName, + information.players[isMe ? 1 : 0].name + ); + } + + 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(); +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java index 4830de1..76bd13e 100644 --- a/app/src/main/java/org/toop/app/game/Connect4Game.java +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -107,7 +107,7 @@ public class Connect4Game { while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "RED" : "BLUE"; - final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount(); view.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index ec9a5e6..a65805e 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -4,9 +4,8 @@ import javafx.animation.SequentialTransition; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.ReversiCanvas; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.GameView; -import org.toop.app.view.views.LocalMultiplayerView; +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; @@ -27,13 +26,15 @@ 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 view; + private final GameView primary; private final ReversiCanvas canvas; private final AtomicBoolean isRunning; @@ -44,7 +45,7 @@ public final class ReversiGame { this.myTurn = myTurn; this.onGameOver = onGameOver; - moveQueue = new LinkedBlockingQueue(); + moveQueue = new LinkedBlockingQueue<>(); game = new Reversi(); ai = new ReversiAI(); @@ -53,12 +54,12 @@ public final class ReversiGame { isPaused = new AtomicBoolean(false); if (onForfeit == null || onExit == null) { - view = new GameView(null, () -> { + primary = new GameView(null, () -> { isRunning.set(false); - ViewStack.push(new LocalMultiplayerView(information)); + WidgetContainer.getCurrentView().transitionPrevious(); }, null); } else { - view = new GameView(onForfeit, () -> { + primary = new GameView(onForfeit, () -> { isRunning.set(false); onExit.run(); }, onMessage); @@ -88,8 +89,8 @@ public final class ReversiGame { - view.add(Pos.CENTER, canvas.getCanvas()); - ViewStack.push(view); + primary.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(primary); if (onForfeit == null || onExit == null) { new Thread(this::localGameThread).start(); @@ -97,8 +98,7 @@ public final class ReversiGame { } else { new EventFlow() .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) - .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) - .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse); setGameLabels(myTurn == 0); } @@ -122,9 +122,9 @@ public final class ReversiGame { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; - final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount(); - view.nextPlayer(information.players[currentTurn].isHuman, + primary.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, currentValue, information.players[nextTurn].name); @@ -167,6 +167,11 @@ public final class ReversiGame { 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); @@ -198,10 +203,10 @@ public final class ReversiGame { if (state != GameState.NORMAL) { if (state == GameState.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { - view.gameOver(true, information.players[0].name); + primary.gameOver(true, information.players[0].name); gameOver(); } else { - view.gameOver(false, information.players[1].name); + primary.gameOver(false, information.players[1].name); gameOver(); } } else if (state == GameState.DRAW) { @@ -246,14 +251,6 @@ public final class ReversiGame { .postEvent(); } - private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { - if (!isRunning.get()) { - return; - } - - view.updateChat(msg.message()); - } - 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(); @@ -271,16 +268,14 @@ public final class ReversiGame { 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()); - - final Color from = flip.value() == 'W' ? Color.BLACK : Color.WHITE; - final Color to = flip.value() == 'W' ? Color.WHITE : Color.BLACK; - - canvas.drawDot(from, flip.position()); - - animation.getChildren().addFirst(canvas.flipDot(from, to, flip.position())); + canvas.drawDot(fromColor, flip.position()); + animation.getChildren().addFirst(canvas.flipDot(fromColor, toColor, flip.position())); } } @@ -291,6 +286,8 @@ public final class ReversiGame { for (final Move legalMove : legalMoves) { canvas.drawLegalPosition(legalMove.position(), game.getCurrentPlayer()); + for (final Game.Move legalMove : legalMoves) { + canvas.drawLegalPosition(fromColor, legalMove.position()); } }); @@ -301,7 +298,7 @@ public final class ReversiGame { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; - view.nextPlayer(isMe, + primary.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/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index e68579c..84fc237 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -3,9 +3,8 @@ package org.toop.app.game; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.TicTacToeCanvas; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.GameView; -import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.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; @@ -31,7 +30,7 @@ public final class TicTacToeGame { private final TicTacToe game; private final TicTacToeAI ai; - private final GameView view; + private final GameView primary; private final TicTacToeCanvas canvas; private final AtomicBoolean isRunning; @@ -49,12 +48,12 @@ public final class TicTacToeGame { isRunning = new AtomicBoolean(true); if (onForfeit == null || onExit == null) { - view = new GameView(null, () -> { + primary = new GameView(null, () -> { isRunning.set(false); - ViewStack.push(new LocalMultiplayerView(information)); + WidgetContainer.getCurrentView().transitionPrevious(); }, null); } else { - view = new GameView(onForfeit, () -> { + primary = new GameView(onForfeit, () -> { isRunning.set(false); onExit.run(); }, onMessage); @@ -82,16 +81,15 @@ public final class TicTacToeGame { } }); - view.add(Pos.CENTER, canvas.getCanvas()); - ViewStack.push(view); + 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) - .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse); setGameLabels(myTurn == 0); } @@ -105,9 +103,9 @@ public final class TicTacToeGame { while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "X" : "O"; - final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount(); - view.nextPlayer(information.players[currentTurn].isHuman, + primary.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, currentValue, information.players[nextTurn].name); @@ -156,9 +154,9 @@ public final class TicTacToeGame { if (state != GameState.NORMAL) { if (state == GameState.WIN) { - view.gameOver(true, information.players[currentTurn].name); + primary.gameOver(true, information.players[currentTurn].name); } else if (state == GameState.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); } isRunning.set(false); @@ -185,15 +183,15 @@ public final class TicTacToeGame { if (state != GameState.NORMAL) { if (state == GameState.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { - view.gameOver(true, information.players[0].name); + primary.gameOver(true, information.players[0].name); gameOver(); } else { - view.gameOver(false, information.players[1].name); + 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. - view.gameOver(false, ""); + primary.gameOver(false, ""); gameOver(); } } @@ -240,19 +238,11 @@ public final class TicTacToeGame { .postEvent(); } - private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { - if (!isRunning.get()) { - return; - } - - view.updateChat(msg.message()); - } - private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "X" : "O"; - view.nextPlayer(isMe, + primary.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/TicTacToeGameThread.java b/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java new file mode 100644 index 0000000..a71bf07 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java @@ -0,0 +1,177 @@ +package org.toop.app.game; + +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.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, + TicTacToe::new, + TicTacToeAI::new, + clickHandler -> new TicTacToeCanvas(Color.GRAY, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, clickHandler) + ); + } + + public TicTacToeGameThread(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 ? "X" : "O"; + } + + private void drawMove(Game.Move move) { + if (move.value() == 'X') canvas.drawX(Color.RED, move.position()); + else canvas.drawO(Color.BLUE, move.position()); + } + + @Override + protected void onMoveResponse(NetworkEvents.GameMoveResponse response) { + if (!isRunning.get()) { + return; + } + + char playerChar; + + if (response.player().equalsIgnoreCase(information.players[0].name)) { + playerChar = myTurn == 0? 'X' : 'O'; + } else { + playerChar = myTurn == 0? 'O' : 'X'; + } + + final Game.Move move = new Game.Move(Integer.parseInt(response.move()), playerChar); + final Game.State state = game.play(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + primary.gameOver(true, information.players[0].name); + gameOver(); + } else { + primary.gameOver(false, information.players[1].name); + gameOver(); + } + } else if (state == Game.State.DRAW) { + if (game.getLegalMoves().length == 0) { + primary.gameOver(false, ""); + gameOver(); + } + } + } + + drawMove(move); + 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 Game.Move move; + if (information.players[1].name.equalsIgnoreCase("pism")) { + move = ai.findWorstMove(game,9); + }else{ + 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()) { + final int currentTurn = game.getCurrentTurn(); + setGameLabels(currentTurn == myTurn); + + Game.Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Game.Move wants = moveQueue.take(); + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + if (legalMove.position() == wants.position() && + legalMove.value() == wants.value()) { + move = wants; + break; + } + } + } catch (InterruptedException _) {} + } else { + final long start = System.currentTimeMillis(); + + move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); + + if (information.players[currentTurn].computerThinkTime > 0) { + final long elapsedTime = System.currentTimeMillis() - start; + final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime; + + try { + Thread.sleep((long)(sleepTime * Math.random())); + } catch (InterruptedException _) {} + } + } + + if (move == null) { + continue; + } + + final Game.State state = game.play(move); + drawMove(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + primary.gameOver(information.players[currentTurn].isHuman, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + primary.gameOver(false, ""); + } + + isRunning.set(false); + } + } + } +} \ 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 index 09d3748..f21e2aa 100644 --- a/app/src/main/java/org/toop/app/view/displays/SongDisplay.java +++ b/app/src/main/java/org/toop/app/view/displays/SongDisplay.java @@ -1,11 +1,13 @@ 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; @@ -13,7 +15,7 @@ import javafx.geometry.Pos; import javafx.scene.text.Text; import org.toop.framework.eventbus.GlobalEventBus; -public class SongDisplay extends VBox { +public class SongDisplay extends VBox implements Widget { private final Text songTitle; private final ProgressBar progressBar; @@ -114,6 +116,11 @@ public class SongDisplay extends VBox { 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 index a403de9..36184f6 100644 --- a/app/src/main/java/org/toop/app/view/views/ChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/ChallengeView.java @@ -87,7 +87,7 @@ public final class ChallengeView extends View { final Slider computerDifficultySlider = slider(); computerDifficultySlider.setMin(0); - computerDifficultySlider.setMax(GameInformation.Type.maxDepth(Server.gameToType(game))); + computerDifficultySlider.setMax(Server.gameToType(game).getMaxDepth()); computerDifficultySlider.setValue(playerInformation.computerDifficulty); computerDifficultySlider.valueProperty().addListener((_, _, newValue) -> { playerInformation.computerDifficulty = newValue.intValue(); diff --git a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java index e99de78..c44d784 100644 --- a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java +++ b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java @@ -3,7 +3,7 @@ package org.toop.app.view.views; import org.toop.app.GameInformation; import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; -import org.toop.app.game.TicTacToeGame; +import org.toop.app.game.TicTacToeGameThread; import org.toop.app.view.View; import org.toop.app.view.ViewStack; import org.toop.app.view.displays.SongDisplay; @@ -45,10 +45,10 @@ public final class LocalMultiplayerView extends View { } switch (information.type) { - case TICTACTOE: new TicTacToeGame(information); break; + case TICTACTOE: new TicTacToeGameThread(information); break; case REVERSI: new ReversiGame(information); break; case CONNECT4: new Connect4Game(information); break; - //case BATTLESHIP: new BattleshipGame(information); break; + // case BATTLESHIP: new BattleshipGame(information); break; } }); @@ -83,7 +83,7 @@ public final class LocalMultiplayerView extends View { } private VBox[] setupPlayers() { - final VBox[] playerBoxes = new VBox[GameInformation.Type.playerCount(information.type)]; + final VBox[] playerBoxes = new VBox[information.type.getPlayerCount()]; for (int i = 0; i < playerBoxes.length; i++) { final int index = i; @@ -141,7 +141,7 @@ public final class LocalMultiplayerView extends View { final Slider computerDifficultySlider = slider(); computerDifficultySlider.setMin(0); - computerDifficultySlider.setMax(GameInformation.Type.maxDepth(information.type)); + computerDifficultySlider.setMax(information.type.getMaxDepth()); computerDifficultySlider.setValue(information.players[i].computerDifficulty); computerDifficultySlider.valueProperty().addListener((_, _, newValue) -> { information.players[index].computerDifficulty = newValue.intValue(); 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 index d4af353..08cdfa0 100644 --- a/app/src/main/java/org/toop/app/view/views/OptionsView.java +++ b/app/src/main/java/org/toop/app/view/views/OptionsView.java @@ -126,7 +126,6 @@ public final class OptionsView extends View { languageCombobox.getSelectionModel().selectedItemProperty().addListener((_, _, newValue) -> { AppSettings.getSettings().setLocale(newValue.toString()); AppContext.setLocale(newValue); - App.reload(); }); languageCombobox.setConverter(new StringConverter<>() { diff --git a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java index f78f2fc..d72560c 100644 --- a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java @@ -86,7 +86,7 @@ public final class SendChallengeView extends View { final Slider computerDifficultySlider = slider(); computerDifficultySlider.setMin(0); - computerDifficultySlider.setMax(GameInformation.Type.maxDepth(Server.gameToType(gamesCombobox.getValue()))); + computerDifficultySlider.setMax(Server.gameToType(gamesCombobox.getValue()).getMaxDepth()); computerDifficultySlider.setValue(playerInformation.computerDifficulty); computerDifficultySlider.valueProperty().addListener((_, _, newValue) -> { playerInformation.computerDifficulty = newValue.intValue(); diff --git a/app/src/main/java/org/toop/app/widget/Primitive.java b/app/src/main/java/org/toop/app/widget/Primitive.java new file mode 100644 index 0000000..75795f2 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/Primitive.java @@ -0,0 +1,166 @@ +package org.toop.app.widget; + +import org.toop.local.AppContext; + +import java.util.function.Consumer; + +import javafx.collections.FXCollections; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.control.Slider; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.util.StringConverter; + +public final class Primitive { + public static Text header(String key) { + var header = new Text(); + header.getStyleClass().add("header"); + + if (!key.isEmpty()) { + header.setText(AppContext.getString(key)); + header.textProperty().bind(AppContext.bindToKey(key)); + } + + return header; + } + + public static Text text(String key) { + var text = new Text(); + text.getStyleClass().add("text"); + + if (!key.isEmpty()) { + text.setText(AppContext.getString(key)); + text.textProperty().bind(AppContext.bindToKey(key)); + } + + return text; + } + + public static Button button(String key, Runnable onAction) { + var button = new Button(); + button.getStyleClass().add("button"); + + if (!key.isEmpty()) { + button.setText(AppContext.getString(key)); + button.textProperty().bind(AppContext.bindToKey(key)); + } + + if (onAction != null) { + button.setOnAction(_ -> + onAction.run()); + } + + return button; + } + + public static TextField input(String promptKey, String text, Consumer onValueChanged) { + var input = new TextField(); + input.getStyleClass().add("input"); + + if (!promptKey.isEmpty()) { + input.setPromptText(AppContext.getString(promptKey)); + input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); + } + + input.setText(text); + + if (onValueChanged != null) { + input.textProperty().addListener((_, _, newValue) -> + onValueChanged.accept(newValue)); + } + + return input; + } + + public static Slider slider(int min, int max, int value, Consumer onValueChanged) { + var slider = new Slider(); + slider.getStyleClass().add("slider"); + + slider.setMin(min); + slider.setMax(max); + slider.setValue(value); + + if (onValueChanged != null) { + slider.valueProperty().addListener((_, _, newValue) -> + onValueChanged.accept(newValue.intValue())); + } + + return slider; + } + + @SafeVarargs + public static ComboBox choice(StringConverter converter, T value, Consumer onValueChanged, T... items) { + var choice = new ComboBox(); + choice.getStyleClass().add("choice"); + + if (converter != null) { + choice.setConverter(converter); + } + + if (value != null) { + choice.setValue(value); + } + + if (onValueChanged != null) { + choice.valueProperty().addListener((_, _, newValue) -> + onValueChanged.accept(newValue)); + } + + choice.setItems(FXCollections.observableArrayList(items)); + + return choice; + } + + public static ScrollPane scroll(Node content) { + var scroll = new ScrollPane(); + scroll.getStyleClass().add("scroll"); + scroll.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + scroll.setFitToWidth(true); + + scroll.setContent(content); + + return scroll; + } + + public static Separator separator() { + var separator = new Separator(); + separator.getStyleClass().add("separator"); + + return separator; + } + + public static HBox hbox(Node... nodes) { + var hbox = new HBox(); + hbox.getStyleClass().add("container"); + hbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + for (var node : nodes) { + if (node != null) { + hbox.getChildren().add(node); + } + } + + return hbox; + } + + public static VBox vbox(Node... nodes) { + var vbox = new VBox(); + vbox.getStyleClass().add("container"); + vbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + for (var node : nodes) { + if (node != null) { + vbox.getChildren().add(node); + } + } + + return vbox; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/Widget.java b/app/src/main/java/org/toop/app/widget/Widget.java new file mode 100644 index 0000000..5f7a269 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/Widget.java @@ -0,0 +1,21 @@ +package org.toop.app.widget; + +import javafx.geometry.Pos; +import javafx.scene.Node; + +public interface Widget { + Node getNode(); + + default void show(Pos position) { + WidgetContainer.add(position, this); + } + + default void hide() { + WidgetContainer.remove(this); + } + + default void replace(Pos position, Widget widget) { + widget.show(position); + hide(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/WidgetContainer.java b/app/src/main/java/org/toop/app/widget/WidgetContainer.java new file mode 100644 index 0000000..aeeec92 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/WidgetContainer.java @@ -0,0 +1,65 @@ +package org.toop.app.widget; + +import org.toop.app.widget.complex.PopupWidget; +import org.toop.app.widget.complex.ViewWidget; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.layout.StackPane; + +public final class WidgetContainer { + private static StackPane root; + private static ViewWidget currentView; + + public static synchronized StackPane setup() { + if (root != null) { + return root; + } + + root = new StackPane(); + root.getStyleClass().add("bg-view"); + + return root; + } + + public static void add(Pos position, Widget widget) { + if (root == null || widget == null) { + return; + } + + Platform.runLater(() -> { + if (root.getChildren().contains(widget.getNode())) { + return; + } + + StackPane.setAlignment(widget.getNode(), position); + + if (widget instanceof ViewWidget view) { + root.getChildren().addFirst(view.getNode()); + currentView = view; + } else if (widget instanceof PopupWidget popup) { + currentView.add(Pos.CENTER, popup); + } else { + root.getChildren().add(widget.getNode()); + } + }); + } + + public static void remove(Widget widget) { + if (root == null || widget == null) { + return; + } + + Platform.runLater(() -> { + if (widget instanceof PopupWidget popup) { + currentView.remove(popup); + } else { + root.getChildren().remove(widget.getNode()); + } + }); + } + + public static ViewWidget getCurrentView() { + return currentView; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java new file mode 100644 index 0000000..e8719ce --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java @@ -0,0 +1,38 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class ConfirmWidget implements Widget { + private final HBox buttonsContainer; + private final Text messageText; + private final VBox container; + + public ConfirmWidget(String confirm) { + buttonsContainer = Primitive.hbox(); + messageText = Primitive.text(""); + container = Primitive.vbox(Primitive.header(confirm), messageText, Primitive.separator(), buttonsContainer); + } + + public void setMessage(String message) { + messageText.setText(message); + } + + public void addButton(String key, Runnable onClick) { + Platform.runLater(() -> { + var button = Primitive.button(key, onClick); + buttonsContainer.getChildren().add(button); + }); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java b/app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java new file mode 100644 index 0000000..a59f6ed --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java @@ -0,0 +1,42 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; + +public class LabeledChoiceWidget implements Widget { + private final ComboBox comboBox; + private final VBox container; + + @SafeVarargs + public LabeledChoiceWidget( + String key, + StringConverter converter, + T initialValue, + Consumer onValueChanged, + T... items + ) { + var label = Primitive.text(key); + comboBox = Primitive.choice(converter, initialValue, onValueChanged, items); + container = Primitive.vbox(label, comboBox); + } + + public T getValue() { + return comboBox.getValue(); + } + + public void setValue(T value) { + comboBox.setValue(value); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java b/app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java new file mode 100644 index 0000000..1223448 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java @@ -0,0 +1,34 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; + +public class LabeledInputWidget implements Widget { + private final TextField input; + private final VBox container; + + public LabeledInputWidget(String key, String promptKey, String initialText, Consumer onValueChanged) { + var label = Primitive.text(key); + input = Primitive.input(promptKey, initialText, onValueChanged); + container = Primitive.vbox(label, input); + } + + public String getValue() { + return input.getText(); + } + + public void setValue(String text) { + input.setText(text); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java b/app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java new file mode 100644 index 0000000..b770b1e --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java @@ -0,0 +1,49 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.Slider; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class LabeledSliderWidget implements Widget { + private final Slider slider; + private final Text labelValue; + private final VBox container; + + public LabeledSliderWidget(String key, int min, int max, int value, Consumer onValueChanged) { + var label = Primitive.text(key); + + labelValue = new Text(String.valueOf(value)); + labelValue.getStyleClass().add("text"); + + slider = Primitive.slider(min, max, value, newValue -> { + labelValue.setText(String.valueOf(newValue)); + + if (onValueChanged != null) { + onValueChanged.accept(newValue); + } + }); + + var sliderRow = Primitive.hbox(slider, labelValue); + container = Primitive.vbox(label, sliderRow); + } + + public int getValue() { + return (int)slider.getValue(); + } + + public void setValue(int newValue) { + slider.setValue(newValue); + labelValue.setText(String.valueOf(newValue)); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java new file mode 100644 index 0000000..90ec2a5 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java @@ -0,0 +1,83 @@ +package org.toop.app.widget.complex; + +import org.toop.app.GameInformation; +import org.toop.app.widget.Primitive; + +import javafx.scene.Node; +import javafx.scene.layout.VBox; + +public class PlayerInfoWidget { + private final GameInformation.Player information; + private final VBox container; + + public PlayerInfoWidget(GameInformation.Player information) { + this.information = information; + container = Primitive.vbox( + buildToggle().getNode(), + buildContent() + ); + } + + private ToggleWidget buildToggle() { + return new ToggleWidget( + "computer", "player", + information.isHuman, + isHuman -> { + information.isHuman = isHuman; + container.getChildren().setAll( + buildToggle().getNode(), + buildContent() + ); + } + ); + } + + private Node buildContent() { + if (information.isHuman) { + var nameInput = new LabeledInputWidget( + "name", + "enter-your-name", + information.name, + newName -> information.name = newName + ); + + return nameInput.getNode(); + } else { + if (information.name == null || information.name.isEmpty()) { + information.name = "Pism Bot"; + } + + var playerName = Primitive.text(""); + playerName.setText(information.name); + + var nameDisplay = Primitive.vbox( + Primitive.text("name"), + playerName + ); + + var difficultySlider = new LabeledSliderWidget( + "computer-difficulty", + 0, 5, + information.computerDifficulty, + newVal -> information.computerDifficulty = newVal + ); + + var thinkTimeSlider = new LabeledSliderWidget( + "computer-think-time", + 0, 5, + information.computerThinkTime, + newVal -> information.computerThinkTime = newVal + ); + + return Primitive.vbox( + nameDisplay, + difficultySlider.getNode(), + thinkTimeSlider.getNode() + ); + } + } + + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java new file mode 100644 index 0000000..853bf88 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java @@ -0,0 +1,7 @@ +package org.toop.app.widget.complex; + +public abstract class PopupWidget extends StackWidget { + public PopupWidget() { + super("bg-popup"); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/StackWidget.java b/app/src/main/java/org/toop/app/widget/complex/StackWidget.java new file mode 100644 index 0000000..1bb70ff --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/StackWidget.java @@ -0,0 +1,47 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Widget; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; + +public abstract class StackWidget implements Widget { + private final StackPane container; + + public StackWidget(String cssClass) { + container = new StackPane(); + container.getStyleClass().add(cssClass); + } + + public void add(Pos position, Node node) { + Platform.runLater(() -> { + if (container.getChildren().contains(node)) { + return; + } + + StackPane.setAlignment(node, position); + container.getChildren().add(node); + }); + } + + public void add(Pos position, Widget widget) { + add(position, widget.getNode()); + } + + public void remove(Node node) { + Platform.runLater(() -> { + container.getChildren().remove(node); + }); + } + + public void remove(Widget widget) { + remove(widget.getNode()); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java b/app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java new file mode 100644 index 0000000..67f338c --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java @@ -0,0 +1,62 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; +import org.toop.local.AppContext; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.layout.VBox; + +public class ToggleWidget implements Widget { + private final Button button; + private final VBox container; + + private final String onKey; + private final String offKey; + + private boolean state; + + public ToggleWidget(String onKey, String offKey, boolean initialState, Consumer onToggle) { + this.onKey = onKey; + this.offKey = offKey; + this.state = initialState; + + button = new Button(AppContext.getString(getCurrentKey())); + button.setOnAction(_ -> { + state = !state; + updateText(); + if (onToggle != null) { + onToggle.accept(state); + } + }); + + container = Primitive.vbox(button); + } + + private String getCurrentKey() { + return state? offKey : onKey; + } + + private void updateText() { + button.setText(AppContext.getString(getCurrentKey())); + } + + public boolean getState() { + return state; + } + + public void setState(boolean newState) { + if (state != newState) { + state = newState; + updateText(); + } + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java new file mode 100644 index 0000000..ad9265c --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java @@ -0,0 +1,49 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; + +import javafx.geometry.Pos; + +public abstract class ViewWidget extends StackWidget { + private ViewWidget previous = null; + + public ViewWidget() { + super("bg-primary"); + } + + public void transition(ViewWidget view) { + view.previous = this; + replace(Pos.CENTER, view); + } + + public void transitionNext(ViewWidget view) { + view.previous = this; + replace(Pos.CENTER, view); + + var backButton = Primitive.button("back", () -> { + view.transitionPrevious(); + }); + + view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); + } + + public void transitionPrevious() { + if (previous == null) { + return; + } + + replace(Pos.CENTER, previous); + previous = null; + } + + public void reload(ViewWidget view) { + view.previous = previous; + replace(Pos.CENTER, view); + + var backButton = Primitive.button("back", () -> { + view.transitionPrevious(); + }); + + view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/display/SongDisplay.java b/app/src/main/java/org/toop/app/widget/display/SongDisplay.java new file mode 100644 index 0000000..c75ce8f --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/display/SongDisplay.java @@ -0,0 +1,117 @@ +package org.toop.app.widget.display; + +import org.toop.app.widget.Widget; +import org.toop.framework.audio.events.AudioEvents; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.eventbus.GlobalEventBus; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class SongDisplay extends VBox implements Widget { + private final Text songTitle; + private final ProgressBar progressBar; + private final Text progressText; + + public SongDisplay() { + new EventFlow() + .listen(this::updateTheSong); + + setAlignment(Pos.CENTER); + setMaxHeight(Region.USE_PREF_SIZE); + getStyleClass().add("song-display"); + + // TODO ADD GOOD SONG TITLES WITH ARTISTS DISPLAYED + 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"); + + // TODO ADD BETTER CSS FOR THE SKIPBUTTON WHERE ITS AT A NICER POSITION + + 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()); + }); + + pauseButton.setOnAction(event -> { + GlobalEventBus.post(new AudioEvents.PauseMusic()); + if (pauseButton.getText().equals("⏸")) { + pauseButton.setText("▶"); + } + else if (pauseButton.getText().equals("▶")) { + pauseButton.setText("⏸"); + } + }); + + previousButton.setOnAction( event -> { + GlobalEventBus.post(new AudioEvents.PreviousMusic()); + }); + + 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; + } + + @Override + public Node getNode() { + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java b/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java new file mode 100644 index 0000000..1a2f755 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java @@ -0,0 +1,60 @@ +package org.toop.app.widget.popup; + +import org.toop.app.GameInformation; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.PlayerInfoWidget; +import org.toop.app.widget.complex.PopupWidget; + +import java.util.function.Consumer; + +import javafx.geometry.Pos; + +public final class ChallengePopup extends PopupWidget { + private final GameInformation.Player playerInformation; + private final String challenger; + private final String game; + private final Consumer onAccept; + + public ChallengePopup(String challenger, String game, Consumer onAccept) { + this.challenger = challenger; + this.game = game; + this.onAccept = onAccept; + + this.playerInformation = new GameInformation.Player(); + + setupLayout(); + } + + private void setupLayout() { + var challengeText = Primitive.text("you-were-challenged-by"); + + var challengerHeader = Primitive.header(""); + challengerHeader.setText(challenger); + + var gameText = Primitive.text("to-a-game-of"); + gameText.setText(gameText.getText() + " " + game); + + var acceptButton = Primitive.button("accept", () -> onAccept.accept(playerInformation)); + var denyButton = Primitive.button("deny", () -> hide()); + + var leftSection = Primitive.vbox( + challengeText, + challengerHeader, + gameText, + Primitive.separator(), + Primitive.hbox( + acceptButton, + denyButton + ) + ); + + var playerInfoWidget = new PlayerInfoWidget(playerInformation); + + add(Pos.CENTER, + Primitive.hbox( + leftSection, + playerInfoWidget.getNode() + ) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java b/app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java new file mode 100644 index 0000000..89d10c6 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java @@ -0,0 +1,16 @@ +package org.toop.app.widget.popup; + +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; + +import javafx.geometry.Pos; + +public class ErrorPopup extends PopupWidget { + public ErrorPopup(String error) { + var confirmWidget = new ConfirmWidget("error"); + confirmWidget.setMessage(error); + confirmWidget.addButton("ok", this::hide); + + add(Pos.CENTER, confirmWidget); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java b/app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java new file mode 100644 index 0000000..f642272 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java @@ -0,0 +1,25 @@ +package org.toop.app.widget.popup; + +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; + +public final class GameOverPopup extends PopupWidget { + public GameOverPopup(boolean iWon, String winner) { + var confirmWidget = new ConfirmWidget("game-over"); + + if (winner.isEmpty()) { + confirmWidget.setMessage(AppContext.getString("the-game-ended-in-a-draw")); + } else if (iWon) { + confirmWidget.setMessage(AppContext.getString("you-win")); + } else { + confirmWidget.setMessage(AppContext.getString("you-lost-against") + ": " + winner); + } + + confirmWidget.addButton("ok", () -> hide()); + + add(Pos.CENTER, confirmWidget); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/QuitPopup.java b/app/src/main/java/org/toop/app/widget/popup/QuitPopup.java new file mode 100644 index 0000000..04349dc --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/QuitPopup.java @@ -0,0 +1,24 @@ +package org.toop.app.widget.popup; + +import org.toop.app.App; +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; + +import javafx.geometry.Pos; + +public class QuitPopup extends PopupWidget { + public QuitPopup() { + var confirmWidget = new ConfirmWidget("are-you-sure"); + + confirmWidget.addButton("yes", () -> { + App.quit(); + }); + + confirmWidget.addButton("no", () -> { + App.stopQuit(); + hide(); + }); + + add(Pos.CENTER, confirmWidget); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java b/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java new file mode 100644 index 0000000..6299fdc --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java @@ -0,0 +1,90 @@ +package org.toop.app.widget.popup; + +import org.toop.app.GameInformation; +import org.toop.app.Server; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.LabeledChoiceWidget; +import org.toop.app.widget.complex.PlayerInfoWidget; +import org.toop.app.widget.complex.PopupWidget; +import org.toop.local.AppContext; + +import java.util.function.BiConsumer; + +import javafx.geometry.Pos; +import javafx.util.StringConverter; + +public final class SendChallengePopup extends PopupWidget { + private final Server server; + private final String opponent; + private final BiConsumer onSend; + + private final GameInformation.Player playerInformation; + + public SendChallengePopup(Server server, String opponent, BiConsumer onSend) { + this.server = server; + this.opponent = opponent; + this.onSend = onSend; + + this.playerInformation = new GameInformation.Player(); + + setupLayout(); + } + + private void setupLayout() { + // --- Left side: challenge text and buttons --- + var challengeText = Primitive.text("challenge"); + + var opponentHeader = Primitive.header(opponent); + + var gameText = Primitive.text("to-a-game-of"); + + var games = server.getGameList(); + var gameChoice = new LabeledChoiceWidget<>( + "game", + new StringConverter<>() { + @Override + public String toString(String game) { + return AppContext.getString(game); + } + @Override + public String fromString(String s) { return null; } + }, + games.getFirst(), + newGame -> { + playerInformation.computerDifficulty = Math.min( + playerInformation.computerDifficulty, + Server.gameToType(newGame).getMaxDepth() + ); + }, + games.toArray(new String[0]) + ); + + var sendButton = Primitive.button( + "send", + () -> onSend.accept(playerInformation, gameChoice.getValue()) + ); + + var cancelButton = Primitive.button("cancel", () -> hide()); + + var leftSection = Primitive.vbox( + challengeText, + opponentHeader, + gameText, + gameChoice.getNode(), + Primitive.separator(), + Primitive.hbox( + sendButton, + cancelButton + ) + ); + + var playerInfoWidget = new PlayerInfoWidget(playerInformation); + + add(Pos.CENTER, + Primitive.hbox( + leftSection, + playerInfoWidget.getNode() + ) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/CreditsView.java b/app/src/main/java/org/toop/app/widget/view/CreditsView.java new file mode 100644 index 0000000..f7aaf01 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/CreditsView.java @@ -0,0 +1,81 @@ +package org.toop.app.widget.view; + +import org.toop.app.App; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.text.Text; +import javafx.util.Duration; + +public class CreditsView extends ViewWidget { + public CreditsView() { + var scrumMasterCredit = newCredit("scrum-master", "Stef"); + var productOwnerCredit = newCredit("product-owner", "Omar"); + var mergeCommanderCredit = newCredit("merge-commander", "Bas"); + var localizationCredit = newCredit("localization", "Ticho"); + var aiCredit = newCredit("ai", "Michiel"); + var developersCredit = newCredit("developers", "Michiel, Bas, Stef, Omar, Ticho"); + var moralSupportCredit = newCredit("moral-support", "Wesley"); + var openglCredit = newCredit("opengl", "Omar"); + + var topSpacer = new Region(); + topSpacer.setPrefHeight(App.getHeight()); + + var bottomSpacer = new Region(); + bottomSpacer.setPrefHeight(App.getHeight()); + + var creditsContainer = Primitive.vbox( + topSpacer, + + scrumMasterCredit, + productOwnerCredit, + mergeCommanderCredit, + localizationCredit, + aiCredit, + developersCredit, + moralSupportCredit, + openglCredit, + + bottomSpacer + ); + + var creditsScroll = Primitive.scroll(creditsContainer); + + creditsScroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + creditsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + + add(Pos.CENTER, creditsScroll); + + animate(creditsScroll, 15); + } + + private HBox newCredit(String key, String other) { + var credit = new Text(": " + other); + credit.getStyleClass().add("header"); + + var creditBox = Primitive.hbox( + Primitive.header(key), + credit + ); + + creditBox.setPrefHeight(App.getHeight() / 3.0); + return creditBox; + } + + private void animate(ScrollPane scroll, int length) { + final Timeline timeline = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(scroll.vvalueProperty(), 0.0)), + new KeyFrame(Duration.seconds(length), new KeyValue(scroll.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/widget/view/GameView.java b/app/src/main/java/org/toop/app/widget/view/GameView.java new file mode 100644 index 0000000..82778b9 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/GameView.java @@ -0,0 +1,96 @@ +package org.toop.app.widget.view; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; +import org.toop.app.widget.popup.GameOverPopup; + +import java.util.function.Consumer; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.text.Text; + +public final class GameView extends ViewWidget { + private final Text currentPlayerHeader; + private final Text currentMoveHeader; + private final Text nextPlayerHeader; + + private final Button forfeitButton; + private final Button exitButton; + + private final TextField chatInput; + + public GameView(Runnable onForfeit, Runnable onExit, Consumer onMessage) { + currentPlayerHeader = Primitive.header(""); + currentMoveHeader = Primitive.header(""); + nextPlayerHeader = Primitive.header(""); + + if (onForfeit != null) { + forfeitButton = Primitive.button("forfeit", () -> onForfeit.run()); + } else { + forfeitButton = null; + } + + exitButton = Primitive.button("exit", () -> { + onExit.run(); + transitionPrevious(); + }); + + if (onMessage != null) { + chatInput = Primitive.input("enter-your-message", "", null); + chatInput.setOnAction(_ -> { + onMessage.accept(chatInput.getText()); + chatInput.clear(); + }); + } else { + chatInput = null; + } + + setupLayout(); + } + + private void setupLayout() { + var playerInfo = Primitive.vbox( + currentPlayerHeader, + Primitive.hbox( + Primitive.separator(), + currentMoveHeader, + Primitive.separator() + ), + nextPlayerHeader + ); + + add(Pos.TOP_RIGHT, playerInfo); + + var buttons = Primitive.vbox( + forfeitButton, + exitButton + ); + + add(Pos.BOTTOM_LEFT, buttons); + + if (chatInput != null) { + add(Pos.BOTTOM_RIGHT, Primitive.vbox(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 gameOver(boolean iWon, String winner) { + new GameOverPopup(iWon, winner).show(Pos.CENTER); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java new file mode 100644 index 0000000..9ddd5f3 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java @@ -0,0 +1,74 @@ +package org.toop.app.widget.view; + +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.widget.Primitive; +import org.toop.app.widget.complex.PlayerInfoWidget; +import org.toop.app.widget.complex.ViewWidget; +import org.toop.app.widget.popup.ErrorPopup; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; + +public class LocalMultiplayerView extends ViewWidget { + private final GameInformation information; + + public LocalMultiplayerView(GameInformation.Type type) { + this(new GameInformation(type)); + } + + public LocalMultiplayerView(GameInformation information) { + this.information = information; + var playButton = Primitive.button("play", () -> { + for (var player : information.players) { + if (player.isHuman && player.name.isEmpty()) { + new ErrorPopup(AppContext.getString("please-enter-your-name")).show(Pos.CENTER); + return; + } + } + + switch (information.type) { + case TICTACTOE -> new TicTacToeGameThread(information); + case REVERSI -> new ReversiGame(information); + case CONNECT4 -> new Connect4Game(information); + // case BATTLESHIP -> new BattleshipGame(information); + } + }); + + var playerSection = setupPlayerSections(); + + add(Pos.CENTER, Primitive.vbox( + playerSection, + Primitive.separator(), + playButton + )); + } + + private ScrollPane setupPlayerSections() { + int playerCount = information.type.getPlayerCount(); + VBox[] playerBoxes = new VBox[playerCount]; + + for (int i = 0; i < playerCount; i++) { + var player = information.players[i]; + + var playerHeader = Primitive.header(""); + playerHeader.setText("player" + " #" + (i + 1)); + + var playerWidget = new PlayerInfoWidget(player); + + playerBoxes[i] = Primitive.vbox( + playerHeader, + Primitive.separator(), + playerWidget.getNode() + ); + } + + return Primitive.scroll(Primitive.hbox( + playerBoxes + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/LocalView.java b/app/src/main/java/org/toop/app/widget/view/LocalView.java new file mode 100644 index 0000000..90cfac6 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/LocalView.java @@ -0,0 +1,29 @@ +package org.toop.app.widget.view; + +import org.toop.app.GameInformation; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; + +import javafx.geometry.Pos; + +public class LocalView extends ViewWidget { + public LocalView() { + var ticTacToeButton = Primitive.button("tic-tac-toe", () -> { + transitionNext(new LocalMultiplayerView(GameInformation.Type.TICTACTOE)); + }); + + var reversiButton = Primitive.button("reversi", () -> { + transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI)); + }); + + var connect4Button = Primitive.button("connect4", () -> { + transitionNext(new LocalMultiplayerView(GameInformation.Type.CONNECT4)); + }); + + add(Pos.CENTER, Primitive.vbox( + ticTacToeButton, + reversiButton, + connect4Button + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/MainView.java b/app/src/main/java/org/toop/app/widget/view/MainView.java new file mode 100644 index 0000000..ff7a710 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/MainView.java @@ -0,0 +1,39 @@ +package org.toop.app.widget.view; + +import org.toop.app.App; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; + +import javafx.geometry.Pos; + +public class MainView extends ViewWidget { + public MainView() { + var localButton = Primitive.button("local", () -> { + transitionNext(new LocalView()); + }); + + var onlineButton = Primitive.button("online", () -> { + transitionNext(new OnlineView()); + }); + + var creditsButton = Primitive.button("credits", () -> { + transitionNext(new CreditsView()); + }); + + var optionsButton = Primitive.button("options", () -> { + transitionNext(new OptionsView()); + }); + + var quitButton = Primitive.button("quit", () -> { + App.startQuit(); + }); + + add(Pos.CENTER, Primitive.vbox( + localButton, + onlineButton, + creditsButton, + optionsButton, + quitButton + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/OnlineView.java b/app/src/main/java/org/toop/app/widget/view/OnlineView.java new file mode 100644 index 0000000..16ed1df --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/OnlineView.java @@ -0,0 +1,38 @@ +package org.toop.app.widget.view; + +import org.toop.app.Server; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.LabeledInputWidget; +import org.toop.app.widget.complex.ViewWidget; + +import javafx.geometry.Pos; + +public class OnlineView extends ViewWidget { + public OnlineView() { + var serverInformationHeader = Primitive.header("server-information"); + + var serverIPInput = new LabeledInputWidget("ip-address", "enter-the-server-ip", "", _ -> {}); + var serverPortInput = new LabeledInputWidget("port", "enter-the-server-port", "", _ -> {}); + var playerNameInput = new LabeledInputWidget("player-name", "enter-your-name", "", _ -> {}); + + var connectButton = Primitive.button("connect", () -> { + new Server( + serverIPInput.getValue(), + serverPortInput.getValue(), + playerNameInput.getValue() + ); + }); + + add(Pos.CENTER, Primitive.vbox( + serverInformationHeader, + Primitive.separator(), + + serverIPInput.getNode(), + serverPortInput.getNode(), + playerNameInput.getNode(), + Primitive.separator(), + + connectButton + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/OptionsView.java b/app/src/main/java/org/toop/app/widget/view/OptionsView.java new file mode 100644 index 0000000..198f75b --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/OptionsView.java @@ -0,0 +1,161 @@ +package org.toop.app.widget.view; + +import org.toop.app.App; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.LabeledChoiceWidget; +import org.toop.app.widget.complex.LabeledSliderWidget; +import org.toop.app.widget.complex.ViewWidget; +import org.toop.app.widget.complex.ToggleWidget; +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 java.util.Locale; + +import javafx.geometry.Pos; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; + +public class OptionsView extends ViewWidget { + public OptionsView() { + add(Pos.CENTER, Primitive.hbox( + generalSection(), + volumeSection(), + styleSection() + )); + } + + private VBox generalSection() { + var languageWidget = new LabeledChoiceWidget<>( + "language", + new StringConverter<>() { + @Override + public String toString(Locale locale) { + return AppContext.getString(locale.getDisplayName().toLowerCase()); + } + @Override + public Locale fromString(String s) { return null; } + }, + AppContext.getLocale(), + newLocale -> { + AppSettings.getSettings().setLocale(newLocale.toString()); + AppContext.setLocale(newLocale); + reload(new OptionsView()); + }, + AppContext.getLocalization().getAvailableLocales().toArray(new Locale[0]) + ); + + var fullscreenToggle = new ToggleWidget( + "fullscreen", "windowed", + AppSettings.getSettings().getFullscreen(), + fullscreen -> { + AppSettings.getSettings().setFullscreen(fullscreen); + App.setFullscreen(fullscreen); + } + ); + + return Primitive.vbox( + Primitive.header("general"), + Primitive.separator(), + + languageWidget.getNode(), + fullscreenToggle.getNode() + ); + } + + private VBox volumeSection() { + var masterVolumeWidget = new LabeledSliderWidget( + "master-volume", + 0, 100, + AppSettings.getSettings().getVolume(), + val -> { + AppSettings.getSettings().setVolume(val); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MASTERVOLUME)) + .asyncPostEvent(); + } + ); + + var effectsVolumeWidget = new LabeledSliderWidget( + "effects-volume", + 0, 100, + AppSettings.getSettings().getFxVolume(), + val -> { + AppSettings.getSettings().setFxVolume(val); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.FX)) + .asyncPostEvent(); + } + ); + + var musicVolumeWidget = new LabeledSliderWidget( + "music-volume", + 0, 100, + AppSettings.getSettings().getMusicVolume(), + val -> { + AppSettings.getSettings().setMusicVolume(val); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MUSIC)) + .asyncPostEvent(); + } + ); + + return Primitive.vbox( + Primitive.header("volume"), + Primitive.separator(), + + masterVolumeWidget.getNode(), + effectsVolumeWidget.getNode(), + musicVolumeWidget.getNode() + ); + } + + private VBox styleSection() { + var themeWidget = new LabeledChoiceWidget<>( + "theme", + new StringConverter<>() { + @Override + public String toString(String theme) { + return AppContext.getString(theme); + } + @Override + public String fromString(String s) { return null; } + }, + AppSettings.getSettings().getTheme(), + newTheme -> { + AppSettings.getSettings().setTheme(newTheme); + App.setStyle(newTheme, AppSettings.getSettings().getLayoutSize()); + }, + "dark", "light", "high-contrast" + ); + + var layoutWidget = new LabeledChoiceWidget<>( + "layout-size", + new StringConverter<>() { + @Override + public String toString(String layout) { + return AppContext.getString(layout); + } + @Override + public String fromString(String s) { return null; } + }, + AppSettings.getSettings().getLayoutSize(), + newLayout -> { + AppSettings.getSettings().setLayoutSize(newLayout); + App.setStyle(AppSettings.getSettings().getTheme(), newLayout); + }, + "small", "medium", "large" + ); + + + return Primitive.vbox( + Primitive.header("style"), + Primitive.separator(), + + themeWidget.getNode(), + layoutWidget.getNode() + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/ServerView.java b/app/src/main/java/org/toop/app/widget/view/ServerView.java new file mode 100644 index 0000000..d69c036 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/ServerView.java @@ -0,0 +1,60 @@ +package org.toop.app.widget.view; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; + +import java.util.List; +import java.util.function.Consumer; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; + +public final class ServerView extends ViewWidget { + private final String user; + private final Consumer onPlayerClicked; + private final Runnable onDisconnect; + + private final ListView