* 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>
This commit is contained in:
Bas Antonius de Jong
2025-12-09 21:28:45 +01:00
committed by GitHub
parent afb4844084
commit 8146be16ed
262 changed files with 11587 additions and 6669 deletions

View File

@@ -1,8 +1,13 @@
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>org.toop</groupId>
<artifactId>pism_game</artifactId>
<parent>
<groupId>org.toop</groupId>
<artifactId>pism</artifactId>
<version>0.1</version>
</parent>
<artifactId>game</artifactId>
<version>0.1</version>
<properties>
@@ -83,39 +88,78 @@
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
</dependency>
</dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>2.42.0</version>
</dependency>
<dependency>
<groupId>org.toop</groupId>
<artifactId>framework</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>-->
<!-- <arg>-Xplugin:ErrorProne</arg>-->
<!-- </compilerArgs>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.42.0</version>-->
<!-- </path>-->
<!-- &lt;!&ndash; Other annotation processors go here.-->
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
<!-- together with other annotation processors' below. &ndash;&gt;-->
<!-- </annotationProcessorPaths>-->
<!-- <fork>true</fork>-->
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<showWarnings>true</showWarnings>
<fork>true</fork>
<executable>${java.home}/bin/javac</executable>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>
-Xplugin:ErrorProne
</arg>
<!-- TODO-->
<!-- -Xep:RestrictedApi:ERROR \-->
<!-- -XepOpt:RestrictedApi:annotation=org.toop.annotations.TestsOnly \-->
<!-- -XepOpt:RestrictedApi:allowlistRegex=(?s).*/src/test/java/.*|.*test\.java \-->
<!-- -XepOpt:RestrictedApi:message=This API is marked @TestsOnly and shouldn't be normally used.-->
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</path>
</annotationProcessorPaths>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>

View File

@@ -1,5 +0,0 @@
package org.toop.game;
public abstract class AI<T extends Game> {
public abstract Game.Move findBestMove(T game, int depth);
}

View File

@@ -0,0 +1,86 @@
package org.toop.game;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
// There is AI performance to be gained by getting rid of non-primitives and thus speeding up deepCopy
public abstract class BitboardGame<T extends BitboardGame<T>> implements TurnBasedGame<T> {
private final int columnSize;
private final int rowSize;
private Player<T>[] players;
// long is 64 bits. Every game has a limit of 64 cells maximum.
private final long[] playerBitboard;
private int currentTurn = 0;
public BitboardGame(int columnSize, int rowSize, int playerCount, Player<T>[] players) {
this.columnSize = columnSize;
this.rowSize = rowSize;
this.players = players;
this.playerBitboard = new long[playerCount];
Arrays.fill(playerBitboard, 0L);
}
public BitboardGame(BitboardGame<T> other) {
this.columnSize = other.columnSize;
this.rowSize = other.rowSize;
this.playerBitboard = other.playerBitboard.clone();
this.currentTurn = other.currentTurn;
this.players = Arrays.stream(other.players)
.map(Player<T>::deepCopy)
.toArray(Player[]::new);
}
public int getColumnSize() {
return this.columnSize;
}
public int getRowSize() {
return this.rowSize;
}
public long getPlayerBitboard(int player) {
return this.playerBitboard[player];
}
public void setPlayerBitboard(int player, long bitboard) {
this.playerBitboard[player] = bitboard;
}
public int getPlayerCount() {
return playerBitboard.length;
}
public int getCurrentTurn() {
return getCurrentPlayerIndex();
}
public Player<T> getPlayer(int index) {return players[index];}
public int getCurrentPlayerIndex() {
return currentTurn % playerBitboard.length;
}
public int getNextPlayer() {
return (currentTurn + 1) % playerBitboard.length;
}
public Player<T> getCurrentPlayer(){
return players[getCurrentPlayerIndex()];
}
@Override
public long[] getBoard() {return this.playerBitboard;}
public void nextTurn() {
currentTurn++;
}
}

View File

