69 Commits

Author SHA1 Message Date
056d9180b9 Merge branch 'UI-Updates' into 276-refactor-the-game-to-use-bitboards-instead 2025-12-06 15:05:05 +01:00
b33c85419c Update BitGameCanvas.java 2025-12-05 22:08:45 +01:00
449387c8ed Removed System-Outs to clean up console 2025-12-05 14:18:47 +01:00
237f708a54 Added a ChatGPT generated MiniMaxAI based on the old MiniMaxAI but with alpha-beta pruning and heuristics for Reversi 2025-12-05 14:09:54 +01:00
lieght
ea179eb0e2 Added find functionality 2025-12-05 12:28:06 +01:00
64016bd3f0 Added a method getWinner() to game interface.Controller now tells gameThreads how to deal with drawing UI and sending a move to server. 2025-12-05 12:16:03 +01:00
38de4f2156 Drawing of board on canvas is now done from bitboards rather than translating. 2025-12-05 01:20:01 +01:00
b3a97a8978 Removed old AI and old files. Added a new generic random AI. game no longer deals with translation. 2025-12-05 01:05:11 +01:00
lieght
61f03fab26 Working escape menu 2025-12-04 22:42:39 +01:00
lieght
2fbda6f14d Escape remove popup 2025-12-04 22:14:58 +01:00
3c283092a3 Made all network events async again 2025-12-04 21:59:31 +01:00
ramollia
0d564283fb fixed redundant container 2025-12-04 21:52:22 +01:00
lieght
f24ca88246 Escape popup 2025-12-04 21:47:51 +01:00
7d90d475f1 Merge branch 'Development' into 276-refactor-the-game-to-use-bitboards-instead 2025-12-04 21:27:35 +01:00
05dcea03a1 Added comment 2025-12-04 21:27:02 +01:00
ramollia
c8e2c3747e fixed getAllWidgets 2025-12-04 21:26:03 +01:00
76f836b1c1 made more classes deepClonable. 2025-12-04 21:23:26 +01:00
ramollia
8849515af6 made the game text a header instead 2025-12-04 21:09:36 +01:00
ramollia
1b2733c0be localize the ChallengePopup text 2025-12-04 21:07:49 +01:00
6607918635 Removed souts for debugging 2025-12-04 20:55:05 +01:00
lieght
f205669b41 Popups now remove themselves 2025-12-04 20:54:28 +01:00
86e740a34a Fixed local and online play for both games 2025-12-04 20:50:58 +01:00
lieght
4608135ee5 Challenge popups "Fixed" 2025-12-04 20:46:59 +01:00
ramollia
be57e25a48 fixed getLegalMoves & getFlips 2025-12-04 20:12:15 +01:00
lieght
27a35c8117 Removed todo 2025-12-04 20:10:24 +01:00
0993abeec7 Everything is broken 2025-12-04 19:43:03 +01:00
ramollia
7ea45586ae fixed? getLegalMoves 2025-12-04 19:36:38 +01:00
lieght
b1224ba6af Fixt wrong view order 2025-12-04 19:26:01 +01:00
ramollia
a7b9484aa4 fixed incorrect index counting 2025-12-04 18:28:25 +01:00
ramollia
75c4e55da6 added removeIndexFromPreviousChain 2025-12-04 18:10:25 +01:00
ramollia
d5223c6cd1 added replacePrevious in ViewWidget 2025-12-04 17:49:23 +01:00
lieght
730bd5c3dc Merge remote-tracking branch 'origin/UI-Updates' into UI-Updates 2025-12-04 17:16:16 +01:00
lieght
296641b82e Correct back view 2025-12-04 17:16:00 +01:00
ramollia
8dccabe37b added getAllWidgets to WidgetContainer 2025-12-04 17:15:59 +01:00
ramollia
dd73d1810a Merge remote-tracking branch 'origin/UI-Updates' into UI-Updates 2025-12-04 17:02:33 +01:00
ramollia
e2be973c9a changed the transitionNextCustom to be easier to use 2025-12-04 17:02:12 +01:00
lieght
75af655b49 Changed to debug instead of info 2025-12-04 16:51:32 +01:00
lieght
c6d95479af Merge remote-tracking branch 'origin/Development' into UI-Updates 2025-12-04 16:49:11 +01:00
ramollia
72d5989d24 fixed overlapping back and disconnect buttons 2025-12-04 16:06:43 +01:00
michiel
4ea458c92e depth + thinktime back to AIs, along with a a specific TicTacToeAIRSleep 2025-12-04 15:11:41 +01:00
788cf08b49 Merge remote-tracking branch 'origin/276-refactor-the-game-to-use-bitboards-instead' into 276-refactor-the-game-to-use-bitboards-instead 2025-12-04 15:00:41 +01:00
ramollia
d74e7b4517 fixed getLegalMoves 2025-12-04 14:48:35 +01:00
michiel
134c9a2fd8 better human/ai selector with bot selection and depth on TicTacToeAIR 2025-12-04 14:31:04 +01:00
21b489bbe7 Almost done with implementing bitboards. Reversi is broken and artifical players don't work yet. 2025-12-04 14:19:48 +01:00
f4ee992166 Bitboard implemented with scuffed TicTacToe translation done by game. This should be done by the view. 2025-12-04 00:49:49 +01:00
lieght
f60df73b66 Loading circle, better loading colors. 2025-12-03 23:55:12 +01:00
michiel301b
a00d25f24a smalle fixes aan turn updates 2025-12-03 23:16:03 +01:00
michiel301b
406ad713f4 turn updates 2025-12-03 22:47:17 +01:00
34d83ff7a5 (DOES NOT COMPILE) In-between commit 2025-12-03 22:44:43 +01:00
8ca2399e6a Merge branch 'Development' of https://github.com/2OOP/pism into Development 2025-12-03 21:51:14 +01:00
8c75ac1471 Making threads verbose regarding exceptions 2025-12-03 21:51:01 +01:00
lieght
f866eab8ba Best fix for white screen at start 2025-12-03 21:50:32 +01:00
b3d65e73a0 Merge branch 'Development' into 276-refactor-the-game-to-use-bitboards-instead 2025-12-03 20:39:27 +01:00
Stef
8d77f51355 272 remake game framework interfaces to properly represent vmc (#278)
* Cleaned up a lot of old files and renamed/remade interfaces to better suit the framework

* Broken commit

* Fixed online play

* Better file structure and closer to MVC
2025-12-03 20:35:37 +01:00
lieght
040287ad70 Added infinite boolean, fixed loading behaviour at startup 2025-12-03 18:56:08 +01:00
lieght
740d2cf3db Fixed systems starting, before assets being loaded (I am retarded) 2025-12-03 18:13:57 +01:00
Bas Antonius de Jong
628e4f30b5 Main menu loader (#277)
* LoadingWidget main menu

* fixed garbage code

* Fixed garbage code 2

* LoadWidget fix, added loading to starting the game. Removed unnecessary console output

---------

Co-authored-by: ramollia <>
2025-12-03 14:49:59 +01:00
ramollia
c25be04ff1 added new classes for the games that use bitboards instead. also combined game with turnbasedgame 2025-12-03 12:51:09 +01:00
Bas de Jong
bc84171029 Double loading call fix, LoadingWidget docs 2025-12-03 11:39:25 +01:00
Bas Antonius de Jong
3c9b010dd3 231 connecting to server feedback (#275)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler

* getAllListeners

* Removed nulls

* Fixed stress tests

* Added docs, no more list creation when adding events to the bus.

* Fixed unsubscribe not working.

* Moved away from deprecated functions

* moved from wildcard to typed

* Moved away from deprecated function

* Added debugging to GlobalEventBus

* Fixed cleaning flow

* Fixed unsubscribe all

* Fixed unsubscribe all

* Removed unused import

* Added LoadingWidget.java for server feedback

* Imports

* fixed loadingwidget

* Workable LoadingWidget and trying to connect to server

* Removed output

* Small bug temp fix

---------

Co-authored-by: ramollia <>
2025-12-02 20:59:46 +01:00
michiel
4dbc4997a0 added back button sounds because SOMEONE fucked it up..... 2025-12-02 11:51:00 +01:00
Stef
9f55f8e1c7 Merge new framework into development (#269)
* Created a somewhat generic TurnBasedGame thread. Temporary UI that only works for TicTacToe rn. Added a LocalPlayer with the intent to add more players

* (RANDOM COMMIT) Hope it works

* Changes by bas

* Fixed dependency issues

* Fixed major issue in game deepcopy

* Merge conflict fix

* Removed unused import

* Update GTBGT branch from dev branch (#263)

* started a basis for the tutorials, tic tac toe is almost done with some general stuff still to do.

* rest van de tutorials toegevoegd

* Removed views

* Merge conflict fix

* Removed unused import

---------

Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
Co-authored-by: Bas Antonius de Jong <49651652+BAFGdeJong@users.noreply.github.com>

* Revert "Update GTBGT branch from dev branch (#263)"

This reverts commit 9134d7e343.

* Fixed frontend not using GameController because of spaghetti code.

* Removed unused imports

* GameCanvas not implements a DrawPlayerMove that can be overridden for specific implementations

* Created an event that will request the controller to refresh the UI.

* ADDED DEPENDENCY. Renamed GameControllers to GameManagers, gameThread is not game controller.

* Attempt at adding an online player. I think it doesn't work because of unsubscriben after success not working

* Multiplayer is functional through OnlineThreadBehaviour. Empty slots are currently represented by -1 in the GUI.

* Removed sout spam, added logger than I can't get to work.

* Idek what these changes are

* Te lang geen commit, sorry

* Multiplayer seems to work pretty well now, hopefully I can add the other games soon.

* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler

* getAllListeners

* Removed nulls

* Inbetween commit of adding Reversi. This is a lot of spaghetti.

* Fixed stress tests

* Fixed typo in NetworkingGameClientHandler that prevented losses from being received

* Missed 2nd typo. Fixed

* Added docs, no more list creation when adding events to the bus.

* Fixed unsubscribe not working.

* Moved away from deprecated functions

* moved from wildcard to typed

* Moved away from deprecated function

* Added debugging to GlobalEventBus

* Fixed cleaning flow

* Fixed unsubscribe all

* Fixed unsubscribe all

* Removed unused import

* Works now with updated EventFlow(). Unsubscribing works. ReversiAIR has an issue where a forced move returns -1 and local play back button doesn't work properly. To be fixed

* Fixed ReversiR issue that caused skip turn desync

* Fixed color mismatch with server and online main player is now correct.

* Added a bunch of java doc and small changes

* Small changes

* Added a new Thread Behaviour to test framework.

* Fixed human error I made in TicTacToeR logic...

* Fixed broken event and wrong player being presented as winner.

* Idk changes

* Fixed PR conflicts

---------

Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: Bas Antonius de Jong <49651652+BAFGdeJong@users.noreply.github.com>
2025-12-02 11:25:22 +01:00
Bas Antonius de Jong
d9437c1b8a Tutorials to Dev (#264)
* Fixed garbage code

* added a pop button

* Tutorial images now use ImageAsset.java

* Added button to continue and start game. Refactors

* Refactored nextScreen runnable

* Removed unused imports

* Refactored switch statement

* Added documentation

* Removed space

* Added translations

* Added function input for enabling/disabling localization p/text

---------

Co-authored-by: ramollia <>
2025-12-02 10:57:46 +01:00
c332033a06 Fixed old new EventFlow().listen() missing false as third param 2025-11-30 18:03:16 +01:00
lieght
3953762178 Removed loading widget from Server.java 2025-11-30 17:55:52 +01:00
Bas Antonius de Jong
12a20a224e Fix music display not working (#267)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler

* getAllListeners

* Removed nulls

* Fixed stress tests

* Added docs, no more list creation when adding events to the bus.

* Fixed unsubscribe not working.

* Moved away from deprecated functions

* moved from wildcard to typed

* Moved away from deprecated function

* Added debugging to GlobalEventBus

* Fixed cleaning flow

* Fixed unsubscribe all

* Fixed unsubscribe all

* Removed unused import

* Added LoadingWidget.java for server feedback

* Replace deprecated with correct function
2025-11-30 17:51:52 +01:00
Bas Antonius de Jong
81740acd04 Debugs for EventBus and fixed unsubscribe all (#266)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler

* getAllListeners

* Removed nulls

* Fixed stress tests

* Added docs, no more list creation when adding events to the bus.

* Fixed unsubscribe not working.

* Moved away from deprecated functions

* moved from wildcard to typed

* Moved away from deprecated function

* Added debugging to GlobalEventBus

* Fixed cleaning flow

* Fixed unsubscribe all

* Fixed unsubscribe all

* Removed unused import
2025-11-30 17:15:14 +01:00
Bas Antonius de Jong
25c02c7ad0 Fix eventbus problems (#265)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler

* getAllListeners

* Removed nulls

* Fixed stress tests

* Added docs, no more list creation when adding events to the bus.

* Fixed unsubscribe not working.

* Moved away from deprecated functions

* moved from wildcard to typed

* Moved away from deprecated function
2025-11-30 14:22:05 +01:00
lieght
ec0ce4ea37 Added function input for enabling/disabling localization p/text 2025-11-29 12:23:17 +01:00
132 changed files with 4006 additions and 3496 deletions

View File

@@ -11,30 +11,6 @@ import org.toop.framework.resource.resources.SoundEffectAsset;
public final class Main { public final class Main {
static void main(String[] args) { static void main(String[] args) {
initSystems();
App.run(args); App.run(args);
} }
private static void initSystems() {
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
new Thread(() -> new NetworkingClientEventListener(new NetworkingClientManager())).start();
new Thread(() -> {
MusicManager<MusicAsset> musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), true);
SoundEffectManager<SoundEffectAsset> 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<>(
musicManager,
soundEffectManager,
audioVolumeManager
).initListeners("medium-button-click.wav");
}).start();
}
} }

View File

@@ -1,13 +1,27 @@
package org.toop.app; package org.toop.app;
import javafx.application.Platform;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.WidgetContainer; 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.display.SongDisplay;
import org.toop.app.widget.popup.EscapePopup;
import org.toop.app.widget.popup.QuitPopup; import org.toop.app.widget.popup.QuitPopup;
import org.toop.app.widget.view.MainView; import org.toop.app.widget.view.MainView;
import org.toop.framework.audio.*;
import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
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.ResourceManager;
import org.toop.framework.resource.events.AssetLoaderEvents;
import org.toop.framework.resource.resources.CssAsset; 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.AppContext;
import org.toop.local.AppSettings; import org.toop.local.AppSettings;
@@ -17,6 +31,8 @@ import javafx.scene.Scene;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.util.Objects;
public final class App extends Application { public final class App extends Application {
private static Stage stage; private static Stage stage;
private static Scene scene; private static Scene scene;
@@ -24,22 +40,26 @@ public final class App extends Application {
private static int height; private static int height;
private static int width; private static int width;
private static boolean isQuitting;
public static void run(String[] args) { public static void run(String[] args) {
launch(args); launch(args);
} }
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
// Start loading localization
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/localization"));
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/style"));
final StackPane root = WidgetContainer.setup(); final StackPane root = WidgetContainer.setup();
final Scene scene = new Scene(root); final Scene scene = new Scene(root);
stage.setOpacity(0.0);
stage.setTitle(AppContext.getString("app-title")); stage.setTitle(AppContext.getString("app-title"));
stage.titleProperty().bind(AppContext.bindToKey("app-title")); stage.titleProperty().bind(AppContext.bindToKey("app-title"));
stage.setWidth(1080); stage.setWidth(0);
stage.setHeight(720); stage.setHeight(0);
scene.getRoot(); scene.getRoot();
@@ -47,42 +67,143 @@ public final class App extends Application {
stage.setMinHeight(720); stage.setMinHeight(720);
stage.setOnCloseRequest(event -> { stage.setOnCloseRequest(event -> {
event.consume(); event.consume();
startQuit(); quit();
}); });
stage.setScene(scene); stage.setScene(scene);
stage.setResizable(true); stage.setResizable(true);
stage.show();
App.stage = stage; App.stage = stage;
App.scene = scene; App.scene = scene;
App.width = (int)stage.getWidth(); App.width = (int)stage.getWidth();
App.height = (int)stage.getHeight(); App.height = (int)stage.getHeight();
App.isQuitting = false;
AppSettings.applySettings(); AppSettings.applySettings();
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent();
WidgetContainer.add(Pos.CENTER, new MainView()); setKeybinds(root);
WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay());
LoadingWidget loading = new LoadingWidget(Primitive.text(
"Loading...", false), 0, 0, Integer.MAX_VALUE, false, false // Just set a high default
);
WidgetContainer.setCurrentView(loading);
setOnLoadingSuccess(loading);
EventFlow loadingFlow = new EventFlow();
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));
}
Platform.runLater(() -> loading.setMaxAmount(e.isLoadingAmount()));
Platform.runLater(() -> {
try {
loading.setAmount(e.hasLoadedAmount());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
if (e.hasLoadedAmount() >= e.isLoadingAmount()) {
Platform.runLater(loading::triggerSuccess);
loadingFlow.unsubscribe("init_loading");
}
}, false, "init_loading");
// Start loading assets
new Thread(() -> ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")))
.start();
stage.show();
} }
public static void startQuit() { private void setKeybinds(StackPane root) {
if (isQuitting) { root.addEventHandler(KeyEvent.KEY_PRESSED,event -> {
if (event.getCode() == KeyCode.ESCAPE) {
escapePopup();
}
});
}
public void escapePopup() {
if (!Objects.requireNonNull(
WidgetContainer.find(widget -> widget instanceof QuitPopup || widget instanceof EscapePopup)
).isEmpty()) {
WidgetContainer.removeFirst(QuitPopup.class);
WidgetContainer.removeFirst(EscapePopup.class);
return; return;
} }
WidgetContainer.add(Pos.CENTER, new QuitPopup()); EscapePopup escPopup = new EscapePopup();
isQuitting = true; escPopup.show(Pos.CENTER);
} }
public static void stopQuit() { private void setOnLoadingSuccess(LoadingWidget loading) {
isQuitting = false; loading.setOnSuccess(() -> {
initSystems();
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() { // TODO Move to better place
new Thread(() -> new NetworkingClientEventListener(new NetworkingClientManager())).start();
new Thread(() -> {
MusicManager<MusicAsset> musicManager =
new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), true);
SoundEffectManager<SoundEffectAsset> 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<>(
musicManager,
soundEffectManager,
audioVolumeManager
).initListeners("medium-button-click.wav");
}).start();
// Threads must be ready, before continue, TODO use latch instead
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void quit() { public static void quit() {
stage.close(); stage.close();
System.exit(0); // TODO: This is like dropping a nuke System.exit(0); // TODO: This is like dropping a nuke

View File

@@ -3,9 +3,7 @@ package org.toop.app;
public class GameInformation { public class GameInformation {
public enum Type { public enum Type {
TICTACTOE(2, 5), TICTACTOE(2, 5),
REVERSI(2, 10), REVERSI(2, 10);
CONNECT4(2, 7),
BATTLESHIP(2, 5);
private final int playerCount; private final int playerCount;
private final int maxDepth; private final int maxDepth;

View File

@@ -1,17 +1,26 @@
package org.toop.app; package org.toop.app;
import org.toop.app.game.Connect4Game; import javafx.application.Platform;
import org.toop.app.game.ReversiGame; import javafx.geometry.Pos;
import org.toop.app.game.TicTacToeGame; import org.toop.app.gameControllers.*;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.WidgetContainer; 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.ChallengePopup;
import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.popup.SendChallengePopup; import org.toop.app.widget.popup.SendChallengePopup;
import org.toop.app.widget.view.ServerView; import org.toop.app.widget.view.ServerView;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.clients.TournamentNetworkingClient;
import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.types.NetworkingConnector; 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 org.toop.local.AppContext;
import java.util.List; import java.util.List;
@@ -31,24 +40,27 @@ public final class Server {
private ServerView primary; private ServerView primary;
private boolean isPolling = true; private boolean isPolling = true;
private GameController gameController;
private final AtomicBoolean isSingleGame = new AtomicBoolean(false); private final AtomicBoolean isSingleGame = new AtomicBoolean(false);
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
private EventFlow connectFlow;
public static GameInformation.Type gameToType(String game) { public static GameInformation.Type gameToType(String game) {
if (game.equalsIgnoreCase("tic-tac-toe")) { if (game.equalsIgnoreCase("tic-tac-toe")) {
return GameInformation.Type.TICTACTOE; return GameInformation.Type.TICTACTOE;
} else if (game.equalsIgnoreCase("reversi")) { } else if (game.equalsIgnoreCase("reversi")) {
return GameInformation.Type.REVERSI; return GameInformation.Type.REVERSI;
} else if (game.equalsIgnoreCase("connect4")) {
return GameInformation.Type.CONNECT4;
// } else if (game.equalsIgnoreCase("battleship")) {
// return GameInformation.Type.BATTLESHIP;
} }
return null; 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) { public Server(String ip, String port, String user) {
if (ip.split("\\.").length < 4) { if (ip.split("\\.").length < 4) {
new ErrorPopup("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address")); new ErrorPopup("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address"));
@@ -69,43 +81,101 @@ public final class Server {
return; return;
} }
new EventFlow() final int reconnectAttempts = 10;
LoadingWidget loading = new LoadingWidget(
Primitive.text("connecting"), 0, 0, reconnectAttempts, true, true
);
WidgetContainer.getCurrentView().transitionNext(loading);
var a = new EventFlow()
.addPostEvent(NetworkEvents.StartClient.class, .addPostEvent(NetworkEvents.StartClient.class,
new TournamentNetworkingClient(), new TournamentNetworkingClient(),
new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS) new NetworkingConnector(ip, parsedPort, reconnectAttempts, 1, TimeUnit.SECONDS)
) );
.onResponse(NetworkEvents.StartClientResponse.class, e -> {
this.user = user;
clientId = e.clientId();
new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent(); loading.setOnFailure(() -> {
if (WidgetContainer.getCurrentView() == loading) WidgetContainer.getCurrentView().transitionPrevious();
a.unsubscribeAll();
WidgetContainer.add(
Pos.CENTER,
new ErrorPopup(AppContext.getString("connecting-failed") + " " + ip + ":" + port)
);
});
primary = new ServerView(user, this::sendChallenge, this::disconnect); a.onResponse(NetworkEvents.StartClientResponse.class, e -> {
WidgetContainer.getCurrentView().transitionNext(primary);
startPopulateScheduler(); if (!e.successful()) {
populateGameList(); return;
}
}).postEvent(); primary = new ServerView(user, this::sendChallenge);
WidgetContainer.getCurrentView().transitionNextCustom(primary, "disconnect", this::disconnect);
new EventFlow().listen(this::handleReceivedChallenge) a.unsubscribe("connecting");
.listen(this::handleMatchResponse); a.unsubscribe("startclient");
this.user = user;
clientId = e.clientId();
new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent();
startPopulateScheduler();
populateGameList();
primary.removeViewFromPreviousChain(loading);
}, false, "startclient")
.listen(
NetworkEvents.ConnectTry.class,
e -> 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) { private void sendChallenge(String opponent) {
if (!isPolling) return; if (!isPolling) return;
new SendChallengePopup(this, opponent, (playerInformation, gameType) -> { var a = new SendChallengePopup(this, opponent, (playerInformation, gameType) -> {
new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent(); new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent();
isSingleGame.set(true); isSingleGame.set(true);
}); });
a.show(Pos.CENTER);
} }
private void handleMatchResponse(NetworkEvents.GameMatchResponse response) { private void handleMatchResponse(NetworkEvents.GameMatchResponse response) {
if (!isPolling) return; // TODO: Redo all of this mess
if (gameController != null) {
gameController.stop();
}
gameController = null;
//if (!isPolling) return;
String gameType = extractQuotedValue(response.gameType()); String gameType = extractQuotedValue(response.gameType());
if (response.clientId() == clientId) { if (response.clientId() == clientId) {
isPolling = false; isPolling = false;
onlinePlayers.clear(); onlinePlayers.clear();
@@ -126,32 +196,74 @@ public final class Server {
information.players[0].computerThinkTime = 1; information.players[0].computerThinkTime = 1;
information.players[1].name = response.opponent(); information.players[1].name = response.opponent();
Runnable onGameOverRunnable = isSingleGame.get()? null: this::gameOver; /*switch (type){
case TICTACTOE ->{
players[myTurn] = new ArtificialPlayer<>(new TicTacToeAIR(9), user);
}
case REVERSI ->{
players[myTurn] = new ArtificialPlayer<>(new ReversiAIR(), user);
}
}*/
switch (type) { switch (type) {
case TICTACTOE -> case TICTACTOE ->{
new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); Player<BitboardTicTacToe>[] players = new Player[2];
case REVERSI -> players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent());
new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); players[myTurn] = new ArtificialPlayer<>(new RandomAI<BitboardTicTacToe>(), user);
case CONNECT4 -> gameController = new TicTacToeBitController(players);
new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); }
case REVERSI -> {
Player<BitboardReversi>[] players = new Player[2];
players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent());
players[myTurn] = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(), user);
gameController = new ReversiBitController(players);}
default -> new ErrorPopup("Unsupported game type."); 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) { private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) {
if (!isPolling) return; if (!isPolling) return;
String challengerName = extractQuotedValue(response.challengerName()); String challengerName = extractQuotedValue(response.challengerName());
String gameType = extractQuotedValue(response.gameType()); String gameType = extractQuotedValue(response.gameType());
final String finalGameType = gameType; final String finalGameType = gameType;
new ChallengePopup(challengerName, gameType, (playerInformation) -> { var a = new ChallengePopup(challengerName, gameType, (playerInformation) -> {
final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", ""));
new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent();
isSingleGame.set(true); isSingleGame.set(true);
}); });
a.show(Pos.CENTER);
} }
private void sendMessage(String message) { private void sendMessage(String message) {
@@ -162,7 +274,9 @@ public final class Server {
new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent();
isPolling = false; isPolling = false;
stopScheduler(); stopScheduler();
primary.transitionPrevious(); connectFlow.unsubscribeAll();
WidgetContainer.getCurrentView().transitionPrevious();
} }
private void forfeitGame() { private void forfeitGame() {
@@ -200,7 +314,7 @@ public final class Server {
} else { } else {
stopScheduler(); stopScheduler();
} }
}, 0, 5, TimeUnit.SECONDS); }, 0, 1, TimeUnit.SECONDS);
} }
private void stopScheduler() { private void stopScheduler() {

View File

@@ -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<T extends TurnBasedGame<T>> implements GameCanvas<T> {
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<Long> onCellCLicked;
public void setOnCellClicked(Consumer<Long> 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<Integer> 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;
}
}

View File

@@ -1,11 +0,0 @@
package org.toop.app.canvas;
import javafx.scene.paint.Color;
import java.util.function.Consumer;
public class Connect4Canvas extends GameCanvas {
public Connect4Canvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
super(color, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked,null);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
package org.toop.app.canvas;
public interface DrawPlayerMove {
void drawPlayerMove(int player, int move);
}

View File

@@ -1,215 +1,8 @@
package org.toop.app.canvas; package org.toop.app.canvas;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.canvas.Canvas; import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import java.util.concurrent.atomic.AtomicReference; public interface GameCanvas<T extends TurnBasedGame<T>> extends GameDrawer<T>{
import java.util.function.Consumer; Canvas getCanvas();
}
public abstract class 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;
protected GameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer<Integer> onCellClicked, Consumer<Integer> newCellEntered) {
canvas = new Canvas(width, height);
graphics = canvas.getGraphicsContext2D();
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();
onCellClicked.accept(column + row * rowSize);
}
});
render();
}
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 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;
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.app.canvas;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface GameDrawer<T extends TurnBasedGame<T>> {
void redraw(T gameCopy);
}

View File

@@ -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<BitboardReversi> {
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));
}
}

View File

@@ -1,84 +0,0 @@
package org.toop.app.canvas;
import javafx.scene.paint.Color;
import org.toop.game.records.Move;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public final class ReversiCanvas extends GameCanvas {
private Move[] currentlyHighlightedMoves = null;
public ReversiCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked, Consumer<Integer> newCellEntered) {
super(color, new Color(0f,0.4f,0.2f,1f), width, height, 8, 8, 5, true, onCellClicked, newCellEntered);
drawStartingDots();
final AtomicReference<Cell> lastHoveredCell = new AtomicReference<>(null);
canvas.setOnMouseMoved(event -> {
double mouseX = event.getX();
double mouseY = event.getY();
int cellId = -1;
Cell hovered = null;
for (Cell cell : cells) {
if (cell.isInside(mouseX, mouseY)) {
hovered = cell;
cellId = turnCoordsIntoCellId(mouseX, mouseY);
break;
}
}
Cell previous = lastHoveredCell.get();
if (hovered != previous) {
lastHoveredCell.set(hovered);
newCellEntered.accept(cellId);
}
});
}
public void setCurrentlyHighlightedMovesNull() {
currentlyHighlightedMoves = null;
}
public void drawHighlightDots(Move[] moves){
if (currentlyHighlightedMoves != null){
for (final Move move : currentlyHighlightedMoves){
Color color = move.value() == 'W'? Color.BLACK: Color.WHITE;
drawInnerDot(color, move.position(), true);
}
}
currentlyHighlightedMoves = moves;
if (moves != null) {
for (Move move : moves) {
Color color = move.value() == 'B' ? Color.BLACK : Color.WHITE;
drawInnerDot(color, move.position(), false);
}
}
}
private int turnCoordsIntoCellId(double x, double y) {
final int column = (int) ((x / this.width) * rowSize);
final int row = (int) ((y / this.height) * columnSize);
return column + row * rowSize;
}
public void drawStartingDots() {
drawDot(Color.BLACK, 28);
drawDot(Color.WHITE, 36);
drawDot(Color.BLACK, 35);
drawDot(Color.WHITE, 27);
}
public void drawLegalPosition(int cell, char player) {
Color innerColor;
if (player == 'B') {
innerColor = new Color(0.0f, 0.0f, 0.0f, 0.6f);
}
else {
innerColor = new Color(1.0f, 1.0f, 1.0f, 0.75f);
}
drawInnerDot(innerColor, cell,false);
}
}

View File

@@ -0,0 +1,63 @@
package org.toop.app.canvas;
import javafx.scene.paint.Color;
import org.toop.app.App;
import org.toop.game.games.tictactoe.BitboardTicTacToe;
import java.util.Arrays;
import java.util.function.Consumer;
public class TicTacToeBitCanvas extends BitGameCanvas<BitboardTicTacToe>{
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);
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.strokeLine(x, y, x + width, y + height);
graphics.strokeLine(x + width, y, x, y + height);
}
public void drawO(Color color, int cell) {
graphics.setStroke(color);
graphics.setLineWidth(gapSize);
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.strokeOval(x, y, width, height);
}
}

View File

@@ -1,38 +0,0 @@
package org.toop.app.canvas;
import javafx.scene.paint.Color;
import java.util.function.Consumer;
public final class TicTacToeCanvas extends GameCanvas {
public TicTacToeCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
super(color, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked,null);
}
public void drawX(Color color, int cell) {
graphics.setStroke(color);
graphics.setLineWidth(gapSize);
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.strokeLine(x, y, x + width, y + height);
graphics.strokeLine(x + width, y, x, y + height);
}
public void drawO(Color color, int cell) {
graphics.setStroke(color);
graphics.setLineWidth(gapSize);
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.strokeOval(x, y, width, height);
}
}

View File

@@ -1,122 +0,0 @@
package org.toop.app.game;
import org.toop.app.GameInformation;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.view.GameView;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.game.Game;
import org.toop.game.records.Move;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public abstract class BaseGameThread<TGame extends Game, TAI, TCanvas> {
protected final GameInformation information;
protected final int myTurn;
protected final Runnable onGameOver;
protected final BlockingQueue<Move> moveQueue;
protected final TGame game;
protected final TAI ai;
protected final GameView primary;
protected final TCanvas canvas;
protected final AtomicBoolean isRunning = new AtomicBoolean(true);
protected BaseGameThread(
GameInformation information,
int myTurn,
Runnable onForfeit,
Runnable onExit,
Consumer<String> onMessage,
Runnable onGameOver,
Supplier<TGame> gameSupplier,
Supplier<TAI> aiSupplier,
Function<Consumer<Integer>, TCanvas> canvasFactory) {
this.information = information;
this.myTurn = myTurn;
this.onGameOver = onGameOver;
this.moveQueue = new LinkedBlockingQueue<>();
this.game = gameSupplier.get();
this.ai = aiSupplier.get();
String type = information.type.getTypeToString();
if (onForfeit == null || onExit == null) {
primary = new GameView(null, () -> {
isRunning.set(false);
WidgetContainer.getCurrentView().transitionPrevious();
}, null, type);
} else {
primary = new GameView(onForfeit, () -> {
isRunning.set(false);
onExit.run();
}, onMessage, type);
}
this.canvas = canvasFactory.apply(this::onCellClicked);
addCanvasToPrimary();
WidgetContainer.getCurrentView().transitionNext(primary);
if (onForfeit == null || onExit == null)
new Thread(this::localGameThread).start();
else
new EventFlow()
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse);
setGameLabels(myTurn == 0);
}
private void onCellClicked(int cell) {
if (!isRunning.get()) return;
final int currentTurn = getCurrentTurn();
if (!information.players[currentTurn].isHuman) return;
final char value = getSymbolForTurn(currentTurn);
try {
moveQueue.put(new Move(cell, value));
} catch (InterruptedException _) {}
}
protected void gameOver() {
if (onGameOver != null) {
isRunning.set(false);
onGameOver.run();
}
}
protected void setGameLabels(boolean isMe) {
final int currentTurn = getCurrentTurn();
final String turnName = getNameForTurn(currentTurn);
primary.nextPlayer(
isMe,
information.players[isMe ? 0 : 1].name,
turnName,
information.players[isMe ? 1 : 0].name
);
}
protected abstract void addCanvasToPrimary();
protected abstract int getCurrentTurn();
protected abstract char getSymbolForTurn(int turn);
protected abstract String getNameForTurn(int turn);
protected abstract void onMoveResponse(NetworkEvents.GameMoveResponse response);
protected abstract void onYourTurnResponse(NetworkEvents.YourTurnResponse response);
protected abstract void localGameThread();
}

View File

@@ -1,265 +0,0 @@
package org.toop.app.game;
import javafx.geometry.Pos;
import javafx.scene.paint.Color;
import org.toop.app.App;
import org.toop.app.GameInformation;
import org.toop.app.canvas.Connect4Canvas;
import org.toop.app.widget.view.GameView;
import org.toop.app.widget.WidgetContainer;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.game.Connect4.Connect4;
import org.toop.game.Connect4.Connect4AI;
import org.toop.game.enumerators.GameState;
import org.toop.game.records.Move;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class Connect4Game {
private final GameInformation information;
private final int myTurn;
private Runnable onGameOver;
private final BlockingQueue<Move> moveQueue;
private final Connect4 game;
private final Connect4AI ai;
private final int columnSize = 7;
private final int rowSize = 6;
private final GameView primary;
private final Connect4Canvas canvas;
private final AtomicBoolean isRunning;
public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
this.information = information;
this.myTurn = myTurn;
this.onGameOver = onGameOver;
moveQueue = new LinkedBlockingQueue<Move>();
game = new Connect4();
ai = new Connect4AI();
isRunning = new AtomicBoolean(true);
if (onForfeit == null || onExit == null) {
primary = new GameView(null, () -> {
isRunning.set(false);
WidgetContainer.getCurrentView().transitionPrevious();
}, null, "Connect4");
} else {
primary = new GameView(onForfeit, () -> {
isRunning.set(false);
onExit.run();
}, onMessage, "Connect4");
}
canvas = new Connect4Canvas(Color.GRAY,
(App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,
(cell) -> {
if (onForfeit == null || onExit == null) {
if (information.players[game.getCurrentTurn()].isHuman) {
final char value = game.getCurrentTurn() == 0? 'X' : 'O';
try {
moveQueue.put(new Move(cell%columnSize, value));
} catch (InterruptedException _) {}
}
} else {
if (information.players[0].isHuman) {
final char value = myTurn == 0? 'X' : 'O';
try {
moveQueue.put(new Move(cell%columnSize, value));
} catch (InterruptedException _) {}
}
}
});
primary.add(Pos.CENTER, canvas.getCanvas());
WidgetContainer.getCurrentView().transitionNext(primary);
if (onForfeit == null || onExit == null) {
new Thread(this::localGameThread).start();
setGameLabels(information.players[0].isHuman);
} else {
new EventFlow()
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse);
setGameLabels(myTurn == 0);
}
updateCanvas();
}
public Connect4Game(GameInformation information) {
this(information, 0, null, null, null, null);
}
private void localGameThread() {
while (isRunning.get()) {
final int currentTurn = game.getCurrentTurn();
final String currentValue = currentTurn == 0? "RED" : "BLUE";
final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount();
primary.nextPlayer(information.players[currentTurn].isHuman,
information.players[currentTurn].name,
currentValue,
information.players[nextTurn].name);
Move move = null;
if (information.players[currentTurn].isHuman) {
try {
final Move wants = moveQueue.take();
final Move[] legalMoves = game.getLegalMoves();
for (final Move legalMove : legalMoves) {
if (legalMove.position() == wants.position() &&
legalMove.value() == wants.value()) {
move = wants;
break;
}
}
} catch (InterruptedException _) {}
} else {
final long start = System.currentTimeMillis();
move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty);
if (information.players[currentTurn].computerThinkTime > 0) {
final long elapsedTime = System.currentTimeMillis() - start;
final long sleepTime = Math.abs(information.players[currentTurn].computerThinkTime * 1000L - elapsedTime);
try {
Thread.sleep((long)(sleepTime * Math.random()));
} catch (InterruptedException _) {}
}
}
if (move == null) {
continue;
}
final GameState state = game.play(move);
updateCanvas();
/*
if (move.value() == 'X') {
canvas.drawX(Color.INDIANRED, move.position());
} else if (move.value() == 'O') {
canvas.drawO(Color.ROYALBLUE, move.position());
}
*/
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
primary.gameOver(true, information.players[currentTurn].name);
} else if (state == GameState.DRAW) {
primary.gameOver(false, "");
}
isRunning.set(false);
}
}
}
private void onMoveResponse(NetworkEvents.GameMoveResponse response) {
if (!isRunning.get()) {
return;
}
char playerChar;
if (response.player().equalsIgnoreCase(information.players[0].name)) {
playerChar = myTurn == 0? 'X' : 'O';
} else {
playerChar = myTurn == 0? 'O' : 'X';
}
final Move move = new Move(Integer.parseInt(response.move()), playerChar);
final GameState state = game.play(move);
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
if (response.player().equalsIgnoreCase(information.players[0].name)) {
primary.gameOver(true, information.players[0].name);
gameOver();
} else {
primary.gameOver(false, information.players[1].name);
gameOver();
}
} else if (state == GameState.DRAW) {
primary.gameOver(false, "");
gameOver();
}
}
if (move.value() == 'X') {
canvas.drawDot(Color.INDIANRED, move.position());
} else if (move.value() == 'O') {
canvas.drawDot(Color.ROYALBLUE, move.position());
}
updateCanvas();
setGameLabels(game.getCurrentTurn() == myTurn);
}
private void gameOver() {
if (onGameOver == null){
return;
}
isRunning.set(false);
onGameOver.run();
}
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
if (!isRunning.get()) {
return;
}
moveQueue.clear();
int position = -1;
if (information.players[0].isHuman) {
try {
position = moveQueue.take().position();
} catch (InterruptedException _) {}
} else {
final Move move = ai.findBestMove(game, information.players[0].computerDifficulty);
assert move != null;
position = move.position();
}
new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position))
.postEvent();
}
private void updateCanvas() {
canvas.clearAll();
for (int i = 0; i < game.getBoard().length; i++) {
if (game.getBoard()[i] == 'X') {
canvas.drawDot(Color.RED, i);
} else if (game.getBoard()[i] == 'O') {
canvas.drawDot(Color.BLUE, i);
}
}
}
private void setGameLabels(boolean isMe) {
final int currentTurn = game.getCurrentTurn();
final String currentValue = currentTurn == 0? "RED" : "BLUE";
primary.nextPlayer(isMe,
information.players[isMe? 0 : 1].name,
currentValue,
information.players[isMe? 1 : 0].name);
}
}

