118 Commits

Author SHA1 Message Date
Bas de Jong
a1f0d48477 Refactored Tournament to use matchExecutor and ResultBroadcaster. Added turnTime and players are now added through Tournament creation instead of on MatchMaker/ScoreSystem creation 2026-01-12 08:33:54 +01:00
lieght
5caf6900d1 Removed input mistake, removed print 2026-01-11 10:50:53 +01:00
lieght
94d85bf78d Null handling 2026-01-11 09:07:30 +01:00
lieght
c9ea8f5e5b Tournament now uses propper builder pattern 2026-01-11 09:03:49 +01:00
lieght
cc7acf9f0c Moved scoring calculation into scoring system 2026-01-11 07:45:55 +01:00
Bas de Jong
955cb6109c Added back ability to shuffle matchmaker 2026-01-11 01:54:40 +01:00
Bas de Jong
013dd90705 Async tournament runner 2026-01-11 01:42:59 +01:00
Bas de Jong
c77499c36d Added result comeback with a draw 2026-01-11 00:53:31 +01:00
Bas de Jong
28791fcc8a Tournament is now without admins 2026-01-10 22:47:53 +01:00
Bas de Jong
97657b01c9 Added admins to games 2026-01-10 22:28:41 +01:00
Bas de Jong
a5bf6ca9fb Request admin list 2026-01-10 21:22:15 +01:00
Bas de Jong
fc25c15736 Starting a tournament now requires to be admin 2026-01-10 21:19:04 +01:00
Bas de Jong
d4cad3311e Tournament refactor for better naming and easier to understand code 2026-01-10 20:38:26 +01:00
Bas de Jong
96afc9543a Removed unnecessary imports 2026-01-10 04:29:38 +01:00
Bas de Jong
0c1b106da5 Refactored tournament to use interfaces and builders 2026-01-10 04:28:12 +01:00
Bas de Jong
aca0b2dcc0 Tournament now returns result to clients 2026-01-10 02:44:04 +01:00
Bas de Jong
75963a891b Tournament results are now send back to the clients connected to the server 2026-01-10 02:14:23 +01:00
Bas de Jong
6b644ed8fa Shuffle now changeable, host can now switch tournament gametype 2026-01-10 00:06:52 +01:00
Bas de Jong
6a395cc40b GlobalEventBus is now async instead 2026-01-09 23:34:29 +01:00
Bas de Jong
5e5948d1fe Working tournament 2026-01-09 22:28:15 +01:00
Bas de Jong
9c01aabbe1 Fixed merge mistakes 2026-01-07 23:46:21 +01:00
Bas Antonius de Jong
0cb52b042f Merge branch 'Development' into 289-server 2026-01-07 23:44:13 +01:00
Bas de Jong
56a8d12e46 Logging and fixed user input getting stuck 2026-01-07 23:38:53 +01:00
65220d9649 Hotfix for stuff 2026-01-07 17:16:45 +01:00
Bas Antonius de Jong
c64a2e2c65 Server update with new dev changes (#305)
* merge widgets with development

* readd previous game thread code

* Revert "readd previous game thread code"

This reverts commit d24feef73e.

* Revert "Merge remote-tracking branch 'origin/Development' into Development"

This reverts commit 59d46cb73c, reversing
changes made to 38681c5db0.

* Revert "merge widgets with development"

This reverts commit 38681c5db0.

* Merge 292 into development (#293)

Applied template method pattern to abstract player

* Added documentation to player classes and improved method names (#295)

* mcts v1

* bitboard optimization

* bitboard fix & mcts v2 & mcts v3. v3 still in progress and v4 coming soon

* main

---------

Co-authored-by: ramollia <>
Co-authored-by: Stef <stbuwalda@gmail.com>
Co-authored-by: Stef <48526421+StefBuwalda@users.noreply.github.com>
2026-01-07 16:15:49 +01:00
58a9ce78fe Merge remote-tracking branch 'origin/289-server' into Development
# Conflicts:
#	app/src/main/java/org/toop/app/Server.java
#	app/src/main/java/org/toop/app/gameControllers/GenericGameController.java
#	app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
#	framework/src/main/java/org/toop/framework/game/BitboardGame.java
#	framework/src/main/java/org/toop/framework/game/players/ArtificialPlayer.java
#	framework/src/main/java/org/toop/framework/game/players/LocalPlayer.java
#	framework/src/main/java/org/toop/framework/game/players/OnlinePlayer.java
#	framework/src/main/java/org/toop/framework/game/players/ai/MiniMaxAI.java
#	framework/src/main/java/org/toop/framework/game/players/ai/RandomAI.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java
#	game/src/main/java/org/toop/game/players/MiniMaxAI.java
#	game/src/main/java/org/toop/game/players/RandomAI.java
#	game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java
#	game/src/main/java/org/toop/game/players/ai/RandomAI.java
2026-01-07 16:13:53 +01:00
230f7480e4 Merge remote-tracking branch 'origin/289-server' into 289-server 2026-01-07 15:41:28 +01:00
ramollia
6aa0eb952a main 2026-01-07 14:44:45 +01:00
ramollia
df93b44d19 bitboard fix & mcts v2 & mcts v3. v3 still in progress and v4 coming soon 2026-01-07 14:39:38 +01:00
Bas de Jong
67f39c3f3b Code readability 2026-01-07 14:38:19 +01:00
6e6a383708 Collapsed interfaces in View section 2026-01-07 13:26:43 +01:00
b7dec7798b Collapsed interfaces in Controller section 2026-01-07 13:13:32 +01:00
2caa4fc79f Fixed runtime error I forgot to fix. 2026-01-07 12:42:54 +01:00
e72d888d84 Collapsed interfaces from model portion 2026-01-07 12:41:25 +01:00
ramollia
e149588b60 bitboard optimization 2025-12-15 10:31:22 +01:00
Bas de Jong
a7d1a964c2 Moved subscriptions to store 2025-12-15 10:01:23 +01:00
ramollia
380e219c08 mcts v1 2025-12-15 09:06:56 +01:00
lieght
dccf428bb8 TableWidget 2025-12-14 17:18:57 +01:00
ramollia
4ad922423c Merge remote-tracking branch 'origin/Development' into Development 2025-12-14 17:05:17 +01:00
lieght
6e2ea82a32 UI fixes after game end 2025-12-14 13:30:21 +01:00
lieght
34c85ec472 Removed user from subscription if in a game 2025-12-14 13:11:55 +01:00
2d9b34b7f6 Quick fix so more than one game can be played in succession 2025-12-14 11:36:51 +01:00
lieght
8867d5a1ea Missed a boolean 2025-12-14 01:19:16 +01:00
lieght
b94d1b6c9d Small improvements to usability, auto disconnect when server closes connection 2025-12-14 01:13:42 +01:00
lieght
8cb0a86d4e Working subscription, button only subs to reversi right now 2025-12-13 23:20:28 +01:00
lieght
c2f1df7143 Refactor done, added ability to subscribe 2025-12-13 22:44:13 +01:00
lieght
0956286616 Partial server refactor 2025-12-13 21:11:26 +01:00
55de6b5b18 Merge remote-tracking branch 'origin/289-server' into 289-server 2025-12-13 18:53:18 +01:00
73d71f2a2d Making moves works. Game notifies when game has ended. 2025-12-13 18:53:10 +01:00
lieght
cbcce29780 Closable server 2025-12-13 18:38:31 +01:00
lieght
afcd9be71e Fixed hasArgs 2025-12-13 17:53:31 +01:00
a9145d44cf Merge remote-tracking branch 'origin/289-server' into 289-server 2025-12-13 17:50:03 +01:00
c015100ebf Werkt nog niet 2025-12-13 17:49:54 +01:00
lieght
cd5736afc8 Removed space in naming 2025-12-13 17:38:36 +01:00
lieght
89a9cb1e55 Using pairs now in server.java 2025-12-13 17:37:34 +01:00
lieght
22270e58dc Added pairs 2025-12-13 17:33:14 +01:00
lieght
edd2c24b65 Added ability to take ServerPlayer from user 2025-12-13 17:22:56 +01:00
c929abc4b8 Removed Generics, pray nothing breaks. 2025-12-13 17:08:34 +01:00
lieght
8b85915c74 Fixes 2025-12-13 17:08:10 +01:00
lieght
150fb2986f Fixed tic tac toe naming 2025-12-13 15:17:16 +01:00
lieght
9c20fcbc39 Fixed bugs, easy to use host button 2025-12-13 15:01:28 +01:00
lieght
4d31a8ed44 Working challenges 2025-12-12 21:48:57 +01:00
lieght
fc47d81b8e Init challenges 2025-12-12 19:47:51 +01:00
lieght
a60c702306 Tests and better instantiation 2025-12-12 16:47:17 +01:00
lieght
c30c118c04 Code cleanup 2025-12-12 16:04:12 +01:00
4b8edf1585 Merge remote-tracking branch 'origin/289-server' into 289-server
# Conflicts:
#	app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java
#	app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java
#	framework/src/main/java/org/toop/framework/game/games/reversi/BitboardReversi.java
#	framework/src/main/java/org/toop/framework/game/games/tictactoe/BitboardTicTacToe.java
2025-12-12 15:53:45 +01:00
fa9c2ce32b Removed Generics, pray nothing breaks. 2025-12-12 15:53:24 +01:00
Bas de Jong
2599f9fa40 Testing code 2025-12-12 15:17:29 +01:00
Bas de Jong
84e411fa38 Moves 2025-12-12 15:17:12 +01:00
Bas de Jong
66cb000fad Init server code 2025-12-12 15:15:55 +01:00
Stef
1ae79daef0 Added documentation to player classes and improved method names (#295) 2025-12-10 13:17:01 +01:00
Stef
cd8eb99559 Merge 292 into development (#293)
Applied template method pattern to abstract player
2025-12-10 12:39:40 +01:00
Bas Antonius de Jong
0132981d94 Merge branch 'main' into Development 2025-12-09 21:20:08 +01:00
Bas de Jong
322197494c Will fix tests etc later 2025-12-09 21:19:30 +01:00
Bas de Jong
a9c99df5d2 Better limits to generic acceptance 2025-12-09 21:07:30 +01:00
ramollia
d702f82194 Merge remote-tracking branch 'origin/Development' into Development
# Conflicts:
#	game/src/main/java/org/toop/game/reversi/ReversiAI.java
2025-12-09 12:54:12 +01:00
Stef
912d25c01f Merge bitboards into development (#285)
* added new classes for the games that use bitboards instead. also combined game with turnbasedgame

* (DOES NOT COMPILE) In-between commit

* turn updates

* smalle fixes aan turn updates

* Bitboard implemented with scuffed TicTacToe translation done by game. This should be done by the view.

* Almost done with implementing bitboards. Reversi is broken and artifical players don't work yet.

* better human/ai selector with bot selection and depth on TicTacToeAIR

* fixed getLegalMoves

* depth + thinktime back to AIs, along with a a specific TicTacToeAIRSleep

* fixed overlapping back and disconnect buttons

* Changed to debug instead of info

* changed the transitionNextCustom to be easier to use

* added getAllWidgets to WidgetContainer

* Correct back view

* added replacePrevious in ViewWidget

* added removeIndexFromPreviousChain

* fixed incorrect index counting

* Fixt wrong view order

* fixed? getLegalMoves

* Everything is broken

* Removed todo

* fixed getLegalMoves & getFlips

* Challenge popups "Fixed"

* Fixed local and online play for both games

* Popups now remove themselves

* Removed souts for debugging

* localize the ChallengePopup text

* made the game text a header instead

* made more classes deepClonable.

* fixed getAllWidgets

* Added comment

* Escape popup

* fixed redundant container

* Made all network events async again

* Escape remove popup

* Working escape menu

* Removed old AI and old files. Added a new generic random AI. game no longer deals with translation.

* Drawing of board on canvas is now done from bitboards rather than translating.

* Added a method getWinner() to game interface.Controller now tells gameThreads how to deal with drawing UI and sending a move to server.

* Added find functionality

* Added a ChatGPT generated MiniMaxAI based on the old MiniMaxAI but with alpha-beta pruning and heuristics for Reversi

* Removed System-Outs to clean up console

* Update BitGameCanvas.java

* Merge fixes

* Removed unused imports

---------

Co-authored-by: ramollia <>
Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: lieght <49651652+BAFGdeJong@users.noreply.github.com>
2025-12-08 18:23:06 +01:00
Ticho Hidding
adc7b1a8f3 fixed reversi colors being switched, causing multiple issues 2025-12-08 17:14:31 +01:00
Ticho Hidding
3a8b1c2454 shitty fix for player selector spacing issue v2 2025-12-08 16:12:42 +01:00
Ticho Hidding
846898988f shitty fix for player selector spacing issue 2025-12-08 15:59:11 +01:00
Ticho Hidding
ecd0fd26be changed "fullscreen exit key combination" from esc to F11 2025-12-08 15:11:17 +01:00
lieght
a838973e67 Code cleanup 2025-12-07 20:43:56 +01:00
lieght
a4f2a67d9c Deleted unnecessary imports 2025-12-07 20:39:29 +01:00
lieght
12bccb8854 Safety 2025-12-07 20:33:27 +01:00
lieght
80d3e47ad9 initSystems now uses latch instead of timer. Moved single threads to Executor 2025-12-07 18:59:19 +01:00
Bas Antonius de Jong
38f50cc16d Event bus now testable, improved UI (#284)
* turn updates

* smalle fixes aan turn updates

* better human/ai selector with bot selection and depth on TicTacToeAIR

* depth + thinktime back to AIs, along with a a specific TicTacToeAIRSleep

* fixed overlapping back and disconnect buttons

* Changed to debug instead of info

* changed the transitionNextCustom to be easier to use

* added getAllWidgets to WidgetContainer

* Correct back view

* added replacePrevious in ViewWidget

* added removeIndexFromPreviousChain

* fixed incorrect index counting

* Fixt wrong view order

* Removed todo

* Challenge popups "Fixed"

* Popups now remove themselves

* localize the ChallengePopup text

* made the game text a header instead

* fixed getAllWidgets

* Escape popup

* fixed redundant container

* Escape remove popup

* Working escape menu

* Added find functionality

* Tutorials moved to escape menu

* Escape can't be opened in mainview now

* Can now test the event bus, created testable interfaces

* Logging errors

* Made events and handlers more generic

* Suppress

* Managers now have changeable eventbus

* Tutorials fixed

* Removed import

* Single threaded eventbus

* Fixed wrong eventbus

* Removed get

* Removed old code

* Renaming

* Optimization

* Removed useless comment

* Removed unnecessary imports

* Rename

* Renaming, refactor and type safety

* Rename

* Removed import

---------

Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
2025-12-07 17:38:34 +01:00
lieght
f60df73b66 Loading circle, better loading colors. 2025-12-03 23:55:12 +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
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
f4678edf2f Merge remote-tracking branch 'origin/Development' into Development 2025-12-03 12:49:14 +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
ramollia
9a0bfc4fce Merge remote-tracking branch 'origin/Development' into Development 2025-12-02 11:25:42 +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
ramollia
0b750d5c0d Merge remote-tracking branch 'origin/Development' into Development
# Conflicts:
#	app/src/main/java/org/toop/app/view/views/GameView.java
2025-12-02 11:24:42 +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
ramollia
07f3319675 Merge remote-tracking branch 'origin/Development' into Development 2025-11-27 18:50:12 +01:00
ramollia
1dfa6a7c6e Merge remote-tracking branch 'origin/Development' into Development
# Conflicts:
#	app/src/main/java/org/toop/app/App.java
#	app/src/main/java/org/toop/app/canvas/ReversiCanvas.java
#	app/src/main/java/org/toop/app/game/ReversiGame.java
#	app/src/main/java/org/toop/app/game/TicTacToeGame.java
#	app/src/main/java/org/toop/app/view/displays/SongDisplay.java
#	app/src/main/java/org/toop/local/AppContext.java
2025-11-27 17:28:12 +01:00
144de5100e Revert "merge widgets with development"
This reverts commit 38681c5db0.
2025-11-27 16:58:54 +01:00
40ddf08e2d Revert "Merge remote-tracking branch 'origin/Development' into Development"
This reverts commit 59d46cb73c, reversing
changes made to 38681c5db0.
2025-11-27 16:58:51 +01:00
2936cc0b72 Revert "readd previous game thread code"
This reverts commit d24feef73e.
2025-11-27 16:58:47 +01:00
ramollia
d24feef73e readd previous game thread code 2025-11-27 16:53:58 +01:00
ramollia
59d46cb73c Merge remote-tracking branch 'origin/Development' into Development
# Conflicts:
#	app/src/main/java/org/toop/app/game/ReversiGame.java
#	app/src/main/java/org/toop/app/view/views/GameView.java
2025-11-27 15:58:22 +01:00
ramollia
38681c5db0 merge widgets with development 2025-11-27 15:57:46 +01:00
ramollia
cbe01fbca5 Merge branch 'Widgets' into Development
# Conflicts:
#	app/src/main/java/org/toop/app/App.java
#	app/src/main/java/org/toop/app/canvas/ReversiCanvas.java
#	app/src/main/java/org/toop/app/game/ReversiGame.java
#	app/src/main/java/org/toop/app/game/TicTacToeGame.java
#	app/src/main/java/org/toop/app/view/displays/SongDisplay.java
#	app/src/main/java/org/toop/local/AppContext.java
2025-11-27 14:44:00 +01:00
Bas Antonius de Jong
afb4844084 Merge pull request #164 from 2OOP/Development
Development update, demo 2
2025-10-07 19:57:50 +02:00
217 changed files with 7802 additions and 3888 deletions

View File

@@ -1,42 +1,42 @@
name: Checks #name: Checks
on: #on:
push: # push:
branches: # branches:
- 'main' # - 'main'
pull_request: # pull_request:
branches: # branches:
- 'main' # - 'main'
#
#jobs:
# formatting-check:
# name: Follow Google Formatting Guidelines
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v5
# with:
# fetch-depth: 0 # Fix for incremental formatting
# - uses: actions/setup-java@v5
# with:
# java-version: '25'
# distribution: 'temurin'
# cache: maven
# - name: Run Format Check
# run: mvn spotless:check
jobs: # tests:
formatting-check: # name: Unittests
name: Follow Google Formatting Guidelines # runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest # needs: formatting-check
steps: # strategy:
- uses: actions/checkout@v5 # matrix:
with: # os: [ubuntu-latest] #windows-latest, macos-latest
fetch-depth: 0 # Fix for incremental formatting # steps:
- uses: actions/setup-java@v5 # - uses: actions/checkout@v5
with: # - uses: actions/setup-java@v5
java-version: '25' # with:
distribution: 'temurin' # java-version: '25'
cache: maven # distribution: 'temurin'
- name: Run Format Check # cache: maven
run: mvn spotless:check # - name: Run Unittests
# run: mvn -B test
tests:
name: Unittests
runs-on: ${{ matrix.os }}
needs: formatting-check
strategy:
matrix:
os: [ubuntu-latest] #windows-latest, macos-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Run Unittests
run: mvn -B test

View File

@@ -2,7 +2,7 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.lang.foreign.Arena,ofAuto,java.lang.foreign.Arena,global,org.toop.framework.audio.AudioPlayer,play,java.util.Map,remove,java.util.concurrent.Executors,newSingleThreadScheduledExecutor" /> <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.lang.foreign.Arena,ofAuto,java.lang.foreign.Arena,global,org.toop.framework.audio.AudioPlayer,play,java.util.Map,remove,java.util.concurrent.Executors,newSingleThreadScheduledExecutor|newFixedThreadPool|newSingleThreadExecutor" />
</inspection_tool> </inspection_tool>
<inspection_tool class="WriteOnlyObject" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="WriteOnlyObject" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>

View File

@@ -1,40 +1,53 @@
package org.toop; package org.toop;
import org.toop.app.App; import org.toop.app.App;
import org.toop.framework.audio.*;
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.resources.MusicAsset;
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); // testMCTS(10);
} }
private static void initSystems() { // Voor onderzoek
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")); // private static void testMCTS(int games) {
new Thread(() -> new NetworkingClientEventListener(new NetworkingClientManager())).start(); // var random = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(), "Random AI");
// var v1 = new ArtificialPlayer<>(new MCTSAI<BitboardTicTacToe>(10), "MCTS V1 AI");
// var v2 = new ArtificialPlayer<>(new MCTSAI2<BitboardTicTacToe>(10), "MCTS V2 AI");
// var v2_2 = new ArtificialPlayer<>(new MCTSAI2<BitboardTicTacToe>(100), "MCTS V2_2 AI");
// var v3 = new ArtificialPlayer<>(new MCTSAI3<BitboardTicTacToe>(10), "MCTS V3 AI");
new Thread(() -> { // testAI(games, new Player[]{ v1, v2 });
MusicManager<MusicAsset> musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), true); // // testAI(games, new Player[]{ v1, v3 });
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<>( // // testAI(games, new Player[]{ random, v3 });
musicManager, // // testAI(games, new Player[]{ v2, v3 });
soundEffectManager, // testAI(games, new Player[]{ v2, v3 });
audioVolumeManager // // testAI(games, new Player[]{ v3, v2 });
).initListeners("medium-button-click.wav"); // }
}).start(); // private static void testAI(int games, Player<BitboardReversi>[] ais) {
} // int wins = 0;
// int ties = 0;
// for (int i = 0; i < games; i++) {
// final BitboardReversi match = new BitboardReversi(ais);
// while (!match.isTerminal()) {
// final int currentAI = match.getCurrentTurn();
// final long move = ais[currentAI].getMove(match);
// match.play(move);
// }
// if (match.getWinner() < 0) {
// ties++;
// continue;
// }
// wins += match.getWinner() == 0? 1 : 0;
// }
// System.out.printf("Out of %d games, %s won %d -- tied %d -- lost %d, games against %s\n", games, ais[0].getName(), wins, ties, games - wins - ties, ais[1].getName());
// System.out.printf("Average win rate was: %.2f\n\n", wins / (float)games);
// }
} }

