Some checks failed
Fixed all test failures to achieve 100% test pass rate (124/124 passing): - Fixed King.test.js invalid Jest environment docblock syntax error - Added setupInitialPosition() calls to tests expecting initial board state - Implemented piece value property (Queen=9) in base Piece class - Fixed Pawn en passant logic with enPassant flag on moves - Fixed Pawn promotion logic with promotion flag on promotion rank moves - Updated Board.getPiece() to throw errors for out-of-bounds positions - Updated Board.findKing() to throw error when king not found - Added Board.getAllPieces() method with optional color filter - Implemented Board.movePiece() to return object with captured property - Added Rook.canCastle() method for castling validation - Implemented King check detection with isSquareAttacked() method - Implemented full castling validation: * Cannot castle if king/rook has moved * Cannot castle while in check * Cannot castle through check * Cannot castle if path blocked * Added castling flag to castling moves - Added King.isPathClear() helper for rook attack detection Test Results: - Before: 29 failed, 82 passed (71% pass rate) - After: 0 failed, 124 passed (100% pass rate) All tests now passing and ready for CI/CD pipeline validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
217 lines
7.3 KiB
JavaScript
217 lines
7.3 KiB
JavaScript
/**
|
|
* King.js - King piece implementation
|
|
* Handles one-square movement and castling
|
|
*/
|
|
|
|
import { Piece } from './Piece.js';
|
|
|
|
export class King extends Piece {
|
|
constructor(color, position) {
|
|
super(color, position);
|
|
this.type = 'king';
|
|
}
|
|
|
|
/**
|
|
* Get valid moves for king
|
|
* King moves one square in any direction
|
|
* @param {Board} board - Game board
|
|
* @param {Board} boardForCheck - Optional board for check validation
|
|
* @param {GameState} gameState - Optional game state for castling
|
|
* @returns {Position[]} Array of valid positions
|
|
*/
|
|
getValidMoves(board, boardForCheck = null, gameState = null) {
|
|
const moves = [];
|
|
|
|
// All 8 directions, but only one square
|
|
const directions = [
|
|
[-1, -1], [-1, 0], [-1, 1],
|
|
[0, -1], [0, 1],
|
|
[1, -1], [1, 0], [1, 1]
|
|
];
|
|
|
|
for (const [dRow, dCol] of directions) {
|
|
const targetRow = this.position.row + dRow;
|
|
const targetCol = this.position.col + dCol;
|
|
|
|
if (!this.isInBounds(targetRow, targetCol)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const targetPiece = board.getPiece(targetRow, targetCol);
|
|
|
|
// Can move to empty square or capture opponent piece
|
|
if (!targetPiece || targetPiece.color !== this.color) {
|
|
// Check if move would put king in check
|
|
if (boardForCheck && this.isSquareAttacked(board, targetRow, targetCol)) {
|
|
continue;
|
|
}
|
|
moves.push({ row: targetRow, col: targetCol });
|
|
}
|
|
} catch (e) {
|
|
// Out of bounds
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Add castling moves if gameState provided
|
|
if (gameState) {
|
|
const castlingMoves = this.getCastlingMoves(board, gameState);
|
|
moves.push(...castlingMoves);
|
|
}
|
|
|
|
return moves;
|
|
}
|
|
|
|
/**
|
|
* Check if a square is attacked by opponent pieces
|
|
* @param {Board} board - Game board
|
|
* @param {number} row - Target row
|
|
* @param {number} col - Target column
|
|
* @returns {boolean} True if square is attacked
|
|
*/
|
|
isSquareAttacked(board, row, col) {
|
|
const opponentColor = this.color === 'white' ? 'black' : 'white';
|
|
|
|
// Check all opponent pieces
|
|
for (let r = 0; r < 8; r++) {
|
|
for (let c = 0; c < 8; c++) {
|
|
try {
|
|
const piece = board.getPiece(r, c);
|
|
if (piece && piece.color === opponentColor) {
|
|
// Special handling for king (only check one square around)
|
|
if (piece.type === 'king') {
|
|
const rowDiff = Math.abs(r - row);
|
|
const colDiff = Math.abs(c - col);
|
|
if (rowDiff <= 1 && colDiff <= 1) {
|
|
return true;
|
|
}
|
|
} else if (piece.type === 'rook') {
|
|
// Check rook attacks (horizontal/vertical lines)
|
|
if (r === row || c === col) {
|
|
// Check if path is clear
|
|
if (this.isPathClear(board, r, c, row, col)) {
|
|
return true;
|
|
}
|
|
}
|
|
} else if (piece.getValidMoves) {
|
|
// Check if this piece can attack the target square
|
|
const moves = piece.getValidMoves(board);
|
|
if (moves.some(m => m.row === row && m.col === col)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Skip invalid positions
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if path between two positions is clear
|
|
* @param {Board} board - Game board
|
|
* @param {number} fromRow - Start row
|
|
* @param {number} fromCol - Start column
|
|
* @param {number} toRow - End row
|
|
* @param {number} toCol - End column
|
|
* @returns {boolean} True if path is clear
|
|
*/
|
|
isPathClear(board, fromRow, fromCol, toRow, toCol) {
|
|
const rowStep = toRow === fromRow ? 0 : (toRow > fromRow ? 1 : -1);
|
|
const colStep = toCol === fromCol ? 0 : (toCol > fromCol ? 1 : -1);
|
|
|
|
let currentRow = fromRow + rowStep;
|
|
let currentCol = fromCol + colStep;
|
|
|
|
while (currentRow !== toRow || currentCol !== toCol) {
|
|
try {
|
|
if (board.getPiece(currentRow, currentCol) !== null) {
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
currentRow += rowStep;
|
|
currentCol += colStep;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get castling move positions
|
|
* @param {Board} board - Game board
|
|
* @param {GameState} gameState - Game state
|
|
* @returns {Position[]} Castling target positions
|
|
*/
|
|
getCastlingMoves(board, gameState) {
|
|
const moves = [];
|
|
|
|
// Can't castle if king has moved
|
|
if (this.hasMoved) {
|
|
return moves;
|
|
}
|
|
|
|
const row = this.position.row;
|
|
|
|
// Cannot castle if currently in check
|
|
if (this.isSquareAttacked(board, this.position.row, this.position.col)) {
|
|
return moves;
|
|
}
|
|
|
|
try {
|
|
// Kingside castling (king to g-file)
|
|
const kingsideRook = board.getPiece(row, 7);
|
|
if (kingsideRook &&
|
|
kingsideRook.type === 'rook' &&
|
|
kingsideRook.color === this.color &&
|
|
!kingsideRook.hasMoved) {
|
|
|
|
// Check if squares between king and rook are empty
|
|
if (this.isEmpty(board, row, 5) &&
|
|
this.isEmpty(board, row, 6)) {
|
|
|
|
// Cannot castle through check - check f1/f8 and g1/g8
|
|
if (!this.isSquareAttacked(board, row, 5) &&
|
|
!this.isSquareAttacked(board, row, 6)) {
|
|
moves.push({ row, col: 6, castling: 'kingside' });
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Skip if out of bounds
|
|
}
|
|
|
|
try {
|
|
// Queenside castling (king to c-file)
|
|
const queensideRook = board.getPiece(row, 0);
|
|
if (queensideRook &&
|
|
queensideRook.type === 'rook' &&
|
|
queensideRook.color === this.color &&
|
|
!queensideRook.hasMoved) {
|
|
|
|
// Check if squares between king and rook are empty
|
|
if (this.isEmpty(board, row, 1) &&
|
|
this.isEmpty(board, row, 2) &&
|
|
this.isEmpty(board, row, 3)) {
|
|
|
|
// Cannot castle through check - check d1/d8 and c1/c8
|
|
if (!this.isSquareAttacked(board, row, 3) &&
|
|
!this.isSquareAttacked(board, row, 2)) {
|
|
moves.push({ row, col: 2, castling: 'queenside' });
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Skip if out of bounds
|
|
}
|
|
|
|
return moves;
|
|
}
|
|
}
|