View File

@@ -1,333 +0,0 @@
package org.toop.app.game;
import javafx.animation.SequentialTransition;
import org.toop.app.App;
import org.toop.app.GameInformation;
import org.toop.app.canvas.ReversiCanvas;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.view.GameView;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.game.enumerators.GameState;
import org.toop.game.records.Move;
import org.toop.game.reversi.Reversi;
import org.toop.game.reversi.ReversiAI;
import javafx.geometry.Pos;
import javafx.scene.paint.Color;
import java.awt.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public final class ReversiGame {
private final GameInformation information;
private final int myTurn;
private final Runnable onGameOver;
private final BlockingQueue<Move> moveQueue;
private final Reversi game;
private final ReversiAI ai;
private final GameView primary;
private final ReversiCanvas canvas;
private final AtomicBoolean isRunning;
private final AtomicBoolean isPaused;
public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
this.information = information;
this.myTurn = myTurn;
this.onGameOver = onGameOver;
moveQueue = new LinkedBlockingQueue<>();
game = new Reversi();
ai = new ReversiAI();
isRunning = new AtomicBoolean(true);
isPaused = new AtomicBoolean(false);
if (onForfeit == null || onExit == null) {
primary = new GameView(null, () -> {
isRunning.set(false);
WidgetContainer.getCurrentView().transitionPrevious();
}, null, "Reversi");
} else {
primary = new GameView(onForfeit, () -> {
isRunning.set(false);
onExit.run();
}, onMessage, "Reversi");
}
canvas = new ReversiCanvas(Color.BLACK,
(App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,
(cell) -> {
if (onForfeit == null || onExit == null) {
if (information.players[game.getCurrentTurn()].isHuman) {
final char value = game.getCurrentTurn() == 0? 'B' : 'W';
try {
moveQueue.put(new Move(cell, value));
} catch (InterruptedException _) {}
}
} else {
if (information.players[0].isHuman) {
final char value = myTurn == 0? 'B' : 'W';
try {
moveQueue.put(new Move(cell, value));
} catch (InterruptedException _) {}
}
}
},this::highlightCells);
primary.add(Pos.CENTER, canvas.getCanvas());
WidgetContainer.getCurrentView().transitionNext(primary);
if (onForfeit == null || onExit == null) {
new Thread(this::localGameThread).start();
setGameLabels(information.players[0].isHuman);
} else {
new EventFlow()
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse);
setGameLabels(myTurn == 0);
}
updateCanvas(false);
}
public ReversiGame(GameInformation information) {
this(information, 0, null, null, null,null);
}
private void localGameThread() {
while (isRunning.get()) {
if (isPaused.get()) {
try {
Thread.sleep(200);
} catch (InterruptedException _) {}
continue;
}
final int currentTurn = game.getCurrentTurn();
final String currentValue = currentTurn == 0? "BLACK" : "WHITE";
final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount();
primary.nextPlayer(information.players[currentTurn].isHuman,
information.players[currentTurn].name,
currentValue,
information.players[nextTurn].name);
Move move = null;
if (information.players[currentTurn].isHuman) {
try {
final Move wants = moveQueue.take();
final Move[] legalMoves = game.getLegalMoves();
for (final Move legalMove : legalMoves) {
if (legalMove.position() == wants.position() &&
legalMove.value() == wants.value()) {
move = wants;
break;
}
}
} catch (InterruptedException _) {}
} else {
final long start = System.currentTimeMillis();
move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty);
if (information.players[currentTurn].computerThinkTime > 0) {
final long elapsedTime = System.currentTimeMillis() - start;
final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime;
try {
Thread.sleep((long) (sleepTime * Math.random()));
} catch (InterruptedException _) {}
}
}
if (move == null) {
continue;
}
canvas.setCurrentlyHighlightedMovesNull();
final GameState state = game.play(move);
updateCanvas(true);
if (state != GameState.NORMAL) {
if (state == GameState.TURN_SKIPPED){
continue;
}
int winningPLayerNumber = getPlayerNumberWithHighestScore();
if (state == GameState.WIN && winningPLayerNumber > -1) {
primary.gameOver(true, information.players[winningPLayerNumber].name);
} else if (state == GameState.DRAW || winningPLayerNumber == -1) {
primary.gameOver(false, "");
}
isRunning.set(false);
}
}
}
private int getPlayerNumberWithHighestScore(){
Reversi.Score score = game.getScore();
if (score.player1Score() > score.player2Score()) return 0;
if (score.player1Score() < score.player2Score()) return 1;
return -1;
}
private void onMoveResponse(NetworkEvents.GameMoveResponse response) {
if (!isRunning.get()) {
return;
}
char playerChar;
if (response.player().equalsIgnoreCase(information.players[0].name)) {
playerChar = myTurn == 0? 'B' : 'W';
} else {
playerChar = myTurn == 0? 'W' : 'B';
}
final Move move = new Move(Integer.parseInt(response.move()), playerChar);
final GameState state = game.play(move);
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
if (response.player().equalsIgnoreCase(information.players[0].name)) {
primary.gameOver(true, information.players[0].name);
gameOver();
} else {
primary.gameOver(false, information.players[1].name);
gameOver();
}
} else if (state == GameState.DRAW) {
primary.gameOver(false, "");
game.play(move);
}
}
updateCanvas(false);
setGameLabels(game.getCurrentTurn() == myTurn);
}
private void gameOver() {
if (onGameOver == null){
return;
}
isRunning.set(false);
onGameOver.run();
}
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
if (!isRunning.get()) {
return;
}
moveQueue.clear();
int position = -1;
if (information.players[0].isHuman) {
try {
position = moveQueue.take().position();
} catch (InterruptedException _) {}
} else {
final Move move = ai.findBestMove(game, information.players[0].computerDifficulty);
assert move != null;
position = move.position();
}
new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short) position))
.postEvent();
}
private void updateCanvas(boolean animate) {
// Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve.
canvas.clearAll();
for (int i = 0; i < game.getBoard().length; i++) {
if (game.getBoard()[i] == 'B') {
canvas.drawDot(Color.BLACK, i);
} else if (game.getBoard()[i] == 'W') {
canvas.drawDot(Color.WHITE, i);
}
}
final Move[] flipped = game.getMostRecentlyFlippedPieces();
final SequentialTransition animation = new SequentialTransition();
isPaused.set(true);
final Color fromColor = game.getCurrentPlayer() == 'W'? Color.WHITE : Color.BLACK;
final Color toColor = game.getCurrentPlayer() == 'W'? Color.BLACK : Color.WHITE;
if (animate && flipped != null) {
for (final Move flip : flipped) {
canvas.clear(flip.position());
canvas.drawDot(fromColor, flip.position());
animation.getChildren().addFirst(canvas.flipDot(fromColor, toColor, flip.position()));
}
}
animation.setOnFinished(_ -> {
isPaused.set(false);
if (information.players[game.getCurrentTurn()].isHuman) {
final Move[] legalMoves = game.getLegalMoves();
for (final Move legalMove : legalMoves) {
canvas.drawLegalPosition(legalMove.position(), game.getCurrentPlayer());
}
}
});
animation.play();
}
private void setGameLabels(boolean isMe) {
final int currentTurn = game.getCurrentTurn();
final String currentValue = currentTurn == 0? "BLACK" : "WHITE";
primary.nextPlayer(isMe,
information.players[isMe? 0 : 1].name,
currentValue,
information.players[isMe? 1 : 0].name);
}
private void highlightCells(int cellEntered) {
if (information.players[game.getCurrentTurn()].isHuman) {
Move[] legalMoves = game.getLegalMoves();
boolean isLegalMove = false;
for (Move move : legalMoves) {
if (move.position() == cellEntered){
isLegalMove = true;
break;
}
}
if (cellEntered >= 0){
Move[] moves = null;
if (isLegalMove) {
moves = game.getFlipsForPotentialMove(
new Point(cellEntered%game.getColumnSize(),cellEntered/game.getRowSize()),
game.getCurrentPlayer());
}
canvas.drawHighlightDots(moves);
}
}
}
}

