diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/resourceBundles.xml b/.idea/resourceBundles.xml deleted file mode 100644 index af8f6fc..0000000 --- a/.idea/resourceBundles.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - localization - - - - - - - - - - - - - - - localization - - - \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index fe06b87..64513d5 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,15 +1,8 @@ package org.toop.app; -import java.util.Stack; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.scene.Scene; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; -import org.toop.app.layer.Layer; -import org.toop.app.layer.layers.MainLayer; -import org.toop.app.layer.layers.QuitPopup; -import org.toop.framework.audio.VolumeControl; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.MainView; +import org.toop.app.view.views.QuitView; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -17,154 +10,110 @@ import org.toop.framework.resource.resources.CssAsset; import org.toop.local.AppContext; import org.toop.local.AppSettings; -public final class App extends Application { - private static Stage stage; - private static Scene scene; - private static StackPane root; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +public final class App extends Application { + private static Stage stage; + private static Scene scene; - private static Stack stack; private static int height; private static int width; - private static boolean isQuitting; + private static boolean isQuitting; - public static void run(String[] args) { - launch(args); - } + public static void run(String[] args) { + launch(args); + } - @Override - public void start(Stage stage) throws Exception { + @Override + public void start(Stage stage) throws Exception { final StackPane root = new StackPane(); - final Scene scene = new Scene(root); + final Scene scene = new Scene(root); + ViewStack.setup(scene); - stage.setTitle(AppContext.getString("appTitle")); - stage.setWidth(1080); - stage.setHeight(720); + stage.setTitle(AppContext.getString("app-title")); + stage.setWidth(1080); + stage.setHeight(720); - stage.setOnCloseRequest( - event -> { - event.consume(); + stage.setOnCloseRequest(event -> { + event.consume(); + startQuit(); + }); - if (!isQuitting) { - quitPopup(); - } - }); + stage.setScene(scene); + stage.setResizable(false); - stage.setScene(scene); - stage.setResizable(false); + stage.show(); - stage.show(); + App.stage = stage; + App.scene = scene; - App.stage = stage; - App.scene = scene; - App.root = root; + App.width = (int)stage.getWidth(); + App.height = (int)stage.getHeight(); - App.stack = new Stack<>(); + App.isQuitting = false; - App.width = (int) stage.getWidth(); - App.height = (int) stage.getHeight(); + AppSettings.applySettings(); + new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - App.isQuitting = false; + ViewStack.push(new MainView()); + } - new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent(); + public static void startQuit() { + if (isQuitting) { + return; + } - final AppSettings settings = new AppSettings(); - settings.applySettings(); + ViewStack.push(new QuitView()); + isQuitting = true; + } - activate(new MainLayer()); - } + public static void stopQuit() { + ViewStack.pop(); + isQuitting = false; + } - public static void activate(Layer layer) { - Platform.runLater( - () -> { - popAll(); - push(layer); - }); - } + public static void quit() { + ViewStack.cleanup(); + stage.close(); + } - public static void push(Layer layer) { - Platform.runLater( - () -> { - root.getChildren().addLast(layer.getLayer()); - stack.push(layer); - }); - } + public static void reload() { + stage.setTitle(AppContext.getString("app-title")); + ViewStack.reload(); + } - public static void pop() { - Platform.runLater( - () -> { - root.getChildren().removeLast(); - stack.pop(); + public static void setFullscreen(boolean fullscreen) { + stage.setFullScreen(fullscreen); - isQuitting = false; - }); - } + width = (int) stage.getWidth(); + height = (int) stage.getHeight(); - public static void popAll() { - Platform.runLater( - () -> { - final int childrenCount = root.getChildren().size(); + reload(); + } - for (int i = 0; i < childrenCount; i++) { - try { - root.getChildren().removeLast(); - } catch (Exception e) { - IO.println(e); // TODO: Use logger - } - } + public static void setStyle(String theme, String layoutSize) { + final int stylesCount = scene.getStylesheets().size(); - stack.removeAllElements(); - }); - } + for (int i = 0; i < stylesCount; i++) { + scene.getStylesheets().removeLast(); + } - public static void quitPopup() { - Platform.runLater( - () -> { - push(new QuitPopup()); - isQuitting = true; - }); - } + scene.getStylesheets().add(ResourceManager.get("general.css").getUrl()); + scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); + scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - public static void quit() { - new EventFlow().addPostEvent(new AudioEvents.StopAudioManager()).postEvent(); - stage.close(); - } + reload(); + } - public static void reloadAll() { - stage.setTitle(AppContext.getString("appTitle")); + public static int getWidth() { + return width; + } - for (final Layer layer : stack) { - layer.reload(); - } - } - - public static void setFullscreen(boolean fullscreen) { - stage.setFullScreen(fullscreen); - - width = (int) stage.getWidth(); - height = (int) stage.getHeight(); - - reloadAll(); - } - - public static void setStyle(String theme, String layoutSize) { - final int stylesCount = scene.getStylesheets().size(); - - for (int i = 0; i < stylesCount; i++) { - scene.getStylesheets().removeLast(); - } - - scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); - scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - - reloadAll(); - } - - public static int getWidth() { - return width; - } - - public static int getHeight() { - return height; - } -} + public static int getHeight() { + return height; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java index 2a3e56f..5380003 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -1,10 +1,41 @@ package org.toop.app; -public record GameInformation( - String[] playerName, - boolean[] isPlayerHuman, - int[] computerDifficulty, - int[] computerThinkTime, - boolean isConnectionLocal, - String serverIP, - String serverPort) {} +public class GameInformation { + public enum Type { + TICTACTOE, + REVERSI; + + public static int playerCount(Type type) { + return switch (type) { + case TICTACTOE -> 2; + case REVERSI -> 2; + }; + } + + public static int maxDepth(Type type) { + return switch (type) { + case TICTACTOE -> 5; // Todo. 5 seems to always draw or win. could increase to 9 but that might affect performance + case REVERSI -> 10; // Todo. 10 is a guess. might be too slow or too bad. + }; + } + } + + public static class Player { + public String name = ""; + public boolean isHuman = true; + public int computerDifficulty = 1; + public int computerThinkTime = 1; + } + + public final Type type; + public final Player[] players; + + public GameInformation(Type type) { + this.type = type; + players = new Player[Type.playerCount(type)]; + + for (int i = 0; i < players.length; i++) { + players[i] = new Player(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java new file mode 100644 index 0000000..8ec565a --- /dev/null +++ b/app/src/main/java/org/toop/app/Server.java @@ -0,0 +1,210 @@ +package org.toop.app; + +import org.toop.app.game.ReversiGame; +import org.toop.app.game.TicTacToeGame; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.ChallengeView; +import org.toop.app.view.views.ErrorView; +import org.toop.app.view.views.OnlineView; +import org.toop.app.view.views.SendChallengeView; +import org.toop.app.view.views.ServerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.local.AppContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class Server { + private String user = ""; + + private long clientId = -1; + private List onlinePlayers = new CopyOnWriteArrayList(); + + private ServerView view; + + private boolean isPolling = true; + + public static GameInformation.Type gameToType(String game) { + if (game.equalsIgnoreCase("tic-tac-toe")) { + return GameInformation.Type.TICTACTOE; + } else if (game.equalsIgnoreCase("reversi")) { + return GameInformation.Type.REVERSI; + } + + return null; + } + + public Server(String ip, String port, String user) { + if (ip.split("\\.").length < 4) { + ViewStack.push(new ErrorView("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address"))); + return; + } + + int parsedPort = -1; + + try { + parsedPort = Integer.parseInt(port); + } catch (NumberFormatException _) { + ViewStack.push(new ErrorView("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port"))); + return; + } + + if (user.isEmpty()) { + ViewStack.push(new ErrorView(AppContext.getString("invalid-username"))); + return; + } + + new EventFlow() + .addPostEvent(NetworkEvents.StartClient.class, ip, parsedPort) + .onResponse(NetworkEvents.StartClientResponse.class, e -> { + this.user = user; + clientId = e.clientId(); + + new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent(); + + view = new ServerView(user, this::sendChallenge, this::disconnect); + ViewStack.push(view); + + startPopulateThread(); + }).postEvent(); + + new EventFlow().listen(this::handleReceivedChallenge); + } + + private void populatePlayerList() { + new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> { + if (e.clientId() == clientId) { + onlinePlayers = new ArrayList(List.of(e.playerlist())); + onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + + view.update(onlinePlayers); + } + }); + + final EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)); + + while (isPolling) { + sendGetPlayerList.postEvent(); + + try { + Thread.sleep(5000); + } catch (InterruptedException _) {} + } + } + + private void sendChallenge(String opponent) { + if (!isPolling) { + return; + } + + ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { + new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)) + .listen(NetworkEvents.GameMatchResponse.class, e -> { + if (e.clientId() == clientId) { + isPolling = false; + onlinePlayers.clear(); + + final GameInformation.Type type = gameToType(gameType); + final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent())? 1 : 0; + + final GameInformation information = new GameInformation(type); + information.players[0] = playerInformation; + information.players[0].name = user; + information.players[1].name = opponent; + + switch (type) { + case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + } + } + }).postEvent(); + })); + } + + private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { + if (!isPolling) { + return; + } + + String challengerName = response.challengerName(); + challengerName = challengerName.substring(challengerName.indexOf("\"") + 1); + challengerName = challengerName.substring(0, challengerName.indexOf("\"")); + + String gameType = response.gameType(); + gameType = gameType.substring(gameType.indexOf("\"") + 1); + gameType = gameType.substring(0, gameType.indexOf("\"")); + + final String finalGameType = gameType; + + ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { + final int challengeId = Integer.parseInt(response.challengeId().substring(18, response.challengeId().length() - 2)); + new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); + + ViewStack.pop(); + + new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { + if (e.clientId() == clientId) { + isPolling = false; + onlinePlayers.clear(); + + final GameInformation.Type type = gameToType(finalGameType); + final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent())? 1 : 0; + + final GameInformation information = new GameInformation(type); + information.players[0] = playerInformation; + information.players[0].name = user; + information.players[1].name = e.opponent(); + + switch (type) { + case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + } + } + }); + })); + } + + private void sendMessage(String message) { + new EventFlow().addPostEvent(new NetworkEvents.SendMessage(clientId, message)).postEvent(); + } + + private void disconnect() { + new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); + ViewStack.push(new OnlineView()); + } + + private void forfeitGame() { + new EventFlow().addPostEvent(new NetworkEvents.SendForfeit(clientId)).postEvent(); + } + + private void exitGame() { + forfeitGame(); + + ViewStack.push(view); + startPopulateThread(); + } + + private void startPopulateThread() { + isPolling = true; + + final Thread populateThread = new Thread(this::populatePlayerList); + populateThread.setDaemon(false); + populateThread.start(); + } + + public List getGamesList() { + final List list = new ArrayList(); + list.add("tic-tac-toe"); // Todo: get games list from server and check if the game is supported + list.add("reversi"); + + new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) + .listen(NetworkEvents.GamelistResponse.class, e -> { + System.out.println(Arrays.toString(e.gamelist())); + }).postEvent(); + + return list; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/GameCanvas.java b/app/src/main/java/org/toop/app/canvas/GameCanvas.java index 5ed5775..52d0521 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -1,130 +1,122 @@ package org.toop.app.canvas; -import java.util.function.Consumer; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.MouseButton; import javafx.scene.paint.Color; +import java.util.function.Consumer; + public abstract class GameCanvas { - protected record Cell(float x, float y, float width, float height) {} + protected record Cell(float x, float y, float width, float height) { + public boolean isInside(double x, double y) { + return x >= this.x && x <= this.x + width && + y >= this.y && y <= this.y + height; + } + } - protected final Canvas canvas; - protected final GraphicsContext graphics; + protected final Canvas canvas; + protected final GraphicsContext graphics; - protected final Color color; + protected final Color color; - protected int width; - protected int height; + protected final int width; + protected final int height; - protected final int rows; - protected final int columns; + protected final int rowSize; + protected final int columnSize; - protected final int gapSize; - protected final boolean edges; + protected final int gapSize; + protected final boolean edges; - protected final Cell[] cells; + protected final Cell[] cells; - protected GameCanvas( - Color color, - int width, - int height, - int rows, - int columns, - int gapSize, - boolean edges, - Consumer onCellClicked) { - canvas = new Canvas(width, height); - graphics = canvas.getGraphicsContext2D(); + protected GameCanvas(Color color, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked) { + canvas = new Canvas(width, height); + graphics = canvas.getGraphicsContext2D(); - this.color = color; + this.color = color; - this.width = width; - this.height = height; + this.width = width; + this.height = height; - this.rows = rows; - this.columns = columns; + this.rowSize = rowSize; + this.columnSize = columnSize; - this.gapSize = gapSize; - this.edges = edges; + this.gapSize = gapSize; + this.edges = edges; - cells = new Cell[rows * columns]; + cells = new Cell[rowSize * columnSize]; - final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows; - final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns; + final float cellWidth = ((float)width - gapSize * rowSize - gapSize) / rowSize; + final float cellHeight = ((float)height - gapSize * columnSize - gapSize) / columnSize; - for (int y = 0; y < columns; y++) { - final float startY = y * cellHeight + y * gapSize; + for (int y = 0; y < columnSize; y++) { + final float startY = y * cellHeight + y * gapSize + gapSize; - for (int x = 0; x < rows; x++) { - final float startX = x * cellWidth + x * gapSize; - cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight); - } - } + for (int x = 0; x < rowSize; x++) { + final float startX = x * cellWidth + x * gapSize + gapSize; + cells[x + y * rowSize] = new Cell(startX, startY, cellWidth, cellHeight); + } + } - canvas.setOnMouseClicked( - event -> { - if (event.getButton() != MouseButton.PRIMARY) { - return; - } + canvas.setOnMouseClicked(event -> { + if (event.getButton() != MouseButton.PRIMARY) { + return; + } - final int column = (int) ((event.getX() / width) * rows); - final int row = (int) ((event.getY() / height) * columns); + final int column = (int)((event.getX() / this.width) * rowSize); + final int row = (int)((event.getY() / this.height) * columnSize); - event.consume(); - onCellClicked.accept(row * rows + column); - }); + final Cell cell = cells[column + row * rowSize]; - render(); - } + if (cell.isInside(event.getX(), event.getY())) { + event.consume(); + onCellClicked.accept(column + row * rowSize); + } + }); - public void clear() { - graphics.clearRect(0, 0, width, height); - } + render(); + } - public void render() { - graphics.setFill(color); + public void clear() { + graphics.clearRect(0, 0, width, height); + } - for (int x = 1; x < rows; x++) { - graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height); - } + public void render() { + graphics.setFill(color); - for (int y = 1; y < columns; y++) { - graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize); - } + for (int x = 0; x < rowSize - 1; x++) { + final float start = cells[x].x + cells[x].width; + graphics.fillRect(start, gapSize, gapSize, height - gapSize * 2); + } - if (edges) { - graphics.fillRect(-gapSize, 0, gapSize, height); - graphics.fillRect(0, -gapSize, width, gapSize); + for (int y = 0; y < columnSize - 1; y++) { + final float start = cells[y * rowSize].y + cells[y * rowSize].height; + graphics.fillRect(gapSize, start, width - gapSize * 2, gapSize); + } - graphics.fillRect(width - gapSize, 0, gapSize, height); - graphics.fillRect(0, height - gapSize, width, gapSize); - } - } + if (edges) { + graphics.fillRect(0, 0, width, gapSize); + graphics.fillRect(0, 0, gapSize, height); - public void draw(Color color, int cell) { - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; + graphics.fillRect(width - gapSize, 0, gapSize, height); + graphics.fillRect(0, height - gapSize, width, gapSize); + } + } - final float width = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; + public void fill(Color color, int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); - graphics.setFill(color); - graphics.fillRect(x, y, width, height); - } + final float width = cells[cell].width(); + final float height = cells[cell].height(); - public void resize(int width, int height) { - canvas.setWidth(width); - canvas.setHeight(height); + graphics.setFill(color); + graphics.fillRect(x, y, width, height); + } - this.width = width; - this.height = height; - - clear(); - render(); - } - - public Canvas getCanvas() { - return canvas; - } -} + public Canvas getCanvas() { + return canvas; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java new file mode 100644 index 0000000..277937c --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -0,0 +1,34 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; + +import java.util.function.Consumer; + +public final class ReversiCanvas extends GameCanvas { + public ReversiCanvas(Color color, int width, int height, Consumer onCellClicked) { + super(color, width, height, 8, 8, 10, true, onCellClicked); + drawStartingDots(); + } + + public void drawDot(Color color, int cell) { + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + gapSize; + + final float width = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; + + graphics.setFill(color); + graphics.fillOval(x, y, width, height); + } + + public void drawStartingDots() { + drawDot(Color.BLACK, 28); + drawDot(Color.WHITE, 36); + drawDot(Color.BLACK, 35); + drawDot(Color.WHITE, 27); + } + + public void drawLegalPosition(int cell) { + drawDot(new Color(1.0f, 0.0f, 0.0f, 0.5f), 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 1838335..06d0b31 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -1,37 +1,38 @@ package org.toop.app.canvas; -import java.util.function.Consumer; import javafx.scene.paint.Color; -public class TicTacToeCanvas extends GameCanvas { - public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 3, 3, 10, false, onCellClicked); - } +import java.util.function.Consumer; - public void drawX(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); +public final class TicTacToeCanvas extends GameCanvas { + public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { + super(color, width, height, 3, 3, 30, false, onCellClicked); + } - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; + public void drawX(Color color, int cell) { + graphics.setStroke(color); + graphics.setLineWidth(gapSize); - final float width = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + gapSize; - graphics.strokeLine(x, y, x + width, y + height); - graphics.strokeLine(x + width, y, x, y + height); - } + final float width = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; - public void drawO(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); + graphics.strokeLine(x, y, x + width, y + height); + graphics.strokeLine(x + width, y, x, y + height); + } - final float x = cells[cell].x() + gapSize; - final float y = cells[cell].y() + gapSize; + public void drawO(Color color, int cell) { + graphics.setStroke(color); + graphics.setLineWidth(gapSize); - final float width = cells[cell].width() - gapSize * 2; - final float height = cells[cell].height() - gapSize * 2; + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + gapSize; - graphics.strokeOval(x, y, width, height); - } -} + final float width = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; + + graphics.strokeOval(x, y, width, height); + } +} \ 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..18527fe --- /dev/null +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -0,0 +1,257 @@ +package org.toop.app.game; + +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.ReversiCanvas; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.GameView; +import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.Game; +import org.toop.game.reversi.Reversi; +import org.toop.game.reversi.ReversiAI; + +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 ReversiGame { + private final GameInformation information; + + private final int myTurn; + private final BlockingQueue moveQueue; + + private final Reversi game; + private final ReversiAI ai; + + private final GameView view; + private final ReversiCanvas canvas; + + private final AtomicBoolean isRunning; + + public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + this.information = information; + + this.myTurn = myTurn; + moveQueue = new LinkedBlockingQueue(); + + game = new Reversi(); + ai = new ReversiAI(); + + 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 ReversiCanvas(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? 'B' : 'W'; + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'B' : 'W'; + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + } + }); + + view.add(Pos.CENTER, canvas.getCanvas()); + ViewStack.push(view); + + if (onForfeit == null || onExit == null) { + new Thread(this::localGameThread).start(); + } else { + new EventFlow() + .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) + .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + + setGameLabels(myTurn == 0); + } + + updateCanvas(); + } + + public ReversiGame(GameInformation information) { + this(information, 0, null, null, null); + } + + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'B' : 'W'; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + String.valueOf(currentValue), + information.players[nextTurn].name); + + Game.Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Game.Move wants = moveQueue.take(); + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + if (legalMove.position() == wants.position() && + legalMove.value() == wants.value()) { + move = wants; + break; + } + } + } catch (InterruptedException _) {} + } else { + final long start = System.currentTimeMillis(); + + move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); + + if (information.players[currentTurn].computerThinkTime > 0) { + final long elapsedTime = System.currentTimeMillis() - start; + final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime; + + try { + Thread.sleep((long) (sleepTime * Math.random())); + } catch (InterruptedException _) {} + } + } + + if (move == null) { + continue; + } + + final Game.State state = game.play(move); + updateCanvas(); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + view.gameOver(true, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + + isRunning.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 Game.Move move = new Game.Move(Integer.parseInt(response.move()), playerChar); + final Game.State state = game.play(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + view.gameOver(true, information.players[0].name); + } else { + view.gameOver(false, information.players[1].name); + } + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + } + + updateCanvas(); + setGameLabels(game.getCurrentTurn() == myTurn); + } + + 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 Game.Move move = ai.findBestMove(game, information.players[0].computerDifficulty); + + assert move != null; + position = move.position(); + } + + new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short) position)) + .postEvent(); + } + + private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { + if (!isRunning.get()) { + return; + } + + view.updateChat(msg.message()); + } + + private void updateCanvas() { + // Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve. + + canvas.clear(); + canvas.render(); + + for (int i = 0; i < game.board.length; i++) { + if (game.board[i] == 'B') { + canvas.drawDot(Color.BLACK, i); + } else if (game.board[i] == 'W') { + canvas.drawDot(Color.WHITE, i); + } + } + + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + canvas.drawLegalPosition(legalMove.position()); + } + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'B' : 'W'; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + String.valueOf(currentValue), + information.players[isMe? 1 : 0].name); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java new file mode 100644 index 0000000..cddef0a --- /dev/null +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -0,0 +1,244 @@ +package org.toop.app.game; + +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.TicTacToeCanvas; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.GameView; +import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.Game; +import org.toop.game.tictactoe.TicTacToe; +import org.toop.game.tictactoe.TicTacToeAI; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class TicTacToeGame { + private final GameInformation information; + + private final int myTurn; + private final BlockingQueue moveQueue; + + private final TicTacToe game; + private final TicTacToeAI ai; + + private final GameView view; + private final TicTacToeCanvas canvas; + + private AtomicBoolean isRunning; + + public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + this.information = information; + + this.myTurn = myTurn; + moveQueue = new LinkedBlockingQueue(); + + game = new TicTacToe(); + ai = new TicTacToeAI(); + + 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 TicTacToeCanvas(Color.GRAY, + (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, + (cell) -> { + if (onForfeit == null || onExit == null) { + if (information.players[game.getCurrentTurn()].isHuman) { + final char value = game.getCurrentTurn() == 0? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + } + }); + + view.add(Pos.CENTER, canvas.getCanvas()); + ViewStack.push(view); + + if (onForfeit == null || onExit == null) { + new Thread(this::localGameThread).start(); + } else { + new EventFlow() + .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) + .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + + setGameLabels(myTurn == 0); + } + } + + public TicTacToeGame(GameInformation information) { + this(information, 0, null, null, null); + } + + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'X' : 'O'; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + String.valueOf(currentValue), + information.players[nextTurn].name); + + Game.Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Game.Move wants = moveQueue.take(); + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + if (legalMove.position() == wants.position() && + legalMove.value() == wants.value()) { + move = wants; + break; + } + } + } catch (InterruptedException _) {} + } else { + final long start = System.currentTimeMillis(); + + move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); + + if (information.players[currentTurn].computerThinkTime > 0) { + final long elapsedTime = System.currentTimeMillis() - start; + final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime; + + try { + Thread.sleep((long)(sleepTime * Math.random())); + } catch (InterruptedException _) {} + } + } + + if (move == null) { + continue; + } + + final Game.State state = game.play(move); + + if (move.value() == 'X') { + canvas.drawX(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.ROYALBLUE, move.position()); + } + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + view.gameOver(true, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + + isRunning.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 Game.Move move = new Game.Move(Integer.parseInt(response.move()), playerChar); + final Game.State state = game.play(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + view.gameOver(true, information.players[0].name); + } else { + view.gameOver(false, information.players[1].name); + } + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + } + + if (move.value() == 'X') { + canvas.drawX(Color.RED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.BLUE, move.position()); + } + + setGameLabels(game.getCurrentTurn() == myTurn); + } + + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + if (!isRunning.get()) { + return; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final Game.Move move = 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 setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'X' : 'O'; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + String.valueOf(currentValue), + information.players[isMe? 1 : 0].name); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/Container.java b/app/src/main/java/org/toop/app/layer/Container.java deleted file mode 100644 index 89e6436..0000000 --- a/app/src/main/java/org/toop/app/layer/Container.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.toop.app.layer; - -import javafx.scene.Node; -import javafx.scene.layout.Region; - -public abstract class Container { - public abstract Region getContainer(); - - public abstract void addNodes(Node... nodes); - - public abstract void addContainer(Container container, boolean fill); -} diff --git a/app/src/main/java/org/toop/app/layer/Layer.java b/app/src/main/java/org/toop/app/layer/Layer.java deleted file mode 100644 index d357200..0000000 --- a/app/src/main/java/org/toop/app/layer/Layer.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.toop.app.layer; - -import javafx.geometry.Pos; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import org.toop.app.App; -import org.toop.app.canvas.GameCanvas; - -public abstract class Layer { - protected StackPane layer; - protected Region background; - - protected Layer(String... backgroundStyles) { - layer = new StackPane(); - - background = new Region(); - background.getStyleClass().addAll(backgroundStyles); - background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE); - - layer.getChildren().addLast(background); - } - - protected void addContainer( - Container container, - Pos position, - int xOffset, - int yOffset, - int widthPercent, - int heightPercent) { - StackPane.setAlignment(container.getContainer(), position); - - final double widthUnit = App.getWidth() / 100.0; - final double heightUnit = App.getHeight() / 100.0; - - if (widthPercent > 0) { - container.getContainer().setMaxWidth(widthPercent * widthUnit); - } else { - container.getContainer().setMaxWidth(Region.USE_PREF_SIZE); - } - - if (heightPercent > 0) { - container.getContainer().setMaxHeight(heightPercent * heightUnit); - } else { - container.getContainer().setMaxHeight(Region.USE_PREF_SIZE); - } - - container.getContainer().setTranslateX(xOffset * widthUnit); - container.getContainer().setTranslateY(yOffset * heightUnit); - - layer.getChildren().addLast(container.getContainer()); - } - - protected void addGameCanvas(GameCanvas canvas, Pos position, int xOffset, int yOffset) { - StackPane.setAlignment(canvas.getCanvas(), position); - - final double widthUnit = App.getWidth() / 100.0; - final double heightUnit = App.getHeight() / 100.0; - - canvas.getCanvas().setTranslateX(xOffset * widthUnit); - canvas.getCanvas().setTranslateY(yOffset * heightUnit); - - layer.getChildren().addLast(canvas.getCanvas()); - } - - protected void pop() { - if (layer.getChildren().size() <= 1) { - return; - } - - layer.getChildren().removeLast(); - } - - protected void popAll() { - final int containers = layer.getChildren().size(); - - for (int i = 1; i < containers; i++) { - layer.getChildren().removeLast(); - } - } - - public StackPane getLayer() { - return layer; - } - - public abstract void reload(); -} diff --git a/app/src/main/java/org/toop/app/layer/NodeBuilder.java b/app/src/main/java/org/toop/app/layer/NodeBuilder.java deleted file mode 100644 index b55a70d..0000000 --- a/app/src/main/java/org/toop/app/layer/NodeBuilder.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.toop.app.layer; - -import java.util.function.Consumer; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.geometry.Orientation; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.text.Text; -import org.toop.framework.audio.events.AudioEvents; -import org.toop.framework.eventbus.EventFlow; - -public final class NodeBuilder { - public static void addCss(Node node, String... cssClasses) { - node.getStyleClass().addAll(cssClasses); - } - - public static void setCss(Node node, String... cssClasses) { - node.getStyleClass().removeAll(); - node.getStyleClass().addAll(cssClasses); - } - - public static Text header(String x) { - final Text element = new Text(x); - setCss(element, "text-primary", "text-header"); - - return element; - } - - public static Text text(String x) { - final Text element = new Text(x); - setCss(element, "text-secondary", "text-normal"); - - return element; - } - - public static Label button(String x, Runnable runnable) { - final Label element = new Label(x); - setCss(element, "button", "text-normal"); - - element.setOnMouseClicked( - _ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - runnable.run(); - }); - - return element; - } - - public static Label toggle(String x1, String x2, boolean toggled, Consumer consumer) { - final Label element = new Label(toggled ? x2 : x1); - setCss(element, "toggle", "text-normal"); - - final BooleanProperty checked = new SimpleBooleanProperty(toggled); - - element.setOnMouseClicked( - _ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - checked.set(!checked.get()); - - if (checked.get()) { - element.setText(x1); - } else { - element.setText(x2); - } - - consumer.accept(checked.get()); - }); - - return element; - } - - public static Slider slider(int max, int initial, Consumer consumer) { - final Slider element = new Slider(0, max, initial); - setCss(element, "bg-slider-track"); - - element.setMinorTickCount(0); - element.setMajorTickUnit(1); - element.setBlockIncrement(1); - - element.setSnapToTicks(true); - element.setShowTickLabels(true); - - element.setOnMouseClicked( - _ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - }); - - element.valueProperty() - .addListener( - (_, _, newValue) -> { - consumer.accept(newValue.intValue()); - }); - - return element; - } - - public static TextField input(String x, Consumer consumer) { - final TextField element = new TextField(x); - setCss(element, "input", "text-normal"); - - element.setOnMouseClicked( - _ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - }); - - element.textProperty() - .addListener( - (_, _, newValue) -> { - consumer.accept(newValue); - }); - - return element; - } - - public static ChoiceBox choiceBox(Consumer consumer) { - final ChoiceBox element = new ChoiceBox<>(); - setCss(element, "choice-box", "text-normal"); - - element.setOnMouseClicked( - _ -> { - new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); - }); - - element.valueProperty() - .addListener( - (_, _, newValue) -> { - consumer.accept(newValue); - }); - - return element; - } - - public static Separator separator() { - final Separator element = new Separator(Orientation.HORIZONTAL); - setCss(element, "separator"); - - return element; - } -} diff --git a/app/src/main/java/org/toop/app/layer/Popup.java b/app/src/main/java/org/toop/app/layer/Popup.java deleted file mode 100644 index 7e498df..0000000 --- a/app/src/main/java/org/toop/app/layer/Popup.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.toop.app.layer; - -import org.toop.app.App; - -public abstract class Popup extends Layer { - protected Popup(boolean popOnBackground, String... backgroundStyles) { - super(backgroundStyles); - - if (popOnBackground) { - background.setOnMouseClicked( - _ -> { - App.pop(); - }); - } - } - - protected Popup(boolean popOnBackground) { - this(popOnBackground, "bg-popup"); - } -} diff --git a/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java b/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java deleted file mode 100644 index 2350216..0000000 --- a/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.toop.app.layer.containers; - -import javafx.collections.ObservableList; -import javafx.scene.Node; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import org.toop.app.layer.Container; - -public final class HorizontalContainer extends Container { - private final HBox container; - - public HorizontalContainer(int spacing, String... cssClasses) { - container = new HBox(spacing); - container.getStyleClass().addAll(cssClasses); - } - - public HorizontalContainer(int spacing) { - this(spacing, "container"); - } - - @Override - public Region getContainer() { - return container; - } - - @Override - public void addNodes(Node... nodes) { - container.getChildren().addAll(nodes); - } - - @Override - public void addContainer(Container container, boolean fill) { - if (fill) { - container.getContainer().setMinSize(0, 0); - container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - HBox.setHgrow(container.getContainer(), Priority.ALWAYS); - } else { - container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - } - - this.container.getChildren().add(container.getContainer()); - - if (fill) { - balanceChildWidths(); - } - } - - private void balanceChildWidths() { - final ObservableList children = container.getChildren(); - final double widthPerChild = container.getWidth() / children.size(); - - for (final Node child : children) { - if (child instanceof Region) { - ((Region) child).setPrefWidth(widthPerChild); - } - } - } -} diff --git a/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java b/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java deleted file mode 100644 index 56d610c..0000000 --- a/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.toop.app.layer.containers; - -import javafx.collections.ObservableList; -import javafx.scene.Node; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; -import org.toop.app.layer.Container; - -public final class VerticalContainer extends Container { - private final VBox container; - - public VerticalContainer(int spacing, String... cssClasses) { - container = new VBox(spacing); - container.getStyleClass().addAll(cssClasses); - } - - public VerticalContainer(int spacing) { - this(spacing, "container"); - } - - @Override - public Region getContainer() { - return container; - } - - @Override - public void addNodes(Node... nodes) { - container.getChildren().addAll(nodes); - } - - @Override - public void addContainer(Container container, boolean fill) { - if (fill) { - container.getContainer().setMinSize(0, 0); - container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - VBox.setVgrow(container.getContainer(), Priority.ALWAYS); - } else { - container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - } - - this.container.getChildren().add(container.getContainer()); - - if (fill) { - balanceChildHeights(); - } - } - - private void balanceChildHeights() { - final ObservableList children = container.getChildren(); - final double heightPerChild = container.getHeight() / children.size(); - - for (final Node child : children) { - if (child instanceof Region) { - ((Region) child).setPrefHeight(heightPerChild); - } - } - } -} diff --git a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java index c631932..e69de29 100644 --- a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java +++ b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java @@ -1,237 +0,0 @@ -package org.toop.app.layer.layers; - -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; -import javafx.application.Platform; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.ListView; -import org.toop.app.App; -import org.toop.app.GameInformation; -import org.toop.app.layer.Container; -import org.toop.app.layer.Layer; -import org.toop.app.layer.NodeBuilder; -import org.toop.app.layer.Popup; -import org.toop.app.layer.containers.HorizontalContainer; -import org.toop.app.layer.containers.VerticalContainer; -import org.toop.app.layer.layers.game.TicTacToeLayer; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.clients.TournamentNetworkingClient; -import org.toop.framework.networking.events.NetworkEvents; -import org.toop.local.AppContext; - -public final class ConnectedLayer extends Layer { - private static Timer pollTimer = new Timer(); - - private static class ChallengePopup extends Popup { - private final GameInformation information; - - private final String challenger; - private final String game; - - private final long clientID; - private final int challengeID; - - public ChallengePopup( - GameInformation information, - String challenger, - String game, - long clientID, - String challengeID) { - super(false, "bg-popup"); - - this.information = information; - - this.challenger = challenger; - this.game = game; - - this.clientID = clientID; - this.challengeID = - Integer.parseInt(challengeID.substring(18, challengeID.length() - 2)); - - reload(); - } - - @Override - public void reload() { - popAll(); - - final var challengeText = NodeBuilder.header(AppContext.getString("challengeText")); - final var challengerNameText = NodeBuilder.header(challenger); - - final var gameText = NodeBuilder.text(AppContext.getString("gameIsText")); - final var gameNameText = NodeBuilder.text(game); - - final var acceptButton = - NodeBuilder.button( - AppContext.getString("accept"), - () -> { - pollTimer.cancel(); - - new EventFlow() - .addPostEvent( - new NetworkEvents.SendAcceptChallenge( - clientID, challengeID)) - .postEvent(); - App.activate(new TicTacToeLayer(information, clientID)); - }); - - final var denyButton = - NodeBuilder.button( - AppContext.getString("deny"), - () -> { - App.pop(); - }); - - final Container controlContainer = new HorizontalContainer(30); - controlContainer.addNodes(acceptButton, denyButton); - - final Container mainContainer = new VerticalContainer(30); - mainContainer.addNodes(challengeText, challengerNameText); - mainContainer.addNodes(gameText, gameNameText); - - mainContainer.addContainer(controlContainer, false); - - addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30); - } - } - - GameInformation information; - long clientId; - String user; - List onlinePlayers = new CopyOnWriteArrayList<>(); - - public ConnectedLayer(GameInformation information) { - super("bg-primary"); - - this.information = information; - - new EventFlow() - .addPostEvent( - NetworkEvents.StartClient.class, - new TournamentNetworkingClient(), - information.serverIP(), - Integer.parseInt(information.serverPort())) - .onResponse( - NetworkEvents.StartClientResponse.class, - e -> { - clientId = e.clientId(); - user = information.playerName()[0].replaceAll("\\s+", ""); - - new EventFlow() - .addPostEvent( - new NetworkEvents.SendLogin(this.clientId, this.user)) - .postEvent(); - - Thread popThread = new Thread(this::populatePlayerList); - popThread.setDaemon(false); - popThread.start(); - }) - .postEvent(); - - new EventFlow().listen(this::handleReceivedChallenge); - - reload(); - } - - private void populatePlayerList() { - EventFlow sendGetPlayerList = - new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(this.clientId)); - new EventFlow() - .listen( - NetworkEvents.PlayerlistResponse.class, - e -> { - if (e.clientId() == this.clientId) { - List playerList = - new java.util.ArrayList<>( - List.of(e.playerlist())); // TODO: Garbage, - // but works - playerList.removeIf(name -> name.equalsIgnoreCase(user)); - if (this.onlinePlayers != playerList) { - this.onlinePlayers.clear(); - this.onlinePlayers.addAll(playerList); - } - } - }); - - TimerTask task = - new TimerTask() { - public void run() { - sendGetPlayerList.postEvent(); - Platform.runLater(() -> reload()); - } - }; - - pollTimer.schedule(task, 0L, 5000L); // TODO: Block app exit, fix later - } - - private void sendChallenge(String oppUsername, String gameType) { - final AtomicInteger challengeId = new AtomicInteger(-1); - - if (onlinePlayers.contains(oppUsername)) { - new EventFlow() - .addPostEvent( - new NetworkEvents.SendChallenge(this.clientId, oppUsername, gameType)) - .listen( - NetworkEvents.ChallengeResponse.class, - e -> { - challengeId.set( - Integer.parseInt( - e.challengeId() - .substring( - 18, e.challengeId().length() - 2))); - }) - .listen( - NetworkEvents.GameMatchResponse.class, - e -> { - if (e.clientId() == this.clientId) { - pollTimer.cancel(); - App.activate(new TicTacToeLayer(information, this.clientId)); - } - }, - false) - .postEvent(); - // ^ - // | - // | - // | - } - } - - private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { - App.push( - new ChallengePopup( - information, - response.challengerName(), - response.gameType(), - clientId, - response.challengeId())); - } - - @Override - public void reload() { - popAll(); - - ListView