691 lines
24 KiB
Java
691 lines
24 KiB
Java
|
||
package backend;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
|
||
public class Board {
|
||
//class fields - All core game state lives here
|
||
private int width;
|
||
private int height;
|
||
private ArrayList<Piece> pieces;
|
||
private int turnNumber = 0;
|
||
private boolean turnWhite = true;
|
||
//for UI State
|
||
private int[] selected = null; //for user touch
|
||
private ArrayList<int[]> highlighted = new ArrayList<>();
|
||
//for Undo and Special rules
|
||
private ArrayList<Move> moveHistory = new ArrayList<>();
|
||
private int[] enPassantTarget = null;
|
||
|
||
//Constructors - Initialize the empty board of the specific size 8*8
|
||
public Board(int colNum, int lineNum) {
|
||
this.width = colNum;
|
||
this.height = lineNum;
|
||
this.pieces = new ArrayList<>();
|
||
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) {
|
||
//copy primitives and simple fields
|
||
this.width = other.width;
|
||
this.height = other.height;
|
||
this.turnNumber = other.turnNumber;
|
||
this.turnWhite = other.turnWhite;
|
||
this.selected = other.selected != null ? new int[]{other.selected[0], other.selected[1]} : null;
|
||
this.enPassantTarget = other.enPassantTarget != null
|
||
? new int[]{ other.enPassantTarget[0], other.enPassantTarget[1] }
|
||
: null;
|
||
|
||
|
||
//Deep copy each piece
|
||
this.pieces = new ArrayList<>();
|
||
for (Piece p : other.pieces) {
|
||
this.pieces.add(new Piece(p)); // uses Piece copy constructor (next step)
|
||
}
|
||
|
||
//Copy UI Highlights
|
||
this.highlighted = new ArrayList<>();
|
||
for (int[] pos : other.highlighted) {
|
||
this.highlighted.add(new int[]{pos[0], pos[1]});
|
||
|
||
}
|
||
//shallow copy move history
|
||
this.moveHistory = new ArrayList<>(other.moveHistory);
|
||
}
|
||
|
||
|
||
|
||
//Simple Getters
|
||
//returns board with column
|
||
public int getWidth() {
|
||
return width;
|
||
}
|
||
|
||
public int getHeight() {
|
||
return height;
|
||
}
|
||
|
||
public int getTurnNumber() {
|
||
return turnNumber;
|
||
}
|
||
|
||
//return true if its white turn to move
|
||
public boolean isTurnWhite() {
|
||
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) {
|
||
Piece piece = new Piece(isWhite, type, x, y);
|
||
pieces.add(piece);
|
||
}
|
||
|
||
public void populateBoard() {
|
||
cleanBoard(); // clear any existing pieces
|
||
|
||
// White pieces- Places white pawns at rank 1
|
||
for (int i = 0; i < 8; i++) {
|
||
setPiece(true, PieceType.Pawn, i, 1);
|
||
}
|
||
//places white back-rank pieces
|
||
setPiece(true, PieceType.Rook, 1, 0);
|
||
setPiece(true, PieceType.Knight, 1, 0);
|
||
setPiece(true, PieceType.Bishop, 2, 0);
|
||
setPiece(true, PieceType.Queen, 3, 0);
|
||
setPiece(true, PieceType.King, 4, 0);
|
||
setPiece(true, PieceType.Bishop, 5, 0);
|
||
setPiece(true, PieceType.Knight, 6, 0);
|
||
setPiece(true, PieceType.Rook, 7, 0);
|
||
|
||
// Black pieces - positions
|
||
for (int i = 0; i < 8; i++) {
|
||
setPiece(false, PieceType.Pawn, i, 6);
|
||
}
|
||
setPiece(false, PieceType.Rook, 0, 7);
|
||
setPiece(false, PieceType.Knight, 1, 7);
|
||
setPiece(false, PieceType.Bishop, 2, 7);
|
||
setPiece(false, PieceType.Queen, 3, 7);
|
||
setPiece(false, PieceType.King, 4, 7);
|
||
setPiece(false, PieceType.Bishop, 5, 7);
|
||
setPiece(false, PieceType.Knight, 6, 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() {
|
||
pieces.clear();
|
||
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() {
|
||
String[][] grid = new String[height][width];
|
||
|
||
// Fill grid with dots (empty)
|
||
for (int y = 0; y < height; y++) {
|
||
for (int x = 0; x < width; x++) {
|
||
grid[y][x] = ".";
|
||
}
|
||
}
|
||
|
||
// Place each piece in its position
|
||
for (Piece p : pieces) {
|
||
String symbol = p.getType().toString().substring(0, 1); // e.g., "P" for Pawn
|
||
if (!p.isWhite()) {
|
||
symbol = symbol.toLowerCase(); // lowercase for black
|
||
}
|
||
grid[p.getY()][p.getX()] = symbol;
|
||
}
|
||
|
||
// Build the string representation
|
||
StringBuilder sb = new StringBuilder();
|
||
for (int y = 0; y < height; y++) {
|
||
for (int x = 0; x < width; x++) {
|
||
sb.append(grid[y][x]).append(" ");
|
||
}
|
||
sb.append("\n");
|
||
}
|
||
|
||
return sb.toString();
|
||
}
|
||
|
||
|
||
//Lookup by coordinates and helpful for move generation and UI
|
||
//return all active pieces on the board
|
||
public ArrayList<Piece> getPieces() {
|
||
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) {
|
||
for (Piece p : pieces) {
|
||
if (p.isWhite() == isWhite && p.getType() == PieceType.King) {
|
||
return p;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
//Scans every opponent move to see if it lands on your king
|
||
public boolean isInCheck(boolean whitePlayer) {
|
||
Piece king = getKing(whitePlayer);
|
||
if (king == null) return false;
|
||
|
||
int kingX = king.getX();
|
||
int kingY = king.getY();
|
||
|
||
for (Piece piece : pieces) {
|
||
if (piece.isWhite() != whitePlayer) {
|
||
ArrayList<int[]> enemyMoves = getLegalMoves(piece, false); // disable check validation
|
||
for (int[] move : enemyMoves) {
|
||
if (move[0] == kingX && move[1] == kingY) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
//Declares Checkmate if in check and no legal moves left
|
||
public boolean isCheckmate(boolean whitePlayer) {
|
||
if (!isInCheck(whitePlayer)) return false;
|
||
|
||
//if any legal moves exists - not checkmate
|
||
for (Piece p : pieces) {
|
||
if (p.isWhite() == whitePlayer) {
|
||
ArrayList<int[]> legalMoves = getLegalMoves(p);
|
||
if (!legalMoves.isEmpty()) return false;
|
||
}
|
||
}
|
||
|
||
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) {
|
||
Piece clickedPiece = getPieceAt(x, y);
|
||
|
||
if (selected == null) {
|
||
// First click: try selecting a piece
|
||
if (clickedPiece != null && clickedPiece.isWhite() == turnWhite) {
|
||
selected = new int[]{x, y};
|
||
highlighted = getLegalMoves(clickedPiece);
|
||
}
|
||
} else {
|
||
// Second click: check if move is legal
|
||
//it also checks if target is highlighted,then play move
|
||
if (isHighlighted(x, y)) {
|
||
Piece piece = getPieceAt(selected[0], selected[1]);
|
||
Piece captured = getPieceAt(x, y);
|
||
|
||
Move move = new Move(piece, selected[0], selected[1], x, y, captured);
|
||
playMove(move);
|
||
} else {
|
||
// If clicked on another own piece, reselect
|
||
if (clickedPiece != null && clickedPiece.isWhite() == turnWhite) {
|
||
selected = new int[]{x, y};
|
||
highlighted = getLegalMoves(clickedPiece);
|
||
} else {
|
||
selected = null;
|
||
highlighted.clear();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private boolean simulateOnly = false;
|
||
|
||
public void setSimulateOnly(boolean simulateOnly) {
|
||
this.simulateOnly = simulateOnly;
|
||
}
|
||
|
||
public boolean isSimulateOnly() {
|
||
return simulateOnly;
|
||
}
|
||
//is used to validate castling and pinned-piece moves
|
||
private boolean isSquareUnderAttack(boolean byWhite, int x, int y) {
|
||
for (Piece p : pieces) {
|
||
if (p.isWhite() != byWhite) continue;
|
||
ArrayList<int[]> enemyMoves = getLegalMoves(p, false); // No king safety check here
|
||
for (int[] move : enemyMoves) {
|
||
if (move[0] == x && move[1] == y) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
//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) {
|
||
return getLegalMoves(piece, true); // default: check for king safety
|
||
}
|
||
|
||
public ArrayList<int[]> getLegalMoves(Piece piece, boolean validateCheck) {
|
||
ArrayList<int[]> moves = new ArrayList<>();
|
||
int x = piece.getX(), y = piece.getY();
|
||
int dir = piece.isWhite() ? 1 : -1;
|
||
|
||
switch (piece.getType()) {
|
||
case Pawn:
|
||
//single & double advance
|
||
//captures(including en passant)
|
||
int forwardY = y + dir;
|
||
|
||
if (isInBounds(x, forwardY) && getPieceAt(x, forwardY) == null) {
|
||
moves.add(new int[]{x, forwardY});
|
||
int twoForwardY = y + 2 * dir;
|
||
boolean atStartingRank = (piece.isWhite() && y == 1) || (!piece.isWhite() && y == 6);
|
||
if (atStartingRank && isInBounds(x, twoForwardY) && getPieceAt(x, twoForwardY) == null) {
|
||
moves.add(new int[]{x, twoForwardY});
|
||
}
|
||
}
|
||
|
||
Piece diagLeft = getPieceAt(x - 1, forwardY);
|
||
if (diagLeft != null && diagLeft.isWhite() != piece.isWhite())
|
||
moves.add(new int[]{x - 1, forwardY});
|
||
|
||
Piece diagRight = getPieceAt(x + 1, forwardY);
|
||
if (diagRight != null && diagRight.isWhite() != piece.isWhite())
|
||
moves.add(new int[]{x + 1, forwardY});
|
||
if (enPassantTarget != null) {
|
||
int tx = enPassantTarget[0], ty = enPassantTarget[1];
|
||
if (ty == forwardY && Math.abs(tx - x) == 1) {
|
||
moves.add(new int[]{ tx, ty });
|
||
}
|
||
}
|
||
break;
|
||
|
||
case Rook:
|
||
addLinearMoves(moves, piece, 1, 0);
|
||
addLinearMoves(moves, piece, -1, 0);
|
||
addLinearMoves(moves, piece, 0, 1);
|
||
addLinearMoves(moves, piece, 0, -1);
|
||
break;
|
||
|
||
case Bishop:
|
||
addLinearMoves(moves, piece, 1, 1);
|
||
addLinearMoves(moves, piece, -1, 1);
|
||
addLinearMoves(moves, piece, 1, -1);
|
||
addLinearMoves(moves, piece, -1, -1);
|
||
break;
|
||
|
||
case Queen:
|
||
addLinearMoves(moves, piece, 1, 0);
|
||
addLinearMoves(moves, piece, -1, 0);
|
||
addLinearMoves(moves, piece, 0, 1);
|
||
addLinearMoves(moves, piece, 0, -1);
|
||
addLinearMoves(moves, piece, 1, 1);
|
||
addLinearMoves(moves, piece, -1, 1);
|
||
addLinearMoves(moves, piece, 1, -1);
|
||
addLinearMoves(moves, piece, -1, -1);
|
||
break;
|
||
|
||
case King:
|
||
//one square in every direction + castling
|
||
for (int dx = -1; dx <= 1; dx++) {
|
||
for (int dy = -1; dy <= 1; dy++) {
|
||
if (dx == 0 && dy == 0) continue;
|
||
int nx = x + dx, ny = y + dy;
|
||
if (isInBounds(nx, ny)) {
|
||
Piece target = getPieceAt(nx, ny);
|
||
if (target == null || target.isWhite() != piece.isWhite()) {
|
||
moves.add(new int[]{nx, ny});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Castling logic
|
||
if (!piece.hasMoved() && (!validateCheck || !isInCheck(piece.isWhite()))) {
|
||
int row = piece.isWhite() ? 0 : 7;
|
||
|
||
// Kingside castling
|
||
Piece kingsideRook = getPieceAt(7, row);
|
||
if (kingsideRook != null &&
|
||
kingsideRook.getType() == PieceType.Rook &&
|
||
!kingsideRook.hasMoved() &&
|
||
getPieceAt(5, row) == null &&
|
||
getPieceAt(6, row) == null) {
|
||
|
||
if (!validateCheck || (
|
||
!isSquareUnderAttack(!piece.isWhite(), 4, row) &&
|
||
!isSquareUnderAttack(!piece.isWhite(), 5, row) &&
|
||
!isSquareUnderAttack(!piece.isWhite(), 6, row))) {
|
||
|
||
moves.add(new int[]{6, row});
|
||
}
|
||
}
|
||
|
||
// Queenside castling
|
||
Piece queensideRook = getPieceAt(0, row);
|
||
if (queensideRook != null &&
|
||
queensideRook.getType() == PieceType.Rook &&
|
||
!queensideRook.hasMoved() &&
|
||
getPieceAt(1, row) == null &&
|
||
getPieceAt(2, row) == null &&
|
||
getPieceAt(3, row) == null) {
|
||
|
||
if (!validateCheck || (
|
||
!isSquareUnderAttack(!piece.isWhite(), 4, row) &&
|
||
!isSquareUnderAttack(!piece.isWhite(), 3, row) &&
|
||
!isSquareUnderAttack(!piece.isWhite(), 2, row))) {
|
||
|
||
moves.add(new int[]{2, row});
|
||
}
|
||
}
|
||
}
|
||
|
||
break;
|
||
|
||
case Knight:
|
||
int[][] knightMoves = {
|
||
{1, 2}, {2, 1}, {-1, 2}, {-2, 1},
|
||
{1, -2}, {2, -1}, {-1, -2}, {-2, -1}
|
||
};
|
||
for (int[] m : knightMoves) {
|
||
int nx = x + m[0], ny = y + m[1];
|
||
if (isInBounds(nx, ny) && (getPieceAt(nx, ny) == null || getPieceAt(nx, ny).isWhite() != piece.isWhite())) {
|
||
moves.add(new int[]{nx, ny});
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
if (!validateCheck) return moves;
|
||
|
||
// Filter out moves that put own king in check
|
||
ArrayList<int[]> legalMoves = new ArrayList<>();
|
||
for (int[] move : moves) {
|
||
Board copy = new Board(this);
|
||
Piece p = copy.getPieceAt(x, y);
|
||
Piece captured = copy.getPieceAt(move[0], move[1]);
|
||
Move testMove = new Move(p, x, y, move[0], move[1], captured);
|
||
copy.playMove(testMove);
|
||
|
||
if (!copy.isInCheck(piece.isWhite())) {
|
||
legalMoves.add(move);
|
||
}
|
||
}
|
||
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) {
|
||
int x = piece.getX(), y = piece.getY();
|
||
while (true) {
|
||
x += dx;
|
||
y += dy;
|
||
if (!isInBounds(x, y)) break;
|
||
Piece target = getPieceAt(x, y);
|
||
if (target == null) {
|
||
moves.add(new int[]{x, y});
|
||
} else {
|
||
if (target.isWhite() != piece.isWhite())
|
||
moves.add(new int[]{x, y});
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
//helpers for bounds checks and UI flags
|
||
//return true of (x,y) is a valid board square
|
||
private boolean isInBounds(int x, int y) {
|
||
return x >= 0 && y >= 0 && x < width && y < height;
|
||
}
|
||
|
||
|
||
public boolean isSelected(int x, int y) {
|
||
return selected != null && selected[0] == x && selected[1] == y;
|
||
}
|
||
|
||
|
||
/* saving-loading feature :*/
|
||
// @return each line representing one piece, plus a header:
|
||
// Line 0: width,height,turnNumber,turnWhite,enPassantX,enPassantY
|
||
// Lines 1–N: serialized Pieces
|
||
public String[] toFileRep() {
|
||
List<String> lines = new ArrayList<>();
|
||
|
||
int epX = enPassantTarget == null ? -1 : enPassantTarget[0];
|
||
int epY = enPassantTarget == null ? -1 : enPassantTarget[1];
|
||
// header:width,height,turnNumber,turnWhite,enPassantX,enpassantY
|
||
lines.add(width + "," +
|
||
height + "," +
|
||
turnNumber + "," +
|
||
turnWhite + "," +
|
||
epX + "," + epY);
|
||
|
||
// pieces - each piece's own serialization
|
||
for (Piece p : pieces) {
|
||
lines.add(p.toFileRep());
|
||
}
|
||
|
||
return lines.toArray(new String[0]);
|
||
}
|
||
|
||
public Board(String[] array) {
|
||
this.pieces = new ArrayList<>();
|
||
// parse header fields
|
||
String[] hdr = array[0].split(",");
|
||
this.width = Integer.parseInt(hdr[0]);
|
||
this.height = Integer.parseInt(hdr[1]);
|
||
this.turnNumber = Integer.parseInt(hdr[2]);
|
||
this.turnWhite = Boolean.parseBoolean(hdr[3]);
|
||
int epX = Integer.parseInt(hdr[4]);
|
||
int epY = Integer.parseInt(hdr[5]);
|
||
this.enPassantTarget = (epX >= 0 && epY >= 0) ? new int[]{epX, epY} : null;
|
||
|
||
// parse pieces
|
||
for (int i = 1; i < array.length; i++) {
|
||
Piece p = Piece.fromFileRep(array[i]);
|
||
this.pieces.add(p);
|
||
}
|
||
|
||
// clear other state - resets UI and History
|
||
this.selected = null;
|
||
this.highlighted = new ArrayList<>();
|
||
this.moveHistory = new ArrayList<>();
|
||
}
|
||
|
||
//return the Piece at (x,y) or null if none present
|
||
public Piece getPieceAt(int x, int y) {
|
||
for (Piece piece : pieces) {
|
||
if (piece.getX() == x && piece.getY() == y) {
|
||
return piece;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
//returns true if (x,y) is one of the highlighted legal moves
|
||
public boolean isHighlighted(int x, int y) {
|
||
for (int[] pos : highlighted) {
|
||
if (pos[0] == x && pos[1] == y) return true;
|
||
}
|
||
return false;
|
||
}
|
||
//reverses the very last playMove including rook reposition for castling and recovers captured pawns
|
||
public void undoLastMove() {
|
||
|
||
if (moveHistory.isEmpty()) return;
|
||
|
||
Move lastMove = moveHistory.remove(moveHistory.size() - 1);
|
||
Piece movedPiece = lastMove.getPieceMoved();
|
||
|
||
// Undo castling
|
||
if (movedPiece.getType() == PieceType.King && Math.abs(lastMove.getToX() - lastMove.getFromX()) == 2) {
|
||
int row = movedPiece.isWhite() ? 0 : 7;
|
||
|
||
if (lastMove.getToX() == 6) { // Kingside
|
||
Piece rook = getPieceAt(5, row);
|
||
if (rook != null && rook.getType() == PieceType.Rook) {
|
||
rook.setPosition(7, row);
|
||
rook.setHasMoved(false);
|
||
}
|
||
} else if (lastMove.getToX() == 2) { // Queenside
|
||
Piece rook = getPieceAt(3, row);
|
||
if (rook != null && rook.getType() == PieceType.Rook) {
|
||
rook.setPosition(0, row);
|
||
rook.setHasMoved(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Move the piece back to its original position
|
||
movedPiece.setPosition(lastMove.getFromX(), lastMove.getFromY());
|
||
|
||
movedPiece.setHasMoved(false);
|
||
|
||
|
||
// Restore the captured piece if any
|
||
if (lastMove.getPieceCaptured() != null) {
|
||
pieces.add(lastMove.getPieceCaptured());
|
||
}
|
||
|
||
// Switch turn and decrease turn count
|
||
turnWhite = !turnWhite;
|
||
turnNumber--;
|
||
|
||
// Clear selection and highlights
|
||
selected = null;
|
||
highlighted.clear();
|
||
}
|
||
//applies every rule: castling,en passant,captures,promotions,history and turn switching
|
||
public void playMove(Move move) {
|
||
Piece piece = move.getPieceMoved();
|
||
int fromX = move.getFromX(), fromY = move.getFromY();
|
||
int toX = move.getToX(), toY = move.getToY();
|
||
|
||
// ─── castling ─────────────────────────────────────────────────────────
|
||
if (piece.getType() == PieceType.King
|
||
&& Math.abs(toX - fromX) == 2) {
|
||
int row = piece.isWhite() ? 0 : 7;
|
||
// kingside
|
||
if (toX == 6) {
|
||
Piece rook = getPieceAt(7, row);
|
||
if (rook != null) rook.setPosition(5, row);
|
||
}
|
||
// queenside
|
||
else {
|
||
Piece rook = getPieceAt(0, row);
|
||
if (rook != null) rook.setPosition(3, row);
|
||
}
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// ─── en passant capture ─────────────────────────────────────────────
|
||
if (piece.getType() == PieceType.Pawn
|
||
&& enPassantTarget != null
|
||
&& toX == enPassantTarget[0]
|
||
&& toY == enPassantTarget[1]) {
|
||
// the pawn being captured sits on (toX, fromY)
|
||
Piece captured = getPieceAt(toX, fromY);
|
||
if (captured != null && captured.getType() == PieceType.Pawn) {
|
||
pieces.remove(captured);
|
||
move.setPieceCaptured(captured); // so undo can restore it
|
||
}
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// ─── normal capture ─────────────────────────────────────────────────
|
||
else if (move.getPieceCaptured() != null) {
|
||
pieces.remove(move.getPieceCaptured());
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// move the piece
|
||
piece.setPosition(toX, toY);
|
||
|
||
// ─── pawn promotion ──────────────────────────────────────────────────
|
||
if (piece.getType() == PieceType.Pawn) {
|
||
int promotionRank = piece.isWhite() ? height - 1 : 0;
|
||
if (toY == promotionRank) {
|
||
piece.setType(PieceType.Queen);
|
||
}
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// ─── update enPassantTarget ────────────────────────────────────────
|
||
if (piece.getType() == PieceType.Pawn
|
||
&& Math.abs(toY - fromY) == 2) {
|
||
// record the square it jumped over
|
||
enPassantTarget = new int[]{ fromX, (fromY + toY) / 2 };
|
||
} else {
|
||
enPassantTarget = null;
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// record history & advance turn
|
||
moveHistory.add(move);
|
||
turnWhite = !turnWhite;
|
||
turnNumber++;
|
||
|
||
// clear selection/highlight
|
||
selected = null;
|
||
highlighted.clear();
|
||
}
|
||
|
||
//Simple static evaluation for negamax
|
||
public int evaluateBoard() {
|
||
int score = 0;
|
||
for (Piece piece : pieces) {
|
||
int value = 0;
|
||
switch (piece.getType()) {
|
||
case Pawn: value = 100; break;
|
||
case Knight: value = 320; break;
|
||
case Bishop: value = 330; break;
|
||
case Rook: value = 500; break;
|
||
case Queen: value = 900; break;
|
||
}
|
||
if (piece.isWhite()) score += value;
|
||
else score -= value;
|
||
}
|
||
return score;
|
||
}
|
||
public enum GameResult {
|
||
ONGOING, // game still in progress
|
||
DRAW, // stalemate (or other draw condition)
|
||
WHITE_WINS, // black is checkmated
|
||
BLACK_WINS // white is checkmated
|
||
}
|
||
|
||
|
||
//@return the current game result: ONGOING, DRAW, WHITE_WINS or BLACK_WINS.
|
||
public GameResult getGameResult() {
|
||
// if White is checkmated → Black wins
|
||
if (isCheckmate(true)) return GameResult.BLACK_WINS;
|
||
// if Black is checkmated → White wins
|
||
if (isCheckmate(false)) return GameResult.WHITE_WINS;
|
||
// if side to move has no legal moves but is not in check → stalemate
|
||
if (isStalemate(true) || isStalemate(false)) return GameResult.DRAW;
|
||
return GameResult.ONGOING;
|
||
}
|
||
|
||
//@param whitePlayer whose turn we're checking for stalemate
|
||
//@return true if whitePlayer is not in check and has no legal moves
|
||
private boolean isStalemate(boolean whitePlayer) {
|
||
// must not be in check
|
||
if (isInCheck(whitePlayer)) return false;
|
||
// and have no legal moves
|
||
for (Piece p : pieces) {
|
||
if (p.isWhite() == whitePlayer && !getLegalMoves(p).isEmpty()) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|