diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index db6ec38..88cd949 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,10 +1,9 @@ package org.toop.app; -import org.toop.app.widget.Widget; 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.primary.MainPrimary; +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; @@ -65,7 +64,7 @@ public final class App extends Application { AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - WidgetContainer.add(Pos.CENTER, new MainPrimary()); + WidgetContainer.add(Pos.CENTER, new MainView()); WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay()); } diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 9421004..00003b9 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; } @@ -164,7 +135,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."); } } } @@ -175,17 +146,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) { @@ -196,7 +161,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() { @@ -205,15 +170,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() { @@ -227,7 +188,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 313e0e9..9b9cecf 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -18,7 +18,6 @@ public final class ReversiCanvas extends GameCanvas { } public void drawLegalPosition(Color color, int cell) { - fill(new Color(color.getRed() * 0.25, color.getGreen() * 0.25, color.getBlue() * 0.25, 1.0), cell); - drawDot(new Color(color.getRed() * 0.5, color.getGreen() * 0.5, color.getBlue() * 0.5, 1.0), 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/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index 3645fb0..36afd09 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.Game; @@ -25,13 +24,13 @@ public final class ReversiGame { private final GameInformation information; private final int myTurn; - private Runnable onGameOver; + private final 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; @@ -42,7 +41,7 @@ public final class ReversiGame { this.myTurn = myTurn; this.onGameOver = onGameOver; - moveQueue = new LinkedBlockingQueue(); + moveQueue = new LinkedBlockingQueue<>(); game = new Reversi(); ai = new ReversiAI(); @@ -51,12 +50,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); @@ -84,8 +83,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(); @@ -93,8 +92,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); } @@ -120,7 +118,7 @@ public final class ReversiGame { final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); - view.nextPlayer(information.players[currentTurn].isHuman, + primary.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, currentValue, information.players[nextTurn].name); @@ -164,9 +162,9 @@ public final class ReversiGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { - view.gameOver(true, information.players[currentTurn].name); + primary.gameOver(true, information.players[currentTurn].name); } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); } isRunning.set(false); @@ -193,14 +191,14 @@ public final class ReversiGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { - view.gameOver(true, information.players[0].name); + 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 == Game.State.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); game.play(move); } } @@ -241,14 +239,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(); @@ -266,8 +256,8 @@ public final class ReversiGame { final SequentialTransition animation = new SequentialTransition(); isPaused.set(true); - final Color fromColor = game.getCurrentPlayer() == 0? Color.WHITE : Color.BLACK; - final Color toColor = game.getCurrentPlayer() == 0? Color.BLACK : Color.WHITE; + 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 Game.Move flip : flipped) { @@ -283,7 +273,7 @@ public final class ReversiGame { final Game.Move[] legalMoves = game.getLegalMoves(); for (final Game.Move legalMove : legalMoves) { - canvas.drawLegalPosition(toColor, legalMove.position()); + canvas.drawLegalPosition(fromColor, legalMove.position()); } }); @@ -294,7 +284,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 1ca98a9..12d800f 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.Game; @@ -24,13 +23,13 @@ public final class TicTacToeGame { private final GameInformation information; private final int myTurn; - private Runnable onGameOver; + private final Runnable onGameOver; private final BlockingQueue moveQueue; private final TicTacToe game; private final TicTacToeAI ai; - private final GameView view; + private final GameView primary; private final TicTacToeCanvas canvas; private final AtomicBoolean isRunning; @@ -48,12 +47,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); @@ -81,16 +80,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); } @@ -106,7 +104,7 @@ public final class TicTacToeGame { final String currentValue = currentTurn == 0? "X" : "O"; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); - view.nextPlayer(information.players[currentTurn].isHuman, + primary.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, currentValue, information.players[nextTurn].name); @@ -155,9 +153,9 @@ public final class TicTacToeGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { - view.gameOver(true, information.players[currentTurn].name); + primary.gameOver(true, information.players[currentTurn].name); } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); } isRunning.set(false); @@ -184,15 +182,15 @@ public final class TicTacToeGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { - view.gameOver(true, information.players[0].name); + 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 == Game.State.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(); } } @@ -244,19 +242,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/views/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java index e99de78..b96751d 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; } }); diff --git a/app/src/main/java/org/toop/app/widget/Primitive.java b/app/src/main/java/org/toop/app/widget/Primitive.java index 7fe8832..75795f2 100644 --- a/app/src/main/java/org/toop/app/widget/Primitive.java +++ b/app/src/main/java/org/toop/app/widget/Primitive.java @@ -23,8 +23,10 @@ public final class Primitive { var header = new Text(); header.getStyleClass().add("header"); - header.setText(AppContext.getString(key)); - header.textProperty().bind(AppContext.bindToKey(key)); + if (!key.isEmpty()) { + header.setText(AppContext.getString(key)); + header.textProperty().bind(AppContext.bindToKey(key)); + } return header; } @@ -33,8 +35,10 @@ public final class Primitive { var text = new Text(); text.getStyleClass().add("text"); - text.setText(AppContext.getString(key)); - text.textProperty().bind(AppContext.bindToKey(key)); + if (!key.isEmpty()) { + text.setText(AppContext.getString(key)); + text.textProperty().bind(AppContext.bindToKey(key)); + } return text; } @@ -43,8 +47,10 @@ public final class Primitive { var button = new Button(); button.getStyleClass().add("button"); - button.setText(AppContext.getString(key)); - button.textProperty().bind(AppContext.bindToKey(key)); + if (!key.isEmpty()) { + button.setText(AppContext.getString(key)); + button.textProperty().bind(AppContext.bindToKey(key)); + } if (onAction != null) { button.setOnAction(_ -> @@ -58,8 +64,10 @@ public final class Primitive { var input = new TextField(); input.getStyleClass().add("input"); - input.setPromptText(AppContext.getString(promptKey)); - input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); + if (!promptKey.isEmpty()) { + input.setPromptText(AppContext.getString(promptKey)); + input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); + } input.setText(text); @@ -133,7 +141,11 @@ public final class Primitive { hbox.getStyleClass().add("container"); hbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - hbox.getChildren().addAll(nodes); + for (var node : nodes) { + if (node != null) { + hbox.getChildren().add(node); + } + } return hbox; } @@ -143,7 +155,11 @@ public final class Primitive { vbox.getStyleClass().add("container"); vbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - vbox.getChildren().addAll(nodes); + for (var node : nodes) { + if (node != null) { + vbox.getChildren().add(node); + } + } return vbox; } diff --git a/app/src/main/java/org/toop/app/widget/WidgetContainer.java b/app/src/main/java/org/toop/app/widget/WidgetContainer.java index 1d56c43..aeeec92 100644 --- a/app/src/main/java/org/toop/app/widget/WidgetContainer.java +++ b/app/src/main/java/org/toop/app/widget/WidgetContainer.java @@ -1,19 +1,15 @@ package org.toop.app.widget; import org.toop.app.widget.complex.PopupWidget; -import org.toop.app.widget.complex.PrimaryWidget; - -import java.util.ArrayDeque; -import java.util.Deque; +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 final Deque popups = new ArrayDeque<>(); - private static StackPane root; + private static ViewWidget currentView; public static synchronized StackPane setup() { if (root != null) { @@ -21,7 +17,7 @@ public final class WidgetContainer { } root = new StackPane(); - root.getStyleClass().add("bg-primary"); + root.getStyleClass().add("bg-view"); return root; } @@ -38,15 +34,14 @@ public final class WidgetContainer { StackPane.setAlignment(widget.getNode(), position); - if (widget instanceof PrimaryWidget) { - root.getChildren().addFirst(widget.getNode()); + 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()); } - - if (widget instanceof PopupWidget popup) { - popups.push(popup); - } }); } @@ -56,15 +51,15 @@ public final class WidgetContainer { } Platform.runLater(() -> { - root.getChildren().remove(widget.getNode()); - - if (widget instanceof PrimaryWidget) { - for (var popup : popups) { - root.getChildren().remove(popup.getNode()); - } - - popups.clear(); + 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 index 107429c..e8719ce 100644 --- a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java @@ -7,14 +7,21 @@ 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(); - container = Primitive.vbox(Primitive.header(confirm), buttonsContainer); + 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) { 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/PrimaryWidget.java b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java deleted file mode 100644 index 848e76e..0000000 --- a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.toop.app.widget.complex; - -import javafx.geometry.Pos; -import org.toop.app.widget.Primitive; - -public abstract class PrimaryWidget extends StackWidget { - private PrimaryWidget previous = null; - - public PrimaryWidget() { - super("bg-primary"); - } - - public void transitionNext(PrimaryWidget primary) { - primary.previous = this; - replace(Pos.CENTER, primary); - - var backButton = Primitive.button("back", () -> { - primary.transitionPrevious(); - }); - - primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); - } - - public void transitionPrevious() { - if (previous == null) { - return; - } - - replace(Pos.CENTER, previous); - previous = null; - } - - public void reload(PrimaryWidget primary) { - primary.previous = previous; - replace(Pos.CENTER, primary); - - var backButton = Primitive.button("back", () -> { - primary.transitionPrevious(); - }); - - primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); - } -} \ 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/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/SendChallengePopup.java b/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java new file mode 100644 index 0000000..f8754fd --- /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, + GameInformation.Type.maxDepth(Server.gameToType(newGame)) + ); + }, + 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/primary/LocalPrimary.java b/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java deleted file mode 100644 index ef0364f..0000000 --- a/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.toop.app.widget.primary; - -import org.toop.app.widget.Primitive; -import org.toop.app.widget.complex.PrimaryWidget; - -import javafx.geometry.Pos; - -public class LocalPrimary extends PrimaryWidget { - public LocalPrimary() { - var ticTacToeButton = Primitive.button("tic-tac-toe", () -> { - }); - - var reversiButton = Primitive.button("reversi", () -> { - }); - - var connect4Button = Primitive.button("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/primary/CreditsPrimary.java b/app/src/main/java/org/toop/app/widget/view/CreditsView.java similarity index 92% rename from app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java rename to app/src/main/java/org/toop/app/widget/view/CreditsView.java index 82590cb..f7aaf01 100644 --- a/app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/CreditsView.java @@ -1,8 +1,8 @@ -package org.toop.app.widget.primary; +package org.toop.app.widget.view; import org.toop.app.App; import org.toop.app.widget.Primitive; -import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; @@ -14,8 +14,8 @@ import javafx.scene.layout.Region; import javafx.scene.text.Text; import javafx.util.Duration; -public class CreditsPrimary extends PrimaryWidget { - public CreditsPrimary() { +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"); 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..609a260 --- /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 = GameInformation.Type.playerCount(information.type); + 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/primary/MainPrimary.java b/app/src/main/java/org/toop/app/widget/view/MainView.java similarity index 63% rename from app/src/main/java/org/toop/app/widget/primary/MainPrimary.java rename to app/src/main/java/org/toop/app/widget/view/MainView.java index 06ecdf0..ff7a710 100644 --- a/app/src/main/java/org/toop/app/widget/primary/MainPrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/MainView.java @@ -1,27 +1,27 @@ -package org.toop.app.widget.primary; +package org.toop.app.widget.view; import org.toop.app.App; import org.toop.app.widget.Primitive; -import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import javafx.geometry.Pos; -public class MainPrimary extends PrimaryWidget { - public MainPrimary() { +public class MainView extends ViewWidget { + public MainView() { var localButton = Primitive.button("local", () -> { - transitionNext(new LocalPrimary()); + transitionNext(new LocalView()); }); var onlineButton = Primitive.button("online", () -> { - transitionNext(new OnlinePrimary()); + transitionNext(new OnlineView()); }); var creditsButton = Primitive.button("credits", () -> { - transitionNext(new CreditsPrimary()); + transitionNext(new CreditsView()); }); var optionsButton = Primitive.button("options", () -> { - transitionNext(new OptionsPrimary()); + transitionNext(new OptionsView()); }); var quitButton = Primitive.button("quit", () -> { diff --git a/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java b/app/src/main/java/org/toop/app/widget/view/OnlineView.java similarity index 84% rename from app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java rename to app/src/main/java/org/toop/app/widget/view/OnlineView.java index 98bc68e..16ed1df 100644 --- a/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/OnlineView.java @@ -1,14 +1,14 @@ -package org.toop.app.widget.primary; +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.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import javafx.geometry.Pos; -public class OnlinePrimary extends PrimaryWidget { - public OnlinePrimary() { +public class OnlineView extends ViewWidget { + public OnlineView() { var serverInformationHeader = Primitive.header("server-information"); var serverIPInput = new LabeledInputWidget("ip-address", "enter-the-server-ip", "", _ -> {}); diff --git a/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java b/app/src/main/java/org/toop/app/widget/view/OptionsView.java similarity index 95% rename from app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java rename to app/src/main/java/org/toop/app/widget/view/OptionsView.java index 9061df4..198f75b 100644 --- a/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/OptionsView.java @@ -1,10 +1,10 @@ -package org.toop.app.widget.primary; +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.PrimaryWidget; +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; @@ -18,8 +18,8 @@ import javafx.geometry.Pos; import javafx.scene.layout.VBox; import javafx.util.StringConverter; -public class OptionsPrimary extends PrimaryWidget { - public OptionsPrimary() { +public class OptionsView extends ViewWidget { + public OptionsView() { add(Pos.CENTER, Primitive.hbox( generalSection(), volumeSection(), @@ -42,7 +42,7 @@ public class OptionsPrimary extends PrimaryWidget { newLocale -> { AppSettings.getSettings().setLocale(newLocale.toString()); AppContext.setLocale(newLocale); - reload(new OptionsPrimary()); + reload(new OptionsView()); }, AppContext.getLocalization().getAvailableLocales().toArray(new Locale[0]) ); 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