View File

@@ -1,250 +0,0 @@
package org.toop.app.game;
import org.toop.app.App;
import org.toop.app.GameInformation;
import org.toop.app.canvas.TicTacToeCanvas;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.view.GameView;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.game.enumerators.GameState;
import org.toop.game.records.Move;
import org.toop.game.tictactoe.TicTacToe;
import org.toop.game.tictactoe.TicTacToeAI;
import javafx.geometry.Pos;
import javafx.scene.paint.Color;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public final class TicTacToeGame {
private final GameInformation information;
private final int myTurn;
private final Runnable onGameOver;
private final BlockingQueue<Move> moveQueue;
private final TicTacToe game;
private final TicTacToeAI ai;
private final GameView primary;
private final TicTacToeCanvas canvas;
private final AtomicBoolean isRunning;
public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
this.information = information;
this.myTurn = myTurn;
this.onGameOver = onGameOver;
moveQueue = new LinkedBlockingQueue<Move>();
game = new TicTacToe();
ai = new TicTacToeAI();
isRunning = new AtomicBoolean(true);
if (onForfeit == null || onExit == null) {
primary = new GameView(null, () -> {
isRunning.set(false);
WidgetContainer.getCurrentView().transitionPrevious();
}, null, "TicTacToe");
} else {
primary = new GameView(onForfeit, () -> {
isRunning.set(false);
onExit.run();
}, onMessage, "TicTacToe");
}
canvas = new TicTacToeCanvas(Color.GRAY,
(App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,
(cell) -> {
if (onForfeit == null || onExit == null) {
if (information.players[game.getCurrentTurn()].isHuman) {
final char value = game.getCurrentTurn() == 0? 'X' : 'O';
try {
moveQueue.put(new Move(cell, value));
} catch (InterruptedException _) {}
}
} else {
if (information.players[0].isHuman) {
final char value = myTurn == 0? 'X' : 'O';
try {
moveQueue.put(new Move(cell, value));
} catch (InterruptedException _) {}
}
}
});
primary.add(Pos.CENTER, canvas.getCanvas());
WidgetContainer.getCurrentView().transitionNext(primary);
if (onForfeit == null || onExit == null) {
new Thread(this::localGameThread).start();
} else {
new EventFlow()
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse);
setGameLabels(myTurn == 0);
}
}
public TicTacToeGame(GameInformation information) {
this(information, 0, null, null, null, null);
}
private void localGameThread() {
while (isRunning.get()) {
final int currentTurn = game.getCurrentTurn();
final String currentValue = currentTurn == 0? "X" : "O";
final int nextTurn = (currentTurn + 1) % information.type.getPlayerCount();
primary.nextPlayer(information.players[currentTurn].isHuman,
information.players[currentTurn].name,
currentValue,
information.players[nextTurn].name);
Move move = null;
if (information.players[currentTurn].isHuman) {
try {
final Move wants = moveQueue.take();
final Move[] legalMoves = game.getLegalMoves();
for (final Move legalMove : legalMoves) {
if (legalMove.position() == wants.position() &&
legalMove.value() == wants.value()) {
move = wants;
break;
}
}
} catch (InterruptedException _) {}
} else {
final long start = System.currentTimeMillis();
move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty);
if (information.players[currentTurn].computerThinkTime > 0) {
final long elapsedTime = System.currentTimeMillis() - start;
final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime;
try {
Thread.sleep((long)(sleepTime * Math.random()));
} catch (InterruptedException _) {}
}
}
if (move == null) {
continue;
}
final GameState state = game.play(move);
if (move.value() == 'X') {
canvas.drawX(Color.INDIANRED, move.position());
} else if (move.value() == 'O') {
canvas.drawO(Color.ROYALBLUE, move.position());
}
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
primary.gameOver(true, information.players[currentTurn].name);
} else if (state == GameState.DRAW) {
primary.gameOver(false, "");
}
isRunning.set(false);
}
}
}
private void onMoveResponse(NetworkEvents.GameMoveResponse response) {
if (!isRunning.get()) {
return;
}
char playerChar;
if (response.player().equalsIgnoreCase(information.players[0].name)) {
playerChar = myTurn == 0? 'X' : 'O';
} else {
playerChar = myTurn == 0? 'O' : 'X';
}
final Move move = new Move(Integer.parseInt(response.move()), playerChar);
final GameState state = game.play(move);
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
if (response.player().equalsIgnoreCase(information.players[0].name)) {
primary.gameOver(true, information.players[0].name);
gameOver();
} else {
primary.gameOver(false, information.players[1].name);
gameOver();
}
} else if (state == GameState.DRAW) {
if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over.
primary.gameOver(false, "");
gameOver();
}
}
}
if (move.value() == 'X') {
canvas.drawX(Color.RED, move.position());
} else if (move.value() == 'O') {
canvas.drawO(Color.BLUE, move.position());
}
setGameLabels(game.getCurrentTurn() == myTurn);
}
private void gameOver() {
if (onGameOver == null){
return;
}
isRunning.set(false);
onGameOver.run();
}
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
if (!isRunning.get()) {
return;
}
moveQueue.clear();
int position = -1;
if (information.players[0].isHuman) {
try {
position = moveQueue.take().position();
} catch (InterruptedException _) {}
} else {
final Move move;
move = ai.findBestMove(game, information.players[0].computerDifficulty);
assert move != null;
position = move.position();
}
new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position))
.postEvent();
}
private void setGameLabels(boolean isMe) {
final int currentTurn = game.getCurrentTurn();
final String currentValue = currentTurn == 0? "X" : "O";
primary.nextPlayer(isMe,
information.players[isMe? 0 : 1].name,
currentValue,
information.players[isMe? 1 : 0].name);
}
}

View File

@@ -1,176 +0,0 @@
package org.toop.app.game;
import org.toop.app.App;
import org.toop.app.GameInformation;
import org.toop.app.canvas.TicTacToeCanvas;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.game.enumerators.GameState;
import org.toop.game.records.Move;
import org.toop.game.tictactoe.TicTacToe;
import org.toop.game.tictactoe.TicTacToeAI;
import java.util.function.Consumer;
import javafx.geometry.Pos;
import javafx.scene.paint.Color;
public final class TicTacToeGameThread extends BaseGameThread<TicTacToe, TicTacToeAI, TicTacToeCanvas> {
public TicTacToeGameThread(GameInformation info, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
super(info, myTurn, onForfeit, onExit, onMessage, onGameOver,
TicTacToe::new,
TicTacToeAI::new,
clickHandler -> new TicTacToeCanvas(Color.GRAY, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, clickHandler)
);
}
public TicTacToeGameThread(GameInformation info) {
this(info, 0, null, null, null, null);
}
@Override
protected void addCanvasToPrimary() {
primary.add(Pos.CENTER, canvas.getCanvas());
}
@Override
protected int getCurrentTurn() {
return game.getCurrentTurn();
}
@Override
protected char getSymbolForTurn(int turn) {
return turn == 0 ? 'X' : 'O';
}
@Override
protected String getNameForTurn(int turn) {
return turn == 0 ? "X" : "O";
}
private void drawMove(Move move) {
if (move.value() == 'X') canvas.drawX(Color.RED, move.position());
else canvas.drawO(Color.BLUE, move.position());
}
@Override
protected void onMoveResponse(NetworkEvents.GameMoveResponse response) {
if (!isRunning.get()) {
return;
}
char playerChar;
if (response.player().equalsIgnoreCase(information.players[0].name)) {
playerChar = myTurn == 0? 'X' : 'O';
} else {
playerChar = myTurn == 0? 'O' : 'X';
}
final Move move = new Move(Integer.parseInt(response.move()), playerChar);
final GameState state = game.play(move);
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
if (response.player().equalsIgnoreCase(information.players[0].name)) {
primary.gameOver(true, information.players[0].name);
gameOver();
} else {
primary.gameOver(false, information.players[1].name);
gameOver();
}
} else if (state == GameState.DRAW) {
if (game.getLegalMoves().length == 0) {
primary.gameOver(false, "");
gameOver();
}
}
}
drawMove(move);
setGameLabels(game.getCurrentTurn() == myTurn);
}
@Override
protected void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
if (!isRunning.get()) {
return;
}
moveQueue.clear();
int position = -1;
if (information.players[0].isHuman) {
try {
position = moveQueue.take().position();
} catch (InterruptedException _) {}
} else {
final Move move;
if (information.players[1].name.equalsIgnoreCase("pism")) {
move = ai.findWorstMove(game,9);
}else{
move = ai.findBestMove(game, information.players[0].computerDifficulty);
}
assert move != null;
position = move.position();
}
new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position))
.postEvent();
}
@Override
protected void localGameThread() {
while (isRunning.get()) {
final int currentTurn = game.getCurrentTurn();
setGameLabels(currentTurn == myTurn);
Move move = null;
if (information.players[currentTurn].isHuman) {
try {
final Move wants = moveQueue.take();
final Move[] legalMoves = game.getLegalMoves();
for (final Move legalMove : legalMoves) {
if (legalMove.position() == wants.position() &&
legalMove.value() == wants.value()) {
move = wants;
break;
}
}
} catch (InterruptedException _) {}
} else {
final long start = System.currentTimeMillis();
move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty);
if (information.players[currentTurn].computerThinkTime > 0) {
final long elapsedTime = System.currentTimeMillis() - start;
final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime;
try {
Thread.sleep((long)(sleepTime * Math.random()));
} catch (InterruptedException _) {}
}
}
if (move == null) {
continue;
}
final GameState state = game.play(move);
drawMove(move);
if (state != GameState.NORMAL) {
if (state == GameState.WIN) {
primary.gameOver(information.players[currentTurn].isHuman, information.players[currentTurn].name);
} else if (state == GameState.DRAW) {
primary.gameOver(false, "");
}
isRunning.set(false);
}
}
}
}

View File

@@ -0,0 +1,144 @@
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;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class GenericGameController<T extends TurnBasedGame<T>> 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<T> canvas;
protected final TurnBasedGame<T> 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<T> 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.postAsync(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<T> 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<T> 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<T> 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());
}
}

View File

@@ -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<BitboardReversi> {
public ReversiBitController(Player<BitboardReversi>[] players) {
BitboardReversi game = new BitboardReversi(players);
ThreadBehaviour thread = new LocalThreadBehaviour<>(game);
for (Player<BitboardReversi> player : players) {
if (player instanceof OnlinePlayer<BitboardReversi>){
thread = new OnlineThreadBehaviour<>(game);
}
}
super(new ReversiBitCanvas(), game, thread, "Reversi");
}
}

View File

@@ -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<BitboardTicTacToe> {
public TicTacToeBitController(Player<BitboardTicTacToe>[] players) {
BitboardTicTacToe game = new BitboardTicTacToe(players);
ThreadBehaviour thread = new LocalThreadBehaviour<>(game);
for (Player<BitboardTicTacToe> player : players) {
if (player instanceof OnlinePlayer<BitboardTicTacToe>){
thread = new OnlineThreadBehaviour<>(game);
}
}
super(new TicTacToeBitCanvas(), game, thread , "TicTacToe");
}
}

View File

@@ -1,10 +1,11 @@
package org.toop.app.widget; package org.toop.app.widget;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.resources.ImageAsset; import org.toop.framework.resource.resources.ImageAsset;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.awt.*;
import java.io.File; import java.io.File;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -22,33 +23,40 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.util.StringConverter; import javafx.util.StringConverter;
public final class Primitive { public final class Primitive {
public static Text header(String key) { public static Text header(String key, boolean localize) {
var header = new Text(); var header = new Text();
header.getStyleClass().add("header"); header.getStyleClass().add("header");
if (!key.isEmpty()) { if (!key.isEmpty()) {
header.setText(AppContext.getString(key)); if (localize) header.setText(AppContext.getString(key)); else header.setText(key);
header.textProperty().bind(AppContext.bindToKey(key)); header.textProperty().bind(AppContext.bindToKey(key, localize));
} }
return header; return header;
} }
public static Text text(String key) { public static Text header(String key) {
return header(key, true);
}
public static Text text(String key, boolean localize) {
var text = new Text(); var text = new Text();
text.getStyleClass().add("text"); text.getStyleClass().add("text");
if (!key.isEmpty()) { if (!key.isEmpty()) {
text.setText(AppContext.getString(key)); if (localize) text.setText(AppContext.getString(key)); else text.setText(key);
text.textProperty().bind(AppContext.bindToKey(key)); text.textProperty().bind(AppContext.bindToKey(key, localize));
} }
return text; return text;
} }
public static ImageView image(File file) { public static Text text(String key) {
ImageAsset imageAsset = new ImageAsset(file); return text(key, true);
}
public static ImageView image(ImageAsset imageAsset) {
ImageView imageView = new ImageView(imageAsset.getImage()); ImageView imageView = new ImageView(imageAsset.getImage());
imageView.getStyleClass().add("image"); imageView.getStyleClass().add("image");
imageView.setPreserveRatio(true); imageView.setPreserveRatio(true);
@@ -57,30 +65,36 @@ public final class Primitive {
return imageView; return imageView;
} }
public static Button button(String key, Runnable onAction) { public static Button button(String key, Runnable onAction, boolean localize) {
var button = new Button(); var button = new Button();
button.getStyleClass().add("button"); button.getStyleClass().add("button");
if (!key.isEmpty()) { if (!key.isEmpty()) {
button.setText(AppContext.getString(key)); if (localize) button.setText(AppContext.getString(key)); else button.setText(key);
button.textProperty().bind(AppContext.bindToKey(key)); button.textProperty().bind(AppContext.bindToKey(key, localize));
} }
if (onAction != null) { if (onAction != null) {
button.setOnAction(_ -> button.setOnAction(_ -> {
onAction.run()); onAction.run();
playButtonSound();
});
} }
return button; return button;
} }
public static TextField input(String promptKey, String text, Consumer<String> onValueChanged) { public static Button button(String key, Runnable onAction) {
return button(key, onAction, true);
}
public static TextField input(String promptKey, String text, Consumer<String> onValueChanged, boolean localize) {
var input = new TextField(); var input = new TextField();
input.getStyleClass().add("input"); input.getStyleClass().add("input");
if (!promptKey.isEmpty()) { if (!promptKey.isEmpty()) {
input.setPromptText(AppContext.getString(promptKey)); if (localize) input.setPromptText(AppContext.getString(promptKey)); else input.setPromptText(promptKey);
input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); input.promptTextProperty().bind(AppContext.bindToKey(promptKey, localize));
} }
input.setText(text); input.setText(text);
@@ -93,6 +107,10 @@ public final class Primitive {
return input; return input;
} }
public static TextField input(String promptKey, String text, Consumer<String> onValueChanged) {
return input(promptKey, text, onValueChanged, true);
}
public static Slider slider(int min, int max, int value, Consumer<Integer> onValueChanged) { public static Slider slider(int min, int max, int value, Consumer<Integer> onValueChanged) {
var slider = new Slider(); var slider = new Slider();
slider.getStyleClass().add("slider"); slider.getStyleClass().add("slider");
@@ -101,12 +119,17 @@ public final class Primitive {
slider.setMax(max); slider.setMax(max);
slider.setValue(value); slider.setValue(value);
if (onValueChanged != null) { if (onValueChanged != null) {
slider.valueProperty().addListener((_, _, newValue) -> slider.valueProperty().addListener((_, _, newValue) -> {
onValueChanged.accept(newValue.intValue())); onValueChanged.accept(newValue.intValue());
} });
}
return slider; slider.setOnMouseReleased(event -> {
playButtonSound();
});
return slider;
} }
@SafeVarargs @SafeVarargs
@@ -123,9 +146,11 @@ public final class Primitive {
} }
if (onValueChanged != null) { if (onValueChanged != null) {
choice.valueProperty().addListener((_, _, newValue) -> choice.valueProperty().addListener((_, _, newValue) -> {
onValueChanged.accept(newValue)); onValueChanged.accept(newValue);
} playButtonSound();
});
}
choice.setItems(FXCollections.observableArrayList(items)); choice.setItems(FXCollections.observableArrayList(items));
@@ -177,4 +202,8 @@ public final class Primitive {
return vbox; return vbox;
} }
private static void playButtonSound() {
new EventFlow().addPostEvent(new AudioEvents.ClickButton()).postEvent();
}
} }

View File

@@ -0,0 +1,5 @@
package org.toop.app.widget;
public interface Updatable {
void update();
}

View File

@@ -2,19 +2,27 @@ package org.toop.app.widget;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public interface Widget { public interface Widget {
Logger logger = LogManager.getLogger(Widget.class);
Node getNode(); Node getNode();
default void show(Pos position) { default void show(Pos position) {
logger.debug("Showing Widget: {} at position: {}", this.getNode(), position.toString());
WidgetContainer.add(position, this); WidgetContainer.add(position, this);
} }
default void hide() { default void hide() {
logger.debug("Hiding Widget: {}", this.getNode());
WidgetContainer.remove(this); WidgetContainer.remove(this);
} }
default void replace(Pos position, Widget widget) { default void replace(Pos position, Widget widget) {
logger.debug("Replacing Widget: {}, with widget: {}, to position: {}",
this.getNode(), widget.getNode(), position.toString());
widget.show(position); widget.show(position);
hide(); hide();
} }

View File

@@ -1,5 +1,7 @@
package org.toop.app.widget; package org.toop.app.widget;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import org.toop.app.widget.complex.PopupWidget; import org.toop.app.widget.complex.PopupWidget;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
@@ -7,6 +9,10 @@ import javafx.application.Platform;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public final class WidgetContainer { public final class WidgetContainer {
private static StackPane root; private static StackPane root;
private static ViewWidget currentView; private static ViewWidget currentView;
@@ -38,7 +44,7 @@ public final class WidgetContainer {
root.getChildren().addFirst(view.getNode()); root.getChildren().addFirst(view.getNode());
currentView = view; currentView = view;
} else if (widget instanceof PopupWidget popup) { } else if (widget instanceof PopupWidget popup) {
currentView.add(Pos.CENTER, popup); currentView.add(Pos.CENTER, (Widget) popup);
} else { } else {
root.getChildren().add(widget.getNode()); root.getChildren().add(widget.getNode());
} }
@@ -52,13 +58,61 @@ public final class WidgetContainer {
Platform.runLater(() -> { Platform.runLater(() -> {
if (widget instanceof PopupWidget popup) { if (widget instanceof PopupWidget popup) {
currentView.remove(popup); currentView.remove((Widget) popup);
} else { } else {
root.getChildren().remove(widget.getNode()); root.getChildren().remove(widget.getNode());
} }
}); });
} }
public static void remove(Class<? extends Widget> widgetClass) {
if (root == null || currentView == null) return;
Platform.runLater(() ->
currentView.getChildren().removeIf(widget -> widget.getClass().isAssignableFrom(widgetClass))
);
}
public static void removeFirst(Class<? extends Widget> widgetClass) {
if (root == null || currentView == null) return;
Platform.runLater(() -> {
for (Node widget : currentView.getChildren()) {
if (widgetClass.isAssignableFrom(widget.getClass())) {
currentView.getChildren().remove(widget);
break;
}
}
});
}
public static List<Widget> find(Class<? extends Widget> widgetClass) {
if (root == null || currentView == null) return null;
return getAllWidgets()
.stream()
.filter(widget -> widget.getClass().isAssignableFrom(widgetClass))
.toList();
}
public static List<Widget> find(Predicate<Widget> predicate) {
if (root == null || currentView == null) return null;
return getAllWidgets()
.stream()
.filter(predicate)
.toList();
}
public static Widget findFirst(Class<? extends Widget> widgetClass) {
if (root == null || currentView == null) return null;
return getAllWidgets()
.stream()
.filter(widget -> widget.getClass().isAssignableFrom(widgetClass))
.findFirst().orElse(null);
}
public static ViewWidget getCurrentView() { public static ViewWidget getCurrentView() {
return currentView; return currentView;
} }
@@ -74,4 +128,22 @@ public final class WidgetContainer {
currentView = view; currentView = view;
}); });
} }
public static List<Widget> getAllWidgets() {
final List<Widget> children = new ArrayList<>();
for (var child : root.getChildren()) {
if (child instanceof Widget widget) {
children.add(widget);
}
}
for (var child : currentView.getNode().getChildren()) {
if (child instanceof Widget widget) {
children.add(widget);
}
}
return children;
}
} }