View File

@@ -1,21 +1,40 @@
package org.toop.app; package org.toop.app;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
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.eventbus.GlobalEventBus;
import org.toop.framework.networking.connection.NetworkingClientEventListener;
import org.toop.framework.networking.connection.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;
import javafx.application.Application; import java.util.Objects;
import javafx.geometry.Pos; import java.util.concurrent.CountDownLatch;
import javafx.scene.Scene; import java.util.concurrent.ExecutorService;
import javafx.scene.layout.StackPane; import java.util.concurrent.Executors;
import javafx.stage.Stage;
public final class App extends Application { public final class App extends Application {
private static Stage stage; private static Stage stage;
@@ -24,65 +43,209 @@ 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) {
// 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();
stage.setMinWidth(1080); stage.setMinWidth(1200);
stage.setMinHeight(720); stage.setMinHeight(800);
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());
try {
loading.setAmount(e.hasLoadedAmount());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
if (e.hasLoadedAmount() >= e.isLoadingAmount()-1) {
Platform.runLater(loading::triggerSuccess);
loadingFlow.unsubscribe("init_loading");
}
});
}, false, "init_loading");
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
executor.submit(
() -> ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")
));
} finally {
executor.shutdown();
}
stage.show();
}
private void setKeybinds(StackPane root) {
root.addEventHandler(KeyEvent.KEY_PRESSED,event -> {
if (event.getCode() == KeyCode.ESCAPE) {
escapePopup();
}
});
stage.setFullScreenExitKeyCombination(
new KeyCodeCombination(
KeyCode.F11
)
);
} }
public static void startQuit() { public void escapePopup() {
if (isQuitting) {
if ( WidgetContainer.getCurrentView() == null
|| WidgetContainer.getCurrentView() instanceof MainView) {
return; return;
} }
WidgetContainer.add(Pos.CENTER, new QuitPopup()); if (!Objects.requireNonNull(
isQuitting = true; WidgetContainer.find(widget -> widget instanceof QuitPopup || widget instanceof EscapePopup)
).isEmpty()) {
WidgetContainer.removeFirst(QuitPopup.class);
WidgetContainer.removeFirst(EscapePopup.class);
return;
}
EscapePopup escPopup = new EscapePopup();
escPopup.show(Pos.CENTER);
} }
public static void stopQuit() { private void setOnLoadingSuccess(LoadingWidget loading) {
isQuitting = false; loading.setOnSuccess(() -> {
try {
initSystems();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
AppSettings.applyMusicVolumeSettings();
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent();
loading.hide();
WidgetContainer.add(Pos.CENTER, new MainView());
WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay());
stage.setOnCloseRequest(event -> {
event.consume();
if (WidgetContainer.getAllWidgets().stream().anyMatch(e -> e instanceof QuitPopup)) return;
QuitPopup a = new QuitPopup();
a.show(Pos.CENTER);
});
});
} }
private void initSystems() throws InterruptedException { // TODO Move to better place
final int THREAD_COUNT = 2;
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
@SuppressWarnings("resource")
ExecutorService threads = Executors.newFixedThreadPool(THREAD_COUNT);
try {
threads.submit(() -> {
new NetworkingClientEventListener(
GlobalEventBus.get(),
new NetworkingClientManager(GlobalEventBus.get()));
latch.countDown();
});
threads.submit(() -> {
MusicManager<MusicAsset> musicManager =
new MusicManager<>(
GlobalEventBus.get(),
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<>(
GlobalEventBus.get(),
musicManager,
soundEffectManager,
audioVolumeManager
).initListeners("medium-button-click.wav");
latch.countDown();
});
} finally {
latch.await();
threads.shutdown();
}
}
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,19 +1,28 @@
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.networking.clients.TournamentNetworkingClient; import org.toop.framework.game.players.OnlinePlayer;
import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.networking.types.NetworkingConnector; import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.networking.connection.clients.TournamentNetworkingClient;
import org.toop.framework.networking.connection.events.NetworkEvents;
import org.toop.framework.networking.connection.types.NetworkingConnector;
import org.toop.framework.networking.server.gateway.NettyGatewayServer;
import org.toop.framework.game.players.LocalPlayer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -22,6 +31,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public final class Server { public final class Server {
private NettyGatewayServer nettyGatewayServer;
private String user = ""; private String user = "";
private long clientId = -1; private long clientId = -1;
@@ -31,25 +42,31 @@ 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;
} }
public Server(String ip, String port, String user) { public Server(String ip, String port, String user) {
this(ip, port, user, 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, NettyGatewayServer nettyGatewayServer) {
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"));
return; return;
@@ -69,43 +86,109 @@ public final class Server {
return; return;
} }
new EventFlow() this.nettyGatewayServer = nettyGatewayServer;
final int reconnectAttempts = 10;
LoadingWidget loading = new LoadingWidget(
Primitive.text("connecting"), 0, 0, reconnectAttempts, true, true
);
WidgetContainer.getCurrentView().transitionNextCustom(loading, "disconnect", this::disconnect);
var a = new EventFlow()
.addPostEvent(NetworkEvents.StartClient.class, .addPostEvent(NetworkEvents.StartClient.class,
new TournamentNetworkingClient(), new TournamentNetworkingClient(GlobalEventBus.get()),
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.CreatedIdForClient.class, e -> clientId = e.clientId(), true);
WidgetContainer.getCurrentView().transitionNext(primary);
startPopulateScheduler(); a.onResponse(NetworkEvents.StartClientResponse.class, e -> {
populateGameList(); if (!e.successful()) {
return;
}
}).postEvent(); primary = new ServerView(user, this::sendChallenge, user, clientId);
new EventFlow().listen(this::handleReceivedChallenge) WidgetContainer.getCurrentView().transitionNextCustom(primary, "disconnect", this::disconnect);
.listen(this::handleMatchResponse);
a.unsubscribe("connecting");
a.unsubscribe("startclient");
this.user = user;
new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent();
startPopulateScheduler();
populateGameList();
primary.removeViewFromPreviousChain(loading);
}, false, "startclient")
.listen(
NetworkEvents.ConnectTry.class,
e -> {
if (clientId != e.clientId()) return;
Platform.runLater(
() -> {
try {
loading.setAmount(e.amount());
if (e.amount() >= loading.getMaxAmount()) {
loading.triggerFailure();
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
);
},
false, "connecting"
)
.postEvent();
a.listen(NetworkEvents.ChallengeResponse.class, this::handleReceivedChallenge, false, "challenge")
.listen(NetworkEvents.GameMatchResponse.class, this::handleMatchResponse, false, "match-response")
.listen(NetworkEvents.GameResultResponse.class, this::handleGameResult, false, "game-result")
.listen(NetworkEvents.GameMoveResponse.class, this::handleReceivedMove, false, "game-move")
.listen(NetworkEvents.YourTurnResponse.class, this::handleYourTurn, false, "your-turn")
.listen(NetworkEvents.ClosedConnection.class, this::closedConnection, false, "closed-connection")
.listen(NetworkEvents.TournamentResultResponse.class, this::handleTournamentResult, false, "tournament-result");
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();
@@ -116,42 +199,74 @@ public final class Server {
return; return;
} }
final int myTurn = response.playerToMove().equalsIgnoreCase(response.opponent()) ? 1 : 0; final String startingPlayer = response.playerToMove();
final int userStartingTurn = startingPlayer.equalsIgnoreCase(user) ? 0 : 1;
final int opponentStartingTurn = 1 - userStartingTurn;
final GameInformation information = new GameInformation(type); final GameInformation information = new GameInformation(type);
//information.players[0] = playerInformation; information.players[userStartingTurn].name = user;
information.players[0].name = user; information.players[opponentStartingTurn].name = response.opponent();
information.players[0].isHuman = false;
information.players[0].computerDifficulty = 5;
information.players[0].computerThinkTime = 1;
information.players[1].name = response.opponent();
Runnable onGameOverRunnable = isSingleGame.get()? null: this::gameOver;
Player[] players = new Player[2];
players[userStartingTurn] = new LocalPlayer(user);
players[opponentStartingTurn] = new OnlinePlayer(response.opponent());
switch (type) { switch (type) {
case TICTACTOE -> case TICTACTOE -> gameController = new TicTacToeBitController(players);
new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case REVERSI -> gameController = new ReversiBitController(players);
case REVERSI ->
new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable);
case CONNECT4 ->
new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable);
default -> new ErrorPopup("Unsupported game type."); default -> new ErrorPopup("Unsupported game type.");
}
if (gameController != null) {
primary.reEnableButton(); // Re enable subscribe button
gameController.start();
isPolling = true; // Fixes server getting stuck
} }
} }
} }
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 handleTournamentResult(NetworkEvents.TournamentResultResponse response) {
IO.println(response.gameType());
IO.println(Arrays.toString(response.names()));
IO.println(Arrays.toString(response.scores()));
}
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 long challengeId = Long.parseLong(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 +277,27 @@ 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();
if (nettyGatewayServer != null) {
nettyGatewayServer.stop();
}
WidgetContainer.getCurrentView().transitionPrevious();
}
private void closedConnection(NetworkEvents.ClosedConnection e) {
new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent();
isPolling = false;
stopScheduler();
connectFlow.unsubscribeAll();
if (nettyGatewayServer != null) {
nettyGatewayServer.stop();
}
WidgetContainer.getCurrentView().transitionPrevious();
WidgetContainer.add(Pos.CENTER, new ErrorPopup("Server closed connection."));
} }
private void forfeitGame() { private void forfeitGame() {
@@ -200,7 +335,7 @@ public final class Server {
} else { } else {
stopScheduler(); stopScheduler();
} }
}, 0, 5, TimeUnit.SECONDS); }, 0, 1, TimeUnit.SECONDS);
} }
private void stopScheduler() { private void stopScheduler() {
@@ -211,7 +346,10 @@ public final class Server {
private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) {
gameList.clear(); gameList.clear();
gameList.addAll(List.of(event.gamelist())); var gl = new java.util.ArrayList<>(List.of(event.gamelist()));
gl.sort(String::compareTo);
gameList.addAll(gl);
primary.updateGameList(gl);
} }
public void populateGameList() { public void populateGameList() {

View File

@@ -0,0 +1,245 @@
package org.toop.app.canvas;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.util.Duration;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.view.GUIEvents;
import java.util.function.Consumer;
public abstract class BitGameCanvas implements GameCanvas {
protected record Cell(float x, float y, float width, float height) {
public boolean isInside(double x, double y) {
return x >= this.x && x <= this.x + width &&
y >= this.y && y <= this.y + height;
}
}
protected final Canvas canvas;
protected final GraphicsContext graphics;
protected final Color color;
protected final Color backgroundColor;
protected final int width;
protected final int height;
protected final int rowSize;
protected final int columnSize;
protected final int gapSize;
protected final boolean edges;
protected final Cell[] cells;
private Consumer<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,9 @@
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 {
import java.util.function.Consumer; Canvas getCanvas();
void redraw(TurnBasedGame gameCopy);
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,39 @@
package org.toop.app.canvas;
import javafx.scene.paint.Color;
import org.toop.app.App;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public class ReversiBitCanvas extends BitGameCanvas {
public ReversiBitCanvas() {
super(Color.GRAY, new Color(0f, 0.4f, 0.2f, 1f), (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, 8, 8, 5, true);
canvas.setOnMouseMoved(event -> {
double mouseX = event.getX();
double mouseY = event.getY();
int cellId = -1;
BitGameCanvas.Cell hovered = null;
for (BitGameCanvas.Cell cell : cells) {
if (cell.isInside(mouseX, mouseY)) {
hovered = cell;
cellId = turnCoordsIntoCellId(mouseX, mouseY);
break;
}
}
});
}
private int turnCoordsIntoCellId(double x, double y) {
final int column = (int) ((x / this.width) * rowSize);
final int row = (int) ((y / this.height) * columnSize);
return column + row * rowSize;
}
@Override
public void redraw(TurnBasedGame 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,60 @@
package org.toop.app.canvas;
import javafx.scene.paint.Color;
import org.toop.app.App;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public class TicTacToeBitCanvas extends BitGameCanvas{
public TicTacToeBitCanvas() {
super(
Color.GRAY,
Color.TRANSPARENT,
(App.getHeight() / 4) * 3,
(App.getHeight() / 4) * 3,
3,
3,
30,
false
);
}
@Override
public void redraw(TurnBasedGame 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,158 @@
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.threadBehaviour.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.connection.events.NetworkEvents;
import org.toop.framework.game.players.LocalPlayer;
public class GenericGameController implements GameController {
protected final EventFlow eventFlow = new EventFlow();
// Logger for logging
protected final Logger logger = LogManager.getLogger(this.getClass());
// Reference to gameView view
protected final GameView gameView;
// Reference to game canvas
protected final GameCanvas canvas;
protected final TurnBasedGame game; // Reference to game instance
private final ThreadBehaviour gameThreadBehaviour;
// TODO: Change gameType to automatically happen with either dependency injection or something else.
public GenericGameController(GameCanvas canvas, TurnBasedGame game, ThreadBehaviour gameThreadBehaviour, String gameType) {
logger.info("Creating: {}", this.getClass());
this.canvas = canvas;
this.game = game;
this.gameThreadBehaviour = gameThreadBehaviour;
// Tell thread how to send moves
this.gameThreadBehaviour.setOnSendMove(
(id, m) -> GlobalEventBus.get().post(new NetworkEvents.SendMove(id, (short)translateMove(m)))
);
// Tell thread how to update UI
this.gameThreadBehaviour.setOnUpdateUI(() -> Platform.runLater(this::updateUI));
// Change scene to game view
gameView = new GameView(null, null, null, gameType);
gameView.add(Pos.CENTER, canvas.getCanvas());
WidgetContainer.getCurrentView().transitionNext(gameView, true);
// Listen to updates
logger.info("Game controller started listening");
eventFlow
.listen(GUIEvents.GameEnded.class, this::onGameFinish, false)
.listen(GUIEvents.PlayerAttemptedMove.class, event -> {
logger.info("User attempting move {}", event.move());
logger.info("Current player's turn {}", getCurrentPlayer().getName());
logger.info("First player {}", game.getPlayer(0).getName());
logger.info("Username {}", getCurrentPlayer().getName());
logger.info("User is class {}, {}", getCurrentPlayer().getClass(), getCurrentPlayer() instanceof LocalPlayer);
if (getCurrentPlayer() instanceof LocalPlayer lp) {
try {
lp.setLastMove(event.move());
} catch (Exception e) {
IO.println(e);
}
}
}, false);
}
public void start(){
logger.info("Starting GameManager");
updateUI();
gameThreadBehaviour.start();
logger.info("GameManager started");
}
public void stop(){
logger.info("Stopping GameManager");
removeListeners();
gameThreadBehaviour.stop();
logger.info("GameManager stopped");
}
public Player getCurrentPlayer(){
return game.getPlayer(getCurrentPlayerIndex());
}
public int getCurrentPlayerIndex(){
return game.getCurrentTurn();
}
protected long translateMove(int move){
return 1L << move;
}
protected int translateMove(long move){
return Long.numberOfTrailingZeros(move);
}
private void removeListeners(){
eventFlow.unsubscribeAll();
}
private void onGameFinish(GUIEvents.GameEnded event){
logger.info("OnlineTurnBasedGame Finished");
String name = event.winner() == -1 ? null : getPlayer(event.winner()).getName();
gameView.gameOver(event.winOrTie(), name);
stop();
}
public Player getPlayer(int player){
if (player < 0 || player > game.getPlayerCount()-1){ // 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,23 @@
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.framework.game.gameThreads.LocalThreadBehaviour;
import org.toop.framework.game.gameThreads.OnlineThreadBehaviour;
import org.toop.framework.game.games.reversi.BitboardReversi;
import org.toop.framework.game.players.OnlinePlayer;
import java.util.Arrays;
public class ReversiBitController extends GenericGameController {
public ReversiBitController(Player[] players) {
BitboardReversi game = new BitboardReversi();
game.init(players);
ThreadBehaviour thread = Arrays.stream(players).anyMatch(e -> e instanceof OnlinePlayer) ?
new OnlineThreadBehaviour(game) : new LocalThreadBehaviour(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.framework.game.gameThreads.LocalThreadBehaviour;
import org.toop.framework.game.gameThreads.OnlineThreadBehaviour;
import org.toop.framework.game.games.tictactoe.BitboardTicTacToe;
import org.toop.framework.game.players.OnlinePlayer;
import java.util.Arrays;
public class TicTacToeBitController extends GenericGameController {
public TicTacToeBitController(Player[] players) {
BitboardTicTacToe game = new BitboardTicTacToe();
game.init(players);
ThreadBehaviour thread = Arrays.stream(players).anyMatch(e -> e instanceof OnlinePlayer) ?
new OnlineThreadBehaviour(game) : new LocalThreadBehaviour(game);
super(new TicTacToeBitCanvas(), game, thread, "TicTacToe");
}
}

View File

@@ -1,11 +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.util.function.Consumer; import java.util.function.Consumer;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@@ -22,33 +22,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 +64,37 @@ 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, boolean disableOnClick) {
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()); if (disableOnClick) button.setDisable(true);
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, boolean disableOnClick) {
return button(key, onAction, true, disableOnClick);
}
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,6 @@
package org.toop.app.widget; package org.toop.app.widget;
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 +8,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 +43,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 +57,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 +127,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

@@ -26,7 +26,7 @@ public class ConfirmWidget implements Widget {
public void addButton(String key, Runnable onClick) { public void addButton(String key, Runnable onClick) {
Platform.runLater(() -> { Platform.runLater(() -> {
var button = Primitive.button(key, onClick); var button = Primitive.button(key, onClick, false);
buttonsContainer.getChildren().add(button); buttonsContainer.getChildren().add(button);
}); });
} }

View File

@@ -0,0 +1,162 @@
package org.toop.app.widget.complex;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
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,10 +19,11 @@ public class PlayerInfoWidget {
buildToggle().getNode(), buildToggle().getNode(),
buildContent() buildContent()
); );
this.playerName = null;
} }
private ToggleWidget buildToggle() { private ToggleWidget buildToggle() {
return new ToggleWidget( return new ToggleWidget(
"computer", "player", "computer", "player",
information.isHuman, information.isHuman,
isHuman -> { isHuman -> {
@@ -33,51 +37,76 @@ public class PlayerInfoWidget {
} }
private Node buildContent() { private Node buildContent() {
if (information.isHuman) {
var nameInput = new LabeledInputWidget(
"name",
"enter-your-name",
information.name,
newName -> information.name = newName
);
return nameInput.getNode(); if (information.isHuman) {
} else { var spacer = Primitive.vbox(
if (information.name == null || information.name.isEmpty()) { makeAIButton(0, 0, "zwartepiet"),
information.name = "Pism Bot"; makeAIButton(0, 0, "sinterklaas"),
} makeAIButton(0, 0, "santa")
); //todo make a better solution
spacer.setVisible(false);
var nameInput = new LabeledInputWidget(
"name",
"enter-your-name",
information.name,
newName -> information.name = newName
);
var playerName = Primitive.text(""); return Primitive.vbox(spacer,nameInput.getNode());
playerName.setText(information.name); } else {
var AIBox = Primitive.vbox(
makeAIButton(0, 1, "zwartepiet"),
makeAIButton(2, 1, "sinterklaas"),
makeAIButton(9, 1, "santa")
);
var nameDisplay = Primitive.vbox( this.playerName = Primitive.text("");
Primitive.text("name"), playerName.setText(information.name);
playerName
);
var difficultySlider = new LabeledSliderWidget( var nameDisplay = Primitive.vbox(
"computer-difficulty", Primitive.text("name"),
0, 5, playerName
information.computerDifficulty, );
newVal -> information.computerDifficulty = newVal
);
var thinkTimeSlider = new LabeledSliderWidget( if (!hasSet) {
"computer-think-time", doDefault();
0, 5, hasSet = true;
information.computerThinkTime, }
newVal -> information.computerThinkTime = newVal
);
return Primitive.vbox( return Primitive.vbox(
nameDisplay, AIBox,
difficultySlider.getNode(), nameDisplay
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));
}, false);
}
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

@@ -0,0 +1,61 @@
package org.toop.app.widget.complex;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import java.util.ArrayList;
import java.util.List;
public class TableWidget<DATATYPE> extends PopupWidget {
private ObservableList<DATATYPE> serverList = FXCollections.observableArrayList();
private TableView<DATATYPE> table = new TableView<>();
public TableWidget(String... columns) {
var cols = new ArrayList<TableColumn<DATATYPE, String>>();
for (String column : columns) {
TableColumn<DATATYPE, String> col = new TableColumn<>(column.toUpperCase());
col.setCellValueFactory(new PropertyValueFactory<>(column));
cols.add(col);
}
table.getColumns().addAll(cols);
update();
onColumnClicked();
add(Pos.CENTER, table);
}
public void add(DATATYPE serverFound) {
serverList.add(serverFound);
update();
}
public void add(List<DATATYPE> serverFound) {
serverList.addAll(serverFound);
}
public void remove(DATATYPE serverFound) {
serverList.remove(serverFound);
update();
}
public void onColumnClicked() {
table.setOnMouseClicked(event -> {
DATATYPE selected = table.getSelectionModel().getSelectedItem();
if (selected == null) return;
IO.println(selected.toString());
});
}
private void update() {
table.setItems(serverList);
}
}

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,17 +16,40 @@ 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", () -> {
view.transitionPrevious(); view.transitionPrevious();
}); }, false);
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();
}, false);
view.add(Pos.BOTTOM_LEFT, Primitive.vbox(customButton));
}
public void transitionPrevious() { public void transitionPrevious() {
if (previous == null) { if (previous == null) {
return; return;
@@ -36,13 +59,45 @@ 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);
var backButton = Primitive.button("back", () -> { var backButton = Primitive.button("back", () -> {
view.transitionPrevious(); view.transitionPrevious();
}); }, false);
view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton));
} }

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");
@@ -49,11 +49,11 @@ public class SongDisplay extends VBox implements Widget {
previousButton.getStyleClass().setAll("previous-button"); previousButton.getStyleClass().setAll("previous-button");
skipButton.setOnAction( event -> { skipButton.setOnAction( event -> {
GlobalEventBus.post(new AudioEvents.SkipMusic()); GlobalEventBus.get().post(new AudioEvents.SkipMusic());
}); });
pauseButton.setOnAction(event -> { pauseButton.setOnAction(event -> {
GlobalEventBus.post(new AudioEvents.PauseMusic()); GlobalEventBus.get().post(new AudioEvents.PauseMusic());
if (pauseButton.getText().equals("")) { if (pauseButton.getText().equals("")) {
pauseButton.setText(""); pauseButton.setText("");
} }
@@ -63,7 +63,7 @@ public class SongDisplay extends VBox implements Widget {
}); });
previousButton.setOnAction( event -> { previousButton.setOnAction( event -> {
GlobalEventBus.post(new AudioEvents.PreviousMusic()); GlobalEventBus.get().post(new AudioEvents.PreviousMusic());
}); });
HBox control = new HBox(10, previousButton, pauseButton, skipButton); HBox control = new HBox(10, previousButton, pauseButton, skipButton);

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", () -> {
var denyButton = Primitive.button("deny", () -> hide()); onAccept.accept(playerInformation);
this.hide();
}, false);
var denyButton = Primitive.button("deny", () -> hide(), false);
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,48 @@
package org.toop.app.widget.popup;
import javafx.geometry.Pos;
import javafx.scene.Node;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.Widget;
import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.complex.PopupWidget;
import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.view.GameView;
import org.toop.app.widget.view.OptionsView;
import org.toop.local.AppContext;
import java.util.ArrayList;
public class EscapePopup extends PopupWidget {
public EscapePopup() {
ViewWidget currentView = WidgetContainer.getCurrentView();
ArrayList<Node> nodes = new ArrayList<>();
nodes.add(Primitive.button("Continue", this::hide, false, false)); // TODO, localize
if (!(currentView.getClass().isAssignableFrom(OptionsView.class))) {
var opt = Primitive.button("options", () -> {
hide();
WidgetContainer.getCurrentView().transitionNext(new OptionsView());
}, false);
nodes.add(opt);
}
if (currentView.getClass().isAssignableFrom(GameView.class)) {
Widget tut = AppContext.currentTutorial();
if (tut != null) {
nodes.add(Primitive.button("tutorialstring", () -> {
WidgetContainer.getCurrentView().add(Pos.CENTER, tut);
}, false));
}
}
nodes.add(Primitive.button("quit", () -> {
hide();
WidgetContainer.add(Pos.CENTER, new QuitPopup());
}, false));
add(Pos.CENTER, Primitive.vbox(nodes.toArray(new Node[0])));
}
}

View File

@@ -2,23 +2,21 @@ package org.toop.app.widget.popup;
import org.toop.app.widget.complex.ConfirmWidget; import org.toop.app.widget.complex.ConfirmWidget;
import org.toop.app.widget.complex.PopupWidget; import org.toop.app.widget.complex.PopupWidget;
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,10 +61,10 @@ 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(); }
); , false);
var cancelButton = Primitive.button("cancel", () -> hide()); var cancelButton = Primitive.button("cancel", () -> hide(), false);
var leftSection = Primitive.vbox( var leftSection = Primitive.vbox(
challengeText, challengeText,

View File

@@ -1,60 +1,112 @@
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.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(); }, false);
nextButton = Primitive.button(">", () -> update(true), false);
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));
} }
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(); }, false),
Primitive.button("no", () -> { appSettingsSetter.run(); nextScreen.run(); this.hide(); }, false),
Primitive.button("never", () -> { AppSettings.getSettings().setTutorialFlag(false); nextScreen.run(); this.hide(); }, false)
);
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,37 +1,46 @@
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 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(), false);
} else { } else {
forfeitButton = null; forfeitButton = null;
} }
@@ -39,7 +48,7 @@ public final class GameView extends ViewWidget {
exitButton = Primitive.button("exit", () -> { exitButton = Primitive.button("exit", () -> {
onExit.run(); onExit.run();
transitionPrevious(); transitionPrevious();
}); }, false);
if (onMessage != null) { if (onMessage != null) {
chatInput = Primitive.input("enter-your-message", "", null); chatInput = Primitive.input("enter-your-message", "", null);
@@ -51,24 +60,15 @@ public final class GameView extends ViewWidget {
chatInput = null; chatInput = null;
} }
switch(gameType) { switch (gameType) {
case "TicTacToe": case "TicTacToe":
this.tutorialButton = Primitive.button("tutorialstring", () -> { AppContext.setCurrentTutorial(new TicTacToeTutorialWidget(() -> {}));
transitionNext(new TicTacToeTutorialWidget());
});
break; break;
case "Reversi": case "Reversi":
this.tutorialButton = Primitive.button("tutorialstring", () -> { AppContext.setCurrentTutorial(new ReversiTutorialWidget(() -> {}));
transitionNext(new ReversiTutorialWidget());
});
break; break;
case "Connect4": case "Connect4":
this.tutorialButton = Primitive.button("tutorialstring", () -> { AppContext.setCurrentTutorial(new Connect4TutorialWidget(() -> {}));
transitionNext(new Connect4TutorialWidget());
});
break;
default:
this.tutorialButton = null;
break; break;
} }
@@ -76,17 +76,11 @@ public final class GameView extends ViewWidget {
} }
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,
@@ -98,27 +92,88 @@ public final class GameView extends ViewWidget {
if (chatInput != null) { if (chatInput != null) {
add(Pos.BOTTOM_RIGHT, Primitive.vbox(chatInput)); add(Pos.BOTTOM_RIGHT, Primitive.vbox(chatInput));
} }
if (tutorialButton != null) {
add(Pos.TOP_LEFT, tutorialButton);
}
} }
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,33 @@ 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.gameControllers.ReversiBitController;
import org.toop.app.game.ReversiGame; import org.toop.app.gameControllers.TicTacToeBitController;
import org.toop.app.game.TicTacToeGameThread; import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.players.ArtificialPlayer;
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.framework.game.players.LocalPlayer;
import org.toop.app.widget.tutorial.ReversiTutorialWidget; import org.toop.game.players.ai.MCTSAI;
import org.toop.app.widget.tutorial.TicTacToeTutorialWidget; import org.toop.game.players.ai.MCTSAI2;
import org.toop.game.players.ai.MCTSAI3;
import org.toop.game.players.ai.MiniMaxAI;
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;
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 +36,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 +46,69 @@ 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 MCTSAI(100), "MCTS 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(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");
players[0] = new ArtificialPlayer(new MCTSAI3(50), "MCTS V3 AI");
}
if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name);
} else {
players[1] = new ArtificialPlayer(new MCTSAI2(50), "MCTS V2 AI");
}
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()) { }, false);
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

