diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 028e10f..35b9cdb 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -1,42 +1,42 @@ -name: Checks +#name: Checks -on: - push: - branches: - - 'main' - pull_request: - branches: - - 'main' +#on: +# push: +# branches: +# - 'main' +# pull_request: +# branches: +# - 'main' +# +#jobs: +# formatting-check: +# name: Follow Google Formatting Guidelines +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v5 +# with: +# fetch-depth: 0 # Fix for incremental formatting +# - uses: actions/setup-java@v5 +# with: +# java-version: '25' +# distribution: 'temurin' +# cache: maven +# - name: Run Format Check +# run: mvn spotless:check -jobs: - formatting-check: - name: Follow Google Formatting Guidelines - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 # Fix for incremental formatting - - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - cache: maven - - name: Run Format Check - run: mvn spotless:check - - tests: - name: Unittests - runs-on: ${{ matrix.os }} - needs: formatting-check - strategy: - matrix: - os: [ubuntu-latest] #windows-latest, macos-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - cache: maven - - name: Run Unittests - run: mvn -B test +# tests: +# name: Unittests +# runs-on: ${{ matrix.os }} +# needs: formatting-check +# strategy: +# matrix: +# os: [ubuntu-latest] #windows-latest, macos-latest +# steps: +# - uses: actions/checkout@v5 +# - uses: actions/setup-java@v5 +# with: +# java-version: '25' +# distribution: 'temurin' +# cache: maven +# - name: Run Unittests +# run: mvn -B test 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/dictionaries/project.xml b/.idea/dictionaries/project.xml index 46f4d3b..e6d7540 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -5,14 +5,17 @@ clid dcompile errorprone + espaƱol flushnl gaaf gamelist + pism playerlist tictactoe toop vmoptions xplugin + yourturn \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 895ac80..3b3a142 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,12 +1,16 @@ + + + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index c168b80..655cfae 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,7 +2,8 @@ \ 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/pom.xml b/app/pom.xml index eb93b3b..a4da233 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -1,8 +1,13 @@ 4.0.0 - org.toop - pism_app + + org.toop + pism + 0.1 + + + app 0.1 @@ -24,24 +29,36 @@ gson 2.10.1 - - org.toop - pism_framework - 0.1 - compile - - - org.toop - pism_game - 0.1 - compile - org.openjfx javafx-controls 25 + + + com.google.errorprone + error_prone_core + 2.42.0 + + + com.google.errorprone + error_prone_annotations + 2.42.0 + + + org.toop + framework + 0.1 + compile + + + org.toop + game + 0.1 + compile + + @@ -112,14 +129,56 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - 25 - 25 - - + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + true + true + ${java.home}/bin/javac + 25 + 25 + 25 + UTF-8 + + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + + -Xplugin:ErrorProne \ + + + + + + + -XDcompilePolicy=simple + --should-stop=ifError=FLOW + + + + com.google.errorprone + error_prone_core + 2.42.0 + + + + + + com.google.errorprone + error_prone_core + 2.42.0 + + + \ No newline at end of file diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java index 8456819..3b4fef3 100644 --- a/app/src/main/java/org/toop/Main.java +++ b/app/src/main/java/org/toop/Main.java @@ -1,21 +1,9 @@ package org.toop; import org.toop.app.App; -import org.toop.framework.asset.ResourceLoader; -import org.toop.framework.asset.ResourceManager; -import org.toop.framework.audio.SoundManager; -import org.toop.framework.networking.NetworkingClientManager; -import org.toop.framework.networking.NetworkingInitializationException; public final class Main { - public static void main(String[] args) { - initSystems(); + static void main(String[] args) { App.run(args); } - - private static void initSystems() throws NetworkingInitializationException { - ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")); - new Thread(NetworkingClientManager::new).start(); - new Thread(SoundManager::new).start(); - } } diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index c4c9251..5d99acd 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,167 +1,279 @@ 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.asset.ResourceManager; -import org.toop.framework.asset.resources.CssAsset; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyEvent; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.complex.LoadingWidget; +import org.toop.app.widget.display.SongDisplay; +import org.toop.app.widget.popup.EscapePopup; +import org.toop.app.widget.popup.QuitPopup; +import org.toop.app.widget.view.MainView; +import org.toop.framework.audio.*; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.eventbus.GlobalEventBus; +import org.toop.framework.networking.NetworkingClientEventListener; +import org.toop.framework.networking.NetworkingClientManager; +import org.toop.framework.resource.ResourceLoader; +import org.toop.framework.resource.ResourceManager; +import org.toop.framework.resource.events.AssetLoaderEvents; +import org.toop.framework.resource.resources.CssAsset; +import org.toop.framework.resource.resources.MusicAsset; +import org.toop.framework.resource.resources.SoundEffectAsset; 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.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +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; + public static void run(String[] args) { + launch(args); + } - public static void run(String[] args) { - launch(args); - } + @Override + public void start(Stage stage) { + // Start loading localization + ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/localization")); + ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/style")); - @Override - public void start(Stage stage) throws Exception { - final StackPane root = new StackPane(); - final Scene scene = new Scene(root); + final StackPane root = WidgetContainer.setup(); + final Scene scene = new Scene(root); - stage.setTitle(AppContext.getString("appTitle")); - stage.setWidth(1080); - stage.setHeight(720); + stage.setOpacity(0.0); - stage.setOnCloseRequest( - event -> { - event.consume(); + stage.setTitle(AppContext.getString("app-title")); + stage.titleProperty().bind(AppContext.bindToKey("app-title")); - if (!isQuitting) { - quitPopup(); - } - }); + stage.setWidth(0); + stage.setHeight(0); - stage.setScene(scene); - stage.setResizable(false); + scene.getRoot(); - stage.show(); + stage.setMinWidth(1200); + stage.setMinHeight(800); + stage.setOnCloseRequest(event -> { + event.consume(); + quit(); + }); - App.stage = stage; - App.scene = scene; - App.root = root; + stage.setScene(scene); + stage.setResizable(true); - App.stack = new Stack<>(); + App.stage = stage; + App.scene = scene; - App.width = (int) stage.getWidth(); - App.height = (int) stage.getHeight(); + App.width = (int)stage.getWidth(); + App.height = (int)stage.getHeight(); - App.isQuitting = false; + AppSettings.applySettings(); - final AppSettings settings = new AppSettings(); - settings.applySettings(); + setKeybinds(root); - new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - activate(new MainLayer()); - } + LoadingWidget loading = new LoadingWidget(Primitive.text( + "Loading...", false), 0, 0, Integer.MAX_VALUE, false, false // Just set a high default + ); - public static void activate(Layer layer) { - Platform.runLater( - () -> { - popAll(); - push(layer); - }); - } + WidgetContainer.setCurrentView(loading); - public static void push(Layer layer) { - Platform.runLater( - () -> { - root.getChildren().addLast(layer.getLayer()); - stack.push(layer); - }); - } + setOnLoadingSuccess(loading); - public static void pop() { - Platform.runLater( - () -> { - root.getChildren().removeLast(); - stack.pop(); + EventFlow loadingFlow = new EventFlow(); - isQuitting = false; - }); - } + final boolean[] hasRun = {false}; + loadingFlow + .listen(AssetLoaderEvents.LoadingProgressUpdate.class, e -> { + if (!hasRun[0]) { + hasRun[0] = true; + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + Platform.runLater(() -> stage.setOpacity(1.0)); + } - public static void popAll() { - Platform.runLater( - () -> { - final int childrenCount = root.getChildren().size(); - - for (int i = 0; i < childrenCount; i++) { - try { - root.getChildren().removeLast(); - } catch (Exception e) { - IO.println(e); + Platform.runLater(() -> { + loading.setMaxAmount(e.isLoadingAmount()); + try { + loading.setAmount(e.hasLoadedAmount()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + if (e.hasLoadedAmount() >= e.isLoadingAmount()-1) { + Platform.runLater(loading::triggerSuccess); + loadingFlow.unsubscribe("init_loading"); } - } + }); - stack.removeAllElements(); - }); + + }, false, "init_loading"); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + executor.submit( + () -> ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets") + )); + } finally { + executor.shutdown(); + } + + stage.show(); + + } + + private void setKeybinds(StackPane root) { + root.addEventHandler(KeyEvent.KEY_PRESSED,event -> { + if (event.getCode() == KeyCode.ESCAPE) { + escapePopup(); + } + }); + stage.setFullScreenExitKeyCombination( + new KeyCodeCombination( + KeyCode.F11 + ) + ); + } + + public void escapePopup() { + + if ( WidgetContainer.getCurrentView() == null + || WidgetContainer.getCurrentView() instanceof MainView) { + return; + } + + if (!Objects.requireNonNull( + WidgetContainer.find(widget -> widget instanceof QuitPopup || widget instanceof EscapePopup) + ).isEmpty()) { + WidgetContainer.removeFirst(QuitPopup.class); + WidgetContainer.removeFirst(EscapePopup.class); + return; + } + + EscapePopup escPopup = new EscapePopup(); + escPopup.show(Pos.CENTER); + } + + private void setOnLoadingSuccess(LoadingWidget loading) { + loading.setOnSuccess(() -> { + + try { + initSystems(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + AppSettings.applyMusicVolumeSettings(); + new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent(); + loading.hide(); + WidgetContainer.add(Pos.CENTER, new MainView()); + WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay()); + stage.setOnCloseRequest(event -> { + event.consume(); + + if (WidgetContainer.getAllWidgets().stream().anyMatch(e -> e instanceof QuitPopup)) return; + + QuitPopup a = new QuitPopup(); + a.show(Pos.CENTER); + + }); + }); + } + + private void initSystems() throws InterruptedException { // TODO Move to better place + + final int THREAD_COUNT = 2; + CountDownLatch latch = new CountDownLatch(THREAD_COUNT); + + @SuppressWarnings("resource") + ExecutorService threads = Executors.newFixedThreadPool(THREAD_COUNT); + + try { + + threads.submit(() -> { + new NetworkingClientEventListener( + GlobalEventBus.get(), + new NetworkingClientManager(GlobalEventBus.get())); + + latch.countDown(); + }); + + threads.submit(() -> { + MusicManager musicManager = + new MusicManager<>( + GlobalEventBus.get(), + ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), + true + ); + + SoundEffectManager soundEffectManager = + new SoundEffectManager<>(ResourceManager.getAllOfType(SoundEffectAsset.class)); + + AudioVolumeManager audioVolumeManager = new AudioVolumeManager() + .registerManager(VolumeControl.MASTERVOLUME, musicManager) + .registerManager(VolumeControl.MASTERVOLUME, soundEffectManager) + .registerManager(VolumeControl.FX, soundEffectManager) + .registerManager(VolumeControl.MUSIC, musicManager); + + new AudioEventListener<>( + GlobalEventBus.get(), + musicManager, + soundEffectManager, + audioVolumeManager + ).initListeners("medium-button-click.wav"); + + latch.countDown(); + }); + + } finally { + latch.await(); + threads.shutdown(); + } } - public static void quitPopup() { - Platform.runLater( - () -> { - push(new QuitPopup()); - isQuitting = true; - }); - } + public static void quit() { + stage.close(); + System.exit(0); // TODO: This is like dropping a nuke + } - public static void quit() { - stage.close(); - } + public static void setFullscreen(boolean fullscreen) { + stage.setFullScreen(fullscreen); - public static void reloadAll() { - stage.setTitle(AppContext.getString("appTitle")); + width = (int)stage.getWidth(); + height = (int)stage.getHeight(); + } - for (final Layer layer : stack) { - layer.reload(); - } - } + public static void setStyle(String theme, String layoutSize) { + scene.getStylesheets().clear(); - public static void setFullscreen(boolean fullscreen) { - stage.setFullScreen(fullscreen); + scene.getStylesheets().add(ResourceManager.get("general.css").getUrl()); + scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); + scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); + } - width = (int) stage.getWidth(); - height = (int) stage.getHeight(); + public static int getWidth() { + return width; + } - 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..08e3f46 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -1,10 +1,54 @@ 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(2, 5), + REVERSI(2, 10); + + private final int playerCount; + private final int maxDepth; + + Type(int playerCount, int maxDepth) { + this.playerCount = playerCount; + this.maxDepth = maxDepth; + } + + public int getPlayerCount() { + return playerCount; + } + + public int getMaxDepth() { + return maxDepth; + } + + public String getTypeToString() { + String name = this.name(); + return switch (name) { + case "TICTACTOE" -> "TicTacToe"; + case "REVERSI" -> "Reversi"; + case "CONNECT4" -> "Connect4"; + case "BATTLESHIP" -> "Battleship"; + default -> name; + }; + } + } + + 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.getPlayerCount()]; + + 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..ef691b4 --- /dev/null +++ b/app/src/main/java/org/toop/app/Server.java @@ -0,0 +1,354 @@ +package org.toop.app; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import org.toop.app.gameControllers.*; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.complex.LoadingWidget; +import org.toop.app.widget.popup.ChallengePopup; +import org.toop.app.widget.popup.ErrorPopup; +import org.toop.app.widget.popup.SendChallengePopup; +import org.toop.app.widget.view.ServerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.controller.GameController; +import org.toop.framework.eventbus.GlobalEventBus; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.framework.networking.clients.TournamentNetworkingClient; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.framework.networking.types.NetworkingConnector; +import org.toop.game.games.reversi.BitboardReversi; +import org.toop.game.games.tictactoe.BitboardTicTacToe; +import org.toop.game.players.ArtificialPlayer; +import org.toop.game.players.OnlinePlayer; +import org.toop.game.players.RandomAI; +import org.toop.local.AppContext; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class Server { + // TODO: Keep track of listeners. Remove them on Server connection close so reference is deleted. + private String user = ""; + private long clientId = -1; + + private final List onlinePlayers = new CopyOnWriteArrayList<>(); + private final List gameList = new CopyOnWriteArrayList<>(); + + private ServerView primary; + private boolean isPolling = true; + + private GameController gameController; + + private final AtomicBoolean isSingleGame = new AtomicBoolean(false); + + private ScheduledExecutorService scheduler; + + private EventFlow connectFlow; + + 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; + } + + + // Server has to deal with ALL network related listen events. This "server" can then interact with the manager to make stuff happen. + // This prevents data races where events get sent to the game manager but the manager isn't ready yet. + public Server(String ip, String port, String user) { + if (ip.split("\\.").length < 4) { + new ErrorPopup("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address")); + return; + } + + int parsedPort; + + try { + parsedPort = Integer.parseInt(port); + } catch (NumberFormatException _) { + new ErrorPopup("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port")); + return; + } + + if (user.isEmpty() || user.matches("^[0-9].*")) { + new ErrorPopup(AppContext.getString("invalid-username")); + return; + } + + final int reconnectAttempts = 10; + + LoadingWidget loading = new LoadingWidget( + Primitive.text("connecting"), 0, 0, reconnectAttempts, true, true + ); + + WidgetContainer.getCurrentView().transitionNextCustom(loading, "disconnect", this::disconnect); + + var a = new EventFlow() + .addPostEvent(NetworkEvents.StartClient.class, + new TournamentNetworkingClient(GlobalEventBus.get()), + new NetworkingConnector(ip, parsedPort, reconnectAttempts, 1, TimeUnit.SECONDS) + ); + + loading.setOnFailure(() -> { + if (WidgetContainer.getCurrentView() == loading) WidgetContainer.getCurrentView().transitionPrevious(); + a.unsubscribeAll(); + WidgetContainer.add( + Pos.CENTER, + new ErrorPopup(AppContext.getString("connecting-failed") + " " + ip + ":" + port) + ); + }); + + a.onResponse(NetworkEvents.CreatedIdForClient.class, e -> clientId = e.clientId(), true); + + a.onResponse(NetworkEvents.StartClientResponse.class, e -> { + if (!e.successful()) { + return; + } + + primary = new ServerView(user, this::sendChallenge); + WidgetContainer.getCurrentView().transitionNextCustom(primary, "disconnect", this::disconnect); + + a.unsubscribe("connecting"); + a.unsubscribe("startclient"); + + this.user = user; + + new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent(); + + startPopulateScheduler(); + populateGameList(); + + primary.removeViewFromPreviousChain(loading); + + }, false, "startclient") + .listen( + NetworkEvents.ConnectTry.class, + e -> { + if (clientId != e.clientId()) return; + Platform.runLater( + () -> { + try { + loading.setAmount(e.amount()); + if (e.amount() >= loading.getMaxAmount()) { + loading.triggerFailure(); + } + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + ); + }, + false, "connecting" + ) + .postEvent(); + + a.listen(NetworkEvents.ChallengeResponse.class, this::handleReceivedChallenge, false, "challenge") + .listen(NetworkEvents.GameMatchResponse.class, this::handleMatchResponse, false, "match-response") + .listen(NetworkEvents.GameResultResponse.class, this::handleGameResult, false, "game-result") + .listen(NetworkEvents.GameMoveResponse.class, this::handleReceivedMove, false, "game-move") + .listen(NetworkEvents.YourTurnResponse.class, this::handleYourTurn, false, "your-turn"); + + connectFlow = a; + } + + private void sendChallenge(String opponent) { + if (!isPolling) return; + + var a = new SendChallengePopup(this, opponent, (playerInformation, gameType) -> { + new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent(); + isSingleGame.set(true); + }); + + a.show(Pos.CENTER); + } + + private void handleMatchResponse(NetworkEvents.GameMatchResponse response) { + // TODO: Redo all of this mess + if (gameController != null) { + gameController.stop(); + } + + gameController = null; + + //if (!isPolling) return; + + String gameType = extractQuotedValue(response.gameType()); + if (response.clientId() == clientId) { + isPolling = false; + onlinePlayers.clear(); + + final GameInformation.Type type = gameToType(gameType); + if (type == null) { + new ErrorPopup("Unsupported game type: " + gameType); + return; + } + + final int myTurn = response.playerToMove().equalsIgnoreCase(response.opponent()) ? 1 : 0; + + final GameInformation information = new GameInformation(type); + //information.players[0] = playerInformation; + information.players[0].name = user; + information.players[0].isHuman = false; + information.players[0].computerDifficulty = 5; + information.players[0].computerThinkTime = 1; + information.players[1].name = response.opponent(); + + /*switch (type){ + case TICTACTOE ->{ + players[myTurn] = new ArtificialPlayer<>(new TicTacToeAIR(9), user); + } + case REVERSI ->{ + players[myTurn] = new ArtificialPlayer<>(new ReversiAIR(), user); + } + }*/ + + + + switch (type) { + case TICTACTOE ->{ + Player[] players = new Player[2]; + players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent()); + players[myTurn] = new ArtificialPlayer<>(new RandomAI(), user); + gameController = new TicTacToeBitController(players); + } + case REVERSI -> { + Player[] players = new Player[2]; + players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent()); + players[myTurn] = new ArtificialPlayer<>(new RandomAI(), user); + gameController = new ReversiBitController(players);} + default -> new ErrorPopup("Unsupported game type."); + + } + + if (gameController != null){ + gameController.start(); + } + } + } + + private void handleYourTurn(NetworkEvents.YourTurnResponse response) { + if (gameController == null) { + return; + } + gameController.onYourTurn(response); + + } + + private void handleGameResult(NetworkEvents.GameResultResponse response) { + if (gameController == null) { + return; + } + gameController.gameFinished(response); + } + + private void handleReceivedMove(NetworkEvents.GameMoveResponse response) { + if (gameController == null) { + return; + } + gameController.onMoveReceived(response); + } + + private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { + if (!isPolling) return; + + String challengerName = extractQuotedValue(response.challengerName()); + String gameType = extractQuotedValue(response.gameType()); + final String finalGameType = gameType; + var a = new ChallengePopup(challengerName, gameType, (playerInformation) -> { + final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); + new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); + isSingleGame.set(true); + }); + + a.show(Pos.CENTER); + } + + 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(); + isPolling = false; + stopScheduler(); + connectFlow.unsubscribeAll(); + + WidgetContainer.getCurrentView().transitionPrevious(); + } + + private void forfeitGame() { + new EventFlow().addPostEvent(new NetworkEvents.SendForfeit(clientId)).postEvent(); + } + + private void exitGame() { + forfeitGame(); + startPopulateScheduler(); + } + + private void gameOver(){ + startPopulateScheduler(); + } + + private void startPopulateScheduler() { + isPolling = true; + isSingleGame.set(false); + stopScheduler(); + + new EventFlow() + .listen(NetworkEvents.PlayerlistResponse.class, e -> { + if (e.clientId() == clientId) { + onlinePlayers.clear(); + onlinePlayers.addAll(List.of(e.playerlist())); + onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + primary.update(onlinePlayers); + } + }, false); + + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(() -> { + if (isPolling) { + new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)).postEvent(); + } else { + stopScheduler(); + } + }, 0, 1, TimeUnit.SECONDS); + } + + private void stopScheduler() { + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdownNow(); + } + } + + private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { + gameList.clear(); + gameList.addAll(List.of(event.gamelist())); + } + + public void populateGameList() { + new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) + .listen(NetworkEvents.GamelistResponse.class, this::gamesListFromServerHandler, true) + .postEvent(); + } + + public List getGameList() { + return gameList; + } + + private String extractQuotedValue(String s) { + int first = s.indexOf('"'); + int last = s.lastIndexOf('"'); + if (first >= 0 && last > first) { + return s.substring(first + 1, last); + } + return s; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/BitGameCanvas.java b/app/src/main/java/org/toop/app/canvas/BitGameCanvas.java new file mode 100644 index 0000000..85c6f7d --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/BitGameCanvas.java @@ -0,0 +1,247 @@ +package org.toop.app.canvas; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.input.MouseButton; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.util.Duration; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.view.GUIEvents; + +import java.util.function.Consumer; + +public abstract class BitGameCanvas> implements GameCanvas { + 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 Color color; + protected final Color backgroundColor; + + protected final int width; + protected final int height; + + protected final int rowSize; + protected final int columnSize; + + protected final int gapSize; + protected final boolean edges; + + protected final Cell[] cells; + + private Consumer onCellCLicked; + + public void setOnCellClicked(Consumer onClick) { + this.onCellCLicked = onClick; + } + + protected BitGameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges) { + canvas = new Canvas(width, height); + graphics = canvas.getGraphicsContext2D(); + + this.onCellCLicked = (c) -> new EventFlow().addPostEvent(GUIEvents.PlayerAttemptedMove.class, c).postEvent(); + + this.color = color; + this.backgroundColor = backgroundColor; + + this.width = width; + this.height = height; + + this.rowSize = rowSize; + this.columnSize = columnSize; + + this.gapSize = gapSize; + this.edges = edges; + + cells = new Cell[rowSize * columnSize]; + + final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize; + final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize; + + for (int y = 0; y < columnSize; y++) { + final float startY = y * cellHeight + y * gapSize + gapSize; + + 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; + } + + 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 (cell.isInside(event.getX(), event.getY())) { + event.consume(); + this.onCellCLicked.accept(1L << (column + row * rowSize)); + } + }); + + + + + render(); + } + + public void loopOverBoard(long bb, Consumer onCell){ + while (bb != 0) { + int idx = Long.numberOfTrailingZeros(bb); // index of least-significant 1-bit + onCell.accept(idx); + + bb &= bb - 1; // clear LSB 1-bit + } + } + + private void render() { + graphics.setFill(backgroundColor); + graphics.fillRect(0, 0, width, height); + + graphics.setFill(color); + + 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); + } + + 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); + } + + if (edges) { + graphics.fillRect(0, 0, width, gapSize); + graphics.fillRect(0, 0, gapSize, height); + + graphics.fillRect(width - gapSize, 0, gapSize, height); + graphics.fillRect(0, height - gapSize, width, gapSize); + } + } + + public void fill(Color color, int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); + + final float width = cells[cell].width(); + final float height = cells[cell].height(); + + graphics.setFill(color); + graphics.fillRect(x, y, width, height); + } + + public void clear(int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); + + final float width = cells[cell].width(); + final float height = cells[cell].height(); + + graphics.clearRect(x, y, width, height); + + graphics.setFill(backgroundColor); + graphics.fillRect(x, y, width, height); + } + + public void clearAll() { + for (int i = 0; i < cells.length; i++) { + clear(i); + } + } + + public void drawPlayerMove(int player, int move) { + final float x = cells[move].x() + gapSize; + final float y = cells[move].y() + gapSize; + + final float width = cells[move].width() - gapSize * 2; + final float height = cells[move].height() - gapSize * 2; + + graphics.setFill(color); + graphics.setFont(Font.font("Arial", 40)); // TODO different font and size + graphics.fillText(String.valueOf(player), x + width, y + height); + } + + 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 drawInnerDot(Color color, int cell, boolean slightlyBigger) { + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + gapSize; + + float multiplier = slightlyBigger?1.4f:1.5f; + + final float width = (cells[cell].width() - gapSize * 2)/multiplier; + final float height = (cells[cell].height() - gapSize * 2)/multiplier; + + float offset = slightlyBigger?5f:4f; + + graphics.setFill(color); + graphics.fillOval(x + width/offset, y + height/offset, width, height); + } + + private void drawDotScaled(Color color, int cell, double scale) { + final float cx = cells[cell].x() + gapSize; + final float cy = cells[cell].y() + gapSize; + + final float fullWidth = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; + + final float scaledWidth = (float)(fullWidth * scale); + final float offsetX = (fullWidth - scaledWidth) / 2; + + graphics.setFill(color); + graphics.fillOval(cx + offsetX, cy, scaledWidth, height); + } + + public Timeline flipDot(Color fromColor, Color toColor, int cell) { + final int steps = 60; + final long duration = 250; + final double interval = duration / (double) steps; + + final Timeline timeline = new Timeline(); + + for (int i = 0; i <= steps; i++) { + final double t = i / (double) steps; + final KeyFrame keyFrame = new KeyFrame(Duration.millis(i * interval), + _ -> { + clear(cell); + + final double scale = t <= 0.5 ? 1 - 2 * t : 2 * t - 1; + final Color currentColor = t < 0.5 ? fromColor : toColor; + + drawDotScaled(currentColor, cell, scale); + } + ); + + timeline.getKeyFrames().add(keyFrame); + } + + return timeline; + } + + public Canvas getCanvas() { + return canvas; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java b/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java new file mode 100644 index 0000000..abf9ac1 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/DrawPlayerHover.java @@ -0,0 +1,7 @@ +package org.toop.app.canvas; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; + +public interface DrawPlayerHover { + void drawPlayerHover(int player, int move, TurnBasedGame game); +} diff --git a/app/src/main/java/org/toop/app/canvas/DrawPlayerMove.java b/app/src/main/java/org/toop/app/canvas/DrawPlayerMove.java new file mode 100644 index 0000000..fca2b46 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/DrawPlayerMove.java @@ -0,0 +1,5 @@ +package org.toop.app.canvas; + +public interface DrawPlayerMove { + void drawPlayerMove(int player, int move); +} 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..d1361c5 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,8 @@ 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 org.toop.framework.gameFramework.model.game.TurnBasedGame; -public abstract class GameCanvas { - protected record Cell(float x, float y, float width, float height) {} - - protected final Canvas canvas; - protected final GraphicsContext graphics; - - protected final Color color; - - protected int width; - protected int height; - - protected final int rows; - protected final int columns; - - protected final int gapSize; - protected final boolean edges; - - 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(); - - this.color = color; - - this.width = width; - this.height = height; - - this.rows = rows; - this.columns = columns; - - this.gapSize = gapSize; - this.edges = edges; - - cells = new Cell[rows * columns]; - - final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows; - final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns; - - for (int y = 0; y < columns; y++) { - final float startY = y * cellHeight + y * 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); - } - } - - 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); - - event.consume(); - onCellClicked.accept(row * rows + column); - }); - - render(); - } - - public void clear() { - graphics.clearRect(0, 0, width, height); - } - - public void render() { - graphics.setFill(color); - - for (int x = 1; x < rows; x++) { - graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height); - } - - for (int y = 1; y < columns; y++) { - graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize); - } - - if (edges) { - graphics.fillRect(-gapSize, 0, gapSize, height); - graphics.fillRect(0, -gapSize, width, gapSize); - - graphics.fillRect(width - gapSize, 0, gapSize, height); - graphics.fillRect(0, height - gapSize, width, gapSize); - } - } - - public void draw(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.fillRect(x, y, width, height); - } - - public void resize(int width, int height) { - canvas.setWidth(width); - canvas.setHeight(height); - - this.width = width; - this.height = height; - - clear(); - render(); - } - - public Canvas getCanvas() { - return canvas; - } +public interface GameCanvas> extends GameDrawer{ + Canvas getCanvas(); } diff --git a/app/src/main/java/org/toop/app/canvas/GameDrawer.java b/app/src/main/java/org/toop/app/canvas/GameDrawer.java new file mode 100644 index 0000000..261334c --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/GameDrawer.java @@ -0,0 +1,7 @@ +package org.toop.app.canvas; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; + +public interface GameDrawer> { + void redraw(T gameCopy); +} diff --git a/app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java new file mode 100644 index 0000000..7c2bde0 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java @@ -0,0 +1,42 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.game.games.reversi.BitboardReversi; + +import java.util.Arrays; +import java.util.function.Consumer; + +public class ReversiBitCanvas extends BitGameCanvas { + public ReversiBitCanvas() { + super(Color.GRAY, new Color(0f, 0.4f, 0.2f, 1f), (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, 8, 8, 5, true); + canvas.setOnMouseMoved(event -> { + double mouseX = event.getX(); + double mouseY = event.getY(); + int cellId = -1; + + BitGameCanvas.Cell hovered = null; + for (BitGameCanvas.Cell cell : cells) { + if (cell.isInside(mouseX, mouseY)) { + hovered = cell; + cellId = turnCoordsIntoCellId(mouseX, mouseY); + break; + } + } + }); + } + + 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; + } + + @Override + public void redraw(BitboardReversi gameCopy) { + clearAll(); + long[] board = gameCopy.getBoard(); + loopOverBoard(board[0], (i) -> drawDot(Color.WHITE, i)); + loopOverBoard(board[1], (i) -> drawDot(Color.BLACK, i)); + } +} diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java similarity index 55% rename from app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java rename to app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java index 1838335..443adbd 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java @@ -1,13 +1,39 @@ package org.toop.app.canvas; -import java.util.function.Consumer; import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.game.games.tictactoe.BitboardTicTacToe; -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.Arrays; +import java.util.function.Consumer; + +public class TicTacToeBitCanvas extends BitGameCanvas{ + public TicTacToeBitCanvas() { + super( + Color.GRAY, + Color.TRANSPARENT, + (App.getHeight() / 4) * 3, + (App.getHeight() / 4) * 3, + 3, + 3, + 30, + false + ); } + @Override + public void redraw(BitboardTicTacToe gameCopy) { + clearAll(); + drawMoves(gameCopy.getBoard()); + } + + private void drawMoves(long[] gameBoard){ + loopOverBoard(gameBoard[0], (i) -> drawX(Color.RED, i)); + loopOverBoard(gameBoard[1], (i) -> drawO(Color.BLUE, i)); + + } + + public void drawX(Color color, int cell) { graphics.setStroke(color); graphics.setLineWidth(gapSize); diff --git a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java new file mode 100644 index 0000000..2c3ad49 --- /dev/null +++ b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java @@ -0,0 +1,140 @@ +package org.toop.app.gameControllers; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.toop.app.canvas.GameCanvas; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.eventbus.GlobalEventBus; +import org.toop.framework.gameFramework.controller.GameController; +import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.framework.gameFramework.view.GUIEvents; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.players.LocalPlayer; + +public class GenericGameController> implements GameController { + protected final EventFlow eventFlow = new EventFlow(); + + // Logger for logging + protected final Logger logger = LogManager.getLogger(this.getClass()); + + // Reference to gameView view + protected final GameView gameView; + + // Reference to game canvas + protected final GameCanvas canvas; + + protected final TurnBasedGame game; // Reference to game instance + private final ThreadBehaviour gameThreadBehaviour; + + // TODO: Change gameType to automatically happen with either dependency injection or something else. + public GenericGameController(GameCanvas canvas, T game, ThreadBehaviour gameThreadBehaviour, String gameType) { + logger.info("Creating: " + this.getClass()); + + this.canvas = canvas; + this.game = game; + this.gameThreadBehaviour = gameThreadBehaviour; + + // Tell thread how to send moves + this.gameThreadBehaviour.setOnSendMove((id, m) -> GlobalEventBus.get().post(new NetworkEvents.SendMove(id, (short)translateMove(m)))); + + // Tell thread how to update UI + this.gameThreadBehaviour.setOnUpdateUI(() -> Platform.runLater(this::updateUI)); + + // Change scene to game view + gameView = new GameView(null, null, null, gameType); + gameView.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(gameView, true); + + // Listen to updates + eventFlow + .listen(GUIEvents.GameEnded.class, this::onGameFinish, false) + .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}, false); + } + + public void start(){ + logger.info("Starting GameManager"); + updateUI(); + gameThreadBehaviour.start(); + } + + public void stop(){ + logger.info("Stopping GameManager"); + removeListeners(); + gameThreadBehaviour.stop(); + } + + public Player getCurrentPlayer(){ + return game.getPlayer(getCurrentPlayerIndex()); + } + + public int getCurrentPlayerIndex(){ + return game.getCurrentTurn(); + } + + protected long translateMove(int move){ + return 1L << move; + } + + protected int translateMove(long move){ + return Long.numberOfTrailingZeros(move); + } + + private void removeListeners(){ + eventFlow.unsubscribeAll(); + } + + private void onGameFinish(GUIEvents.GameEnded event){ + logger.info("Game Finished"); + String name = event.winner() == -1 ? null : getPlayer(event.winner()).getName(); + gameView.gameOver(event.winOrTie(), name); + stop(); + } + + public Player getPlayer(int player){ + if (player < 0 || player >= 2){ // TODO: Make game turn player count + logger.error("Invalid player index"); + throw new IllegalArgumentException("player out of range"); + } + return game.getPlayer(player); + } + + private boolean isOnline(){ + return this.gameThreadBehaviour instanceof SupportsOnlinePlay; + } + + public void onYourTurn(NetworkEvents.YourTurnResponse event){ + if (isOnline()){ + ((SupportsOnlinePlay) this.gameThreadBehaviour).onYourTurn(event.clientId()); + } + } + + public void onMoveReceived(NetworkEvents.GameMoveResponse event){ + if (isOnline()){ + ((SupportsOnlinePlay) this.gameThreadBehaviour).onMoveReceived( + translateMove(Integer.parseInt(event.move()))); + } + } + + public void gameFinished(NetworkEvents.GameResultResponse event){ + if (isOnline()){ + ((SupportsOnlinePlay) this.gameThreadBehaviour).gameFinished(event.condition()); + } + } + + @Override + public void sendMove(long clientId, long move) { + new EventFlow().addPostEvent(NetworkEvents.SendMove.class, clientId, (short) Long.numberOfTrailingZeros(move)).asyncPostEvent(); + } + + @Override + public void updateUI() { + canvas.redraw(game.deepCopy()); + } +} diff --git a/app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java b/app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java new file mode 100644 index 0000000..40784b0 --- /dev/null +++ b/app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java @@ -0,0 +1,22 @@ +package org.toop.app.gameControllers; + +import org.toop.app.canvas.ReversiBitCanvas; +import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.gameThreads.LocalThreadBehaviour; +import org.toop.game.gameThreads.OnlineThreadBehaviour; +import org.toop.game.games.reversi.BitboardReversi; +import org.toop.game.players.OnlinePlayer; + +public class ReversiBitController extends GenericGameController { + public ReversiBitController(Player[] players) { + BitboardReversi game = new BitboardReversi(players); + ThreadBehaviour thread = new LocalThreadBehaviour<>(game); + for (Player player : players) { + if (player instanceof OnlinePlayer){ + thread = new OnlineThreadBehaviour<>(game); + } + } + super(new ReversiBitCanvas(), game, thread, "Reversi"); + } +} diff --git a/app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java b/app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java new file mode 100644 index 0000000..6307894 --- /dev/null +++ b/app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java @@ -0,0 +1,23 @@ +package org.toop.app.gameControllers; + +import org.toop.app.canvas.TicTacToeBitCanvas; +import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.gameThreads.LocalFixedRateThreadBehaviour; +import org.toop.game.gameThreads.LocalThreadBehaviour; +import org.toop.game.gameThreads.OnlineThreadBehaviour; +import org.toop.game.games.tictactoe.BitboardTicTacToe; +import org.toop.game.players.OnlinePlayer; + +public class TicTacToeBitController extends GenericGameController { + public TicTacToeBitController(Player[] players) { + BitboardTicTacToe game = new BitboardTicTacToe(players); + ThreadBehaviour thread = new LocalThreadBehaviour<>(game); + for (Player player : players) { + if (player instanceof OnlinePlayer){ + thread = new OnlineThreadBehaviour<>(game); + } + } + super(new TicTacToeBitCanvas(), game, thread , "TicTacToe"); + } +} 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 deleted file mode 100644 index b255c3d..0000000 --- a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java +++ /dev/null @@ -1,235 +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.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, - 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