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