@@ -1,39 +0,0 @@
package org.toop.game;
import java.util.Arrays;
public abstract class Game {
public enum State {
NORMAL,
DRAW,
WIN,
}
public record Move(int position, char value) {}
public static final char EMPTY = (char) 0;
public final int rowSize;
public final int columnSize;
public final char[] board;
protected Game(int rowSize, int columnSize) {
assert rowSize > 0 && columnSize > 0;
this.rowSize = rowSize;
this.columnSize = columnSize;
board = new char[rowSize * columnSize];
Arrays.fill(board, EMPTY);
}
protected Game(Game other) {
rowSize = other.rowSize;
columnSize = other.columnSize;
board = Arrays.copyOf(other.board, other.board.length);
}
public abstract Move[] getLegalMoves();
public abstract State play(Move move);
}

View File

@@ -0,0 +1,3 @@
package org.toop.game;
// TODO: Remove this, only used in ReversiCanvas. Needs to not
public record Move(int position, char value) {}

View File

@@ -1,27 +0,0 @@
package org.toop.game;
public abstract class TurnBasedGame extends Game {
public final int turns;
protected int currentTurn;
protected TurnBasedGame(int rowSize, int columnSize, int turns) {
super(rowSize, columnSize);
assert turns >= 2;
this.turns = turns;
}
protected TurnBasedGame(TurnBasedGame other) {
super(other);
turns = other.turns;
currentTurn = other.currentTurn;
}
protected void nextTurn() {
currentTurn = (currentTurn + 1) % turns;
}
public int getCurrentTurn() {
return currentTurn;
}
}

View File

@@ -0,0 +1,88 @@
package org.toop.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.threadBehaviour.AbstractThreadBehaviour;
import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.function.Consumer;
/**
* Handles local turn-based game logic at a fixed update rate.
* <p>
* Runs a separate thread that executes game turns at a fixed frequency (default 60 updates/sec),
* applying player moves, updating the game state, and dispatching UI events.
*/
public class LocalFixedRateThreadBehaviour<T extends TurnBasedGame<T>> extends AbstractThreadBehaviour<T> implements Runnable {
/**
* Creates a fixed-rate behaviour for a local turn-based game.
*
* @param game the game instance
*/
public LocalFixedRateThreadBehaviour(T game) {
super(game);
}
/** Starts the game loop thread if not already running. */
@Override
public void start() {
if (isRunning.compareAndSet(false, true)) {
new Thread(this).start();
}
}
/** Stops the game loop after the current iteration. */
@Override
public void stop() {
isRunning.set(false);
}
/**
* Main loop running at a fixed rate.
* <p>
* Fetches the current player's move, applies it to the game,
* updates the UI, and handles game-ending states.
*/
@Override
public void run() {
final int UPS = 1;
final long UPDATE_INTERVAL = 1_000_000_000L / UPS;
long nextUpdate = System.nanoTime();
while (isRunning.get()) {
long now = System.nanoTime();
if (now >= nextUpdate) {
nextUpdate += UPDATE_INTERVAL;
Player<T> currentPlayer = game.getPlayer(game.getCurrentTurn());
long move = currentPlayer.getMove(game.deepCopy());
PlayResult result = game.play(move);
updateUI();
GameState state = result.state();
switch (state) {
case WIN, DRAW -> {
isRunning.set(false);
new EventFlow().addPostEvent(GUIEvents.GameEnded.class, state == GameState.WIN, result.player()).postEvent();
}
case NORMAL, TURN_SKIPPED -> { /* continue */ }
default -> {
logger.error("Unexpected state {}", state);
isRunning.set(false);
throw new RuntimeException("Unknown state: " + state);
}
}
} else {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {}
}
}
}
}

View File