@@ -10,20 +10,15 @@ public class LocalView extends ViewWidget {
public LocalView() { public LocalView() {
var ticTacToeButton = Primitive.button("tic-tac-toe", () -> { var ticTacToeButton = Primitive.button("tic-tac-toe", () -> {
transitionNext(new LocalMultiplayerView(GameInformation.Type.TICTACTOE)); transitionNext(new LocalMultiplayerView(GameInformation.Type.TICTACTOE));
}); }, false);
var reversiButton = Primitive.button("reversi", () -> { var reversiButton = Primitive.button("reversi", () -> {
transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI)); transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI));
}); }, false);
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

@@ -1,31 +1,32 @@
package org.toop.app.widget.view; package org.toop.app.widget.view;
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() {
var localButton = Primitive.button("local", () -> { var localButton = Primitive.button("local", () -> {
transitionNext(new LocalView()); transitionNext(new LocalView());
}); }, false);
var onlineButton = Primitive.button("online", () -> { var onlineButton = Primitive.button("online", () -> {
transitionNext(new OnlineView()); transitionNext(new OnlineView());
}); }, false);
var creditsButton = Primitive.button("credits", () -> { var creditsButton = Primitive.button("credits", () -> {
transitionNext(new CreditsView()); transitionNext(new CreditsView());
}); }, false);
var optionsButton = Primitive.button("options", () -> { var optionsButton = Primitive.button("options", () -> {
transitionNext(new OptionsView()); transitionNext(new OptionsView());
}); }, false);
var quitButton = Primitive.button("quit", () -> { var quitButton = Primitive.button("quit", () -> {
App.startQuit(); var a = new QuitPopup();
}); a.show(Pos.CENTER);
}, false);
add(Pos.CENTER, Primitive.vbox( add(Pos.CENTER, Primitive.vbox(
localButton, localButton,

View File

@@ -6,6 +6,14 @@ import org.toop.app.widget.complex.LabeledInputWidget;
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.framework.game.games.reversi.BitboardReversi;
import org.toop.framework.game.games.tictactoe.BitboardTicTacToe;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.networking.server.gateway.NettyGatewayServer;
import org.toop.framework.networking.server.stores.TurnBasedGameTypeStore;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
public class OnlineView extends ViewWidget { public class OnlineView extends ViewWidget {
public OnlineView() { public OnlineView() {
@@ -21,7 +29,31 @@ public class OnlineView extends ViewWidget {
serverPortInput.getValue(), serverPortInput.getValue(),
playerNameInput.getValue() playerNameInput.getValue()
); );
}); }, false);
var localHostButton = Primitive.button("host!", () -> {
var tps = new TurnBasedGameTypeStore();
tps.register("tic-tac-toe", BitboardTicTacToe::new);
tps.register("reversi", BitboardReversi::new);
var a = new NettyGatewayServer(6666, tps, Duration.ofSeconds(10));
new Thread(() -> {
try {
a.start();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Server(
"127.0.0.1",
"6666",
"host",
a
);
}, false, false);
add(Pos.CENTER, Primitive.vbox( add(Pos.CENTER, Primitive.vbox(
serverInformationHeader, serverInformationHeader,
@@ -32,7 +64,9 @@ public class OnlineView extends ViewWidget {
playerNameInput.getNode(), playerNameInput.getNode(),
Primitive.separator(), Primitive.separator(),
connectButton connectButton,
Primitive.separator(),
localHostButton
)); ));
} }
} }

View File

@@ -74,7 +74,7 @@ public class OptionsView extends ViewWidget {
AppSettings.getSettings().setVolume(val); AppSettings.getSettings().setVolume(val);
new EventFlow() new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MASTERVOLUME)) .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MASTERVOLUME))
.asyncPostEvent(); .postEvent();
} }
); );
@@ -86,7 +86,7 @@ public class OptionsView extends ViewWidget {
AppSettings.getSettings().setFxVolume(val); AppSettings.getSettings().setFxVolume(val);
new EventFlow() new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.FX)) .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.FX))
.asyncPostEvent(); .postEvent();
} }
); );
@@ -98,7 +98,7 @@ public class OptionsView extends ViewWidget {
AppSettings.getSettings().setMusicVolume(val); AppSettings.getSettings().setMusicVolume(val);
new EventFlow() new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MUSIC)) .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MUSIC))
.asyncPostEvent(); .postEvent();
} }
); );

