other) {
this.name = other.name;
}
+
/**
- * Determines the next move based on the provided game state.
+ * Gets the player's move for the given game state.
+ * A deep copy is provided so the player cannot modify the real state.
*
- * The default implementation throws an {@link UnsupportedOperationException},
- * indicating that concrete subclasses must override this method to provide
- * actual move logic.
- *
+ * This method uses the Template Method Pattern: it defines the fixed
+ * algorithm and delegates the variable part to {@link #determineMove(T)}.
*
- * @param gameCopy a snapshot of the current game state
- * @return an integer representing the chosen move
- * @throws UnsupportedOperationException if the method is not overridden
+ * @param game the current game
+ * @return the chosen move
*/
- public long getMove(T gameCopy) {
- logger.error("Method getMove not implemented.");
- throw new UnsupportedOperationException("Not supported yet.");
+ public final long getMove(T game) {
+ return determineMove(game.deepCopy());
}
- public String getName(){
+
+ /**
+ * Determines the player's move using a safe copy of the game.
+ *
+ * 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(T gameCopy);
+
+
+ /**
+ * Returns the player's name.
+ *
+ * @return the name
+ */
+ public String getName() {
return this.name;
}
}
diff --git a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java
index 418cbed..d141503 100644
--- a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java
+++ b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java
@@ -44,7 +44,7 @@ public class ArtificialPlayer> extends AbstractPlayer
* @return the integer representing the chosen move
* @throws ClassCastException if {@code gameCopy} is not of type {@code T}
*/
- public long getMove(T gameCopy) {
+ protected long determineMove(T gameCopy) {
return ai.getMove(gameCopy);
}
diff --git a/game/src/main/java/org/toop/game/players/LocalPlayer.java b/game/src/main/java/org/toop/game/players/LocalPlayer.java
index 8f3b94d..f5c2daa 100644
--- a/game/src/main/java/org/toop/game/players/LocalPlayer.java
+++ b/game/src/main/java/org/toop/game/players/LocalPlayer.java
@@ -22,7 +22,7 @@ public class LocalPlayer> extends AbstractPlayer {
}
@Override
- public long getMove(T gameCopy) {
+ protected long determineMove(T gameCopy) {
return getValidMove(gameCopy);
}
diff --git a/game/src/main/java/org/toop/game/players/MiniMaxAI.java b/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java
similarity index 99%
rename from game/src/main/java/org/toop/game/players/MiniMaxAI.java
rename to game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java
index 440bb50..8ae270e 100644
--- a/game/src/main/java/org/toop/game/players/MiniMaxAI.java
+++ b/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java
@@ -1,4 +1,4 @@
-package org.toop.game.players;
+package org.toop.game.players.ai;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
diff --git a/game/src/main/java/org/toop/game/players/RandomAI.java b/game/src/main/java/org/toop/game/players/ai/RandomAI.java
similarity index 96%
rename from game/src/main/java/org/toop/game/players/RandomAI.java
rename to game/src/main/java/org/toop/game/players/ai/RandomAI.java
index 2d0fe02..1c4223a 100644
--- a/game/src/main/java/org/toop/game/players/RandomAI.java
+++ b/game/src/main/java/org/toop/game/players/ai/RandomAI.java
@@ -1,4 +1,4 @@
-package org.toop.game.players;
+package org.toop.game.players.ai;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI;
From 1ae79daef090631f6802c68deae43065f4d08e32 Mon Sep 17 00:00:00 2001
From: Stef <48526421+StefBuwalda@users.noreply.github.com>
Date: Wed, 10 Dec 2025 13:17:01 +0100
Subject: [PATCH 07/11] Added documentation to player classes and improved
method names (#295)
---
.../GenericGameController.java | 2 +-
.../toop/game/players/ArtificialPlayer.java | 38 +++----
.../org/toop/game/players/LocalPlayer.java | 105 +++++++++---------
.../org/toop/game/players/OnlinePlayer.java | 37 ++++--
4 files changed, 99 insertions(+), 83 deletions(-)
diff --git a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java
index 2c3ad49..70b256a 100644
--- a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java
+++ b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java
@@ -55,7 +55,7 @@ public class GenericGameController> implements GameCo
// Listen to updates
eventFlow
.listen(GUIEvents.GameEnded.class, this::onGameFinish, false)
- .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}}, false);
+ .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setLastMove(event.move());}}, false);
}
public void start(){
diff --git a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java
index d141503..c3df033 100644
--- a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java
+++ b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java
@@ -4,52 +4,52 @@ 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.
- *
- * 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.
- *
+ * Represents a player controlled by an AI.
*
- * @param the specific type of game this AI player can play
+ * @param the type of turn-based game
*/
public class ArtificialPlayer> extends AbstractPlayer {
- /** The AI instance used to calculate moves. */
private final AI ai;
/**
- * Constructs a new ArtificialPlayer using the specified AI.
+ * Creates a new AI-controlled player.
*
- * @param ai the AI instance that determines moves for this player
+ * @param ai the AI controlling this player
+ * @param name the player's name
*/
public ArtificialPlayer(AI ai, String name) {
super(name);
this.ai = ai;
}
+ /**
+ * Creates a copy of another AI-controlled player.
+ *
+ * @param other the player to copy
+ */
public ArtificialPlayer(ArtificialPlayer other) {
super(other);
this.ai = other.ai.deepCopy();
}
/**
- * Determines the next move for this player using its AI.
- *
- * 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}.
- *
+ * Determines the player's move using the AI.
*
- * @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}
+ * @param gameCopy a copy of the current game
+ * @return the move chosen by the AI
*/
protected long determineMove(T gameCopy) {
return ai.getMove(gameCopy);
}
+ /**
+ * Creates a deep copy of this AI player.
+ *
+ * @return a copy of this player
+ */
@Override
public ArtificialPlayer deepCopy() {
- return new ArtificialPlayer(this);
+ return new ArtificialPlayer<>(this);
}
}
diff --git a/game/src/main/java/org/toop/game/players/LocalPlayer.java b/game/src/main/java/org/toop/game/players/LocalPlayer.java
index f5c2daa..4ae135b 100644
--- a/game/src/main/java/org/toop/game/players/LocalPlayer.java
+++ b/game/src/main/java/org/toop/game/players/LocalPlayer.java
@@ -2,85 +2,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;
+/**
+ * Represents a local player who provides moves manually.
+ *
+ * @param the type of turn-based game
+ */
public class LocalPlayer> extends AbstractPlayer {
- // Future can be used with event system, IF unsubscribeAfterSuccess works...
- // private CompletableFuture LastMove = new CompletableFuture<>();
- private CompletableFuture LastMove;
+ private CompletableFuture LastMove = new CompletableFuture<>();
+ /**
+ * Creates a new local player with the given name.
+ *
+ * @param name the player's name
+ */
public LocalPlayer(String name) {
super(name);
}
+ /**
+ * Creates a copy of another local player.
+ *
+ * @param other the player to copy
+ */
public LocalPlayer(LocalPlayer other) {
super(other);
+ this.LastMove = other.LastMove;
}
+ /**
+ * Waits for and returns the player's next legal move.
+ *
+ * @param gameCopy a copy of the current game
+ * @return the chosen move
+ */
@Override
protected long determineMove(T 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);
}
- // 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;
+ /**
+ * Waits for the next move from the player.
+ *
+ * @return the chosen move or 0 if interrupted
+ */
+ private long getLastMove() {
+ LastMove = new CompletableFuture<>(); // Reset the future
try {
- move = LastMove.get();
- System.out.println(Long.toBinaryString(move));
- } catch (InterruptedException | ExecutionException e) {
- // TODO: Add proper logging.
- e.printStackTrace();
+ return LastMove.get();
+ } catch (ExecutionException | InterruptedException e) {
+ return 0;
}
- return move;
- }
-
- protected long getValidMove(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;
}
+ /**
+ * Creates a deep copy of this local player.
+ *
+ * @return a copy of this player
+ */
@Override
public LocalPlayer deepCopy() {
- return new LocalPlayer(this.getName());
+ return new LocalPlayer<>(this);
}
-
- /*public void register() {
- // 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;
- }*/
}
diff --git a/game/src/main/java/org/toop/game/players/OnlinePlayer.java b/game/src/main/java/org/toop/game/players/OnlinePlayer.java
index 9f011c0..fe6b19d 100644
--- a/game/src/main/java/org/toop/game/players/OnlinePlayer.java
+++ b/game/src/main/java/org/toop/game/players/OnlinePlayer.java
@@ -5,30 +5,45 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import org.toop.framework.gameFramework.model.player.Player;
/**
- * Represents a player controlled remotely or over a network.
- *
- * 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.
- *
+ * Represents a player that participates online.
+ *
+ * @param the type of turn-based game
*/
public class OnlinePlayer> extends AbstractPlayer {
/**
- * Constructs a new OnlinePlayer.
- *
- * Currently, no additional initialization is performed. Subclasses or
- * future implementations should provide mechanisms to receive moves from
- * an external source.
+ * Creates a new online player with the given name.
+ *
+ * @param name the name of the player
*/
public OnlinePlayer(String name) {
super(name);
}
+ /**
+ * Creates a copy of another online player.
+ *
+ * @param other the player to copy
+ */
public OnlinePlayer(OnlinePlayer other) {
super(other);
}
+ /**
+ * {@inheritDoc}
+ *
+ * This method is not supported for online players.
+ *
+ * @throws UnsupportedOperationException always
+ */
+ @Override
+ protected long determineMove(T gameCopy) {
+ throw new UnsupportedOperationException("An online player does not support determining move");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
@Override
public Player deepCopy() {
return new OnlinePlayer<>(this);
From 380e219c08d94ec4ead1b83e5edf3bb0a6d40199 Mon Sep 17 00:00:00 2001
From: ramollia <>
Date: Mon, 15 Dec 2025 09:06:56 +0100
Subject: [PATCH 08/11] mcts v1
---
.../app/widget/view/LocalMultiplayerView.java | 3 +-
.../java/org/toop/game/players/ai/MCTSAI.java | 191 ++++++++++++++++++
2 files changed, 193 insertions(+), 1 deletion(-)
create mode 100644 game/src/main/java/org/toop/game/players/ai/MCTSAI.java
diff --git a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
index f28f49d..139a33c 100644
--- a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
+++ b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
@@ -15,6 +15,7 @@ import org.toop.app.widget.complex.PlayerInfoWidget;
import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.tutorial.*;
+import org.toop.game.players.ai.MCTSAI;
import org.toop.game.players.ai.MiniMaxAI;
import org.toop.game.players.ai.RandomAI;
import org.toop.local.AppContext;
@@ -87,7 +88,7 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[1].isHuman) {
players[1] = new LocalPlayer<>(information.players[1].name);
} else {
- players[1] = new ArtificialPlayer<>(new MiniMaxAI(6), "MiniMax");
+ players[1] = new ArtificialPlayer<>(new MCTSAI(1000), "MCTS AI");
}
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) {
new ShowEnableTutorialWidget(
diff --git a/game/src/main/java/org/toop/game/players/ai/MCTSAI.java b/game/src/main/java/org/toop/game/players/ai/MCTSAI.java
new file mode 100644
index 0000000..41cfd77
--- /dev/null
+++ b/game/src/main/java/org/toop/game/players/ai/MCTSAI.java
@@ -0,0 +1,191 @@
+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;
+ }
+
+ public Node bestUCTChild(float explorationFactor) {
+ int bestChildIndex = -1;
+ float bestScore = Float.NEGATIVE_INFINITY;
+
+ for (int i = 0; i < expanded; i++) {
+ float exploitation = children[i].visits <= 0? 0 : children[i].value / children[i].visits;
+ float exploration = explorationFactor * (float)(Math.sqrt(Math.log(visits) / (children[i].visits + 0.001f)));
+
+ float score = exploitation + exploration;
+
+ 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(T 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;
+ }
+ }
+
+ System.out.println("Visit count: " + root.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(1.41f);
+ }
+
+ 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--;
+ }
+ }
+}
\ No newline at end of file
From e149588b60a1969e96058afd5e528e8fee2435a5 Mon Sep 17 00:00:00 2001
From: ramollia <>
Date: Mon, 15 Dec 2025 10:31:22 +0100
Subject: [PATCH 09/11] bitboard optimization
---
.../game/games/reversi/BitboardReversi.java | 259 ++++++++++++++----
1 file changed, 204 insertions(+), 55 deletions(-)
diff --git a/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java
index f380bef..23816d7 100644
--- a/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java
+++ b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java
@@ -27,47 +27,227 @@ public class BitboardReversi extends BitboardGame {
}
public long getLegalMoves() {
+ long legalMoves = 0L;
+
final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer());
- long legalMoves = 0L;
+ final long empty = ~(player | opponent);
- // north & south
- legalMoves |= computeMoves(player, opponent, 8, -1L);
- legalMoves |= computeMoves(player, opponent, -8, -1L);
+ long mask;
+ long direction;
- // east & west
- legalMoves |= computeMoves(player, opponent, 1, notAFile);
- legalMoves |= computeMoves(player, opponent, -1, notHFile);
+ // north
+ mask = opponent;
+ direction = (player << 8) & mask;
- // north-east & north-west & south-east & south-west
- legalMoves |= computeMoves(player, opponent, 9, notAFile);
- legalMoves |= computeMoves(player, opponent, 7, notHFile);
- legalMoves |= computeMoves(player, opponent, -7, notAFile);
- legalMoves |= computeMoves(player, opponent, -9, notHFile);
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ legalMoves |= (direction << 8) & empty;
+
+ // south
+ mask = opponent;
+ direction = (player >>> 8) & mask;
+
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ legalMoves |= (direction >>> 8) & empty;
+
+ // east
+ mask = opponent & notAFile;
+ direction = (player << 1) & mask;
+
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ legalMoves |= (direction << 1) & empty & notAFile;
+
+ // west
+ mask = opponent & notHFile;
+ direction = (player >>> 1) & mask;
+
+ direction |= (direction >>> 1) & mask;
+ direction |= (direction >>> 1) & mask;
+ direction |= (direction >>> 1) & mask;
+ direction |= (direction >>> 1) & mask;
+ direction |= (direction >>> 1) & mask;
+ legalMoves |= (direction >>> 1) & empty & notHFile;
+
+ // north-east
+ mask = opponent & notAFile;
+ direction = (player << 9) & mask;
+
+ direction |= (direction << 9) & mask;
+ direction |= (direction << 9) & mask;
+ direction |= (direction << 9) & mask;
+ direction |= (direction << 9) & mask;
+ direction |= (direction << 9) & mask;
+ legalMoves |= (direction << 9) & empty & notAFile;
+
+ // north-west
+ mask = opponent & notHFile;
+ direction = (player << 7) & mask;
+
+ direction |= (direction << 7) & mask;
+ direction |= (direction << 7) & mask;
+ direction |= (direction << 7) & mask;
+ direction |= (direction << 7) & mask;
+ direction |= (direction << 7) & mask;
+ legalMoves |= (direction << 7) & empty & notHFile;
+
+ // south-east
+ mask = opponent & notAFile;
+ direction = (player >>> 7) & mask;
+
+ direction |= (direction >>> 7) & mask;
+ direction |= (direction >>> 7) & mask;
+ direction |= (direction >>> 7) & mask;
+ direction |= (direction >>> 7) & mask;
+ direction |= (direction >>> 7) & mask;
+ legalMoves |= (direction >>> 7) & empty & notAFile;
+
+ // south-west
+ mask = opponent & notHFile;
+ direction = (player >>> 9) & mask;
+
+ direction |= (direction >>> 9) & mask;
+ direction |= (direction >>> 9) & mask;
+ direction |= (direction >>> 9) & mask;
+ direction |= (direction >>> 9) & mask;
+ direction |= (direction >>> 9) & mask;
+ legalMoves |= (direction >>> 9) & empty & notHFile;
return legalMoves;
}
public long getFlips(long move) {
+ long flips = 0L;
+
final long player = getPlayerBitboard(getCurrentPlayerIndex());
final long opponent = getPlayerBitboard(getNextPlayer());
- long flips = 0L;
+ long mask;
+ long direction;
- // north & south
- flips |= computeFlips(move, player, opponent, 8, -1L);
- flips |= computeFlips(move, player, opponent, -8, -1L);
+ // north
+ mask = opponent;
+ direction = (move << 8) & mask;
- // east & west
- flips |= computeFlips(move, player, opponent, 1, notAFile);
- flips |= computeFlips(move, player, opponent, -1, notHFile);
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
+ direction |= (direction << 8) & mask;
- // north-east & north-west & south-east & south-west
- flips |= computeFlips(move, player, opponent, 9, notAFile);
- flips |= computeFlips(move, player, opponent, 7, notHFile);
- flips |= computeFlips(move, player, opponent, -7, notAFile);
- flips |= computeFlips(move, player, opponent, -9, notHFile);
+ if (((direction << 8) & player) != 0) {
+ flips |= direction;
+ }
+
+ // south
+ mask = opponent;
+ direction = (move >>> 8) & mask;
+
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+ direction |= (direction >>> 8) & mask;
+
+ if (((direction >>> 8) & player) != 0) {
+ flips |= direction;
+ }
+
+ // east
+ mask = opponent & notAFile;
+ direction = (move << 1) & mask;
+
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+ direction |= (direction << 1) & mask;
+
+ if (((direction << 1) & player) != 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) != 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) != 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) != 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) != 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) != 0) {
+ flips |= direction;
+ }
return flips;
}
@@ -136,35 +316,4 @@ public class BitboardReversi extends BitboardGame {
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;
- }
}
\ No newline at end of file
From df93b44d19007c27809d57a4305aca3110e3563f Mon Sep 17 00:00:00 2001
From: ramollia <>
Date: Wed, 7 Jan 2026 14:39:38 +0100
Subject: [PATCH 10/11] bitboard fix & mcts v2 & mcts v3. v3 still in progress
and v4 coming soon
---
app/src/main/java/org/toop/Main.java | 54 +++-
.../app/widget/view/LocalMultiplayerView.java | 9 +-
.../model/game/TurnBasedGame.java | 3 +
.../model/player/AbstractPlayer.java | 1 -
.../main/java/org/toop/game/BitboardGame.java | 23 +-
.../game/games/reversi/BitboardReversi.java | 24 +-
.../games/tictactoe/BitboardTicTacToe.java | 12 +-
.../java/org/toop/game/players/ai/MCTSAI.java | 18 +-
.../org/toop/game/players/ai/MCTSAI2.java | 195 +++++++++++++
.../org/toop/game/players/ai/MCTSAI3.java | 258 ++++++++++++++++++
10 files changed, 568 insertions(+), 29 deletions(-)
create mode 100644 game/src/main/java/org/toop/game/players/ai/MCTSAI2.java
create mode 100644 game/src/main/java/org/toop/game/players/ai/MCTSAI3.java
diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java
index 3b4fef3..874276a 100644
--- a/app/src/main/java/org/toop/Main.java
+++ b/app/src/main/java/org/toop/Main.java
@@ -1,9 +1,61 @@
package org.toop;
import org.toop.app.App;
+import org.toop.framework.gameFramework.model.player.AbstractPlayer;
+import org.toop.framework.gameFramework.model.player.Player;
+import org.toop.game.games.reversi.BitboardReversi;
+import org.toop.game.games.tictactoe.BitboardTicTacToe;
+import org.toop.game.players.ArtificialPlayer;
+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.RandomAI;
public final class Main {
static void main(String[] args) {
- App.run(args);
+ // App.run(args);
+ testMCTS(10);
}
+
+ private static void testMCTS(int games) {
+ var random = new ArtificialPlayer<>(new RandomAI(), "Random AI");
+ var v1 = new ArtificialPlayer<>(new MCTSAI(10), "MCTS V1 AI");
+ var v2 = new ArtificialPlayer<>(new MCTSAI2(10), "MCTS V2 AI");
+ var v2_2 = new ArtificialPlayer<>(new MCTSAI2(100), "MCTS V2_2 AI");
+ var v3 = new ArtificialPlayer<>(new MCTSAI3(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[] 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);
+ }
}
diff --git a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
index 139a33c..3e9e675 100644
--- a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
+++ b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java
@@ -16,6 +16,8 @@ import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.tutorial.*;
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.game.players.ai.RandomAI;
import org.toop.local.AppContext;
@@ -55,7 +57,7 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[0].isHuman) {
players[0] = new LocalPlayer<>(information.players[0].name);
} else {
- players[0] = new ArtificialPlayer<>(new RandomAI(), "Random AI");
+ players[0] = new ArtificialPlayer<>(new MCTSAI(100), "MCTS AI");
}
if (information.players[1].isHuman) {
players[1] = new LocalPlayer<>(information.players[1].name);
@@ -83,12 +85,13 @@ public class LocalMultiplayerView extends ViewWidget {
if (information.players[0].isHuman) {
players[0] = new LocalPlayer<>(information.players[0].name);
} else {
- players[0] = new ArtificialPlayer<>(new RandomAI(), "Random AI");
+ // players[0] = new ArtificialPlayer<>(new RandomAI(), "Random AI");
+ players[0] = new ArtificialPlayer<>(new MCTSAI3(50), "MCTS V3 AI");
}
if (information.players[1].isHuman) {
players[1] = new LocalPlayer<>(information.players[1].name);
} else {
- players[1] = new ArtificialPlayer<>(new MCTSAI(1000), "MCTS AI");
+ players[1] = new ArtificialPlayer<>(new MCTSAI2(50), "MCTS V2 AI");
}
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) {
new ShowEnableTutorialWidget(
diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java
index d4cb4df..41f9b8c 100644
--- a/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java
+++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/TurnBasedGame.java
@@ -4,4 +4,7 @@ public interface TurnBasedGame> extends Playable, Dee
int getCurrentTurn();
int getPlayerCount();
int getWinner();
+
+ PlayResult getState();
+ boolean isTerminal();
}
diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java
index 52b0de4..601b93d 100644
--- a/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java
+++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/AbstractPlayer.java
@@ -11,7 +11,6 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame;
*/
public abstract class AbstractPlayer> implements Player {
- private final Logger logger = LogManager.getLogger(this.getClass());
private final String name;
/**
diff --git a/game/src/main/java/org/toop/game/BitboardGame.java b/game/src/main/java/org/toop/game/BitboardGame.java
index 4ebdb95..8e708b5 100644
--- a/game/src/main/java/org/toop/game/BitboardGame.java
+++ b/game/src/main/java/org/toop/game/BitboardGame.java
@@ -1,5 +1,7 @@
package org.toop.game;
+import org.toop.framework.gameFramework.GameState;
+import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
@@ -11,6 +13,8 @@ public abstract class BitboardGame> implements TurnBas
private final int columnSize;
private final int rowSize;
+ protected PlayResult state;
+
private Player[] players;
// long is 64 bits. Every game has a limit of 64 cells maximum.
@@ -20,6 +24,9 @@ public abstract class BitboardGame> implements TurnBas
public BitboardGame(int columnSize, int rowSize, int playerCount, Player[] players) {
this.columnSize = columnSize;
this.rowSize = rowSize;
+
+ this.state = new PlayResult(GameState.NORMAL, -1);
+
this.players = players;
this.playerBitboard = new long[playerCount];
@@ -30,6 +37,8 @@ public abstract class BitboardGame> implements TurnBas
this.columnSize = other.columnSize;
this.rowSize = other.rowSize;
+ this.state = other.state;
+
this.playerBitboard = other.playerBitboard.clone();
this.currentTurn = other.currentTurn;
this.players = Arrays.stream(other.players)
@@ -61,7 +70,9 @@ public abstract class BitboardGame> implements TurnBas
return getCurrentPlayerIndex();
}
- public Player getPlayer(int index) {return players[index];}
+ public Player getPlayer(int index) {
+ return players[index];
+ }
public int getCurrentPlayerIndex() {
return currentTurn % playerBitboard.length;
@@ -75,9 +86,17 @@ public abstract class BitboardGame> implements TurnBas
return players[getCurrentPlayerIndex()];
}
+ @Override
+ public PlayResult getState() {
+ return state;
+ }
+ @Override
+ public boolean isTerminal() {
+ return state.state() == GameState.WIN || state.state() == GameState.DRAW;
+ }
- @Override
+ @Override
public long[] getBoard() {return this.playerBitboard;}
public void nextTurn() {
diff --git a/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java
index 23816d7..aa8d5b8 100644
--- a/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java
+++ b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java
@@ -175,7 +175,7 @@ public class BitboardReversi extends BitboardGame {
direction |= (direction << 1) & mask;
direction |= (direction << 1) & mask;
- if (((direction << 1) & player) != 0) {
+ if (((direction << 1) & player & notAFile) != 0) {
flips |= direction;
}
@@ -189,7 +189,7 @@ public class BitboardReversi extends BitboardGame {
direction |= (direction >>> 1) & mask;
direction |= (direction >>> 1) & mask;
- if (((direction >>> 1) & player) != 0) {
+ if (((direction >>> 1) & player & notHFile) != 0) {
flips |= direction;
}
@@ -203,7 +203,7 @@ public class BitboardReversi extends BitboardGame {
direction |= (direction << 9) & mask;
direction |= (direction << 9) & mask;
- if (((direction << 9) & player) != 0) {
+ if (((direction << 9) & player & notAFile) != 0) {
flips |= direction;
}
@@ -217,7 +217,7 @@ public class BitboardReversi extends BitboardGame {
direction |= (direction << 7) & mask;
direction |= (direction << 7) & mask;
- if (((direction << 7) & player) != 0) {
+ if (((direction << 7) & player & notHFile) != 0) {
flips |= direction;
}
@@ -231,7 +231,7 @@ public class BitboardReversi extends BitboardGame {
direction |= (direction >>> 7) & mask;
direction |= (direction >>> 7) & mask;
- if (((direction >>> 7) & player) != 0) {
+ if (((direction >>> 7) & player & notAFile) != 0) {
flips |= direction;
}
@@ -245,7 +245,7 @@ public class BitboardReversi extends BitboardGame {
direction |= (direction >>> 9) & mask;
direction |= (direction >>> 9) & mask;
- if (((direction >>> 9) & player) != 0) {
+ if (((direction >>> 9) & player & notHFile) != 0) {
flips |= direction;
}
@@ -280,16 +280,20 @@ public class BitboardReversi extends BitboardGame {
int winner = getWinner();
if (winner == -1) {
- return new PlayResult(GameState.DRAW, -1);
+ state = new PlayResult(GameState.DRAW, -1);
+ return state;
}
- return new PlayResult(GameState.WIN, winner);
+ state = new PlayResult(GameState.WIN, winner);
+ return state;
}
- return new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex());
+ state = new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex());
+ return state;
}
- return new PlayResult(GameState.NORMAL, getCurrentPlayerIndex());
+ state = new PlayResult(GameState.NORMAL, getCurrentPlayerIndex());
+ return state;
}
public Score getScore() {
diff --git a/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java b/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java
index 0927431..e06eff2 100644
--- a/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java
+++ b/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java
@@ -39,7 +39,8 @@ public class BitboardTicTacToe extends BitboardGame {
public PlayResult play(long move) {
// Player loses if move is invalid
if ((move & getLegalMoves()) == 0 || Long.bitCount(move) != 1){
- return new PlayResult(GameState.WIN, getNextPlayer());
+ state = new PlayResult(GameState.WIN, getNextPlayer());
+ return state;
}
// Move is legal, make move
@@ -50,7 +51,8 @@ public class BitboardTicTacToe extends BitboardGame {
// Check if current player won
if (checkWin(playerBitboard)) {
- return new PlayResult(GameState.WIN, getCurrentPlayerIndex());
+ state = new PlayResult(GameState.WIN, getCurrentPlayerIndex());
+ return state;
}
// Proceed to next turn
@@ -59,11 +61,13 @@ public class BitboardTicTacToe extends BitboardGame {
// Check for early draw
if (getLegalMoves() == 0L || checkEarlyDraw()) {
- return new PlayResult(GameState.DRAW, -1);
+ state = new PlayResult(GameState.DRAW, -1);
+ return state;
}
// Nothing weird happened, continue on as normal
- return new PlayResult(GameState.NORMAL, -1);
+ state = new PlayResult(GameState.NORMAL, -1);
+ return state;
}
private boolean checkWin(long board) {
diff --git a/game/src/main/java/org/toop/game/players/ai/MCTSAI.java b/game/src/main/java/org/toop/game/players/ai/MCTSAI.java
index 41cfd77..7a30caa 100644
--- a/game/src/main/java/org/toop/game/players/ai/MCTSAI.java
+++ b/game/src/main/java/org/toop/game/players/ai/MCTSAI.java
@@ -41,15 +41,19 @@ public class MCTSAI> extends AbstractAI {
return expanded >= children.length;
}
- public Node bestUCTChild(float explorationFactor) {
+ 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++) {
- float exploitation = children[i].visits <= 0? 0 : children[i].value / children[i].visits;
- float exploration = explorationFactor * (float)(Math.sqrt(Math.log(visits) / (children[i].visits + 0.001f)));
-
- float score = exploitation + exploration;
+ final float score = calculateUCT();
if (score > bestScore) {
bestChildIndex = i;
@@ -109,14 +113,12 @@ public class MCTSAI> extends AbstractAI {
}
}
- System.out.println("Visit count: " + root.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(1.41f);
+ node = node.bestUCTChild();
}
return node;
diff --git a/game/src/main/java/org/toop/game/players/ai/MCTSAI2.java b/game/src/main/java/org/toop/game/players/ai/MCTSAI2.java
new file mode 100644
index 0000000..bde88b2
--- /dev/null
+++ b/game/src/main/java/org/toop/game/players/ai/MCTSAI2.java
@@ -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(T 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;
+ }
+}
\ No newline at end of file
diff --git a/game/src/main/java/org/toop/game/players/ai/MCTSAI3.java b/game/src/main/java/org/toop/game/players/ai/MCTSAI3.java
new file mode 100644
index 0000000..efef955
--- /dev/null
+++ b/game/src/main/java/org/toop/game/players/ai/MCTSAI3.java
@@ -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(T 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(T 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;
+ }
+}
From 6aa0eb952ad9185f882ac7eec41038399dc00832 Mon Sep 17 00:00:00 2001
From: ramollia <>
Date: Wed, 7 Jan 2026 14:44:45 +0100
Subject: [PATCH 11/11] main
---
app/src/main/java/org/toop/Main.java | 69 ++++++++++++++--------------
1 file changed, 35 insertions(+), 34 deletions(-)
diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java
index 874276a..87171ff 100644
--- a/app/src/main/java/org/toop/Main.java
+++ b/app/src/main/java/org/toop/Main.java
@@ -13,49 +13,50 @@ import org.toop.game.players.ai.RandomAI;
public final class Main {
static void main(String[] args) {
- // App.run(args);
- testMCTS(10);
+ App.run(args);
+ // testMCTS(10);
}
- private static void testMCTS(int games) {
- var random = new ArtificialPlayer<>(new RandomAI(), "Random AI");
- var v1 = new ArtificialPlayer<>(new MCTSAI(10), "MCTS V1 AI");
- var v2 = new ArtificialPlayer<>(new MCTSAI2(10), "MCTS V2 AI");
- var v2_2 = new ArtificialPlayer<>(new MCTSAI2(100), "MCTS V2_2 AI");
- var v3 = new ArtificialPlayer<>(new MCTSAI3(10), "MCTS V3 AI");
+ // Voor onderzoek
+ // private static void testMCTS(int games) {
+ // var random = new ArtificialPlayer<>(new RandomAI(), "Random AI");
+ // var v1 = new ArtificialPlayer<>(new MCTSAI(10), "MCTS V1 AI");
+ // var v2 = new ArtificialPlayer<>(new MCTSAI2(10), "MCTS V2 AI");
+ // var v2_2 = new ArtificialPlayer<>(new MCTSAI2(100), "MCTS V2_2 AI");
+ // var v3 = new ArtificialPlayer<>(new MCTSAI3(10), "MCTS V3 AI");
- testAI(games, new Player[]{ v1, v2 });
- // testAI(games, new Player[]{ v1, v3 });
+ // 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 });
- }
+ // // 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[] ais) {
- int wins = 0;
- int ties = 0;
+ // private static void testAI(int games, Player[] ais) {
+ // int wins = 0;
+ // int ties = 0;
- for (int i = 0; i < games; i++) {
- final BitboardReversi match = new BitboardReversi(ais);
+ // 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);
+ // while (!match.isTerminal()) {
+ // final int currentAI = match.getCurrentTurn();
+ // final long move = ais[currentAI].getMove(match);
- match.play(move);
- }
+ // match.play(move);
+ // }
- if (match.getWinner() < 0) {
- ties++;
- continue;
- }
+ // if (match.getWinner() < 0) {
+ // ties++;
+ // continue;
+ // }
- wins += match.getWinner() == 0? 1 : 0;
- }
+ // 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);
- }
+ // 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);
+ // }
}