From 810a0f215961e7d9e0a1228690da8e2fa55f6d3c Mon Sep 17 00:00:00 2001 From: Persson-dev Date: Tue, 6 May 2025 17:15:55 +0200 Subject: [PATCH] feat: add ai castling (Fixes #4) --- app/src/main/java/chess/ai/AI.java | 50 ++--------- app/src/main/java/chess/ai/DumbAI.java | 42 +-------- app/src/main/java/chess/ai/HungryAI.java | 43 ++++----- .../main/java/chess/ai/actions/AIAction.java | 33 +++++++ .../chess/ai/actions/AIActionCastling.java | 20 +++++ .../java/chess/ai/actions/AIActionMove.java | 25 ++++++ .../ai/actions/AIActionMoveAndPromote.java | 26 ++++++ .../main/java/chess/ai/actions/AIActions.java | 87 +++++++++++++++++++ .../java/chess/ai/minimax/AlphaBetaAI.java | 37 +++----- .../chess/ai/minimax/AlphaBetaThread.java | 24 ++--- .../ai/minimax/AlphaBetaThreadCreator.java | 4 +- .../java/chess/ai/minimax/GameSimulation.java | 11 ++- 12 files changed, 252 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/chess/ai/actions/AIAction.java create mode 100644 app/src/main/java/chess/ai/actions/AIActionCastling.java create mode 100644 app/src/main/java/chess/ai/actions/AIActionMove.java create mode 100644 app/src/main/java/chess/ai/actions/AIActionMoveAndPromote.java create mode 100644 app/src/main/java/chess/ai/actions/AIActions.java diff --git a/app/src/main/java/chess/ai/AI.java b/app/src/main/java/chess/ai/AI.java index ec69e36..c322b09 100644 --- a/app/src/main/java/chess/ai/AI.java +++ b/app/src/main/java/chess/ai/AI.java @@ -2,20 +2,15 @@ package chess.ai; import java.util.List; -import chess.controller.Command; +import chess.ai.actions.AIAction; +import chess.ai.actions.AIActions; import chess.controller.CommandExecutor; -import chess.controller.Command.CommandResult; -import chess.controller.commands.GetPieceAtCommand; -import chess.controller.commands.GetPlayerMovesCommand; -import chess.controller.commands.GetAllowedCastlingsCommand; -import chess.controller.commands.GetAllowedCastlingsCommand.CastlingResult; import chess.controller.event.GameAdapter; import chess.model.Color; import chess.model.Coordinate; -import chess.model.Move; import chess.model.Piece; -public abstract class AI extends GameAdapter{ +public abstract class AI extends GameAdapter { protected final CommandExecutor commandExecutor; protected final Color color; @@ -26,7 +21,6 @@ public abstract class AI extends GameAdapter{ } protected abstract void play(); - protected abstract void promote(Coordinate pawnCoords); @Override public void onPlayerTurn(Color color, boolean undone) { @@ -36,44 +30,12 @@ public abstract class AI extends GameAdapter{ play(); } - @Override - public void onPromotePawn(Coordinate pieceCoords) { - Piece pawn = pieceAt(pieceCoords); - if (pawn.getColor() != this.color) - return; - promote(pieceCoords); + protected List getAllowedActions() { + return AIActions.getAllowedActions(this.commandExecutor); } protected Piece pieceAt(Coordinate coordinate) { - GetPieceAtCommand command = new GetPieceAtCommand(coordinate); - sendCommand(command); - return command.getPiece(); + return AIActions.pieceAt(coordinate, this.commandExecutor); } - protected List getAllowedMoves() { - return getAllowedMoves(this.commandExecutor); - } - - protected List getAllowedMoves(CommandExecutor commandExecutor) { - GetPlayerMovesCommand cmd = new GetPlayerMovesCommand(); - sendCommand(cmd, commandExecutor); - return cmd.getMoves(); - } - - protected CastlingResult getAllowedCastlings() { - GetAllowedCastlingsCommand cmd2 = new GetAllowedCastlingsCommand(); - sendCommand(cmd2); - return cmd2.getCastlingResult(); - } - - protected CommandResult sendCommand(Command command) { - return sendCommand(command, this.commandExecutor); - } - - protected CommandResult sendCommand(Command command, CommandExecutor commandExecutor) { - CommandResult result = commandExecutor.executeCommand(command); - assert result != CommandResult.NotAllowed : "Command not allowed!"; - return result; - } - } diff --git a/app/src/main/java/chess/ai/DumbAI.java b/app/src/main/java/chess/ai/DumbAI.java index c1ea82f..de146c5 100644 --- a/app/src/main/java/chess/ai/DumbAI.java +++ b/app/src/main/java/chess/ai/DumbAI.java @@ -3,15 +3,9 @@ package chess.ai; import java.util.List; import java.util.Random; +import chess.ai.actions.AIAction; import chess.controller.CommandExecutor; -import chess.controller.commands.CastlingCommand; -import chess.controller.commands.GetAllowedCastlingsCommand.CastlingResult; -import chess.controller.commands.MoveCommand; -import chess.controller.commands.PromoteCommand; -import chess.controller.commands.PromoteCommand.PromoteType; import chess.model.Color; -import chess.model.Coordinate; -import chess.model.Move; public class DumbAI extends AI { @@ -23,39 +17,11 @@ public class DumbAI extends AI { @Override protected void play() { - CastlingResult castlings = getAllowedCastlings(); - List moves = getAllowedMoves(); + List actions = getAllowedActions(); - switch (castlings) { - case Both: { - int randomMove = this.random.nextInt(moves.size() + 2); - if (randomMove < moves.size() - 2) - break; - sendCommand(new CastlingCommand(randomMove == moves.size())); - return; - } + int randomAction = this.random.nextInt(actions.size()); - case Small: - case Big: { - int randomMove = this.random.nextInt(moves.size() + 1); - if (randomMove != moves.size()) - break; - sendCommand(new CastlingCommand(castlings == CastlingResult.Big)); - return; - } - - default: - break; - } - - int randomMove = this.random.nextInt(moves.size()); - sendCommand(new MoveCommand(moves.get(randomMove))); - } - - @Override - protected void promote(Coordinate pawnCoords) { - int promote = this.random.nextInt(PromoteType.values().length); - sendCommand(new PromoteCommand(PromoteType.values()[promote])); + actions.get(randomAction).applyAction(); } } diff --git a/app/src/main/java/chess/ai/HungryAI.java b/app/src/main/java/chess/ai/HungryAI.java index 435c465..d568eb6 100644 --- a/app/src/main/java/chess/ai/HungryAI.java +++ b/app/src/main/java/chess/ai/HungryAI.java @@ -4,12 +4,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; +import chess.ai.actions.AIAction; +import chess.ai.actions.AIActionMove; import chess.controller.CommandExecutor; -import chess.controller.commands.MoveCommand; -import chess.controller.commands.PromoteCommand; -import chess.controller.commands.PromoteCommand.PromoteType; import chess.model.Color; -import chess.model.Coordinate; import chess.model.Move; import chess.model.Piece; @@ -26,21 +24,23 @@ public class HungryAI extends AI { private int getMoveCost(Move move) { Piece piece = pieceAt(move.getDeadPieceCoords()); - return - (int) pieceCost.getCost(piece); + return -(int) pieceCost.getCost(piece); } - private List getBestMoves() { - List moves = getAllowedMoves(); - List bestMoves = new ArrayList<>(); + private List getBestMoves() { + List actions = getAllowedActions(); + List bestMoves = new ArrayList<>(); int bestCost = 0; - for (Move move : moves) { - int moveCost = getMoveCost(move); - if (moveCost == bestCost) { - bestMoves.add(move); - } else if (moveCost > bestCost) { - bestMoves.clear(); - bestMoves.add(move); - bestCost = moveCost; + for (AIAction action : actions) { + if (action instanceof AIActionMove move) { + int moveCost = getMoveCost(move.getMove()); + if (moveCost == bestCost) { + bestMoves.add(move); + } else if (moveCost > bestCost) { + bestMoves.clear(); + bestMoves.add(move); + bestCost = moveCost; + } } } return bestMoves; @@ -48,14 +48,7 @@ public class HungryAI extends AI { @Override protected void play() { - List bestMoves = getBestMoves(); - int randomMove = this.random.nextInt(bestMoves.size()); - this.commandExecutor.executeCommand(new MoveCommand(bestMoves.get(randomMove))); + List bestMoves = getBestMoves(); + bestMoves.get(this.random.nextInt(bestMoves.size())).applyAction(); } - - @Override - protected void promote(Coordinate pawnCoords) { - sendCommand(new PromoteCommand(PromoteType.Queen)); - } - } diff --git a/app/src/main/java/chess/ai/actions/AIAction.java b/app/src/main/java/chess/ai/actions/AIAction.java new file mode 100644 index 0000000..9241a35 --- /dev/null +++ b/app/src/main/java/chess/ai/actions/AIAction.java @@ -0,0 +1,33 @@ +package chess.ai.actions; + +import chess.controller.Command; +import chess.controller.CommandExecutor; +import chess.controller.Command.CommandResult; +import chess.controller.commands.UndoCommand; + +public abstract class AIAction { + + private final CommandExecutor commandExecutor; + + public AIAction(CommandExecutor commandExecutor) { + this.commandExecutor = commandExecutor; + } + + protected CommandResult sendCommand(Command cmd, CommandExecutor commandExecutor) { + return commandExecutor.executeCommand(cmd); + } + + public void undoAction(CommandExecutor commandExecutor) { + sendCommand(new UndoCommand(), commandExecutor); + } + + public void undoAction() { + undoAction(this.commandExecutor); + } + + public void applyAction() { + applyAction(this.commandExecutor); + } + + public abstract void applyAction(CommandExecutor commandExecutor); +} diff --git a/app/src/main/java/chess/ai/actions/AIActionCastling.java b/app/src/main/java/chess/ai/actions/AIActionCastling.java new file mode 100644 index 0000000..69a55bb --- /dev/null +++ b/app/src/main/java/chess/ai/actions/AIActionCastling.java @@ -0,0 +1,20 @@ +package chess.ai.actions; + +import chess.controller.CommandExecutor; +import chess.controller.commands.CastlingCommand; + +public class AIActionCastling extends AIAction{ + + private final boolean bigCastling; + + public AIActionCastling(CommandExecutor commandExecutor, boolean bigCastling) { + super(commandExecutor); + this.bigCastling = bigCastling; + } + + @Override + public void applyAction(CommandExecutor commandExecutor) { + sendCommand(new CastlingCommand(this.bigCastling), commandExecutor); + } + +} diff --git a/app/src/main/java/chess/ai/actions/AIActionMove.java b/app/src/main/java/chess/ai/actions/AIActionMove.java new file mode 100644 index 0000000..b59cbb8 --- /dev/null +++ b/app/src/main/java/chess/ai/actions/AIActionMove.java @@ -0,0 +1,25 @@ +package chess.ai.actions; + +import chess.controller.CommandExecutor; +import chess.controller.commands.MoveCommand; +import chess.model.Move; + +public class AIActionMove extends AIAction{ + + private final Move move; + + public AIActionMove(CommandExecutor commandExecutor, Move move) { + super(commandExecutor); + this.move = move; + } + + public Move getMove() { + return move; + } + + @Override + public void applyAction(CommandExecutor commandExecutor) { + sendCommand(new MoveCommand(move), commandExecutor); + } + +} diff --git a/app/src/main/java/chess/ai/actions/AIActionMoveAndPromote.java b/app/src/main/java/chess/ai/actions/AIActionMoveAndPromote.java new file mode 100644 index 0000000..e5aa74a --- /dev/null +++ b/app/src/main/java/chess/ai/actions/AIActionMoveAndPromote.java @@ -0,0 +1,26 @@ +package chess.ai.actions; + +import chess.controller.CommandExecutor; +import chess.controller.commands.MoveCommand; +import chess.controller.commands.PromoteCommand; +import chess.controller.commands.PromoteCommand.PromoteType; +import chess.model.Move; + +public class AIActionMoveAndPromote extends AIAction{ + + private final Move move; + private final PromoteType promoteType; + + public AIActionMoveAndPromote(CommandExecutor commandExecutor, Move move, PromoteType promoteType) { + super(commandExecutor); + this.move = move; + this.promoteType = promoteType; + } + + @Override + public void applyAction(CommandExecutor commandExecutor) { + sendCommand(new MoveCommand(move), commandExecutor); + sendCommand(new PromoteCommand(promoteType), commandExecutor); + } + +} diff --git a/app/src/main/java/chess/ai/actions/AIActions.java b/app/src/main/java/chess/ai/actions/AIActions.java new file mode 100644 index 0000000..1c13fb4 --- /dev/null +++ b/app/src/main/java/chess/ai/actions/AIActions.java @@ -0,0 +1,87 @@ +package chess.ai.actions; + +import java.util.ArrayList; +import java.util.List; + +import chess.controller.Command; +import chess.controller.Command.CommandResult; +import chess.controller.CommandExecutor; +import chess.controller.commands.GetAllowedCastlingsCommand; +import chess.controller.commands.GetAllowedCastlingsCommand.CastlingResult; +import chess.controller.commands.PromoteCommand.PromoteType; +import chess.controller.commands.GetPieceAtCommand; +import chess.controller.commands.GetPlayerMovesCommand; +import chess.model.Color; +import chess.model.Coordinate; +import chess.model.Move; +import chess.model.Piece; +import chess.model.pieces.Pawn; + +public class AIActions { + + public static List getAllowedActions(CommandExecutor commandExecutor) { + List moves = getAllowedMoves(commandExecutor); + CastlingResult castlingResult = getAllowedCastlings(commandExecutor); + + List actions = new ArrayList<>(moves.size() + 10); + + for (Move move : moves) { + Piece movingPiece = pieceAt(move.getStart(), commandExecutor); + if (movingPiece instanceof Pawn) { + int enemyLineY = movingPiece.getColor() == Color.White ? 0 : 7; + if (move.getFinish().getY() == enemyLineY) { + PromoteType[] promotes = PromoteType.values(); + for (PromoteType promote : promotes) { + actions.add(new AIActionMoveAndPromote(commandExecutor, move, promote)); + } + continue; + } + } + actions.add(new AIActionMove(commandExecutor, move)); + } + + switch (castlingResult) { + case Both: + actions.add(new AIActionCastling(commandExecutor, true)); + actions.add(new AIActionCastling(commandExecutor, false)); + break; + + case Small: + actions.add(new AIActionCastling(commandExecutor, false)); + break; + + case Big: + actions.add(new AIActionCastling(commandExecutor, true)); + break; + + case None: + break; + } + + return actions; + } + + private static CastlingResult getAllowedCastlings(CommandExecutor commandExecutor) { + GetAllowedCastlingsCommand cmd2 = new GetAllowedCastlingsCommand(); + sendCommand(cmd2, commandExecutor); + return cmd2.getCastlingResult(); + } + + private static List getAllowedMoves(CommandExecutor commandExecutor) { + GetPlayerMovesCommand cmd = new GetPlayerMovesCommand(); + sendCommand(cmd, commandExecutor); + return cmd.getMoves(); + } + + private static CommandResult sendCommand(Command command, CommandExecutor commandExecutor) { + CommandResult result = commandExecutor.executeCommand(command); + assert result != CommandResult.NotAllowed : "Command not allowed!"; + return result; + } + + public static Piece pieceAt(Coordinate coordinate, CommandExecutor commandExecutor) { + GetPieceAtCommand command = new GetPieceAtCommand(coordinate); + commandExecutor.executeCommand(command); + return command.getPiece(); + } +} diff --git a/app/src/main/java/chess/ai/minimax/AlphaBetaAI.java b/app/src/main/java/chess/ai/minimax/AlphaBetaAI.java index d92e6ce..4d6d630 100644 --- a/app/src/main/java/chess/ai/minimax/AlphaBetaAI.java +++ b/app/src/main/java/chess/ai/minimax/AlphaBetaAI.java @@ -8,13 +8,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import chess.ai.AI; +import chess.ai.actions.AIAction; import chess.controller.CommandExecutor; -import chess.controller.commands.MoveCommand; -import chess.controller.commands.PromoteCommand; -import chess.controller.commands.PromoteCommand.PromoteType; import chess.model.Color; -import chess.model.Coordinate; -import chess.model.Move; import common.Signal1; public class AlphaBetaAI extends AI { @@ -38,23 +34,23 @@ public class AlphaBetaAI extends AI { new AlphaBetaThreadCreator(commandExecutor, color, threadCount)); } - private Move getBestMove() { - List moves = getAllowedMoves(); - List> moveEvaluations = new ArrayList<>(50); + private AIAction getBestMove() { + List actions = getAllowedActions(); + List> moveEvaluations = new ArrayList<>(actions.size()); float bestMoveValue = MIN_FLOAT; - Move bestMove = null; + AIAction bestMove = null; - this.onStartEval.emit(moves.size()); + this.onStartEval.emit(actions.size()); - for (Move move : moves) { + for (AIAction action : actions) { moveEvaluations.add(this.threadPool.submit(() -> { - return AlphaBetaThreadCreator.getMoveValue(move, this.searchDepth); + return AlphaBetaThreadCreator.getMoveValue(action, this.searchDepth); })); } - for (int i = 0; i < moves.size(); i++) { - this.onProgress.emit((float) i / (float) moves.size()); - Move move = moves.get(i); + for (int i = 0; i < actions.size(); i++) { + this.onProgress.emit((float) i / (float) actions.size()); + AIAction action = actions.get(i); float value = MIN_FLOAT; try { @@ -64,7 +60,7 @@ public class AlphaBetaAI extends AI { } if (value > bestMoveValue) { bestMoveValue = value; - bestMove = move; + bestMove = action; } } @@ -80,13 +76,8 @@ public class AlphaBetaAI extends AI { @Override protected void play() { - Move move = getBestMove(); - sendCommand(new MoveCommand(move)); - } - - @Override - protected void promote(Coordinate pawnCoords) { - sendCommand(new PromoteCommand(PromoteType.Queen)); + AIAction move = getBestMove(); + move.applyAction(); } } diff --git a/app/src/main/java/chess/ai/minimax/AlphaBetaThread.java b/app/src/main/java/chess/ai/minimax/AlphaBetaThread.java index 7657037..2b0f278 100644 --- a/app/src/main/java/chess/ai/minimax/AlphaBetaThread.java +++ b/app/src/main/java/chess/ai/minimax/AlphaBetaThread.java @@ -8,10 +8,10 @@ import java.util.Map.Entry; import chess.ai.PieceCost; import chess.ai.PiecePosCost; +import chess.ai.actions.AIAction; import chess.model.ChessBoard; import chess.model.Color; import chess.model.Coordinate; -import chess.model.Move; import chess.model.Piece; public class AlphaBetaThread extends Thread { @@ -52,27 +52,27 @@ public class AlphaBetaThread extends Thread { return result; } - public float getMoveValue(Move move, int searchDepth) { - this.simulation.tryMove(move); + public float getMoveValue(AIAction move, int searchDepth) { + move.applyAction(this.simulation.getCommandExecutor()); float value = -negaMax(searchDepth - 1, MIN_FLOAT, MAX_FLOAT); - this.simulation.undoMove(); + move.undoAction(this.simulation.getCommandExecutor()); return value; } private float negaMax(int depth, float alpha, float beta) { float value = MIN_FLOAT; - List moves = this.simulation.getAllowedMoves(); + List moves = this.simulation.getAllowedActions(); if (moves.isEmpty()) return -getEndGameEvaluation(); - List> movesCost = new ArrayList<>(moves.size()); + List> movesCost = new ArrayList<>(moves.size()); - for (Move move : moves) { - this.simulation.tryMove(move); + for (AIAction move : moves) { + move.applyAction(); movesCost.add(Map.entry(move, -getBoardEvaluation())); - this.simulation.undoMove(); + move.undoAction(); } Collections.sort(movesCost, (first, second) -> { @@ -83,10 +83,10 @@ public class AlphaBetaThread extends Thread { return -movesCost.getFirst().getValue(); for (var moveEntry : movesCost) { - Move move = moveEntry.getKey(); - this.simulation.tryMove(move); + AIAction move = moveEntry.getKey(); + move.applyAction(); value = Float.max(value, -negaMax(depth - 1, -beta, -alpha)); - this.simulation.undoMove(); + move.undoAction(); alpha = Float.max(alpha, value); if (alpha >= beta) return value; diff --git a/app/src/main/java/chess/ai/minimax/AlphaBetaThreadCreator.java b/app/src/main/java/chess/ai/minimax/AlphaBetaThreadCreator.java index 8bee1d9..4e46a2b 100644 --- a/app/src/main/java/chess/ai/minimax/AlphaBetaThreadCreator.java +++ b/app/src/main/java/chess/ai/minimax/AlphaBetaThreadCreator.java @@ -2,9 +2,9 @@ package chess.ai.minimax; import java.util.concurrent.ThreadFactory; +import chess.ai.actions.AIAction; import chess.controller.CommandExecutor; import chess.model.Color; -import chess.model.Move; public class AlphaBetaThreadCreator implements ThreadFactory{ @@ -21,7 +21,7 @@ public class AlphaBetaThreadCreator implements ThreadFactory{ } } - public static float getMoveValue(Move move, int searchDepth) { + public static float getMoveValue(AIAction move, int searchDepth) { AlphaBetaThread t = (AlphaBetaThread) Thread.currentThread(); return t.getMoveValue(move, searchDepth); } diff --git a/app/src/main/java/chess/ai/minimax/GameSimulation.java b/app/src/main/java/chess/ai/minimax/GameSimulation.java index d0eda11..ef9cef1 100644 --- a/app/src/main/java/chess/ai/minimax/GameSimulation.java +++ b/app/src/main/java/chess/ai/minimax/GameSimulation.java @@ -2,16 +2,17 @@ package chess.ai.minimax; import java.util.List; +import chess.ai.actions.AIAction; +import chess.ai.actions.AIActions; import chess.controller.Command; import chess.controller.Command.CommandResult; import chess.controller.CommandExecutor; import chess.controller.commands.CastlingCommand; -import chess.controller.commands.GetPlayerMovesCommand; import chess.controller.commands.MoveCommand; import chess.controller.commands.NewGameCommand; import chess.controller.commands.PromoteCommand; -import chess.controller.commands.UndoCommand; import chess.controller.commands.PromoteCommand.PromoteType; +import chess.controller.commands.UndoCommand; import chess.controller.event.EmptyGameDispatcher; import chess.controller.event.GameAdapter; import chess.model.ChessBoard; @@ -90,10 +91,8 @@ public class GameSimulation extends GameAdapter { return this.gameSimulation.getPlayerTurn(); } - public List getAllowedMoves() { - GetPlayerMovesCommand cmd = new GetPlayerMovesCommand(); - sendCommand(cmd); - return cmd.getMoves(); + public List getAllowedActions() { + return AIActions.getAllowedActions(this.simulation); } public void close() {