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>
This commit is contained in:
Bas Antonius de Jong
2026-01-07 16:15:49 +01:00
committed by GitHub
parent 230f7480e4
commit c64a2e2c65
19 changed files with 1226 additions and 305 deletions

View File

@@ -5,5 +5,49 @@ import org.toop.app.App;
public final class Main { public final class Main {
static void main(String[] args) { static void main(String[] args) {
App.run(args); App.run(args);
// testMCTS(10);
} }
// Voor onderzoek
// private static void testMCTS(int games) {
// var random = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(), "Random AI");
// var v1 = new ArtificialPlayer<>(new MCTSAI<BitboardTicTacToe>(10), "MCTS V1 AI");
// var v2 = new ArtificialPlayer<>(new MCTSAI2<BitboardTicTacToe>(10), "MCTS V2 AI");
// var v2_2 = new ArtificialPlayer<>(new MCTSAI2<BitboardTicTacToe>(100), "MCTS V2_2 AI");
// var v3 = new ArtificialPlayer<>(new MCTSAI3<BitboardTicTacToe>(10), "MCTS V3 AI");
// testAI(games, new Player[]{ v1, v2 });
// // testAI(games, new Player[]{ v1, v3 });
// // testAI(games, new Player[]{ random, v3 });
// // testAI(games, new Player[]{ v2, v3 });
// testAI(games, new Player[]{ v2, v3 });
// // testAI(games, new Player[]{ v3, v2 });
// }
// private static void testAI(int games, Player<BitboardReversi>[] ais) {
// int wins = 0;
// int ties = 0;
// for (int i = 0; i < games; i++) {
// final BitboardReversi match = new BitboardReversi(ais);
// while (!match.isTerminal()) {
// final int currentAI = match.getCurrentTurn();
// final long move = ais[currentAI].getMove(match);
// match.play(move);
// }
// if (match.getWinner() < 0) {
// ties++;
// continue;
// }
// wins += match.getWinner() == 0? 1 : 0;
// }
// System.out.printf("Out of %d games, %s won %d -- tied %d -- lost %d, games against %s\n", games, ais[0].getName(), wins, ties, games - wins - ties, ais[1].getName());
// System.out.printf("Average win rate was: %.2f\n\n", wins / (float)games);
// }
} }

View File

@@ -11,17 +11,15 @@ import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.popup.SendChallengePopup; import org.toop.app.widget.popup.SendChallengePopup;
import org.toop.app.widget.view.ServerView; import org.toop.app.widget.view.ServerView;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.game.players.LocalPlayer; import org.toop.framework.game.players.OnlinePlayer;
import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.networking.connection.clients.TournamentNetworkingClient; import org.toop.framework.networking.connection.clients.TournamentNetworkingClient;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.connection.events.NetworkEvents;
import org.toop.framework.networking.connection.types.NetworkingConnector; 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.framework.networking.server.gateway.NettyGatewayServer;
import org.toop.game.players.LocalPlayer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.util.List; import java.util.List;

View File

