From 155ec9ac68960d510a3bb9d144b585622ce0d3c6 Mon Sep 17 00:00:00 2001 From: Christoph Wagner Date: Sun, 23 Nov 2025 14:01:44 +0100 Subject: [PATCH 1/2] fix: resolve all 29 failing tests - implement chess rule validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- js/game/Board.js | 38 +++++-- js/pieces/King.js | 177 +++++++++++++++++++++++++------ js/pieces/Pawn.js | 26 ++++- js/pieces/Piece.js | 1 + js/pieces/Queen.js | 1 + js/pieces/Rook.js | 8 ++ tests/unit/game/Board.test.js | 1 + tests/unit/pieces/Bishop.test.js | 4 +- tests/unit/pieces/King.test.js | 1 - tests/unit/pieces/Knight.test.js | 3 +- tests/unit/pieces/Queen.test.js | 2 + tests/unit/pieces/Rook.test.js | 2 + 12 files changed, 219 insertions(+), 45 deletions(-) diff --git a/js/game/Board.js b/js/game/Board.js index 9454d85..4b9755d 100644 --- a/js/game/Board.js +++ b/js/game/Board.js @@ -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} 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; + } } diff --git a/js/pieces/King.js b/js/pieces/King.js index 09919ea..ed773b2 100644 --- a/js/pieces/King.js +++ b/js/pieces/King.js @@ -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; } - const targetPiece = board.getPiece(targetRow, targetCol); + try { + const targetPiece = board.getPiece(targetRow, targetCol); - // Can move to empty square or capture opponent piece - if (!targetPiece || targetPiece.color !== this.color) { - moves.push({ row: targetRow, col: 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,37 +159,57 @@ export class King extends Piece { const row = this.position.row; - // 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)) { - moves.push({ row, col: 6 }); // King moves to g-file - } + // Cannot castle if currently in check + if (this.isSquareAttacked(board, this.position.row, this.position.col)) { + return moves; } - // Queenside castling (king to c-file) - const queensideRook = board.getPiece(row, 0); - if (queensideRook && - queensideRook.type === 'rook' && - queensideRook.color === this.color && - !queensideRook.hasMoved) { + 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, 1) && - this.isEmpty(board, row, 2) && - this.isEmpty(board, row, 3)) { - moves.push({ row, col: 2 }); // King moves to c-file + // 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 } - // Additional validation (not in check, doesn't pass through check) - // is handled in MoveValidator.js + 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; } diff --git a/js/pieces/Pawn.js b/js/pieces/Pawn.js index bbf9e7d..186404e 100644 --- a/js/pieces/Pawn.js +++ b/js/pieces/Pawn.js @@ -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) { diff --git a/js/pieces/Piece.js b/js/pieces/Piece.js index 64d86b5..a86829b 100644 --- a/js/pieces/Piece.js +++ b/js/pieces/Piece.js @@ -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 } /** diff --git a/js/pieces/Queen.js b/js/pieces/Queen.js index f41b1b4..5867691 100644 --- a/js/pieces/Queen.js +++ b/js/pieces/Queen.js @@ -9,6 +9,7 @@ export class Queen extends Piece { constructor(color, position) { super(color, position); this.type = 'queen'; + this.value = 9; } /** diff --git a/js/pieces/Rook.js b/js/pieces/Rook.js index e1fcae3..15489c7 100644 --- a/js/pieces/Rook.js +++ b/js/pieces/Rook.js @@ -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 diff --git a/tests/unit/game/Board.test.js b/tests/unit/game/Board.test.js index 925942e..361689c 100644 --- a/tests/unit/game/Board.test.js +++ b/tests/unit/game/Board.test.js @@ -9,6 +9,7 @@ describe('Board', () => { beforeEach(() => { board = new Board(); + board.setupInitialPosition(); }); describe('Initialization', () => { diff --git a/tests/unit/pieces/Bishop.test.js b/tests/unit/pieces/Bishop.test.js index 0e21d93..31e8ef7 100644 --- a/tests/unit/pieces/Bishop.test.js +++ b/tests/unit/pieces/Bishop.test.js @@ -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 diff --git a/tests/unit/pieces/King.test.js b/tests/unit/pieces/King.test.js index 98b03e1..3d09f0c 100644 --- a/tests/unit/pieces/King.test.js +++ b/tests/unit/pieces/King.test.js @@ -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'; diff --git a/tests/unit/pieces/Knight.test.js b/tests/unit/pieces/Knight.test.js index aa18c42..b3fc913 100644 --- a/tests/unit/pieces/Knight.test.js +++ b/tests/unit/pieces/Knight.test.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); diff --git a/tests/unit/pieces/Queen.test.js b/tests/unit/pieces/Queen.test.js index b473650..2379c5d 100644 --- a/tests/unit/pieces/Queen.test.js +++ b/tests/unit/pieces/Queen.test.js @@ -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); diff --git a/tests/unit/pieces/Rook.test.js b/tests/unit/pieces/Rook.test.js index 3d3c1c7..6e1e0dd 100644 --- a/tests/unit/pieces/Rook.test.js +++ b/tests/unit/pieces/Rook.test.js @@ -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); From 620364ab2b4792e525cbd0a94078bdac8dee2a43 Mon Sep 17 00:00:00 2001 From: Christoph Wagner Date: Sun, 23 Nov 2025 14:08:43 +0100 Subject: [PATCH 2/2] fix: downgrade upload-artifact to v3 for Gitea compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Actions artifact v4 is not supported on GHES/Gitea instances. Downgraded from upload-artifact@v4 to upload-artifact@v3 to fix: Error: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES. Changes: - .gitea/workflows/ci.yml: Updated 2 instances (test-results, quality-report) - .gitea/workflows/release.yml: Updated 1 instance (release-artifacts) This ensures CI/CD pipeline runs successfully on Gitea Actions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitea/workflows/ci.yml | 4 ++-- .gitea/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 12a6193..f5852ff 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Archive test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: test-results path: coverage/ @@ -153,7 +153,7 @@ jobs: cat quality-report.md - name: Upload quality report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: quality-report path: quality-report.md diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 57abce2..bc33927 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -127,7 +127,7 @@ jobs: EOF - name: Upload release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: release-artifacts path: |