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

# Conflicts:
#	app/src/main/java/org/toop/Main.java
#	app/src/main/java/org/toop/app/App.java
#	app/src/main/java/org/toop/app/Server.java
#	app/src/main/java/org/toop/app/canvas/BitGameCanvas.java
#	app/src/main/java/org/toop/app/canvas/GameCanvas.java
#	app/src/main/java/org/toop/app/canvas/ReversiBitCanvas.java
#	app/src/main/java/org/toop/app/canvas/TicTacToeBitCanvas.java
#	app/src/main/java/org/toop/app/gameControllers/GenericGameController.java
#	app/src/main/java/org/toop/app/gameControllers/ReversiBitController.java
#	app/src/main/java/org/toop/app/gameControllers/TicTacToeBitController.java
#	app/src/main/java/org/toop/app/widget/Primitive.java
#	app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java
#	app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java
#	app/src/main/java/org/toop/app/widget/complex/ViewWidget.java
#	app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java
#	app/src/main/java/org/toop/app/widget/popup/EscapePopup.java
#	app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java
#	app/src/main/java/org/toop/app/widget/tutorial/BaseTutorialWidget.java
#	app/src/main/java/org/toop/app/widget/tutorial/ShowEnableTutorialWidget.java
#	app/src/main/java/org/toop/app/widget/view/GameView.java
#	app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
#	app/src/main/java/org/toop/app/widget/view/LocalView.java
#	app/src/main/java/org/toop/app/widget/view/MainView.java
#	app/src/main/java/org/toop/app/widget/view/OnlineView.java
#	app/src/main/java/org/toop/app/widget/view/ServerView.java
#	framework/pom.xml
#	framework/src/main/java/org/toop/framework/gameFramework/GameState.java
#	framework/src/main/java/org/toop/framework/gameFramework/controller/GameController.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/AbstractThreadBehaviour.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/ThreadBehaviour.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/player/AI.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractAI.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java
#	framework/src/main/java/org/toop/framework/gameFramework/model/player/Player.java
#	framework/src/main/java/org/toop/framework/networking/NetworkingClient.java
#	framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java
#	framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java
#	framework/src/main/java/org/toop/framework/networking/NetworkingInitializationException.java
#	framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java
#	framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java
#	framework/src/main/java/org/toop/framework/networking/connection/exceptions/NetworkingInitializationException.java
#	framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java
#	framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java
#	framework/src/main/java/org/toop/framework/networking/exceptions/NetworkingInitializationException.java
#	framework/src/main/java/org/toop/framework/networking/handlers/NetworkingGameClientHandler.java
#	framework/src/test/java/org/toop/framework/networking/NetworkingClientManagerTest.java
#	framework/src/test/java/org/toop/framework/networking/events/NetworkEventsTest.java
This commit is contained in:
Bas de Jong
2026-01-09 19:22:14 +01:00
33 changed files with 1393 additions and 0 deletions

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

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

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

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