View File

@@ -0,0 +1,163 @@
package org.toop.app.widget.complex;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import org.toop.app.widget.Primitive;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
public class LoadingWidget extends ViewWidget implements Update { // TODO make of widget type
private final Text loadingText; // TODO Make changeable
private final ProgressIndicator progressBar;
private final AtomicBoolean successTriggered = new AtomicBoolean(false);
private final AtomicBoolean failureTriggered = new AtomicBoolean(false);
private Runnable success = () -> {};
private Runnable failure = () -> {};
private int maxAmount;
private int minAmount;
private int amount;
private Callable<Boolean> successTrigger = () -> (amount >= maxAmount);
private Callable<Boolean> failureTrigger = () -> (amount < minAmount);
private float percentage = 0.0f;
private boolean isInfinite = false;
/**
*
* Widget that shows a loading bar.
*
* @param loadingText Text above the loading bar.
* @param minAmount The minimum amount.
* @param startAmount The starting amount.
* @param maxAmount The max amount.
*/
public LoadingWidget(Text loadingText, int minAmount, int startAmount, int maxAmount, boolean infinite, boolean circle) {
isInfinite = infinite;
this.maxAmount = maxAmount;
this.minAmount = minAmount;
amount = startAmount;
this.loadingText = loadingText;
progressBar = circle ? new ProgressIndicator() : new ProgressBar();
VBox box = Primitive.vbox(this.loadingText, progressBar);
progressBar.getStyleClass().add("loading-progress-bar");
add(Pos.CENTER, box);
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
public void setAmount(int amount) throws Exception {
this.amount = amount;
update();
}
public int getMaxAmount() {
return maxAmount;
}
public int getAmount() {
return amount;
}
public float getPercentage() {
return percentage;
}
public boolean isTriggered() {
return (failureTriggered.get() || successTriggered.get());
}
public ProgressIndicator getProgressBar() {
return progressBar;
}
/**
* What to do when success is triggered.
* @param onSuccess The lambda that gets run on success.
*/
public void setOnSuccess(Runnable onSuccess) {
success = onSuccess;
}
/**
* What to do when failure is triggered.
* @param onFailure The lambda that gets run on failure.
*/
public void setOnFailure(Runnable onFailure) {
failure = onFailure;
}
/**
* The trigger to activate onSuccess.
* @param trigger The lambda that triggers onSuccess.
*/
public void setSuccessTrigger(Callable<Boolean> trigger) {
successTrigger = trigger;
}
/**
* The trigger to activate onFailure.
* @param trigger The lambda that triggers onFailure.
*/
public void setFailureTrigger(Callable<Boolean> trigger) {
failureTrigger = trigger;
}
/**
* Forcefully trigger success.
*/
public void triggerSuccess() {
if (successTriggered.compareAndSet(false, true)) {
Platform.runLater(() -> {
if (success != null) success.run();
});
}
}
/**
* Forcefully trigger failure.
*/
public void triggerFailure() {
if (failureTriggered.compareAndSet(false, true)) {
Platform.runLater(() -> {
if (failure != null) failure.run();
});
}
}
@Override
public void update() throws Exception { // TODO Better exception
if (successTriggered.get() || failureTriggered.get()) { // If already triggered, throw exception.
throw new RuntimeException();
}
if (successTrigger.call()) {
triggerSuccess();
this.remove((Node) this);
return;
} else if (failureTrigger.call()) {
triggerFailure();
this.remove((Node) this);
return;
}
if (maxAmount != 0) {
percentage = (float) amount / maxAmount;
}
if (!isInfinite) {
progressBar.setProgress(percentage);
}
}
}

View File

@@ -5,10 +5,13 @@ import org.toop.app.widget.Primitive;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class PlayerInfoWidget { public class PlayerInfoWidget {
private final GameInformation.Player information; private final GameInformation.Player information;
private final VBox container; private final VBox container;
private Text playerName;
private boolean hasSet;
public PlayerInfoWidget(GameInformation.Player information) { public PlayerInfoWidget(GameInformation.Player information) {
this.information = information; this.information = information;
@@ -16,6 +19,7 @@ public class PlayerInfoWidget {
buildToggle().getNode(), buildToggle().getNode(),
buildContent() buildContent()
); );
this.playerName = null;
} }
private ToggleWidget buildToggle() { private ToggleWidget buildToggle() {
@@ -33,51 +37,69 @@ public class PlayerInfoWidget {
} }
private Node buildContent() { private Node buildContent() {
if (information.isHuman) { if (information.isHuman) {
var nameInput = new LabeledInputWidget( var nameInput = new LabeledInputWidget(
"name", "name",
"enter-your-name", "enter-your-name",
information.name, information.name,
newName -> information.name = newName newName -> information.name = newName
); );
return nameInput.getNode(); return nameInput.getNode();
} else { } else {
if (information.name == null || information.name.isEmpty()) { var AIBox = Primitive.vbox(
information.name = "Pism Bot"; makeAIButton(0, 1, "zwartepiet"),
} makeAIButton(2, 1, "sinterklaas"),
makeAIButton(9, 1, "santa")
);
var playerName = Primitive.text(""); this.playerName = Primitive.text("");
playerName.setText(information.name); playerName.setText(information.name);
var nameDisplay = Primitive.vbox( var nameDisplay = Primitive.vbox(
Primitive.text("name"), Primitive.text("name"),
playerName playerName
); );
var difficultySlider = new LabeledSliderWidget( if (!hasSet) {
"computer-difficulty", doDefault();
0, 5, hasSet = true;
information.computerDifficulty, }
newVal -> information.computerDifficulty = newVal
);
var thinkTimeSlider = new LabeledSliderWidget( return Primitive.vbox(
"computer-think-time", AIBox,
0, 5, nameDisplay
information.computerThinkTime, );
newVal -> information.computerThinkTime = newVal
);
return Primitive.vbox( }
nameDisplay, }
difficultySlider.getNode(),
thinkTimeSlider.getNode()
);
}
}
public Node getNode() { public Node getNode() {
return container; return container;
} }
private Node makeAIButton(int depth, int thinktime, String name) {
return Primitive.button(name, () -> {
information.name = getName(name);
information.computerDifficulty = depth;
information.computerThinkTime = thinktime;
this.playerName.setText(getName(name));
});
}
private String getName(String name) {
return switch (name) {
case "sinterklaas" -> "Sint. R. Klaas";
case "zwartepiet" -> "Zwarte Piet";
case "santa" -> "Santa";
default -> "Default";
};
}
private void doDefault() {
information.name = getName("zwartepiet");
information.computerDifficulty = 0;
information.computerThinkTime = 1;
this.playerName.setText(getName("zwartepiet"));
}
} }

View File

@@ -1,7 +1,21 @@
package org.toop.app.widget.complex; package org.toop.app.widget.complex;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
public abstract class PopupWidget extends StackWidget { public abstract class PopupWidget extends StackWidget {
private final Button popButton;
public PopupWidget() { public PopupWidget() {
super("bg-popup"); super("bg-popup");
popButton = new Button("X");
popButton.setOnAction(_ -> hide());
add(Pos.TOP_RIGHT, popButton);
}
protected void setOnPop(Runnable onPop) {
popButton.setOnAction(_ -> onPop.run());
} }
} }

View File

@@ -7,22 +7,19 @@ import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
public abstract class StackWidget implements Widget { public abstract class StackWidget extends StackPane implements Widget {
private final StackPane container;
public StackWidget(String cssClass) { public StackWidget(String cssClass) {
container = new StackPane(); this.getStyleClass().add(cssClass);
container.getStyleClass().add(cssClass);
} }
public void add(Pos position, Node node) { public void add(Pos position, Node node) {
Platform.runLater(() -> { Platform.runLater(() -> {
if (container.getChildren().contains(node)) { if (this.getChildren().contains(node)) {
return; return;
} }
StackPane.setAlignment(node, position); StackPane.setAlignment(node, position);
container.getChildren().add(node); this.getChildren().add(node);
}); });
} }
@@ -32,7 +29,7 @@ public abstract class StackWidget implements Widget {
public void remove(Node node) { public void remove(Node node) {
Platform.runLater(() -> { Platform.runLater(() -> {
container.getChildren().remove(node); this.getChildren().remove(node);
}); });
} }
@@ -41,7 +38,7 @@ public abstract class StackWidget implements Widget {
} }
@Override @Override
public Node getNode() { public StackPane getNode() {
return container; return this;
} }
} }

View File

@@ -2,6 +2,8 @@ package org.toop.app.widget.complex;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.Widget; import org.toop.app.widget.Widget;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -29,8 +31,9 @@ public class ToggleWidget implements Widget {
state = !state; state = !state;
updateText(); updateText();
if (onToggle != null) { if (onToggle != null) {
onToggle.accept(state); onToggle.accept(state);
} new EventFlow().addPostEvent(new AudioEvents.ClickButton()).postEvent(); // TODO FIX PRIMITIVES
}
}); });
container = Primitive.vbox(button); container = Primitive.vbox(button);

View File

@@ -0,0 +1,5 @@
package org.toop.app.widget.complex;
public interface Update {
void update() throws Exception;
}

View File