View File

@@ -1,50 +1,89 @@
package org.toop.app.widget.view; package org.toop.app.widget.view;
import javafx.collections.FXCollections;
import javafx.css.converter.StringConverter;
import javafx.scene.control.ComboBox;
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 java.io.Reader;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
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.ListView; import javafx.scene.control.ListView;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.networking.connection.events.NetworkEvents;
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 long clientId;
private final ComboBox<String> gameListSub;
private final ComboBox<String> gameListTour;
private final ListView<Button> listView; private final ListView<Button> listView;
private Button subscribeButton;
public ServerView(String user, Consumer<String> onPlayerClicked, Runnable onDisconnect) { public ServerView(String user, Consumer<String> onPlayerClicked, String userName, long clientId) {
this.user = user; this.user = user;
this.onPlayerClicked = onPlayerClicked; this.onPlayerClicked = onPlayerClicked;
this.onDisconnect = onDisconnect; this.clientId = clientId;
this.gameListSub = new ComboBox<>();
this.gameListTour = new ComboBox<>();
this.listView = new ListView<>(); this.listView = new ListView<>();
setupLayout(); setupLayout(userName);
} }
private void setupLayout() { private void setupLayout(String userName) {
var playerHeader = Primitive.header(user); var playerHeader = Primitive.header(user, false);
var playerListSection = Primitive.vbox( if (userName.equals("host")) { // TODO is fragile
playerHeader, var tournamentButton = Primitive.hbox(
Primitive.separator(), gameListTour,
listView Primitive.button(
); "tournament",
() -> GlobalEventBus.get().post(new NetworkEvents.SendCommand(clientId, "tournament", "start", gameListTour.getValue())),
false,
false
)
);
add(Pos.CENTER, playerListSection); add(Pos.BOTTOM_CENTER, tournamentButton);
} else {
subscribeButton = Primitive.button(
"subscribe",
() -> new EventFlow().addPostEvent(new NetworkEvents.SendSubscribe(clientId, gameListSub.getValue())).postEvent(),
false,
true
); // TODO localize
var disconnectButton = Primitive.button("disconnect", () -> { var subscribe = Primitive.hbox(gameListSub, subscribeButton);
onDisconnect.run();
transitionPrevious();
});
add(Pos.BOTTOM_LEFT, Primitive.vbox(disconnectButton)); var playerListSection = Primitive.vbox(
playerHeader,
Primitive.separator(),
subscribe,
listView
);
add(Pos.CENTER, playerListSection);
var disconnectButton = Primitive.button(
"disconnect",
this::transitionPrevious,
false
);
add(Pos.BOTTOM_LEFT, Primitive.vbox(disconnectButton));
}
} }
public void update(List<String> players) { public void update(List<String> players) {
@@ -52,9 +91,25 @@ 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, false);
listView.getItems().add(playerButton); listView.getItems().add(playerButton);
} }
}); });
} }
public void updateGameList(List<String> games) {
Platform.runLater(() -> {
gameListSub.getItems().clear();
gameListSub.setItems(FXCollections.observableArrayList(games));
gameListSub.getSelectionModel().select(0);
gameListTour.getItems().clear();
gameListTour.setItems(FXCollections.observableArrayList(games));
gameListTour.getSelectionModel().select(0);
});
}
public void reEnableButton() {
subscribeButton.setDisable(false);
}
} }

