OOP_2B5_Project/src/backend/Board.java

691 lines
24 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 1N: 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;
}
}