diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java index 3b4fef3..bd330d2 100644 --- a/app/src/main/java/org/toop/Main.java +++ b/app/src/main/java/org/toop/Main.java @@ -4,6 +4,50 @@ import org.toop.app.App; public final class Main { 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(), "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/Server.java b/app/src/main/java/org/toop/app/Server.java index d8b074a..1042a26 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -11,17 +11,15 @@ import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.popup.SendChallengePopup; import org.toop.app.widget.view.ServerView; import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.game.players.LocalPlayer; +import org.toop.framework.game.players.OnlinePlayer; import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.networking.connection.clients.TournamentNetworkingClient; import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.connection.types.NetworkingConnector; -import org.toop.framework.game.players.ArtificialPlayer; -import org.toop.framework.game.players.OnlinePlayer; -import org.toop.framework.game.players.ai.RandomAI; import org.toop.framework.networking.server.gateway.NettyGatewayServer; +import org.toop.game.players.LocalPlayer; import org.toop.local.AppContext; import java.util.List; 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 e55722d..c19e6fb 100644 --- a/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java +++ b/app/src/main/java/org/toop/app/gameControllers/GenericGameController.java @@ -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.view.GUIEvents; 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 { protected final EventFlow eventFlow = new EventFlow(); @@ -55,9 +55,7 @@ public class GenericGameController implements GameController { // 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/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java index d773c06..a4fa180 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 @@ -7,14 +7,16 @@ import org.toop.app.gameControllers.TicTacToeBitController; import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.game.players.ArtificialPlayer; -import org.toop.framework.game.players.LocalPlayer; import org.toop.app.widget.Primitive; import org.toop.app.widget.complex.PlayerInfoWidget; import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.tutorial.*; -import org.toop.framework.game.players.ai.MiniMaxAI; -import org.toop.framework.game.players.ai.RandomAI; +import org.toop.game.players.LocalPlayer; +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 javafx.geometry.Pos; @@ -52,7 +54,7 @@ public class LocalMultiplayerView extends ViewWidget { if (information.players[0].isHuman) { players[0] = new LocalPlayer(information.players[0].name); } else { - players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); + players[0] = new ArtificialPlayer(new MCTSAI(100), "MCTS AI"); } if (information.players[1].isHuman) { players[1] = new LocalPlayer(information.players[1].name); @@ -80,12 +82,13 @@ public class LocalMultiplayerView extends ViewWidget { if (information.players[0].isHuman) { players[0] = new LocalPlayer(information.players[0].name); } else { - players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); + // players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); + players[0] = new ArtificialPlayer(new MCTSAI3(50), "MCTS V3 AI"); } if (information.players[1].isHuman) { players[1] = new LocalPlayer(information.players[1].name); } else { - players[1] = new ArtificialPlayer(new MiniMaxAI(6), "MiniMax"); + players[1] = new ArtificialPlayer(new MCTSAI2(50), "MCTS V2 AI"); } if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) { new ShowEnableTutorialWidget( diff --git a/framework/src/main/java/org/toop/framework/game/BitboardGame.java b/framework/src/main/java/org/toop/framework/game/BitboardGame.java index 3e26d40..8562432 100644 --- a/framework/src/main/java/org/toop/framework/game/BitboardGame.java +++ b/framework/src/main/java/org/toop/framework/game/BitboardGame.java @@ -1,5 +1,7 @@ package org.toop.framework.game; +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.Player; @@ -10,6 +12,8 @@ public abstract class BitboardGame implements TurnBasedGame { private final int columnSize; private final int rowSize; + protected PlayResult state; + private Player[] players; // long is 64 bits. Every game has a limit of 64 cells maximum. @@ -20,21 +24,27 @@ public abstract class BitboardGame implements TurnBasedGame { public BitboardGame(int columnSize, int rowSize, int playerCount) { this.columnSize = columnSize; this.rowSize = rowSize; - this.playerCount = playerCount; - this.playerBitboard = new long[playerCount]; - } + this.playerCount = playerCount; + this.state = new PlayResult(GameState.NORMAL, -1); - @Override - public void init(Player[] players) { - this.players = players; + this.playerBitboard = new long[playerCount]; Arrays.fill(playerBitboard, 0L); } + @Override + public void init(Player[] players) { + this.players = players; + + Arrays.fill(playerBitboard, 0L); + } + public BitboardGame(BitboardGame other) { this.columnSize = other.columnSize; this.rowSize = other.rowSize; this.playerCount = other.playerCount; + this.state = other.state; + this.playerBitboard = other.playerBitboard.clone(); this.currentTurn = other.currentTurn; this.players = Arrays.stream(other.players) @@ -80,9 +90,17 @@ public abstract class BitboardGame implements TurnBasedGame { return players[getCurrentPlayerIndex()]; } + @Override + public PlayResult getState() { + return state; + } + @Override + public boolean isTerminal() { + return state.state() == GameState.WIN || state.state() == GameState.DRAW; + } - @Override + @Override public long[] getBoard() {return this.playerBitboard;} public void nextTurn() { diff --git a/framework/src/main/java/org/toop/framework/game/games/reversi/BitboardReversi.java b/framework/src/main/java/org/toop/framework/game/games/reversi/BitboardReversi.java index e116c9a..102b94d 100644 --- a/framework/src/main/java/org/toop/framework/game/games/reversi/BitboardReversi.java +++ b/framework/src/main/java/org/toop/framework/game/games/reversi/BitboardReversi.java @@ -32,47 +32,227 @@ public class BitboardReversi extends BitboardGame { } public long getLegalMoves() { + long legalMoves = 0L; + final long player = getPlayerBitboard(getCurrentPlayerIndex()); final long opponent = getPlayerBitboard(getNextPlayer()); - long legalMoves = 0L; + final long empty = ~(player | opponent); - // north & south - legalMoves |= computeMoves(player, opponent, 8, -1L); - legalMoves |= computeMoves(player, opponent, -8, -1L); + long mask; + long direction; - // east & west - legalMoves |= computeMoves(player, opponent, 1, notAFile); - legalMoves |= computeMoves(player, opponent, -1, notHFile); + // north + mask = opponent; + direction = (player << 8) & mask; - // north-east & north-west & south-east & south-west - legalMoves |= computeMoves(player, opponent, 9, notAFile); - legalMoves |= computeMoves(player, opponent, 7, notHFile); - legalMoves |= computeMoves(player, opponent, -7, notAFile); - legalMoves |= computeMoves(player, opponent, -9, notHFile); + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + legalMoves |= (direction << 8) & empty; + + // south + mask = opponent; + direction = (player >>> 8) & mask; + + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + legalMoves |= (direction >>> 8) & empty; + + // east + mask = opponent & notAFile; + direction = (player << 1) & mask; + + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + legalMoves |= (direction << 1) & empty & notAFile; + + // west + mask = opponent & notHFile; + direction = (player >>> 1) & mask; + + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + legalMoves |= (direction >>> 1) & empty & notHFile; + + // north-east + mask = opponent & notAFile; + direction = (player << 9) & mask; + + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + legalMoves |= (direction << 9) & empty & notAFile; + + // north-west + mask = opponent & notHFile; + direction = (player << 7) & mask; + + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + legalMoves |= (direction << 7) & empty & notHFile; + + // south-east + mask = opponent & notAFile; + direction = (player >>> 7) & mask; + + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + legalMoves |= (direction >>> 7) & empty & notAFile; + + // south-west + mask = opponent & notHFile; + direction = (player >>> 9) & mask; + + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + legalMoves |= (direction >>> 9) & empty & notHFile; return legalMoves; } public long getFlips(long move) { + long flips = 0L; + final long player = getPlayerBitboard(getCurrentPlayerIndex()); final long opponent = getPlayerBitboard(getNextPlayer()); - long flips = 0L; + long mask; + long direction; - // north & south - flips |= computeFlips(move, player, opponent, 8, -1L); - flips |= computeFlips(move, player, opponent, -8, -1L); + // north + mask = opponent; + direction = (move << 8) & mask; - // east & west - flips |= computeFlips(move, player, opponent, 1, notAFile); - flips |= computeFlips(move, player, opponent, -1, notHFile); + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; + direction |= (direction << 8) & mask; - // north-east & north-west & south-east & south-west - flips |= computeFlips(move, player, opponent, 9, notAFile); - flips |= computeFlips(move, player, opponent, 7, notHFile); - flips |= computeFlips(move, player, opponent, -7, notAFile); - flips |= computeFlips(move, player, opponent, -9, notHFile); + if (((direction << 8) & player) != 0) { + flips |= direction; + } + + // south + mask = opponent; + direction = (move >>> 8) & mask; + + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + direction |= (direction >>> 8) & mask; + + if (((direction >>> 8) & player) != 0) { + flips |= direction; + } + + // east + mask = opponent & notAFile; + direction = (move << 1) & mask; + + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + direction |= (direction << 1) & mask; + + if (((direction << 1) & player & notAFile) != 0) { + flips |= direction; + } + + // west + mask = opponent & notHFile; + direction = (move >>> 1) & mask; + + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + direction |= (direction >>> 1) & mask; + + if (((direction >>> 1) & player & notHFile) != 0) { + flips |= direction; + } + + // north-east + mask = opponent & notAFile; + direction = (move << 9) & mask; + + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + direction |= (direction << 9) & mask; + + if (((direction << 9) & player & notAFile) != 0) { + flips |= direction; + } + + // north-west + mask = opponent & notHFile; + direction = (move << 7) & mask; + + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + direction |= (direction << 7) & mask; + + if (((direction << 7) & player & notHFile) != 0) { + flips |= direction; + } + + // south-east + mask = opponent & notAFile; + direction = (move >>> 7) & mask; + + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + direction |= (direction >>> 7) & mask; + + if (((direction >>> 7) & player & notAFile) != 0) { + flips |= direction; + } + + // south-west + mask = opponent & notHFile; + direction = (move >>> 9) & mask; + + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + direction |= (direction >>> 9) & mask; + + if (((direction >>> 9) & player & notHFile) != 0) { + flips |= direction; + } return flips; } @@ -105,16 +285,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() { @@ -141,35 +325,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 diff --git a/framework/src/main/java/org/toop/framework/game/games/tictactoe/BitboardTicTacToe.java b/framework/src/main/java/org/toop/framework/game/games/tictactoe/BitboardTicTacToe.java index 7115ccc..fa89efc 100644 --- a/framework/src/main/java/org/toop/framework/game/games/tictactoe/BitboardTicTacToe.java +++ b/framework/src/main/java/org/toop/framework/game/games/tictactoe/BitboardTicTacToe.java @@ -45,7 +45,8 @@ public class BitboardTicTacToe extends BitboardGame { public PlayResult play(long move) { // Player loses if move is invalid if ((move & getLegalMoves()) == 0 || Long.bitCount(move) != 1){ - return new PlayResult(GameState.WIN, getNextPlayer()); + state = new PlayResult(GameState.WIN, getNextPlayer()); + return state; } // Move is legal, make move @@ -56,7 +57,8 @@ public class BitboardTicTacToe extends BitboardGame { // Check if current player won if (checkWin(playerBitboard)) { - return new PlayResult(GameState.WIN, getCurrentPlayerIndex()); + state = new PlayResult(GameState.WIN, getCurrentPlayerIndex()); + return state; } // Proceed to next turn @@ -65,11 +67,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/framework/src/main/java/org/toop/framework/game/players/ArtificialPlayer.java b/framework/src/main/java/org/toop/framework/game/players/ArtificialPlayer.java index af87a42..23bb2bc 100644 --- a/framework/src/main/java/org/toop/framework/game/players/ArtificialPlayer.java +++ b/framework/src/main/java/org/toop/framework/game/players/ArtificialPlayer.java @@ -44,10 +44,15 @@ public class ArtificialPlayer extends AbstractPlayer { * @return the integer representing the chosen move * @throws ClassCastException if {@code gameCopy} is not of type {@code T} */ - public long getMove(TurnBasedGame gameCopy) { + protected long determineMove(TurnBasedGame gameCopy) { return ai.getMove(gameCopy); } + /** + * Creates a deep copy of this AI player. + * + * @return a copy of this player + */ @Override public ArtificialPlayer deepCopy() { return new ArtificialPlayer(this); diff --git a/framework/src/main/java/org/toop/framework/game/players/LocalPlayer.java b/framework/src/main/java/org/toop/framework/game/players/LocalPlayer.java index 821cf08..9cab095 100644 --- a/framework/src/main/java/org/toop/framework/game/players/LocalPlayer.java +++ b/framework/src/main/java/org/toop/framework/game/players/LocalPlayer.java @@ -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.player.AbstractPlayer; @@ -6,65 +6,83 @@ import org.toop.framework.gameFramework.model.player.AbstractPlayer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +/** + * Represents a local player who provides moves manually. + * + * @param 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 - public long getMove(TurnBasedGame gameCopy) { - return getValidMove(gameCopy); + protected long determineMove(TurnBasedGame 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(TurnBasedGame gameCopy) { - LastMove = new CompletableFuture<>(); - long move = 0; + /** + * Waits for the next move from the player. + * + * @return the chosen move or 0 if interrupted + */ + private long getLastMove() { + LastMove = new CompletableFuture<>(); // Reset the future try { - move = LastMove.get(); - System.out.println(Long.toBinaryString(move)); - } catch (InterruptedException | ExecutionException e) { - // TODO: Add proper logging. - e.printStackTrace(); + return LastMove.get(); + } catch (ExecutionException | InterruptedException e) { + return 0; } - return move; - } - - protected long getValidMove(TurnBasedGame gameCopy){ - // Get this player's valid moves - long validMoves = gameCopy.getLegalMoves(); - // Make sure provided move is valid - // TODO: Limit amount of retries? - // TODO: Stop copying game so many times - long move = getMove2(gameCopy.deepCopy()); - while ((validMoves & move) == 0) { - System.out.println("Not a valid move, try again"); - move = getMove2(gameCopy.deepCopy()); - } - return move; } + /** + * Creates a deep copy of this local player. + * + * @return a copy of this player + */ @Override public LocalPlayer deepCopy() { - return new LocalPlayer(this.getName()); + return new LocalPlayer(this); } /*public void register() { diff --git a/framework/src/main/java/org/toop/framework/game/players/OnlinePlayer.java b/framework/src/main/java/org/toop/framework/game/players/OnlinePlayer.java index 6551b1a..bd4ddd1 100644 --- a/framework/src/main/java/org/toop/framework/game/players/OnlinePlayer.java +++ b/framework/src/main/java/org/toop/framework/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(TurnBasedGame gameCopy) { + throw new UnsupportedOperationException("An online player does not support determining move"); + } + + /** + * {@inheritDoc} + */ @Override public Player deepCopy() { return new OnlinePlayer(this); diff --git a/framework/src/main/java/org/toop/framework/game/players/ServerPlayer.java b/framework/src/main/java/org/toop/framework/game/players/ServerPlayer.java index f4a7ed0..610e609 100644 --- a/framework/src/main/java/org/toop/framework/game/players/ServerPlayer.java +++ b/framework/src/main/java/org/toop/framework/game/players/ServerPlayer.java @@ -27,7 +27,7 @@ public class ServerPlayer extends AbstractPlayer { } @Override - public long getMove(TurnBasedGame game) { + public long determineMove(TurnBasedGame game) { lastMove = new CompletableFuture<>(); System.out.println("Sending yourturn"); client.send("SVR GAME YOURTURN {TURNMESSAGE: \"\"}\n"); diff --git a/framework/src/main/java/org/toop/framework/game/players/ai/MiniMaxAI.java b/framework/src/main/java/org/toop/framework/game/players/ai/MiniMaxAI.java index 38aacbd..e69de29 100644 --- a/framework/src/main/java/org/toop/framework/game/players/ai/MiniMaxAI.java +++ b/framework/src/main/java/org/toop/framework/game/players/ai/MiniMaxAI.java @@ -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 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; - } -} 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 8272264..6f5f8a7 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 @@ -11,4 +11,6 @@ public interface TurnBasedGame extends DeepCopyable { int getWinner(); long getLegalMoves(); PlayResult play(long move); + 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 8a294e8..9c4b787 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 @@ -40,12 +40,29 @@ public abstract class AbstractPlayer implements Player { * @return an integer representing the chosen move * @throws UnsupportedOperationException if the method is not overridden */ - public long getMove(TurnBasedGame gameCopy) { - logger.error("Method getMove not implemented."); - throw new UnsupportedOperationException("Not supported yet."); + public final long getMove(TurnBasedGame game) { + return determineMove(game.deepCopy()); } - public final String getName(){ + + /** + * Determines the player's move using a safe copy of the game. + *

+ * 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; } } 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..8687846 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/ai/MCTSAI.java @@ -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--; + } + } +} \ No newline at end of file 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..872c693 --- /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(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; + } +} \ 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..fe892d1 --- /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(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; + } +} diff --git a/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java b/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java new file mode 100644 index 0000000..6b70e00 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/ai/MiniMaxAI.java @@ -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 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; + } +} diff --git a/framework/src/main/java/org/toop/framework/game/players/ai/RandomAI.java b/game/src/main/java/org/toop/game/players/ai/RandomAI.java similarity index 95% rename from framework/src/main/java/org/toop/framework/game/players/ai/RandomAI.java rename to game/src/main/java/org/toop/game/players/ai/RandomAI.java index 91fd960..ad814fc 100644 --- a/framework/src/main/java/org/toop/framework/game/players/ai/RandomAI.java +++ b/game/src/main/java/org/toop/game/players/ai/RandomAI.java @@ -1,4 +1,4 @@ -package org.toop.framework.game.players.ai; +package org.toop.game.players.ai; import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.player.AbstractAI;