View File

@@ -1,10 +1,10 @@
package org.toop.local; package org.toop.local;
import java.util.Locale;
import java.util.MissingResourceException; import java.util.MissingResourceException;
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.app.widget.tutorial.BaseTutorialWidget;
import org.toop.framework.resource.ResourceManager; import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.resources.LocalizationAsset; import org.toop.framework.resource.resources.LocalizationAsset;
@@ -16,11 +16,13 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
public class AppContext { public class AppContext {
private static final Logger logger = LogManager.getLogger(AppContext.class);
private static final LocalizationAsset localization = ResourceManager.get("localization"); private static final LocalizationAsset localization = ResourceManager.get("localization");
private static Locale locale = Locale.forLanguageTag("en"); private static Locale locale = Locale.forLanguageTag("en");
private static final ObjectProperty<Locale> localeProperty = new SimpleObjectProperty<>(locale); private static final ObjectProperty<Locale> localeProperty = new SimpleObjectProperty<>(locale);
private static final Logger logger = LogManager.getLogger(AppContext.class); private static BaseTutorialWidget tutorialWidget;
public static LocalizationAsset getLocalization() { public static LocalizationAsset getLocalization() {
return localization; return localization;
@@ -58,10 +60,27 @@ 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);
}
public static void setCurrentTutorial(BaseTutorialWidget tutorial) {
AppContext.tutorialWidget = tutorial;
}
public static BaseTutorialWidget currentTutorial() {
return AppContext.tutorialWidget;
}
} }

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))
.postEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX))
.postEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC))
.postEvent();
}
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