@@ -16,8 +16,18 @@ public abstract class ViewWidget extends StackWidget {
replace(Pos.CENTER, view); replace(Pos.CENTER, view);
} }
public void transitionNext(ViewWidget view) { public void transitionNext(ViewWidget view) {
view.previous = this; transitionNext(view, false);
}
public void transitionNext(ViewWidget view, boolean aware) {
if (aware && this.getClass().equals(view.getClass())) {
view.previous = this.previous;
}
else{
view.previous = this;
}
replace(Pos.CENTER, view); replace(Pos.CENTER, view);
var backButton = Primitive.button("back", () -> { var backButton = Primitive.button("back", () -> {
@@ -27,6 +37,19 @@ public abstract class ViewWidget extends StackWidget {
view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton));
} }
public void transitionNextCustom(ViewWidget view, String key, Runnable runnable) {
view.previous = this;
replace(Pos.CENTER, view);
var customButton = Primitive.button(key, () -> {
runnable.run();
view.transitionPrevious();
});
view.add(Pos.BOTTOM_LEFT, Primitive.vbox(customButton));
}
public void transitionPrevious() { public void transitionPrevious() {
if (previous == null) { if (previous == null) {
return; return;
@@ -36,6 +59,38 @@ public abstract class ViewWidget extends StackWidget {
previous = null; previous = null;
} }
public void removeIndexFromPreviousChain(int index) {
ViewWidget view = this;
while (index > 0 && view != null) {
index--;
if (index == 0) {
if (view.previous != null && view.previous.previous != null) {
view.previous = view.previous.previous;
}
}
view = view.previous;
}
}
public void removeViewFromPreviousChain(ViewWidget view) {
ViewWidget prev = previous;
int index = 0;
while (prev != null) {
index++;
if (prev == view) {
removeIndexFromPreviousChain(index);
break;
}
prev = prev.previous;
}
}
public void reload(ViewWidget view) { public void reload(ViewWidget view) {
view.previous = previous; view.previous = previous;
replace(Pos.CENTER, view); replace(Pos.CENTER, view);

View File

@@ -22,7 +22,7 @@ public class SongDisplay extends VBox implements Widget {
public SongDisplay() { public SongDisplay() {
new EventFlow() new EventFlow()
.listen(this::updateTheSong); .listen(AudioEvents.PlayingMusic.class, this::updateTheSong, false);
setAlignment(Pos.CENTER); setAlignment(Pos.CENTER);
setMaxHeight(Region.USE_PREF_SIZE); setMaxHeight(Region.USE_PREF_SIZE);
@@ -33,7 +33,7 @@ public class SongDisplay extends VBox implements Widget {
songTitle.getStyleClass().add("song-title"); songTitle.getStyleClass().add("song-title");
progressBar = new ProgressBar(0); progressBar = new ProgressBar(0);
progressBar.getStyleClass().add("progress-bar"); progressBar.getStyleClass().add("loading-progress-bar");
progressText = new Text("0:00/0:00"); progressText = new Text("0:00/0:00");
progressText.getStyleClass().add("progress-text"); progressText.getStyleClass().add("progress-text");

View File

@@ -8,6 +8,7 @@ import org.toop.app.widget.complex.PopupWidget;
import java.util.function.Consumer; import java.util.function.Consumer;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import org.toop.local.AppContext;
public final class ChallengePopup extends PopupWidget { public final class ChallengePopup extends PopupWidget {
private final GameInformation.Player playerInformation; private final GameInformation.Player playerInformation;
@@ -28,19 +29,22 @@ public final class ChallengePopup extends PopupWidget {
private void setupLayout() { private void setupLayout() {
var challengeText = Primitive.text("you-were-challenged-by"); var challengeText = Primitive.text("you-were-challenged-by");
var challengerHeader = Primitive.header(""); var challengerHeader = Primitive.header(challenger, false);
challengerHeader.setText(challenger);
var gameText = Primitive.text("to-a-game-of"); var toAGameOfText = Primitive.text("to-a-game-of");
gameText.setText(gameText.getText() + " " + game); var gameHeader = Primitive.header(game, false);
var acceptButton = Primitive.button("accept", () -> onAccept.accept(playerInformation)); var acceptButton = Primitive.button("accept", () -> {
onAccept.accept(playerInformation);
this.hide();
});
var denyButton = Primitive.button("deny", () -> hide()); var denyButton = Primitive.button("deny", () -> hide());
var leftSection = Primitive.vbox( var leftSection = Primitive.vbox(
challengeText, challengeText,
challengerHeader, challengerHeader,
gameText, toAGameOfText,
gameHeader,
Primitive.separator(), Primitive.separator(),
Primitive.hbox( Primitive.hbox(
acceptButton, acceptButton,

View File

@@ -0,0 +1,31 @@
package org.toop.app.widget.popup;
import javafx.geometry.Pos;
import org.toop.app.App;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.complex.ConfirmWidget;
import org.toop.app.widget.complex.PopupWidget;
import org.toop.app.widget.view.OptionsView;
public class EscapePopup extends PopupWidget {
public EscapePopup() {
var con = Primitive.button("Continue", this::hide, false); // TODO, localize
var qui = Primitive.button("quit", () -> {
hide();
WidgetContainer.add(Pos.CENTER, new QuitPopup());
});
if (!(WidgetContainer.getCurrentView().getClass().isAssignableFrom(OptionsView.class))) {
var opt = Primitive.button("options", () -> {
hide();
WidgetContainer.getCurrentView().transitionNext(new OptionsView());
});
add(Pos.CENTER, Primitive.vbox(con, opt, qui));
} else {
add(Pos.CENTER, Primitive.vbox(con, qui));
}
}
}

View File

@@ -7,18 +7,17 @@ import org.toop.local.AppContext;
import javafx.geometry.Pos; import javafx.geometry.Pos;
public final class GameOverPopup extends PopupWidget { public final class GameOverPopup extends PopupWidget {
public GameOverPopup(boolean iWon, String winner) { public GameOverPopup(boolean winOrTie, String winner) {
var confirmWidget = new ConfirmWidget("game-over"); var confirmWidget = new ConfirmWidget("game-over");
if (winner.isEmpty()) { if (winOrTie) {
confirmWidget.setMessage(AppContext.getString("the-game-ended-in-a-draw")); confirmWidget.setMessage(winner + " won the game!");
} else if (iWon) { }
confirmWidget.setMessage(AppContext.getString("you-win")); else{
} else { confirmWidget.setMessage("It was a tie!");
confirmWidget.setMessage(AppContext.getString("you-lost-against") + ": " + winner); }
}
confirmWidget.addButton("ok", () -> hide()); confirmWidget.addButton("ok", this::hide);
add(Pos.CENTER, confirmWidget); add(Pos.CENTER, confirmWidget);
} }

View File

@@ -15,10 +15,13 @@ public class QuitPopup extends PopupWidget {
}); });
confirmWidget.addButton("no", () -> { confirmWidget.addButton("no", () -> {
App.stopQuit();
hide(); hide();
}); });
add(Pos.CENTER, confirmWidget); add(Pos.CENTER, confirmWidget);
setOnPop(() -> {
hide();
});
} }
} }

View File

@@ -34,7 +34,7 @@ public final class SendChallengePopup extends PopupWidget {
// --- Left side: challenge text and buttons --- // --- Left side: challenge text and buttons ---
var challengeText = Primitive.text("challenge"); var challengeText = Primitive.text("challenge");
var opponentHeader = Primitive.header(opponent); var opponentHeader = Primitive.header(opponent, false);
var gameText = Primitive.text("to-a-game-of"); var gameText = Primitive.text("to-a-game-of");
@@ -61,7 +61,7 @@ public final class SendChallengePopup extends PopupWidget {
var sendButton = Primitive.button( var sendButton = Primitive.button(
"send", "send",
() -> onSend.accept(playerInformation, gameChoice.getValue()) () -> { onSend.accept(playerInformation, gameChoice.getValue()); this.hide(); }
); );
var cancelButton = Primitive.button("cancel", () -> hide()); var cancelButton = Primitive.button("cancel", () -> hide());

View File

@@ -1,60 +1,115 @@
package org.toop.app.widget.tutorial; package org.toop.app.widget.tutorial;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import org.apache.maven.surefire.shared.lang3.tuple.ImmutablePair;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.Updatable;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.complex.PopupWidget;
import javafx.scene.control.Button; import org.toop.framework.resource.resources.ImageAsset;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.io.File; import java.util.List;
public class BaseTutorialWidget extends ViewWidget { /**
* A widget base for all the tutorial widgets.
*
* <p>Usage example:
*
* <pre>{@code
* public class Connect4TutorialWidget extends BaseTutorialWidget {
* public Connect4TutorialWidget(Runnable nextScreen) {
* super(List.of(
* new ImmutablePair<>("connect4.1", ResourceManager.get("connect41.png")),
* new ImmutablePair<>("connect4.2", ResourceManager.get("connect42.png"))
* ), nextScreen);
* }
* }</pre>
*/
public class BaseTutorialWidget extends PopupWidget implements Updatable {
private TState state; private final Text tutorialText;
private Text tutorialText; private final ImageView imagery;
private Button previousButton; private final Button previousButton;
private Button nextButton; private final Button nextButton;
private Button noButton; private final List<ImmutablePair<String, ImageAsset>> pages;
private Button yesButton; private final Runnable nextScreen;
private Button neverButton;
private ImageView imagery;
public BaseTutorialWidget(String key, Runnable onNo, Runnable onYes, Runnable onNever) { private int pageIndex = 0;
System.out.println("Trying to initialize...");
this.tutorialText = Primitive.text(key);
this.yesButton = Primitive.button("ok", () -> onYes.run());
this.noButton = Primitive.button("no", () -> onNo.run());
this.neverButton = Primitive.button("never", () -> onNever.run());
var a = Primitive.hbox(yesButton, noButton, neverButton);
add(Pos.CENTER, Primitive.vbox(tutorialText, a));
}
public BaseTutorialWidget(TState state, String key, Runnable onPrevious, Runnable onNext) { public BaseTutorialWidget(List<ImmutablePair<String, ImageAsset>> pages, Runnable nextScreen) {
this.state = state; this.tutorialText = Primitive.text(pages.getFirst().getKey());
this.tutorialText = Primitive.text(key); this.imagery = Primitive.image(pages.getFirst().getValue());
this.previousButton = Primitive.button("<", () -> onPrevious.run());
this.nextButton = Primitive.button(">", () -> onNext.run()); this.pages = pages;
var w = Primitive.hbox(previousButton, nextButton); this.nextScreen = nextScreen;
add(Pos.CENTER, Primitive.vbox(tutorialText, w));
} previousButton = Primitive.button("goback", () -> { update(false); this.hide(); });
nextButton = Primitive.button(">", () -> update(true));
var w = Primitive.hbox(
previousButton,
nextButton
);
var x = Primitive.vbox(imagery, tutorialText);
public BaseTutorialWidget(TState state, String key, File image, Runnable onPrevious, Runnable onNext) {
this.state = state;
this.imagery = Primitive.image(image);
this.tutorialText = Primitive.text(key);
this.previousButton = Primitive.button("<", () -> onPrevious.run());
this.nextButton = Primitive.button(">", () -> onNext.run());
var w = Primitive.hbox(previousButton, nextButton);
var x = Primitive.vbox(imagery, tutorialText);
add(Pos.CENTER, Primitive.vbox(x, w)); add(Pos.CENTER, Primitive.vbox(x, w));
WidgetContainer.add(Pos.CENTER, this);
} }
public void update(String key, File image) { @Override
public void update() {
update(true);
}
// TODO Refactor if statements to make code easier to read.
public void update(boolean next) {
pageIndex = next ? pageIndex + 1 : pageIndex - 1;
if (pageIndex >= pages.size()) {
pageIndex--;
return;
} else if (pageIndex < 0) {
pageIndex++;
return;
}
if (pageIndex == pages.size()-1) {
nextButton.textProperty().unbind();
nextButton.setText(AppContext.getString("startgame"));
nextButton.setOnAction((_) -> {
this.hide();
nextScreen.run();
});
} else {
nextButton.textProperty().unbind();
nextButton.setText(AppContext.getString(">"));
nextButton.setOnAction((_) -> this.update(true));
}
if (pageIndex == 0) {
previousButton.textProperty().unbind();
previousButton.setText(AppContext.getString("goback"));
previousButton.setOnAction((_) -> this.hide());
} else {
previousButton.textProperty().unbind();
previousButton.setText(AppContext.getString("<"));
previousButton.setOnAction((_) -> this.update(false));
}
var currentPage = pages.get(pageIndex);
var text = currentPage.getKey();
var image = currentPage.getValue();
tutorialText.textProperty().unbind(); tutorialText.textProperty().unbind();
tutorialText.setText(AppContext.getString(key)); tutorialText.setText(AppContext.getString(text));
imagery.setImage(Primitive.image(image).getImage()); imagery.setImage(Primitive.image(image).getImage());
} }
} }

View File

@@ -1,39 +1,15 @@
package org.toop.app.widget.tutorial; package org.toop.app.widget.tutorial;
import javafx.geometry.Pos; import org.apache.maven.surefire.shared.lang3.tuple.ImmutablePair;
import org.toop.app.widget.complex.ViewWidget; import org.toop.framework.resource.ResourceManager;
import java.io.File; import java.util.List;
public class Connect4TutorialWidget extends ViewWidget { public class Connect4TutorialWidget extends BaseTutorialWidget {
private TState state; public Connect4TutorialWidget(Runnable nextScreen) {
private String[] keys = {"connect4.1", "connect4.2"}; super(List.of(
private File[] images = {new File("app/src/main/resources/assets/images/connect41.png"), new File("app/src/main/resources/assets/images/connect42.png")}; new ImmutablePair<>("connect4.1", ResourceManager.get("connect41.png")),
private BaseTutorialWidget tutorialWidget; new ImmutablePair<>("connect4.2", ResourceManager.get("connect42.png"))
), nextScreen);
public Connect4TutorialWidget() {
this.state = new TState(keys.length);
tutorialWidget = new BaseTutorialWidget(
state,
keys[state.getCurrent()],
images[state.getCurrent()],
() -> {
if (state.hasPrevious()) {
state.previous();
update();
}
},
() -> {
if (state.hasNext()) {
state.next();
update();
}
}
);
add(Pos.CENTER, tutorialWidget);
}
private void update() {
tutorialWidget.update(keys[state.getCurrent()], images[state.getCurrent()]);
} }
} }

View File

@@ -1,39 +1,17 @@
package org.toop.app.widget.tutorial; package org.toop.app.widget.tutorial;
import javafx.geometry.Pos; import org.apache.maven.surefire.shared.lang3.tuple.ImmutablePair;
import org.toop.app.widget.complex.ViewWidget; import org.toop.framework.resource.ResourceManager;
import java.io.File; import java.util.List;
public class ReversiTutorialWidget extends ViewWidget { public class ReversiTutorialWidget extends BaseTutorialWidget {
private TState state; public ReversiTutorialWidget(Runnable nextScreen) {
private String[] keys = {"reversi1", "reversi2", "reversi3", "reversi4"}; super(List.of(
private File[] images = {new File("app/src/main/resources/assets/images/reversi1.png"), new File("app/src/main/resources/assets/images/reversi2.png"), new File("app/src/main/resources/assets/images/cat.jpg"), new File("app/src/main/resources/assets/images/cat.jpg")}; new ImmutablePair<>("reversi1", ResourceManager.get("reversi1.png")),
private BaseTutorialWidget tutorialWidget; new ImmutablePair<>("reversi2", ResourceManager.get("reversi2.png")),
new ImmutablePair<>("reversi3", ResourceManager.get("cat.jpg")),
public ReversiTutorialWidget() { new ImmutablePair<>("reversi4", ResourceManager.get("cat.jpg"))
this.state = new TState(keys.length); ), nextScreen);
tutorialWidget = new BaseTutorialWidget(
state,
keys[state.getCurrent()],
images[state.getCurrent()],
() -> {
if (state.hasPrevious()) {
state.previous();
update();
}
},
() -> {
if (state.hasNext()) {
state.next();
update();
}
}
);
add(Pos.CENTER, tutorialWidget);
}
private void update() {
tutorialWidget.update(keys[state.getCurrent()], images[state.getCurrent()]);
} }
} }

View File

@@ -0,0 +1,22 @@
package org.toop.app.widget.tutorial;
import javafx.geometry.Pos;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.complex.PopupWidget;
import org.toop.local.AppSettings;
public class ShowEnableTutorialWidget extends PopupWidget {
public ShowEnableTutorialWidget(Runnable tutorial, Runnable nextScreen, Runnable appSettingsSetter) {
var a = Primitive.hbox(
Primitive.button("ok", () -> { appSettingsSetter.run(); tutorial.run(); this.hide(); }),
Primitive.button("no", () -> { appSettingsSetter.run(); nextScreen.run(); this.hide(); }),
Primitive.button("never", () -> { AppSettings.getSettings().setTutorialFlag(false); nextScreen.run(); this.hide(); })
);
var txt = Primitive.text("tutorial");
add(Pos.CENTER, Primitive.vbox(txt, a));
WidgetContainer.add(Pos.CENTER, this);
}
}

View File

@@ -1,44 +0,0 @@
package org.toop.app.widget.tutorial;
public class TState {
private int current;
private int total;
public TState(int total) {
this.total = total;
this.current = 0;
}
public int getCurrent() {
return current;
}
public void setCurrent(int current) {
this.current = current;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public void next() {
current = current + 1;
}
public void previous() {
current = current - 1;
}
public boolean hasNext() {
return current < total - 1;
}
public boolean hasPrevious() {
return current > 0;
}
}

View File

@@ -1,42 +1,16 @@
package org.toop.app.widget.tutorial; package org.toop.app.widget.tutorial;
import javafx.geometry.Pos; import org.apache.maven.surefire.shared.lang3.tuple.ImmutablePair;
import org.toop.app.widget.complex.ViewWidget; import org.toop.framework.resource.ResourceManager;
import java.io.File;
public class TicTacToeTutorialWidget extends ViewWidget { import java.util.List;
private TState state; public class TicTacToeTutorialWidget extends BaseTutorialWidget {
private String[] keys = {"tictactoe1", "tictactoe2"}; public TicTacToeTutorialWidget(Runnable nextScreen) {
private File[] images = { super(List.of(
new File("app/src/main/resources/assets/images/tictactoe1.png"), new ImmutablePair<>("tictactoe1", ResourceManager.get("tictactoe1.png")),
new File("app/src/main/resources/assets/images/tictactoe2.png") new ImmutablePair<>("tictactoe2", ResourceManager.get("tictactoe2.png"))
}; ), nextScreen);
private BaseTutorialWidget tutorialWidget;
public TicTacToeTutorialWidget() {
this.state = new TState(keys.length);
tutorialWidget = new BaseTutorialWidget(
state,
keys[state.getCurrent()],
images[state.getCurrent()],
() -> {
if (state.hasPrevious()) {
state.previous();
update();
}
},
() -> {
if (state.hasNext()) {
state.next();
update();
}
}
);
add(Pos.CENTER, tutorialWidget);
} }
private void update() {
tutorialWidget.update(keys[state.getCurrent()], images[state.getCurrent()]);
}
} }

View File

@@ -1,34 +1,44 @@
package org.toop.app.widget.view; package org.toop.app.widget.view;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.GameOverPopup; import org.toop.app.widget.popup.GameOverPopup;
import java.util.function.Consumer; import java.util.function.Consumer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import org.toop.app.widget.tutorial.BaseTutorialWidget;
import org.toop.app.widget.tutorial.Connect4TutorialWidget; import org.toop.app.widget.tutorial.Connect4TutorialWidget;
import org.toop.app.widget.tutorial.ReversiTutorialWidget; import org.toop.app.widget.tutorial.ReversiTutorialWidget;
import org.toop.app.widget.tutorial.TicTacToeTutorialWidget; import org.toop.app.widget.tutorial.TicTacToeTutorialWidget;
import org.toop.local.AppContext;
public final class GameView extends ViewWidget { public final class GameView extends ViewWidget {
private final Text currentPlayerHeader; private final Text playerHeader;
private final Text currentMoveHeader; private final Text turnHeader;
private final Text nextPlayerHeader; private final Text player1Header;
private final Text player2Header;
private Circle player1Icon;
private Circle player2Icon;
private final Button forfeitButton; private final Button forfeitButton;
private final Button exitButton; private final Button exitButton;
private final Button tutorialButton; private final Button tutorialButton;
private final TextField chatInput; private final TextField chatInput;
private final Text keyThingy;
private boolean hasSet = false;
public GameView(Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, String gameType) { public GameView(Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, String gameType) {
currentPlayerHeader = Primitive.header(""); playerHeader = Primitive.header("");
currentMoveHeader = Primitive.header(""); turnHeader = Primitive.header("");
nextPlayerHeader = Primitive.header(""); keyThingy = Primitive.text("turnof");
player1Header = Primitive.header("");
player2Header = Primitive.header("");
player1Icon = new Circle();
player2Icon = new Circle();
if (onForfeit != null) { if (onForfeit != null) {
forfeitButton = Primitive.button("forfeit", () -> onForfeit.run()); forfeitButton = Primitive.button("forfeit", () -> onForfeit.run());
@@ -53,40 +63,24 @@ public final class GameView extends ViewWidget {
switch(gameType) { switch(gameType) {
case "TicTacToe": case "TicTacToe":
this.tutorialButton = Primitive.button("tutorialstring", () -> { this.tutorialButton = Primitive.button("tutorialstring", () -> new TicTacToeTutorialWidget(() -> {})); break;
transitionNext(new TicTacToeTutorialWidget());
});
break;
case "Reversi": case "Reversi":
this.tutorialButton = Primitive.button("tutorialstring", () -> { this.tutorialButton = Primitive.button("tutorialstring", () -> new ReversiTutorialWidget(() -> {})); break;
transitionNext(new ReversiTutorialWidget());
});
break;
case "Connect4": case "Connect4":
this.tutorialButton = Primitive.button("tutorialstring", () -> { this.tutorialButton = Primitive.button("tutorialstring", () -> new Connect4TutorialWidget(() -> {})); break;
transitionNext(new Connect4TutorialWidget());
});
break;
default: default:
this.tutorialButton = null; this.tutorialButton = null; break;
break;
} }
setupLayout(); setupLayout();
} }
private void setupLayout() { private void setupLayout() {
var playerInfo = Primitive.vbox( var turnInfo = Primitive.vbox(
currentPlayerHeader, turnHeader
Primitive.hbox( );
Primitive.separator(),
currentMoveHeader,
Primitive.separator()
),
nextPlayerHeader
);
add(Pos.TOP_RIGHT, playerInfo); add(Pos.TOP_CENTER, turnInfo);
var buttons = Primitive.vbox( var buttons = Primitive.vbox(
forfeitButton, forfeitButton,
@@ -104,21 +98,86 @@ public final class GameView extends ViewWidget {
} }
} }
public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) { public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer, char GameType) {
Platform.runLater(() -> { Platform.runLater(() -> {
currentPlayerHeader.setText(currentPlayer); if (!(hasSet)) {
currentMoveHeader.setText(currentMove); playerHeader.setText(currentPlayer + " vs. " + nextPlayer);
nextPlayerHeader.setText(nextPlayer); hasSet = true;
setPlayerHeaders(isMe, currentPlayer, nextPlayer, GameType);
if (isMe) { }
currentPlayerHeader.getStyleClass().add("my-turn"); //TODO idk if theres any way to check this? only EN uses 's and the rest doesnt. if theres a better way to do this pls let me know
} else { if (AppContext.getLocale().toLanguageTag().equals("en")) {
currentPlayerHeader.getStyleClass().remove("my-turn"); turnHeader.setText(currentPlayer + keyThingy.getText());
} }
}); });
} }
public void gameOver(boolean iWon, String winner) { public void gameOver(boolean iWon, String winner) {
new GameOverPopup(iWon, winner).show(Pos.CENTER); new GameOverPopup(iWon, winner).show(Pos.CENTER);
} }
private void setPlayerHeaders(boolean isMe, String currentPlayer, String nextPlayer, char GameType) {
if (GameType == 'T') {
if (isMe) {
player1Header.setText("X: " + currentPlayer);
player2Header.setText("O: " + nextPlayer);
}
else {
player1Header.setText("X: " + nextPlayer);
player2Header.setText("O: " + currentPlayer);
}
setPlayerInfoTTT();
}
else if (GameType == 'R') {
if (isMe) {
player1Header.setText(currentPlayer);
player2Header.setText(nextPlayer);
}
else {
player1Header.setText(nextPlayer);
player2Header.setText(currentPlayer);
}
setPlayerInfoReversi();
}
}
private void setPlayerInfoTTT() {
var playerInfo = Primitive.vbox(
playerHeader,
Primitive.separator(),
player1Header,
player2Header
);
add(Pos.TOP_RIGHT, playerInfo);
}
private void setPlayerInfoReversi() {
var player1box = Primitive.hbox(
player1Icon,
player1Header
);
player1box.getStyleClass().add("hboxspacing");
var player2box = Primitive.hbox(
player2Icon,
player2Header
);
player2box.getStyleClass().add("hboxspacing");
var playerInfo = Primitive.vbox(
playerHeader,
Primitive.separator(),
player1box,
player2box
);
player1Icon.setRadius(player1Header.fontProperty().map(Font::getSize).getValue());
player2Icon.setRadius(player2Header.fontProperty().map(Font::getSize).getValue());
player1Icon.setFill(Color.BLACK);
player2Icon.setFill(Color.WHITE);
add(Pos.TOP_RIGHT, playerInfo);
}
} }

View File

@@ -2,27 +2,39 @@ package org.toop.app.widget.view;
import javafx.application.Platform; import javafx.application.Platform;
import org.toop.app.GameInformation; import org.toop.app.GameInformation;
import org.toop.app.game.Connect4Game; import org.toop.app.canvas.ReversiBitCanvas;
import org.toop.app.game.ReversiGame; import org.toop.app.canvas.TicTacToeBitCanvas;
import org.toop.app.game.TicTacToeGameThread; import org.toop.app.gameControllers.GenericGameController;
import org.toop.app.gameControllers.ReversiBitController;
import org.toop.app.gameControllers.TicTacToeBitController;
import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.player.Player;
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.LocalPlayer;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.complex.PlayerInfoWidget; import org.toop.app.widget.complex.PlayerInfoWidget;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.tutorial.BaseTutorialWidget; import org.toop.app.widget.tutorial.*;
import org.toop.app.widget.tutorial.Connect4TutorialWidget; import org.toop.game.players.MiniMaxAI;
import org.toop.app.widget.tutorial.ReversiTutorialWidget; import org.toop.game.players.RandomAI;
import org.toop.app.widget.tutorial.TicTacToeTutorialWidget;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.toop.local.AppSettings; import org.toop.local.AppSettings;
import java.util.Arrays;
import java.util.Random;
public class LocalMultiplayerView extends ViewWidget { public class LocalMultiplayerView extends ViewWidget {
private final GameInformation information; private final GameInformation information;
private GameController gameController;
public LocalMultiplayerView(GameInformation.Type type) { public LocalMultiplayerView(GameInformation.Type type) {
this(new GameInformation(type)); this(new GameInformation(type));
} }
@@ -30,6 +42,9 @@ public class LocalMultiplayerView extends ViewWidget {
public LocalMultiplayerView(GameInformation information) { public LocalMultiplayerView(GameInformation information) {
this.information = information; this.information = information;
var playButton = Primitive.button("play", () -> { var playButton = Primitive.button("play", () -> {
if (gameController != null) {
gameController.stop();
}
for (var player : information.players) { for (var player : information.players) {
if (player.isHuman && player.name.isEmpty()) { if (player.isHuman && player.name.isEmpty()) {
new ErrorPopup(AppContext.getString("please-enter-your-name")).show(Pos.CENTER); new ErrorPopup(AppContext.getString("please-enter-your-name")).show(Pos.CENTER);
@@ -37,94 +52,68 @@ public class LocalMultiplayerView extends ViewWidget {
} }
} }
// TODO: Fix this temporary ass way of setting the players
Player[] players = new Player[2];
switch (information.type) { switch (information.type) {
case TICTACTOE: case TICTACTOE:
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstTTT()) { if (information.players[0].isHuman) {
BaseTutorialWidget a = new BaseTutorialWidget( players[0] = new LocalPlayer<>(information.players[0].name);
"tutorial", } else {
() -> { players[0] = new ArtificialPlayer<>(new RandomAI<BitboardTicTacToe>(), "Random AI");
AppSettings.getSettings().setFirstTTT(false); }
Platform.runLater(() -> { if (information.players[1].isHuman) {
new TicTacToeGameThread(information); players[1] = new LocalPlayer<>(information.players[1].name);
}); } else {
}, players[1] = new ArtificialPlayer<>(new MiniMaxAI<BitboardTicTacToe>(9), "MiniMax AI");
() -> { }
ViewWidget c = new TicTacToeTutorialWidget(); if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstTTT()) {
transitionNext(c); new ShowEnableTutorialWidget(
WidgetContainer.setCurrentView(c); () -> new TicTacToeTutorialWidget(() -> {
AppSettings.getSettings().setFirstTTT(false); gameController = new TicTacToeBitController(players);
}, gameController.start();
() -> { }),
AppSettings.getSettings().setTutorialFlag(false); () -> Platform.runLater(() -> {
Platform.runLater(() -> { gameController = new TicTacToeBitController(players);
new TicTacToeGameThread(information); gameController.start();
}); }),
} () -> AppSettings.getSettings().setFirstTTT(false)
); );
transitionNext(a); } else {
break; gameController = new TicTacToeBitController(players);
gameController.start();
} }
new TicTacToeGameThread(information);
break; break;
case REVERSI: case REVERSI:
if (information.players[0].isHuman) {
players[0] = new LocalPlayer<>(information.players[0].name);
} else {
players[0] = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(), "Random AI");
}
if (information.players[1].isHuman) {
players[1] = new LocalPlayer<>(information.players[1].name);
} else {
players[1] = new ArtificialPlayer<>(new MiniMaxAI<BitboardReversi>(6), "MiniMax");
}
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) { if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) {
BaseTutorialWidget a = new BaseTutorialWidget( new ShowEnableTutorialWidget(
"tutorial", () -> new ReversiTutorialWidget(() -> {
() -> { Platform.runLater(() -> { gameController = new ReversiBitController(players);
AppSettings.getSettings().setFirstReversi(false); gameController.start();
new ReversiGame(information); }),
}); () -> Platform.runLater(() -> {
}, gameController = new ReversiBitController(players);
() -> { gameController.start();
Platform.runLater(() -> { }),
ViewWidget c = new ReversiTutorialWidget(); () -> AppSettings.getSettings().setFirstReversi(false)
transitionNext(c); );
WidgetContainer.setCurrentView(c); } else {
AppSettings.getSettings().setFirstReversi(false); gameController = new ReversiBitController(players);
}); gameController.start();
},
() -> {
Platform.runLater(() -> {
AppSettings.getSettings().setTutorialFlag(false);
new ReversiGame(information);
});
});
transitionNext(a);
break;
} }
new ReversiGame(information);
break; break;
case CONNECT4: }
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstConnect4()) { });
BaseTutorialWidget a = new BaseTutorialWidget(
"tutorial",
() -> { Platform.runLater(() -> {
AppSettings.getSettings().setFirstConnect4(false);
new Connect4Game(information);
});
},
() -> {
Platform.runLater(() -> {
ViewWidget c = new Connect4TutorialWidget();
transitionNext(c);
WidgetContainer.setCurrentView(c);
AppSettings.getSettings().setFirstConnect4(false);
});
},
() -> {
Platform.runLater(() -> {
AppSettings.getSettings().setTutorialFlag(false);
new Connect4Game(information);
});
});
transitionNext(a);
break;
}
new Connect4Game(information);
break;
}
// case BATTLESHIP -> new BattleshipGame(information);
});
var playerSection = setupPlayerSections(); var playerSection = setupPlayerSections();

View File

@@ -16,14 +16,9 @@ public class LocalView extends ViewWidget {
transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI)); transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI));
}); });
var connect4Button = Primitive.button("connect4", () -> {
transitionNext(new LocalMultiplayerView(GameInformation.Type.CONNECT4));
});
add(Pos.CENTER, Primitive.vbox( add(Pos.CENTER, Primitive.vbox(
ticTacToeButton, ticTacToeButton,
reversiButton, reversiButton
connect4Button
)); ));
} }
} }

View File

@@ -4,6 +4,7 @@ import org.toop.app.App;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import org.toop.app.widget.popup.QuitPopup;
public class MainView extends ViewWidget { public class MainView extends ViewWidget {
public MainView() { public MainView() {
@@ -24,7 +25,8 @@ public class MainView extends ViewWidget {
}); });
var quitButton = Primitive.button("quit", () -> { var quitButton = Primitive.button("quit", () -> {
App.startQuit(); var a = new QuitPopup();
a.show(Pos.CENTER);
}); });
add(Pos.CENTER, Primitive.vbox( add(Pos.CENTER, Primitive.vbox(

View File

@@ -3,6 +3,7 @@ package org.toop.app.widget.view;
import org.toop.app.Server; import org.toop.app.Server;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.LabeledInputWidget; import org.toop.app.widget.complex.LabeledInputWidget;
import org.toop.app.widget.complex.LoadingWidget;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
import javafx.geometry.Pos; import javafx.geometry.Pos;

View File

@@ -14,14 +14,12 @@ import javafx.scene.control.ListView;
public final class ServerView extends ViewWidget { public final class ServerView extends ViewWidget {
private final String user; private final String user;
private final Consumer<String> onPlayerClicked; private final Consumer<String> onPlayerClicked;
private final Runnable onDisconnect;
private final ListView<Button> listView; private final ListView<Button> listView;
public ServerView(String user, Consumer<String> onPlayerClicked, Runnable onDisconnect) { public ServerView(String user, Consumer<String> onPlayerClicked) {
this.user = user; this.user = user;
this.onPlayerClicked = onPlayerClicked; this.onPlayerClicked = onPlayerClicked;
this.onDisconnect = onDisconnect;
this.listView = new ListView<>(); this.listView = new ListView<>();
@@ -29,7 +27,7 @@ public final class ServerView extends ViewWidget {
} }
private void setupLayout() { private void setupLayout() {
var playerHeader = Primitive.header(user); var playerHeader = Primitive.header(user, false);
var playerListSection = Primitive.vbox( var playerListSection = Primitive.vbox(
playerHeader, playerHeader,
@@ -40,7 +38,6 @@ public final class ServerView extends ViewWidget {
add(Pos.CENTER, playerListSection); add(Pos.CENTER, playerListSection);
var disconnectButton = Primitive.button("disconnect", () -> { var disconnectButton = Primitive.button("disconnect", () -> {
onDisconnect.run();
transitionPrevious(); transitionPrevious();
}); });
@@ -52,7 +49,7 @@ public final class ServerView extends ViewWidget {
listView.getItems().clear(); listView.getItems().clear();
for (String player : players) { for (String player : players) {
var playerButton = Primitive.button(player, () -> onPlayerClicked.accept(player)); var playerButton = Primitive.button(player, () -> onPlayerClicked.accept(player), false);
listView.getItems().add(playerButton); listView.getItems().add(playerButton);
} }
}); });

View File

@@ -58,10 +58,19 @@ public class AppContext {
return "MISSING RESOURCE"; return "MISSING RESOURCE";
} }
public static StringBinding bindToKey(String key) { public static StringBinding bindToKey(String key, boolean localize) {
return Bindings.createStringBinding( if (localize) return Bindings.createStringBinding(
() -> localization.getString(key, locale), () -> localization.getString(key, locale),
localeProperty localeProperty
); );
return Bindings.createStringBinding(
() -> key
);
} }
public static StringBinding bindToKey(String key) {
return bindToKey(key, true);
}
} }

View File

@@ -27,18 +27,24 @@ public class AppSettings {
AppContext.setLocale(Locale.of(settingsData.locale)); AppContext.setLocale(Locale.of(settingsData.locale));
App.setFullscreen(settingsData.fullScreen); App.setFullscreen(settingsData.fullScreen);
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume, VolumeControl.MASTERVOLUME))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC))
.asyncPostEvent();
App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize()); App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize());
} }
public static void applyMusicVolumeSettings() {
Settings settingsData = settingsAsset.getContent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume, VolumeControl.MASTERVOLUME))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC))
.asyncPostEvent();
}
public static SettingsAsset getPath() { public static SettingsAsset getPath() {
if (settingsAsset == null) { if (settingsAsset == null) {
String os = System.getProperty("os.name").toLowerCase(); String os = System.getProperty("os.name").toLowerCase();

View File

@@ -83,6 +83,13 @@ reversi2=\u0639\u0646\u062f\u0645\u0627 \u062a\u0646\u0642\u0631 \u0639\u0644\u0
reversi3=\u0645\u0631\u062a\u0643 \u0642\u062f \u064a\u062a\u063a\u0627\u0637 \u0625\u0630\u0627 \u0644\u0645 \u064a\u0643\u0646 \u0647\u0646\u0627\u0643 \u062d\u0631\u0643 \u0642\u0627\u0646\u0648\u0646\u064a. reversi3=\u0645\u0631\u062a\u0643 \u0642\u062f \u064a\u062a\u063a\u0627\u0637 \u0625\u0630\u0627 \u0644\u0645 \u064a\u0643\u0646 \u0647\u0646\u0627\u0643 \u062d\u0631\u0643 \u0642\u0627\u0646\u0648\u0646\u064a.
reversi4=\u0627\u0644\u0644\u0627\u0639\u0628 \u0627\u0644\u0630\u064a \u064a\u0641\u0648\u0632 \u0641\u064a \u0646\u0647\u0627\u064a\u0629 \u0627\u0644\u0644\u0639\u0628 \u0647\u0648 \u0627\u0644\u0630\u064a \u064a\u0643\u0648\u0646 \u0644\u062f\u064a\u0647 \u0627\u0644\u0623\u0643\u062b\u0631 \u0645\u0646 \u0627\u0644\u0644\u0648\u0639\u0627\u0628 \u0639\u0644\u0649 \u0627\u0644\u0644\u0648\u062d\u0629. reversi4=\u0627\u0644\u0644\u0627\u0639\u0628 \u0627\u0644\u0630\u064a \u064a\u0641\u0648\u0632 \u0641\u064a \u0646\u0647\u0627\u064a\u0629 \u0627\u0644\u0644\u0639\u0628 \u0647\u0648 \u0627\u0644\u0630\u064a \u064a\u0643\u0648\u0646 \u0644\u062f\u064a\u0647 \u0627\u0644\u0623\u0643\u062b\u0631 \u0645\u0646 \u0627\u0644\u0644\u0648\u0639\u0627\u0628 \u0639\u0644\u0649 \u0627\u0644\u0644\u0648\u062d\u0629.
tutorialstring=\u0627\u0644\u062f\u0631\u0633 \u0627\u0644\u062a\u0648\u0636\u064a\u062d\u064a tutorialstring=\u0627\u0644\u062f\u0631\u0633 \u0627\u0644\u062a\u0648\u0636\u064a\u062d\u064a
startgame=\u0627\u0628\u062f\u0623 \u0627\u0644\u0644\u0639\u0628\u0629!
goback=\u0627\u0631\u062c\u0639
turnof=\u062F\u0648\u0631\u0647
zwartepiet=\u0633\u0647\u0644: Zwarte Piet
sinterklaas=\u0645\u062a\u0648\u0633\u0637: Sint R. Klaas
santa=\u0635\u0639\u0628: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629
chinese=\u4e2d\u6587 (\u0627\u0644\u0635\u064a\u0646\u064a\u0629) chinese=\u4e2d\u6587 (\u0627\u0644\u0635\u064a\u0646\u064a\u0629)

View File

@@ -85,6 +85,12 @@ reversi2=Wenn du auf einen Punkt klickst, werden alle Spielsteine dazwischen umg
reversi3=Dein Zug kann übersprungen werden, wenn es keinen legalen Zug gibt. Dein Gegner spielt dann weiter, bis du einen legalen Zug machen kannst. reversi3=Dein Zug kann übersprungen werden, wenn es keinen legalen Zug gibt. Dein Gegner spielt dann weiter, bis du einen legalen Zug machen kannst.
reversi4=Der Spieler, der am Ende die meisten Steine auf dem Brett hat, gewinnt. reversi4=Der Spieler, der am Ende die meisten Steine auf dem Brett hat, gewinnt.
tutorialstring=Tutorial tutorialstring=Tutorial
startgame=Spiel starten!
goback=Zurück
turnof=ist dran
zwartepiet=Leicht: Zwarte Piet
sinterklaas=Mittel: Sint R. Klaas
santa=Schwer: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabisch) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabisch)
chinese=\u4e2d\u6587 (Chinesisch) chinese=\u4e2d\u6587 (Chinesisch)

View File

@@ -9,6 +9,8 @@ computer-difficulty=Computer difficulty
computer-think-time=Computer think time computer-think-time=Computer think time
computer=Computer computer=Computer
connect=Connect connect=Connect
connecting=Connecting to server...
connecting-failed=Could not connect to server:
credits=Credits credits=Credits
dark=Dark dark=Dark
deny=Deny deny=Deny
@@ -22,6 +24,7 @@ error=Error
exit=Exit exit=Exit
forfeit=Forfeit forfeit=Forfeit
fullscreen=Fullscreen fullscreen=Fullscreen
game=REPLACE ME
game-over=Game Over game-over=Game Over
general=General general=General
high-contrast=High contrast high-contrast=High contrast
@@ -84,7 +87,12 @@ reversi2=Clicking on a dot will flip all the moves between where you place the d
reversi3=Your turn may be skipped if there is no legal move. This will let your opponent play again until you get an opportunity at a legal move. reversi3=Your turn may be skipped if there is no legal move. This will let your opponent play again until you get an opportunity at a legal move.
reversi4=The player who wins at the end of the game is the one who has the most pieces on the board. reversi4=The player who wins at the end of the game is the one who has the most pieces on the board.
tutorialstring=Tutorial tutorialstring=Tutorial
startgame=Start game!
goback=Go back
turnof='s turn
zwartepiet=Easy: Zwarte Piet
sinterklaas=Medium: Sint R. Klaas
santa=Hard:Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabic) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabic)
chinese=\u4e2d\u6587 (Chinese) chinese=\u4e2d\u6587 (Chinese)

View File

@@ -84,7 +84,12 @@ reversi2=Al hacer clic en un punto, se voltear
reversi3=Tu turno puede ser saltado si no hay un movimiento legal. Esto permitirá que tu oponente juegue nuevamente hasta que tengas una oportunidad legal. reversi3=Tu turno puede ser saltado si no hay un movimiento legal. Esto permitirá que tu oponente juegue nuevamente hasta que tengas una oportunidad legal.
reversi4=El jugador que gane al final del juego es quien tenga más fichas en el tablero. reversi4=El jugador que gane al final del juego es quien tenga más fichas en el tablero.
tutorialstring=Tutorial tutorialstring=Tutorial
startgame=\u00a1Iniciar juego!
goback=Volver
turnof=le toca
zwartepiet=F\u00e1cil: Zwarte Piet
sinterklaas=Medio: Sint R. Klaas
santa=Dif\u00edcil: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Ar\u00e1bigo) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Ar\u00e1bigo)
chinese=\u4e2d\u6587 (Chino) chinese=\u4e2d\u6587 (Chino)

View File

@@ -84,6 +84,12 @@ reversi2=Cliquer sur un point retournera tous les pions entre le point plac
reversi3=Votre tour peut être sauté s'il n'y a pas de coup légal. Cela permettra à votre adversaire de jouer jusqu'à ce que vous ayez un coup légal. reversi3=Votre tour peut être sauté s'il n'y a pas de coup légal. Cela permettra à votre adversaire de jouer jusqu'à ce que vous ayez un coup légal.
reversi4=Le joueur qui a le plus de pions à la fin du jeu gagne. reversi4=Le joueur qui a le plus de pions à la fin du jeu gagne.
tutorialstring=Tutoriel tutorialstring=Tutoriel
startgame=D\u00e9marrer le jeu!
goback=Retour
turnof=\u00E0 son tour
zwartepiet=Facile: Zwarte Piet
sinterklaas=Moyen : Sint R. Klaas
santa=Difficile: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabe) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabe)
chinese=\u4e2d\u6587 (Chinois) chinese=\u4e2d\u6587 (Chinois)

View File

@@ -84,6 +84,12 @@ reversi2=\u0915\u093f\u0938 \u092a\u0930 \u092a\u093f\u0938 \u092a\u0948\u0918 \
reversi3=\u092f\u0939 \u092a\u0930\u094d\u092f \u0938\u0947 \u0938\u0947\u091a \u0915\u0940 \u091c\u093e\u0902\u091c \u0928\u0939\u0940\u0902 \u0939\u0948 \u0914\u0938\u0924\u0947 \u0915\u0948 \u092a\u0948\u0928 \u092a\u0930\u094d\u092f \u0939\u0948 \u0914\u092a\u0915\u0940 \u0915\u0940 \u092a\u0932\u0947 \u092d\u0942\u0924 \u0915\u0940 \u0906\u0927\u093e \u092a\u0948\u0928 \u091c\u093e\u0902\u091c \u0915\u0930 \u0938\u0915\u0924\u0947 \u0939\u0948\u0902. reversi3=\u092f\u0939 \u092a\u0930\u094d\u092f \u0938\u0947 \u0938\u0947\u091a \u0915\u0940 \u091c\u093e\u0902\u091c \u0928\u0939\u0940\u0902 \u0939\u0948 \u0914\u0938\u0924\u0947 \u0915\u0948 \u092a\u0948\u0928 \u092a\u0930\u094d\u092f \u0939\u0948 \u0914\u092a\u0915\u0940 \u0915\u0940 \u092a\u0932\u0947 \u092d\u0942\u0924 \u0915\u0940 \u0906\u0927\u093e \u092a\u0948\u0928 \u091c\u093e\u0902\u091c \u0915\u0930 \u0938\u0915\u0924\u0947 \u0939\u0948\u0902.
reversi4=\u0916\u0941\u092f \u0915\u093f \u0915\u0940 \u0928\u093f\u092e\u0940 \u092e\u0947\u0902 \u091a\u093e\u0932 \u0938\u092c\u0938\u0947 \u091a\u0942\u0928\u094d\u0928\u0947 \u0939\u0948, \u0935\u0949 \u0915\u0947 \u092e\u093e\u0924\u094d\u0930 \u091c\u0940\u0924\u0947 \u0939\u0948. reversi4=\u0916\u0941\u092f \u0915\u093f \u0915\u0940 \u0928\u093f\u092e\u0940 \u092e\u0947\u0902 \u091a\u093e\u0932 \u0938\u092c\u0938\u0947 \u091a\u0942\u0928\u094d\u0928\u0947 \u0939\u0948, \u0935\u0949 \u0915\u0947 \u092e\u093e\u0924\u094d\u0930 \u091c\u0940\u0924\u0947 \u0939\u0948.
tutorialstring=\u0924\u0942\u091f\u0949\u0930\u093f\u092f\u0932 tutorialstring=\u0924\u0942\u091f\u0949\u0930\u093f\u092f\u0932
startgame=\u0916\u0947\u0932 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902!
goback=\u0935\u093e\u092a\u0938 \u091c\u093e\u090f\u0901
turnof=\u0915\u0940 \u092C\u093E\u0930\u0940
zwartepiet=\u0905\u0938\u093e\u0928: Zwarte Piet
sinterklaas=\u092e\u0927\u094d\u092f\u092e: Sint R. Klaas
santa=\u0915\u0924\u093f\u0928: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0905\u0930\u092c\u0940) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0905\u0930\u092c\u0940)
chinese=\u4e2d\u6587 (\u091a\u0940\u0928\u0940) chinese=\u4e2d\u6587 (\u091a\u0940\u0928\u0940)