@@ -16,7 +16,7 @@ import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehavio
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.gameFramework.view.GUIEvents; import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.connection.events.NetworkEvents;
import org.toop.framework.game.players.LocalPlayer; import org.toop.game.players.LocalPlayer;
public class GenericGameController implements GameController { public class GenericGameController implements GameController {
protected final EventFlow eventFlow = new EventFlow(); protected final EventFlow eventFlow = new EventFlow();
@@ -55,9 +55,7 @@ public class GenericGameController implements GameController {
// Listen to updates // Listen to updates
eventFlow eventFlow
.listen(GUIEvents.GameEnded.class, this::onGameFinish, false) .listen(GUIEvents.GameEnded.class, this::onGameFinish, false)
.listen(GUIEvents.PlayerAttemptedMove.class, event -> { .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setLastMove(event.move());}}, false);
if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}
}, false);
} }
public void start(){ public void start(){

View File

@@ -7,14 +7,16 @@ import org.toop.app.gameControllers.TicTacToeBitController;
import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.players.ArtificialPlayer; 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.Primitive;
import org.toop.app.widget.complex.PlayerInfoWidget; import org.toop.app.widget.complex.PlayerInfoWidget;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.tutorial.*; import org.toop.app.widget.tutorial.*;
import org.toop.framework.game.players.ai.MiniMaxAI; import org.toop.game.players.LocalPlayer;
import org.toop.framework.game.players.ai.RandomAI; import org.toop.game.players.ai.MCTSAI;
import org.toop.game.players.ai.MCTSAI2;
import org.toop.game.players.ai.MCTSAI3;
import org.toop.game.players.ai.MiniMaxAI;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@@ -52,7 +54,7 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[0].isHuman) { if (information.players[0].isHuman) {
players[0] = new LocalPlayer(information.players[0].name); players[0] = new LocalPlayer(information.players[0].name);
} else { } else {
players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); players[0] = new ArtificialPlayer(new MCTSAI(100), "MCTS AI");
} }
if (information.players[1].isHuman) { if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name); players[1] = new LocalPlayer(information.players[1].name);
@@ -80,12 +82,13 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[0].isHuman) { if (information.players[0].isHuman) {
players[0] = new LocalPlayer(information.players[0].name); players[0] = new LocalPlayer(information.players[0].name);
} else { } else {
players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); // players[0] = new ArtificialPlayer(new RandomAI<BitboardReversi>(), "Random AI");
players[0] = new ArtificialPlayer(new MCTSAI3(50), "MCTS V3 AI");
} }
if (information.players[1].isHuman) { if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name); players[1] = new LocalPlayer(information.players[1].name);
} else { } else {
players[1] = new ArtificialPlayer(new MiniMaxAI(6), "MiniMax"); players[1] = new ArtificialPlayer(new MCTSAI2(50), "MCTS V2 AI");
} }
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) { if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) {
new ShowEnableTutorialWidget( new ShowEnableTutorialWidget(

View File

@@ -1,5 +1,7 @@
package org.toop.framework.game; 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.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
@@ -10,6 +12,8 @@ public abstract class BitboardGame implements TurnBasedGame {
private final int columnSize; private final int columnSize;
private final int rowSize; private final int rowSize;
protected PlayResult state;
private Player[] players; private Player[] players;
// long is 64 bits. Every game has a limit of 64 cells maximum. // long is 64 bits. Every game has a limit of 64 cells maximum.
@@ -21,7 +25,11 @@ public abstract class BitboardGame implements TurnBasedGame {
this.columnSize = columnSize; this.columnSize = columnSize;
this.rowSize = rowSize; this.rowSize = rowSize;
this.playerCount = playerCount; this.playerCount = playerCount;
this.state = new PlayResult(GameState.NORMAL, -1);
this.playerBitboard = new long[playerCount]; this.playerBitboard = new long[playerCount];
Arrays.fill(playerBitboard, 0L);
} }
@Override @Override
@@ -35,6 +43,8 @@ public abstract class BitboardGame implements TurnBasedGame {
this.columnSize = other.columnSize; this.columnSize = other.columnSize;
this.rowSize = other.rowSize; this.rowSize = other.rowSize;
this.playerCount = other.playerCount; this.playerCount = other.playerCount;
this.state = other.state;
this.playerBitboard = other.playerBitboard.clone(); this.playerBitboard = other.playerBitboard.clone();
this.currentTurn = other.currentTurn; this.currentTurn = other.currentTurn;
this.players = Arrays.stream(other.players) this.players = Arrays.stream(other.players)
@@ -80,7 +90,15 @@ public abstract class BitboardGame implements TurnBasedGame {
return players[getCurrentPlayerIndex()]; 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 long[] getBoard() {return this.playerBitboard;}

View File

@@ -32,47 +32,227 @@ public class BitboardReversi extends BitboardGame {
} }
public long getLegalMoves() { public long getLegalMoves() {
long legalMoves = 0L;
final long player = getPlayerBitboard(getCurrentPlayerIndex()); final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer()); final long opponent = getPlayerBitboard(getNextPlayer());
long legalMoves = 0L; final long empty = ~(player | opponent);
// north & south long mask;
legalMoves |= computeMoves(player, opponent, 8, -1L); long direction;
legalMoves |= computeMoves(player, opponent, -8, -1L);
// east & west // north
legalMoves |= computeMoves(player, opponent, 1, notAFile); mask = opponent;
legalMoves |= computeMoves(player, opponent, -1, notHFile); direction = (player << 8) & mask;
// north-east & north-west & south-east & south-west direction |= (direction << 8) & mask;
legalMoves |= computeMoves(player, opponent, 9, notAFile); direction |= (direction << 8) & mask;
legalMoves |= computeMoves(player, opponent, 7, notHFile); direction |= (direction << 8) & mask;
legalMoves |= computeMoves(player, opponent, -7, notAFile); direction |= (direction << 8) & mask;
legalMoves |= computeMoves(player, opponent, -9, notHFile); 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; return legalMoves;
} }
public long getFlips(long move) { public long getFlips(long move) {
long flips = 0L;
final long player = getPlayerBitboard(getCurrentPlayerIndex()); final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer()); final long opponent = getPlayerBitboard(getNextPlayer());
long flips = 0L; long mask;
long direction;
// north & south // north
flips |= computeFlips(move, player, opponent, 8, -1L); mask = opponent;
flips |= computeFlips(move, player, opponent, -8, -1L); direction = (move << 8) & mask;
// east & west direction |= (direction << 8) & mask;
flips |= computeFlips(move, player, opponent, 1, notAFile); direction |= (direction << 8) & mask;
flips |= computeFlips(move, player, opponent, -1, notHFile); direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
direction |= (direction << 8) & mask;
// north-east & north-west & south-east & south-west if (((direction << 8) & player) != 0) {
flips |= computeFlips(move, player, opponent, 9, notAFile); flips |= direction;
flips |= computeFlips(move, player, opponent, 7, notHFile); }
flips |= computeFlips(move, player, opponent, -7, notAFile);
flips |= computeFlips(move, player, opponent, -9, notHFile); // 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; return flips;
} }
@@ -105,16 +285,20 @@ public class BitboardReversi extends BitboardGame {
int winner = getWinner(); int winner = getWinner();
if (winner == -1) { 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() { public Score getScore() {
@@ -141,35 +325,4 @@ public class BitboardReversi extends BitboardGame {
return 1; 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

@@ -45,7 +45,8 @@ public class BitboardTicTacToe extends BitboardGame {
public PlayResult play(long move) { public PlayResult play(long move) {
// Player loses if move is invalid // Player loses if move is invalid
if ((move & getLegalMoves()) == 0 || Long.bitCount(move) != 1){ 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 // Move is legal, make move
@@ -56,7 +57,8 @@ public class BitboardTicTacToe extends BitboardGame {
// Check if current player won // Check if current player won
if (checkWin(playerBitboard)) { if (checkWin(playerBitboard)) {
return new PlayResult(GameState.WIN, getCurrentPlayerIndex()); state = new PlayResult(GameState.WIN, getCurrentPlayerIndex());
return state;
} }
// Proceed to next turn // Proceed to next turn
@@ -65,11 +67,13 @@ public class BitboardTicTacToe extends BitboardGame {
// Check for early draw // Check for early draw
if (getLegalMoves() == 0L || checkEarlyDraw()) { 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 // 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) { private boolean checkWin(long board) {

View File

@@ -44,10 +44,15 @@ public class ArtificialPlayer extends AbstractPlayer {
* @return the integer representing the chosen move * @return the integer representing the chosen move
* @throws ClassCastException if {@code gameCopy} is not of type {@code T} * @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); return ai.getMove(gameCopy);
} }
/**
* Creates a deep copy of this AI player.
*
* @return a copy of this player
*/
@Override @Override
public ArtificialPlayer deepCopy() { public ArtificialPlayer deepCopy() {
return new ArtificialPlayer(this); return new ArtificialPlayer(this);

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.players; package org.toop.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer; import org.toop.framework.gameFramework.model.player.AbstractPlayer;
@@ -6,65 +6,83 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; 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 { 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) { public LocalPlayer(String name) {
super(name); super(name);
} }
/**
* Creates a copy of another local player.
*
* @param other the player to copy
*/
public LocalPlayer(LocalPlayer other) { public LocalPlayer(LocalPlayer other) {
super(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 @Override
public long getMove(TurnBasedGame gameCopy) { protected long determineMove(TurnBasedGame gameCopy) {
return getValidMove(gameCopy); long legalMoves = gameCopy.getLegalMoves();
long move;
do {
move = getLastMove();
} while ((legalMoves & move) == 0);
return move;
} }
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); LastMove.complete(move);
} }
// TODO: helper function, would like to replace to get rid of this method /**
public static boolean contains(int[] array, int value){ * Waits for the next move from the player.
for (int i : array) if (i == value) return true; *
return false; * @return the chosen move or 0 if interrupted
} */
private long getLastMove() {
private long getMove2(TurnBasedGame gameCopy) { LastMove = new CompletableFuture<>(); // Reset the future
LastMove = new CompletableFuture<>();
long move = 0;
try { try {
move = LastMove.get(); return LastMove.get();
System.out.println(Long.toBinaryString(move)); } catch (ExecutionException | InterruptedException e) {
} catch (InterruptedException | ExecutionException e) { return 0;
// TODO: Add proper logging.
e.printStackTrace();
} }
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 @Override
public LocalPlayer deepCopy() { public LocalPlayer deepCopy() {
return new LocalPlayer(this.getName()); return new LocalPlayer(this);
} }
/*public void register() { /*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; import org.toop.framework.gameFramework.model.player.Player;
/** /**
* Represents a player controlled remotely or over a network. * Represents a player that participates online.
* <p> *
* This class extends {@link AbstractPlayer} and can be used to implement game logic * @param <T> the type of turn-based game
* 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 extends AbstractPlayer { public class OnlinePlayer extends AbstractPlayer {
/** /**
* Constructs a new OnlinePlayer. * Creates a new online player with the given name.
* <p> *
* Currently, no additional initialization is performed. Subclasses or * @param name the name of the player
* future implementations should provide mechanisms to receive moves from
* an external source.
*/ */
public OnlinePlayer(String name) { public OnlinePlayer(String name) {
super(name); super(name);
} }
/**
* Creates a copy of another online player.
*
* @param other the player to copy
*/
public OnlinePlayer(OnlinePlayer other) { public OnlinePlayer(OnlinePlayer other) {
super(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 @Override
public Player deepCopy() { public Player deepCopy() {
return new OnlinePlayer(this); return new OnlinePlayer(this);

View File

@@ -27,7 +27,7 @@ public class ServerPlayer extends AbstractPlayer {
} }
@Override @Override
public long getMove(TurnBasedGame game) { public long determineMove(TurnBasedGame game) {
lastMove = new CompletableFuture<>(); lastMove = new CompletableFuture<>();
System.out.println("Sending yourturn"); System.out.println("Sending yourturn");
client.send("SVR GAME YOURTURN {TURNMESSAGE: \"<bericht voor deze beurt>\"}\n"); client.send("SVR GAME YOURTURN {TURNMESSAGE: \"<bericht voor deze beurt>\"}\n");

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,6 @@ public interface TurnBasedGame extends DeepCopyable<TurnBasedGame> {
int getWinner(); int getWinner();
long getLegalMoves(); long getLegalMoves();
PlayResult play(long move); PlayResult play(long move);
PlayResult getState();
boolean isTerminal();
} }

View File

@@ -40,12 +40,29 @@ public abstract class AbstractPlayer implements Player {
* @return an integer representing the chosen move * @return an integer representing the chosen move
* @throws UnsupportedOperationException if the method is not overridden * @throws UnsupportedOperationException if the method is not overridden
*/ */
public long getMove(TurnBasedGame gameCopy) { public final long getMove(TurnBasedGame game) {
logger.error("Method getMove not implemented."); return determineMove(game.deepCopy());
throw new UnsupportedOperationException("Not supported yet.");
} }
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; return this.name;
} }
} }

View File

@@ -0,0 +1,193 @@
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.Random;
public class MCTSAI extends AbstractAI {
private static class Node {
public TurnBasedGame state;
public long move;
public Node parent;
public int expanded;
public Node[] children;
public int visits;
public float value;
public Node(TurnBasedGame state, long move, Node parent) {
this.state = state;
this.move = move;
this.parent = parent;
this.expanded = 0;
this.children = new Node[Long.bitCount(state.getLegalMoves())];
this.visits = 0;
this.value = 0.0f;
}
public Node(TurnBasedGame state) {
this(state, 0L, null);
}
public boolean isFullyExpanded() {
return expanded >= children.length;
}
float calculateUCT() {
float exploitation = visits <= 0? 0 : value / visits;
float exploration = 1.41f * (float)(Math.sqrt(Math.log(visits) / visits));
return exploitation + exploration;
}
public Node bestUCTChild() {
int bestChildIndex = -1;
float bestScore = Float.NEGATIVE_INFINITY;
for (int i = 0; i < expanded; i++) {
final float score = calculateUCT();
if (score > bestScore) {
bestChildIndex = i;
bestScore = score;
}
}
return bestChildIndex >= 0? children[bestChildIndex] : this;
}
}
private final int milliseconds;
public MCTSAI(int milliseconds) {
this.milliseconds = milliseconds;
}
public MCTSAI(MCTSAI other) {
this.milliseconds = other.milliseconds;
}
@Override
public MCTSAI deepCopy() {
return new MCTSAI(this);
}
@Override
public long getMove(TurnBasedGame game) {
Node root = new Node(game.deepCopy());
long endTime = System.currentTimeMillis() + milliseconds;
while (System.currentTimeMillis() <= endTime) {
Node node = selection(root);
long legalMoves = node.state.getLegalMoves();
if (legalMoves != 0) {
node = expansion(node, legalMoves);
}
float result = 0.0f;
if (node.state.getLegalMoves() != 0) {
result = simulation(node.state, game.getCurrentTurn());
}
backPropagation(node, result);
}
int mostVisitedIndex = -1;
int mostVisits = -1;
for (int i = 0; i < root.expanded; i++) {
if (root.children[i].visits > mostVisits) {
mostVisitedIndex = i;
mostVisits = root.children[i].visits;
}
}
return mostVisitedIndex != -1? root.children[mostVisitedIndex].move : randomSetBit(game.getLegalMoves());
}
private Node selection(Node node) {
while (node.state.getLegalMoves() != 0L && node.isFullyExpanded()) {
node = node.bestUCTChild();
}
return node;
}
private Node expansion(Node node, long legalMoves) {
for (int i = 0; i < node.expanded; i++) {
legalMoves &= ~node.children[i].move;
}
if (legalMoves == 0L) {
return node;
}
long move = randomSetBit(legalMoves);
TurnBasedGame copy = node.state.deepCopy();
copy.play(move);
Node newlyExpanded = new Node(copy, move, node);
node.children[node.expanded] = newlyExpanded;
node.expanded++;
return newlyExpanded;
}
private float simulation(TurnBasedGame state, int playerIndex) {
TurnBasedGame copy = state.deepCopy();
long legalMoves = copy.getLegalMoves();
PlayResult result = null;
while (legalMoves != 0) {
result = copy.play(randomSetBit(legalMoves));
legalMoves = copy.getLegalMoves();
}
if (result.state() == GameState.WIN) {
if (result.player() == playerIndex) {
return 1.0f;
}
return -1.0f;
}
return -0.2f;
}
private void backPropagation(Node node, float value) {
while (node != null) {
node.visits++;
node.value += value;
node = node.parent;
}
}
public static long randomSetBit(long value) {
Random random = new Random();
int count = Long.bitCount(value);
int target = random.nextInt(count);
while (true) {
int bit = Long.numberOfTrailingZeros(value);
if (target == 0) {
return 1L << bit;
}
value &= value - 1;
target--;
}
}
}

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.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI; import org.toop.framework.gameFramework.model.player.AbstractAI;