@@ -147,7 +147,6 @@
<version>2.42.0</version> <version>2.42.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>

View File

@@ -5,37 +5,39 @@ import org.toop.framework.audio.interfaces.MusicManager;
import org.toop.framework.audio.interfaces.SoundEffectManager; import org.toop.framework.audio.interfaces.SoundEffectManager;
import org.toop.framework.audio.interfaces.VolumeManager; import org.toop.framework.audio.interfaces.VolumeManager;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
public class AudioEventListener<T extends AudioResource, K extends AudioResource> { public class AudioEventListener<T extends AudioResource, K extends AudioResource> {
private final EventBus eventBus;
private final MusicManager<T> musicManager; private final MusicManager<T> musicManager;
private final SoundEffectManager<K> soundEffectManager; private final SoundEffectManager<K> soundEffectManager;
private final VolumeManager audioVolumeManager; private final VolumeManager audioVolumeManager;
public AudioEventListener( public AudioEventListener(
EventBus eventBus,
MusicManager<T> musicManager, MusicManager<T> musicManager,
SoundEffectManager<K> soundEffectManager, SoundEffectManager<K> soundEffectManager,
VolumeManager audioVolumeManager VolumeManager audioVolumeManager
) { ) {
this.eventBus = eventBus;
this.musicManager = musicManager; this.musicManager = musicManager;
this.soundEffectManager = soundEffectManager; this.soundEffectManager = soundEffectManager;
this.audioVolumeManager = audioVolumeManager; this.audioVolumeManager = audioVolumeManager;
} }
public AudioEventListener<?, ?> initListeners(String buttonSoundToPlay) { public AudioEventListener<?, ?> initListeners(String buttonSoundToPlay) {
new EventFlow() new EventFlow(eventBus)
.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;
} }
@@ -74,7 +76,7 @@ public class AudioEventListener<T extends AudioResource, K extends AudioResource
} }
private void handleGetVolume(AudioEvents.GetVolume event) { private void handleGetVolume(AudioEvents.GetVolume event) {
GlobalEventBus.postAsync(new AudioEvents.GetVolumeResponse( eventBus.post(new AudioEvents.GetVolumeResponse(
audioVolumeManager.getVolume(event.controlType()), audioVolumeManager.getVolume(event.controlType()),
event.identifier())); event.identifier()));
} }

View File