View File

@@ -83,6 +83,12 @@ reversi2=Cliccando su un punto, tutti i pezzi tra dove metti il punto e il pross
reversi3=Il tuo turno può essere saltato se non ci sono mosse legali. Questo permetterà al tuo avversario di giocare fino a quando non avrai un'opportunità legale. reversi3=Il tuo turno può essere saltato se non ci sono mosse legali. Questo permetterà al tuo avversario di giocare fino a quando non avrai un'opportunità legale.
reversi4=Il giocatore che alla fine del gioco ha più pezzi sulla scacchiera vince. reversi4=Il giocatore che alla fine del gioco ha più pezzi sulla scacchiera vince.
tutorialstring=Tutorial tutorialstring=Tutorial
startgame=Avvia il gioco!
goback=Indietro
turnof=\u00E8 il suo turno
zwartepiet=Facile: Zwarte Piet
sinterklaas=Medio: Sint R. Klaas
santa=Difficile: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabo) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabo)
chinese=\u4e2d\u6587 (Cinese) chinese=\u4e2d\u6587 (Cinese)

View File

@@ -83,6 +83,12 @@ reversi2=\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3001\u3064\u306a\u304c\u308
reversi3=\u6b21\u306e\u52d5\u304b\u3057\u304c\u306a\u3044\u5834\u5408\u3001\u8a8d\u5b9a\u3055\u308c\u305f\u52d5\u304b\u3057\u306e\u6642\u9593\u306f\u62d2\u7d76\u3055\u308c\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002 reversi3=\u6b21\u306e\u52d5\u304b\u3057\u304c\u306a\u3044\u5834\u5408\u3001\u8a8d\u5b9a\u3055\u308c\u305f\u52d5\u304b\u3057\u306e\u6642\u9593\u306f\u62d2\u7d76\u3055\u308c\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002
reversi4=\u672c\u6b21\u306b\u30dc\u30fc\u30c9\u4e0a\u3067\u6700\u591a\u306e\u8ca0\u3051\u3092\u6301\u3064\u30d7\u30ec\u30a4\u30e4\u30fc\u304c\u52dd\u3061\u307e\u3059\u3002 reversi4=\u672c\u6b21\u306b\u30dc\u30fc\u30c9\u4e0a\u3067\u6700\u591a\u306e\u8ca0\u3051\u3092\u6301\u3064\u30d7\u30ec\u30a4\u30e4\u30fc\u304c\u52dd\u3061\u307e\u3059\u3002
tutorialstring=\u30c1\u30e5\u30fc\u30c8\u30ea\u30a2\u30eb tutorialstring=\u30c1\u30e5\u30fc\u30c8\u30ea\u30a2\u30eb
startgame=\u30b2\u30fc\u30e0\u3092\u958b\u59cb\uff01
goback=\u623b\u308b
turnof=\u306E\u756A
zwartepiet=\u7c21\u5358: Zwarte Piet
sinterklaas=\u4e2d\u7d1a: Sint R. Klaas
santa=\u96e3\u3057\u3044: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u30a2\u30e9\u30d3\u30a2\u8a9e) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u30a2\u30e9\u30d3\u30a2\u8a9e)
chinese=\u4e2d\u6587 (\u4e2d\u6587) chinese=\u4e2d\u6587 (\u4e2d\u6587)

View File

@@ -83,6 +83,12 @@ reversi2=\ud074\ub9ad \ud558\uba70, \ub2e4\ub978 \ud648 \uc704\ub85c \ucd5c\uc2e
reversi3=\uc0ac\uc6a9\uc790\uc758 \ud648\uc744 \ud074 \uc218 \uc5c6\uc2b5\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 \ub2f5\uc5d0 \ub300\ud574 \uc811\ub2c8\ub2e4. reversi3=\uc0ac\uc6a9\uc790\uc758 \ud648\uc744 \ud074 \uc218 \uc5c6\uc2b5\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 \ub2f5\uc5d0 \ub300\ud574 \uc811\ub2c8\ub2e4.
reversi4=\uacbd\uc6b0 \uc5d0\uc11c \ucd5c\ub300 \ud648\uc744 \uac00\uc838\ub294 \uc0ac\uc6a9\uc790\uc774 \uc52c\uc544\uc624\uba70 \uc0ac\uc6a9\uc790\uc758 \ud648\uc744 \uc54c\ub824\ud569\ub2c8\ub2e4. reversi4=\uacbd\uc6b0 \uc5d0\uc11c \ucd5c\ub300 \ud648\uc744 \uac00\uc838\ub294 \uc0ac\uc6a9\uc790\uc774 \uc52c\uc544\uc624\uba70 \uc0ac\uc6a9\uc790\uc758 \ud648\uc744 \uc54c\ub824\ud569\ub2c8\ub2e4.
tutorialstring=\ud14c\ud2b8\ub9ad tutorialstring=\ud14c\ud2b8\ub9ad
startgame=\uac8c\uc784 \uc2dc\uc791!
goback=\ub4a4\ub85c \uac00\uae30
turnof=\uC758 \uCC28\uB840
zwartepiet=\uc218\uc601: Zwarte Piet
sinterklaas=\ubcf4\ud1b5: Sint R. Klaas
santa=\uc5d0\uc18c: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0639\u0631\u0628\u064a\u0629) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0639\u0631\u0628\u064a\u0629)
chinese=\u4e2d\u6587 (\u4e2d\u6587) chinese=\u4e2d\u6587 (\u4e2d\u6587)

View File

@@ -83,6 +83,12 @@ reversi2=Door op een stip te klikken draai je alle stukken om tussen de plaats w
reversi3=Je beurt kan worden overgeslagen als er geen legale zet is. Hierdoor kan je tegenstander doorgaan tot jij een legale zet kunt doen. reversi3=Je beurt kan worden overgeslagen als er geen legale zet is. Hierdoor kan je tegenstander doorgaan tot jij een legale zet kunt doen.
reversi4=De speler die aan het einde van het spel de meeste stukken op het bord heeft, wint. reversi4=De speler die aan het einde van het spel de meeste stukken op het bord heeft, wint.
tutorialstring=Tutorial tutorialstring=Tutorial
startgame=Spel starten!
goback=Ga terug
turnof=is aan de beurt
zwartepiet=Makkelijk: Zwarte Piet
sinterklaas=Gemiddeld: Sint R. Klaas
santa=Moeilijk: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabisch) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabisch)
chinese=\u4e2d\u6587 (Chinees) chinese=\u4e2d\u6587 (Chinees)

View File

@@ -83,6 +83,12 @@ reversi2=\u041d043 \u043d043 \u0430043 \u043a043 \u0430043 \u043a043 \u043e043 \
reversi3=\u0412043 \u0430043 \u0436043 \u0434043 \u0430043 \u043d043 \u0438043 \u043d043 \u0435043 \u0435043 \u0432043 \u0430043. reversi3=\u0412043 \u0430043 \u0436043 \u0434043 \u0430043 \u043d043 \u0438043 \u043d043 \u0435043 \u0435043 \u0432043 \u0430043.
reversi4=\u0418043 \u0433043 \u0440043 \u043e043 \u043a043 \u043e043 \u0442043 \u043e043 \u0442043 \u043e043 \u0435043 \u0435043 \u0430043 \u0435043 \u043d043 \u0438043 \u0435043 \u0435043 \u043c043 \u0430043. reversi4=\u0418043 \u0433043 \u0440043 \u043e043 \u043a043 \u043e043 \u0442043 \u043e043 \u0442043 \u043e043 \u0435043 \u0435043 \u0430043 \u0435043 \u043d043 \u0438043 \u0435043 \u0435043 \u043c043 \u0430043.
tutorialstring=\u0423\u0447\u0435\u0431\u043d\u0438\u043a tutorialstring=\u0423\u0447\u0435\u0431\u043d\u0438\u043a
startgame=\u041d\u0430\u0447\u0430\u0442\u044c \u0438\u0433\u0440\u0443!
goback=\u041d\u0430\u0437\u0430\u0434
turnof=\u0445\u043E\u0434\u0438\u0442
zwartepiet=\u041b\u0435\u0433\u043a\u043e: Zwarte Piet
sinterklaas=\u0421\u0440\u0435\u0434\u043d\u0438\u0439: Sint R. Klaas
santa=\u0421\u043b\u043e\u0436\u043d\u043e: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0410\u0440\u0430\u0431\u0441\u043a\u0438\u0439) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0410\u0440\u0430\u0431\u0441\u043a\u0438\u0439)
chinese=\u4e2d\u6587 (\u041a\u0438\u0442\u0430\u0439\u0441\u043a\u0438\u0439) chinese=\u4e2d\u6587 (\u041a\u0438\u0442\u0430\u0439\u0441\u043a\u0438\u0439)

View File

@@ -83,6 +83,12 @@ reversi2=\u70b9\u51fb\u4e00\u4e2a\u70b9\u65f6\u5c06\u5c06\u6240\u6709\u4e2d\u95f
reversi3=\u5982\u679c\u6ca1\u6709\u5408\u6cd5\u64cd\u4f5c\u4f60\u7684\u8fdb\u6b65\u53ef\u80fd\u88ab\u5ffd\u7565. \u8fd9\u4f1a\u8ba9\u5bf9\u624b\u518d\u6b21\u64cd\u4f5c\u5230\u4f60\u6709\u5408\u6cd5\u64cd\u4f5c\u65f6. reversi3=\u5982\u679c\u6ca1\u6709\u5408\u6cd5\u64cd\u4f5c\u4f60\u7684\u8fdb\u6b65\u53ef\u80fd\u88ab\u5ffd\u7565. \u8fd9\u4f1a\u8ba9\u5bf9\u624b\u518d\u6b21\u64cd\u4f5c\u5230\u4f60\u6709\u5408\u6cd5\u64cd\u4f5c\u65f6.
reversi4=\u672c\u6e38\u620f\u7ed3\u675f\u65f6\u8d62\u5f97\u6ee1\u8fc7\u76d8\u9762\u7684\u4ee3\u7406\u6570\u6700\u591a\u7684\u4eba\u5c31\u80dc. reversi4=\u672c\u6e38\u620f\u7ed3\u675f\u65f6\u8d62\u5f97\u6ee1\u8fc7\u76d8\u9762\u7684\u4ee3\u7406\u6570\u6700\u591a\u7684\u4eba\u5c31\u80dc.
tutorialstring=\u6559\u7a0b tutorialstring=\u6559\u7a0b
startgame=\u5f00\u59cb\u6e38\u620f\uff01
goback=\u8fd4\u56de
turnof=\u7684\u56DE\u5408
zwartepiet=\u7b80\u5355: Zwarte Piet
sinterklaas=\u4e2d\u7b49: Sint R. Klaas
santa=\u56f0\u96be: Santa
arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u963f\u62c9\u4f2f\u8bed) arabic=\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u963f\u62c9\u4f2f\u8bed)
chinese=\u4e2d\u6587 chinese=\u4e2d\u6587

