3 Commits

Author SHA1 Message Date
Bas Antonius de Jong
b3e29a3262 Demo 7 (#314)
* Changed pom to be correct.
Fixed SnowflakeGenerator not making unique ids.
Changed naming for event implementation.
Automated id getter for events.
Added Error-Prone to all modules.
Added parents to all modules.
Added processors module.

* SoundEffectManager now generic

* Removed ResourceManager from AudioManagers

* Added linelistener to SoundEffectAsset

* commit

* commit ofzo

* Tests for SoundEffectManager

* getLegalMoves logic seems fixed //todo write better tests

* Tests toegevoegd

* punk toegevoegd

* Added shuffling on user request

* Reworked NetworkingClientManager into SRP model.

* Forgot to remove

* Improved API for dependency injection

* Some better docs.

* gui refactor

* fixed merge conflicts

* Added exceptions. Added reconnect attempts and changeable address

* Fixed event bug

* add: reversi game

* add: server chat box

* visual update

* Refactor to make Events easier to work with.

* Quick fix for closing connection.

* Documentation

* Correct client creation and user polling

* begin van audio display

* Polling music event, fires every 1 second

* Updated test.

* Updated timings

* Nuke everything on close.

* Basis Audio Display toegevoegd + standaard CSS toegevoegd

Kan nu zien hoe lang de song duurt, hoe lang ie al bezig is met draaien en de titel (-.mp3)

* Clips now also return positional information

* Skip Button

* Skip Button

* Fixes for garbage code by Omar

* Tiny fix when natural skip

* Small event fix

* Faster event schedule for PlayingMusic event

* test fix

* added method for sorting the flipped pieces by distance to Move.position

* new reversi test (both players no legal moves)

* connect4 with minimax AI

* Toegevoegd:

-Play Button + CSS + Events
-Previous Button + CSS + Events
-Changed interface for AudioResource to include a pause button which works really well with mediaplayer, however now SoundEffectAsset has an unnessescary pause

* Made it so that it indicates with the play/pause button if its paused or played

* add simple flip animations and fixed(?) server somewhat

* fixed tests

* can start game from playerlist screen

* tourney ready

* spam minder

* fixed setgamelabels

* spam minder v2

* canvas changes

* moved score out of game

* can now go to last using previous and being at the first song

* mainview false for sendchallengeview

* moved score out of game

* kleine ui fix

* updated music ma,es

* started working on the widget system

* iets met timing verkeerd temporary fix

* Fixes for garbage code by Omar

* Added replace to reduce boiler plate code

* Manually fallback to the fallback locale when a ResourceBundle is missing a resource key. Fallsback to "MISSING RESOUREC" if it's not present in the fallback.

* Removed unused import and unused parameter

* cool onhover effect for reversi

* Made the GameState enum it's own file and fixed imports

* Removed unused import

* Turned abstract methods into an interface

* Moved the Move record into it's own file, seperated from Game

* Removed unused imports

* Renamed Interface Playtable to IPlayable

* Turned Abstract Method for AI into interface

* Refactored Game to follow encapsulation principle

* Removed unused imports

* Applied encapsulation principle to TurnBasedBame.java

* Privated methods that didn't have to be public

* Reversi: made method private

* Changed checkForEarlyDraw so it doesn't need a game as input.

* Fixed warning "Warning:(27, 12) Copy constructor does not copy field 'mostRecentlyFlippedPieces'", removed unused field

* Made connect4 public method private

* half done with the widget system

* added some comments and made some methods a bit more readable

* widget system almost complete

* Functional code, is now object orientated

* Removed no more needed comments

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

* rest van de tutorials toegevoegd

* resizable true

* fixed turn skip bug
fixed end score bug
now only shows legal and highlight moves when human

* Squashed commit of the following:

commit a517f2f302baa89f8ef59946a31c7bb59c56770f
Author: Stef <stbuwalda@gmail.com>
Date:   Thu Nov 27 15:43:43 2025 +0100

    Make it so the game shows "Waiting on ... to make their move". Styling isn't done but it is easier to see who's turn it is. There is a lot of structuring to do in the previous code...

* 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.

* Fixed compilation errors

* Changed the way turns are being stored in TurnBasedGame.

* Removed views

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

* 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

* 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

* 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

* Removed loading widget from Server.java

* Fixed old new EventFlow().listen() missing false as third param

* 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 <>

* 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>

* added back button sounds because SOMEONE fucked it up.....

* 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 <>

* Double loading call fix, LoadingWidget docs

* 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 <>

* Fixed systems starting, before assets being loaded (I am retarded)

* Added infinite boolean, fixed loading behaviour at startup

* 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

* Best fix for white screen at start

* Making threads verbose regarding exceptions

* Loading circle, better loading colors.

* 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 <>

* initSystems now uses latch instead of timer. Moved single threads to Executor

* Safety

* Deleted unnecessary imports

* Code cleanup

* changed "fullscreen exit key combination" from esc to F11

* shitty fix for player selector spacing issue

* shitty fix for player selector spacing issue v2

* fixed reversi colors being switched, causing multiple issues

* 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>

* Better limits to generic acceptance

* Will fix tests etc later

* Merge 292 into development (#293)

Applied template method pattern to abstract player

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

* Init server code

* Moves

* Testing code

* Removed Generics, pray nothing breaks.

* Code cleanup

* Tests and better instantiation

* Init challenges

* Working challenges

* Fixed bugs, easy to use host button

* Fixed tic tac toe naming

* Fixes

* Removed Generics, pray nothing breaks.

* Added ability to take ServerPlayer from user

* Added pairs

* Using pairs now in server.java

* Removed space in naming

* Werkt nog niet

* Fixed hasArgs

* Closable server

* Making moves works. Game notifies when game has ended.

* Partial server refactor

* Refactor done, added ability to subscribe

* Working subscription, button only subs to reversi right now

* Small improvements to usability, auto disconnect when server closes connection

* Missed a boolean

* Quick fix so more than one game can be played in succession

* Removed user from subscription if in a game

* UI fixes after game end

* TableWidget

* mcts v1

* Moved subscriptions to store

* bitboard optimization

* Collapsed interfaces from model portion

* Fixed runtime error I forgot to fix.

* Collapsed interfaces in Controller section

* Collapsed interfaces in View section

* Code readability

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

* main

* 289 server demo ready (#306)

* 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

* Hotfix for stuff

* Logging and fixed user input getting stuck

* Fixed merge mistakes

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

This reverts commit e2132b549d, reversing
changes made to 9aefcb9b7b.

* Turn information

* legal move highlight and onhover effect added back

* 289 server (#308) Incremental server update, with working tournament and player input timeout

* 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>

* Hotfix for stuff

* Logging and fixed user input getting stuck

* Fixed merge mistakes

* Working tournament

* GlobalEventBus is now async instead

* Shuffle now changeable, host can now switch tournament gametype

* Tournament results are now send back to the clients connected to the server

* Tournament now returns result to clients

* Refactored tournament to use interfaces and builders

* Removed unnecessary imports

* Tournament refactor for better naming and easier to understand code

* Starting a tournament now requires to be admin

* Request admin list

* Added admins to games

* Tournament is now without admins

* Added result comeback with a draw

* Async tournament runner

* Added back ability to shuffle matchmaker

* Moved scoring calculation into scoring system

* Tournament now uses propper builder pattern

* Null handling

* Removed input mistake, removed print

* Refactored Tournament to use matchExecutor and ResultBroadcaster. Added turnTime and players are now added through Tournament creation instead of on MatchMaker/ScoreSystem creation

* Added shuffle to builder

* Removed unnecessary throw

* More adaptable scoring system

* Moved async runner to virtual thread

* Timeout added

* AI player given time change

---------

Co-authored-by: Stef <stbuwalda@gmail.com>
Co-authored-by: Stef <48526421+StefBuwalda@users.noreply.github.com>

* update mcts, incremental merge (#311)

* mcts v1, v2, v3, v4 done. v5 wip

* update mcts

* mcts v1, v2, v3, v4 done. v5 wip

* update mcts

* Merge changes on dev

* update mcts

---------

Co-authored-by: ramollia <>

* Research Experiment Data generator

* added visual score to reversi

* New CSV structure thats cleaner, the code in AITest is also cleaner

* implement solved

* fixed things

* saving games data to games.csv

* readded the exploration constant

* changed the way multithreading worked

* Better data collection for overnight run

* Infinite game collection

* Correct time data visualization effect on timetable lookup for data collection purposes

* Back to 10ms

* m4 nu 8 threads

* AI data now correct

* Data collection fixes

* name fixes

* AI wait fixes

* Added thread test

* Small thread count fix

* Fixes

* name fixes

* fixed extra wait time for threads

* readded threads argument

* Parameters added to tests

* Moved back in threads

* AI Thread fixes

---------

Co-authored-by: Ticho Hidding <tichohidding@gmail.com>
Co-authored-by: Stef <stbuwalda@gmail.com>
Co-authored-by: michiel <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <@>
Co-authored-by: ramollia <>
Co-authored-by: tichohidding <58555714+tichohidding@users.noreply.github.com>
Co-authored-by: Stef <48526421+StefBuwalda@users.noreply.github.com>
2026-01-25 12:13:21 +01:00
Bas Antonius de Jong
429e5bc90b Demo 6
* 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

* Hotfix for stuff

* Logging and fixed user input getting stuck

* Fixed merge mistakes (#307)

* fast server connection

* Fixed bugs and oversights

* Renamed asset folder to resource, made resourceLoader more robust. Completed some TODO's, formatting

* AppSettings now also get loaded into the assetmanager

* start to reversi logic

* legal moves now get highlighted in red

* Moves flip dots. all tests pass. can play reversi local.

* Working state. Split AudioManager into 3 different branches for easier testing and srp

* Reworked to now use better defined generics and easier to use API. Added AudioResource to be used in changing volume

* Made all of the updated classes more generic for better flexibility in unittesting

* Added more flexible dependency injection to MusicManager for unittesting. Moved to event driven design for less complex code and lower runtime complexity.

* Split SoundEffectManager from AudioManager. (#171)

Clips no longer create a new clip instance each time they are played.  A singular clip is made for each resource and is opened/closed when loaded/unloaded. When a clip is played that is already playing it'll stop playback and start again. Clip volume handling isn't done very well.

* Unit tests for MusicManager.java

* Hotfix for loading clip volume issue (#174)

* Fixed AudioVolumemanager, all volumes calculations are now made in VolumeTypes enum

* Added ability to remove a manager from VolumeTypes

* Removed file no longer in use

* Fixed grammer and spelling mistakes

* Renamed VOLUME to MASTERVOLUME for better naming

* Minor changes in API design

* Renamed VolumeTypes to VolumeControl. Made it thread safe. Added docs to VolumeControl and co.
removed .updateAllVolumes() in favor of auto updating inside enum instead

* Added ErrorProne for potential bugs. Fixed potential bugs.

* Small fixes.

* Removed no more needed code.

* Finished todo's

* Moved restrictedAPI to future release

* Finished todo's

* Moved restrictedAPI to future release

* Changed pom to be correct.
Fixed SnowflakeGenerator not making unique ids.
Changed naming for event implementation.
Automated id getter for events.
Added Error-Prone to all modules.
Added parents to all modules.
Added processors module.

* SoundEffectManager now generic

* Removed ResourceManager from AudioManagers

* Added linelistener to SoundEffectAsset

* commit

* commit ofzo

* Tests for SoundEffectManager

* getLegalMoves logic seems fixed //todo write better tests

* Tests toegevoegd

* punk toegevoegd

* Added shuffling on user request

* Reworked NetworkingClientManager into SRP model.

* Forgot to remove

* Improved API for dependency injection

* Some better docs.

* gui refactor

* fixed merge conflicts

* Added exceptions. Added reconnect attempts and changeable address

* Fixed event bug

* add: reversi game

* add: server chat box

* visual update

* Refactor to make Events easier to work with.

* Quick fix for closing connection.

* Documentation

* Correct client creation and user polling

* begin van audio display

* Polling music event, fires every 1 second

* Updated test.

* Updated timings

* Nuke everything on close.

* Basis Audio Display toegevoegd + standaard CSS toegevoegd

Kan nu zien hoe lang de song duurt, hoe lang ie al bezig is met draaien en de titel (-.mp3)

* Clips now also return positional information

* Skip Button

* Skip Button

* Fixes for garbage code by Omar

* Tiny fix when natural skip

* Small event fix

* Faster event schedule for PlayingMusic event

* test fix

* added method for sorting the flipped pieces by distance to Move.position

* new reversi test (both players no legal moves)

* connect4 with minimax AI

* Toegevoegd:

-Play Button + CSS + Events
-Previous Button + CSS + Events
-Changed interface for AudioResource to include a pause button which works really well with mediaplayer, however now SoundEffectAsset has an unnessescary pause

* Made it so that it indicates with the play/pause button if its paused or played

* add simple flip animations and fixed(?) server somewhat

* fixed tests

* can start game from playerlist screen

* tourney ready

* spam minder

* fixed setgamelabels

* spam minder v2

* canvas changes

* moved score out of game

* can now go to last using previous and being at the first song

* mainview false for sendchallengeview

* moved score out of game

* kleine ui fix

* updated music ma,es

* started working on the widget system

* iets met timing verkeerd temporary fix

* Fixes for garbage code by Omar

* Added replace to reduce boiler plate code

* Manually fallback to the fallback locale when a ResourceBundle is missing a resource key. Fallsback to "MISSING RESOUREC" if it's not present in the fallback.

* Removed unused import and unused parameter

* cool onhover effect for reversi

* Made the GameState enum it's own file and fixed imports

* Removed unused import

* Turned abstract methods into an interface

* Moved the Move record into it's own file, seperated from Game

* Removed unused imports

* Renamed Interface Playtable to IPlayable

* Turned Abstract Method for AI into interface

* Refactored Game to follow encapsulation principle

* Removed unused imports

* Applied encapsulation principle to TurnBasedBame.java

* Privated methods that didn't have to be public

* Reversi: made method private

* Changed checkForEarlyDraw so it doesn't need a game as input.

* Fixed warning "Warning:(27, 12) Copy constructor does not copy field 'mostRecentlyFlippedPieces'", removed unused field

* Made connect4 public method private

* half done with the widget system

* added some comments and made some methods a bit more readable

* widget system almost complete

* Functional code, is now object orientated

* Removed no more needed comments

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

* rest van de tutorials toegevoegd

* resizable true

* fixed turn skip bug
fixed end score bug
now only shows legal and highlight moves when human

* Squashed commit of the following:

commit a517f2f302baa89f8ef59946a31c7bb59c56770f
Author: Stef <stbuwalda@gmail.com>
Date:   Thu Nov 27 15:43:43 2025 +0100

    Make it so the game shows "Waiting on ... to make their move". Styling isn't done but it is easier to see who's turn it is. There is a lot of structuring to do in the previous code...

* 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.

* Fixed compilation errors

* Changed the way turns are being stored in TurnBasedGame.

* Removed views

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

* 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

* 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

* 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

* Removed loading widget from Server.java

* Fixed old new EventFlow().listen() missing false as third param

* 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 <>

* 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>

* added back button sounds because SOMEONE fucked it up.....

* 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 <>

* Double loading call fix, LoadingWidget docs

* 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 <>

* Fixed systems starting, before assets being loaded (I am retarded)

* Added infinite boolean, fixed loading behaviour at startup

* 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

* Best fix for white screen at start

* Making threads verbose regarding exceptions

* Loading circle, better loading colors.

* 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 <>

* initSystems now uses latch instead of timer. Moved single threads to Executor

* Safety

* Deleted unnecessary imports

* Code cleanup

* changed "fullscreen exit key combination" from esc to F11

* shitty fix for player selector spacing issue

* shitty fix for player selector spacing issue v2

* fixed reversi colors being switched, causing multiple issues

* 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>

* Better limits to generic acceptance

* Will fix tests etc later

* Merge 292 into development (#293)

Applied template method pattern to abstract player

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

* Init server code

* Moves

* Testing code

* Removed Generics, pray nothing breaks.

* Code cleanup

* Tests and better instantiation

* Init challenges

* Working challenges

* Fixed bugs, easy to use host button

* Fixed tic tac toe naming

* Fixes

* Removed Generics, pray nothing breaks.

* Added ability to take ServerPlayer from user

* Added pairs

* Using pairs now in server.java

* Removed space in naming

* Werkt nog niet

* Fixed hasArgs

* Closable server

* Making moves works. Game notifies when game has ended.

* Partial server refactor

* Refactor done, added ability to subscribe

* Working subscription, button only subs to reversi right now

* Small improvements to usability, auto disconnect when server closes connection

* Missed a boolean

* Quick fix so more than one game can be played in succession

* Removed user from subscription if in a game

* UI fixes after game end

* TableWidget

* mcts v1

* Moved subscriptions to store

* bitboard optimization

* Collapsed interfaces from model portion

* Fixed runtime error I forgot to fix.

* Collapsed interfaces in Controller section

* Collapsed interfaces in View section

* Code readability

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

* main

* 289 server demo ready (#306)

* 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

* Hotfix for stuff

* Logging and fixed user input getting stuck

* Fixed merge mistakes

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

This reverts commit e2132b549d, reversing
changes made to 9aefcb9b7b.

---------

Co-authored-by: ramollia <@>
Co-authored-by: Ticho Hidding <tichohidding@gmail.com>
Co-authored-by: Stef <48526421+StefBuwalda@users.noreply.github.com>
Co-authored-by: Stef <stbuwalda@gmail.com>
Co-authored-by: michiel <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
Co-authored-by: tichohidding <58555714+tichohidding@users.noreply.github.com>
2026-01-09 19:30:49 +01:00
Bas Antonius de Jong
8146be16ed Demo 5
* fast server connection

* Fixed bugs and oversights

* Renamed asset folder to resource, made resourceLoader more robust. Completed some TODO's, formatting

* AppSettings now also get loaded into the assetmanager

* start to reversi logic

* legal moves now get highlighted in red

* Moves flip dots. all tests pass. can play reversi local.

* Working state. Split AudioManager into 3 different branches for easier testing and srp

* Reworked to now use better defined generics and easier to use API. Added AudioResource to be used in changing volume

* Made all of the updated classes more generic for better flexibility in unittesting

* Added more flexible dependency injection to MusicManager for unittesting. Moved to event driven design for less complex code and lower runtime complexity.

* Split SoundEffectManager from AudioManager. (#171)

Clips no longer create a new clip instance each time they are played.  A singular clip is made for each resource and is opened/closed when loaded/unloaded. When a clip is played that is already playing it'll stop playback and start again. Clip volume handling isn't done very well.

* Unit tests for MusicManager.java

* Hotfix for loading clip volume issue (#174)

* Fixed AudioVolumemanager, all volumes calculations are now made in VolumeTypes enum

* Added ability to remove a manager from VolumeTypes

* Removed file no longer in use

* Fixed grammer and spelling mistakes

* Renamed VOLUME to MASTERVOLUME for better naming

* Minor changes in API design

* Renamed VolumeTypes to VolumeControl. Made it thread safe. Added docs to VolumeControl and co.
removed .updateAllVolumes() in favor of auto updating inside enum instead

* Added ErrorProne for potential bugs. Fixed potential bugs.

* Small fixes.

* Removed no more needed code.

* Finished todo's

* Moved restrictedAPI to future release

* Finished todo's

* Moved restrictedAPI to future release

* Changed pom to be correct.
Fixed SnowflakeGenerator not making unique ids.
Changed naming for event implementation.
Automated id getter for events.
Added Error-Prone to all modules.
Added parents to all modules.
Added processors module.

* SoundEffectManager now generic

* Removed ResourceManager from AudioManagers

* Added linelistener to SoundEffectAsset

* commit

* commit ofzo

* Tests for SoundEffectManager

* getLegalMoves logic seems fixed //todo write better tests

* Tests toegevoegd

* punk toegevoegd

* Added shuffling on user request

* Reworked NetworkingClientManager into SRP model.

* Forgot to remove

* Improved API for dependency injection

* Some better docs.

* gui refactor

* fixed merge conflicts

* Added exceptions. Added reconnect attempts and changeable address

* Fixed event bug

* add: reversi game

* add: server chat box

* visual update

* Refactor to make Events easier to work with.

* Quick fix for closing connection.

* Documentation

* Correct client creation and user polling

* begin van audio display

* Polling music event, fires every 1 second

* Updated test.

* Updated timings

* Nuke everything on close.

* Basis Audio Display toegevoegd + standaard CSS toegevoegd

Kan nu zien hoe lang de song duurt, hoe lang ie al bezig is met draaien en de titel (-.mp3)

* Clips now also return positional information

* Skip Button

* Skip Button

* Fixes for garbage code by Omar

* Tiny fix when natural skip

* Small event fix

* Faster event schedule for PlayingMusic event

* test fix

* added method for sorting the flipped pieces by distance to Move.position

* new reversi test (both players no legal moves)

* connect4 with minimax AI

* Toegevoegd:

-Play Button + CSS + Events
-Previous Button + CSS + Events
-Changed interface for AudioResource to include a pause button which works really well with mediaplayer, however now SoundEffectAsset has an unnessescary pause

* Made it so that it indicates with the play/pause button if its paused or played

* add simple flip animations and fixed(?) server somewhat

* fixed tests

* can start game from playerlist screen

* tourney ready

* spam minder

* fixed setgamelabels

* spam minder v2

* canvas changes

* moved score out of game

* can now go to last using previous and being at the first song

* mainview false for sendchallengeview

* moved score out of game

* kleine ui fix

* updated music ma,es

* started working on the widget system

* iets met timing verkeerd temporary fix

* Fixes for garbage code by Omar

* Added replace to reduce boiler plate code

* Manually fallback to the fallback locale when a ResourceBundle is missing a resource key. Fallsback to "MISSING RESOUREC" if it's not present in the fallback.

* Removed unused import and unused parameter

* cool onhover effect for reversi

* Made the GameState enum it's own file and fixed imports

* Removed unused import

* Turned abstract methods into an interface

* Moved the Move record into it's own file, seperated from Game

* Removed unused imports

* Renamed Interface Playtable to IPlayable

* Turned Abstract Method for AI into interface

* Refactored Game to follow encapsulation principle

* Removed unused imports

* Applied encapsulation principle to TurnBasedBame.java

* Privated methods that didn't have to be public

* Reversi: made method private

* Changed checkForEarlyDraw so it doesn't need a game as input.

* Fixed warning "Warning:(27, 12) Copy constructor does not copy field 'mostRecentlyFlippedPieces'", removed unused field

* Made connect4 public method private

* half done with the widget system

* added some comments and made some methods a bit more readable

* widget system almost complete

* Functional code, is now object orientated

* Removed no more needed comments

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

* rest van de tutorials toegevoegd

* resizable true

* fixed turn skip bug
fixed end score bug
now only shows legal and highlight moves when human

* Squashed commit of the following:

commit a517f2f302baa89f8ef59946a31c7bb59c56770f
Author: Stef <stbuwalda@gmail.com>
Date:   Thu Nov 27 15:43:43 2025 +0100

    Make it so the game shows "Waiting on ... to make their move". Styling isn't done but it is easier to see who's turn it is. There is a lot of structuring to do in the previous code...

* Fixed compilation errors

* Changed the way turns are being stored in TurnBasedGame.

* Removed views

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

* 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

* 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

* 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

* Removed loading widget from Server.java

* Fixed old new EventFlow().listen() missing false as third param

* 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 <>

* 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>

* added back button sounds because SOMEONE fucked it up.....

* 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 <>

* Double loading call fix, LoadingWidget docs

* 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 <>

* Fixed systems starting, before assets being loaded (I am retarded)

* Added infinite boolean, fixed loading behaviour at startup

* 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

* Best fix for white screen at start

* Making threads verbose regarding exceptions

* Loading circle, better loading colors.

* 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 <>

* initSystems now uses latch instead of timer. Moved single threads to Executor

* Safety

* Deleted unnecessary imports

* Code cleanup

* changed "fullscreen exit key combination" from esc to F11

* shitty fix for player selector spacing issue

* shitty fix for player selector spacing issue v2

* fixed reversi colors being switched, causing multiple issues

* 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>

* Better limits to generic acceptance

* Will fix tests etc later

---------

Co-authored-by: ramollia <@>
Co-authored-by: Ticho Hidding <tichohidding@gmail.com>
Co-authored-by: Stef <48526421+StefBuwalda@users.noreply.github.com>
Co-authored-by: Stef <stbuwalda@gmail.com>
Co-authored-by: michiel <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
Co-authored-by: tichohidding <58555714+tichohidding@users.noreply.github.com>
2025-12-09 21:28:45 +01:00
67 changed files with 3571 additions and 463 deletions

View File

@@ -2,7 +2,7 @@
<profile version="1.0">
<option name="myName" value="Project Default" />
<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 class="WriteOnlyObject" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>

View File

@@ -11,18 +11,19 @@ import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.popup.SendChallengePopup;
import org.toop.app.widget.view.ServerView;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.game.players.ArtificialPlayer;
import org.toop.framework.game.players.OnlinePlayer;
import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.networking.connection.clients.TournamentNetworkingClient;
import org.toop.framework.networking.connection.events.NetworkEvents;
import org.toop.framework.networking.connection.types.NetworkingConnector;
import org.toop.framework.game.players.ArtificialPlayer;
import org.toop.framework.game.players.OnlinePlayer;
import org.toop.framework.game.players.ai.RandomAI;
import org.toop.framework.networking.server.gateway.NettyGatewayServer;
import org.toop.game.players.ai.mcts.MCTSAI3;
import org.toop.local.AppContext;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
@@ -118,7 +119,8 @@ public final class Server {
return;
}
primary = new ServerView(user, this::sendChallenge, clientId);
primary = new ServerView(user, this::sendChallenge, user, clientId);
WidgetContainer.getCurrentView().transitionNextCustom(primary, "disconnect", this::disconnect);
a.unsubscribe("connecting");
@@ -160,7 +162,8 @@ public final class Server {
.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.ClosedConnection.class, this::closedConnection, false, "closed-connection")
.listen(NetworkEvents.TournamentResultResponse.class, this::handleTournamentResult, false, "tournament-result");
connectFlow = a;
}
@@ -197,39 +200,22 @@ public final class Server {
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);
//information.players[0] = playerInformation;
information.players[0].name = user;
information.players[0].isHuman = false;
information.players[0].computerDifficulty = 5;
information.players[0].computerThinkTime = 1;
information.players[1].name = response.opponent();
/*switch (type){
case TICTACTOE ->{
players[myTurn] = new ArtificialPlayer<>(new TicTacToeAIR(9), user);
}
case REVERSI ->{
players[myTurn] = new ArtificialPlayer<>(new ReversiAIR(), user);
}
}*/
information.players[userStartingTurn].name = user;
information.players[opponentStartingTurn].name = response.opponent();
Player[] players = new Player[2];
players[userStartingTurn] = new ArtificialPlayer(new MCTSAI3(1000, 8), user);
players[opponentStartingTurn] = new OnlinePlayer(response.opponent());
switch (type) {
case TICTACTOE ->{
Player[] players = new Player[2];
players[(myTurn + 1) % 2] = new OnlinePlayer(response.opponent());
players[myTurn] = new ArtificialPlayer(new RandomAI(), user);
gameController = new TicTacToeBitController(players);
}
case REVERSI -> {
Player[] players = new Player[2];
players[(myTurn + 1) % 2] = new OnlinePlayer(response.opponent());
players[myTurn] = new ArtificialPlayer(new RandomAI(), user);
gameController = new ReversiBitController(players);}
case TICTACTOE -> gameController = new TicTacToeBitController(players);
case REVERSI -> gameController = new ReversiBitController(players);
default -> new ErrorPopup("Unsupported game type.");
}
@@ -257,6 +243,13 @@ public final class Server {
gameController.gameFinished(response);
}
private void handleTournamentResult(NetworkEvents.TournamentResultResponse response) {
IO.println(response.gameType());
IO.println(Arrays.toString(response.names()));
IO.println(Arrays.toString(response.scoreTypes()));
IO.println(Arrays.toString(response.scores().toArray()));
}
private void handleReceivedMove(NetworkEvents.GameMoveResponse response) {
if (gameController == null) {
return;
@@ -356,7 +349,8 @@ public final class Server {
private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) {
gameList.clear();
var gl = List.of(event.gamelist());
var gl = new java.util.ArrayList<>(List.of(event.gamelist()));
gl.sort(String::compareTo);
gameList.addAll(gl);
primary.updateGameList(gl);
}

View File

@@ -78,6 +78,7 @@ public abstract class BitGameCanvas implements GameCanvas {
}
canvas.setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY) {
return;
}
@@ -93,9 +94,6 @@ public abstract class BitGameCanvas implements GameCanvas {
}
});
render();
}

View File

@@ -2,9 +2,13 @@ package org.toop.app.canvas;
import javafx.scene.paint.Color;
import org.toop.app.App;
import org.toop.framework.game.games.reversi.BitboardReversi;
import org.toop.framework.game.players.LocalPlayer;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public class ReversiBitCanvas extends BitGameCanvas {
private TurnBasedGame gameCopy;
private int previousCell;
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 -> {
@@ -20,6 +24,9 @@ public class ReversiBitCanvas extends BitGameCanvas {
break;
}
}
if (hovered != null) {
checkHoverDots(hovered, cellId);
}
});
}
@@ -31,9 +38,31 @@ public class ReversiBitCanvas extends BitGameCanvas {
@Override
public void redraw(TurnBasedGame gameCopy) {
this.gameCopy = gameCopy;
clearAll();
long[] board = gameCopy.getBoard();
loopOverBoard(board[0], (i) -> drawDot(Color.WHITE, i));
loopOverBoard(board[1], (i) -> drawDot(Color.BLACK, i));
}
public void drawLegalDots(TurnBasedGame gameCopy){
long legal = gameCopy.getLegalMoves();
loopOverBoard(legal, (i) -> drawInnerDot(gameCopy.getCurrentTurn()==0?new Color(1f,1f,1f,0.65f) :new Color(0f,0f,0f,0.65f), i,false));
}
private void checkHoverDots(BitGameCanvas.Cell hovered, int cellId){
if (previousCell == cellId){
return;
}
long backflips = ((BitboardReversi)gameCopy).getFlips(1L << previousCell);
loopOverBoard(backflips, (i) -> drawInnerDot(gameCopy.getCurrentTurn()==1?Color.WHITE:Color.BLACK, i,true));
previousCell = cellId;
if (gameCopy.getPlayer(gameCopy.getCurrentTurn()) instanceof LocalPlayer) {
long legal = gameCopy.getLegalMoves();
if ((legal & (1L << cellId)) != 0) {
long flips = ((BitboardReversi) gameCopy).getFlips(1L << cellId);
loopOverBoard(flips, (i) -> drawInnerDot(gameCopy.getCurrentTurn() == 0 ? Color.WHITE : Color.BLACK, i, false));
}
}
}
}

View File

@@ -5,10 +5,12 @@ 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.canvas.ReversiBitCanvas;
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.game.games.reversi.BitboardReversi;
import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.game.threadBehaviour.SupportsOnlinePlay;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
@@ -35,14 +37,16 @@ public class GenericGameController implements GameController {
// 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());
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))));
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));
@@ -53,23 +57,37 @@ public class GenericGameController implements GameController {
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 -> {
if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}
}, false);
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(){
@@ -100,7 +118,7 @@ public class GenericGameController implements GameController {
}
public Player getPlayer(int player){
if (player < 0 || player >= 2){ // TODO: Make game turn player count
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");
}
@@ -137,6 +155,18 @@ public class GenericGameController implements GameController {
@Override
public void updateUI() {
canvas.redraw(game.deepCopy());
TurnBasedGame gameCopy = game.deepCopy();
canvas.redraw(gameCopy);
String gameType = game.getClass().getSimpleName().replace("Bitboard","");
gameView.nextPlayer(true, getCurrentPlayer().getName(), game.getPlayer(1-getCurrentPlayerIndex()).getName(),gameType);
if (gameType.equals("Reversi")) {
BitboardReversi reversiGame = (BitboardReversi) game;
BitboardReversi.Score reversiScore = reversiGame.getScore();
gameView.setPlayer1Score(reversiScore.black());
gameView.setPlayer2Score(reversiScore.white());
if (getCurrentPlayer() instanceof LocalPlayer) {
((ReversiBitCanvas)canvas).drawLegalDots(gameCopy);
}
}
}
}

View File

@@ -8,16 +8,22 @@ 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 {
private BitboardReversi game;
public ReversiBitController(Player[] players) {
BitboardReversi game = new BitboardReversi();
game.init(players);
ThreadBehaviour thread = new LocalThreadBehaviour(game);
for (Player player : players) {
if (player instanceof OnlinePlayer){
thread = new OnlineThreadBehaviour(game);
}
}
ThreadBehaviour thread = Arrays.stream(players).anyMatch(e -> e instanceof OnlinePlayer) ?
new OnlineThreadBehaviour(game) : new LocalThreadBehaviour(game);
super(new ReversiBitCanvas(), game, thread, "Reversi");
}
public BitboardReversi.Score getScore() {
return game.getScore();
}
}

View File

@@ -8,16 +8,16 @@ 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 = new LocalThreadBehaviour(game);
for (Player player : players) {
if (player instanceof OnlinePlayer){
thread = new OnlineThreadBehaviour(game);
}
}
super(new TicTacToeBitCanvas(), game, thread , "TicTacToe");
ThreadBehaviour thread = Arrays.stream(players).anyMatch(e -> e instanceof OnlinePlayer) ?
new OnlineThreadBehaviour(game) : new LocalThreadBehaviour(game);
super(new TicTacToeBitCanvas(), game, thread, "TicTacToe");
}
}

View File

@@ -4,6 +4,7 @@ import org.toop.app.widget.complex.ConfirmWidget;
import org.toop.app.widget.complex.PopupWidget;
import javafx.geometry.Pos;
import org.toop.framework.game.games.reversi.BitboardReversi;
public final class GameOverPopup extends PopupWidget {
public GameOverPopup(boolean winOrTie, String winner) {
@@ -15,7 +16,6 @@ public final class GameOverPopup extends PopupWidget {
else{
confirmWidget.setMessage("It was a tie!");
}
confirmWidget.addButton("ok", this::hide);
add(Pos.CENTER, confirmWidget);

View File

@@ -6,6 +6,8 @@ import javafx.scene.text.Font;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.GameOverPopup;
import java.util.Objects;
import java.util.function.Consumer;
import javafx.application.Platform;
import javafx.geometry.Pos;
@@ -24,6 +26,8 @@ public final class GameView extends ViewWidget {
private final Text player2Header;
private Circle player1Icon;
private Circle player2Icon;
private final Text player1Score;
private final Text player2Score;
private final Button forfeitButton;
private final Button exitButton;
private final TextField chatInput;
@@ -38,6 +42,8 @@ public final class GameView extends ViewWidget {
player2Header = Primitive.header("");
player1Icon = new Circle();
player2Icon = new Circle();
player1Score = Primitive.header("");
player2Score = Primitive.header("");
if (onForfeit != null) {
forfeitButton = Primitive.button("forfeit", () -> onForfeit.run(), false);
@@ -94,7 +100,7 @@ public final class GameView extends ViewWidget {
}
}
public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer, char GameType) {
public void nextPlayer(boolean isMe, String currentPlayer, String nextPlayer, String GameType) {
Platform.runLater(() -> {
if (!(hasSet)) {
playerHeader.setText(currentPlayer + " vs. " + nextPlayer);
@@ -112,8 +118,8 @@ public final class GameView extends ViewWidget {
new GameOverPopup(iWon, winner).show(Pos.CENTER);
}
private void setPlayerHeaders(boolean isMe, String currentPlayer, String nextPlayer, char GameType) {
if (GameType == 'T') {
private void setPlayerHeaders(boolean isMe, String currentPlayer, String nextPlayer, String GameType) {
if (Objects.equals(GameType, "TicTacToe")) {
if (isMe) {
player1Header.setText("X: " + currentPlayer);
player2Header.setText("O: " + nextPlayer);
@@ -124,7 +130,7 @@ public final class GameView extends ViewWidget {
}
setPlayerInfoTTT();
}
else if (GameType == 'R') {
else if (Objects.equals(GameType, "Reversi")) {
if (isMe) {
player1Header.setText(currentPlayer);
player2Header.setText(nextPlayer);
@@ -151,14 +157,16 @@ public final class GameView extends ViewWidget {
private void setPlayerInfoReversi() {
var player1box = Primitive.hbox(
player1Icon,
player1Header
player1Header,
player1Score
);
player1box.getStyleClass().add("hboxspacing");
var player2box = Primitive.hbox(
player2Icon,
player2Header
player2Header,
player2Score
);
player2box.getStyleClass().add("hboxspacing");
@@ -172,8 +180,16 @@ public final class GameView extends ViewWidget {
player1Icon.setRadius(player1Header.fontProperty().map(Font::getSize).getValue());
player2Icon.setRadius(player2Header.fontProperty().map(Font::getSize).getValue());
player1Icon.setFill(Color.BLACK);
player2Icon.setFill(Color.WHITE);
player1Icon.setFill(Color.WHITE);
player2Icon.setFill(Color.BLACK);
add(Pos.TOP_RIGHT, playerInfo);
}
public void setPlayer1Score(int score) {
player1Score.setText("(" + Integer.toString(score) + ")");
}
public void setPlayer2Score(int score) {
player2Score.setText("(" + Integer.toString(score) + ")");
}
}

View File

@@ -4,17 +4,19 @@ import javafx.application.Platform;
import org.toop.app.GameInformation;
import org.toop.app.gameControllers.ReversiBitController;
import org.toop.app.gameControllers.TicTacToeBitController;
import org.toop.framework.game.players.LocalPlayer;
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.framework.game.players.LocalPlayer;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.PlayerInfoWidget;
import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.tutorial.*;
import org.toop.framework.game.players.ai.MiniMaxAI;
import org.toop.framework.game.players.ai.RandomAI;
import org.toop.game.players.ai.MiniMaxAI;
import org.toop.game.players.ai.mcts.MCTSAI1;
import org.toop.game.players.ai.mcts.MCTSAI3;
import org.toop.game.players.ai.mcts.MCTSAI4;
import org.toop.local.AppContext;
import javafx.geometry.Pos;
@@ -52,7 +54,7 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[0].isHuman) {
players[0] = new LocalPlayer(information.players[0].name);
} else {
players[0] = new ArtificialPlayer(new RandomAI(), "Random AI");
players[0] = new ArtificialPlayer(new MCTSAI1(100), "MCTS AI");
}
if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name);
@@ -80,12 +82,13 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[0].isHuman) {
players[0] = new LocalPlayer(information.players[0].name);
} else {
players[0] = new ArtificialPlayer(new RandomAI(), "Random AI");
// players[0] = new ArtificialPlayer(new RandomAI(), "Random AI");
players[0] = new ArtificialPlayer(new MCTSAI1(100), "MCTS V1 AI");
}
if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name);
} else {
players[1] = new ArtificialPlayer(new MiniMaxAI(6), "MiniMax");
players[1] = new ArtificialPlayer(new MCTSAI4(100, 8), "MCTS V4 AI");
}
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) {
new ShowEnableTutorialWidget(

View File

@@ -6,6 +6,8 @@ import javafx.scene.control.ComboBox;
import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.ViewWidget;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
@@ -15,6 +17,7 @@ import javafx.geometry.Pos;
import javafx.scene.control.Button;
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 {
@@ -22,46 +25,65 @@ public final class ServerView extends ViewWidget {
private final Consumer<String> onPlayerClicked;
private final long clientId;
private final ComboBox<String> gameList;
private final ComboBox<String> gameListSub;
private final ComboBox<String> gameListTour;
private final ListView<Button> listView;
private Button subscribeButton;
public ServerView(String user, Consumer<String> onPlayerClicked, long clientId) {
public ServerView(String user, Consumer<String> onPlayerClicked, String userName, long clientId) {
this.user = user;
this.onPlayerClicked = onPlayerClicked;
this.clientId = clientId;
this.gameList = new ComboBox<>();
this.gameListSub = new ComboBox<>();
this.gameListTour = new ComboBox<>();
this.listView = new ListView<>();
setupLayout();
setupLayout(userName);
}
private void setupLayout() {
private void setupLayout(String userName) {
var playerHeader = Primitive.header(user, false);
subscribeButton = Primitive.button(
"subscribe",
() -> new EventFlow().addPostEvent(new NetworkEvents.SendSubscribe(clientId, gameList.getValue())).postEvent(),
false,
true
); // TODO localize
if (userName.equals("host")) { // TODO is fragile
var tournamentButton = Primitive.hbox(
gameListTour,
Primitive.button(
"tournament",
() -> GlobalEventBus.get().post(new NetworkEvents.SendCommand(clientId, "tournament", "start", gameListTour.getValue())),
false,
false
)
);
var subscribe = Primitive.hbox(gameList, subscribeButton);
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 playerListSection = Primitive.vbox(
playerHeader,
Primitive.separator(),
subscribe,
listView
);
var subscribe = Primitive.hbox(gameListSub, subscribeButton);
add(Pos.CENTER, playerListSection);
var playerListSection = Primitive.vbox(
playerHeader,
Primitive.separator(),
subscribe,
listView
);
var disconnectButton = Primitive.button(
"disconnect", () -> transitionPrevious(), false);
add(Pos.CENTER, playerListSection);
add(Pos.BOTTOM_LEFT, Primitive.vbox(disconnectButton));
var disconnectButton = Primitive.button(
"disconnect",
this::transitionPrevious,
false
);
add(Pos.BOTTOM_LEFT, Primitive.vbox(disconnectButton));
}
}
public void update(List<String> players) {
@@ -77,9 +99,13 @@ public final class ServerView extends ViewWidget {
public void updateGameList(List<String> games) {
Platform.runLater(() -> {
gameList.getItems().clear();
gameList.setItems(FXCollections.observableArrayList(games));
gameList.getSelectionModel().select(0);
gameListSub.getItems().clear();
gameListSub.setItems(FXCollections.observableArrayList(games));
gameListSub.getSelectionModel().select(0);
gameListTour.getItems().clear();
gameListTour.setItems(FXCollections.observableArrayList(games));
gameListTour.getSelectionModel().select(0);
});
}

View File

@@ -415,11 +415,9 @@ public class EventFlow {
/**
* Posts the event added through {@link #addPostEvent} asynchronously.
*
* @deprecated use {@link #postEvent()} instead.
*/
@Deprecated
public EventFlow asyncPostEvent() {
eventBus.post(this.event);
GlobalEventBus.get().post(this.event);
return this;
}

View File

@@ -1,15 +1,20 @@
package org.toop.framework.eventbus;
import org.apache.logging.log4j.LogManager;
import org.toop.framework.eventbus.bus.AsyncEventBus;
import org.toop.framework.eventbus.bus.DefaultEventBus;
import org.toop.framework.eventbus.bus.DisruptorEventBus;
import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.store.DefaultSubscriberStore;
import org.toop.framework.eventbus.subscriber.Subscriber;
public class GlobalEventBus implements EventBus {
private static final EventBus INSTANCE = new DisruptorEventBus(
LogManager.getLogger(DisruptorEventBus.class),
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GlobalEventBus implements AsyncEventBus {
private static final AsyncEventBus INSTANCE = new DefaultEventBus(
LogManager.getLogger(DefaultEventBus.class),
new DefaultSubscriberStore()
);
@@ -34,6 +39,11 @@ public class GlobalEventBus implements EventBus {
INSTANCE.post(event);
}
@Override
public <T extends EventType> void asyncPost(T event) {
INSTANCE.asyncPost(event);
}
@Override
public void shutdown() {
INSTANCE.shutdown();
@@ -43,4 +53,5 @@ public class GlobalEventBus implements EventBus {
public void reset() {
INSTANCE.reset();
}
}

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

@@ -5,12 +5,16 @@ 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 EventBus {
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;
@@ -36,11 +40,16 @@ public class DefaultEventBus implements EventBus {
Class<T> eventClass = (Class<T>) subscriber.event();
Consumer<EventType> action = (Consumer<EventType>) subscriber.handler();
action.accept((EventType) eventClass.cast(event));
action.accept(eventClass.cast(event));
}
}
}
@Override
public <T extends EventType> void asyncPost(T event) {
asyncExecutor.submit(() -> post(event));
}
@Override
public void shutdown() {
eventsHolder.reset();
@@ -50,4 +59,5 @@ public class DefaultEventBus implements EventBus {
public void reset() {
eventsHolder.reset();
}
}

View File

@@ -1,5 +1,7 @@
package org.toop.framework.game;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
@@ -10,36 +12,46 @@ public abstract class BitboardGame implements TurnBasedGame {
private final int columnSize;
private final int rowSize;
protected PlayResult state;
private Player[] players;
// long is 64 bits. Every game has a limit of 64 cells maximum.
private final long[] playerBitboard;
private int currentTurn = 0;
protected final long[] playerBitboard;
protected int currentTurn = 0;
private final int playerCount;
public BitboardGame(int columnSize, int rowSize, int playerCount) {
this.columnSize = columnSize;
this.rowSize = rowSize;
this.playerCount = playerCount;
this.playerBitboard = new long[playerCount];
}
this.playerCount = playerCount;
this.state = new PlayResult(GameState.NORMAL, -1);
@Override
public void init(Player[] players) {
this.players = players;
this.playerBitboard = new long[playerCount];
Arrays.fill(playerBitboard, 0L);
}
@Override
public void init(Player[] players) {
this.players = players;
Arrays.fill(playerBitboard, 0L);
}
public BitboardGame(BitboardGame other) {
this.columnSize = other.columnSize;
this.rowSize = other.rowSize;
this.playerCount = other.playerCount;
this.state = other.state;
this.playerBitboard = other.playerBitboard.clone();
this.currentTurn = other.currentTurn;
this.players = Arrays.stream(other.players)
.map(Player::deepCopy)
.toArray(Player[]::new);
this.players = other.players;
// TODO: Players are not deep copied, which is bad. I don't know why but deepcopying breaks it. Fix it
//this.players = Arrays.stream(other.players)
// .map(Player::deepCopy)
// .toArray(Player[]::new);
}
public int getColumnSize() {
@@ -62,6 +74,8 @@ public abstract class BitboardGame implements TurnBasedGame {
return playerBitboard.length;
}
public int getAmountOfTurns() { return currentTurn; }
public int getCurrentTurn() {
return getCurrentPlayerIndex();
}
@@ -80,9 +94,17 @@ public abstract class BitboardGame implements TurnBasedGame {
return players[getCurrentPlayerIndex()];
}
@Override
public PlayResult getState() {
return state;
}
@Override
public boolean isTerminal() {
return state.state() == GameState.WIN || state.state() == GameState.DRAW;
}
@Override
@Override
public long[] getBoard() {return this.playerBitboard;}
public void nextTurn() {

View File

@@ -8,6 +8,9 @@ import org.toop.framework.gameFramework.model.game.threadBehaviour.SupportsOnlin
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.players.OnlinePlayer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Handles online multiplayer game logic.
* <p>
@@ -15,6 +18,9 @@ import org.toop.framework.game.players.OnlinePlayer;
* for the local player while receiving moves from other players.
*/
public class OnlineThreadBehaviour extends AbstractThreadBehaviour implements SupportsOnlinePlay {
private ExecutorService moveExecutor = Executors.newSingleThreadExecutor();
/**
* Creates behaviour and sets the first local player
* (non-online player) from the given array.
@@ -51,8 +57,30 @@ public class OnlineThreadBehaviour extends AbstractThreadBehaviour implements Su
*/
@Override
public void onYourTurn(long clientId) {
if (!isRunning.get()) return;
long move = game.getPlayer(game.getCurrentTurn()).getMove(game.deepCopy());
logger.info("Yourturn");
if (!isRunning.get()) {
logger.warn("Game is not running!");
return;
}
TurnBasedGame gameCopy = game.deepCopy();
if (gameCopy == null) {
logger.error("Could not deep copy game");
return;
}
logger.info("Successfully collected game copy");
Player player = gameCopy.getPlayer(game.getCurrentTurn());
if (player == null) {
logger.error("Could not find current turn's player");
return;
}
logger.info("Successfully collected current turn's player");
long move = player.getMove(gameCopy);
logger.info("Move set: {}", move);
logger.info("Completed onYourTurn");
sendMove(clientId, move);
}

View File

@@ -1,31 +1,39 @@
package org.toop.framework.game.gameThreads;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.utils.ImmutablePair;
import org.toop.framework.utils.Pair;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.function.Consumer;
import static org.toop.framework.gameFramework.GameState.TURN_SKIPPED;
import static org.toop.framework.gameFramework.GameState.WIN;
public class ServerThreadBehaviour extends AbstractThreadBehaviour implements Runnable {
private final Consumer<ImmutablePair<String, Integer>> onPlayerMove;
private final Consumer<Pair<GameState, Integer>> onGameEnd;
private final ExecutorService moveExecutor = Executors.newSingleThreadExecutor();
private final Duration timeOut;
/**
* Creates a new base behaviour for the specified game.
*
* @param game the turn-based game to control
*/
public ServerThreadBehaviour(TurnBasedGame game, Consumer<ImmutablePair<String, Integer>> onPlayerMove, Consumer<Pair<GameState, Integer>> onGameEnd) {
public ServerThreadBehaviour(
TurnBasedGame game,
Consumer<ImmutablePair<String,
Integer>> onPlayerMove,
Consumer<Pair<GameState, Integer>> onGameEnd,
Duration timeOut
) {
this.onPlayerMove = onPlayerMove;
this.onGameEnd = onGameEnd;
this.timeOut = timeOut;
super(game);
}
@@ -59,23 +67,42 @@ public class ServerThreadBehaviour extends AbstractThreadBehaviour implements Ru
public void run() {
while (isRunning.get()) {
Player currentPlayer = game.getPlayer(game.getCurrentTurn());
long move = currentPlayer.getMove(game.deepCopy());
PlayResult result = game.play(move);
GameState state = result.state();
notifyPlayerMove(new ImmutablePair<>(currentPlayer.getName(), Long.numberOfTrailingZeros(move)));
Future<Long> move = moveExecutor.submit(() -> currentPlayer.getMove(game.deepCopy()));
switch (state) {
case WIN, DRAW -> {
isRunning.set(false);
notifyGameEnd(new ImmutablePair<>(state, game.getWinner()));
}
case NORMAL, TURN_SKIPPED -> { /* continue normally */ }
default -> {
logger.error("Unexpected state {}", state);
isRunning.set(false);
throw new RuntimeException("Unknown state: " + state);
PlayResult result;
try {
long moveResult = move.get(timeOut.toMillis(), TimeUnit.MILLISECONDS);
result = game.play(moveResult);
GameState state = result.state();
notifyPlayerMove(new ImmutablePair<>(currentPlayer.getName(), Long.numberOfTrailingZeros(moveResult)));
switch (state) {
case WIN, DRAW -> {
isRunning.set(false);
moveExecutor.shutdown();
notifyGameEnd(new ImmutablePair<>(state, game.getWinner()));
}
case NORMAL, TURN_SKIPPED -> { /* continue normally */ }
default -> {
logger.error("Unexpected state {}", state);
isRunning.set(false);
moveExecutor.shutdown();
throw new RuntimeException("Unknown state: " + state);
}
}
} catch (InterruptedException | ExecutionException e) {
isRunning.set(false);
notifyGameEnd(new ImmutablePair<>(GameState.DRAW, 0));
moveExecutor.shutdown();
return;
} catch (TimeoutException e) {
isRunning.set(false);
notifyGameEnd(new ImmutablePair<>(GameState.WIN, 1+game.getWinner()%2));
moveExecutor.shutdown();
return;
}
}
}

View File

@@ -4,7 +4,6 @@ import org.toop.framework.game.BitboardGame;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.BitboardGame;
public class BitboardReversi extends BitboardGame {
@@ -32,47 +31,227 @@ public class BitboardReversi extends BitboardGame {
}
public long getLegalMoves() {
long legalMoves = 0L;
final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer());
long legalMoves = 0L;
final long empty = ~(player | opponent);
// north & south
legalMoves |= computeMoves(player, opponent, 8, -1L);
legalMoves |= computeMoves(player, opponent, -8, -1L);
long mask;
long direction;
// east & west
legalMoves |= computeMoves(player, opponent, 1, notAFile);
legalMoves |= computeMoves(player, opponent, -1, notHFile);
// north
mask = opponent;
direction = (player << 8) & mask;
// north-east & north-west & south-east & south-west
legalMoves |= computeMoves(player, opponent, 9, notAFile);
legalMoves |= computeMoves(player, opponent, 7, notHFile);
legalMoves |= computeMoves(player, opponent, -7, notAFile);
legalMoves |= computeMoves(player, opponent, -9, notHFile);
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
legalMoves |= (direction << 8) & empty;
// south
mask = opponent;
direction = (player >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
legalMoves |= (direction >>> 8) & empty;
// east
mask = opponent & notAFile;
direction = (player << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
legalMoves |= (direction << 1) & empty & notAFile;
// west
mask = opponent & notHFile;
direction = (player >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
legalMoves |= (direction >>> 1) & empty & notHFile;
// north-east
mask = opponent & notAFile;
direction = (player << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
legalMoves |= (direction << 9) & empty & notAFile;
// north-west
mask = opponent & notHFile;
direction = (player << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
legalMoves |= (direction << 7) & empty & notHFile;
// south-east
mask = opponent & notAFile;
direction = (player >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
legalMoves |= (direction >>> 7) & empty & notAFile;
// south-west
mask = opponent & notHFile;
direction = (player >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
legalMoves |= (direction >>> 9) & empty & notHFile;
return legalMoves;
}
public long getFlips(long move) {
long flips = 0L;
final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer());
long flips = 0L;
long mask;
long direction;
// north & south
flips |= computeFlips(move, player, opponent, 8, -1L);
flips |= computeFlips(move, player, opponent, -8, -1L);
// north
mask = opponent;
direction = (move << 8) & mask;
// east & west
flips |= computeFlips(move, player, opponent, 1, notAFile);
flips |= computeFlips(move, player, opponent, -1, notHFile);
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
// north-east & north-west & south-east & south-west
flips |= computeFlips(move, player, opponent, 9, notAFile);
flips |= computeFlips(move, player, opponent, 7, notHFile);
flips |= computeFlips(move, player, opponent, -7, notAFile);
flips |= computeFlips(move, player, opponent, -9, notHFile);
if (((direction << 8) & player) != 0) {
flips |= direction;
}
// south
mask = opponent;
direction = (move >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
direction |= (direction >>> 8) & mask;
if (((direction >>> 8) & player) != 0) {
flips |= direction;
}
// east
mask = opponent & notAFile;
direction = (move << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
if (((direction << 1) & player & notAFile) != 0) {
flips |= direction;
}
// west
mask = opponent & notHFile;
direction = (move >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
if (((direction >>> 1) & player & notHFile) != 0) {
flips |= direction;
}
// north-east
mask = opponent & notAFile;
direction = (move << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
if (((direction << 9) & player & notAFile) != 0) {
flips |= direction;
}
// north-west
mask = opponent & notHFile;
direction = (move << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
if (((direction << 7) & player & notHFile) != 0) {
flips |= direction;
}
// south-east
mask = opponent & notAFile;
direction = (move >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
if (((direction >>> 7) & player & notAFile) != 0) {
flips |= direction;
}
// south-west
mask = opponent & notHFile;
direction = (move >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
if (((direction >>> 9) & player & notHFile) != 0) {
flips |= direction;
}
return flips;
}
@@ -105,16 +284,20 @@ public class BitboardReversi extends BitboardGame {
int winner = getWinner();
if (winner == -1) {
return new PlayResult(GameState.DRAW, -1);
state = new PlayResult(GameState.DRAW, -1);
return state;
}
return new PlayResult(GameState.WIN, winner);
state = new PlayResult(GameState.WIN, winner);
return state;
}
return new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex());
state = new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex());
return state;
}
return new PlayResult(GameState.NORMAL, getCurrentPlayerIndex());
state = new PlayResult(GameState.NORMAL, getCurrentPlayerIndex());
return state;
}
public Score getScore() {
@@ -137,39 +320,58 @@ public class BitboardReversi extends BitboardGame {
else if (blackCount > whiteCount){
return 0;
}
else{
else {
return 1;
}
}
private long computeMoves(long player, long opponent, int shift, long mask) {
long moves = shift(player, shift, mask) & opponent;
long captured = moves;
@Override
public float rateMove(long move) {
final long corners = 0x8100000000000081L;
while (moves != 0) {
moves = shift(moves, shift, mask) & opponent;
captured |= moves;
if ((move & corners) != 0L) {
return 0.4f;
}
long landing = shift(captured, shift, mask);
return landing & ~(player | opponent);
}
final long xSquares = 0x0042000000004200L;
private long computeFlips(long move, long player, long opponent, int shift, long mask) {
long flips = 0L;
long pos = move;
while (true) {
pos = shift(pos, shift, mask);
if (pos == 0) return 0L;
if ((pos & opponent) != 0) flips |= pos;
else if ((pos & player) != 0) return flips;
else return 0L;
if ((move & xSquares) != 0) {
return -0.4f;
}
final long cSquares = 0x4281000000008142L;
if ((move & cSquares) != 0) {
return -0.1f;
}
return 0.0f;
}
private long shift(long bit, int shift, long mask) {
return shift > 0 ? (bit << shift) & mask : (bit >>> -shift) & mask;
@Override
public long heuristicMove(long legalMoves) {
long bestMove = 0L;
float bestMoveRate = Float.NEGATIVE_INFINITY;
while (legalMoves != 0L) {
final long move = legalMoves & -legalMoves;
final float moveRate = rateMove(move);
if (moveRate > bestMoveRate) {
bestMove = move;
bestMoveRate = moveRate;
}
legalMoves &= ~move;
}
return bestMove;
}
@Override
public void setFrom(long player1, long player2, int turn) {
this.playerBitboard[0] = player1;
this.playerBitboard[1] = player2;
this.currentTurn = turn;
}
}

View File

@@ -2,6 +2,7 @@ package org.toop.framework.game.games.tictactoe;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.BitboardGame;
@@ -45,7 +46,8 @@ public class BitboardTicTacToe extends BitboardGame {
public PlayResult play(long move) {
// Player loses if move is invalid
if ((move & getLegalMoves()) == 0 || Long.bitCount(move) != 1){
return new PlayResult(GameState.WIN, getNextPlayer());
state = new PlayResult(GameState.WIN, getNextPlayer());
return state;
}
// Move is legal, make move
@@ -56,7 +58,8 @@ public class BitboardTicTacToe extends BitboardGame {
// Check if current player won
if (checkWin(playerBitboard)) {
return new PlayResult(GameState.WIN, getCurrentPlayerIndex());
state = new PlayResult(GameState.WIN, getCurrentPlayerIndex());
return state;
}
// Proceed to next turn
@@ -65,11 +68,13 @@ public class BitboardTicTacToe extends BitboardGame {
// Check for early draw
if (getLegalMoves() == 0L || checkEarlyDraw()) {
return new PlayResult(GameState.DRAW, -1);
state = new PlayResult(GameState.DRAW, -1);
return state;
}
// Nothing weird happened, continue on as normal
return new PlayResult(GameState.NORMAL, -1);
state = new PlayResult(GameState.NORMAL, -1);
return state;
}
private boolean checkWin(long board) {
@@ -106,4 +111,18 @@ public class BitboardTicTacToe extends BitboardGame {
public BitboardTicTacToe deepCopy() {
return new BitboardTicTacToe(this);
}
@Override
public float rateMove(long move) {
return 0.0f;
}
@Override
public long heuristicMove(long legalMoves) {
return legalMoves;
}
@Override
public void setFrom(long player1, long player2, int turn) {
}
}

View File

@@ -14,7 +14,9 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame;
*/
public class ArtificialPlayer extends AbstractPlayer {
/** The AI instance used to calculate moves. */
/**
* The AI instance used to calculate moves.
*/
private final AI ai;
/**
@@ -44,12 +46,21 @@ public class ArtificialPlayer extends AbstractPlayer {
* @return the integer representing the chosen move
* @throws ClassCastException if {@code gameCopy} is not of type {@code T}
*/
public long getMove(TurnBasedGame gameCopy) {
protected long determineMove(TurnBasedGame gameCopy) {
return ai.getMove(gameCopy);
}
/**
* Creates a deep copy of this AI player.
*
* @return a copy of this player
*/
@Override
public ArtificialPlayer deepCopy() {
return new ArtificialPlayer(this);
}
public AI getAi() {
return ai;
}
}

View File

@@ -6,65 +6,88 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* Represents a local player who provides moves manually.
*
* @param <T> the type of turn-based game
*/
public class LocalPlayer extends AbstractPlayer {
// Future can be used with event system, IF unsubscribeAfterSuccess works...
// private CompletableFuture<Integer> LastMove = new CompletableFuture<>();
private CompletableFuture<Long> LastMove;
private CompletableFuture<Long> LastMove = new CompletableFuture<>();
/**
* Creates a new local player with the given name.
*
* @param name the player's name
*/
public LocalPlayer(String name) {
super(name);
}
/**
* Creates a copy of another local player.
*
* @param other the player to copy
*/
public LocalPlayer(LocalPlayer other) {
super(other);
this.LastMove = other.LastMove;
}
/**
* Waits for and returns the player's next legal move.
*
* @param gameCopy a copy of the current game
* @return the chosen move
*/
@Override
public long getMove(TurnBasedGame gameCopy) {
return getValidMove(gameCopy);
protected long determineMove(TurnBasedGame gameCopy) {
long legalMoves = gameCopy.getLegalMoves();
long move;
try {
do {
move = getLastMove();
IO.println("GETTING MOVE");
} while ((legalMoves & move) == 0);
return move;
} catch (Exception e) {
IO.println(e);
}
return -1;
}
public void setMove(long move) {
/**
* Sets the player's last move.
*
* @param move the move to set
*/
public void setLastMove(long move) {
LastMove.complete(move);
}
// TODO: helper function, would like to replace to get rid of this method
public static boolean contains(int[] array, int value){
for (int i : array) if (i == value) return true;
return false;
}
private long getMove2(TurnBasedGame gameCopy) {
LastMove = new CompletableFuture<>();
long move = 0;
/**
* Waits for the next move from the player.
*
* @return the chosen move or 0 if interrupted
*/
private long getLastMove() {
LastMove = new CompletableFuture<>(); // Reset the future
try {
move = LastMove.get();
System.out.println(Long.toBinaryString(move));
} catch (InterruptedException | ExecutionException e) {
// TODO: Add proper logging.
e.printStackTrace();
return LastMove.get();
} catch (ExecutionException | InterruptedException e) {
return 0;
}
return move;
}
protected long getValidMove(TurnBasedGame gameCopy){
// Get this player's valid moves
long validMoves = gameCopy.getLegalMoves();
// Make sure provided move is valid
// TODO: Limit amount of retries?
// TODO: Stop copying game so many times
long move = getMove2(gameCopy.deepCopy());
while ((validMoves & move) == 0) {
System.out.println("Not a valid move, try again");
move = getMove2(gameCopy.deepCopy());
}
return move;
}
/**
* Creates a deep copy of this local player.
*
* @return a copy of this player
*/
@Override
public LocalPlayer deepCopy() {
return new LocalPlayer(this.getName());
return new LocalPlayer(this);
}
/*public void register() {

View File

@@ -5,30 +5,45 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import org.toop.framework.gameFramework.model.player.Player;
/**
* Represents a player controlled remotely or over a network.
* <p>
* This class extends {@link AbstractPlayer} and can be used to implement game logic
* where moves are provided by an external source (e.g., another user or a server).
* Currently, this class is a placeholder and does not implement move logic.
* </p>
* Represents a player that participates online.
*
* @param <T> the type of turn-based game
*/
public class OnlinePlayer extends AbstractPlayer {
/**
* Constructs a new OnlinePlayer.
* <p>
* Currently, no additional initialization is performed. Subclasses or
* future implementations should provide mechanisms to receive moves from
* an external source.
* Creates a new online player with the given name.
*
* @param name the name of the player
*/
public OnlinePlayer(String name) {
super(name);
}
/**
* Creates a copy of another online player.
*
* @param other the player to copy
*/
public OnlinePlayer(OnlinePlayer other) {
super(other);
}
/**
* {@inheritDoc}
* <p>
* This method is not supported for online players.
*
* @throws UnsupportedOperationException always
*/
@Override
protected long determineMove(TurnBasedGame gameCopy) {
throw new UnsupportedOperationException("An online player does not support determining move");
}
/**
* {@inheritDoc}
*/
@Override
public Player deepCopy() {
return new OnlinePlayer(this);

View File

@@ -27,15 +27,10 @@ public class ServerPlayer extends AbstractPlayer {
}
@Override
public long getMove(TurnBasedGame game) {
public long determineMove(TurnBasedGame game) {
lastMove = new CompletableFuture<>();
System.out.println("Sending yourturn");
client.send("SVR GAME YOURTURN {TURNMESSAGE: \"<bericht voor deze beurt>\"}\n");
try {
return lastMove.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
return 0;
}
client.send("SVR GAME YOURTURN {TURNMESSAGE: \"<bericht voor deze beurt>\"}");
return lastMove.join();
}
}

View File

@@ -1,165 +0,0 @@
package org.toop.framework.game.players.ai;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MiniMaxAI extends AbstractAI {
private final int maxDepth;
private final Random random = new Random();
public MiniMaxAI(int depth) {
this.maxDepth = depth;
}
public MiniMaxAI(MiniMaxAI other) {
this.maxDepth = other.maxDepth;
}
@Override
public MiniMaxAI deepCopy() {
return new MiniMaxAI(this);
}
@Override
public long getMove(TurnBasedGame game) {
long legalMoves = game.getLegalMoves();
if (legalMoves == 0) return 0;
List<Long> bestMoves = new ArrayList<>();
int bestScore = Integer.MIN_VALUE;
int aiPlayer = game.getCurrentTurn();
long movesLoop = legalMoves;
while (movesLoop != 0) {
long move = 1L << Long.numberOfTrailingZeros(movesLoop);
TurnBasedGame copy = game.deepCopy();
PlayResult result = copy.play(move);
int score;
switch (result.state()) {
case WIN -> score = (result.player() == aiPlayer ? maxDepth : -maxDepth);
case DRAW -> score = 0;
default -> score = getMoveScore(copy, maxDepth - 1, false, aiPlayer, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
if (score > bestScore) {
bestScore = score;
bestMoves.clear();
bestMoves.add(move);
} else if (score == bestScore) {
bestMoves.add(move);
}
movesLoop &= movesLoop - 1;
}
long chosenMove = bestMoves.get(random.nextInt(bestMoves.size()));
return chosenMove;
}
/**
* Recursive minimax with alpha-beta pruning and heuristic evaluation.
*
* @param game Current game state
* @param depth Remaining depth
* @param maximizing True if AI is maximizing, false if opponent
* @param aiPlayer AI's player index
* @param alpha Alpha value
* @param beta Beta value
* @return score of the position
*/
private int getMoveScore(TurnBasedGame game, int depth, boolean maximizing, int aiPlayer, int alpha, int beta) {
long legalMoves = game.getLegalMoves();
// Terminal state
PlayResult lastResult = null;
if (legalMoves == 0) {
lastResult = new PlayResult(GameState.DRAW, -1);
}
// If the game is over or depth limit reached, evaluate
if (depth <= 0 || legalMoves == 0) {
if (lastResult != null) return 0;
return evaluateBoard(game, aiPlayer);
}
int bestScore = maximizing ? Integer.MIN_VALUE : Integer.MAX_VALUE;
long movesLoop = legalMoves;
while (movesLoop != 0) {
long move = 1L << Long.numberOfTrailingZeros(movesLoop);
TurnBasedGame copy = game.deepCopy();
PlayResult result = copy.play(move);
int score;
switch (result.state()) {
case WIN -> score = (result.player() == aiPlayer ? depth : -depth);
case DRAW -> score = 0;
default -> score = getMoveScore(copy, depth - 1, !maximizing, aiPlayer, alpha, beta);
}
if (maximizing) {
bestScore = Math.max(bestScore, score);
alpha = Math.max(alpha, bestScore);
} else {
bestScore = Math.min(bestScore, score);
beta = Math.min(beta, bestScore);
}
// Alpha-beta pruning
if (beta <= alpha) break;
movesLoop &= movesLoop - 1;
}
return bestScore;
}
/**
* Simple heuristic evaluation for Reversi-like games.
* Positive = good for AI, Negative = good for opponent.
*
* @param game OnlineTurnBasedGame state
* @param aiPlayer AI's player index
* @return heuristic score
*/
private int evaluateBoard(TurnBasedGame game, int aiPlayer) {
long[] board = game.getBoard();
int aiCount = 0;
int opponentCount = 0;
// Count pieces for AI vs opponent
for (int i = 0; i < board.length; i++) {
long bits = board[i];
for (int j = 0; j < 64; j++) {
if ((bits & (1L << j)) != 0) {
// Assume player 0 occupies even indices, player 1 occupies odd
if ((i * 64 + j) % game.getPlayerCount() == aiPlayer) aiCount++;
else opponentCount++;
}
}
}
// Mobility (number of legal moves)
int mobility = Long.bitCount(game.getLegalMoves());
// Corner control (top-left, top-right, bottom-left, bottom-right)
int corners = 0;
long[] cornerMasks = {1L << 0, 1L << 7, 1L << 56, 1L << 63};
for (long mask : cornerMasks) {
for (long b : board) {
if ((b & mask) != 0) corners += 1;
}
}
// Weighted sum
return (aiCount - opponentCount) + 2 * mobility + 5 * corners;
}
}

View File

@@ -11,4 +11,11 @@ public interface TurnBasedGame extends DeepCopyable<TurnBasedGame> {
int getWinner();
long getLegalMoves();
PlayResult play(long move);
PlayResult getState();
boolean isTerminal();
float rateMove(long move);
long heuristicMove(long legalMoves);
void setFrom(long player1, long player2, int turn);
}

View File

@@ -40,12 +40,29 @@ public abstract class AbstractPlayer implements Player {
* @return an integer representing the chosen move
* @throws UnsupportedOperationException if the method is not overridden
*/
public long getMove(TurnBasedGame gameCopy) {
logger.error("Method getMove not implemented.");
throw new UnsupportedOperationException("Not supported yet.");
public final long getMove(TurnBasedGame game) {
return determineMove(game.deepCopy());
}
public final String getName(){
/**
* Determines the player's move using a safe copy of the game.
* <p>
* This method is called by {@link #getMove(T)} and should contain
* the player's strategy for choosing a move.
*
* @param gameCopy a deep copy of the game
* @return the chosen move
*/
protected abstract long determineMove(TurnBasedGame gameCopy);
/**
* Returns the player's name.
*
* @return the name
*/
public String getName() {
return this.name;
}
}

View File

@@ -65,6 +65,9 @@ public class NetworkEvents extends EventsBase {
public record GameResultResponse(long clientId, String condition)
implements GenericEvent {}
public record TournamentResultResponse(long clientId, String gameType, String[] names, String[] scoreTypes, List<Integer[]> scores)
implements GenericEvent {}
/** Indicates that a game move has been processed or received. */
public record GameMoveResponse(long clientId, String player, String move, String details)
implements GenericEvent {}
@@ -219,4 +222,5 @@ public class NetworkEvents extends EventsBase {
/** Response to a {@link ChangeAddress} event, carrying the success result. */
public record ChangeAddressResponse(boolean successful, long identifier)
implements ResponseToUniqueEvent {}
}

View File

@@ -3,6 +3,9 @@ package org.toop.framework.networking.connection.handlers;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -94,6 +97,9 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
case "HELP":
helpHandler(recSrvRemoved);
return;
case "RESULTS":
resultsHandler(recSrvRemoved);
return;
default:
// return
}
@@ -103,6 +109,66 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
}
}
private static String extract(String input, String key) {
Pattern p = Pattern.compile(
key + "\\s*:\\s*(\\[[^]]*]|\"[^\"]*\")",
Pattern.CASE_INSENSITIVE
);
Matcher m = p.matcher(input);
return m.find() ? m.group(1) : null;
}
private void resultsHandler(String rec) {
// TODO all of this
IO.println(rec);
String gameTypeRaw = extract(rec, "GAMETYPE");
String usersRaw = extract(rec, "USERS");
String scoreTypesRaw = extract(rec, "SCORETYPES");
String scoresRaw = extract(rec, "SCORES");
if (usersRaw == null) return;
String[] users;
if (usersRaw.length() > 2) {
users = Arrays.stream(usersRaw.substring(1, usersRaw.length() - 1).split(","))
.map(s -> s.trim().replace("\"", ""))
.toArray(String[]::new);
} else {
users = new String[]{};
}
String[] scoreTypes;
if (scoreTypesRaw.length() > 2) {
scoreTypes = Arrays.stream(scoreTypesRaw.substring(1, usersRaw.length() - 1).split(","))
.map(s -> s.trim().replace("\"", ""))
.toArray(String[]::new);
} else {
scoreTypes = new String[]{};
}
if (scoresRaw == null) return;
if (scoresRaw.length() > 2) {
List<Integer[]> scores = Arrays.stream(
scoresRaw.substring(1, scoresRaw.length() - 1) // remove outer []
.split("\\],\\[")
)
.map(part -> part.replace("[", "").replace("]", ""))
.map(part -> Arrays.stream(part.split(","))
.map(String::trim)
.map(Integer::parseInt)
.toArray(Integer[]::new)
)
.toList();
eventBus.post(new NetworkEvents.TournamentResultResponse(this.connectionId, gameTypeRaw, users, scoreTypes, scores));
} else {
eventBus.post(new NetworkEvents.TournamentResultResponse(this.connectionId, gameTypeRaw, users, scoreTypes, new ArrayList<>()));
}
}
private void gameMoveHandler(String rec) {
String[] msg =
Pattern.compile(

View File

@@ -0,0 +1,7 @@
package org.toop.framework.networking.server;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import java.util.concurrent.CompletableFuture;
public record GameResultFuture(OnlineGame<TurnBasedGame> game, CompletableFuture<Integer> result) {}

View File

@@ -1,9 +1,13 @@
package org.toop.framework.networking.server;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface GameServer<GAMETYPE, CLIENT, CHALLENGEIDTYPE> {
void startGame(String gameType, CLIENT... clients);
GameResultFuture startGame(String gameType, Duration turnTime, CLIENT... clients);
void addClient(CLIENT client);
void removeClient(CLIENT client);

View File

@@ -0,0 +1,10 @@
package org.toop.framework.networking.server;
import org.toop.framework.networking.server.client.NettyClient;
import java.time.Duration;
@FunctionalInterface
public interface MatchExecutor {
GameResultFuture submit(String gameType, Duration turnTime, NettyClient... clients);
}

View File

@@ -5,44 +5,65 @@ import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.networking.server.client.NettyClient;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
public class OnlineTurnBasedGame implements OnlineGame<TurnBasedGame> {
private long id;
private NettyClient[] clients;
private NettyClient[] admins;
private TurnBasedGame game;
private ServerThreadBehaviour gameThread;
public OnlineTurnBasedGame(TurnBasedGame game, NettyClient... clients) {
private final CompletableFuture<Integer> resultFuture;
public OnlineTurnBasedGame(NettyClient[] admins, TurnBasedGame game, CompletableFuture<Integer> resultFuture, Duration timeOut, NettyClient... clients) {
this.game = game;
this.gameThread = new ServerThreadBehaviour(
game,
(pair) -> notifyMoveMade(pair.getLeft(), pair.getRight()),
(pair) -> notifyGameEnd(pair.getLeft(), pair.getRight())
(pair) -> notifyGameEnd(pair.getLeft(), pair.getRight()),
timeOut
);
this.resultFuture = resultFuture;
this.clients = clients;
this.admins = admins;
}
private void notifyMoveMade(String speler, int move){
for (NettyClient admin : admins) {
admin.send(String.format("SVR GAME MOVE {PLAYER: \"%s\", MOVE: \"%s\", DETAILS: \"<reactie spel op zet>\"}", speler, move));
}
for (NettyClient client : clients) {
client.send(String.format("SVR GAME MOVE {PLAYER: \"%s\", MOVE: \"%s\", DETAILS: \"<reactie spel op zet>\"}\n", speler, move));
client.send(String.format("SVR GAME MOVE {PLAYER: \"%s\", MOVE: \"%s\", DETAILS: \"<reactie spel op zet>\"}", speler, move));
}
}
private void notifyGameEnd(GameState state, int winner){
if (state == GameState.DRAW){
private void notifyGameEnd(GameState state, int winner) {
if (state == GameState.DRAW) {
Arrays.stream(admins).forEach(a -> a.send("SVR GAME END"));
for (NettyClient client : clients) {
client.send(String.format("SVR GAME DRAW {PLAYERONESCORE: \"<score speler1>\", PLAYERTWOSCORE: \"<score speler2>\", COMMENT: \"<comment>\"}\n"));
client.send("SVR GAME DRAW {PLAYERONESCORE: \"<score speler1>\", PLAYERTWOSCORE: \"<score speler2>\", COMMENT: \"<comment>\"}");
}
}
else{
clients[winner].send(String.format("SVR GAME WIN {PLAYERONESCORE: \"<score speler1>\", PLAYERTWOSCORE: \"<score speler2>\", COMMENT: \"<comment>\"}\n"));
clients[(winner + 1)%2].send(String.format("SVR GAME LOSS {PLAYERONESCORE: \"<score speler1>\", PLAYERTWOSCORE: \"<score speler2>\", COMMENT: \"<comment>\"}\n"));
} else {
Arrays.stream(admins).forEach(a -> a.send("SVR GAME END"));
clients[winner].send("SVR GAME WIN {PLAYERONESCORE: \"<score speler1>\", PLAYERTWOSCORE: \"<score speler2>\", COMMENT: \"<comment>\"}");
clients[(winner+1)%2].send("SVR GAME LOSS {PLAYERONESCORE: \"<score speler1>\", PLAYERTWOSCORE: \"<score speler2>\", COMMENT: \"<comment>\"}");
}
// Remove game fromt clients
for(NettyClient client : clients) {
// Remove game from clients
for (NettyClient client : clients) {
admins = null;
client.clearGame();
}
if (resultFuture != null) {
if (state.equals(GameState.DRAW)) resultFuture.complete(-1); // Return -1 if draw
else resultFuture.complete(winner); // Return number for winner's index
}
}
@Override
@@ -61,7 +82,7 @@ public class OnlineTurnBasedGame implements OnlineGame<TurnBasedGame> {
}
@Override
public void start(){
public void start() {
this.gameThread.start();
}
}

View File

@@ -1,5 +1,6 @@
package org.toop.framework.networking.server;
import com.google.gson.Gson;
import org.toop.framework.game.players.ServerPlayer;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.networking.server.challenges.gamechallenge.GameChallenge;
@@ -9,6 +10,9 @@ import org.toop.framework.networking.server.stores.ClientStore;
import org.toop.framework.networking.server.stores.SubscriptionStore;
import org.toop.framework.networking.server.stores.TurnBasedGameStore;
import org.toop.framework.networking.server.stores.TurnBasedGameTypeStore;
import org.toop.framework.networking.server.tournaments.*;
import org.toop.framework.networking.server.tournaments.matchmakers.DoubleRoundRobinMatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.*;
import org.toop.framework.utils.ImmutablePair;
import java.util.*;
@@ -27,13 +31,14 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
final private Duration challengeDuration;
final private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final List<NettyClient> admins = new ArrayList<>();
public Server(
Duration challengeDuration,
TurnBasedGameTypeStore turnBasedGameTypeStore,
ClientStore<Long, NettyClient> clientStore,
TurnBasedGameStore gameStore,
SubscriptionStore subStore
) {
this.gameTypesStore = turnBasedGameTypeStore;
this.challengeDuration = challengeDuration;
@@ -46,11 +51,13 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
@Override
public void addClient(NettyClient client) {
if (admins.isEmpty()) admins.addLast(client);
clientStore.add(client);
}
@Override
public void removeClient(NettyClient client) {
admins.remove(client);
clientStore.remove(client.id());
}
@@ -103,7 +110,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
public void acceptChallenge(Long challengeId) {
for (var challenge : gameChallenges) {
if (challenge.id() == challengeId) {
startGame(challenge.acceptChallenge(), challenge.getUsers());
startGame(challenge.acceptChallenge(), Duration.ofSeconds(10), challenge.getUsers());
break;
}
}
@@ -125,18 +132,29 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
}
@Override
public void startGame(String gameType, NettyClient... clients) {
if (!gameTypesStore.all().containsKey(gameType)) return;
public GameResultFuture startGame(String gameType, Duration turnTime, NettyClient... clients) {
if (!gameTypesStore.all().containsKey(gameType)) return null;
try {
ServerPlayer[] players = new ServerPlayer[clients.length];
var game = new OnlineTurnBasedGame(gameTypesStore.create(gameType), clients);
var gameResult = new CompletableFuture<Integer>();
var game = new OnlineTurnBasedGame(
getAdmins().toArray(NettyClient[]::new),
gameTypesStore.create(gameType),
gameResult,
turnTime,
clients
);
var grfReturn = new GameResultFuture(game, gameResult);
for (int i = 0; i < clients.length; i++) {
players[i] = new ServerPlayer(clients[i]);
clients[i].setGame(new ImmutablePair<>(game, players[i]));
}
System.out.println("Starting OnlineTurnBasedGame");
game.game().init(players);
gameStore.add(game);
@@ -149,11 +167,14 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
clients[0].name(),
gameType,
clients[0].name()));
game.start();
return grfReturn;
} catch (Exception e) {
IO.println("ERROR: Failed to start OnlineTurnBasedGame");
e.printStackTrace();
}
return null;
}
@Override
@@ -161,6 +182,10 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
return clientStore.all().stream().toList();
}
public List<NettyClient> getAdmins() {
return new ArrayList<>(admins); // Clone so the list can't be edited.
}
@Override
public void shutdown() {
scheduler.shutdown();
@@ -219,7 +244,6 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
}
if (userInGame) { continue; }
//
int first = Math.max(left, right);
int second = Math.min(left, right);
@@ -227,7 +251,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
userNames.remove(first);
userNames.remove(second);
startGame(key, getUser(userLeft), getUser(userRight));
startGame(key, Duration.ofSeconds(10), getUser(userLeft), getUser(userRight));
}
}
}
@@ -261,4 +285,74 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
return true;
}
public void startTournament(String gameType, NettyClient requestor, boolean shuffle) {
if (!admins.contains(requestor)) {
requestor.send("ERR you do not have the privileges to start a tournament");
return;
}
var tournamentUsers = new ArrayList<>(onlineUsers());
tournamentUsers.removeIf(admins::contains);
Tournament tournament = new Tournament.Builder()
.matchExecutor(this::startGame)
.tournamentRunner(new AsyncTournamentRunner())
.matchMaker(new DoubleRoundRobinMatchMaker())
.addScoreSystem(new MatchCountScoreSystem())
.addScoreSystem(new WinCountScoreSystem())
.addScoreSystem(new DrawCountScoreSystem())
.addScoreSystem(new LoseCountScoreSystem())
.resultBroadcaster(this::endTournament)
.turnTimeout(Duration.ofSeconds(10))
.addPlayers(tournamentUsers.toArray(NettyClient[]::new))
.addAdmins(admins.toArray(NettyClient[]::new))
.build();
new Thread(() -> tournament.run(gameType)).start();
}
public void endTournament(List<IntegerScoreSystem> systems) {
if (systems.isEmpty()) return;
Map<String, List<ImmutablePair<String, Integer>>> combined = new HashMap<>();
for (var system : systems) {
for (var player : system.getScore().keySet()) {
combined.putIfAbsent(player.name(), new ArrayList<>());
combined.get(player.name()).addLast(new ImmutablePair<>(system.scoreName(), system.getScore().get(player)));
}
}
List<String> names = new ArrayList<>();
List<String> systemNames = new ArrayList<>();
List<List<Integer>> scores = new ArrayList<>();
for (var player : combined.entrySet()) {
names.addLast(player.getKey());
scores.addLast(new ArrayList<>());
for (var system : player.getValue()) {
if (!systemNames.contains(system.getLeft())) systemNames.addLast(system.getLeft());
scores.getLast().addLast(system.getRight());
}
}
Gson gson = new Gson();
String namesJson = gson.toJson(names);
String systemNamesJson = gson.toJson(systemNames);
String scoresJson = gson.toJson(scores);
String msg = String.format(
"SVR RESULTS {GAMETYPE: \"%s\", USERS: %s, SCORETYPES: %s, SCORES: %s, TOURNAMENT: 1}",
"none", // TODO gametype
namesJson,
systemNamesJson,
scoresJson
);
for (var user : onlineUsers()) {
user.send(msg);
}
}
}

View File

@@ -56,7 +56,6 @@ public class NettyClient implements Client<OnlineTurnBasedGame, ServerPlayer> {
@Override
public void send(String message) {
IO.println(message);
ctx.channel().writeAndFlush(message + "\r\n");
}

View File

@@ -11,8 +11,6 @@ import org.toop.framework.networking.server.Server;
import org.toop.framework.networking.server.client.Client;
import org.toop.framework.networking.server.parsing.Parser;
import java.util.Arrays;
public class NettyClientSession extends SimpleChannelInboundHandler<String> implements ClientSession<OnlineTurnBasedGame, ServerPlayer> {
private final NettyClient client;
@@ -41,13 +39,9 @@ public class NettyClientSession extends SimpleChannelInboundHandler<String> impl
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
IO.println(msg);
ParsedMessage p = Parser.parse(msg);
if (p == null) return;
IO.println(p.command() + " " + Arrays.toString(p.args()));
handler.handle(p);
}

View File

@@ -1,18 +1,17 @@
package org.toop.framework.networking.server.handlers;
import org.toop.framework.game.players.ServerPlayer;
import org.toop.framework.networking.server.OnlineTurnBasedGame;
import org.toop.framework.networking.server.Server;
import org.toop.framework.networking.server.client.Client;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.parsing.ParsedMessage;
import org.toop.framework.utils.Utils;
public class MessageHandler implements Handler<ParsedMessage> {
private final Server server;
private final Client<OnlineTurnBasedGame, ServerPlayer> client;
private final NettyClient client;
public MessageHandler(Server server, Client<OnlineTurnBasedGame, ServerPlayer> client) {
public MessageHandler(Server server, NettyClient client) {
this.server = server;
this.client = client;
}
@@ -28,6 +27,7 @@ public class MessageHandler implements Handler<ParsedMessage> {
case "challenge" -> handleChallenge(message, client);
case "message" -> handleMessage(message, client);
case "help" -> handleHelp(message, client);
case "tournament" -> handleTournament(message, client);
default -> client.send("ERROR Unknown command");
}
}
@@ -41,27 +41,27 @@ public class MessageHandler implements Handler<ParsedMessage> {
return true;
}
private void handleLogin(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleLogin(ParsedMessage p, NettyClient client) {
if (!hasArgs(p.args())) return;
client.setName(p.args()[0]);
}
private void handleSubscribe(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleSubscribe(ParsedMessage p, NettyClient client) {
if (!hasArgs(p.args())) return;
server.subscribeClient(client.name(), p.args()[0]);
}
private void handleHelp(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleHelp(ParsedMessage p, NettyClient client) {
// TODO
}
private void handleMessage(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleMessage(ParsedMessage p, NettyClient client) {
// TODO
}
private void handleGet(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleGet(ParsedMessage p, NettyClient client) {
if (!hasArgs(p.args())) return;
switch (p.args()[0]) {
@@ -73,10 +73,14 @@ public class MessageHandler implements Handler<ParsedMessage> {
var names = server.gameTypes().stream().iterator();
client.send("SVR GAMELIST " + Utils.returnQuotedString(names));
}
case "admins" -> {
var names = server.getAdmins().stream().map(Client::name).iterator();
client.send("SVR ADMINS " + Utils.returnQuotedString(names));
}
}
}
private void handleChallenge(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleChallenge(ParsedMessage p, NettyClient client) {
if (!hasArgs(p.args())) return;
if (p.args().length < 2) return;
@@ -101,10 +105,21 @@ public class MessageHandler implements Handler<ParsedMessage> {
server.challengeClient(client.name(), p.args()[0], p.args()[1]);
}
private void handleMove(ParsedMessage p, Client<OnlineTurnBasedGame, ServerPlayer> client) {
private void handleMove(ParsedMessage p, NettyClient client) {
if(!hasArgs(p.args())) return;
var player = client.player();
if (player == null) return;
// TODO check if not number
client.player().setMove(1L << Integer.parseInt(p.args()[0]));
player.setMove(1L << Integer.parseInt(p.args()[0]));
}
private void handleTournament(ParsedMessage p, NettyClient client) {
if(!hasArgs(p.args())) return;
if (p.args()[0].equalsIgnoreCase("start") && p.args().length > 1) {
server.startTournament(p.args()[1], client, false); // TODO add shuffle to msg
}
}
}

View File

@@ -0,0 +1,86 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.GameResultFuture;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
public class AsyncTournamentRunner implements TournamentRunner {
@Override
public void run(
MatchExecutor matchRunner,
MatchMaker matchMaker,
List<IntegerScoreSystem> scoreSystems,
ResultBroadcaster<IntegerScoreSystem> broadcaster,
Duration turnTime,
String gameType
) {
ExecutorService matchExecutor = Executors.newVirtualThreadPerTaskExecutor();
ExecutorService scoringExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Queue<TournamentMatch> pendingMatches = new ConcurrentLinkedQueue<>();
matchMaker.forEach(pendingMatches::add);
Set<NettyClient> busyPlayers = ConcurrentHashMap.newKeySet();
List<CompletableFuture<Void>> runningMatches = new CopyOnWriteArrayList<>();
try {
while (!pendingMatches.isEmpty() || !runningMatches.isEmpty()) {
Iterator<TournamentMatch> it = pendingMatches.iterator();
while (it.hasNext()) {
TournamentMatch match = it.next();
NettyClient a = match.getClient0();
NettyClient b = match.getClient1();
// TODO game != null doesn't work here, fix later
if (busyPlayers.contains(a) || busyPlayers.contains(b)) {
continue;
}
busyPlayers.add(a);
busyPlayers.add(b);
it.remove();
CompletableFuture<Void> f =
CompletableFuture.runAsync(() -> {
try {
GameResultFuture game = matchRunner.submit(gameType, turnTime, a, b);
CompletableFuture.runAsync(
() -> scoreSystems.forEach(s -> s.result(match, game.result().join())),
scoringExecutor
).join();
} finally {
a.clearGame();
b.clearGame();
busyPlayers.remove(a);
busyPlayers.remove(b);
}
}, matchExecutor);
runningMatches.add(f);
f.whenComplete((_, _) -> runningMatches.remove(f));
}
Thread.sleep(10); // Safety
}
broadcaster.broadcast(scoreSystems);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
matchExecutor.shutdown();
}
}
}

View File

@@ -0,0 +1,40 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.GameResultFuture;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.*;
public class BasicTournamentRunner implements TournamentRunner {
@Override
public void run(
MatchExecutor matchExecutor,
MatchMaker matchMaker,
List<IntegerScoreSystem> scoreSystems,
ResultBroadcaster<IntegerScoreSystem> broadcaster,
Duration turnTime,
String gameType
) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
threadPool.execute(() -> {
for (TournamentMatch match : matchMaker) {
// Play game and await the results
GameResultFuture game = matchExecutor.submit(gameType, turnTime, match.getClient0(), match.getClient1());
scoreSystems.forEach(e -> e.result(match, game.result().join()));
match.getClient0().clearGame();
match.getClient1().clearGame();
}
broadcaster.broadcast(scoreSystems);
});
} finally {
threadPool.shutdown();
}
}
}

View File

@@ -0,0 +1,10 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.tournaments.scoresystems.ScoreSystem;
import java.util.List;
@FunctionalInterface
public interface ResultBroadcaster<T extends ScoreSystem<?, ?, ?>> {
void broadcast(List<T> scoreSystem);
}

View File

@@ -0,0 +1,121 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class Tournament {
private final MatchExecutor matchExecutor;
private final List<IntegerScoreSystem> scoreSystems;
private final TournamentRunner tournamentRunner;
private final MatchMaker matchMaker;
private final ResultBroadcaster<IntegerScoreSystem> broadcaster;
private final NettyClient[] players;
private final Duration turnTime;
private final Shuffler shuffler;
private Tournament(Tournament.Builder builder) {
matchExecutor = builder.matchExecutor;
scoreSystems = builder.scoreSystems;
tournamentRunner = builder.tournamentRunner;
matchMaker = builder.matchMaker;
broadcaster = builder.broadcaster;
players = builder.players;
turnTime = builder.turnTime;
shuffler = builder.shuffler;
}
public void run(String gameType) {
Arrays.stream(players).forEach(e -> {
matchMaker.addPlayer(e);
scoreSystems.forEach(k -> k.addPlayer(e));
});
if (shuffler != null) matchMaker.shuffle(shuffler);
tournamentRunner.run(matchExecutor, matchMaker, scoreSystems, broadcaster, turnTime, gameType);
}
public static class Builder {
private MatchExecutor matchExecutor;
private List<IntegerScoreSystem> scoreSystems = new ArrayList<>();
private TournamentRunner tournamentRunner;
private MatchMaker matchMaker;
private ResultBroadcaster<IntegerScoreSystem> broadcaster;
private NettyClient[] players;
private NettyClient[] observors;
private NettyClient[] admins;
private Duration turnTime = Duration.ofSeconds(10);
private Shuffler shuffler;
public Builder matchExecutor(MatchExecutor matchExecutor) {
this.matchExecutor = matchExecutor;
return this;
}
public Builder addScoreSystem(IntegerScoreSystem scoreSystem) {
this.scoreSystems.addLast(scoreSystem);
return this;
}
public Builder tournamentRunner(TournamentRunner tournamentRunner) {
this.tournamentRunner = tournamentRunner;
return this;
}
public Builder matchMaker(MatchMaker matchMaker) {
this.matchMaker = matchMaker;
return this;
}
public Builder resultBroadcaster(ResultBroadcaster<IntegerScoreSystem> broadcaster) {
this.broadcaster = broadcaster;
return this;
}
public Builder addPlayers(NettyClient[] players) {
this.players = players;
return this;
}
public Builder addObservers(NettyClient[] observors) { // TODO
this.observors = observors;
return this;
}
public Builder addAdmins(NettyClient[] admins) { // TODO
this.admins = admins;
return this;
}
public Builder turnTimeout(Duration turnTime) {
this.turnTime = turnTime;
return this;
}
public Builder addMatchShuffler(Shuffler shuffler) {
this.shuffler = shuffler;
return this;
}
public Tournament build() {
Objects.requireNonNull(matchExecutor, "matchExecutor");
Objects.requireNonNull(tournamentRunner, "tournamentRunner");
Objects.requireNonNull(matchMaker, "matchMaker");
Objects.requireNonNull(broadcaster, "resultBroadcaster"); // TODO is not always necessary and needs to be more generic, not just at the end
Objects.requireNonNull(players, "players");
return new Tournament(this);
}
}
}

View File

@@ -0,0 +1,18 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.utils.ImmutablePair;
public class TournamentMatch extends ImmutablePair<NettyClient, NettyClient> {
public TournamentMatch(NettyClient a, NettyClient b) {
super(a, b);
}
public NettyClient getClient0() {
return getLeft();
}
public NettyClient getClient1() {
return getRight();
}
}

View File

@@ -0,0 +1,13 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
import java.util.List;
public interface TournamentRunner {
void run(MatchExecutor matchExecutor, MatchMaker matchMaker, List<IntegerScoreSystem> scoreSystems,
ResultBroadcaster<IntegerScoreSystem> broadcaster, Duration turnTime, String gameType);
}

View File

@@ -0,0 +1,81 @@
package org.toop.framework.networking.server.tournaments.matchmakers;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class DoubleRoundRobinMatchMaker implements MatchMaker {
private final List<NettyClient> players = new ArrayList<>();
public DoubleRoundRobinMatchMaker() {} // TODO let user decide store type
@Override
public void addPlayer(NettyClient player) {
players.addLast(player);
}
@Override
public void shuffle(Shuffler shuffler) {
if (players.size() < 2) return;
shuffler.shuffle(players);
}
@Override
public List<NettyClient> getPlayers() {
return players;
}
@Override
public Iterator<TournamentMatch> iterator() {
return new Iterator<>() {
private int i = 0;
private int j = 1;
private boolean reverse = false;
@Override
public boolean hasNext() {
return players.size() > 1
&& i < players.size() - 1
&& j < players.size();
}
@Override
public TournamentMatch next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
NettyClient home = players.get(i);
NettyClient away = players.get(j);
TournamentMatch match = reverse ? new TournamentMatch(away, home) : new TournamentMatch(home, away);
advance();
return match;
}
private void advance() {
j++;
if (j >= players.size()) {
i++;
j = i + 1;
if (i >= players.size() - 1) {
if (!reverse) {
reverse = true;
i = 0;
j = 1;
}
}
}
}
};
}
}

View File

@@ -0,0 +1,13 @@
package org.toop.framework.networking.server.tournaments.matchmakers;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.util.List;
public interface MatchMaker extends Iterable<TournamentMatch> {
void addPlayer(NettyClient player);
List<NettyClient> getPlayers();
void shuffle(Shuffler shuffler);
}

View File

@@ -0,0 +1,67 @@
package org.toop.framework.networking.server.tournaments.matchmakers;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class RoundRobinMatchMaker implements MatchMaker {
private final List<NettyClient> players = new ArrayList<>();
public RoundRobinMatchMaker() {} // TODO let user decide store type
@Override
public void addPlayer(NettyClient player) {
players.addLast(player);
}
@Override
public void shuffle(Shuffler shuffler) {
if (players.size() < 2) return;
shuffler.shuffle(players);
}
@Override
public List<NettyClient> getPlayers() {
return players;
}
@Override
public Iterator<TournamentMatch> iterator() {
return new Iterator<>() {
private int i = 0;
private int j = 1;
@Override
public boolean hasNext() {
return players.size() > 1
&& i < players.size() - 1
&& j < players.size();
}
@Override
public TournamentMatch next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
NettyClient home = players.get(i);
NettyClient away = players.get(j);
j++;
if (j >= players.size()) {
i++;
j = i + 1;
}
return new TournamentMatch(home, away);
}
};
}
}

View File

@@ -0,0 +1,43 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DrawCountScoreSystem implements IntegerScoreSystem {
private final Map<NettyClient, Integer> scores = new ConcurrentHashMap<>();
private final int INIT_SCORE = 0;
private final int WIN_POINTS = 1;
public DrawCountScoreSystem() {} // TODO let user decide store type
@Override
public String scoreName() {
return "draws";
}
@Override
public void addPlayer(NettyClient user) {
scores.putIfAbsent(user, INIT_SCORE);
}
@Override
public void result(TournamentMatch match, Integer result) {
switch (result) {
case 0, 1 -> {}
case -1 -> {
scores.merge(match.getClient0(), WIN_POINTS, Integer::sum);
scores.merge(match.getClient1(), WIN_POINTS, Integer::sum);
}
default -> throw new IllegalArgumentException("Unknown result: " + result);
}
}
@Override
public Map<NettyClient, Integer> getScore() {
return scores;
}
}

View File

@@ -0,0 +1,6 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
public interface IntegerScoreSystem extends ScoreSystem<TournamentMatch, Integer, NettyClient> {}

View File

@@ -0,0 +1,41 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class LoseCountScoreSystem implements IntegerScoreSystem {
private final Map<NettyClient, Integer> scores = new ConcurrentHashMap<>();
private final int INIT_SCORE = 0;
private final int WIN_POINTS = 1;
public LoseCountScoreSystem() {} // TODO let user decide store type
@Override
public String scoreName() {
return "loses";
}
@Override
public void addPlayer(NettyClient user) {
scores.putIfAbsent(user, INIT_SCORE);
}
@Override
public void result(TournamentMatch match, Integer result) {
switch (result) {
case 0 -> scores.merge(match.getClient1(), WIN_POINTS, Integer::sum);
case 1 -> scores.merge(match.getClient0(), WIN_POINTS, Integer::sum);
case -1 -> {} // Draw
default -> throw new IllegalArgumentException("Unknown result: " + result);
}
}
@Override
public Map<NettyClient, Integer> getScore() {
return scores;
}
}

View File

@@ -0,0 +1,37 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MatchCountScoreSystem implements IntegerScoreSystem {
private final Map<NettyClient, Integer> scores = new ConcurrentHashMap<>();
private final int INIT_SCORE = 0;
private final int WIN_POINTS = 1;
public MatchCountScoreSystem() {} // TODO let user decide store type
@Override
public String scoreName() {
return "matches";
}
@Override
public void addPlayer(NettyClient user) {
scores.putIfAbsent(user, INIT_SCORE);
}
@Override
public void result(TournamentMatch match, Integer result) {
scores.merge(match.getClient0(), WIN_POINTS, Integer::sum);
scores.merge(match.getClient1(), WIN_POINTS, Integer::sum);
}
@Override
public Map<NettyClient, Integer> getScore() {
return scores;
}
}

View File

@@ -0,0 +1,10 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import java.util.Map;
public interface ScoreSystem<MATCHTYPE, SCORETYPE, USERTYPE> {
String scoreName();
void addPlayer(USERTYPE user);
void result(MATCHTYPE match, SCORETYPE result);
Map<USERTYPE, SCORETYPE> getScore();
}

View File

@@ -0,0 +1,41 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class WinCountScoreSystem implements IntegerScoreSystem {
private final Map<NettyClient, Integer> scores = new ConcurrentHashMap<>();
private final int INIT_SCORE = 0;
private final int WIN_POINTS = 1;
public WinCountScoreSystem() {} // TODO let user decide store type
@Override
public String scoreName() {
return "wins";
}
@Override
public void addPlayer(NettyClient user) {
scores.putIfAbsent(user, INIT_SCORE);
}
@Override
public void result(TournamentMatch match, Integer result) {
switch (result) {
case 0 -> scores.merge(match.getClient0(), WIN_POINTS, Integer::sum);
case 1 -> scores.merge(match.getClient1(), WIN_POINTS, Integer::sum);
case -1 -> {} // Draw
default -> throw new IllegalArgumentException("Unknown result: " + result);
}
}
@Override
public Map<NettyClient, Integer> getScore() {
return scores;
}
}

View File

@@ -0,0 +1,20 @@
package org.toop.framework.networking.server.tournaments.shufflers;
import java.util.List;
import java.util.Random;
public class RandomShuffle implements Shuffler {
@Override
public <T> void shuffle(List<T> listToShuffle) {
final int SHUFFLE_AMOUNT = listToShuffle.size() * 2;
Random rand = new Random();
for (int i = 0; i <= SHUFFLE_AMOUNT; i++) {
int index = rand.nextInt(listToShuffle.size());
T match = listToShuffle.get(index);
listToShuffle.remove(index);
listToShuffle.addLast(match);
}
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.networking.server.tournaments.shufflers;
import java.util.List;
public interface Shuffler {
<T> void shuffle(List<T> listToShuffle);
}

View File

@@ -0,0 +1,298 @@
package org.toop.game.players.ai;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class MCTSAI extends AbstractAI {
protected static class Node {
public static final int VIRTUAL_LOSS = -1;
public TurnBasedGame state;
public long move;
public long unexpandedMoves;
public Node parent;
public Node[] children;
public AtomicInteger value;
public AtomicInteger visits;
public float heuristic;
public float solved;
public Node(TurnBasedGame state, Node parent, long move) {
final long legalMoves = state.getLegalMoves();
this.state = state;
this.move = move;
this.unexpandedMoves = legalMoves;
this.parent = parent;
this.children = new Node[Long.bitCount(legalMoves)];
this.value = new AtomicInteger(0);
this.visits = new AtomicInteger(0);
this.heuristic = state.rateMove(move);
this.solved = Float.NaN;
}
public Node(TurnBasedGame state) {
this(state, null, 0L);
}
public int getExpanded() {
return children.length - Long.bitCount(unexpandedMoves);
}
public boolean isFullyExpanded() {
return unexpandedMoves == 0L;
}
public float calculateUCT(float explorationFactor) {
if (visits.get() == 0) {
return Float.POSITIVE_INFINITY;
}
final float exploitation = (float) value.get() / visits.get();
final float exploration = (float)(Math.sqrt(explorationFactor / visits.get()));
final float bias = heuristic * 10.0f / (visits.get() + 1);
return exploitation + exploration + bias;
}
public Node bestUCTChild() {
final int expanded = getExpanded();
Node highestUCTChild = null;
float highestUCT = Float.NEGATIVE_INFINITY;
for (int i = 0; i < expanded; i++) {
final float childUCT = children[i].calculateUCT(2.0f * (float)Math.log(visits.get()));
if (childUCT > highestUCT) {
highestUCTChild = children[i];
highestUCT = childUCT;
}
}
return highestUCTChild;
}
}
protected static final ThreadLocal<Random> random = ThreadLocal.withInitial(Random::new);
protected final int milliseconds;
protected int lastIterations;
public MCTSAI(int milliseconds) {
this.milliseconds = milliseconds;
this.lastIterations = 0;
}
public MCTSAI(MCTSAI other) {
this.milliseconds = other.milliseconds;
this.lastIterations = other.lastIterations;
}
public int getLastIterations() {
return lastIterations;
}
protected Node selection(Node root) {
while (Float.isNaN(root.solved) && root.isFullyExpanded() && !root.state.isTerminal()) {
root.value.addAndGet(Node.VIRTUAL_LOSS);
root.visits.incrementAndGet();
root = root.bestUCTChild();
}
root.value.addAndGet(Node.VIRTUAL_LOSS);
root.visits.incrementAndGet();
return root;
}
protected Node expansion(Node leaf) {
synchronized (leaf) {
if (leaf.unexpandedMoves == 0L) {
return leaf;
}
final long unexpandedMove = leaf.unexpandedMoves & -leaf.unexpandedMoves;
final TurnBasedGame copiedState = leaf.state.deepCopy();
copiedState.play(unexpandedMove);
final Node expandedChild = new Node(copiedState, leaf, unexpandedMove);
leaf.children[leaf.getExpanded()] = expandedChild;
leaf.unexpandedMoves &= ~unexpandedMove;
return expandedChild;
}
}
protected int simulation(Node leaf) {
final TurnBasedGame copiedState = leaf.state.deepCopy();
final int playerIndex = 1 - copiedState.getCurrentTurn();
while (!copiedState.isTerminal()) {
final long legalMoves = copiedState.getLegalMoves();
final long randomMove = randomSetBit(legalMoves);
copiedState.play(randomMove);
}
if (copiedState.getWinner() == playerIndex) {
return 1;
}
if (copiedState.getWinner() >= 0) {
return -1;
}
return 0;
}
protected void backPropagation(Node leaf, int value) {
while (leaf != null) {
value -= Node.VIRTUAL_LOSS;
leaf.value.addAndGet(value);
if (Float.isNaN(leaf.solved)) {
updateSolvedStatus(leaf);
}
value = -value;
leaf = leaf.parent;
}
}
protected Node mostVisitedChild(Node root) {
final int expanded = root.getExpanded();
Node mostVisitedChild = null;
int mostVisited = -1;
for (int i = 0; i < expanded; i++) {
if (root.children[i].visits.get() > mostVisited) {
mostVisitedChild = root.children[i];
mostVisited = root.children[i].visits.get();
}
}
return mostVisitedChild;
}
protected Node findOrResetRoot(Node root, TurnBasedGame game) {
if (root == null) {
return new Node(game.deepCopy());
}
if (areStatesEqual(root.state.getBoard(), game.getBoard())) {
return root;
}
final int expanded = root.getExpanded();
for (int i = 0; i < expanded; i++) {
if (areStatesEqual(root.children[i].state.getBoard(), game.getBoard())) {
root.children[i].parent = null;
return root.children[i];
}
}
return new Node(game.deepCopy());
}
protected Node findChildByMove(Node root, long move) {
final int expanded = root.getExpanded();
for (int i = 0; i < expanded; i++) {
if (root.children[i].move == move) {
root.children[i].parent = null;
return root.children[i];
}
}
return null;
}
protected boolean areStatesEqual(long[] state1, long[] state2) {
if (state1.length != state2.length) {
return false;
}
for (int i = 0; i < state1.length; i++) {
if (state1[i] != state2[i]) {
return false;
}
}
return true;
}
protected long randomSetBit(long value) {
if (0L == value) {
return 0;
}
final int bitCount = Long.bitCount(value);
final int randomBitCount = random.get().nextInt(bitCount);
for (int i = 0; i < randomBitCount; i++) {
value &= value - 1;
}
return value & -value;
}
private void updateSolvedStatus(Node node) {
if (node.state.isTerminal()) {
final int winner = node.state.getWinner();
final int mover = 1 - node.state.getCurrentTurn();
node.solved = winner == mover? 1.0f : winner == -1? 0.0f : -1.0f;
return;
}
if (node.isFullyExpanded()) {
boolean allChildrenSolved = true;
boolean foundWinningMove = false;
boolean foundDrawMove = false;
for (final Node child : node.children) {
if (!Float.isNaN(child.solved)) {
if (child.solved == -1.0f) {
foundWinningMove = true;
break;
}
if (child.solved == 0.0f) {
foundDrawMove = true;
}
} else {
allChildrenSolved = false;
}
}
if (foundWinningMove) {
node.solved = 1.0f;
} else if (allChildrenSolved) {
node.solved = foundDrawMove? 0.0f : -1.0f;
}
}
}
}

View File

@@ -0,0 +1,195 @@
package org.toop.game.players.ai;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.Random;
public class MCTSAI2 extends AbstractAI {
private static class Node {
public TurnBasedGame state;
public long move;
public long unexpandedMoves;
public Node parent;
public Node[] children;
public int expanded;
public float value;
public int visits;
public Node(TurnBasedGame state, Node parent, long move) {
final long legalMoves = state.getLegalMoves();
this.state = state;
this.move = move;
this.unexpandedMoves = legalMoves;
this.parent = parent;
this.children = new Node[Long.bitCount(legalMoves)];
this.expanded = 0;
this.value = 0.0f;
this.visits = 0;
}
public Node(TurnBasedGame state) {
this(state, null, 0L);
}
public boolean isFullyExpanded() {
return expanded == children.length;
}
public float calculateUCT(int parentVisits) {
final float exploitation = value / visits;
final float exploration = 1.41f * (float)(Math.sqrt(Math.log(parentVisits) / visits));
return exploitation + exploration;
}
public Node bestUCTChild() {
Node highestUCTChild = null;
float highestUCT = Float.NEGATIVE_INFINITY;
for (int i = 0; i < expanded; i++) {
final float childUCT = children[i].calculateUCT(visits);
if (childUCT > highestUCT) {
highestUCTChild = children[i];
highestUCT = childUCT;
}
}
return highestUCTChild;
}
}
private final Random random;
private final int milliseconds;
public MCTSAI2(int milliseconds) {
this.random = new Random();
this.milliseconds = milliseconds;
}
public MCTSAI2(MCTSAI2 other) {
this.random = other.random;
this.milliseconds = other.milliseconds;
}
@Override
public MCTSAI2 deepCopy() {
return new MCTSAI2(this);
}
@Override
public long getMove(TurnBasedGame game) {
final Node root = new Node(game, null, 0L);
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
while (System.nanoTime() < endTime) {
Node leaf = selection(root);
leaf = expansion(leaf);
final float value = simulation(leaf);
backPropagation(leaf, value);
}
final Node mostVisitedChild = mostVisitedChild(root);
return mostVisitedChild != null? mostVisitedChild.move : 0L;
}
private Node mostVisitedChild(Node root) {
Node mostVisitedChild = null;
int mostVisited = -1;
for (int i = 0; i < root.expanded; i++) {
if (root.children[i].visits > mostVisited) {
mostVisitedChild = root.children[i];
mostVisited = root.children[i].visits;
}
}
return mostVisitedChild;
}
private Node selection(Node root) {
while (root.isFullyExpanded() && !root.state.isTerminal()) {
root = root.bestUCTChild();
}
return root;
}
private Node expansion(Node leaf) {
if (leaf.unexpandedMoves == 0L) {
return leaf;
}
final long unexpandedMove = leaf.unexpandedMoves & -leaf.unexpandedMoves;
final TurnBasedGame copiedState = leaf.state.deepCopy();
copiedState.play(unexpandedMove);
final Node expandedChild = new Node(copiedState, leaf, unexpandedMove);
leaf.children[leaf.expanded] = expandedChild;
leaf.expanded++;
leaf.unexpandedMoves &= ~unexpandedMove;
return expandedChild;
}
private float simulation(Node leaf) {
final TurnBasedGame copiedState = leaf.state.deepCopy();
final int playerIndex = 1 - copiedState.getCurrentTurn();
while (!copiedState.isTerminal()) {
final long legalMoves = copiedState.getLegalMoves();
final long randomMove = randomSetBit(legalMoves);
copiedState.play(randomMove);
}
if (copiedState.getWinner() == playerIndex) {
return 1.0f;
} else if (copiedState.getWinner() >= 0) {
return -1.0f;
}
return 0.0f;
}
private void backPropagation(Node leaf, float value) {
while (leaf != null) {
leaf.value += value;
leaf.visits++;
value = -value;
leaf = leaf.parent;
}
}
private long randomSetBit(long value) {
if (0L == value) {
return 0;
}
final int bitCount = Long.bitCount(value);
final int randomBitCount = random.nextInt(bitCount);
for (int i = 0; i < randomBitCount; i++) {
value &= value - 1;
}
return value & -value;
}
}

View File

@@ -0,0 +1,258 @@
package org.toop.game.players.ai;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.Random;
public class MCTSAI3 extends AbstractAI {
private static class Node {
public TurnBasedGame state;
public long move;
public long unexpandedMoves;
public Node parent;
public Node[] children;
public int expanded;
public float value;
public int visits;
public Node(TurnBasedGame state, Node parent, long move) {
final long legalMoves = state.getLegalMoves();
this.state = state;
this.move = move;
this.unexpandedMoves = legalMoves;
this.parent = parent;
this.children = new Node[Long.bitCount(legalMoves)];
this.expanded = 0;
this.value = 0.0f;
this.visits = 0;
}
public Node(TurnBasedGame state) {
this(state, null, 0L);
}
public boolean isFullyExpanded() {
return expanded == children.length;
}
public float calculateUCT(int parentVisits) {
final float exploitation = value / visits;
final float exploration = 1.41f * (float)(Math.sqrt(Math.log(parentVisits) / visits));
return exploitation + exploration;
}
public Node bestUCTChild() {
Node highestUCTChild = null;
float highestUCT = Float.NEGATIVE_INFINITY;
for (int i = 0; i < expanded; i++) {
final float childUCT = children[i].calculateUCT(visits);
if (childUCT > highestUCT) {
highestUCTChild = children[i];
highestUCT = childUCT;
}
}
return highestUCTChild;
}
}
private final Random random;
private Node root;
private final int milliseconds;
public MCTSAI3(int milliseconds) {
this.random = new Random();
this.root = null;
this.milliseconds = milliseconds;
}
public MCTSAI3(MCTSAI3 other) {
this.random = other.random;
this.root = other.root;
this.milliseconds = other.milliseconds;
}
@Override
public MCTSAI3 deepCopy() {
return new MCTSAI3(this);
}
@Override
public long getMove(TurnBasedGame game) {
detectRoot(game);
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
while (System.nanoTime() < endTime) {
Node leaf = selection(root);
leaf = expansion(leaf);
final float value = simulation(leaf);
backPropagation(leaf, value);
}
final Node mostVisitedChild = mostVisitedChild(root);
final long move = mostVisitedChild != null? mostVisitedChild.move : 0L;
newRoot(move);
return move;
}
private Node mostVisitedChild(Node root) {
Node mostVisitedChild = null;
int mostVisited = -1;
for (int i = 0; i < root.expanded; i++) {
if (root.children[i].visits > mostVisited) {
mostVisitedChild = root.children[i];
mostVisited = root.children[i].visits;
}
}
return mostVisitedChild;
}
private void detectRoot(TurnBasedGame game) {
if (root == null) {
root = new Node(game.deepCopy());
return;
}
final long[] currentBoards = game.getBoard();
final long[] rootBoards = root.state.getBoard();
boolean detected = true;
for (int i = 0; i < rootBoards.length; i++) {
if (rootBoards[i] != currentBoards[i]) {
detected = false;
break;
}
}
if (detected) {
return;
}
for (int i = 0; i < root.expanded; i++) {
final Node child = root.children[i];
final long[] childBoards = child.state.getBoard();
detected = true;
for (int j = 0; j < childBoards.length; j++) {
if (childBoards[j] != currentBoards[j]) {
detected = false;
break;
}
}
if (detected) {
root = child;
return;
}
}
root = new Node(game.deepCopy());
}
private void newRoot(long move) {
for (final Node child : root.children) {
if (child.move == move) {
root = child;
break;
}
}
}
private Node selection(Node root) {
while (root.isFullyExpanded() && !root.state.isTerminal()) {
root = root.bestUCTChild();
}
return root;
}
private Node expansion(Node leaf) {
if (leaf.unexpandedMoves == 0L) {
return leaf;
}
final long unexpandedMove = leaf.unexpandedMoves & -leaf.unexpandedMoves;
final TurnBasedGame copiedState = leaf.state.deepCopy();
copiedState.play(unexpandedMove);
final Node expandedChild = new Node(copiedState, leaf, unexpandedMove);
leaf.children[leaf.expanded] = expandedChild;
leaf.expanded++;
leaf.unexpandedMoves &= ~unexpandedMove;
return expandedChild;
}
private float simulation(Node leaf) {
final TurnBasedGame copiedState = leaf.state.deepCopy();
final int playerIndex = 1 - copiedState.getCurrentTurn();
while (!copiedState.isTerminal()) {
final long legalMoves = copiedState.getLegalMoves();
final long randomMove = randomSetBit(legalMoves);
copiedState.play(randomMove);
}
if (copiedState.getWinner() == playerIndex) {
return 1.0f;
} else if (copiedState.getWinner() >= 0) {
return -1.0f;
}
return 0.0f;
}
private void backPropagation(Node leaf, float value) {
while (leaf != null) {
leaf.value += value;
leaf.visits++;
value = -value;
leaf = leaf.parent;
}
}
private long randomSetBit(long value) {
if (0L == value) {
return 0;
}
final int bitCount = Long.bitCount(value);
final int randomBitCount = random.nextInt(bitCount);
for (int i = 0; i < randomBitCount; i++) {
value &= value - 1;
}
return value & -value;
}
}

View File

@@ -0,0 +1,165 @@
package org.toop.game.players.ai;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MiniMaxAI extends AbstractAI {
private final int maxDepth;
private final Random random = new Random();
public MiniMaxAI(int depth) {
this.maxDepth = depth;
}
public MiniMaxAI(MiniMaxAI other) {
this.maxDepth = other.maxDepth;
}
@Override
public MiniMaxAI deepCopy() {
return new MiniMaxAI(this);
}
@Override
public long getMove(TurnBasedGame game) {
long legalMoves = game.getLegalMoves();
if (legalMoves == 0) return 0;
List<Long> bestMoves = new ArrayList<>();
int bestScore = Integer.MIN_VALUE;
int aiPlayer = game.getCurrentTurn();
long movesLoop = legalMoves;
while (movesLoop != 0) {
long move = 1L << Long.numberOfTrailingZeros(movesLoop);
TurnBasedGame copy = game.deepCopy();
PlayResult result = copy.play(move);
int score;
switch (result.state()) {
case WIN -> score = (result.player() == aiPlayer ? maxDepth : -maxDepth);
case DRAW -> score = 0;
default -> score = getMoveScore(copy, maxDepth - 1, false, aiPlayer, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
if (score > bestScore) {
bestScore = score;
bestMoves.clear();
bestMoves.add(move);
} else if (score == bestScore) {
bestMoves.add(move);
}
movesLoop &= movesLoop - 1;
}
long chosenMove = bestMoves.get(random.nextInt(bestMoves.size()));
return chosenMove;
}
/**
* Recursive minimax with alpha-beta pruning and heuristic evaluation.
*
* @param game Current game state
* @param depth Remaining depth
* @param maximizing True if AI is maximizing, false if opponent
* @param aiPlayer AI's player index
* @param alpha Alpha value
* @param beta Beta value
* @return score of the position
*/
private int getMoveScore(TurnBasedGame game, int depth, boolean maximizing, int aiPlayer, int alpha, int beta) {
long legalMoves = game.getLegalMoves();
// Terminal state
PlayResult lastResult = null;
if (legalMoves == 0) {
lastResult = new PlayResult(GameState.DRAW, -1);
}
// If the game is over or depth limit reached, evaluate
if (depth <= 0 || legalMoves == 0) {
if (lastResult != null) return 0;
return evaluateBoard(game, aiPlayer);
}
int bestScore = maximizing ? Integer.MIN_VALUE : Integer.MAX_VALUE;
long movesLoop = legalMoves;
while (movesLoop != 0) {
long move = 1L << Long.numberOfTrailingZeros(movesLoop);
TurnBasedGame copy = game.deepCopy();
PlayResult result = copy.play(move);
int score;
switch (result.state()) {
case WIN -> score = (result.player() == aiPlayer ? depth : -depth);
case DRAW -> score = 0;
default -> score = getMoveScore(copy, depth - 1, !maximizing, aiPlayer, alpha, beta);
}
if (maximizing) {
bestScore = Math.max(bestScore, score);
alpha = Math.max(alpha, bestScore);
} else {
bestScore = Math.min(bestScore, score);
beta = Math.min(beta, bestScore);
}
// Alpha-beta pruning
if (beta <= alpha) break;
movesLoop &= movesLoop - 1;
}
return bestScore;
}
/**
* Simple heuristic evaluation for Reversi-like games.
* Positive = good for AI, Negative = good for opponent.
*
* @param game OnlineTurnBasedGame state
* @param aiPlayer AI's player index
* @return heuristic score
*/
private int evaluateBoard(TurnBasedGame game, int aiPlayer) {
long[] board = game.getBoard();
int aiCount = 0;
int opponentCount = 0;
// Count pieces for AI vs opponent
for (int i = 0; i < board.length; i++) {
long bits = board[i];
for (int j = 0; j < 64; j++) {
if ((bits & (1L << j)) != 0) {
// Assume player 0 occupies even indices, player 1 occupies odd
if ((i * 64 + j) % game.getPlayerCount() == aiPlayer) aiCount++;
else opponentCount++;
}
}
}
// Mobility (number of legal moves)
int mobility = Long.bitCount(game.getLegalMoves());
// Corner control (top-left, top-right, bottom-left, bottom-right)
int corners = 0;
long[] cornerMasks = {1L << 0, 1L << 7, 1L << 56, 1L << 63};
for (long mask : cornerMasks) {
for (long b : board) {
if ((b & mask) != 0) corners += 1;
}
}
// Weighted sum
return (aiCount - opponentCount) + 2 * mobility + 5 * corners;
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.players.ai;
package org.toop.game.players.ai;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;

View File

@@ -0,0 +1,38 @@
package org.toop.game.players.ai.mcts;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.game.players.ai.MCTSAI;
public class MCTSAI1 extends MCTSAI {
public MCTSAI1(int milliseconds) {
super(milliseconds);
}
public MCTSAI1(MCTSAI1 other) {
super(other);
}
@Override
public MCTSAI1 deepCopy() {
return new MCTSAI1(this);
}
@Override
public long getMove(TurnBasedGame game) {
final Node root = new Node(game, null, 0L);
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
Node leaf = selection(root);
leaf = expansion(leaf);
final int value = simulation(leaf);
backPropagation(leaf, value);
}
lastIterations = root.visits.get();
final Node mostVisitedChild = mostVisitedChild(root);
return mostVisitedChild.move;
}
}

View File

@@ -0,0 +1,48 @@
package org.toop.game.players.ai.mcts;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.game.players.ai.MCTSAI;
public class MCTSAI2 extends MCTSAI {
private Node root;
public MCTSAI2(int milliseconds) {
super(milliseconds);
this.root = null;
}
public MCTSAI2(MCTSAI2 other) {
super(other);
this.root = other.root;
}
@Override
public MCTSAI2 deepCopy() {
return new MCTSAI2(this);
}
@Override
public long getMove(TurnBasedGame game) {
root = findOrResetRoot(root, game);
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
Node leaf = selection(root);
leaf = expansion(leaf);
final int value = simulation(leaf);
backPropagation(leaf, value);
}
lastIterations = root.visits.get();
final Node mostVisitedChild = mostVisitedChild(root);
final long move = mostVisitedChild.move;
root = findChildByMove(root, move);
return move;
}
}

View File

@@ -0,0 +1,76 @@
package org.toop.game.players.ai.mcts;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.game.players.ai.MCTSAI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MCTSAI3 extends MCTSAI {
private final int threads;
private final ExecutorService threadPool;
public MCTSAI3(int milliseconds, int threads) {
super(milliseconds);
this.threads = threads;
this.threadPool = Executors.newFixedThreadPool(threads);
}
public MCTSAI3(MCTSAI3 other) {
super(other);
this.threads = other.threads;
this.threadPool = other.threadPool;
}
@Override
public MCTSAI3 deepCopy() {
return new MCTSAI3(this);
}
@Override
public long getMove(TurnBasedGame game) {
final Node root = new Node(game.deepCopy(), null, 0L);
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
final CountDownLatch latch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
threadPool.submit(() -> {
try {
iterate(root, endTime);
} finally {
latch.countDown();
}
});
}
try {
final long remaining = endTime - System.nanoTime();
latch.await(remaining, TimeUnit.NANOSECONDS);
lastIterations = root.visits.get();
final Node mostVisitedChild = mostVisitedChild(root);
return mostVisitedChild.move;
} catch (Exception _) {
lastIterations = 0;
final long legalMoves = game.getLegalMoves();
return randomSetBit(legalMoves);
}
}
private void iterate(Node root, long endTime) {
while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
Node leaf = selection(root);
leaf = expansion(leaf);
final int value = simulation(leaf);
backPropagation(leaf, value);
}
}
}

View File

@@ -0,0 +1,82 @@
package org.toop.game.players.ai.mcts;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.game.players.ai.MCTSAI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MCTSAI4 extends MCTSAI {
private final int threads;
private final ExecutorService threadPool;
private Node root;
public MCTSAI4(int milliseconds, int threads) {
super(milliseconds);
this.threads = threads;
this.threadPool = Executors.newFixedThreadPool(threads);
}
public MCTSAI4(MCTSAI4 other) {
super(other);
this.threads = other.threads;
this.threadPool = other.threadPool;
}
@Override
public MCTSAI4 deepCopy() {
return new MCTSAI4(this);
}
@Override
public long getMove(TurnBasedGame game) {
root = findOrResetRoot(root, game);
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
final CountDownLatch latch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
threadPool.submit(() -> {
try {
iterate(root, endTime);
} finally {
latch.countDown();
}
});
}
try {
final long remaining = endTime - System.nanoTime();
latch.await(remaining, TimeUnit.NANOSECONDS);
lastIterations = root.visits.get();
final Node mostVisitedChild = mostVisitedChild(root);
final long move = mostVisitedChild.move;
root = findChildByMove(root, move);
return move;
} catch (Exception _) {
lastIterations = 0;
final long legalMoves = game.getLegalMoves();
return randomSetBit(legalMoves);
}
}
private void iterate(Node root, long endTime) {
while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
Node leaf = selection(root);
leaf = expansion(leaf);
final int value = simulation(leaf);
backPropagation(leaf, value);
}
}
}

View File

@@ -0,0 +1,77 @@
package research;
public class AIData {
public String AI;
public long gamesPlayed;
public double winrate;
public double averageIterations;
public double averageIterations10;
public double averageIterations20;
public double averageIterations30;
public AIData(String AI, long gamesPlayed, double winrate, double averageIterations, double averageIterations10, double averageIterations20, double averageIterations30) {
this.AI = AI;
this.gamesPlayed = gamesPlayed;
this.winrate = winrate;
this.averageIterations = averageIterations;
this.averageIterations10 = averageIterations10;
this.averageIterations20 = averageIterations20;
this.averageIterations30 = averageIterations30;
}
public String getAI() {
return AI;
}
public void setAI(String AI) {
this.AI = AI;
}
public long getGamesPlayed() {
return gamesPlayed;
}
public void setGamesPlayed(long gamesPlayed) {
this.gamesPlayed = gamesPlayed;
}
public double getWinrate() {
return winrate;
}
public void setWinrate(double winrate) {
this.winrate = winrate;
}
public double getAverageIterations() {
return averageIterations;
}
public void setAverageIterations(double averageIterations) {
this.averageIterations = averageIterations;
}
public double getAverageIterations10() {
return averageIterations10;
}
public void setAverageIterations10(double averageIterations10) {
this.averageIterations10 = averageIterations10;
}
public double getAverageIterations20() {
return averageIterations20;
}
public void setAverageIterations20(double averageIterations20) {
this.averageIterations20 = averageIterations20;
}
public double getAverageIterations30() {
return averageIterations30;
}
public void setAverageIterations30(double averageIterations30) {
this.averageIterations30 = averageIterations30;
}
}

View File

@@ -0,0 +1,612 @@
package research;
import org.apache.maven.surefire.shared.io.FileDeleteStrategy;
import org.junit.jupiter.api.*;
import org.toop.framework.game.games.reversi.BitboardReversi;
import org.toop.framework.game.players.ArtificialPlayer;
import org.toop.game.players.ai.MCTSAI;
import org.toop.game.players.ai.mcts.MCTSAI1;
import org.toop.game.players.ai.mcts.MCTSAI2;
import org.toop.game.players.ai.mcts.MCTSAI3;
import org.toop.game.players.ai.mcts.MCTSAI4;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AITest {
private static String fileName = "gameData.csv";
private static List<Matchup> matchupList = new ArrayList<Matchup>();
private static List<AIData> dataList = new ArrayList<AIData>();
private static List<GameData> gameDataList = new ArrayList<GameData>();
@BeforeAll
public static void init() {
var versions = new ArtificialPlayer[4];
versions[0] = new ArtificialPlayer(new MCTSAI1(10), "MCTS V1");
versions[1] = new ArtificialPlayer(new MCTSAI2(10), "MCTS V2");
versions[2] = new ArtificialPlayer(new MCTSAI3(10, 8), "MCTS V3");
versions[3] = new ArtificialPlayer(new MCTSAI4(10, 8), "MCTS V4");
for (int i = 0; i < versions.length; i++) {
for (int j = i + 1; j < versions.length; j++) {
final int playerIndex1 = i % versions.length;
final int playerIndex2 = j % versions.length;
addMatch(versions[playerIndex1], versions[playerIndex2]);
addMatch(versions[playerIndex2], versions[playerIndex1]); // home vs away system
}
}
}
// @BeforeAll
// public static void init() {
//
// var versions = new ArtificialPlayer[11];
// versions[0] = new ArtificialPlayer(new MCTSAI3(10, 1), "MCTS V3T1");
// versions[1] = new ArtificialPlayer(new MCTSAI3(10, 2), "MCTS V3T2");
// versions[2] = new ArtificialPlayer(new MCTSAI3(10, 4), "MCTS V3T4");
// versions[3] = new ArtificialPlayer(new MCTSAI3(10, 8), "MCTS V3T8");
// versions[4] = new ArtificialPlayer(new MCTSAI3(10, 16), "MCTS V3T16");
// versions[5] = new ArtificialPlayer(new MCTSAI3(10, 128), "MCTS V3T32");
// versions[6] = new ArtificialPlayer(new MCTSAI3(10, 256), "MCTS V3T64");
// versions[7] = new ArtificialPlayer(new MCTSAI3(10, 128), "MCTS V3T128");
// versions[8] = new ArtificialPlayer(new MCTSAI3(10, 256), "MCTS V3T256");
// versions[9] = new ArtificialPlayer(new MCTSAI3(10, 512), "MCTS V3T512");
// versions[10] = new ArtificialPlayer(new MCTSAI3(10, 1024), "MCTS V3T1024");
//
// for (int i = 0; i < versions.length; i++) {
// for (int j = i + 1; j < versions.length; j++) {
// final int playerIndex1 = i % versions.length;
// final int playerIndex2 = j % versions.length;
// addMatch(versions[playerIndex1], versions[playerIndex2]);
// addMatch(versions[playerIndex2], versions[playerIndex1]); // home vs away system
// }
// }
// }
public static void addMatch(ArtificialPlayer v1, ArtificialPlayer v2) {
matchupList.add(new Matchup(v1, v2));
}
public void addAIData(AIData data) {
dataList.add(data);
}
public void addGameData(GameData data) {
gameDataList.add(data);
}
@Test
public void testAIvsAI() {
while (true) {
for (Matchup m : matchupList) {
playGame(m);
}
}
}
public void playGame(Matchup m) {
long nanocounterAI1 = 0L;
long nanocounterAI2 = 0L;
List<Integer> iterationsAI1 = new ArrayList<>();
List<Integer> iterationsAI2 = new ArrayList<>();
final BitboardReversi match = new BitboardReversi();
ArtificialPlayer[] players = new ArtificialPlayer[2];
players[0] = m.getPlayer1();
players[1] = m.getPlayer2();
match.init(players);
while (!match.isTerminal()) {
final int currentAI = match.getCurrentTurn();
final long startTime = System.nanoTime();
final long move = players[currentAI].getMove(match);
final long endTime = System.nanoTime();
if (players[currentAI].getAi() instanceof MCTSAI) {
final int lastIterations = ((MCTSAI) players[currentAI].getAi()).getLastIterations();
if (currentAI == 0) {
iterationsAI1.add(lastIterations);
nanocounterAI1 += (endTime - startTime);
} else {
iterationsAI2.add(lastIterations);
nanocounterAI2 += (endTime - startTime);
}
}
match.play(move);
}
generateMatchData(m.getPlayer1().getName(), m.getPlayer2().getName(), match, iterationsAI1, iterationsAI2, nanocounterAI1, nanocounterAI2);
}
public void generateMatchData(
String AI1,
String AI2,
BitboardReversi match,
List<Integer> iterationsAI1,
List<Integer> iterationsAI2,
long nanocounterAI1,
long nanocounterAI2
) {
try {
var ai110 = iterationsAI1.subList(0, 10);
var ai120 = iterationsAI1.subList(10, 20);
var ai130 = iterationsAI1.subList(20, iterationsAI1.size());
var ai210 = iterationsAI2.subList(0, 10);
var ai220 = iterationsAI2.subList(10, 20);
var ai230 = iterationsAI2.subList(20, iterationsAI2.size());
writeGamesToCSV(fileName, new GameData(
AI1,
AI2,
getWinnerForMatch(AI1, AI2, match),
match.getAmountOfTurns(),
iterationsAI1.stream().mapToLong(Integer::longValue).sum(),
ai110.stream().mapToLong(Integer::longValue).sum(),
ai120.stream().mapToLong(Integer::longValue).sum(),
ai130.stream().mapToLong(Integer::longValue).sum(),
iterationsAI1.stream().mapToDouble(Integer::doubleValue).sum() / iterationsAI1.size(),
ai110.stream().mapToDouble(Integer::doubleValue).sum() / ai110.size(),
ai120.stream().mapToDouble(Integer::doubleValue).sum() / ai120.size(),
ai130.stream().mapToDouble(Integer::doubleValue).sum() / ai130.size(),
iterationsAI2.stream().mapToInt(Integer::intValue).sum(),
ai210.stream().mapToLong(Integer::longValue).sum(),
ai220.stream().mapToLong(Integer::longValue).sum(),
ai230.stream().mapToLong(Integer::longValue).sum(),
iterationsAI2.stream().mapToDouble(Integer::doubleValue).sum() / iterationsAI2.size(),
ai210.stream().mapToDouble(Integer::doubleValue).sum() / ai210.size(),
ai220.stream().mapToDouble(Integer::doubleValue).sum() / ai220.size(),
ai230.stream().mapToDouble(Integer::doubleValue).sum() / ai230.size(),
nanocounterAI1,
nanocounterAI2,
LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
));
} catch (IOException e) {
throw new RuntimeException(e);
} catch (IndexOutOfBoundsException e) {
return;
}
}
public String getWinnerForMatch(String AI1, String AI2, BitboardReversi match) {
if (match.getWinner() == 0) {
return AI1;
}
if (match.getWinner() == 1) {
return AI2;
} else {
return "TIE";
}
}
public void generateData(Matchup matchup, BitboardReversi match, List<Integer> iterationsAI1, List<Integer> iterationsAI2) {
boolean matchup1Found = false;
boolean matchup2Found = false;
for (AIData aiData : dataList) {
if (aiData.getAI().equals(matchup.getPlayer1().getName())) {
matchup1Found = true;
} if (aiData.getAI().equals(matchup.getPlayer2().getName())) {
matchup2Found = true;
}
}
if (!(matchup1Found)) {
addAIData(new AIData(matchup.getPlayer1().getName(), 0, 0, 0, 0, 0, 0));
}
if (!(matchup2Found)) {
addAIData(new AIData(matchup.getPlayer2().getName(), 0, 0, 0, 0, 0, 0));
}
for (AIData aiData : dataList) { // set data for player 1
if (aiData.getAI().equals(matchup.getPlayer1().getName())) {
aiData.setGamesPlayed(aiData.getGamesPlayed() + 1);
aiData.setWinrate(calculateWinrate(0, aiData.getWinrate(), aiData.getGamesPlayed(), match.getWinner()));
aiData.setAverageIterations(calculateAverageIterations(aiData.getAverageIterations(), iterationsAI1));
aiData.setAverageIterations10(calculateAverageIterationsStartEnd(0, 10, aiData.getAverageIterations10(), iterationsAI1));
aiData.setAverageIterations20(calculateAverageIterationsStartEnd(10, 20, aiData.getAverageIterations20(), iterationsAI1));
aiData.setAverageIterations30(calculateAverageIterationsStartEnd(20, iterationsAI1.size(), aiData.getAverageIterations30(), iterationsAI1));
}
}
for (AIData aiData : dataList) {
if (aiData.getAI().equals(matchup.getPlayer2().getName())) {
aiData.setGamesPlayed(aiData.getGamesPlayed() + 1);
aiData.setWinrate(calculateWinrate(1, aiData.getWinrate(), aiData.getGamesPlayed(), match.getWinner()));
aiData.setAverageIterations(calculateAverageIterations(aiData.getAverageIterations(), iterationsAI2));
aiData.setAverageIterations10(calculateAverageIterationsStartEnd(0, 10, aiData.getAverageIterations10(), iterationsAI2));
aiData.setAverageIterations20(calculateAverageIterationsStartEnd(10, 20, aiData.getAverageIterations20(), iterationsAI2));
aiData.setAverageIterations30(calculateAverageIterationsStartEnd(20, iterationsAI2.size(), aiData.getAverageIterations30(), iterationsAI2));
}
}
}
public double calculateWinrate(int player, double winrate, long gamesPlayed, int winner) {
double result;
if (winner == 0 && player == 0 || winner == 1 && player == 1) {
return (winrate * (gamesPlayed - 1) + 1) / gamesPlayed;
} else if (winner == 0 && player == 1 || winner == 1 && player == 0) {
return (winrate * (gamesPlayed - 1) + 0) / gamesPlayed;
}
return (winrate * (gamesPlayed - 1) + 0) / gamesPlayed;
}
public double calculateAverageIterations(double averageIterations, List<Integer> thisGameIterations) {
double thisGameIterationsAverage = 0;
for (int iterations = 0; iterations < thisGameIterations.size(); iterations += 1) {
thisGameIterationsAverage += thisGameIterations.get(iterations);
}
thisGameIterationsAverage /= thisGameIterations.size();
return (averageIterations + thisGameIterationsAverage) / 2;
}
public double calculateAverageIterationsStartEnd(int start, int end, double averageIterations, List<Integer> thisGameIterations) {
double thisGameIterationsAverage = 0;
for (int iterations = start; iterations < end; iterations += 1) {
thisGameIterationsAverage += thisGameIterations.get(iterations);
}
thisGameIterationsAverage /= (end - start);
return (averageIterations + thisGameIterationsAverage) / 2;
}
@AfterAll
public static void writeAfterTests() {
try {
writeAIToCsv("Data.csv", dataList);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void writeGamesToCSV(String filepath, GameData gameData) throws IOException {
try (
final BufferedWriter writer = Files.newBufferedWriter(
Paths.get(filepath),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
final BufferedReader reader = new BufferedReader(new FileReader(filepath))
) {
if (reader.readLine() == null || reader.readLine().isBlank()) {
writer.write("Black,White,Winner,Turns Played,Black total iterations,Black total iterations 0-10,Black total iterations 11-20,Black total iterations 21-30,Black average iterations,Black average iterations 0-10,Black average iterations 11-20,Black average iterations 21-30,White total iterations,White total iterations 0-10,White total iterations 11-20,White total iterations 21-30,White average iterations,White average iterations 0-10,White average iterations 11-20,White average iterations 21-30,Total Time AI1,Total Time AI2,Time");
writer.newLine();
}
writer.write(
gameData.AI1() + "," +
gameData.AI2() + "," +
gameData.winner() + "," +
gameData.turns() + "," +
gameData.AI1totalIterations() + "," +
gameData.AI1totalIterations10() + "," +
gameData.AI1totalIterations20() + "," +
gameData.AI1totalIterations30() + "," +
BigDecimal.valueOf(gameData.AI1averageIterations()).setScale(2, RoundingMode.HALF_EVEN) + "," +
BigDecimal.valueOf(gameData.AI1averageIterations10()).setScale(2, RoundingMode.HALF_EVEN) + "," +
BigDecimal.valueOf(gameData.AI1averageIterations20()).setScale(2, RoundingMode.HALF_EVEN) + "," +
BigDecimal.valueOf(gameData.AI1averageIterations30()).setScale(2, RoundingMode.HALF_EVEN) + "," +
gameData.AI2totalIterations() + "," +
gameData.AI2totalIterations10() + "," +
gameData.AI2totalIterations20() + "," +
gameData.AI2totalIterations30() + "," +
BigDecimal.valueOf(gameData.AI2averageIterations()).setScale(2, RoundingMode.HALF_EVEN) + "," +
BigDecimal.valueOf(gameData.AI2averageIterations10()).setScale(2, RoundingMode.HALF_EVEN) + "," +
BigDecimal.valueOf(gameData.AI2averageIterations20()).setScale(2, RoundingMode.HALF_EVEN) + "," +
BigDecimal.valueOf(gameData.AI2averageIterations30()).setScale(2, RoundingMode.HALF_EVEN) + "," +
(gameData.nanoAI1() / 1_000_000L) + "," +
(gameData.nanoAI2() / 1_000_000L) + "," +
gameData.time());
writer.newLine();
}
}
public static void writeAIToCsv(String filepath, List<AIData> dataList) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filepath))) {
writer.write("AI Name,Games Played,Winrate,Average Iterations,Average Iterations 0-10, Average Iterations 11-20, Average Iterations 20-30");
writer.newLine();
for (AIData data : dataList) {
writer.write(
data.getAI() + "," +
data.getGamesPlayed() + "," +
data.getWinrate() + "," +
Math.round(data.getAverageIterations()) + "," +
Math.round(data.getAverageIterations10()) + "," +
Math.round(data.getAverageIterations20()) + "," +
Math.round(data.getAverageIterations30()));
writer.newLine();
}
}
}
}
//public class AITest {
// private static int games = 2;
//
// @BeforeAll
// public static void setUp() {
// var versions = new ArtificialPlayer[5];
// versions[0] = new ArtificialPlayer(new RandomAI(), "Random AI");
// versions[1] = new ArtificialPlayer(new MCTSAI1(20), "MCTS V1 AI");
// versions[2] = new ArtificialPlayer(new org.toop.game.players.ai.mcts.MCTSAI2(20), "MCTS V2 AI");
// versions[3] = new ArtificialPlayer(new org.toop.game.players.ai.mcts.MCTSAI3(20, 10), "MCTS V3 AI");
// versions[4] = new ArtificialPlayer(new MCTSAI4(20, 10), "MCTS V4 AI");
//
// for (int i = 0; i < versions.length; i++) {
// for (int j = i + 1; j < versions.length; j++) {
// final int playerIndex1 = i % versions.length;
// final int playerIndex2 = j % versions.length;
// addMatchup(versions[playerIndex1], versions[playerIndex2]);
// }
// }
//
// }
//
// @BeforeEach
// public void setUpEach() {
// matchupList = new ArrayList<>();
// }
//
// @Test
// public void testIterationsInRealGame() {
// for (int i = 0; i < matchups.size(); i++) {
// testAIVSAI(games, getMatchup(i));
// }
// }
//
//
// private void testAIVSAI(int games, ArtificialPlayer[] ais) {
//
// List<List<Integer>> gamesList = new ArrayList<>();
// for (int i = 0; i < games; i++) {
// final BitboardReversi match = new BitboardReversi();
// match.init(ais);
//
// List<Integer> iterations1 = new ArrayList<>();
// List<Integer> iterations2 = new ArrayList<>();
//
// while (!match.isTerminal()) {
// final int currentAI = match.getCurrentTurn();
// final long move = ais[currentAI].getMove(match);
// if (ais[currentAI].getAi() instanceof MCTSAI) {
// final int lastIterations = ((MCTSAI) ais[currentAI].getAi()).getLastIterations();
// if (currentAI == 0) {
// iterations1.add(lastIterations);
// } else if (currentAI == 1) {
// iterations2.add(lastIterations);
// }
// }
// match.play(move);
// }
// int winner = match.getWinner();
// iterations1.addFirst(winner);
//// iterations1.add(-999);
// iterations1.addAll(iterations2);
//
// gamesList.add(iterations1);
// }
// matchupList.add(gamesList);
// }
//
// @Test
// public void testIterationsAtFixedMove() {
// for (ArtificialPlayer[] matchup : matchups) {
// List<List<Integer>> gamesList = new ArrayList<>();
// for (int j = 0; j < games; j++) {
// final BitboardReversi match = new BitboardReversi();
// match.init(matchup);
//
// List<Integer> iterations = new ArrayList<>();
//
// for (Long move : fixedMoveSet) {
// match.play(move);
// if (move == 32L) {
// break;
// }
// }
//// iterations.add(-999);
// var player = matchup[match.getCurrentTurn()];
// for (int k = 0; k < 10; k++) {
// player.getMove(match);
// if (player.getAi() instanceof MCTSAI) {
// iterations.add(((MCTSAI) player.getAi()).getLastIterations());
// }
// }
// gamesList.add(iterations);
// }
// matchupList.add(gamesList);
// }
// }
//
//
// @Test
// public void testIterationsInFixedGame() {
// for (ArtificialPlayer[] matchup : matchups) {
// List<List<Integer>> gamesList = new ArrayList<>();
// for (int j = 0; j < games; j++) {
// final BitboardReversi match = new BitboardReversi();
// match.init(matchup);
//
// List<Integer> iterations = new ArrayList<>();
//
// iterations.add(-999);
//
// for (Long move : fixedMoveSet) {
// var player = matchup[match.getCurrentTurn()];
// player.getMove(match);
// if (player.getAi() instanceof MCTSAI) {
// iterations.add(((MCTSAI) player.getAi()).getLastIterations());
// }
// match.play(move);
// }
//
// gamesList.add(iterations);
// }
// matchupList.add(gamesList);
// }
// }
//
// @AfterEach
// public void tearDown() {
// data.add(matchupList);
// }
//
// @AfterAll
// public static void writeAfterTests() {
// try {
// writeToCsv("Data.csv", data);
// } catch (IOException e) {
//
// }
// }
//
//
// public static void writeToCsv(String filepath, List<List<List<List<Integer>>>> data) throws IOException {
// try (BufferedWriter writer = new BufferedWriter(new FileWriter(filepath))) {
//
// writer.write("TestID,Matchup,GameNr,Winner");
// for (int i = 0; i < data.size(); i++) {
// writer.write(",Iterations");
// }
//
// writer.newLine();
//
// for (int TestID = 0; TestID < data.size(); TestID++) {
// List<List<List<Integer>>> testCase = data.get(TestID);
//
// for (int matchupNr = 0; matchupNr < testCase.size(); matchupNr++) {
// List<List<Integer>> matchup = testCase.get(matchupNr);
//
// for (int gameNr = 0; gameNr < matchup.size(); gameNr++) {
// List<Integer> game = matchup.get(gameNr);
// writer.write((TestID + 1) + "," + (getMatchupName(matchupNr)) + "," + (gameNr + 1));
// for (int i = 0; i < game.size(); i++) {
// if (i == 0) {
// writer.write("," + getWinnerFromMatchup(game.get(i), matchupNr));
// } else {
// writer.write("," + game.get(i));
// }
// }
// writer.newLine();
// }
// }
// }
// }
//
// }
//
//
// private static final List<List<List<List<Integer>>>> data = new ArrayList<>();
// private List<List<List<Integer>>> matchupList = new ArrayList<>();
// private static final List<String> matchupNames = new ArrayList<>();
// private static final List<ArtificialPlayer[]> matchups = new ArrayList<>();
//
// private static String getMatchupName(int matchupNr) {
// return matchupNames.get(matchupNr);
// }
//
// private static ArtificialPlayer[] getMatchup(int matchupNr) {
// return matchups.get(matchupNr);
// }
//
// private static String getWinnerFromMatchup(Integer winner, int matchupNr) {
// String matchup = matchupNames.get(matchupNr);
//
// String[] parts = matchup.split(" vs ");
//
// if (parts.length != 2) {
// return "Invalid matchup formatting.";
// }
//
// return winner == 0 ? parts[0] : winner == 1 ? parts[1] : winner == -999 ? "NVT" : "Tie";
// }
//
// private static void addMatchup(ArtificialPlayer player1, ArtificialPlayer player2) {
// matchups.add(new ArtificialPlayer[]{player1, player2});
// matchupNames.add(player1.getName() + " vs " + player2.getName());
// }
//}
// private final Long[] fixedMoveSet = new Long[]{17592186044416L,
// 35184372088832L,
// 67108864L,
// 8796093022208L,
// 2251799813685248L,
// 288230376151711744L,
// 70368744177664L,
// 1125899906842624L,
// 137438953472L,
// 140737488355328L,
// 4503599627370496L,
// 2305843009213693952L,
// 18014398509481984L,
// 274877906944L,
// 576460752303423488L,
// -9223372036854775808L,
// 549755813888L,
// 1152921504606846976L,
// 144115188075855872L,
// 72057594037927936L,
// 36028797018963968L,
// 17179869184L,
// 2199023255552L,
// 1048576L,
// 4398046511104L,
// 281474976710656L,
// 9007199254740992L,
// 2147483648L,
// 1073741824L,
// 33554432L,
// 262144L,
// 8388608L,
// 8192L,
// 4611686018427387904L,
// 4294967296L,
// 524288L,
// 4096L,
// 16777216L,
// 65536L,
// 32L,
// 2048L,
// 8L,
// 4L,
// 8589934592L,
// 16L,
// 2097152L,
// 4194304L,
// 1024L,
// 512L,
// 16384L,
// 536870912L,
// 1099511627776L,
// 64L,
// 562949953421312L,
// 128L,
// 1L,
// 32768L,
// 2L,
// 256L,
// 131072L};
// }

View File

@@ -0,0 +1,32 @@
package research;
public record GameData(
String AI1,
String AI2,
String winner,
int turns,
long AI1totalIterations,
long AI1totalIterations10,
long AI1totalIterations20,
long AI1totalIterations30,
double AI1averageIterations,
double AI1averageIterations10,
double AI1averageIterations20,
double AI1averageIterations30,
long AI2totalIterations,
long AI2totalIterations10,
long AI2totalIterations20,
long AI2totalIterations30,
double AI2averageIterations,
double AI2averageIterations10,
double AI2averageIterations20,
double AI2averageIterations30,
long nanoAI1,
long nanoAI2,
String time
) {}

View File

@@ -0,0 +1,30 @@
package research;
import org.toop.framework.game.players.ArtificialPlayer;
import java.util.ArrayList;
import java.util.List;
public class Matchup {
public ArtificialPlayer player1;
public ArtificialPlayer player2;
public Matchup(ArtificialPlayer player1, ArtificialPlayer player2) {
this.player1 = player1;
this.player2 = player2;
}
public Matchup() {}
public String toString() {
return player1.toString() + " VS " + player2.toString();
}
public ArtificialPlayer getPlayer1() {
return player1;
}
public ArtificialPlayer getPlayer2() {
return player2;
}
}