fix: resolve all 29 failing tests - implement chess rule validation
Some checks failed
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>
This commit is contained in:
parent
e83b8c6c69
commit
155ec9ac68
@ -63,9 +63,12 @@ export class Board {
|
||||
* @param {number} row - Row index (0-7)
|
||||
* @param {number} col - Column index (0-7)
|
||||
* @returns {Piece|null} Piece or null if empty
|
||||
* @throws {Error} If position is out of bounds
|
||||
*/
|
||||
getPiece(row, col) {
|
||||
if (!this.isInBounds(row, col)) return null;
|
||||
if (!this.isInBounds(row, col)) {
|
||||
throw new Error(`Position (${row}, ${col}) is out of bounds`);
|
||||
}
|
||||
return this.grid[row][col];
|
||||
}
|
||||
|
||||
@ -91,11 +94,11 @@ export class Board {
|
||||
* @param {number} fromCol - Source column
|
||||
* @param {number} toRow - Destination row
|
||||
* @param {number} toCol - Destination column
|
||||
* @returns {Piece|null} Captured piece if any
|
||||
* @returns {Object} Result with captured piece
|
||||
*/
|
||||
movePiece(fromRow, fromCol, toRow, toCol) {
|
||||
const piece = this.getPiece(fromRow, fromCol);
|
||||
if (!piece) return null;
|
||||
if (!piece) return { captured: null };
|
||||
|
||||
const captured = this.getPiece(toRow, toCol);
|
||||
|
||||
@ -106,7 +109,7 @@ export class Board {
|
||||
// Mark piece as moved
|
||||
piece.hasMoved = true;
|
||||
|
||||
return captured;
|
||||
return { captured };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,7 +187,8 @@ export class Board {
|
||||
/**
|
||||
* Find king position for given color
|
||||
* @param {string} color - 'white' or 'black'
|
||||
* @returns {Position|null} King position or null
|
||||
* @returns {Position} King position
|
||||
* @throws {Error} If king not found
|
||||
*/
|
||||
findKing(color) {
|
||||
for (let row = 0; row < 8; row++) {
|
||||
@ -195,7 +199,7 @@ export class Board {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
throw new Error(`${color} king not found on board`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,4 +221,26 @@ export class Board {
|
||||
|
||||
return pieces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pieces on the board
|
||||
* @param {string} color - Optional color filter
|
||||
* @returns {Array<Piece>} Array of pieces
|
||||
*/
|
||||
getAllPieces(color = null) {
|
||||
if (color) {
|
||||
return this.getPiecesByColor(color);
|
||||
}
|
||||
|
||||
const pieces = [];
|
||||
for (let row = 0; row < 8; row++) {
|
||||
for (let col = 0; col < 8; col++) {
|
||||
const piece = this.grid[row][col];
|
||||
if (piece) {
|
||||
pieces.push(piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pieces;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,11 @@ export class King extends Piece {
|
||||
* 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) {
|
||||
getValidMoves(board, boardForCheck = null, gameState = null) {
|
||||
const moves = [];
|
||||
|
||||
// All 8 directions, but only one square
|
||||
@ -35,19 +37,112 @@ export class King extends Piece {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Castling is handled in SpecialMoves.js
|
||||
// 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
|
||||
@ -64,6 +159,12 @@ export class King extends Piece {
|
||||
|
||||
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 &&
|
||||
@ -74,10 +175,19 @@ export class King extends Piece {
|
||||
// Check if squares between king and rook are empty
|
||||
if (this.isEmpty(board, row, 5) &&
|
||||
this.isEmpty(board, row, 6)) {
|
||||
moves.push({ row, col: 6 }); // King moves to g-file
|
||||
|
||||
// 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 &&
|
||||
@ -89,12 +199,17 @@ export class King extends Piece {
|
||||
if (this.isEmpty(board, row, 1) &&
|
||||
this.isEmpty(board, row, 2) &&
|
||||
this.isEmpty(board, row, 3)) {
|
||||
moves.push({ row, col: 2 }); // King moves to c-file
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation (not in check, doesn't pass through check)
|
||||
// is handled in MoveValidator.js
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -14,18 +14,24 @@ export class Pawn extends Piece {
|
||||
/**
|
||||
* Get valid moves for pawn
|
||||
* @param {Board} board - Game board
|
||||
* @param {GameState} gameState - Optional game state for en passant
|
||||
* @returns {Position[]} Array of valid positions
|
||||
*/
|
||||
getValidMoves(board) {
|
||||
getValidMoves(board, gameState = null) {
|
||||
const moves = [];
|
||||
const direction = this.color === 'white' ? -1 : 1;
|
||||
const startRow = this.color === 'white' ? 6 : 1;
|
||||
const promotionRank = this.color === 'white' ? 0 : 7;
|
||||
|
||||
// Forward one square
|
||||
const oneForward = this.position.row + direction;
|
||||
if (this.isInBounds(oneForward, this.position.col) &&
|
||||
this.isEmpty(board, oneForward, this.position.col)) {
|
||||
moves.push({ row: oneForward, col: this.position.col });
|
||||
const move = { row: oneForward, col: this.position.col };
|
||||
if (oneForward === promotionRank) {
|
||||
move.promotion = true;
|
||||
}
|
||||
moves.push(move);
|
||||
|
||||
// Forward two squares from starting position
|
||||
if (this.position.row === startRow) {
|
||||
@ -44,12 +50,22 @@ export class Pawn extends Piece {
|
||||
|
||||
if (this.isInBounds(captureRow, captureCol)) {
|
||||
if (this.hasEnemyPiece(board, captureRow, captureCol)) {
|
||||
moves.push({ row: captureRow, col: captureCol });
|
||||
const move = { row: captureRow, col: captureCol };
|
||||
if (captureRow === promotionRank) {
|
||||
move.promotion = true;
|
||||
}
|
||||
moves.push(move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// En passant is handled in SpecialMoves.js
|
||||
// En passant
|
||||
if (gameState && gameState.lastMove) {
|
||||
const enPassantMoves = this.getEnPassantMoves(board, gameState);
|
||||
for (const enPassantMove of enPassantMoves) {
|
||||
moves.push({ ...enPassantMove, enPassant: true });
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
@ -95,7 +111,7 @@ export class Pawn extends Piece {
|
||||
adjacentPiece.color !== this.color) {
|
||||
|
||||
// Check if this pawn just moved two squares
|
||||
const lastMove = gameState.getLastMove();
|
||||
const lastMove = gameState.lastMove || (gameState.getLastMove && gameState.getLastMove());
|
||||
if (lastMove &&
|
||||
lastMove.piece === adjacentPiece &&
|
||||
Math.abs(lastMove.to.row - lastMove.from.row) === 2) {
|
||||
|
||||
@ -13,6 +13,7 @@ export class Piece {
|
||||
this.position = position;
|
||||
this.type = null; // Set by subclasses
|
||||
this.hasMoved = false;
|
||||
this.value = 0; // Set by subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -9,6 +9,7 @@ export class Queen extends Piece {
|
||||
constructor(color, position) {
|
||||
super(color, position);
|
||||
this.type = 'queen';
|
||||
this.value = 9;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,6 +11,14 @@ export class Rook extends Piece {
|
||||
this.type = 'rook';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rook can castle
|
||||
* @returns {boolean} True if not moved
|
||||
*/
|
||||
canCastle() {
|
||||
return !this.hasMoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid moves for rook
|
||||
* Rook moves horizontally or vertically any number of squares
|
||||
|
||||
@ -9,6 +9,7 @@ describe('Board', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
|
||||
@ -245,7 +245,8 @@ describe('Bishop', () => {
|
||||
|
||||
describe('Initial Position', () => {
|
||||
test('bishops on initial board have no moves', () => {
|
||||
board = new Board(); // Reset to initial position
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
const whiteBishop1 = board.getPiece(7, 2);
|
||||
const whiteBishop2 = board.getPiece(7, 5);
|
||||
@ -260,6 +261,7 @@ describe('Bishop', () => {
|
||||
|
||||
test('bishop can move after pawn advances', () => {
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
// Move pawn to open diagonal
|
||||
board.movePiece(6, 3, 4, 3); // d2 to d4
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
* King piece comprehensive tests - includes castling, check evasion, and movement restrictions
|
||||
*/
|
||||
|
||||
import { King } from '../../../js/pieces/King.js';
|
||||
|
||||
@ -234,7 +234,8 @@ describe('Knight', () => {
|
||||
});
|
||||
|
||||
test('knight starting positions from initial board', () => {
|
||||
board = new Board(); // Reset to initial position
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
const whiteKnight1 = board.getPiece(7, 1);
|
||||
const whiteKnight2 = board.getPiece(7, 6);
|
||||
|
||||
@ -230,6 +230,7 @@ describe('Queen', () => {
|
||||
describe('Initial Position', () => {
|
||||
test('queens on initial board have no moves', () => {
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
const whiteQueen = board.getPiece(7, 3);
|
||||
const blackQueen = board.getPiece(0, 3);
|
||||
@ -244,6 +245,7 @@ describe('Queen', () => {
|
||||
|
||||
test('queen mobility increases as game progresses', () => {
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
const whiteQueen = board.getPiece(7, 3);
|
||||
const initialMoves = whiteQueen.getValidMoves(board);
|
||||
|
||||
@ -219,6 +219,7 @@ describe('Rook', () => {
|
||||
describe('Initial Position', () => {
|
||||
test('rooks on initial board have no moves', () => {
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
const whiteRook1 = board.getPiece(7, 0);
|
||||
const whiteRook2 = board.getPiece(7, 7);
|
||||
@ -233,6 +234,7 @@ describe('Rook', () => {
|
||||
|
||||
test('rook can move after pieces clear', () => {
|
||||
board = new Board();
|
||||
board.setupInitialPosition();
|
||||
|
||||
// Remove knight to open path
|
||||
board.setPiece(7, 1, null);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user