View File

@@ -16,7 +16,6 @@
-fx-padding: 8 12 8 12; -fx-padding: 8 12 8 12;
-fx-spacing: 6px; -fx-spacing: 6px;
-fx-alignment: center; -fx-alignment: center;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 6, 0.5, 0, 2);
-fx-min-width: 220px; -fx-min-width: 220px;
-fx-max-width: 260px; -fx-max-width: 260px;
} }
@@ -39,6 +38,18 @@
-fx-background-radius: 30px; -fx-background-radius: 30px;
} }
.loading-progress-bar {
-fx-progress-color: #3caf3f;
}
.loading-progress-bar > .bar {
-fx-background-color: #3caf3f;
}
.loading-progress-bar > .track {
-fx-background-color: rgba(0,0,0,0.15);
}
.progress-text { .progress-text {
-fx-font-size: 11px; -fx-font-size: 11px;
-fx-fill: white; -fx-fill: white;
@@ -155,7 +166,7 @@
-fx-effect: dropshadow(gaussian, #88cc8899, 5, 0, 0, 1); -fx-effect: dropshadow(gaussian, #88cc8899, 5, 0, 0, 1);
} }
.my-turn { .text.my-turn {
-fx-fill: #e05656; -fx-fill: #e05656;
-fx-font-weight: bold; -fx-font-weight: bold;
} }

View File

@@ -13,68 +13,77 @@
} }
.song-display { .song-display {
-fx-padding: 8 12 8 12; -fx-padding: 8 12 8 12;
-fx-spacing: 6px; -fx-spacing: 6px;
-fx-alignment: center; -fx-alignment: center;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 6, 0.5, 0, 2); -fx-min-width: 220px;
-fx-min-width: 220px; -fx-max-width: 260px;
-fx-max-width: 260px;
} }
.song-title { .song-title {
-fx-font-size: 14px; -fx-font-size: 14px;
-fx-fill: white; -fx-fill: white;
} }
.progress-bar { .progress-bar {
-fx-pref-width: 200px; -fx-inner-background-color: black;
-fx-accent: red; -fx-pref-width: 200px;
-fx-accent: red;
} }
.progress-bar > .track { .progress-bar > .track {
-fx-background-radius: 30; -fx-background-radius: 30;
} }
.progress-bar > .bar { .progress-bar > .bar {
-fx-background-radius: 30px; -fx-background-radius: 30px;
}
.loading-progress-bar {
-fx-progress-color: #28a428;
}
.loading-progress-bar > .bar {
-fx-background-color: #28a428;
}
.loading-progress-bar > .track {
-fx-background-color: rgba(0,0,0,0.15);
} }
.progress-text { .progress-text {
-fx-font-size: 11px; -fx-font-size: 11px;
-fx-fill: white; -fx-fill: white;
} }
.skip-button { .skip-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-cursor: hand; -fx-cursor: hand;
-fx-text-fill: white; }
}
.skip-button .text { .skip-button > .text {
-fx-fill: white; -fx-fill: white;
} }
.pause-button { .pause-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-cursor: hand; -fx-cursor: hand;
-fx-text-fill: white; }
}
.pause-button .text { .pause-button > .text {
-fx-fill: white; -fx-fill: white;
} }
.previous-button { .previous-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-cursor: hand; -fx-cursor: hand;
-fx-text-fill: white; }
}
.previous-button .text { .previous-button > .text {
-fx-fill: white; -fx-fill: white;
} }
.button { .button {
@@ -155,7 +164,7 @@
-fx-effect: dropshadow(gaussian, #70e070cc, 6, 0, 0, 2); -fx-effect: dropshadow(gaussian, #70e070cc, 6, 0, 0, 2);
} }
.my-turn { .text.my-turn {
-fx-fill: #ff4b4b; -fx-fill: #ff4b4b;
-fx-font-weight: bold; -fx-font-weight: bold;
} }

View File

@@ -23,6 +23,11 @@
-fx-spacing: 14; -fx-spacing: 14;
} }
.hboxspacing {
-fx-padding: 2;
-fx-spacing: 10;
}
.current-player { .current-player {
-fx-font-size: 32px; -fx-font-size: 32px;
} }

View File

@@ -13,69 +13,79 @@
} }
.song-display { .song-display {
-fx-padding: 8 12 8 12; -fx-padding: 8 12 8 12;
-fx-spacing: 6px; -fx-spacing: 6px;
-fx-alignment: center; -fx-alignment: center;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 6, 0.5, 0, 2); -fx-min-width: 220px;
-fx-min-width: 220px; -fx-max-width: 260px;
-fx-max-width: 260px;
} }
.song-title { .song-title {
-fx-font-size: 14px; -fx-font-size: 14px;
-fx-fill: black; -fx-effect: null;
} }
.progress-bar { .progress-bar {
-fx-inner-background-color: black; -fx-inner-background-color: black;
-fx-pref-width: 200px; -fx-pref-width: 200px;
-fx-accent: red; -fx-accent: red;
} }
.progress-bar > .track { .progress-bar > .track {
-fx-background-radius: 30; -fx-background-radius: 30;
} }
.progress-bar > .bar { .progress-bar > .bar {
-fx-background-radius: 30px; -fx-background-radius: 30px;
}
.loading-progress-bar {
-fx-progress-color: #3caf3f;
}
.loading-progress-bar > .bar {
-fx-background-color: linear-gradient(to bottom, #a2d1a1, #b8e3b9);
}
.loading-progress-bar > .track {
-fx-background-color: rgba(0,0,0,0.15);
} }
.progress-text { .progress-text {
-fx-font-size: 11px; -fx-font-size: 11px;
-fx-fill: black; -fx-effect: null;
} }
.skip-button { .skip-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-cursor: hand; -fx-cursor: hand;
-fx-text-fill: black; -fx-effect: null;
} }
.skip-button .text { .skip-button .text {
-fx-fill: black;
} }
.pause-button { .pause-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-cursor: hand; -fx-cursor: hand;
-fx-text-fill: black; -fx-effect: null;
} }
.pause-button .text { .pause-button .text {
-fx-fill: black; -fx-effect: null;
} }
.previous-button { .previous-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-radius: 0; -fx-background-radius: 0;
-fx-cursor: hand; -fx-cursor: hand;
-fx-text-fill: black; -fx-effect: null;
} }
.previous-button .text { .previous-button .text {
-fx-fill: black; -fx-effect: null;
} }
.button { .button {
@@ -156,7 +166,7 @@
-fx-effect: dropshadow(gaussian, #aad3aa99, 4, 0, 0, 1); -fx-effect: dropshadow(gaussian, #aad3aa99, 4, 0, 0, 1);
} }
.my-turn { .text.my-turn {
-fx-fill: #d14b4b; -fx-fill: #d14b4b;
-fx-font-weight: bold; -fx-font-weight: bold;
} }

View File

@@ -23,6 +23,11 @@
-fx-spacing: 10; -fx-spacing: 10;
} }
.hboxspacing {
-fx-padding: 2;
-fx-spacing: 10;
}
.current-player { .current-player {
-fx-font-size: 24px; -fx-font-size: 24px;
} }

View File

@@ -23,6 +23,11 @@
-fx-spacing: 6; -fx-spacing: 6;
} }
.hboxspacing {
-fx-padding: 2;
-fx-spacing: 10;
}
.current-player { .current-player {
-fx-font-size: 16px; -fx-font-size: 16px;
} }

View File

@@ -25,17 +25,16 @@ public class AudioEventListener<T extends AudioResource, K extends AudioResource
public AudioEventListener<?, ?> initListeners(String buttonSoundToPlay) { public AudioEventListener<?, ?> initListeners(String buttonSoundToPlay) {
new EventFlow() new EventFlow()
.listen(this::handleStopMusicManager) .listen(AudioEvents.StopAudioManager.class, this::handleStopMusicManager, false)
.listen(this::handlePlaySound) .listen(AudioEvents.PlayEffect.class, this::handlePlaySound, false)
.listen(this::handleSkipSong) .listen(AudioEvents.SkipMusic.class, this::handleSkipSong, false)
.listen(this::handlePauseSong) .listen(AudioEvents.PauseMusic.class, this::handlePauseSong, false)
.listen(this::handlePreviousSong) .listen(AudioEvents.PreviousMusic.class, this::handlePreviousSong, false)
.listen(this::handleStopSound) .listen(AudioEvents.StopEffect.class, this::handleStopSound, false)
.listen(this::handleMusicStart) .listen(AudioEvents.StartBackgroundMusic.class, this::handleMusicStart, false)
.listen(this::handleVolumeChange) .listen(AudioEvents.ChangeVolume.class, this::handleVolumeChange, false)
.listen(this::handleGetVolume) .listen(AudioEvents.GetVolume.class, this::handleGetVolume,false)
.listen(AudioEvents.ClickButton.class, _ -> .listen(AudioEvents.ClickButton.class, _ -> soundEffectManager.play(buttonSoundToPlay, false), false);
soundEffectManager.play(buttonSoundToPlay, false));
return this; return this;
} }

View File

