This commit is contained in:
Yash Shah 2025-05-22 08:35:40 +02:00
parent ccabbde52d
commit 36e923650e
3 changed files with 89 additions and 78 deletions

View File

@ -4,13 +4,7 @@ import java.util.ArrayList;
import java.util.Random; import java.util.Random;
public class AutoPlayer { public class AutoPlayer {
//Creates an Empty list to hold all the legal moves for the current player
/**
* returns the best Move to try on provided board for active player
* @param board
* @return
*/
public Move computeBestMove(Board board) {ArrayList<Move> possibleMoves = new ArrayList<>(); public Move computeBestMove(Board board) {ArrayList<Move> possibleMoves = new ArrayList<>();
boolean isWhiteTurn = board.isTurnWhite(); boolean isWhiteTurn = board.isTurnWhite();
@ -26,7 +20,7 @@ public class AutoPlayer {
} }
} }
// If there are no moves available, return null // If there are no moves available, return null- Handles checkmate or stalemate
if (possibleMoves.isEmpty()) { if (possibleMoves.isEmpty()) {
return null; return null;
} }

View File

@ -5,24 +5,30 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Board { public class Board {
//class fields - All core game state lives here
private int width; private int width;
private int height; private int height;
private ArrayList<Piece> pieces; private ArrayList<Piece> pieces;
private int turnNumber = 0; private int turnNumber = 0;
private boolean turnWhite = true; private boolean turnWhite = true;
//for UI State
private int[] selected = null; //for user touch private int[] selected = null; //for user touch
private ArrayList<int[]> highlighted = new ArrayList<>(); private ArrayList<int[]> highlighted = new ArrayList<>();
//for Undo and Special rules
private ArrayList<Move> moveHistory = new ArrayList<>(); private ArrayList<Move> moveHistory = new ArrayList<>();
private int[] enPassantTarget = null; private int[] enPassantTarget = null;
//Constructors - Initialize the empty board of the specific size 8*8
public Board(int colNum, int lineNum) { public Board(int colNum, int lineNum) {
this.width = colNum; this.width = colNum;
this.height = lineNum; this.height = lineNum;
this.pieces = new ArrayList<>(); this.pieces = new ArrayList<>();
this.enPassantTarget = null; this.enPassantTarget = null;
} }
//Deep Copy - clones the entire board to simulate without mutating the original
//Allows safe simulation without side effects on the source board
public Board(Board other) { public Board(Board other) {
//copy primitives and simple fields
this.width = other.width; this.width = other.width;
this.height = other.height; this.height = other.height;
this.turnNumber = other.turnNumber; this.turnNumber = other.turnNumber;
@ -33,22 +39,26 @@ public class Board {
: null; : null;
//Deep copy each piece
this.pieces = new ArrayList<>(); this.pieces = new ArrayList<>();
for (Piece p : other.pieces) { for (Piece p : other.pieces) {
this.pieces.add(new Piece(p)); // uses Piece copy constructor (next step) this.pieces.add(new Piece(p)); // uses Piece copy constructor (next step)
} }
//Copy UI Highlights
this.highlighted = new ArrayList<>(); this.highlighted = new ArrayList<>();
for (int[] pos : other.highlighted) { for (int[] pos : other.highlighted) {
this.highlighted.add(new int[]{pos[0], pos[1]}); this.highlighted.add(new int[]{pos[0], pos[1]});
} }
//shallow copy move history
this.moveHistory = new ArrayList<>(other.moveHistory); this.moveHistory = new ArrayList<>(other.moveHistory);
} }
//Simple Getters
//returns board with column
public int getWidth() { public int getWidth() {
return width; return width;
} }
@ -61,10 +71,13 @@ public class Board {
return turnNumber; return turnNumber;
} }
//return true if its white turn to move
public boolean isTurnWhite() { public boolean isTurnWhite() {
return turnWhite; return turnWhite;
} }
//places a new piece of a given colour & type at (x,y)
//used by populateBoard
public void setPiece(boolean isWhite, PieceType type, int x, int y) { public void setPiece(boolean isWhite, PieceType type, int x, int y) {
Piece piece = new Piece(isWhite, type, x, y); Piece piece = new Piece(isWhite, type, x, y);
pieces.add(piece); pieces.add(piece);
@ -73,11 +86,12 @@ public class Board {
public void populateBoard() { public void populateBoard() {
cleanBoard(); // clear any existing pieces cleanBoard(); // clear any existing pieces
// White pieces // White pieces- Places white pawns at rank 1
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
setPiece(true, PieceType.Pawn, i, 1); setPiece(true, PieceType.Pawn, i, 1);
} }
setPiece(true, PieceType.Rook, 0, 0); //places white back-rank pieces
setPiece(true, PieceType.Rook, 1, 0);
setPiece(true, PieceType.Knight, 1, 0); setPiece(true, PieceType.Knight, 1, 0);
setPiece(true, PieceType.Bishop, 2, 0); setPiece(true, PieceType.Bishop, 2, 0);
setPiece(true, PieceType.Queen, 3, 0); setPiece(true, PieceType.Queen, 3, 0);
@ -86,7 +100,7 @@ public class Board {
setPiece(true, PieceType.Knight, 6, 0); setPiece(true, PieceType.Knight, 6, 0);
setPiece(true, PieceType.Rook, 7, 0); setPiece(true, PieceType.Rook, 7, 0);
// Black pieces // Black pieces - positions
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
setPiece(false, PieceType.Pawn, i, 6); setPiece(false, PieceType.Pawn, i, 6);
} }
@ -100,11 +114,17 @@ public class Board {
setPiece(false, PieceType.Rook, 7, 7); setPiece(false, PieceType.Rook, 7, 7);
} }
//empties the board state
//used before populating or loading new positions
//resets all pieces and reset en passant state
public void cleanBoard() { public void cleanBoard() {
pieces.clear(); pieces.clear();
enPassantTarget = null; enPassantTarget = null;
} }
//renders the board as ASCII art (eg. "..p......")
//one rank per line and uses Uppercase for BLACK and lowercase for White
//could have used char[][] for memory efficiency
public String toString() { public String toString() {
String[][] grid = new String[height][width]; String[][] grid = new String[height][width];
@ -137,10 +157,13 @@ public class Board {
} }
//Lookup by coordinates and helpful for move generation and UI
//return all active pieces on the board
public ArrayList<Piece> getPieces() { public ArrayList<Piece> getPieces() {
return pieces; return pieces;
} }
//finds the king side - necessary for check/checkmate
//returns the king piece for the given colour , or null if missing
public Piece getKing(boolean isWhite) { public Piece getKing(boolean isWhite) {
for (Piece p : pieces) { for (Piece p : pieces) {
if (p.isWhite() == isWhite && p.getType() == PieceType.King) { if (p.isWhite() == isWhite && p.getType() == PieceType.King) {
@ -149,7 +172,7 @@ public class Board {
} }
return null; return null;
} }
//Scans every opponent move to see if it lands on your king
public boolean isInCheck(boolean whitePlayer) { public boolean isInCheck(boolean whitePlayer) {
Piece king = getKing(whitePlayer); Piece king = getKing(whitePlayer);
if (king == null) return false; if (king == null) return false;
@ -170,10 +193,11 @@ public class Board {
return false; return false;
} }
//Declares Checkmate if in check and no legal moves left
public boolean isCheckmate(boolean whitePlayer) { public boolean isCheckmate(boolean whitePlayer) {
if (!isInCheck(whitePlayer)) return false; if (!isInCheck(whitePlayer)) return false;
//if any legal moves exists - not checkmate
for (Piece p : pieces) { for (Piece p : pieces) {
if (p.isWhite() == whitePlayer) { if (p.isWhite() == whitePlayer) {
ArrayList<int[]> legalMoves = getLegalMoves(p); ArrayList<int[]> legalMoves = getLegalMoves(p);
@ -184,6 +208,8 @@ public class Board {
return true; return true;
} }
//Implements a two-tap GUI: First selects a Piece
//then highlights the move and second tap either moves , reselect or deselects
public void userTouch(int x, int y) { public void userTouch(int x, int y) {
Piece clickedPiece = getPieceAt(x, y); Piece clickedPiece = getPieceAt(x, y);
@ -195,6 +221,7 @@ public class Board {
} }
} else { } else {
// Second click: check if move is legal // Second click: check if move is legal
//it also checks if target is highlighted,then play move
if (isHighlighted(x, y)) { if (isHighlighted(x, y)) {
Piece piece = getPieceAt(selected[0], selected[1]); Piece piece = getPieceAt(selected[0], selected[1]);
Piece captured = getPieceAt(x, y); Piece captured = getPieceAt(x, y);
@ -225,6 +252,7 @@ public class Board {
public boolean isSimulateOnly() { public boolean isSimulateOnly() {
return simulateOnly; return simulateOnly;
} }
//is used to validate castling and pinned-piece moves
private boolean isSquareUnderAttack(boolean byWhite, int x, int y) { private boolean isSquareUnderAttack(boolean byWhite, int x, int y) {
for (Piece p : pieces) { for (Piece p : pieces) {
if (p.isWhite() != byWhite) continue; if (p.isWhite() != byWhite) continue;
@ -237,6 +265,7 @@ public class Board {
} }
//generates a pseudo-legal moves by piece type and then filters out those that leave your king in check
public ArrayList<int[]> getLegalMoves(Piece piece) { public ArrayList<int[]> getLegalMoves(Piece piece) {
return getLegalMoves(piece, true); // default: check for king safety return getLegalMoves(piece, true); // default: check for king safety
} }
@ -248,6 +277,8 @@ public class Board {
switch (piece.getType()) { switch (piece.getType()) {
case Pawn: case Pawn:
//single & double advance
//captures(including en passant)
int forwardY = y + dir; int forwardY = y + dir;
if (isInBounds(x, forwardY) && getPieceAt(x, forwardY) == null) { if (isInBounds(x, forwardY) && getPieceAt(x, forwardY) == null) {
@ -300,6 +331,7 @@ public class Board {
break; break;
case King: case King:
//one square in every direction + castling
for (int dx = -1; dx <= 1; dx++) { for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) { for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue; if (dx == 0 && dy == 0) continue;
@ -313,7 +345,6 @@ public class Board {
} }
} }
// Castling logic
// Castling logic // Castling logic
if (!piece.hasMoved() && (!validateCheck || !isInCheck(piece.isWhite()))) { if (!piece.hasMoved() && (!validateCheck || !isInCheck(piece.isWhite()))) {
int row = piece.isWhite() ? 0 : 7; int row = piece.isWhite() ? 0 : 7;
@ -388,7 +419,8 @@ public class Board {
return legalMoves; return legalMoves;
} }
//walks in direction (dx,dy) adding empty squares and a single capture
//Avoids repeating rook/bishop/queen code
private void addLinearMoves(ArrayList<int[]> moves, Piece piece, int dx, int dy) { private void addLinearMoves(ArrayList<int[]> moves, Piece piece, int dx, int dy) {
int x = piece.getX(), y = piece.getY(); int x = piece.getX(), y = piece.getY();
while (true) { while (true) {
@ -406,6 +438,8 @@ public class Board {
} }
} }
//helpers for bounds checks and UI flags
//return true of (x,y) is a valid board square
private boolean isInBounds(int x, int y) { private boolean isInBounds(int x, int y) {
return x >= 0 && y >= 0 && x < width && y < height; return x >= 0 && y >= 0 && x < width && y < height;
} }
@ -417,27 +451,22 @@ public class Board {
/* saving-loading feature :*/ /* saving-loading feature :*/
// @return each line representing one piece, plus a header:
// in Board.java // Line 0: width,height,turnNumber,turnWhite,enPassantX,enPassantY
// Lines 1N: serialized Pieces
/**
* @return each line representing one piece, plus a header:
* Line 0: width,height,turnNumber,turnWhite,enPassantX,enPassantY
* Lines 1N: serialized Pieces
*/
public String[] toFileRep() { public String[] toFileRep() {
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
int epX = enPassantTarget == null ? -1 : enPassantTarget[0]; int epX = enPassantTarget == null ? -1 : enPassantTarget[0];
int epY = enPassantTarget == null ? -1 : enPassantTarget[1]; int epY = enPassantTarget == null ? -1 : enPassantTarget[1];
// header // header:width,height,turnNumber,turnWhite,enPassantX,enpassantY
lines.add(width + "," + lines.add(width + "," +
height + "," + height + "," +
turnNumber + "," + turnNumber + "," +
turnWhite + "," + turnWhite + "," +
epX + "," + epY); epX + "," + epY);
// pieces // pieces - each piece's own serialization
for (Piece p : pieces) { for (Piece p : pieces) {
lines.add(p.toFileRep()); lines.add(p.toFileRep());
} }
@ -445,12 +474,9 @@ public class Board {
return lines.toArray(new String[0]); return lines.toArray(new String[0]);
} }
// in Board.java
public Board(String[] array) { public Board(String[] array) {
this.pieces = new ArrayList<>(); this.pieces = new ArrayList<>();
// parse header // parse header fields
String[] hdr = array[0].split(","); String[] hdr = array[0].split(",");
this.width = Integer.parseInt(hdr[0]); this.width = Integer.parseInt(hdr[0]);
this.height = Integer.parseInt(hdr[1]); this.height = Integer.parseInt(hdr[1]);
@ -466,13 +492,13 @@ public class Board {
this.pieces.add(p); this.pieces.add(p);
} }
// clear other state // clear other state - resets UI and History
this.selected = null; this.selected = null;
this.highlighted = new ArrayList<>(); this.highlighted = new ArrayList<>();
this.moveHistory = new ArrayList<>(); this.moveHistory = new ArrayList<>();
} }
//return the Piece at (x,y) or null if none present
public Piece getPieceAt(int x, int y) { public Piece getPieceAt(int x, int y) {
for (Piece piece : pieces) { for (Piece piece : pieces) {
if (piece.getX() == x && piece.getY() == y) { if (piece.getX() == x && piece.getY() == y) {
@ -482,17 +508,14 @@ public class Board {
return null; return null;
} }
//returns true if (x,y) is one of the highlighted legal moves
/* The following methods require more work ! */
public boolean isHighlighted(int x, int y) { public boolean isHighlighted(int x, int y) {
for (int[] pos : highlighted) { for (int[] pos : highlighted) {
if (pos[0] == x && pos[1] == y) return true; if (pos[0] == x && pos[1] == y) return true;
} }
//TODO
return false; return false;
} }
//reverses the very last playMove including rook reposition for castling and recovers captured pawns
public void undoLastMove() { public void undoLastMove() {
if (moveHistory.isEmpty()) return; if (moveHistory.isEmpty()) return;
@ -539,7 +562,7 @@ public void undoLastMove() {
selected = null; selected = null;
highlighted.clear(); highlighted.clear();
} }
//applies every rule: castling,en passant,captures,promotions,history and turn switching
public void playMove(Move move) { public void playMove(Move move) {
Piece piece = move.getPieceMoved(); Piece piece = move.getPieceMoved();
int fromX = move.getFromX(), fromY = move.getFromY(); int fromX = move.getFromX(), fromY = move.getFromY();
@ -614,7 +637,8 @@ public void playMove(Move move) {
highlighted.clear(); highlighted.clear();
} }
public int evaluateBoard() { //Simple static evaluation for negamax
public int evaluateBoard() {
int score = 0; int score = 0;
for (Piece piece : pieces) { for (Piece piece : pieces) {
int value = 0; int value = 0;
@ -637,9 +661,8 @@ public void playMove(Move move) {
BLACK_WINS // white is checkmated BLACK_WINS // white is checkmated
} }
/**
* @return the current game result: ONGOING, DRAW, WHITE_WINS or BLACK_WINS. //@return the current game result: ONGOING, DRAW, WHITE_WINS or BLACK_WINS.
*/
public GameResult getGameResult() { public GameResult getGameResult() {
// if White is checkmated Black wins // if White is checkmated Black wins
if (isCheckmate(true)) return GameResult.BLACK_WINS; if (isCheckmate(true)) return GameResult.BLACK_WINS;
@ -650,10 +673,8 @@ public void playMove(Move move) {
return GameResult.ONGOING; return GameResult.ONGOING;
} }
/** //@param whitePlayer whose turn we're checking for stalemate
* @param whitePlayer whose turn we're checking for stalemate //@return true if whitePlayer is not in check and has no legal moves
* @return true if whitePlayer is not in check and has no legal moves
*/
private boolean isStalemate(boolean whitePlayer) { private boolean isStalemate(boolean whitePlayer) {
// must not be in check // must not be in check
if (isInCheck(whitePlayer)) return false; if (isInCheck(whitePlayer)) return false;

View File

@ -1,19 +1,16 @@
package backend; package backend;
/**
* A simple twoplayer chess clock with an optional increment. //A simple twoplayer chess clock with an optional increment.
* Getters do NOT mutate state; only switchPlayer() and pause() do. //Getters do NOT mutate state; only switchPlayer() and pause() do.
*/
public class ChessClock { public class ChessClock {
private long whiteTimeMs, blackTimeMs; // stored remaining times private long whiteTimeMs, blackTimeMs; // stored remaining times
private final long incrementMs; // permove bonus private final long incrementMs; // permove bonus
private boolean whiteToMove; // whose clock is running private boolean whiteToMove; // whose clock is running
private long lastTimestamp; // when current turn started private long lastTimestamp; // when current turn started
/** //@param initialTimeMs starting time per player (ms)
* @param initialTimeMs starting time per player (ms) //@param incrementMs permove increment (ms)
* @param incrementMs permove increment (ms)
*/
public ChessClock(long initialTimeMs, long incrementMs) { public ChessClock(long initialTimeMs, long incrementMs) {
this.whiteTimeMs = initialTimeMs; this.whiteTimeMs = initialTimeMs;
this.blackTimeMs = initialTimeMs; this.blackTimeMs = initialTimeMs;
@ -22,10 +19,9 @@ public class ChessClock {
this.lastTimestamp = System.currentTimeMillis(); this.lastTimestamp = System.currentTimeMillis();
} }
/**
* Called only when a move is made to flip the clock. //Called only when a move is made to flip the clock.
* Subtracts the elapsed time, adds the increment, and switches sides. //Subtracts the elapsed time, adds the increment, and switches sides.
*/
public void switchPlayer() { public void switchPlayer() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long elapsed = now - lastTimestamp; long elapsed = now - lastTimestamp;
@ -40,10 +36,10 @@ public class ChessClock {
lastTimestamp = now; lastTimestamp = now;
} }
/**
* Pause the clock (e.g. at game end). //Pause the clock (e.g. at game end).
* Subtracts elapsed from whichever side was running. // Subtracts elapsed from whichever side was running.
*/
public void pause() { public void pause() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long elapsed = now - lastTimestamp; long elapsed = now - lastTimestamp;
@ -55,9 +51,9 @@ public class ChessClock {
lastTimestamp = now; lastTimestamp = now;
} }
/**
* @return Whites remaining time (ms), computed without mutating state //@return Whites remaining time (ms), computed without mutating state
*/
public long getWhiteTimeMs() { public long getWhiteTimeMs() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (whiteToMove) { if (whiteToMove) {
@ -68,9 +64,9 @@ public class ChessClock {
} }
} }
/**
* @return Blacks remaining time (ms), computed without mutating state //@return Blacks remaining time (ms), computed without mutating state
*/
public long getBlackTimeMs() { public long getBlackTimeMs() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (!whiteToMove) { if (!whiteToMove) {
@ -81,16 +77,16 @@ public class ChessClock {
} }
} }
/**
* @return true if either player has run out of time //@return true if either player has run out of time
*/
public boolean isFlagged() { public boolean isFlagged() {
return getWhiteTimeMs() <= 0 || getBlackTimeMs() <= 0; return getWhiteTimeMs() <= 0 || getBlackTimeMs() <= 0;
} }
/**
* Format mm:ss for display //Format mm:ss for display
*/
public static String format(long timeMs) { public static String format(long timeMs) {
long totalSec = Math.max(timeMs / 1000, 0); long totalSec = Math.max(timeMs / 1000, 0);
long minutes = totalSec / 60; long minutes = totalSec / 60;