@@ -0,0 +1,76 @@
package org.toop.game.gameThreads;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour;
import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.function.Consumer;
/**
* Handles local turn-based game logic in its own thread.
* <p>
* Repeatedly gets the current player's move, applies it to the game,
* updates the UI, and stops when the game ends or {@link #stop()} is called.
*/
public class LocalThreadBehaviour<T extends TurnBasedGame<T>> extends AbstractThreadBehaviour<T> implements Runnable {
/**
* Creates a new behaviour for a local turn-based game.
*
* @param game the game instance
*/
public LocalThreadBehaviour(T game) {
super(game);
}
/** Starts the game loop in a new thread. */
@Override
public void start() {
if (isRunning.compareAndSet(false, true)) {
new Thread(this).start();
}
}
/** Stops the game loop after the current iteration. */
@Override
public void stop() {
isRunning.set(false);
}
/**
* Main game loop: gets the current player's move, applies it,
* updates the UI, and handles end-of-game states.
*/
@Override
public void run() {
while (isRunning.get()) {
Player<T> currentPlayer = game.getPlayer(game.getCurrentTurn());
long move = currentPlayer.getMove(game.deepCopy());
PlayResult result = game.play(move);
updateUI();
GameState state = result.state();
switch (state) {
case WIN, DRAW -> {
isRunning.set(false);
new EventFlow().addPostEvent(
GUIEvents.GameEnded.class,
state == GameState.WIN,
result.player()
).postEvent();
}
case NORMAL, TURN_SKIPPED -> { /* continue normally */ }
default -> {
logger.error("Unexpected state {}", state);
isRunning.set(false);
throw new RuntimeException("Unknown state: " + state);
}
}
}
}
}

View File

@@ -0,0 +1,84 @@
package org.toop.game.gameThreads;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour;
import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.game.players.OnlinePlayer;
/**
* Handles online multiplayer game logic.
* <p>
* Reacts to server events, sending moves and updating the game state
* for the local player while receiving moves from other players.
*/
public class OnlineThreadBehaviour<T extends TurnBasedGame<T>> extends AbstractThreadBehaviour<T> implements SupportsOnlinePlay {
/**
* Creates behaviour and sets the first local player
* (non-online player) from the given array.
*/
public OnlineThreadBehaviour(T game) {
super(game);
}
/** Finds the first non-online player in the array. */
private int getFirstNotOnlinePlayer(Player<T>[] players) {
for (int i = 0; i < players.length; i++) {
if (!(players[i] instanceof OnlinePlayer)) {
return i;
}
}
throw new RuntimeException("All players are online players");
}
/** Starts processing network events for the local player. */
@Override
public void start() {
isRunning.set(true);
}
/** Stops processing network events. */
@Override
public void stop() {
isRunning.set(false);
}
/**
* Called when the server notifies that it is the local player's turn.
* Sends the generated move back to the server.
*/
@Override
public void onYourTurn(long clientId) {
if (!isRunning.get()) return;
long move = game.getPlayer(game.getCurrentTurn()).getMove(game.deepCopy());
sendMove(clientId, move);
}
/**
* Handles a move received from the server for any player.
* Updates the game state and triggers a UI refresh.
*/
public void onMoveReceived(long move) {
if (!isRunning.get()) return;
game.play(move);
updateUI();
}
/**
* Handles the end of the game as notified by the server.
* Updates the UI to show a win or draw result for the local player.
*/
public void gameFinished(String condition) {
switch(condition.toUpperCase()){
case "WIN", "LOSS" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, true, game.getWinner()).postEvent();
case "DRAW" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, false, -1).postEvent();
default -> {
logger.error("Invalid condition");
throw new RuntimeException("Unknown condition");
}
}
}
}

View File

@@ -0,0 +1,40 @@
package org.toop.game.gameThreads;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.networking.events.NetworkEvents;
/**
* Online thread behaviour that adds a fixed delay before processing
* the local player's turn.
* <p>
* This is identical to {@link OnlineThreadBehaviour}, but inserts a
* short sleep before delegating to the base implementation.
*/
public class OnlineWithSleepThreadBehaviour<T extends TurnBasedGame<T>> extends OnlineThreadBehaviour<T> {
/**
* Creates the behaviour and forwards the players to the base class.
*
* @param game the online-capable turn-based game
*/
public OnlineWithSleepThreadBehaviour(T game) {
super(game);
}
/**
* Waits briefly before handling the "your turn" event.
*
* @param event the network event indicating it's this client's turn
*/
@Override
public void onYourTurn(long clientId) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.onYourTurn(clientId);
}
}

View File