@@ -38,7 +38,7 @@ public class EventFlow {
private EventType event = null; private EventType event = null;
/** The listener returned by GlobalEventBus subscription. Used for unsubscription. */ /** The listener returned by GlobalEventBus subscription. Used for unsubscription. */
private final List<ListenerHandler> listeners = new ArrayList<>(); private final List<ListenerHandler<?>> listeners = new ArrayList<>();
/** Holds the results returned from the subscribed event, if any. */ /** Holds the results returned from the subscribed event, if any. */
private Map<String, ?> result = null; private Map<String, ?> result = null;
@@ -46,16 +46,15 @@ public class EventFlow {
/** Empty constructor (event must be added via {@link #addPostEvent(Class, Object...)}). */ /** Empty constructor (event must be added via {@link #addPostEvent(Class, Object...)}). */
public EventFlow() {} public EventFlow() {}
public EventFlow addPostEvent(EventType event) { /**
this.event = event; *
return this; * Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
} *
* @param eventClass The event that will be posted.
public EventFlow addPostEvent(Supplier<? extends EventType> eventSupplier) { * @param args The event arguments, see the added event record for more information.
this.event = eventSupplier.get(); * @return {@link #EventFlow}
return this; *
} */
public <T extends EventType> EventFlow addPostEvent(Class<T> eventClass, Object... args) { public <T extends EventType> EventFlow addPostEvent(Class<T> eventClass, Object... args) {
try { try {
boolean isUuidEvent = UniqueEvent.class.isAssignableFrom(eventClass); boolean isUuidEvent = UniqueEvent.class.isAssignableFrom(eventClass);
@@ -98,155 +97,421 @@ public class EventFlow {
} }
} }
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */ /**
public <TT extends ResponseToUniqueEvent> EventFlow onResponse( *
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) { * Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
ListenerHandler[] listenerHolder = new ListenerHandler[1]; *
listenerHolder[0] = * @param event The event to be posted.
new ListenerHandler( * @return {@link #EventFlow}
GlobalEventBus.subscribe( *
eventClass, */
event -> { public EventFlow addPostEvent(EventType event) {
if (event.getIdentifier() != this.eventSnowflake) return; this.event = event;
action.accept(event);
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]);
}
this.result = event.result();
}));
this.listeners.add(listenerHolder[0]);
return this; return this;
} }
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */ /**
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) { *
return this.onResponse(eventClass, action, true); * Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
*
* @param eventSupplier The event that will be posted through a Supplier.
* @return {@link #EventFlow}
*
*/
public EventFlow addPostEvent(Supplier<? extends EventType> eventSupplier) {
this.event = eventSupplier.get();
return this;
} }
/** Subscribe by ID without explicit class. */ /**
*
* Start listening for an event and trigger when ID correlates.
*
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
* @param action The lambda to run when triggered.
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
* @param name A name given to the event, can later be used to unsubscribe.
* @return {@link #EventFlow}
*
*/
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess, String name
) {
final long id = SnowflakeGenerator.nextId();
Consumer<TT> newAction = eventClass -> {
if (eventClass.getIdentifier() != this.eventSnowflake) return;
action.accept(eventClass);
if (unsubscribeAfterSuccess) unsubscribe(id);
this.result = eventClass.result();
};
// TODO Remove casts
var listener = new ListenerHandler<>(
id,
name,
(Class<ResponseToUniqueEvent>) event,
(Consumer<ResponseToUniqueEvent>) newAction
);
GlobalEventBus.subscribe(listener);
this.listeners.add(listener);
return this;
}
/**
*
* Start listening for an event and trigger when ID correlates, auto unsubscribes after being triggered and adds an empty name.
*
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
* @param action The lambda to run when triggered.
* @return {@link #EventFlow}
*
*/
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> event, Consumer<TT> action) {
return this.onResponse(event, action, true, "");
}
/**
*
* Start listening for an event and trigger when ID correlates, auto adds an empty name.
*
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
* @param action The lambda to run when triggered.
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
* @return {@link #EventFlow}
*
*/
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
return this.onResponse(event, action, unsubscribeAfterSuccess, "");
}
/**
*
* Start listening for an event and trigger when ID correlates, auto unsubscribes after being triggered.
*
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
* @param action The lambda to run when triggered.
* @param name A name given to the event, can later be used to unsubscribe.
* @return {@link #EventFlow}
*
*/
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> event, Consumer<TT> action, String name) {
return this.onResponse(event, action, true, name);
}
/**
*
* Subscribe by ID without explicit class.
*
* @param action The lambda to run when triggered.
* @return {@link #EventFlow}
*
* @deprecated use {@link #onResponse(Class, Consumer, boolean, String)} instead.
*/
@Deprecated
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <TT extends ResponseToUniqueEvent> EventFlow onResponse( public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
Consumer<TT> action, boolean unsubscribeAfterSuccess) { Consumer<TT> action, boolean unsubscribeAfterSuccess, String name) {
ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] = final long id = SnowflakeGenerator.nextId();
new ListenerHandler(
GlobalEventBus.subscribe( Consumer<TT> newAction = event -> {
event -> { if (!(event instanceof UniqueEvent uuidEvent)) return;
if (!(event instanceof UniqueEvent uuidEvent)) return; if (uuidEvent.getIdentifier() == this.eventSnowflake) {
if (uuidEvent.getIdentifier() == this.eventSnowflake) { try {
try { TT typedEvent = (TT) uuidEvent;
TT typedEvent = (TT) uuidEvent; action.accept(typedEvent);
action.accept(typedEvent);
if (unsubscribeAfterSuccess if (unsubscribeAfterSuccess) unsubscribe(id);
&& listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]); this.result = typedEvent.result();
this.listeners.remove(listenerHolder[0]); } catch (ClassCastException _) {
} throw new ClassCastException(
this.result = typedEvent.result(); "Cannot cast "
} catch (ClassCastException _) { + event.getClass().getName()
throw new ClassCastException( + " to UniqueEvent");
"Cannot cast " }
+ event.getClass().getName() }
+ " to UniqueEvent"); };
}
} var listener = new ListenerHandler<>(
})); id,
this.listeners.add(listenerHolder[0]); name,
(Class<TT>) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0],
newAction
);
GlobalEventBus.subscribe(listener);
this.listeners.add(listener);
return this; return this;
} }
/**
*
* Subscribe by ID without explicit class.
*
* @param action The lambda to run when triggered.
* @return {@link #EventFlow}
*
* @deprecated use {@link #onResponse(Class, Consumer)} instead.
*/
@Deprecated
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Consumer<TT> action) { public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Consumer<TT> action) {
return this.onResponse(action, true); return this.onResponse(action, true, "");
} }
/**
*
* Start listening for an event, and run a lambda when triggered.
*
* @param event The {@link EventType} to trigger the lambda.
* @param action The lambda to run when triggered.
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
* @param name A name given to the event, can later be used to unsubscribe.
* @return {@link #EventFlow}
*
*/
public <TT extends EventType> EventFlow listen( public <TT extends EventType> EventFlow listen(
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) { Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess, String name) {
ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] =
new ListenerHandler(
GlobalEventBus.subscribe(
eventClass,
event -> {
action.accept(event);
if (unsubscribeAfterSuccess && listenerHolder[0] != null) { long id = SnowflakeGenerator.nextId();
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]); Consumer<TT> newAction = eventc -> {
} action.accept(eventc);
}));
this.listeners.add(listenerHolder[0]); if (unsubscribeAfterSuccess) unsubscribe(id);
};
var listener = new ListenerHandler<>(
id,
name,
event,
newAction
);
GlobalEventBus.subscribe(listener);
this.listeners.add(listener);
return this; return this;
} }
public <TT extends EventType> EventFlow listen(Class<TT> eventClass, Consumer<TT> action) { /**
return this.listen(eventClass, action, true); *
* Start listening for an event, and run a lambda when triggered, auto unsubscribes.
*
* @param event The {@link EventType} to trigger the lambda.
* @param action The lambda to run when triggered.
* @param name A name given to the event, can later be used to unsubscribe.
* @return {@link #EventFlow}
*
*/
public <TT extends EventType> EventFlow listen(Class<TT> event, Consumer<TT> action, String name) {
return this.listen(event, action, true, name);
} }
/**
*
* Start listening for an event, and run a lambda when triggered, auto unsubscribe and gives it an empty name.
*
* @param event The {@link EventType} to trigger the lambda.
* @param action The lambda to run when triggered.
* @return {@link #EventFlow}
*
*/
public <TT extends EventType> EventFlow listen(Class<TT> event, Consumer<TT> action) {
return this.listen(event, action, true, "");
}
/**
*
* Start listening for an event, and run a lambda when triggered, adds an empty name.
*
* @param event The {@link EventType} to trigger the lambda.
* @param action The lambda to run when triggered.
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
* @return {@link #EventFlow}
*
*/
public <TT extends EventType> EventFlow listen(Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
return this.listen(event, action, unsubscribeAfterSuccess, "");
}
/**
*
* Start listening to an event.
*
* @param action The lambda to run when triggered.
* @return {@link EventFlow}
*
* @deprecated use {@link #listen(Class, Consumer, boolean, String)} instead.
*/
@Deprecated
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <TT extends EventType> EventFlow listen( public <TT extends EventType> EventFlow listen(
Consumer<TT> action, boolean unsubscribeAfterSuccess) { Consumer<TT> action, boolean unsubscribeAfterSuccess, String name) {
ListenerHandler[] listenerHolder = new ListenerHandler[1]; long id = SnowflakeGenerator.nextId();
listenerHolder[0] =
new ListenerHandler( Class<TT> eventClass = (Class<TT>) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0];
GlobalEventBus.subscribe(
event -> { Consumer<TT> newAction = event -> {
if (!(event instanceof EventType nonUuidEvent)) return; if (!(event instanceof EventType nonUuidEvent)) return;
try { try {
TT typedEvent = (TT) nonUuidEvent; TT typedEvent = (TT) nonUuidEvent;
action.accept(typedEvent); action.accept(typedEvent);
if (unsubscribeAfterSuccess && listenerHolder[0] != null) { if (unsubscribeAfterSuccess) unsubscribe(id);
GlobalEventBus.unsubscribe(listenerHolder[0]); } catch (ClassCastException _) {
this.listeners.remove(listenerHolder[0]); throw new ClassCastException(
} "Cannot cast "
} catch (ClassCastException _) { + event.getClass().getName()
throw new ClassCastException( + " to UniqueEvent");
"Cannot cast " }
+ event.getClass().getName() };
+ " to UniqueEvent");
} var listener = new ListenerHandler<>(
})); id,
this.listeners.add(listenerHolder[0]); name,
eventClass,
newAction
);
GlobalEventBus.subscribe(listener);
this.listeners.add(listener);
return this; return this;
} }
/**
*
* Start listening to an event.
*
* @param action The lambda to run when triggered.
* @return {@link EventFlow}
*
* @deprecated use {@link #listen(Class, Consumer)} instead.
*/
@Deprecated
public <TT extends EventType> EventFlow listen(Consumer<TT> action) { public <TT extends EventType> EventFlow listen(Consumer<TT> action) {
return this.listen(action, true); return this.listen(action, true, "");
} }
/** Post synchronously */ /**
* Posts the event added through {@link #addPostEvent}.
*/
public EventFlow postEvent() { public EventFlow postEvent() {
GlobalEventBus.post(this.event); GlobalEventBus.post(this.event);
return this; return this;
} }
/** Post asynchronously */ /**
* Posts the event added through {@link #addPostEvent} asynchronously.
*/
public EventFlow asyncPostEvent() { public EventFlow asyncPostEvent() {
GlobalEventBus.postAsync(this.event); GlobalEventBus.postAsync(this.event);
return this; return this;
} }
/**
*
* Unsubscribe from an event.
*
* @param listenerObject The listener object to remove and unsubscribe.
*/
public void unsubscribe(Object listenerObject) {
this.listeners.removeIf(handler -> {
if (handler.getListener() == listenerObject) {
GlobalEventBus.unsubscribe(handler);
return true;
}
return false;
});
}
/**
*
* Unsubscribe from an event.
*
* @param listenerId The id given to the {@link ListenerHandler}.
*/
public void unsubscribe(long listenerId) {
this.listeners.removeIf(handler -> {
if (handler.getId() == listenerId) {
GlobalEventBus.unsubscribe(handler);
return true;
}
return false;
});
}
/**
* Unsubscribe from an event.
*
* @param name The name given to the listener.
*/
public void unsubscribe(String name) {
this.listeners.removeIf(handler -> {
if (handler.getName().equals(name)) {
GlobalEventBus.unsubscribe(handler);
return true;
}
return false;
});
}
/**
* Unsubscribe all events.
*/
public void unsubscribeAll() {
listeners.removeIf(handler -> {
GlobalEventBus.unsubscribe(handler);
return true;
});
}
/**
* Clean and remove everything inside {@link EventFlow}.
*/
private void clean() { private void clean() {
this.listeners.clear(); unsubscribeAll();
this.event = null; this.event = null;
this.result = null; this.result = null;
} // TODO } // TODO
/**
* TODO
*
* @return TODO
*/
public Map<String, ?> getResult() { public Map<String, ?> getResult() {
return this.result; return this.result;
} }
/**
* TODO
*
* @return TODO
*/
public EventType getEvent() { public EventType getEvent() {
return event; return event;
} }
/**
*
* Returns a copy of the list of listeners.
*
* @return Copy of the list of listeners.
*/
public ListenerHandler[] getListeners() { public ListenerHandler[] getListeners() {
return listeners.toArray(new ListenerHandler[0]); return listeners.toArray(new ListenerHandler[0]);
} }
/**
* Returns the generated snowflake for the {@link EventFlow}
*
* @return The generated snowflake for this {@link EventFlow}
*/
public long getEventSnowflake() { public long getEventSnowflake() {
return eventSnowflake; return eventSnowflake;
} }

View File

@@ -6,6 +6,9 @@ import com.lmax.disruptor.dsl.ProducerType;
import java.util.Map; import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.UniqueEvent; import org.toop.framework.eventbus.events.UniqueEvent;
@@ -14,9 +17,10 @@ import org.toop.framework.eventbus.events.UniqueEvent;
* publishing. * publishing.
*/ */
public final class GlobalEventBus { public final class GlobalEventBus {
private static final Logger logger = LogManager.getLogger(GlobalEventBus.class);
/** Map of event class to type-specific listeners. */ /** Map of event class to type-specific listeners. */
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>> private static final Map<Class<?>, CopyOnWriteArrayList<ListenerHandler<?>>>
LISTENERS = new ConcurrentHashMap<>(); LISTENERS = new ConcurrentHashMap<>();
/** Map of event class to Snowflake-ID-specific listeners. */ /** Map of event class to Snowflake-ID-specific listeners. */
@@ -49,7 +53,6 @@ public final class GlobalEventBus {
ProducerType.MULTI, ProducerType.MULTI,
new BusySpinWaitStrategy()); new BusySpinWaitStrategy());
// Single consumer that dispatches to subscribers
DISRUPTOR.handleEventsWith( DISRUPTOR.handleEventsWith(
(holder, seq, endOfBatch) -> { (holder, seq, endOfBatch) -> {
if (holder.event != null) { if (holder.event != null) {
@@ -73,23 +76,12 @@ public final class GlobalEventBus {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Subscription // Subscription
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
public static <T extends EventType> Consumer<? super EventType> subscribe( public static <T extends EventType> void subscribe(ListenerHandler<T> listener) {
Class<T> eventClass, Consumer<T> listener) { logger.debug("Subscribing to {}: {}", listener.getListenerClass().getSimpleName(), listener.getListener().getClass().getSimpleName());
LISTENERS.computeIfAbsent(listener.getListenerClass(), _ -> new CopyOnWriteArrayList<>()).add(listener);
CopyOnWriteArrayList<Consumer<? super EventType>> list =
LISTENERS.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
Consumer<? super EventType> wrapper = event -> listener.accept(eventClass.cast(event));
list.add(wrapper);
return wrapper;
}
public static Consumer<? super EventType> subscribe(Consumer<Object> listener) {
Consumer<? super EventType> wrapper = event -> listener.accept(event);
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>()).add(wrapper);
return wrapper;
} }
// TODO
public static <T extends UniqueEvent> void subscribeById( public static <T extends UniqueEvent> void subscribeById(
Class<T> eventClass, long eventId, Consumer<T> listener) { Class<T> eventClass, long eventId, Consumer<T> listener) {
UUID_LISTENERS UUID_LISTENERS
@@ -97,10 +89,14 @@ public final class GlobalEventBus {
.put(eventId, listener); .put(eventId, listener);
} }
public static void unsubscribe(Object listener) { public static void unsubscribe(ListenerHandler<?> listener) {
LISTENERS.values().forEach(list -> list.remove(listener)); logger.debug("Unsubscribing from {}: {}", listener.getListenerClass().getSimpleName(), listener.getListener().getClass().getSimpleName());
LISTENERS.getOrDefault(listener.getListenerClass(), new CopyOnWriteArrayList<>())
.remove(listener);
LISTENERS.entrySet().removeIf(entry -> entry.getValue().isEmpty());
} }
// TODO
public static <T extends UniqueEvent> void unsubscribeById( public static <T extends UniqueEvent> void unsubscribeById(
Class<T> eventClass, long eventId) { Class<T> eventClass, long eventId) {
Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(eventClass); Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(eventClass);
@@ -124,36 +120,44 @@ public final class GlobalEventBus {
} }
} }
@SuppressWarnings("unchecked")
private static <T extends EventType> void callListener(ListenerHandler<?> raw, EventType event) {
ListenerHandler<T> handler = (ListenerHandler<T>) raw;
Consumer<T> listener = handler.getListener();
T casted = (T) event;
listener.accept(casted);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static void dispatchEvent(EventType event) { private static void dispatchEvent(EventType event) {
Class<?> clazz = event.getClass(); Class<?> clazz = event.getClass();
// class-specific listeners logger.debug("Triggered event: {}", event.getClass().getSimpleName());
CopyOnWriteArrayList<Consumer<? super EventType>> classListeners = LISTENERS.get(clazz);
CopyOnWriteArrayList<ListenerHandler<?>> classListeners = LISTENERS.get(clazz);
if (classListeners != null) { if (classListeners != null) {
for (Consumer<? super EventType> listener : classListeners) { for (ListenerHandler<?> listener : classListeners) {
try { try {
listener.accept(event); callListener(listener, event);
} catch (Throwable e) { } catch (Throwable e) {
// e.printStackTrace(); e.printStackTrace();
} }
} }
} }
// generic listeners CopyOnWriteArrayList<ListenerHandler<?>> genericListeners = LISTENERS.get(Object.class);
CopyOnWriteArrayList<Consumer<? super EventType>> genericListeners =
LISTENERS.get(Object.class);
if (genericListeners != null) { if (genericListeners != null) {
for (Consumer<? super EventType> listener : genericListeners) { for (ListenerHandler<?> listener : genericListeners) {
try { try {
listener.accept(event); callListener(listener, event);
} catch (Throwable e) { } catch (Throwable e) {
// e.printStackTrace(); e.printStackTrace();
} }
} }
} }
// snowflake listeners
if (event instanceof UniqueEvent snowflakeEvent) { if (event instanceof UniqueEvent snowflakeEvent) {
Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(clazz); Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(clazz);
if (map != null) { if (map != null) {
@@ -182,4 +186,8 @@ public final class GlobalEventBus {
LISTENERS.clear(); LISTENERS.clear();
UUID_LISTENERS.clear(); UUID_LISTENERS.clear();
} }
public static Map<Class<?>, CopyOnWriteArrayList<ListenerHandler<?>>> getAllListeners() {
return LISTENERS;
}
} }

View File

@@ -1,25 +1,48 @@
package org.toop.framework.eventbus; package org.toop.framework.eventbus;
public class ListenerHandler { import org.toop.framework.SnowflakeGenerator;
private Object listener; import org.toop.framework.eventbus.events.EventType;
// private boolean unsubscribeAfterSuccess = true; import java.util.function.Consumer;
// public ListenerHandler(Object listener, boolean unsubAfterSuccess) { public class ListenerHandler<T extends EventType> {
// this.listener = listener; private final long id;
// this.unsubscribeAfterSuccess = unsubAfterSuccess; private final String name;
// } private final Class<T> clazz;
private final Consumer<T> listener;
public ListenerHandler(Object listener) { public ListenerHandler(long id, String name, Class<T> clazz, Consumer<T> listener) {
this.id = id;
this.name = name;
this.clazz = clazz;
this.listener = listener; this.listener = listener;
} }
public Object getListener() { public ListenerHandler(String name, Class<T> clazz, Consumer<T> listener) {
return this.listener; this(SnowflakeGenerator.nextId(), name, clazz, listener);
} }
// public boolean isUnsubscribeAfterSuccess() { public ListenerHandler(long id, Class<T> clazz, Consumer<T> listener) {
// return this.unsubscribeAfterSuccess; this(id, String.valueOf(id), clazz, listener);
// } }
public ListenerHandler(Class<T> clazz, Consumer<T> listener) {
this(SnowflakeGenerator.nextId(), clazz, listener);
}
public long getId() {
return id;
}
public Consumer<T> getListener() {
return listener;
}
public Class<T> getListenerClass() {
return clazz;
}
public String getName() {
return name;
}
} }

View File

@@ -0,0 +1,18 @@
package org.toop.framework.gameFramework;
/**
* Represents the current state of a turn-based game.
*/
public enum GameState {
/** Game is ongoing and no special condition applies. */
NORMAL,
/** Game ended in a draw. */
DRAW,
/** Game ended with a win for a player. */
WIN,
/** Next player's turn was skipped. */
TURN_SKIPPED,
}

View File

@@ -0,0 +1,6 @@
package org.toop.framework.gameFramework;
@FunctionalInterface
public interface LongPairConsumer {
void accept(long a, long b);
}

View File

@@ -0,0 +1,18 @@
package org.toop.framework.gameFramework.controller;
import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay;
import org.toop.framework.gameFramework.model.game.threadBehaviour.Controllable;
import org.toop.framework.networking.events.NetworkEvents;
public interface GameController extends Controllable, UpdatesGameUI {
/** Called when it is this player's turn to make a move. */
void onYourTurn(NetworkEvents.YourTurnResponse event);
/** Called when a move from another player is received. */
void onMoveReceived(NetworkEvents.GameMoveResponse event);
/** Called when the game has finished, with the final result. */
void gameFinished(NetworkEvents.GameResultResponse event);
void sendMove(long clientId, long move);
}

View File

@@ -0,0 +1,10 @@
package org.toop.framework.gameFramework.controller;
/**
* Interface for classes that can trigger a UI update.
*/
public interface UpdatesGameUI {
/** Called to refresh or update the game UI. */
void updateUI();
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.gameFramework.model.game;
public interface BoardProvider {
long[] getBoard();
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.gameFramework.model.game;
public interface DeepCopyable<T> {
T deepCopy();
}

View File

@@ -0,0 +1,12 @@
package org.toop.framework.gameFramework.model.game;
import org.toop.framework.gameFramework.GameState;
/**
* Represents the result of a move in a turn-based game.
*
* @param state the resulting {@link GameState} after the move
* @param player the index of the player associated with the result (winner or relevant player)
*/
public record PlayResult(GameState state, int player) {
}

View File

@@ -0,0 +1,24 @@
package org.toop.framework.gameFramework.model.game;
import org.toop.framework.gameFramework.GameState;
/**
* Interface for turn-based games that can be played and queried for legal moves.
*/
public interface Playable {
/**
* Returns the moves that are currently valid in the game.
*
* @return an array of integers representing legal moves
*/
long getLegalMoves();
/**
* Plays the given move and returns the resulting game state.
*
* @param move the move to apply
* @return the {@link GameState} and additional info after the move
*/
PlayResult play(long move);
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.gameFramework.model.game;
import org.toop.framework.gameFramework.model.player.Player;
public interface PlayerProvider<T extends TurnBasedGame<T>> {
Player<T> getPlayer(int index);
}

View File

@@ -0,0 +1,20 @@
package org.toop.framework.gameFramework.model.game;
import org.toop.framework.networking.events.NetworkEvents;
/**
* Interface for games that support online multiplayer play.
* <p>
* Methods are called in response to network events from the server.
*/
public interface SupportsOnlinePlay {
/** Called when it is this player's turn to make a move. */
void onYourTurn(long clientId);
/** Called when a move from another player is received. */
void onMoveReceived(long move);
/** Called when the game has finished, with the final result. */
void gameFinished(String condition);
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.gameFramework.model.game;
public interface TurnBasedGame<T extends TurnBasedGame<T>> extends Playable, DeepCopyable<T>, PlayerProvider<T>, BoardProvider {
int getCurrentTurn();
int getPlayerCount();
int getWinner();
}

View File

@@ -0,0 +1,62 @@
package org.toop.framework.gameFramework.model.game.threadBehaviour;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.gameFramework.LongPairConsumer;
import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* Base class for thread-based game behaviours.
* <p>
* Provides common functionality for managing game state and execution:
* a running flag, a game reference, and a logger.
* Subclasses implement the actual game-loop logic.
*/
public abstract class AbstractThreadBehaviour<T extends TurnBasedGame<T>> implements ThreadBehaviour {
private LongPairConsumer onSendMove;
private Runnable onUpdateUI;
/** Indicates whether the game loop or event processing is active. */
protected final AtomicBoolean isRunning = new AtomicBoolean();
/** The game instance controlled by this behaviour. */
protected final T game;
/** Logger for the subclass to report errors or debug info. */
protected final Logger logger = LogManager.getLogger(this.getClass());
/**
* Creates a new base behaviour for the specified game.
*
* @param game the turn-based game to control
*/
public AbstractThreadBehaviour(T game) {
this.game = game;
}
protected void updateUI(){
if (onUpdateUI != null) {
onUpdateUI.run();
}
}
protected void sendMove(long clientId, long move){
if (onSendMove != null) {
onSendMove.accept(clientId, move);
}
}
@Override
public void setOnUpdateUI(Runnable onUpdateUI) {
this.onUpdateUI = onUpdateUI;
}
@Override
public void setOnSendMove(LongPairConsumer onSendMove) {
this.onSendMove = onSendMove;
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.gameFramework.model.game.threadBehaviour;
public interface Controllable {
void start();
void stop();
}

View File

@@ -0,0 +1,20 @@
package org.toop.framework.gameFramework.model.game.threadBehaviour;
import org.toop.framework.gameFramework.LongPairConsumer;
import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.function.Consumer;
/**
* Strategy interface for controlling game thread behavior.
* <p>
* Defines how a game's execution is started, stopped, and which player is active.
*/
public interface ThreadBehaviour extends Controllable {
void setOnUpdateUI(Runnable onUpdateUI);
void setOnSendMove(LongPairConsumer onSendMove);
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.DeepCopyable;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface AI<T extends TurnBasedGame<T>> extends MoveProvider<T>, DeepCopyable<AI<T>> {
}

View File

@@ -0,0 +1,17 @@
package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
/**
* Abstract base class for AI implementations for games extending {@link GameR}.
* <p>
* Provides a common superclass for specific AI algorithms. Concrete subclasses
* must implement the {@link #findBestMove(GameR, int)} method defined by
* {@link IAIMoveR} to determine the best move given a game state and a search depth.
* </p>
*
* @param <T> the specific type of game this AI can play, extending {@link GameR}
*/
public abstract class AbstractAI<T extends TurnBasedGame<T>> implements AI<T> {
// Concrete AI implementations should override findBestMove(T game, int depth)
}

View File

@@ -0,0 +1,50 @@
package org.toop.framework.gameFramework.model.player;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
/**
* Abstract class representing a player in a game.
* <p>
* Players are entities that can make moves based on the current state of a game.
* player types, such as human players or AI players.
* </p>
* <p>
* Subclasses should override the {@link #getMove(GameR)} method to provide
* specific move logic.
* </p>
*/
public abstract class AbstractPlayer<T extends TurnBasedGame<T>> implements Player<T> {
private final Logger logger = LogManager.getLogger(this.getClass());
private final String name;
protected AbstractPlayer(String name) {
this.name = name;
}
protected AbstractPlayer(AbstractPlayer<T> other) {
this.name = other.name;
}
/**
* Determines the next move based on the provided game state.
* <p>
* The default implementation throws an {@link UnsupportedOperationException},
* indicating that concrete subclasses must override this method to provide
* actual move logic.
* </p>
*
* @param gameCopy a snapshot of the current game state
* @return an integer representing the chosen move
* @throws UnsupportedOperationException if the method is not overridden
*/
public long getMove(T gameCopy) {
logger.error("Method getMove not implemented.");
throw new UnsupportedOperationException("Not supported yet.");
}
public String getName(){
return this.name;
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface MoveProvider<T extends TurnBasedGame<T>> {
long getMove(T game);
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.gameFramework.model.player;
public interface NameProvider {
String getName();
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.DeepCopyable;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface Player<T extends TurnBasedGame<T>> extends NameProvider, MoveProvider<T>, DeepCopyable<Player<T>> {
}

View File

@@ -0,0 +1,26 @@
package org.toop.framework.gameFramework.view;
import org.toop.framework.eventbus.events.EventsBase;
import org.toop.framework.eventbus.events.GenericEvent;
/**
* Defines GUI-related events for the event bus.
* <p>
* These events notify the UI about updates such as game progress,
* player actions, and game completion.
*/
public class GUIEvents extends EventsBase {
/**
* Event indicating the game has ended.
*
* @param winOrTie true if the game ended in a win, false for a draw
* @param winner the index of the winning player, or -1 if no winner
*/
public record GameEnded(boolean winOrTie, int winner) implements GenericEvent {}
/** Event indicating a player has attempted a move. */
public record PlayerAttemptedMove(long move) implements GenericEvent {}
/** Event indicating a player is hovering over a move (for UI feedback). */
public record PlayerMoveHovered(long move) implements GenericEvent {}
}

View File

@@ -17,25 +17,25 @@ public class NetworkingClientEventListener {
public NetworkingClientEventListener(NetworkingClientManager clientManager) { public NetworkingClientEventListener(NetworkingClientManager clientManager) {
this.clientManager = clientManager; this.clientManager = clientManager;
new EventFlow() new EventFlow()
.listen(this::handleStartClient) .listen(NetworkEvents.StartClient.class, this::handleStartClient, false)
.listen(this::handleCommand) .listen(NetworkEvents.SendCommand.class, this::handleCommand, false)
.listen(this::handleSendLogin) .listen(NetworkEvents.SendLogin.class, this::handleSendLogin, false)
.listen(this::handleSendLogout) .listen(NetworkEvents.SendLogout.class, this::handleSendLogout, false)
.listen(this::handleSendGetPlayerlist) .listen(NetworkEvents.SendGetPlayerlist.class, this::handleSendGetPlayerlist, false)
.listen(this::handleSendGetGamelist) .listen(NetworkEvents.SendGetGamelist.class, this::handleSendGetGamelist, false)
.listen(this::handleSendSubscribe) .listen(NetworkEvents.SendSubscribe.class, this::handleSendSubscribe, false)
.listen(this::handleSendMove) .listen(NetworkEvents.SendMove.class, this::handleSendMove, false)
.listen(this::handleSendChallenge) .listen(NetworkEvents.SendChallenge.class, this::handleSendChallenge, false)
.listen(this::handleSendAcceptChallenge) .listen(NetworkEvents.SendAcceptChallenge.class, this::handleSendAcceptChallenge, false)
.listen(this::handleSendForfeit) .listen(NetworkEvents.SendForfeit.class, this::handleSendForfeit, false)
.listen(this::handleSendMessage) .listen(NetworkEvents.SendMessage.class, this::handleSendMessage, false)
.listen(this::handleSendHelp) .listen(NetworkEvents.SendHelp.class, this::handleSendHelp, false)
.listen(this::handleSendHelpForCommand) .listen(NetworkEvents.SendHelpForCommand.class, this::handleSendHelpForCommand, false)
.listen(this::handleCloseClient) .listen(NetworkEvents.CloseClient.class, this::handleCloseClient, false)
.listen(this::handleReconnect) .listen(NetworkEvents.Reconnect.class, this::handleReconnect, false)
.listen(this::handleChangeAddress) .listen(NetworkEvents.ChangeAddress.class, this::handleChangeAddress, false)
.listen(this::handleGetAllConnections) .listen(NetworkEvents.RequestsAllClients.class, this::handleGetAllConnections, false)
.listen(this::handleShutdownAll); .listen(NetworkEvents.ForceCloseAllClients.class, this::handleShutdownAll, false);
} }
void handleStartClient(NetworkEvents.StartClient event) { void handleStartClient(NetworkEvents.StartClient event) {

View File

@@ -8,6 +8,8 @@ import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.exceptions.ClientNotFoundException; import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.exceptions.CouldNotConnectException; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.interfaces.NetworkingClient; import org.toop.framework.networking.interfaces.NetworkingClient;
@@ -44,6 +46,7 @@ public class NetworkingClientManager implements org.toop.framework.networking.in
nClient.connect(id, nConnector.host(), nConnector.port()); nClient.connect(id, nConnector.host(), nConnector.port());
networkClients.put(id, nClient); networkClients.put(id, nClient);
logger.info("New client started successfully for {}:{}", nConnector.host(), nConnector.port()); logger.info("New client started successfully for {}:{}", nConnector.host(), nConnector.port());
GlobalEventBus.post(new NetworkEvents.ConnectTry(id, attempts, nConnector.reconnectAttempts(), true));
onSuccess.run(); onSuccess.run();
scheduler.shutdown(); scheduler.shutdown();
} catch (CouldNotConnectException e) { } catch (CouldNotConnectException e) {
@@ -51,14 +54,17 @@ public class NetworkingClientManager implements org.toop.framework.networking.in
if (attempts < nConnector.reconnectAttempts()) { if (attempts < nConnector.reconnectAttempts()) {
logger.warn("Could not connect to {}:{}. Retrying in {} {}", logger.warn("Could not connect to {}:{}. Retrying in {} {}",
nConnector.host(), nConnector.port(), nConnector.timeout(), nConnector.timeUnit()); nConnector.host(), nConnector.port(), nConnector.timeout(), nConnector.timeUnit());
GlobalEventBus.post(new NetworkEvents.ConnectTry(id, attempts, nConnector.reconnectAttempts(), false));
scheduler.schedule(this, nConnector.timeout(), nConnector.timeUnit()); scheduler.schedule(this, nConnector.timeout(), nConnector.timeUnit());
} else { } else {
logger.error("Failed to start client for {}:{} after {} attempts", nConnector.host(), nConnector.port(), attempts); logger.error("Failed to start client for {}:{} after {} attempts", nConnector.host(), nConnector.port(), attempts);
GlobalEventBus.post(new NetworkEvents.ConnectTry(id, -1, nConnector.reconnectAttempts(), false));
onFailure.run(); onFailure.run();
scheduler.shutdown(); scheduler.shutdown();
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Unexpected exception during startClient", e); logger.error("Unexpected exception during startClient", e);
GlobalEventBus.post(new NetworkEvents.ConnectTry(id, -1, nConnector.reconnectAttempts(), false));
onFailure.run(); onFailure.run();
scheduler.shutdown(); scheduler.shutdown();
} }

View File

@@ -181,6 +181,8 @@ public class NetworkEvents extends EventsBase {
public record StartClientResponse(long clientId, boolean successful, long identifier) public record StartClientResponse(long clientId, boolean successful, long identifier)
implements ResponseToUniqueEvent {} implements ResponseToUniqueEvent {}
public record ConnectTry(long clientId, int amount, int maxAmount, boolean success) implements GenericEvent {}
/** /**
* Requests reconnection of an existing client using its previous configuration. * Requests reconnection of an existing client using its previous configuration.
* <p> * <p>

Some files were not shown because too many files have changed in this diff Show More