diff --git a/src/ai.ts b/src/ai.ts index 8dd9b1a..1a69d2d 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -55,10 +55,9 @@ export class AI { } // Vérifie si un mouvement d'ouverture basé sur les coups passés est disponible - const pastMoves = this.getPastMoves(); - const chosenMove = this.chooseMove(pastMoves, board); + const chosenMove = this.chooseMove(board); if (chosenMove) { - this.moveHistory.push(chosenMove); // Ajoute à l'historique des coups + this.moveHistory.push(chosenMove); return chosenMove; } const endgameMove = this.useEndgameTablebase(board); @@ -149,13 +148,6 @@ export class AI { this.historicalMoveScores.set(moveKey, currentScore + 1); } - // Fonction pour récupérer les coups passés en format abrégé - private getPastMoves(): string[] { - return this.moveHistory.map( - (move) => `${move.fromX}${move.fromY}${move.toX}${move.toY}`, - ); - } - // Fonction Minimax avec Alpha-Beta Pruning et table de transposition private minimax( board: Board, @@ -275,10 +267,14 @@ export class AI { } private chooseMove( - pastMoves: string[], board: Board, ): { fromX: number; fromY: number; toX: number; toY: number } | null { - const openingMove = getNextOpeningMove(pastMoves, this.openingMoves); + const key = this.moveHistory + .map((move) => `${move.fromX}${move.fromY}${move.toX}${move.toY}`) + .join(' '); + + console.log(key); + const openingMove = getNextOpeningMove(key, this.openingMoves); if (openingMove) { const flippedMove = flipMove( diff --git a/src/ai/evaluator.ts b/src/ai/evaluator.ts index c3037e7..9afb548 100644 --- a/src/ai/evaluator.ts +++ b/src/ai/evaluator.ts @@ -105,11 +105,19 @@ function getPieceSquareValue( x: number, y: number, flipBoard: boolean, + board: Board, + color: PieceColor, ): number { const table = pieceSquareTables[type]; if (!table) return 0; - // Inversion de la table pour les pièces noires si flipBoard est activé + // Empêcher les pions isolés de recevoir un bonus de position + if (type === PieceType.PAWN) { + const isIsolated = checkIsolatedPawns(board, x, y, color) > 0; + return isIsolated ? 0 : flipBoard ? table[7 - y][7 - x] : table[y][x]; + } + + // Retourne la valeur de position pour les autres pièces return flipBoard ? table[7 - y][7 - x] : table[y][x]; } @@ -124,16 +132,28 @@ export function evaluateBoard( for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { const piece = board.getPiece(x, y); + const positionKey = `${x},${y}`; if (!piece) continue; // Applique la valeur de base et la table de position let pieceScore = pieceValues[piece.type]; - pieceScore += getPieceSquareValue(piece.type, x, y, flipBoard); + pieceScore += getPieceSquareValue( + piece.type, + x, + y, + flipBoard, + board, + piece.color, + ); // Ajoute le bonus pour le contrôle du centre - const positionKey = `${x},${y}`; - if (centerControlBonus[positionKey]) { - pieceScore += centerControlBonus[positionKey]; + if ( + piece.type === PieceType.PAWN && + checkIsolatedPawns(board, x, y, piece.color) === 0 + ) { + if (centerControlBonus[positionKey]) { + pieceScore += centerControlBonus[positionKey]; + } } // Évalue les pions pour la structure et les chaînes protégées @@ -185,7 +205,6 @@ function evaluatePawnChains( return score; } -// Évaluer la structure des pions function evaluatePawnStructure( board: Board, x: number, @@ -194,17 +213,22 @@ function evaluatePawnStructure( ): number { let score = 0; - // Pénalise les pions doublés - score -= checkDoubledPawns(board, x, y, color) * 1.5; - - // Pénalise les pions isolés - score -= checkIsolatedPawns(board, x, y, color) * 1.5; + const isPassed = isPassedPawn(board, x, y, color); + const doubledPenalty = checkDoubledPawns(board, x, y, color) * 0.25; + const isolatedPenalty = checkIsolatedPawns(board, x, y, color) * 4.0; - // Bonus pour les pions passés - if (isPassedPawn(board, x, y, color)) { - score += 1.0; + if (isPassed) { + score += 4.5; // Bonus pour pion passé + console.log( + `Passed Pawn at (${x},${y}) | Passed bonus: 4.5, Score: ${score}`, + ); } + score -= doubledPenalty + isolatedPenalty; + console.log( + `Pawn at (${x},${y}) | Doubled penalty: ${doubledPenalty}, Isolated penalty: ${isolatedPenalty}, Score: ${score}`, + ); + return score; } @@ -235,18 +259,15 @@ function checkIsolatedPawns( const leftColumn = x - 1 >= 0 ? board.getPiece(x - 1, y) : null; const rightColumn = x + 1 < 8 ? board.getPiece(x + 1, y) : null; - if ( - (!leftColumn || - leftColumn.type !== PieceType.PAWN || - leftColumn.color !== color) && - (!rightColumn || - rightColumn.type !== PieceType.PAWN || - rightColumn.color !== color) - ) { - return 1.5; // Augmentation de la pénalité pour les pions isolés - } + const hasAdjacentSameColorPawns = + (leftColumn && + leftColumn.type === PieceType.PAWN && + leftColumn.color === color) || + (rightColumn && + rightColumn.type === PieceType.PAWN && + rightColumn.color === color); - return 0; + return hasAdjacentSameColorPawns ? 0 : 1.5; // Retourne une pénalité si le pion est isolé } // Gère les cases hors limites pour éviter des expositions de roi mal calculées @@ -291,8 +312,9 @@ function isPassedPawn( y: number, color: PieceColor, ): boolean { - const direction = color === PieceColor.WHITE ? -1 : 1; + const direction = color === PieceColor.WHITE ? 1 : -1; + // Vérifie s'il y a des pions adverses devant le pion sur la même colonne for (let i = y + direction; i >= 0 && i < 8; i += direction) { const pieceInFront = board.getPiece(x, i); if ( @@ -304,20 +326,22 @@ function isPassedPawn( } } - // Vérifier s'il y a des pions alliés sur les colonnes adjacentes + // Vérifie les colonnes adjacentes pour s'assurer qu'il n'y a pas de pions adverses bloquant const adjacentColumns = [x - 1, x + 1]; - return adjacentColumns.every((col) => { - if (col < 0 || col >= 8) return true; - for (let i = 0; i < 8; i++) { - const adjacentPiece = board.getPiece(col, i); - if ( - adjacentPiece && - adjacentPiece.type === PieceType.PAWN && - adjacentPiece.color === color - ) { - return false; + for (const col of adjacentColumns) { + if (col >= 0 && col < 8) { + for (let i = y + direction; i >= 0 && i < 8; i += direction) { + const adjacentPiece = board.getPiece(col, i); + if ( + adjacentPiece && + adjacentPiece.type === PieceType.PAWN && + adjacentPiece.color !== color + ) { + return false; + } } } - return true; - }); + } + + return true; } diff --git a/src/ai/openingBook.ts b/src/ai/openingBook.ts index 2264000..c39441b 100644 --- a/src/ai/openingBook.ts +++ b/src/ai/openingBook.ts @@ -119,11 +119,10 @@ export function flipMove( } export function getNextOpeningMove( - moves: string[], + key: string, openingBook: OpeningBook, ): { fromX: number; fromY: number; toX: number; toY: number } | null { - const key = moves.join(' '); - return openingBook[key]?.[moves.length] ?? null; + return openingBook[key]?.[0] ?? null; } export type OpeningBook = { diff --git a/tests/endgameTablebase.test.ts b/tests/endgameTablebase.test.ts index 372494c..03b3fc7 100644 --- a/tests/endgameTablebase.test.ts +++ b/tests/endgameTablebase.test.ts @@ -15,7 +15,7 @@ describe('Endgame Move Generation Tests', () => { beforeEach(async () => { board = new Board(); - await board.init(); // Initialise un échiquier vide pour chaque test + await board.init(); board['grid'] = Array(8).fill(null).map(() => Array(8).fill(null)); }); @@ -38,7 +38,7 @@ describe('Endgame Move Generation Tests', () => { const move = getEndgameMove(board, PieceColor.WHITE); expect(move).not.toBeNull(); - expect(move?.fromX).toBe(2); // Cavalier se déplace pour pousser le roi adverse + expect(move?.fromX).toBe(2); // Fou se déplace pour rapprocher le roi adverse }); test('King + Two Bishops vs King endgame move', async () => { @@ -49,7 +49,7 @@ describe('Endgame Move Generation Tests', () => { const move = getEndgameMove(board, PieceColor.WHITE); expect(move).not.toBeNull(); - expect(move?.fromX).toBe(2); // Fou se déplace pour forcer le roi adverse vers un coin + expect(move?.fromX).toBe(2); // Fou pousse le roi adverse vers le bord }); test('King + Pawn vs King endgame move', async () => { @@ -63,4 +63,24 @@ describe('Endgame Move Generation Tests', () => { expect(move?.fromY).toBe(5); expect(move?.toY).toBe(6); // Pion avance pour promotion }); + + test('No endgame move available when pieces do not match any endgame scenario', async () => { + await placePiece(board, PieceType.KING, PieceColor.WHITE, 4, 4); + await placePiece(board, PieceType.ROOK, PieceColor.WHITE, 5, 5); + await placePiece(board, PieceType.BISHOP, PieceColor.WHITE, 2, 2); + await placePiece(board, PieceType.KING, PieceColor.BLACK, 7, 7); + + const move = getEndgameMove(board, PieceColor.WHITE); + expect(move).toBeNull(); // Aucun mouvement optimal pour cette configuration + }); + + test('Endgame move generation considers flipBoard for black pieces', async () => { + await placePiece(board, PieceType.KING, PieceColor.BLACK, 0, 0); + await placePiece(board, PieceType.ROOK, PieceColor.BLACK, 1, 0); + await placePiece(board, PieceType.KING, PieceColor.WHITE, 7, 7); + + const move = getEndgameMove(board, PieceColor.BLACK, true); + expect(move).not.toBeNull(); + expect(move?.fromX).toBe(6); // La position est inversée pour la vue noire + }); }); diff --git a/tests/evaluateBoard.test.ts b/tests/evaluateBoard.test.ts index 69db3a6..bae661f 100644 --- a/tests/evaluateBoard.test.ts +++ b/tests/evaluateBoard.test.ts @@ -1,4 +1,4 @@ -// evaluateBoard.test.ts +// tests/evaluateBoard.test.ts import { Board } from '../src/board'; import { PieceColor } from '../src/piece'; import { Pawn } from '../src/pieces/pawn'; @@ -30,22 +30,22 @@ describe('evaluateBoard', () => { board.setPiece(4, 4, new Pawn(PieceColor.WHITE)); board.setPiece(4, 3, new Pawn(PieceColor.BLACK)); const score = evaluateBoard(board, PieceColor.WHITE); - expect(score).toBeGreaterThan(-0.1); // Ajustement du seuil attendu + expect(score).toBeGreaterThan(-0.1); // Seuil attendu pour les pions }); it('should evaluate material advantage correctly', () => { board.setPiece(0, 0, new Queen(PieceColor.WHITE)); board.setPiece(7, 7, new Rook(PieceColor.BLACK)); const score = evaluateBoard(board, PieceColor.WHITE); - expect(score).toBeCloseTo(3.8, 1); // Ajustement de la précision à 1 + expect(score).toBeCloseTo(3.8, 1); // Précision d'une décimale }); it('should consider pawn structure (doubled and isolated pawns)', () => { board.setPiece(0, 1, new Pawn(PieceColor.WHITE)); - board.setPiece(0, 2, new Pawn(PieceColor.WHITE)); - board.setPiece(2, 2, new Pawn(PieceColor.WHITE)); // pion isolé + board.setPiece(0, 2, new Pawn(PieceColor.WHITE)); // Pions doublés + board.setPiece(2, 2, new Pawn(PieceColor.WHITE)); // Pion isolé const score = evaluateBoard(board, PieceColor.WHITE); - expect(score).toBeLessThan(0); // Structure de pions affaiblie + expect(score).toBeLessThan(0); // Pénalisation pour structure faible }); it('should give a bonus for a bishop pair', () => { @@ -62,7 +62,7 @@ describe('evaluateBoard', () => { }); it('should apply center control bonus', () => { - board.setPiece(3, 3, new Knight(PieceColor.WHITE)); // Contrôle centre + board.setPiece(3, 3, new Knight(PieceColor.WHITE)); // Contrôle du centre const score = evaluateBoard(board, PieceColor.WHITE); expect(score).toBeGreaterThan(0); // Bonus pour contrôle du centre }); @@ -70,4 +70,23 @@ describe('evaluateBoard', () => { it('should return 0 for an empty board', () => { expect(evaluateBoard(board, PieceColor.WHITE)).toBe(0); }); + + it('should apply penalty for doubled pawns', () => { + board.setPiece(4, 4, new Pawn(PieceColor.WHITE)); + board.setPiece(4, 5, new Pawn(PieceColor.WHITE)); // Pions doublés sur la colonne 4 + const score = evaluateBoard(board, PieceColor.WHITE); + expect(score).toBeLessThan(0); // Pénalisation pour pions doublés + }); + + it('should penalize isolated pawn', () => { + board.setPiece(3, 3, new Pawn(PieceColor.WHITE)); + const score = evaluateBoard(board, PieceColor.WHITE); + expect(score).toBeLessThan(0); // Pénalisation pour pion isolé + }); + + it('should apply position value for knight', () => { + board.setPiece(2, 2, new Knight(PieceColor.WHITE)); + const score = evaluateBoard(board, PieceColor.WHITE); + expect(score).toBeGreaterThan(3); // Inclut la position favorable du cavalier + }); }); diff --git a/tests/openingBook.test.ts b/tests/openingBook.test.ts index e65c425..d2169ea 100644 --- a/tests/openingBook.test.ts +++ b/tests/openingBook.test.ts @@ -1,5 +1,5 @@ // tests/openingBook.test.ts -import { openingBook } from '../src/ai/openingBook'; +import { flipMove, getNextOpeningMove, openingBook } from '../src/ai/openingBook'; describe('Opening Book Tests', () => { test('Ruy Lopez Opening', () => { @@ -21,6 +21,16 @@ describe('Opening Book Tests', () => { ]); }); + test('Extended Sicilian Defense', () => { + const moves = openingBook['e2e4 c7c5 g1f3 d7d6']; + expect(moves).toEqual([ + { fromX: 4, fromY: 6, toX: 4, toY: 4 }, + { fromX: 2, fromY: 1, toX: 2, toY: 3 }, + { fromX: 6, fromY: 7, toX: 5, toY: 5 }, + { fromX: 3, fromY: 1, toX: 3, toY: 2 }, + ]); + }); + test('Queen\'s Gambit', () => { const moves = openingBook['d2d4 d7d5 c2c4']; expect(moves).toEqual([ @@ -38,6 +48,16 @@ describe('Opening Book Tests', () => { ]); }); + test('Extended Caro-Kann Defense', () => { + const moves = openingBook['e2e4 c7c6 d2d4 d7d5']; + expect(moves).toEqual([ + { fromX: 4, fromY: 6, toX: 4, toY: 4 }, + { fromX: 2, fromY: 1, toX: 2, toY: 2 }, + { fromX: 3, fromY: 6, toX: 3, toY: 4 }, + { fromX: 3, fromY: 1, toX: 3, toY: 3 }, + ]); + }); + test('French Defense', () => { const moves = openingBook['e2e4 e7e6']; expect(moves).toEqual([ @@ -46,6 +66,16 @@ describe('Opening Book Tests', () => { ]); }); + test('Extended French Defense', () => { + const moves = openingBook['e2e4 e7e6 d2d4 d7d5']; + expect(moves).toEqual([ + { fromX: 4, fromY: 6, toX: 4, toY: 4 }, + { fromX: 4, fromY: 1, toX: 4, toY: 2 }, + { fromX: 3, fromY: 6, toX: 3, toY: 4 }, + { fromX: 3, fromY: 1, toX: 3, toY: 3 }, + ]); + }); + test('Italian Game', () => { const moves = openingBook['e2e4 e7e5 g1f3 b8c6 f1c4']; expect(moves).toEqual([ @@ -88,4 +118,33 @@ describe('Opening Book Tests', () => { { fromX: 3, fromY: 1, toX: 3, toY: 3 }, ]); }); + + test('Réti Opening', () => { + const moves = openingBook['g1f3 d7d5']; + expect(moves).toEqual([ + { fromX: 6, fromY: 7, toX: 5, toY: 5 }, + { fromX: 3, fromY: 1, toX: 3, toY: 3 }, + ]); + }); + + test('Pirc Defense', () => { + const moves = openingBook['e2e4 d7d6']; + expect(moves).toEqual([ + { fromX: 4, fromY: 6, toX: 4, toY: 4 }, + { fromX: 3, fromY: 1, toX: 3, toY: 2 }, + ]); + }); + + + test('flipMove without flipping', () => { + const move = { fromX: 4, fromY: 6, toX: 4, toY: 4 }; + const result = flipMove(move, false); + expect(result).toEqual({ fromX: 4, fromY: 6, toX: 4, toY: 4 }); + }); + + test('flipMove with flipping', () => { + const move = { fromX: 4, fromY: 6, toX: 4, toY: 4 }; + const result = flipMove(move, true); + expect(result).toEqual({ fromX: 3, fromY: 1, toX: 3, toY: 3 }); + }); }); diff --git a/tests/piece.test.ts b/tests/piece.test.ts index e900bf2..39c427e 100644 --- a/tests/piece.test.ts +++ b/tests/piece.test.ts @@ -16,7 +16,8 @@ class MockBoard implements BoardInterface { this.board[y][x] = piece; } - updateEnPassantTarget(): void {} + updateEnPassantTarget(): void { + } isEnPassantMove(): boolean { return false; @@ -63,15 +64,20 @@ describe('Piece', () => { blackRook = await createPiece(PieceType.ROOK, PieceColor.BLACK); }); - test('isPathClear returns true for a clear path', () => { + test('isPathClear returns true for a clear vertical path', () => { board.setPiece(3, 3, whiteRook); expect(whiteRook.isPathClear(3, 3, 3, 6, board)).toBe(true); }); - test('isPathClear returns false when path is blocked', () => { + test('isPathClear returns true for a clear horizontal path', () => { board.setPiece(3, 3, whiteRook); - board.setPiece(3, 5, blackRook); - expect(whiteRook.isPathClear(3, 3, 3, 6, board)).toBe(false); + expect(whiteRook.isPathClear(3, 3, 6, 3, board)).toBe(true); + }); + + test('isPathClear returns false when path is blocked horizontally', () => { + board.setPiece(3, 3, whiteRook); + board.setPiece(5, 3, blackRook); + expect(whiteRook.isPathClear(3, 3, 6, 3, board)).toBe(false); }); test('canCapture returns true for opponent piece', () => { @@ -92,7 +98,7 @@ describe('Piece', () => { expect(data).toEqual({ color: PieceColor.WHITE, type: PieceType.ROOK, - hasMoved: false, + hasMoved: false, // Inclure le champ hasMoved avec sa valeur initiale }); }); @@ -103,4 +109,34 @@ describe('Piece', () => { expect(piece.color).toBe(PieceColor.BLACK); expect(piece.type).toBe(PieceType.KING); }); + + test('isKing correctly identifies a King piece', () => { + const blackKing = new King(PieceColor.BLACK); + expect(Piece.isKing(blackKing)).toBe(true); + expect(Piece.isKing(whiteRook)).toBe(false); + }); + + test('isPathClear for diagonal movement', () => { + board.setPiece(2, 2, whiteRook); + expect(whiteRook.isPathClear(2, 2, 5, 5, board)).toBe(true); + + // Blocking piece + board.setPiece(3, 3, blackRook); + expect(whiteRook.isPathClear(2, 2, 5, 5, board)).toBe(false); + }); + + test('canCapture returns true if target square is empty', () => { + board.setPiece(4, 4, whiteRook); + expect(whiteRook.canCapture(5, 5, board)).toBe(true); + }); + + test('toData correctly serializes piece data with hasMoved property', () => { + whiteRook.hasMoved = true; + const data = whiteRook.toData(); + expect(data).toEqual({ + color: PieceColor.WHITE, + type: PieceType.ROOK, + hasMoved: true, + }); + }); }); diff --git a/tests/pieces/bishop.test.ts b/tests/pieces/bishop.test.ts index f7dec1a..3326ac0 100644 --- a/tests/pieces/bishop.test.ts +++ b/tests/pieces/bishop.test.ts @@ -15,7 +15,8 @@ class MockBoard implements BoardInterface { this.board[y][x] = piece; } - updateEnPassantTarget(): void {} + updateEnPassantTarget(): void { + } isEnPassantMove(): boolean { return false; @@ -91,4 +92,25 @@ describe('Bishop', () => { board.setPiece(5, 5, anotherWhiteBishop); // Place friendly piece expect(whiteBishop.isValidMove(3, 3, 5, 5, board)).toBe(false); }); + + test('isValidMove returns false if moving outside of board limits', () => { + board.setPiece(7, 7, whiteBishop); + expect(whiteBishop.isValidMove(7, 7, 8, 8, board)).toBe(false); // Off the board + }); + + test('isValidMove returns true for diagonal moves near the edge of the board', () => { + board.setPiece(7, 7, whiteBishop); + expect(whiteBishop.isValidMove(7, 7, 5, 5, board)).toBe(true); // Move diagonally within limits + }); + + test('isValidMove returns true for long-range diagonal move with no obstacles', () => { + board.setPiece(0, 0, whiteBishop); + expect(whiteBishop.isValidMove(0, 0, 7, 7, board)).toBe(true); // Move from bottom-left to top-right corner + }); + + test('isValidMove returns false if blocked along a long diagonal path', () => { + board.setPiece(0, 0, whiteBishop); + board.setPiece(4, 4, blackBishop); // Block the path at (4,4) + expect(whiteBishop.isValidMove(0, 0, 7, 7, board)).toBe(false); + }); }); diff --git a/tests/pieces/king.test.ts b/tests/pieces/king.test.ts index 17091df..9ae45e5 100644 --- a/tests/pieces/king.test.ts +++ b/tests/pieces/king.test.ts @@ -3,10 +3,6 @@ import { King } from '../../src/pieces/king'; import { BoardInterface, PieceColor, PieceType } from '../../src/piece'; class MockBoard implements BoardInterface { - promotePawn(x: number, y: number, pieceType: PieceType): Promise { - throw new Error('Method not implemented.'); - } - private board: (King | null)[][] = Array(8) .fill(null) .map(() => Array(8).fill(null)); @@ -20,7 +16,8 @@ class MockBoard implements BoardInterface { this.board[y][x] = piece; } - updateEnPassantTarget(): void {} + updateEnPassantTarget(): void { + } isEnPassantMove(): boolean { return false; @@ -49,6 +46,10 @@ class MockBoard implements BoardInterface { toY: number, ): void { } + + promotePawn(x: number, y: number, pieceType: PieceType): Promise { + throw new Error('Method not implemented.'); + } } describe('King', () => { @@ -62,55 +63,101 @@ describe('King', () => { blackKing = new King(PieceColor.BLACK); }); + test('isValidMove returns false if moving outside of board limits', () => { + board.setPiece(0, 0, whiteKing); + expect(whiteKing.isValidMove(0, 0, 0, -1, board)).toBe(false); + }); + test('isValidMove returns true for a valid single square move', () => { board.setPiece(4, 4, whiteKing); - expect(whiteKing.isValidMove(4, 4, 4, 5, board)).toBe(true); // Move 1 up - expect(whiteKing.isValidMove(4, 4, 3, 4, board)).toBe(true); // Move 1 left - expect(whiteKing.isValidMove(4, 4, 4, 3, board)).toBe(true); // Move 1 down + expect(whiteKing.isValidMove(4, 4, 4, 5, board)).toBe(true); }); test('isValidMove returns false for a move adjacent to another king', () => { board.setPiece(4, 4, whiteKing); - expect(whiteKing.isValidMove(4, 4, 5, 4, board)).toBe(false); // Adjacent to a king at (5,4) + expect(whiteKing.isValidMove(4, 4, 5, 4, board)).toBe(false); // Adjacent to a king }); test('isValidMove returns true for capturing an opponent piece', () => { board.setPiece(4, 4, whiteKing); - board.setPiece(5, 5, blackKing); // Place opponent piece + board.setPiece(5, 5, blackKing); expect(whiteKing.isValidMove(4, 4, 5, 5, board)).toBe(true); }); test('isValidMove returns false for capturing a piece of the same color', () => { const anotherWhiteKing = new King(PieceColor.WHITE); board.setPiece(4, 4, whiteKing); - board.setPiece(5, 5, anotherWhiteKing); // Place friendly piece + board.setPiece(5, 5, anotherWhiteKing); expect(whiteKing.isValidMove(4, 4, 5, 5, board)).toBe(false); }); - test('isValidMove returns true for castling move', () => { + test('isValidMove returns true for castling move (queenside)', () => { + board.setPiece(4, 0, whiteKing); + const whiteRook = { + type: PieceType.ROOK, + hasMoved: false, + color: PieceColor.WHITE, + } as King; + board.setPiece(0, 0, whiteRook); + expect(whiteKing.isValidMove(4, 0, 2, 0, board)).toBe(true); + }); + + test('isValidMove returns true for castling move (kingside)', () => { + board.setPiece(4, 0, whiteKing); + const whiteRook = { + type: PieceType.ROOK, + hasMoved: false, + color: PieceColor.WHITE, + } as King; + board.setPiece(7, 0, whiteRook); + expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(true); + }); + + test('isValidMove returns false for castling if king has moved', () => { + whiteKing.hasMoved = true; board.setPiece(4, 0, whiteKing); const whiteRook = { type: PieceType.ROOK, hasMoved: false, color: PieceColor.WHITE, } as King; - board.setPiece(0, 0, whiteRook); // Place rook for queenside castle - expect(whiteKing.isValidMove(4, 0, 2, 0, board)).toBe(true); // Queenside castling + board.setPiece(7, 0, whiteRook); + expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(false); + }); - board.setPiece(7, 0, whiteRook); // Place rook for kingside castle - expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(true); // Kingside castling + test('isValidMove returns false for castling if rook has moved', () => { + board.setPiece(4, 0, whiteKing); + const whiteRook = { + type: PieceType.ROOK, + hasMoved: true, // Rook has moved + color: PieceColor.WHITE, + } as King; + board.setPiece(7, 0, whiteRook); + expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(false); }); - test('isValidMove returns false for castling if the path is under attack', () => { + test('isValidMove returns false for castling if path is blocked', () => { board.setPiece(4, 0, whiteKing); const whiteRook = { type: PieceType.ROOK, hasMoved: false, color: PieceColor.WHITE, } as King; - board.setPiece(7, 0, whiteRook); // Place rook for kingside castle + board.setPiece(7, 0, whiteRook); + const blockingPiece = new King(PieceColor.WHITE); + board.setPiece(5, 0, blockingPiece); // Block the castling path + expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(false); + }); - board.isSquareUnderAttackCalled = true; // Simule un carré sous attaque - expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(false); // Path under attack + test('isValidMove returns false for castling if any square is under attack', () => { + board.setPiece(4, 0, whiteKing); + const whiteRook = { + type: PieceType.ROOK, + hasMoved: false, + color: PieceColor.WHITE, + } as King; + board.setPiece(7, 0, whiteRook); + board.isSquareUnderAttackCalled = true; // Simulate square under attack + expect(whiteKing.isValidMove(4, 0, 6, 0, board)).toBe(false); }); }); diff --git a/tests/pieces/knight.test.ts b/tests/pieces/knight.test.ts index 21c5ded..115fec4 100644 --- a/tests/pieces/knight.test.ts +++ b/tests/pieces/knight.test.ts @@ -15,7 +15,8 @@ class MockBoard implements BoardInterface { this.board[y][x] = piece; } - updateEnPassantTarget(): void {} + updateEnPassantTarget(): void { + } isEnPassantMove(): boolean { return false; @@ -24,7 +25,7 @@ class MockBoard implements BoardInterface { promotePawn(x: number, y: number, pieceType: PieceType): Promise { throw new Error('Method not implemented.'); } - + isSquareUnderAttack(): boolean { return false; } @@ -91,4 +92,16 @@ describe('Knight', () => { board.setPiece(5, 4, new Knight(PieceColor.WHITE)); // Place friendly piece expect(whiteKnight.isValidMove(3, 3, 5, 4, board)).toBe(false); }); + + test('isValidMove returns false if moving outside of board limits', () => { + board.setPiece(0, 0, whiteKnight); + expect(whiteKnight.isValidMove(0, 0, -2, 1, board)).toBe(false); // Off left edge + expect(whiteKnight.isValidMove(0, 0, 1, -2, board)).toBe(false); // Off top edge + }); + + test('isValidMove returns true for L-shape moves near the edge of the board', () => { + board.setPiece(1, 0, whiteKnight); // Near top-left edge + expect(whiteKnight.isValidMove(1, 0, 0, 2, board)).toBe(true); // Move 1 left, 2 down + expect(whiteKnight.isValidMove(1, 0, 2, 2, board)).toBe(true); // Move 1 right, 2 down + }); }); diff --git a/tests/pieces/pawn.test.ts b/tests/pieces/pawn.test.ts index 1a0f938..225a7b4 100644 --- a/tests/pieces/pawn.test.ts +++ b/tests/pieces/pawn.test.ts @@ -75,6 +75,13 @@ describe('Pawn', () => { blackPawn = new Pawn(PieceColor.BLACK); }); + test('isValidMove returns false if moving outside of board limits', () => { + board.setPiece(0, 0, whitePawn); + board.setPiece(7, 7, blackPawn); + expect(whitePawn.isValidMove(0, 0, 0, -1, board)).toBe(false); + expect(blackPawn.isValidMove(7, 7, 7, 8, board)).toBe(false); + }); + test('isValidMove returns true for a single step forward', () => { board.setPiece(3, 1, whitePawn); expect(whitePawn.isValidMove(3, 1, 3, 2, board)).toBe(true); @@ -106,15 +113,32 @@ describe('Pawn', () => { board.setPiece(4, 4, whitePawn); board.setPiece(5, 4, blackPawn); - // Simule le mouvement de deux cases pour le pion noir + // Simulate a two-square move for the black pawn board.updateEnPassantTarget(5, 4, 5, 5, blackPawn); - // Test de la capture en passant par le pion blanc + // Test en passant capture by white pawn expect(whitePawn.isValidMove(4, 4, 5, 5, board)).toBe(true); }); - test('isValidMove returns true for promotion row move', () => { + test('isValidMove returns true for promotion move', () => { board.setPiece(3, 6, whitePawn); expect(whitePawn.isValidMove(3, 6, 3, 7, board)).toBe(true); }); + + test('isValidMove returns false for backward move', () => { + board.setPiece(3, 3, whitePawn); + expect(whitePawn.isValidMove(3, 3, 3, 2, board)).toBe(false); + }); + + test('en passant target is reset after capture', () => { + board.setPiece(4, 4, whitePawn); + board.setPiece(5, 4, blackPawn); + + // Simulate black pawn's two-square move + board.updateEnPassantTarget(5, 4, 5, 5, blackPawn); + + // White pawn captures en passant + expect(whitePawn.isValidMove(4, 4, 5, 5, board)).toBe(true); + board.captureEnPassantIfValid(4, 4, 5, 5); + }); }); diff --git a/tests/pieces/queen.test.ts b/tests/pieces/queen.test.ts index 72652fa..4d6ca55 100644 --- a/tests/pieces/queen.test.ts +++ b/tests/pieces/queen.test.ts @@ -15,7 +15,8 @@ class MockBoard implements BoardInterface { this.board[y][x] = piece; } - updateEnPassantTarget(): void {} + updateEnPassantTarget(): void { + } isEnPassantMove(): boolean { return false; @@ -61,6 +62,18 @@ describe('Queen', () => { blackQueen = new Queen(PieceColor.BLACK); }); + test('isValidMove returns false when path is blocked horizontally', () => { + board.setPiece(3, 3, whiteQueen); + board.setPiece(4, 3, blackQueen); + expect(whiteQueen.isValidMove(3, 3, 5, 3, board)).toBe(false); + }); + + test('isValidMove returns false if moving outside of board limits', () => { + board.setPiece(3, 3, whiteQueen); + expect(whiteQueen.isValidMove(3, 3, -1, 3, board)).toBe(false); + expect(whiteQueen.isValidMove(3, 3, 3, 8, board)).toBe(false); + }); + test('isValidMove returns true for a valid vertical move', () => { board.setPiece(3, 3, whiteQueen); expect(whiteQueen.isValidMove(3, 3, 3, 7, board)).toBe(true); diff --git a/tests/pieces/rook.test.ts b/tests/pieces/rook.test.ts index ba6c72c..43cda5c 100644 --- a/tests/pieces/rook.test.ts +++ b/tests/pieces/rook.test.ts @@ -15,7 +15,8 @@ class MockBoard implements BoardInterface { this.board[y][x] = piece; } - updateEnPassantTarget(): void {} + updateEnPassantTarget(): void { + } isEnPassantMove(): boolean { return false; @@ -76,18 +77,37 @@ describe('Rook', () => { expect(whiteRook.isValidMove(3, 3, 5, 5, board)).toBe(false); }); - test('isValidMove returns false when path is blocked', () => { + test('isValidMove returns false when path is blocked vertically', () => { board.setPiece(3, 3, whiteRook); - board.setPiece(3, 4, blackRook); // Placer une pièce pour bloquer le chemin + board.setPiece(3, 4, blackRook); // Block the path expect(whiteRook.isValidMove(3, 3, 3, 5, board)).toBe(false); }); + test('isValidMove returns false when path is blocked horizontally', () => { + board.setPiece(3, 3, whiteRook); + board.setPiece(4, 3, blackRook); // Block the path + expect(whiteRook.isValidMove(3, 3, 5, 3, board)).toBe(false); + }); + + test('isValidMove returns false if moving outside of board limits', () => { + board.setPiece(3, 3, whiteRook); + expect(whiteRook.isValidMove(3, 3, -1, 3, board)).toBe(false); + expect(whiteRook.isValidMove(3, 3, 3, 8, board)).toBe(false); + }); + test('canCapture returns true for opponent piece', () => { board.setPiece(3, 3, whiteRook); board.setPiece(3, 5, blackRook); expect(whiteRook.canCapture(3, 5, board)).toBe(true); }); + test('canCapture returns false for same color piece', () => { + const anotherWhiteRook = new Rook(PieceColor.WHITE); + board.setPiece(3, 3, whiteRook); + board.setPiece(3, 5, anotherWhiteRook); + expect(whiteRook.canCapture(3, 5, board)).toBe(false); + }); + test('toData returns correct piece data', () => { const data = whiteRook.toData(); expect(data).toEqual({ @@ -104,4 +124,14 @@ describe('Rook', () => { expect(rook.color).toBe(PieceColor.BLACK); expect(rook.hasMoved).toBe(true); }); + + test('toData serializes piece with hasMoved property set to true', () => { + whiteRook.hasMoved = true; + const data = whiteRook.toData(); + expect(data).toEqual({ + color: PieceColor.WHITE, + type: 'rook', + hasMoved: true, + }); + }); }); diff --git a/tests/timer.test.ts b/tests/timer.test.ts index 0769d2d..f70dc10 100644 --- a/tests/timer.test.ts +++ b/tests/timer.test.ts @@ -18,14 +18,12 @@ describe('Timer', () => { it('should start the timer and call onTimeUpdate every second', () => { timer.start(); - // Avancer le temps de 3 secondes jest.advanceTimersByTime(3000); - // Vérifier que la fonction onTimeUpdate a été appelée 3 fois avec le temps mis à jour expect(onTimeUpdate).toHaveBeenCalledTimes(3); - expect(onTimeUpdate).toHaveBeenCalledWith(9); // 1 sec écoulée (10-1) - expect(onTimeUpdate).toHaveBeenCalledWith(8); // 2 sec écoulées (10-2) - expect(onTimeUpdate).toHaveBeenCalledWith(7); // 3 sec écoulées (10-3) + expect(onTimeUpdate).toHaveBeenCalledWith(9); + expect(onTimeUpdate).toHaveBeenCalledWith(8); + expect(onTimeUpdate).toHaveBeenCalledWith(7); }); it('should stop the timer when stop is called', () => { @@ -33,36 +31,75 @@ describe('Timer', () => { jest.advanceTimersByTime(3000); timer.stop(); - // Avancer le temps de 3 secondes de plus jest.advanceTimersByTime(3000); - // Vérifier que la fonction onTimeUpdate n'a pas été appelée après l'arrêt expect(onTimeUpdate).toHaveBeenCalledTimes(3); }); it('should reset the timer to a new time and start immediately', () => { timer.start(); jest.advanceTimersByTime(2000); - timer.reset(5); // Réinitialiser le timer à 5 secondes + timer.reset(5); - // Vérifier immédiatement l'appel initial avec la nouvelle valeur réinitialisée expect(onTimeUpdate).toHaveBeenCalledWith(5); - // Continuer à avancer le temps jest.advanceTimersByTime(5000); - // Vérifier que le timer a atteint 0 expect(onTimeUpdate).toHaveBeenCalledWith(0); }); it('should stop the timer when the time reaches 0', () => { timer.start(); - // Avancer le temps de 10 secondes pour que le timer atteigne 0 jest.advanceTimersByTime(10000); - // Vérifier que le timer s'est arrêté et appelle onTimeUpdate avec 0 expect(onTimeUpdate).toHaveBeenCalledWith(0); expect(timer.isRunning).toBe(false); }); + + it('should not start the timer if already running', () => { + timer.start(); + jest.advanceTimersByTime(2000); + + timer.start(); // Start à nouveau sans effet + jest.advanceTimersByTime(2000); + + expect(onTimeUpdate).toHaveBeenCalledTimes(4); + }); + + it('should reset the timer and not start if startImmediately is false', () => { + timer.start(); + jest.advanceTimersByTime(2000); + timer.reset(8, false); // Réinitialiser à 8 secondes sans démarrage immédiat + + expect(onTimeUpdate).toHaveBeenCalledWith(8); + expect(timer.isRunning).toBe(false); + + jest.advanceTimersByTime(2000); + + expect(onTimeUpdate).toHaveBeenCalledTimes(3); // Aucun appel supplémentaire après la réinitialisation + }); + + it('should handle multiple stops without errors', () => { + timer.start(); + jest.advanceTimersByTime(3000); + + timer.stop(); + timer.stop(); // Appel stop multiple + jest.advanceTimersByTime(2000); + + expect(onTimeUpdate).toHaveBeenCalledTimes(3); + expect(timer.isRunning).toBe(false); + }); + + it('should trigger onTimeUpdate immediately upon reset with the new time', () => { + timer.reset(15); + + expect(onTimeUpdate).toHaveBeenCalledWith(15); + expect(timer.isRunning).toBe(true); + + jest.advanceTimersByTime(5000); + + expect(onTimeUpdate).toHaveBeenCalledWith(10); + }); }); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index ee60e88..7059d86 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -14,7 +14,7 @@ describe('utils', () => { capturedWhite.length = 0; capturedBlack.length = 0; - // Mock de l'élément DOM pour les messages de jeu + // Mock de l'élément DOM pour les messages de jeu et les captures document.body.innerHTML = `
@@ -32,7 +32,6 @@ describe('utils', () => { }); it('should update captured pieces for white and black', () => { - // Ajouter des pièces capturées updateCapturedPieces(PieceType.PAWN, PieceColor.WHITE); updateCapturedPieces(PieceType.QUEEN, PieceColor.BLACK); @@ -41,7 +40,6 @@ describe('utils', () => { }); it('should update the captured pieces in the DOM', () => { - // Ajouter des pièces capturées updateCapturedPieces(PieceType.KNIGHT, PieceColor.WHITE); updateCapturedPieces(PieceType.ROOK, PieceColor.BLACK); updateCapturedPiecesDOM(); @@ -66,4 +64,37 @@ describe('utils', () => { expect(capturedWhiteElement.textContent).toBe('♗ ♙'); expect(capturedBlackElement.textContent).toBe('♜ ♞'); }); + + it('should not display captured pieces if none are captured', () => { + updateCapturedPiecesDOM(); + + const capturedWhiteElement = document.getElementById('capturedWhite')!; + const capturedBlackElement = document.getElementById('capturedBlack')!; + + expect(capturedWhiteElement.textContent).toBe(''); + expect(capturedBlackElement.textContent).toBe(''); + }); + + it('should clear captured pieces when reset', () => { + updateCapturedPieces(PieceType.QUEEN, PieceColor.WHITE); + updateCapturedPieces(PieceType.ROOK, PieceColor.BLACK); + + // Réinitialiser + capturedWhite.length = 0; + capturedBlack.length = 0; + updateCapturedPiecesDOM(); + + const capturedWhiteElement = document.getElementById('capturedWhite')!; + const capturedBlackElement = document.getElementById('capturedBlack')!; + + expect(capturedWhiteElement.textContent).toBe(''); + expect(capturedBlackElement.textContent).toBe(''); + }); + + it('should only show the game message element when there is a message', () => { + const gameMessageElement = document.getElementById('gameMessage')!; + + showMessage('New game started'); + expect(gameMessageElement.style.display).toBe('block'); + }); });