diff --git a/OOP_3B6_Project/src/backend/AutoPlayer.java b/OOP_3B6_Project/src/backend/AutoPlayer.java index a988a22..df8ac0e 100644 --- a/OOP_3B6_Project/src/backend/AutoPlayer.java +++ b/OOP_3B6_Project/src/backend/AutoPlayer.java @@ -1,17 +1,336 @@ 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; - } - - + private final int MAX_DEPTH = 3; // Depth of search + private final Random random = new Random(); + + /** + * Returns the best Move to try on provided board for active player + * @param board + * @return + */ + public Move computeBestMove(Board board) { + ArrayList possibleMoves = generateAllPossibleMoves(board); + + if (possibleMoves.isEmpty()) { + return null; // No moves available + } + + Move bestMove = null; + int bestScore = Integer.MIN_VALUE; + + for (Move move : possibleMoves) { + // Create a copy of the board and apply the move + Board tempBoard = new Board(board); + tempBoard.playMove(move); + + // Evaluate the move using negamax + int score = -negamax(tempBoard, MAX_DEPTH - 1, Integer.MIN_VALUE + 1, Integer.MAX_VALUE - 1); + + // If this move is better than our current best, update bestMove + if (score > bestScore || (score == bestScore && random.nextBoolean())) { + bestScore = score; + bestMove = move; + } + } + + return bestMove; + } + + private int negamax(Board board, int depth, int alpha, int beta) { + // If we've reached the maximum depth or the game is over, evaluate the board + if (depth == 0) { + return evaluateBoard(board); + } + + ArrayList possibleMoves = generateAllPossibleMoves(board); + + // If no moves are available, this might be checkmate or stalemate + if (possibleMoves.isEmpty()) { + return -1000; // Heavily penalize positions with no moves + } + + int bestScore = Integer.MIN_VALUE; + + for (Move move : possibleMoves) { + // Create a copy of the board and apply the move + Board tempBoard = new Board(board); + tempBoard.playMove(move); + + // Recursively evaluate the position + int score = -negamax(tempBoard, depth - 1, -beta, -alpha); + + bestScore = Math.max(bestScore, score); + alpha = Math.max(alpha, score); + + // Alpha-beta pruning + if (alpha >= beta) { + break; + } + } + + return bestScore; + } + + private ArrayList generateAllPossibleMoves(Board board) { + ArrayList moves = new ArrayList<>(); + boolean isWhiteTurn = board.isTurnWhite(); + + for (Piece piece : board.getPieces()) { + // Only consider pieces of the current player + if (piece.isWhite() != isWhiteTurn) { + continue; + } + + // Get all legal moves for this piece + ArrayList legalMoves = computeLegalMoves(board, piece); + + for (int[] destination : legalMoves) { + int toX = destination[0]; + int toY = destination[1]; + + // Check if there's a piece to capture + Piece capturedPiece = null; + for (Piece p : board.getPieces()) { + if (p.getX() == toX && p.getY() == toY) { + capturedPiece = p; + break; + } + } + + // Create a move + Move move = new Move( + piece.getX(), piece.getY(), + toX, toY, + piece.isWhite(), + piece.getType(), + capturedPiece + ); + + moves.add(move); + } + } + + return moves; + } + + private ArrayList computeLegalMoves(Board board, Piece piece) { + ArrayList moves = new ArrayList<>(); + int x = piece.getX(); + int y = piece.getY(); + boolean isWhite = piece.isWhite(); + int width = board.getWidth(); + int height = board.getHeight(); + + switch (piece.getType()) { + case Pawn: + int dir = isWhite ? -1 : 1; + int startRow = isWhite ? 6 : 1; + + // Forward move + if (getPieceAt(board, x, y + dir) == null) { + moves.add(new int[]{x, y + dir}); + // Double move from starting position + if (y == startRow && getPieceAt(board, x, y + 2 * dir) == null) { + moves.add(new int[]{x, y + 2 * dir}); + } + } + + // Captures + for (int dx = -1; dx <= 1; dx += 2) { + int nx = x + dx; + int ny = y + dir; + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + Piece target = getPieceAt(board, nx, ny); + if (target != null && target.isWhite() != isWhite) { + moves.add(new int[]{nx, ny}); + } + } + } + break; + + case Rook: + addSlidingMoves(board, moves, x, y, isWhite, new int[][]{{1,0},{-1,0},{0,1},{0,-1}}); + break; + + case Bishop: + addSlidingMoves(board, moves, x, y, isWhite, new int[][]{{1,1},{1,-1},{-1,1},{-1,-1}}); + break; + + case Queen: + addSlidingMoves(board, moves, x, y, isWhite, new int[][]{ + {1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1} + }); + break; + + case Knight: + int[][] knightMoves = { + {1,2}, {1,-2}, {-1,2}, {-1,-2}, + {2,1}, {2,-1}, {-2,1}, {-2,-1} + }; + for (int[] move : knightMoves) { + int nx = x + move[0]; + int ny = y + move[1]; + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + Piece target = getPieceAt(board, nx, ny); + if (target == null || target.isWhite() != isWhite) { + moves.add(new int[]{nx, ny}); + } + } + } + break; + + case King: + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + if (dx == 0 && dy == 0) continue; + int nx = x + dx; + int ny = y + dy; + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + Piece target = getPieceAt(board, nx, ny); + if (target == null || target.isWhite() != isWhite) { + moves.add(new int[]{nx, ny}); + } + } + } + } + break; + } + + return moves; + } + + private void addSlidingMoves(Board board, ArrayList moves, int x, int y, boolean isWhite, int[][] directions) { + int width = board.getWidth(); + int height = board.getHeight(); + + for (int[] dir : directions) { + for (int i = 1; i < 8; i++) { + int nx = x + dir[0] * i; + int ny = y + dir[1] * i; + if (nx < 0 || nx >= width || ny < 0 || ny >= height) break; + + Piece target = getPieceAt(board, nx, ny); + if (target == null) { + moves.add(new int[]{nx, ny}); + } else { + if (target.isWhite() != isWhite) { + moves.add(new int[]{nx, ny}); + } + break; + } + } + } + } + + private Piece getPieceAt(Board board, int x, int y) { + for (Piece p : board.getPieces()) { + if (p.getX() == x && p.getY() == y) { + return p; + } + } + return null; + } + + private int evaluateBoard(Board board) { + int score = 0; + boolean isWhiteTurn = board.isTurnWhite(); + + // Material value + for (Piece piece : board.getPieces()) { + int pieceValue = getPieceValue(piece.getType()); + + // Add value if it's our piece, subtract if it's opponent's + if (piece.isWhite() == isWhiteTurn) { + score += pieceValue; + } else { + score -= pieceValue; + } + + // Position bonuses + score += getPositionBonus(piece, isWhiteTurn); + } + + // Mobility (number of legal moves) + ArrayList possibleMoves = generateAllPossibleMoves(board); + score += possibleMoves.size() / 10; // Small bonus for mobility + + return score; + } + + private int getPieceValue(PieceType type) { + switch (type) { + case Pawn: return 100; + case Knight: return 320; + case Bishop: return 330; + case Rook: return 500; + case Queen: return 900; + case King: return 20000; // Very high value to prioritize king safety + default: return 0; + } + } + + private int getPositionBonus(Piece piece, boolean isWhiteTurn) { + int bonus = 0; + int x = piece.getX(); + int y = piece.getY(); + + // Adjust y coordinate for black pieces to use the same tables + if (!piece.isWhite()) { + y = 7 - y; + } + + switch (piece.getType()) { + case Pawn: + // Bonus for advancing pawns + bonus += y * 10; + + // Bonus for central pawns + if (x >= 2 && x <= 5) { + bonus += 10; + } + break; + + case Knight: + // Knights are better in the center + if (x >= 2 && x <= 5 && y >= 2 && y <= 5) { + bonus += 20; + } + break; + + case Bishop: + // Bishops control more squares from the center + if (x >= 2 && x <= 5 && y >= 2 && y <= 5) { + bonus += 15; + } + break; + + case Rook: + // Rooks are good on open files and 7th rank + if (y == 6) { + bonus += 20; + } + break; + + case Queen: + // Queens are slightly better in the center + if (x >= 2 && x <= 5 && y >= 2 && y <= 5) { + bonus += 10; + } + break; + + case King: + // Kings are safer at the edge in the middlegame + if ((x <= 1 || x >= 6) && y <= 1) { + bonus += 30; + } + break; + } + + // Adjust sign based on whose piece it is + return piece.isWhite() == isWhiteTurn ? bonus : -bonus; + } } diff --git a/OOP_3B6_Project/src/backend/Board.java b/OOP_3B6_Project/src/backend/Board.java index 32613c7..932767b 100644 --- a/OOP_3B6_Project/src/backend/Board.java +++ b/OOP_3B6_Project/src/backend/Board.java @@ -190,11 +190,50 @@ public class Board { } public Board(Board board) { - // TODO: Part 4 - Clone + this.width = board.width; + this.height = board.height; + this.turnNumber = board.turnNumber; + this.isWhiteTurn = board.isWhiteTurn; + this.selectedX = board.selectedX; + this.selectedY = board.selectedY; + + // Deep copy of pieces + this.pieces = new ArrayList<>(); + for (Piece p : board.pieces) { + this.pieces.add(new Piece(p.getX(), p.getY(), p.isWhite(), p.getType())); + } + + // Copy highlighted squares + this.highlightedSquares = new ArrayList<>(); + for (int[] square : board.highlightedSquares) { + this.highlightedSquares.add(new int[]{square[0], square[1]}); + } + + // We don't copy the move history as it's not needed for evaluation + this.moveHistory = new Stack<>(); } public void playMove(Move move) { - // TODO: Part 4 - AutoPlayer + if (move == null) return; + + // Store move for undo + moveHistory.push(move); + + // Remove captured piece if any + if (move.getCapturedPiece() != null) { + removePieceAt(move.getToX(), move.getToY()); + } + + // Remove piece from original position + removePieceAt(move.getFromX(), move.getFromY()); + + // Add piece to new position + pieces.add(new Piece(move.getToX(), move.getToY(), move.isWhite(), move.getType())); + + // Update turn + turnNumber++; + isWhiteTurn = !isWhiteTurn; + } // ========== Helper Methods ========== @@ -312,4 +351,5 @@ public class Board { } } +