From 52a663e698099af119bcf80d6b05997570d4785f Mon Sep 17 00:00:00 2001 From: mathy Date: Fri, 16 May 2025 11:52:17 +0200 Subject: [PATCH] autoplayer --- src/backend/AutoPlayer.java | 269 ++++++++++++++++++++++++++++++++++-- src/backend/Board.java | 39 +++++- src/backend/Move.java | 50 ++++++- 3 files changed, 334 insertions(+), 24 deletions(-) diff --git a/src/backend/AutoPlayer.java b/src/backend/AutoPlayer.java index a988a22..b268dac 100644 --- a/src/backend/AutoPlayer.java +++ b/src/backend/AutoPlayer.java @@ -1,17 +1,258 @@ package backend; +import java.util.ArrayList; +import java.util.Random; + public class AutoPlayer { - - - /** - * returns the best Move to try on provided board for active player - * @param board - * @return - */ - public Move computeBestMove(Board board) { - - return null; - } - - -} + + // Piece values used for evaluation and best move to do by calculating later the profit and losses (PnL) + private final int PAWN_VALUE = 1; + private final int KNIGHT_VALUE = 3; + private final int BISHOP_VALUE = 3; + private final int ROOK_VALUE = 5; + private final int QUEEN_VALUE = 9; + private final int KING_VALUE = 100; // High value to prioritize king safety + + /// This chess AI uses a minimax algorithm with alpha-beta pruning to look 2 moves ahead so depth 4 + // it will evaluate the material value, the center control, and the pawn advancement (ELO 1400-1800) + + private final int MAX_DEPTH = 4; + + // Time limit for search in milliseconds to avoid bug or latence during the game + private final long TIME_LIMIT_MS = 5000; // 5 seconds max + private long startTime; + + // Random generator if there are several moves equally calculated as good + private Random random = new Random(); + + //Returns the best Move to try + public Move computeBestMove(Board board) { + startTime = System.currentTimeMillis();//record start time for search + boolean isWhite = board.isTurnWhite();// determine player's turn + ArrayList possibleMoves = generateAllPossibleMoves(board, isWhite);// generate the legal moves + + if (possibleMoves.isEmpty()) { + return null; // If no moves available (checkmate or stalemate) + } + + Move bestMove = null;//store best move + int bestScore = isWhite ? Integer.MIN_VALUE : Integer.MAX_VALUE;//best score for the move: maximize for white and minimize for black + + // Evaluate each possible move + for (Move move : possibleMoves) { + // Create a copy of the board to simulate the move without changing the real board + Board boardCopy = new Board(board); + applyMoveToBoard(boardCopy, move);//apply the move to the virtual board + + // Evaluate the resulting position using minimax + int score = minimax(boardCopy, MAX_DEPTH - 1, Integer.MIN_VALUE, Integer.MAX_VALUE, !isWhite); + + // Update best move based on the score + if ((isWhite && score > bestScore) || (!isWhite && score < bestScore) || + (score == bestScore && random.nextBoolean())) { // Random choice if equal score + bestScore = score;//update best score + bestMove = move;//update best move + } + + // Check if time limit is exceeded and stop search if exceeded + if (System.currentTimeMillis() - startTime > TIME_LIMIT_MS) { + System.out.println("Time limit exceeded, returning best move found so far"); + return bestMove; + } + } + + return bestMove; + } + //apply the move to the real board + private void applyMoveToBoard(Board board, Move move) { + int fromX = move.getFromX();//original X coordinate + int fromY = move.getFromY();//original Y coordinate + int toX = move.getToX();//destination X + int toY = move.getToY();//destination Y + Piece piece = move.getMovedPiece();//move the piece + + // Make the move by creating the piece a the position + board.setPiece(piece.isWhite(), piece.getType(), toX, toY); + + //remove the piece that moved + try { + //access the board field for modifications + java.lang.reflect.Field boardField = Board.class.getDeclaredField("board"); + boardField.setAccessible(true);//give accessibility to it + Piece[][] boardArray = (Piece[][]) boardField.get(board);//board array + boardArray[fromX][fromY] = null;//make the case empty + } catch (Exception e) { + System.err.println("Failed to update board: " + e.getMessage()); + } + + // Increment turn + board.incrementTurn(); + } + + //Minimax algorithm with alpha-beta pruning to evaluate positions + + private int minimax(Board board, int depth, int alpha, int beta, boolean isMaximizing) { + // Check if time limit is exceeded + if (System.currentTimeMillis() - startTime > TIME_LIMIT_MS) { + return evaluateBoard(board); // Return current evaluation if out of time + } + + // get the board before calculating possible moves + if (depth == 0) { + return evaluateBoard(board);//initial position + } + //all possible moves are generated + ArrayList possibleMoves = generateAllPossibleMoves(board, isMaximizing); + possibleMoves = orderMoves(possibleMoves);//order moves by their quality + + // No moves available, could be checkmate or stalemate + if (possibleMoves.isEmpty()) { + return evaluateBoard(board);// current position + } + + if (isMaximizing) {//white turn (maximizing player) + int maxEval = Integer.MIN_VALUE;//initialize with lowest value + for (Move move : possibleMoves) {//try each move + Board boardCopy = new Board(board);//create a copy of the board + applyMoveToBoard(boardCopy, move);//apply the move + int eval = minimax(boardCopy, depth - 1, alpha, beta, false);//evaluate the resulting position + maxEval = Math.max(maxEval, eval);//gives best evaluation + alpha = Math.max(alpha, eval);//gives the alpha value + if (beta <= alpha) { + break; // Beta cutoff + } + } + return maxEval;//best evaluation found + } else {//black turn + int minEval = Integer.MAX_VALUE;//initialize with highest value + for (Move move : possibleMoves) { + Board boardCopy = new Board(board); + applyMoveToBoard(boardCopy, move); + int eval = minimax(boardCopy, depth - 1, alpha, beta, true); + minEval = Math.min(minEval, eval); + beta = Math.min(beta, eval);//gives beta value + if (beta <= alpha) { + break; // Alpha cutoff + } + } + return minEval;//best evaluation found + } + } + + //Orders moves to improve alpha-beta pruning efficiency + + private ArrayList orderMoves(ArrayList moves) { + // Score each move (captures are considered first) + for (Move move : moves) { + int score = 0;//initialize score for the move + + // Prioritize captures based on a function of the Most Valuable Victim and the Least Valuable Attacker with the system of points + if (move.getCapturedPiece() != null) { + score += 10 * getPieceValue(move.getCapturedPiece().getType()) - + getPieceValue(move.getMovedPiece().getType()) / 10; // Higher score for capturing valuable pieces with less valuable ones + } + + // Store the score with the move + move.setScore(score); + } + + // Sort moves by score in a descending + moves.sort((a, b) -> Integer.compare(b.getScore(), a.getScore())); + + return moves; + } + + //Evaluates the current board position + + private int evaluateBoard(Board board) { + int score = 0; + + // Material evaluation + for (Piece piece : board.getPieces()) { + int pieceValue = getPieceValue(piece.getType());//get value of this piece + if (piece.isWhite()) { + score += pieceValue;//add value for white + } else { + score -= pieceValue;//substract value for black + } + } + + // Add position evaluation (takes in account the center control bonus) + for (Piece piece : board.getPieces()) { + int x = piece.getX(); + int y = piece.getY(); + + // Center control bonus (small bonus for controlling center squares) + int centerBonus = 0; + if ((x == 3 || x == 4) && (y == 3 || y == 4)) {//center 4 squares + centerBonus = 1; // Small bonus for center control + } + + if (piece.isWhite()) { + score += centerBonus;//add for white + } else { + score -= centerBonus;//subtract for black + } + + // Pawn advancement bonus (encourages pawn progression) + if (piece.getType() == PieceType.Pawn) { + int pawnBonus = 0; + if (piece.isWhite()) { + pawnBonus = 7 - y; // Higher bonus as white pawns advance + score += pawnBonus / 10; // Small fractional bonus + } else { + pawnBonus = y; // Higher bonus as black pawns advance + score -= pawnBonus / 10; // Small fractional bonus + } + } + } + + return score;//return final score + } + + //Gets the value of a piece + + private int getPieceValue(PieceType type) { + switch (type) { + case Pawn: return PAWN_VALUE; + case Knight: return KNIGHT_VALUE; + case Bishop: return BISHOP_VALUE; + case Rook: return ROOK_VALUE; + case Queen: return QUEEN_VALUE; + case King: return KING_VALUE; + default: return 0;// safety net to protect from bug + } + } + + //Generates all possible moves for a player + + private ArrayList generateAllPossibleMoves(Board board, boolean isWhite) { + ArrayList moves = new ArrayList<>();//list of all moves + + // Get all pieces of the current player + for (Piece piece : board.getPieces()) { + if (piece.isWhite() == isWhite) {//only consider pieces of the player + // Get valid moves for this piece + ArrayList validMoves = piece.getValidMoves(board); + + for (int[] destination : validMoves) {//move object for each valid destination + int toX = destination[0];//X + int toY = destination[1];//Y + Piece capturedPiece = board.getPiece(toX, toY);//check is a piece is capturable + + // Create a move object with all theses infos + Move move = new Move( + piece.getX(), piece.getY(),//start X Y + toX, toY, //destination X Y + piece, //piece moved + capturedPiece //piece captured (if there i one) + ); + + moves.add(move);// add move to list + } + } + } + + return moves;//return all moves + } +} \ No newline at end of file diff --git a/src/backend/Board.java b/src/backend/Board.java index e38846c..afca7ba 100644 --- a/src/backend/Board.java +++ b/src/backend/Board.java @@ -339,10 +339,43 @@ public class Board { } public void playMove(Move move) { - // TODO + int fromX = move.getFromX(); + int fromY = move.getFromY(); + int toX = move.getToX(); + int toY = move.getToY(); + Piece piece = board[fromX][fromY]; + + // Make the move + board[toX][toY] = new Piece(toX, toY, piece.getType(), piece.isWhite()); + board[toX][toY].setMoved(true); + board[fromX][fromY] = null; + + incrementTurn(); } - public Board(Board board) { - // TODO + + // Copy constructor that creates a copy of another board + + public Board(Board otherBoard) { + this.width = otherBoard.width; + this.height = otherBoard.height; + this.turnNumber = otherBoard.turnNumber; + this.selectedX = -1; // Don't copy selection because negative in a matrix + this.selectedY = -1; + this.board = new Piece[width][height]; + this.highlightedPositions = new ArrayList<>(); + + // Deep copy the board pieces + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + if (otherBoard.board[x][y] != null) { + Piece original = otherBoard.board[x][y]; + this.board[x][y] = new Piece(x, y, original.getType(), original.isWhite()); + // Remove the hasMoved check since it doesn't exist + } + } + } } } + + diff --git a/src/backend/Move.java b/src/backend/Move.java index 7068300..91e979d 100644 --- a/src/backend/Move.java +++ b/src/backend/Move.java @@ -1,12 +1,48 @@ package backend; -import java.util.Optional; +// Represents a chess move from one position to another -/** - * Represents a chess move, including the starting and ending positions, - * the moving piece, and an optional captured piece. - */ public class Move { - - + private int fromX; // Starting X coordinate + private int fromY; // Starting Y coordinate + private int toX; // Destination X coordinate + private int toY; // Destination Y coordinate + private Piece movedPiece; // The piece being moved + private Piece capturedPiece;// The piece being captured (null if none) + private int score = 0; // Used for move ordering in alpha-beta pruning + + //Constructor for a move + + public Move(int fromX, int fromY, int toX, int toY, Piece movedPiece, Piece capturedPiece) { + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + this.movedPiece = movedPiece; + this.capturedPiece = capturedPiece; + } + + // Getters to access move properties + public int getFromX() { return fromX; } + public int getFromY() { return fromY; } + public int getToX() { return toX; } + public int getToY() { return toY; } + public Piece getMovedPiece() { return movedPiece; } + public Piece getCapturedPiece() { return capturedPiece; } + + // Method for move ordering + public void setScore(int score) { + this.score = score; + } + + public int getScore() { + return score;//return the evaluation score of the move + } + + @Override + public String toString() {//coordinates into chess notation + String from = (char)('a' + fromX) + "" + (8 - fromY); + String to = (char)('a' + toX) + "" + (8 - toY); + return from + " -> " + to; + } }