@@ -0,0 +1,170 @@
package org.toop.game.games.reversi;
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.game.BitboardGame;
public class BitboardReversi extends BitboardGame<BitboardReversi> {
public record Score(int black, int white) {}
private final long notAFile = 0xfefefefefefefefeL;
private final long notHFile = 0x7f7f7f7f7f7f7f7fL;
public BitboardReversi(Player<BitboardReversi>[] players) {
super(8, 8, 2, players);
// Black (player 0)
setPlayerBitboard(0, (1L << (3 + 4 * 8)) | (1L << (4 + 3 * 8)));
// White (player 1)
setPlayerBitboard(1, (1L << (3 + 3 * 8)) | (1L << (4 + 4 * 8)));
}
public BitboardReversi(BitboardReversi other) {
super(other);
}
public long getLegalMoves() {
final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer());
long legalMoves = 0L;
// north & south
legalMoves |= computeMoves(player, opponent, 8, -1L);
legalMoves |= computeMoves(player, opponent, -8, -1L);
// east & west
legalMoves |= computeMoves(player, opponent, 1, notAFile);
legalMoves |= computeMoves(player, opponent, -1, notHFile);
// 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);
return legalMoves;
}
public long getFlips(long move) {
final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer());
long flips = 0L;
// north & south
flips |= computeFlips(move, player, opponent, 8, -1L);
flips |= computeFlips(move, player, opponent, -8, -1L);
// east & west
flips |= computeFlips(move, player, opponent, 1, notAFile);
flips |= computeFlips(move, player, opponent, -1, notHFile);
// 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);
return flips;
}
@Override
public BitboardReversi deepCopy() {return new BitboardReversi(this);}
public PlayResult play(long move) {
final long flips = getFlips(move);
long player = getPlayerBitboard(getCurrentPlayerIndex());
long opponent = getPlayerBitboard(getNextPlayer());
player |= move | flips;
opponent &= ~flips;
setPlayerBitboard(getCurrentPlayerIndex(), player);
setPlayerBitboard(getNextPlayer(), opponent);
nextTurn();
final long nextLegalMoves = getLegalMoves();
if (nextLegalMoves == 0) {
nextTurn();
final long skippedLegalMoves = getLegalMoves();
if (skippedLegalMoves == 0) {
int winner = getWinner();
if (winner == -1) {
return new PlayResult(GameState.DRAW, -1);
}
return new PlayResult(GameState.WIN, winner);
}
return new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex());
}
return new PlayResult(GameState.NORMAL, getCurrentPlayerIndex());
}
public Score getScore() {
return new Score(
Long.bitCount(getPlayerBitboard(0)),
Long.bitCount(getPlayerBitboard(1))
);
}
public int getWinner(){
final long black = getPlayerBitboard(0);
final long white = getPlayerBitboard(1);
final int blackCount = Long.bitCount(black);
final int whiteCount = Long.bitCount(white);
if (blackCount == whiteCount){
return -1;
}
else if (blackCount > whiteCount){
return 0;
}
else{
return 1;
}
}
private long computeMoves(long player, long opponent, int shift, long mask) {
long moves = shift(player, shift, mask) & opponent;
long captured = moves;
while (moves != 0) {
moves = shift(moves, shift, mask) & opponent;
captured |= moves;
}
long landing = shift(captured, shift, mask);
return landing & ~(player | opponent);
}
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;
}
}
private long shift(long bit, int shift, long mask) {
return shift > 0 ? (bit << shift) & mask : (bit >>> -shift) & mask;
}
}

View File

