Skip to content

Commit 4ab0a89

Browse files
Copilotmgierschdev
andcommitted
Implement castling with full validation including castling through check
Co-authored-by: mgierschdev <62764972+mgierschdev@users.noreply.github.com>
1 parent dc023b0 commit 4ab0a89

3 files changed

Lines changed: 353 additions & 4 deletions

File tree

backend/src/main/java/com/backend/domain/Chessboard.java

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ public class Chessboard {
3838
// Square that can be targeted by an en passant capture
3939
private Position enPassantTarget;
4040

41+
// Track if kings and rooks have moved (for castling)
42+
private boolean whiteKingMoved = false;
43+
private boolean blackKingMoved = false;
44+
private boolean whiteKingsideRookMoved = false;
45+
private boolean whiteQueensideRookMoved = false;
46+
private boolean blackKingsideRookMoved = false;
47+
private boolean blackQueensideRookMoved = false;
48+
4149
public Chessboard() {
4250
board = GetInitMatrixBoard();
4351
invalid = new ChessPiece(ChessPieceType.Invalid, Color.None);
@@ -78,6 +86,10 @@ public ChessPiece movePiece(Position source, Position target, Color player, Ches
7886
target.col == enPassantTarget.col &&
7987
Math.abs(source.col - target.col) == 1;
8088

89+
// Check for castling
90+
boolean isCastling = sourcePosition.type() == ChessPieceType.King &&
91+
Math.abs(target.col - source.col) == 2;
92+
8193
// reset en passant target; will be set again if this move is a double step
8294
enPassantTarget = null;
8395

@@ -96,9 +108,53 @@ public ChessPiece movePiece(Position source, Position target, Color player, Ches
96108
return captured;
97109
}
98110

111+
// Handle castling - move both king and rook
112+
if (isCastling) {
113+
int rookSourceCol = target.col > source.col ? 7 : 0; // Kingside or queenside
114+
int rookTargetCol = target.col > source.col ? target.col - 1 : target.col + 1;
115+
116+
ChessPiece rook = board[source.row][rookSourceCol];
117+
board[source.row][source.col] = emptySpace;
118+
board[target.row][target.col] = sourcePosition;
119+
board[source.row][rookSourceCol] = emptySpace;
120+
board[source.row][rookTargetCol] = rook;
121+
122+
// Mark king as moved
123+
if (player == Color.White) {
124+
whiteKingMoved = true;
125+
} else {
126+
blackKingMoved = true;
127+
}
128+
129+
return emptySpace;
130+
}
131+
99132
board[source.row][source.col] = emptySpace;
100133
board[target.row][target.col] = sourcePosition;
101134

135+
// Track king and rook moves for castling eligibility
136+
if (sourcePosition.type() == ChessPieceType.King) {
137+
if (player == Color.White) {
138+
whiteKingMoved = true;
139+
} else {
140+
blackKingMoved = true;
141+
}
142+
} else if (sourcePosition.type() == ChessPieceType.Rock) {
143+
if (player == Color.White) {
144+
if (source.row == 0 && source.col == 0) {
145+
whiteQueensideRookMoved = true;
146+
} else if (source.row == 0 && source.col == 7) {
147+
whiteKingsideRookMoved = true;
148+
}
149+
} else {
150+
if (source.row == 7 && source.col == 0) {
151+
blackQueensideRookMoved = true;
152+
} else if (source.row == 7 && source.col == 7) {
153+
blackKingsideRookMoved = true;
154+
}
155+
}
156+
}
157+
102158
if (sourcePosition.type() == ChessPieceType.Pawn && Math.abs(target.row - source.row) == 2) {
103159
enPassantTarget = new Position((source.row + target.row) / 2, source.col);
104160
}
@@ -321,6 +377,13 @@ public Position[] getValidMoves(Position position) {
321377

322378
ChessPiece chessPiece = board[position.row][position.col];
323379
Position[] candidateMoves = getCandidateMoves(position, chessPiece);
380+
381+
// For kings, add castling moves before filtering
382+
if (chessPiece.type() == ChessPieceType.King) {
383+
List<Position> withCastling = new ArrayList<>(Arrays.asList(candidateMoves));
384+
addCastlingMoves(position, withCastling, chessPiece.color());
385+
candidateMoves = withCastling.toArray(Position[]::new);
386+
}
324387

325388
// Filter out moves that would leave the player's king in check
326389
return filterMovesLeavingKingInCheck(position, candidateMoves, chessPiece.color());
@@ -346,7 +409,7 @@ private Position[] getCandidateMoves(Position position, ChessPiece chessPiece) {
346409
return getValidMovesQueen(position, chessPiece);
347410
}
348411
case King -> {
349-
return getValidMovesKing(position, chessPiece);
412+
return getValidMovesKingBasic(position, chessPiece);
350413
}
351414
case Rock -> {
352415
return getValidMovesRock(position, chessPiece);
@@ -371,8 +434,19 @@ private Position[] getCandidateMoves(Position position, ChessPiece chessPiece) {
371434
*/
372435
private Position[] filterMovesLeavingKingInCheck(Position from, Position[] candidateMoves, Color playerColor) {
373436
List<Position> legalMoves = new ArrayList<>();
437+
ChessPiece piece = board[from.row][from.col];
438+
boolean isKing = piece.type() == ChessPieceType.King;
374439

375440
for (Position to : candidateMoves) {
441+
// Castling moves are already validated and don't need simulation
442+
// (king moves 2 squares horizontally)
443+
boolean isCastlingMove = isKing && Math.abs(to.col - from.col) == 2;
444+
445+
if (isCastlingMove) {
446+
legalMoves.add(to);
447+
continue;
448+
}
449+
376450
// Simulate the move
377451
ChessPiece captured = simulateMove(from, to);
378452

@@ -421,7 +495,114 @@ private Position[] getValidMovesBishop(Position position, ChessPiece chessPiece)
421495
return valid.toArray(Position[]::new);
422496
}
423497

424-
private Position[] getValidMovesKing(Position position, ChessPiece chessPiece) {
498+
/**
499+
* Adds castling moves if conditions are met.
500+
* Castling is allowed if:
501+
* 1. King hasn't moved
502+
* 2. Rook hasn't moved
503+
* 3. No pieces between king and rook
504+
* 4. King is not in check
505+
* 5. King doesn't move through check
506+
* 6. King doesn't land in check
507+
*/
508+
private void addCastlingMoves(Position kingPos, List<Position> valid, Color color) {
509+
// Check if king is in check - can't castle out of check
510+
if (isKingInCheck(color)) {
511+
return;
512+
}
513+
514+
if (color == Color.White) {
515+
// White kingside castling
516+
if (!whiteKingMoved && !whiteKingsideRookMoved &&
517+
kingPos.row == 0 && kingPos.col == 4) {
518+
if (canCastleKingside(0, color)) {
519+
valid.add(new Position(0, 6));
520+
}
521+
}
522+
// White queenside castling
523+
if (!whiteKingMoved && !whiteQueensideRookMoved &&
524+
kingPos.row == 0 && kingPos.col == 4) {
525+
if (canCastleQueenside(0, color)) {
526+
valid.add(new Position(0, 2));
527+
}
528+
}
529+
} else {
530+
// Black kingside castling
531+
if (!blackKingMoved && !blackKingsideRookMoved &&
532+
kingPos.row == 7 && kingPos.col == 4) {
533+
if (canCastleKingside(7, color)) {
534+
valid.add(new Position(7, 6));
535+
}
536+
}
537+
// Black queenside castling
538+
if (!blackKingMoved && !blackQueensideRookMoved &&
539+
kingPos.row == 7 && kingPos.col == 4) {
540+
if (canCastleQueenside(7, color)) {
541+
valid.add(new Position(7, 2));
542+
}
543+
}
544+
}
545+
}
546+
547+
/**
548+
* Check if kingside castling is possible (no pieces between, king doesn't move through check).
549+
*/
550+
private boolean canCastleKingside(int row, Color color) {
551+
// Check squares between king and rook are empty
552+
if (board[row][5].type() != ChessPieceType.Empty ||
553+
board[row][6].type() != ChessPieceType.Empty) {
554+
return false;
555+
}
556+
557+
// Check king doesn't move through check (squares f1/f8 and g1/g8)
558+
return !isSquareUnderAttack(new Position(row, 5), color) &&
559+
!isSquareUnderAttack(new Position(row, 6), color);
560+
}
561+
562+
/**
563+
* Check if queenside castling is possible (no pieces between, king doesn't move through check).
564+
*/
565+
private boolean canCastleQueenside(int row, Color color) {
566+
// Check squares between king and rook are empty
567+
if (board[row][1].type() != ChessPieceType.Empty ||
568+
board[row][2].type() != ChessPieceType.Empty ||
569+
board[row][3].type() != ChessPieceType.Empty) {
570+
return false;
571+
}
572+
573+
// Check king doesn't move through check (squares d1/d8 and c1/c8)
574+
// Note: b1/b8 doesn't need to be safe, only the king's path
575+
return !isSquareUnderAttack(new Position(row, 3), color) &&
576+
!isSquareUnderAttack(new Position(row, 2), color);
577+
}
578+
579+
/**
580+
* Check if a square is under attack by the opponent.
581+
*/
582+
private boolean isSquareUnderAttack(Position square, Color defendingColor) {
583+
Color attackingColor = getOpposite(defendingColor);
584+
585+
for (int r = 0; r < board.length; r++) {
586+
for (int c = 0; c < board[r].length; c++) {
587+
ChessPiece piece = board[r][c];
588+
if (piece.color() == attackingColor) {
589+
Position[] moves = getCandidateMoves(new Position(r, c), piece);
590+
for (Position move : moves) {
591+
if (move.row == square.row && move.col == square.col) {
592+
return true;
593+
}
594+
}
595+
}
596+
}
597+
}
598+
return false;
599+
}
600+
601+
/**
602+
* Gets basic king moves (one square in any direction) without castling.
603+
* Used by getCandidateMoves to avoid infinite recursion.
604+
*/
605+
private Position[] getValidMovesKingBasic(Position position, ChessPiece chessPiece) {
425606
if (chessPiece.type() != ChessPieceType.King) {
426607
return new Position[0];
427608
}

0 commit comments

Comments
 (0)