@@ -6,8 +6,7 @@ import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.dispatch.interfaces.Dispatcher; import org.toop.framework.dispatch.interfaces.Dispatcher;
import org.toop.framework.dispatch.JavaFXDispatcher; import org.toop.framework.dispatch.JavaFXDispatcher;
import org.toop.annotations.TestsOnly; import org.toop.annotations.TestsOnly;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
import java.util.*; import java.util.*;
@@ -18,6 +17,7 @@ import java.util.concurrent.TimeUnit;
public class MusicManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.MusicManager<T> { public class MusicManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.MusicManager<T> {
private static final Logger logger = LogManager.getLogger(MusicManager.class); private static final Logger logger = LogManager.getLogger(MusicManager.class);
private final EventBus eventBus;
private final List<T> backgroundMusic = new ArrayList<>(); private final List<T> backgroundMusic = new ArrayList<>();
private final Dispatcher dispatcher; private final Dispatcher dispatcher;
private final List<T> resources; private final List<T> resources;
@@ -27,7 +27,8 @@ public class MusicManager<T extends AudioResource> implements org.toop.framework
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
public MusicManager(List<T> resources, boolean shuffleMusic) { public MusicManager(EventBus eventbus, List<T> resources, boolean shuffleMusic) {
this.eventBus = eventbus;
this.dispatcher = new JavaFXDispatcher(); this.dispatcher = new JavaFXDispatcher();
this.resources = resources; this.resources = resources;
// Shuffle if wanting to shuffle // Shuffle if wanting to shuffle
@@ -40,7 +41,8 @@ public class MusicManager<T extends AudioResource> implements org.toop.framework
* {@code @TestsOnly} DO NOT USE * {@code @TestsOnly} DO NOT USE
*/ */
@TestsOnly @TestsOnly
public MusicManager(List<T> resources, Dispatcher dispatcher) { public MusicManager(EventBus eventBus, List<T> resources, Dispatcher dispatcher) {
this.eventBus = eventBus;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.resources = new ArrayList<>(resources); this.resources = new ArrayList<>(resources);
backgroundMusic.addAll(resources); backgroundMusic.addAll(resources);
@@ -124,7 +126,7 @@ public class MusicManager<T extends AudioResource> implements org.toop.framework
Runnable currentMusicTask = new Runnable() { Runnable currentMusicTask = new Runnable() {
@Override @Override
public void run() { public void run() {
GlobalEventBus.post(new AudioEvents.PlayingMusic(track.getName(), track.currentPosition(), track.duration())); eventBus.post(new AudioEvents.PlayingMusic(track.getName(), track.currentPosition(), track.duration()));
scheduler.schedule(this, 1, TimeUnit.SECONDS); scheduler.schedule(this, 1, TimeUnit.SECONDS);
} }
}; };

View File

@@ -2,18 +2,10 @@ package org.toop.framework.audio;
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.resource.ResourceManager;
import org.toop.framework.resource.ResourceMeta; import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.BaseResource; import org.toop.framework.resource.resources.BaseResource;
import org.toop.framework.resource.resources.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;

View File

@@ -2,8 +2,6 @@ package org.toop.framework.audio.events;
import org.toop.framework.audio.VolumeControl; import org.toop.framework.audio.VolumeControl;
import org.toop.framework.eventbus.events.*; import org.toop.framework.eventbus.events.*;
import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
import org.toop.framework.eventbus.events.UniqueEvent;
public class AudioEvents extends EventsBase { public class AudioEvents extends EventsBase {
/** Stops the audio manager. */ /** Stops the audio manager. */

View File

@@ -1,12 +1,7 @@
package org.toop.framework.audio.interfaces; package org.toop.framework.audio.interfaces;
import org.toop.framework.resource.resources.SoundEffectAsset;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
public interface SoundEffectManager<T extends AudioResource> extends AudioManager<T> { public interface SoundEffectManager<T extends AudioResource> extends AudioManager<T> {
void play(String name, boolean loop); void play(String name, boolean loop);
void stop(String name); void stop(String name);

View File

@@ -13,10 +13,14 @@ import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.ResponseToUniqueEvent; import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
import org.toop.framework.eventbus.events.UniqueEvent; import org.toop.framework.eventbus.events.UniqueEvent;
import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.eventbus.subscriber.DefaultNamedSubscriber;
import org.toop.framework.eventbus.subscriber.NamedSubscriber;
import org.toop.framework.eventbus.subscriber.Subscriber;
/** /**
* EventFlow is a utility class for creating, posting, and optionally subscribing to events in a * EventFlow is a utility class for creating, posting, and optionally subscribing to events in a
* type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}. * type-safe and chainable manner. It is designed to work with the {@link EventBus}.
* *
* <p>This class supports automatic UUID assignment for {@link UniqueEvent} events, and * <p>This class supports automatic UUID assignment for {@link UniqueEvent} events, and
* allows filtering subscribers so they only respond to events with a specific UUID. All * allows filtering subscribers so they only respond to events with a specific UUID. All
@@ -31,6 +35,8 @@ public class EventFlow {
/** Cache of constructor handles for event classes to avoid repeated reflection lookups. */ /** Cache of constructor handles for event classes to avoid repeated reflection lookups. */
private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>(); private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
private final EventBus eventBus;
/** Automatically assigned UUID for {@link UniqueEvent} events. */ /** Automatically assigned UUID for {@link UniqueEvent} events. */
private long eventSnowflake = -1; private long eventSnowflake = -1;
@@ -38,24 +44,29 @@ 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<NamedSubscriber<?>> 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;
/** 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(EventBus eventBus) {
this.eventBus = eventBus;
public EventFlow addPostEvent(EventType event) {
this.event = event;
return this;
} }
public EventFlow addPostEvent(Supplier<? extends EventType> eventSupplier) { public EventFlow() {
this.event = eventSupplier.get(); this.eventBus = GlobalEventBus.get();
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.
* @param args The event arguments, see the added event record for more information.
* @return {@link #EventFlow}
*
*/
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 +109,401 @@ 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(String.valueOf(id));
this.result = eventClass.result();
};
var subscriber = new DefaultNamedSubscriber<>(
name,
event,
newAction
);
eventBus.subscribe(subscriber);
this.listeners.add(subscriber);
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(String.valueOf(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 DefaultNamedSubscriber<>(
})); name,
this.listeners.add(listenerHolder[0]); (Class<TT>) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0],
newAction
);
eventBus.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(String.valueOf(id));
};
var listener = new DefaultNamedSubscriber<>(
name,
event,
newAction
);
eventBus.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(String.valueOf(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 DefaultNamedSubscriber<>(
})); name,
this.listeners.add(listenerHolder[0]); eventClass,
newAction
);
eventBus.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); eventBus.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.get().post(this.event);
return this; return this;
} }
/**
*
* Unsubscribe from an event.
*
* @param action The listener object to remove and unsubscribe.
*/
public void unsubscribe(Consumer<?> action) {
this.listeners.removeIf(handler -> {
if (handler.handler().equals(action)) {
eventBus.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.id().equals(name)) {
eventBus.unsubscribe(handler);
return true;
}
return false;
});
}
/**
* Unsubscribe all events.
*/
public void unsubscribeAll() {
listeners.removeIf(handler -> {
eventBus.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;
} }
public ListenerHandler[] getListeners() { /**
return listeners.toArray(new ListenerHandler[0]); *
* Returns a copy of the list of listeners.
*
* @return Copy of the list of listeners.
*/
public Subscriber<?>[] getListeners() {
return listeners.toArray(new Subscriber[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

@@ -1,185 +1,57 @@
package org.toop.framework.eventbus; package org.toop.framework.eventbus;
import com.lmax.disruptor.*; import org.apache.logging.log4j.LogManager;
import com.lmax.disruptor.dsl.Disruptor; import org.toop.framework.eventbus.bus.AsyncEventBus;
import com.lmax.disruptor.dsl.ProducerType; import org.toop.framework.eventbus.bus.DefaultEventBus;
import java.util.Map; import org.toop.framework.eventbus.bus.DisruptorEventBus;
import java.util.concurrent.*; import org.toop.framework.eventbus.bus.EventBus;
import java.util.function.Consumer;
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.store.DefaultSubscriberStore;
import org.toop.framework.eventbus.subscriber.Subscriber;
/** import java.util.concurrent.ExecutorService;
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency, high-throughput event import java.util.concurrent.Executors;
* publishing.
*/
public final class GlobalEventBus {
/** Map of event class to type-specific listeners. */ public class GlobalEventBus implements AsyncEventBus {
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>> private static final AsyncEventBus INSTANCE = new DefaultEventBus(
LISTENERS = new ConcurrentHashMap<>(); LogManager.getLogger(DefaultEventBus.class),
new DefaultSubscriberStore()
);
/** Map of event class to Snowflake-ID-specific listeners. */
private static final Map<
Class<?>, ConcurrentHashMap<Long, Consumer<? extends UniqueEvent>>>
UUID_LISTENERS = new ConcurrentHashMap<>();
/** Disruptor ring buffer size (must be power of two). */
private static final int RING_BUFFER_SIZE = 1024 * 64;
/** Disruptor instance. */
private static final Disruptor<EventHolder> DISRUPTOR;
/** Ring buffer used for publishing events. */
private static final RingBuffer<EventHolder> RING_BUFFER;
static {
ThreadFactory threadFactory =
r -> {
Thread t = new Thread(r, "EventBus-Disruptor");
t.setDaemon(true);
return t;
};
DISRUPTOR =
new Disruptor<>(
EventHolder::new,
RING_BUFFER_SIZE,
threadFactory,
ProducerType.MULTI,
new BusySpinWaitStrategy());
// Single consumer that dispatches to subscribers
DISRUPTOR.handleEventsWith(
(holder, seq, endOfBatch) -> {
if (holder.event != null) {
dispatchEvent(holder.event);
holder.event = null;
}
});
DISRUPTOR.start();
RING_BUFFER = DISRUPTOR.getRingBuffer();
}
/** Prevent instantiation. */
private GlobalEventBus() {} private GlobalEventBus() {}
/** Wrapper used inside the ring buffer. */ public static EventBus get() {
private static class EventHolder { return INSTANCE;
EventType event;
} }
// ------------------------------------------------------------------------ @Override
// Subscription public void subscribe(Subscriber<? extends EventType> listener) {
// ------------------------------------------------------------------------ INSTANCE.subscribe(listener);
public static <T extends EventType> Consumer<? super EventType> subscribe(
Class<T> eventClass, Consumer<T> 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) { @Override
Consumer<? super EventType> wrapper = event -> listener.accept(event); public void unsubscribe(Subscriber<? extends EventType> listener) {
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>()).add(wrapper); INSTANCE.unsubscribe(listener);
return wrapper;
} }
public static <T extends UniqueEvent> void subscribeById( @Override
Class<T> eventClass, long eventId, Consumer<T> listener) { public <T extends EventType> void post(T event) {
UUID_LISTENERS INSTANCE.post(event);
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
.put(eventId, listener);
} }
public static void unsubscribe(Object listener) { @Override
LISTENERS.values().forEach(list -> list.remove(listener)); public <T extends EventType> void asyncPost(T event) {
INSTANCE.asyncPost(event);
} }
public static <T extends UniqueEvent> void unsubscribeById( @Override
Class<T> eventClass, long eventId) { public void shutdown() {
Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(eventClass); INSTANCE.shutdown();
if (map != null) map.remove(eventId);
} }
// ------------------------------------------------------------------------ @Override
// Posting public void reset() {
// ------------------------------------------------------------------------ INSTANCE.reset();
public static <T extends EventType> void post(T event) {
dispatchEvent(event); // synchronous
} }
public static <T extends EventType> void postAsync(T event) {
long seq = RING_BUFFER.next();
try {
EventHolder holder = RING_BUFFER.get(seq);
holder.event = event;
} finally {
RING_BUFFER.publish(seq);
}
}
@SuppressWarnings("unchecked")
private static void dispatchEvent(EventType event) {
Class<?> clazz = event.getClass();
// class-specific listeners
CopyOnWriteArrayList<Consumer<? super EventType>> classListeners = LISTENERS.get(clazz);
if (classListeners != null) {
for (Consumer<? super EventType> listener : classListeners) {
try {
listener.accept(event);
} catch (Throwable e) {
// e.printStackTrace();
}
}
}
// generic listeners
CopyOnWriteArrayList<Consumer<? super EventType>> genericListeners =
LISTENERS.get(Object.class);
if (genericListeners != null) {
for (Consumer<? super EventType> listener : genericListeners) {
try {
listener.accept(event);
} catch (Throwable e) {
// e.printStackTrace();
}
}
}
// snowflake listeners
if (event instanceof UniqueEvent snowflakeEvent) {
Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(clazz);
if (map != null) {
Consumer<UniqueEvent> listener =
(Consumer<UniqueEvent>) map.remove(snowflakeEvent.getIdentifier());
if (listener != null) {
try {
listener.accept(snowflakeEvent);
} catch (Throwable ignored) {
}
}
}
}
}
// ------------------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------------------
public static void shutdown() {
DISRUPTOR.shutdown();
LISTENERS.clear();
UUID_LISTENERS.clear();
}
public static void reset() {
LISTENERS.clear();
UUID_LISTENERS.clear();
}
} }

View File

@@ -1,25 +0,0 @@
package org.toop.framework.eventbus;
public class ListenerHandler {
private Object listener;
// private boolean unsubscribeAfterSuccess = true;
// public ListenerHandler(Object listener, boolean unsubAfterSuccess) {
// this.listener = listener;
// this.unsubscribeAfterSuccess = unsubAfterSuccess;
// }
public ListenerHandler(Object listener) {
this.listener = listener;
}
public Object getListener() {
return this.listener;
}
// public boolean isUnsubscribeAfterSuccess() {
// return this.unsubscribeAfterSuccess;
// }
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.eventbus.bus;
import org.toop.framework.eventbus.events.EventType;
public interface AsyncEventBus extends EventBus {
<T extends EventType> void asyncPost(T event);
}

View File

@@ -0,0 +1,63 @@
package org.toop.framework.eventbus.bus;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.store.SubscriberStore;
import org.toop.framework.eventbus.subscriber.Subscriber;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
public class DefaultEventBus implements AsyncEventBus {
private final Logger logger;
private final SubscriberStore eventsHolder;
private final ExecutorService asyncExecutor = Executors.newCachedThreadPool();
public DefaultEventBus(Logger logger, SubscriberStore eventsHolder) {
this.logger = logger;
this.eventsHolder = eventsHolder;
}
@Override
public void subscribe(Subscriber<? extends EventType> subscriber) {
eventsHolder.add(subscriber);
}
@Override
public void unsubscribe(Subscriber<? extends EventType> subscriber) {
eventsHolder.remove(subscriber);
}
@Override
@SuppressWarnings("unchecked")
public <T extends EventType> void post(T event) {
Class<T> eventType = (Class<T>) event.getClass();
var subs = eventsHolder.get(eventType);
if (subs != null) {
for (Subscriber<?> subscriber : subs) {
Class<T> eventClass = (Class<T>) subscriber.event();
Consumer<EventType> action = (Consumer<EventType>) subscriber.handler();
action.accept(eventClass.cast(event));
}
}
}
@Override
public <T extends EventType> void asyncPost(T event) {
asyncExecutor.submit(() -> post(event));
}
@Override
public void shutdown() {
eventsHolder.reset();
}
@Override
public void reset() {
eventsHolder.reset();
}
}

View File

@@ -0,0 +1,117 @@
package org.toop.framework.eventbus.bus;
import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.subscriber.Subscriber;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.store.SubscriberStore;
import java.util.concurrent.ThreadFactory;
import java.util.function.Consumer;
public class DisruptorEventBus implements EventBus {
/** Wrapper used inside the ring buffer. */
private static class EventHolder<T> {
T event;
}
private final Logger logger;
private final SubscriberStore eventsHolder;
private final Disruptor<EventHolder<? extends EventType>> disruptor;
private final RingBuffer<EventHolder<? extends EventType>> ringBuffer;
public DisruptorEventBus(Logger logger, SubscriberStore eventsHolder) {
this.logger = logger;
this.eventsHolder = eventsHolder;
ThreadFactory threadFactory =
r -> {
Thread t = new Thread(r, "EventBus-Disruptor");
t.setDaemon(true);
return t;
};
disruptor = getEventHolderDisruptor(threadFactory);
disruptor.start();
this.ringBuffer = disruptor.getRingBuffer();
}
private Disruptor<EventHolder<? extends EventType>> getEventHolderDisruptor(ThreadFactory threadFactory) {
int RING_BUFFER_SIZE = 1024 * 64;
Disruptor<EventHolder<? extends EventType>> disruptor = new Disruptor<>(
EventHolder::new,
RING_BUFFER_SIZE,
threadFactory,
ProducerType.MULTI,
new BusySpinWaitStrategy());
disruptor.handleEventsWith(
(holder, _, _) -> {
if (holder.event != null) {
dispatchEvent(holder.event);
holder.event = null;
}
});
return disruptor;
}
@Override
public void subscribe(Subscriber<? extends EventType> listener) {
eventsHolder.add(listener);
}
@Override
public void unsubscribe(Subscriber<? extends EventType> listener) {
eventsHolder.remove(listener);
}
@Override
public <T extends EventType> void post(T event) {
long seq = ringBuffer.next();
try {
@SuppressWarnings("unchecked")
EventHolder<T> holder = (EventHolder<T>) ringBuffer.get(seq);
holder.event = event;
} finally {
ringBuffer.publish(seq);
}
}
@Override
public void shutdown() {
disruptor.shutdown();
eventsHolder.reset();
}
@Override
public void reset() {
eventsHolder.reset();
}
private <T extends EventType> void dispatchEvent(T event) {
var classListeners = eventsHolder.get(event.getClass());
if (classListeners != null) {
for (Subscriber<?> listener : classListeners) {
try {
callListener(listener, event);
} catch (Throwable e) {
logger.warn("Exception while handling event: {}", event, e);
}
}
}
}
@SuppressWarnings("unchecked")
private <T> void callListener(Subscriber<?> subscriber, T event) {
Class<T> eventClass = (Class<T>) subscriber.event();
Consumer<EventType> action = (Consumer<EventType>) subscriber.handler();
action.accept((EventType) eventClass.cast(event));
}
}

View File

@@ -0,0 +1,12 @@
package org.toop.framework.eventbus.bus;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.subscriber.Subscriber;
public interface EventBus {
void subscribe(Subscriber<? extends EventType> subscriber);
void unsubscribe(Subscriber<? extends EventType> subscriber);
<T extends EventType> void post(T event);
void shutdown();
void reset();
}

View File

@@ -0,0 +1,47 @@
package org.toop.framework.eventbus.store;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.subscriber.Subscriber;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class AsyncSubscriberStore implements SubscriberStore {
private final ConcurrentHashMap<Class<? extends EventType>, ConcurrentLinkedQueue<Subscriber<? extends EventType>>> queues = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Class<? extends EventType>, Subscriber<? extends EventType>[]> snapshots = new ConcurrentHashMap<>();
@Override
public void add(Subscriber<? extends EventType> sub) {
queues.computeIfAbsent(sub.event(), _ -> new ConcurrentLinkedQueue<>()).add(sub);
rebuildSnapshot(sub.event());
}
@Override
public void remove(Subscriber<? extends EventType> sub) {
ConcurrentLinkedQueue<Subscriber<?>> queue = queues.get(sub.event());
if (queue != null) {
queue.remove(sub);
rebuildSnapshot(sub.event());
}
}
@Override
public Subscriber<? extends EventType>[] get(Class<? extends EventType> event) {
return snapshots.getOrDefault(event, new Subscriber<?>[0]);
}
@Override
public void reset() {
queues.clear();
snapshots.clear();
}
private void rebuildSnapshot(Class<? extends EventType> event) {
ConcurrentLinkedQueue<Subscriber<?>> queue = queues.get(event);
if (queue != null) {
snapshots.put(event, queue.toArray(new Subscriber<?>[0]));
} else {
snapshots.put(event, new Subscriber<?>[0]);
}
}
}

View File

@@ -0,0 +1,73 @@
package org.toop.framework.eventbus.store;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.subscriber.NamedSubscriber;
import org.toop.framework.eventbus.subscriber.Subscriber;
import java.util.concurrent.ConcurrentHashMap;
public class DefaultSubscriberStore implements SubscriberStore {
private static final Subscriber<? extends EventType>[] EMPTY = new Subscriber<?>[0];
private final ConcurrentHashMap<Class<? extends EventType>, Subscriber<? extends EventType>[]>
listeners = new ConcurrentHashMap<>();
@Override
public void add(Subscriber<? extends EventType> sub) {
listeners.compute(sub.event(), (_, arr) -> {
if (arr == null || arr.length == 0) {
return new Subscriber<?>[]{sub};
}
int len = arr.length;
Subscriber<?>[] newArr = new Subscriber[len + 1];
System.arraycopy(arr, 0, newArr, 0, len);
newArr[len] = sub;
return newArr;
});
}
@Override
public void remove(Subscriber<? extends EventType> sub) {
listeners.computeIfPresent(sub.event(), (_, arr) -> {
int len = arr.length;
if (len == 1) {
return arr[0].equals(sub) ? null : arr;
}
int keep = 0;
for (Subscriber<?> s : arr) {
if (!s.equals(sub)) keep++;
}
if (keep == len) {
return arr;
}
if (keep == 0) {
return null;
}
Subscriber<?>[] newArr = new Subscriber[keep];
int i = 0;
for (Subscriber<?> s : arr) {
if (!s.equals(sub)) {
newArr[i++] = s;
}
}
return newArr;
});
}
@Override
public Subscriber<? extends EventType>[] get(Class<? extends EventType> event) {
return listeners.getOrDefault(event, EMPTY);
}
@Override
public void reset() {
listeners.clear();
}
}

View File

@@ -0,0 +1,11 @@
package org.toop.framework.eventbus.store;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.subscriber.Subscriber;
public interface SubscriberStore {
void add(Subscriber<? extends EventType> subscriber);
void remove(Subscriber<? extends EventType> subscriber);
Subscriber<? extends EventType>[] get(Class<? extends EventType> event);
void reset();
}

View File

@@ -0,0 +1,37 @@
package org.toop.framework.eventbus.store;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.subscriber.Subscriber;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SyncSubscriberStore implements SubscriberStore {
private final Map<Class<? extends EventType>, List<Subscriber<? extends EventType>>> LISTENERS = new ConcurrentHashMap<>();
private static final Subscriber<? extends EventType>[] EMPTY = new Subscriber<?>[0];
@Override
public void add(Subscriber<? extends EventType> sub) {
LISTENERS.computeIfAbsent(sub.event(), _ -> new ArrayList<>()).add(sub);
}
@Override
public void remove(Subscriber<? extends EventType> sub) {
LISTENERS.getOrDefault(sub.event(), new ArrayList<>()).remove(sub);
LISTENERS.entrySet().removeIf(entry -> entry.getValue().isEmpty());
}
@Override
public Subscriber<? extends EventType>[] get(Class<? extends EventType> event) {
List<Subscriber<? extends EventType>> list = LISTENERS.get(event);
if (list == null || list.isEmpty()) return EMPTY;
return list.toArray(EMPTY);
}
@Override
public void reset() {
LISTENERS.clear();
}
}

View File

@@ -0,0 +1,8 @@
package org.toop.framework.eventbus.subscriber;
import org.toop.framework.eventbus.events.EventType;
import java.util.function.Consumer;
public record DefaultNamedSubscriber<K extends EventType>(String id, Class<K> event, Consumer<K> handler)
implements NamedSubscriber<K> {}

View File

@@ -0,0 +1,8 @@
package org.toop.framework.eventbus.subscriber;
import org.toop.framework.eventbus.events.EventType;
import java.util.function.Consumer;
public record DefaultSubscriber<K extends EventType>(Class<K> event, Consumer<K> handler) implements Subscriber<K> {}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.eventbus.subscriber;
public interface HasId<ID> {
ID id();
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.eventbus.subscriber;
import org.toop.framework.eventbus.events.EventType;
public interface IdSubscriber<K extends EventType> extends Subscriber<K>, HasId<Long> {}

View File

@@ -0,0 +1,8 @@
package org.toop.framework.eventbus.subscriber;
import org.toop.framework.eventbus.events.EventType;
import java.util.function.Consumer;
public record LongIdSubscriber<K extends EventType>(Long id, Class<K> event, Consumer<K> handler)
implements IdSubscriber<K> {}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.eventbus.subscriber;
import org.toop.framework.eventbus.events.EventType;
public interface NamedSubscriber<K extends EventType> extends Subscriber<K>, HasId<String> {}

View File

@@ -0,0 +1,10 @@
package org.toop.framework.eventbus.subscriber;
import org.toop.framework.eventbus.events.EventType;
import java.util.function.Consumer;
public interface Subscriber<K extends EventType> {
Class<K> event();
Consumer<K> handler();
}

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