@@ -0,0 +1,103 @@
package org.toop.game.games.tictactoe;
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.game.BitboardGame;
public class BitboardTicTacToe extends BitboardGame<BitboardTicTacToe> {
private final long[] winningLines = {
0b111000000L, // top row
0b000111000L, // middle row
0b000000111L, // bottom row
0b100100100L, // left column
0b010010010L, // middle column
0b001001001L, // right column
0b100010001L, // diagonal
0b001010100L // anti-diagonal
};
public BitboardTicTacToe(Player<BitboardTicTacToe>[] players) {
super(3, 3, 2, players);
}
public BitboardTicTacToe(BitboardTicTacToe other) {
super(other);
}
public long getLegalMoves() {
final long xBitboard = getPlayerBitboard(0);
final long oBitboard = getPlayerBitboard(1);
final long taken = (xBitboard | oBitboard);
return (~taken) & 0x1ffL;
}
public int getWinner(){
return getCurrentPlayerIndex();
}
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());
}
// Move is legal, make move
long playerBitboard = getPlayerBitboard(getCurrentPlayerIndex());
playerBitboard |= move;
setPlayerBitboard(getCurrentPlayerIndex(), playerBitboard);
// Check if current player won
if (checkWin(playerBitboard)) {
return new PlayResult(GameState.WIN, getCurrentPlayerIndex());
}
// Proceed to next turn
nextTurn();
// Check for early draw
if (getLegalMoves() == 0L || checkEarlyDraw()) {
return new PlayResult(GameState.DRAW, -1);
}
// Nothing weird happened, continue on as normal
return new PlayResult(GameState.NORMAL, -1);
}
private boolean checkWin(long board) {
for (final long line : winningLines) {
if ((board & line) == line) {
return true;
}
}
return false;
}
private boolean checkEarlyDraw() {
final long xBitboard = getPlayerBitboard(0);
final long oBitboard = getPlayerBitboard(1);
final long taken = (xBitboard | oBitboard);
final long empty = (~taken) & 0x1FFL;
for (final long line : winningLines) {
if (((line & xBitboard) != 0 && (line & oBitboard) != 0)) {
continue;
}
if ((line & empty) != 0) {
return false;
}
}
return true;
}
@Override
public BitboardTicTacToe deepCopy() {
return new BitboardTicTacToe(this);
}
}

View File

@@ -1,19 +0,0 @@
package org.toop.game.othello;
import org.toop.game.TurnBasedGame;
public final class Othello extends TurnBasedGame {
Othello() {
super(8, 8, 2);
}
@Override
public Move[] getLegalMoves() {
return new Move[0];
}
@Override
public State play(Move move) {
return null;
}
}

View File

@@ -1,11 +0,0 @@
package org.toop.game.othello;
import org.toop.game.AI;
import org.toop.game.Game;
public final class OthelloAI extends AI<Othello> {
@Override
public Game.Move findBestMove(Othello game, int depth) {
return null;
}
}

View File

@@ -0,0 +1,55 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.player.*;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
/**
* Represents a player controlled by an AI in a game.
* <p>
* This player uses an {@link AbstractAI} instance to determine its moves. The generic
* parameter {@code T} specifies the type of {@link GameR} the AI can handle.
* </p>
*
* @param <T> the specific type of game this AI player can play
*/
public class ArtificialPlayer<T extends TurnBasedGame<T>> extends AbstractPlayer<T> {
/** The AI instance used to calculate moves. */
private final AI<T> ai;
/**
* Constructs a new ArtificialPlayer using the specified AI.
*
* @param ai the AI instance that determines moves for this player
*/
public ArtificialPlayer(AI<T> ai, String name) {
super(name);
this.ai = ai;
}
public ArtificialPlayer(ArtificialPlayer<T> other) {
super(other);
this.ai = other.ai.deepCopy();
}
/**
* Determines the next move for this player using its AI.
* <p>
* This method overrides {@link AbstractPlayer#getMove(GameR)}. Because the AI is
* typed to {@code T}, a runtime cast is required. It is the caller's
* responsibility to ensure that {@code gameCopy} is of type {@code T}.
* </p>
*
* @param gameCopy a copy of the current game state
* @return the integer representing the chosen move
* @throws ClassCastException if {@code gameCopy} is not of type {@code T}
*/
public long getMove(T gameCopy) {
return ai.getMove(gameCopy);
}
@Override
public ArtificialPlayer<T> deepCopy() {
return new ArtificialPlayer<T>(this);
}
}

View File

@@ -0,0 +1,86 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class LocalPlayer<T extends TurnBasedGame<T>> extends AbstractPlayer<T> {
// Future can be used with event system, IF unsubscribeAfterSuccess works...
// private CompletableFuture<Integer> LastMove = new CompletableFuture<>();
private CompletableFuture<Long> LastMove;
public LocalPlayer(String name) {
super(name);
}
public LocalPlayer(LocalPlayer<T> other) {
super(other);
}
@Override
public long getMove(T gameCopy) {
return getValidMove(gameCopy);
}
public void setMove(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(T gameCopy) {
LastMove = new CompletableFuture<>();
long move = 0;
try {
move = LastMove.get();
System.out.println(Long.toBinaryString(move));
} catch (InterruptedException | ExecutionException e) {
// TODO: Add proper logging.
e.printStackTrace();
}
return move;
}
protected long getValidMove(T 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;
}
@Override
public LocalPlayer<T> deepCopy() {
return new LocalPlayer<T>(this.getName());
}
/*public void register() {
// Listening to PlayerAttemptedMove
new EventFlow().listen(GUIEvents.PlayerAttemptedMove.class, event -> {
if (!LastMove.isDone()) {
LastMove.complete(event.move()); // complete the future
}
}, true); // auto-unsubscribe
}
// This blocks until the next move arrives
public int take() throws ExecutionException, InterruptedException {
int move = LastMove.get(); // blocking
LastMove = new CompletableFuture<>(); // reset for next move
return move;
}*/
}

View File

@@ -0,0 +1,165 @@
package org.toop.game.players;
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<T extends TurnBasedGame<T>> extends AbstractAI<T> {
private final int maxDepth;
private final Random random = new Random();
public MiniMaxAI(int depth) {
this.maxDepth = depth;
}
public MiniMaxAI(MiniMaxAI<T> other) {
this.maxDepth = other.maxDepth;
}
@Override
public MiniMaxAI<T> deepCopy() {
return new MiniMaxAI<>(this);
}
@Override
public long getMove(T 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);
T 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(T 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);
T 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 Game state
* @param aiPlayer AI's player index
* @return heuristic score
*/
private int evaluateBoard(T 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

@@ -0,0 +1,36 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
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>
*/
public class OnlinePlayer<T extends TurnBasedGame<T>> extends AbstractPlayer<T> {
/**
* 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.
*/
public OnlinePlayer(String name) {
super(name);
}
public OnlinePlayer(OnlinePlayer<T> other) {
super(other);
}
@Override
public Player<T> deepCopy() {
return new OnlinePlayer<>(this);
}
}

View File

@@ -0,0 +1,38 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.Random;
public class RandomAI<T extends TurnBasedGame<T>> extends AbstractAI<T> {
public RandomAI() {
super();
}
@Override
public RandomAI<T> deepCopy() {
return new RandomAI<T>();
}
@Override
public long getMove(T game) {
long legalMoves = game.getLegalMoves();
int move = new Random().nextInt(Long.bitCount(legalMoves));
return nthBitIndex(legalMoves, move);
}
public static long nthBitIndex(long bb, int n) {
while (bb != 0) {
int tz = Long.numberOfTrailingZeros(bb);
if (n == 0) {
return 1L << tz;
}
bb &= bb - 1; // clear the least significant 1
n--;
}
return 0L; // not enough 1s
}
}

View File

@@ -1,100 +0,0 @@
package org.toop.game.tictactoe;
import java.util.ArrayList;
import org.toop.game.TurnBasedGame;
public final class TicTacToe extends TurnBasedGame {
private int movesLeft;
public TicTacToe() {
super(3, 3, 2);
movesLeft = board.length;
}
public TicTacToe(TicTacToe other) {
super(other);
movesLeft = other.movesLeft;
}
@Override
public Move[] getLegalMoves() {
final ArrayList<Move> legalMoves = new ArrayList<>();
final char currentValue = getCurrentValue();
for (int i = 0; i < board.length; i++) {
if (board[i] == EMPTY) {
legalMoves.add(new Move(i, currentValue));
}
}
return legalMoves.toArray(new Move[0]);
}
@Override
public State play(Move move) {
assert move != null;
assert move.position() >= 0 && move.position() < board.length;
assert move.value() == getCurrentValue();
board[move.position()] = move.value();
movesLeft--;
if (checkForWin()) {
return State.WIN;
}
nextTurn();
if (movesLeft <= 2) {
if (movesLeft <= 0 || checkForEarlyDraw(this)) {
return State.DRAW;
}
}
return State.NORMAL;
}
private boolean checkForWin() {
// Horizontal
for (int i = 0; i < 3; i++) {
final int index = i * 3;
if (board[index] != EMPTY
&& board[index] == board[index + 1]
&& board[index] == board[index + 2]) {
return true;
}
}
// Vertical
for (int i = 0; i < 3; i++) {
if (board[i] != EMPTY && board[i] == board[i + 3] && board[i] == board[i + 6]) {
return true;
}
}
// B-Slash
if (board[0] != EMPTY && board[0] == board[4] && board[0] == board[8]) {
return true;
}
// F-Slash
return board[2] != EMPTY && board[2] == board[4] && board[2] == board[6];
}
private boolean checkForEarlyDraw(TicTacToe game) {
for (final Move move : game.getLegalMoves()) {
final TicTacToe copy = new TicTacToe(game);
if (copy.play(move) == State.WIN || !checkForEarlyDraw(copy)) {
return false;
}
}
return true;
}
private char getCurrentValue() {
return currentTurn == 0 ? 'X' : 'O';
}
}

View File

@@ -1,68 +0,0 @@
package org.toop.game.tictactoe;
import org.toop.game.AI;
import org.toop.game.Game;
public final class TicTacToeAI extends AI<TicTacToe> {
@Override
public Game.Move findBestMove(TicTacToe game, int depth) {
assert game != null;
assert depth >= 0;
final Game.Move[] legalMoves = game.getLegalMoves();
if (legalMoves.length <= 0) {
return null;
}
if (legalMoves.length == 9) {
return switch ((int)(Math.random() * 4)) {
case 1 -> legalMoves[2];
case 2 -> legalMoves[6];
case 3 -> legalMoves[8];
default -> legalMoves[0];
};
}
int bestScore = -depth;
Game.Move bestMove = null;
for (final Game.Move move : legalMoves) {
final int score = getMoveScore(game, depth, move, true);
if (score > bestScore) {
bestMove = move;
bestScore = score;
}
}
return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)];
}
private int getMoveScore(TicTacToe game, int depth, Game.Move move, boolean maximizing) {
final TicTacToe copy = new TicTacToe(game);
final Game.State state = copy.play(move);
switch (state) {
case Game.State.DRAW: return 0;
case Game.State.WIN: return maximizing? depth + 1 : -depth - 1;
}
if (depth <= 0) {
return 0;
}
final Game.Move[] legalMoves = copy.getLegalMoves();
int score = maximizing? depth + 1 : -depth - 1;
for (final Game.Move next : legalMoves) {
if (maximizing) {
score = Math.min(score, getMoveScore(copy, depth - 1, next, false));
} else {
score = Math.max(score, getMoveScore(copy, depth - 1, next, true));
}
}
return score;
}
}

View File

@@ -1,81 +0,0 @@
package org.toop.game.tictactoe;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.toop.game.Game;
class TicTacToeAITest {
private TicTacToe game;
private TicTacToeAI ai;
@BeforeEach
void setup() {
game = new TicTacToe();
ai = new TicTacToeAI();
}
@Test
void testBestMove_returnWinningMoveWithDepth1() {
// X X -
// O O -
// - - -
game.play(new Game.Move(0, 'X'));
game.play(new Game.Move(3, 'O'));
game.play(new Game.Move(1, 'X'));
game.play(new Game.Move(4, 'O'));
final Game.Move move = ai.findBestMove(game, 1);
assertNotNull(move);
assertEquals('X', move.value());
assertEquals(2, move.position());
}
@Test
void testBestMove_blockOpponentWinDepth1() {
// - - -
// O - -
// X X -
game.play(new Game.Move(6, 'X'));
game.play(new Game.Move(3, 'O'));
game.play(new Game.Move(7, 'X'));
final Game.Move move = ai.findBestMove(game, 1);
assertNotNull(move);
assertEquals('O', move.value());
assertEquals(8, move.position());
}
@Test
void testBestMove_preferCornerOnEmpty() {
final Game.Move move = ai.findBestMove(game, 0);
assertNotNull(move);
assertEquals('X', move.value());
assertTrue(Set.of(0, 2, 6, 8).contains(move.position()));
}
@Test
void testBestMove_findBestMoveDraw() {
// O X -
// - O X
// X O X
game.play(new Game.Move(1, 'X'));
game.play(new Game.Move(0, 'O'));
game.play(new Game.Move(5, 'X'));
game.play(new Game.Move(4, 'O'));
game.play(new Game.Move(6, 'X'));
game.play(new Game.Move(7, 'O'));
game.play(new Game.Move(8, 'X'));
final Game.Move move = ai.findBestMove(game, game.getLegalMoves().length);
assertNotNull(move);
assertEquals('O', move.value());
assertEquals(2, move.position());
}
}