Spaces:
Sleeping
Sleeping
| import { Chess, Square, PieceSymbol, Color } from 'chess.js' | |
| export class PositionEvaluator { | |
| private static readonly PIECE_VALUES: Record<PieceSymbol, number> = { | |
| 'p': 100, | |
| 'n': 320, | |
| 'b': 330, | |
| 'r': 500, | |
| 'q': 900, | |
| 'k': 20000 | |
| } | |
| private static readonly PAWN_TABLE = [ | |
| 0, 0, 0, 0, 0, 0, 0, 0, | |
| 50, 50, 50, 50, 50, 50, 50, 50, | |
| 10, 10, 20, 30, 30, 20, 10, 10, | |
| 5, 5, 10, 25, 25, 10, 5, 5, | |
| 0, 0, 0, 20, 20, 0, 0, 0, | |
| 5, -5,-10, 0, 0,-10, -5, 5, | |
| 5, 10, 10,-20,-20, 10, 10, 5, | |
| 0, 0, 0, 0, 0, 0, 0, 0 | |
| ] | |
| private static readonly KNIGHT_TABLE = [ | |
| -50,-40,-30,-30,-30,-30,-40,-50, | |
| -40,-20, 0, 0, 0, 0,-20,-40, | |
| -30, 0, 10, 15, 15, 10, 0,-30, | |
| -30, 5, 15, 20, 20, 15, 5,-30, | |
| -30, 0, 15, 20, 20, 15, 0,-30, | |
| -30, 5, 10, 15, 15, 10, 5,-30, | |
| -40,-20, 0, 5, 5, 0,-20,-40, | |
| -50,-40,-30,-30,-30,-30,-40,-50 | |
| ] | |
| private static readonly BISHOP_TABLE = [ | |
| -20,-10,-10,-10,-10,-10,-10,-20, | |
| -10, 0, 0, 0, 0, 0, 0,-10, | |
| -10, 0, 5, 10, 10, 5, 0,-10, | |
| -10, 5, 5, 10, 10, 5, 5,-10, | |
| -10, 0, 10, 10, 10, 10, 0,-10, | |
| -10, 10, 10, 10, 10, 10, 10,-10, | |
| -10, 5, 0, 0, 0, 0, 5,-10, | |
| -20,-10,-10,-10,-10,-10,-10,-20 | |
| ] | |
| private static readonly ROOK_TABLE = [ | |
| 0, 0, 0, 0, 0, 0, 0, 0, | |
| 5, 10, 10, 10, 10, 10, 10, 5, | |
| -5, 0, 0, 0, 0, 0, 0, -5, | |
| -5, 0, 0, 0, 0, 0, 0, -5, | |
| -5, 0, 0, 0, 0, 0, 0, -5, | |
| -5, 0, 0, 0, 0, 0, 0, -5, | |
| -5, 0, 0, 0, 0, 0, 0, -5, | |
| 0, 0, 0, 5, 5, 0, 0, 0 | |
| ] | |
| private static readonly QUEEN_TABLE = [ | |
| -20,-10,-10, -5, -5,-10,-10,-20, | |
| -10, 0, 0, 0, 0, 0, 0,-10, | |
| -10, 0, 5, 5, 5, 5, 0,-10, | |
| -5, 0, 5, 5, 5, 5, 0, -5, | |
| 0, 0, 5, 5, 5, 5, 0, -5, | |
| -10, 5, 5, 5, 5, 5, 0,-10, | |
| -10, 0, 5, 0, 0, 0, 0,-10, | |
| -20,-10,-10, -5, -5,-10,-10,-20 | |
| ] | |
| private static readonly KING_MIDDLE_GAME_TABLE = [ | |
| -30,-40,-40,-50,-50,-40,-40,-30, | |
| -30,-40,-40,-50,-50,-40,-40,-30, | |
| -30,-40,-40,-50,-50,-40,-40,-30, | |
| -30,-40,-40,-50,-50,-40,-40,-30, | |
| -20,-30,-30,-40,-40,-30,-30,-20, | |
| -10,-20,-20,-20,-20,-20,-20,-10, | |
| 20, 20, 0, 0, 0, 0, 20, 20, | |
| 20, 30, 10, 0, 0, 10, 30, 20 | |
| ] | |
| private static readonly KING_END_GAME_TABLE = [ | |
| -50,-40,-30,-20,-20,-30,-40,-50, | |
| -30,-20,-10, 0, 0,-10,-20,-30, | |
| -30,-10, 20, 30, 30, 20,-10,-30, | |
| -30,-10, 30, 40, 40, 30,-10,-30, | |
| -30,-10, 30, 40, 40, 30,-10,-30, | |
| -30,-10, 20, 30, 30, 20,-10,-30, | |
| -30,-30, 0, 0, 0, 0,-30,-30, | |
| -50,-30,-30,-30,-30,-30,-30,-50 | |
| ] | |
| /** | |
| * Evaluates the current position from White's perspective | |
| * Positive values favor White, negative values favor Black | |
| */ | |
| public static evaluatePosition(board: Chess): number { | |
| if (board.isGameOver()) { | |
| if (board.isCheckmate()) { | |
| return board.turn() === 'w' ? -30000 : 30000 | |
| } | |
| return 0 // draw | |
| } | |
| let score = 0 | |
| const isEndGame = this.isEndGame(board) | |
| // material and positional evaluation | |
| for (let rank = 0; rank < 8; rank++) { | |
| for (let file = 0; file < 8; file++) { | |
| const square = (String.fromCharCode(97 + file) + (rank + 1)) as Square | |
| const piece = board.get(square) | |
| if (piece) { | |
| const pieceValue = this.evaluatePiece(piece.type, piece.color, rank, file, isEndGame) | |
| score += piece.color === 'w' ? pieceValue : -pieceValue | |
| } | |
| } | |
| } | |
| // mobility bonus | |
| const { whiteMobility, blackMobility } = this.calculateMobility(board) | |
| score += (whiteMobility - blackMobility) * 10 | |
| // king safety in middle game | |
| if (!isEndGame) { | |
| score += this.evaluateKingSafety(board, 'w') | |
| score -= this.evaluateKingSafety(board, 'b') | |
| } | |
| // pawn structure | |
| score += this.evaluatePawnStructure(board) | |
| // center control | |
| score += this.evaluateCenterControl(board) | |
| return score | |
| } | |
| private static evaluatePiece( | |
| pieceType: PieceSymbol, | |
| color: Color, | |
| rank: number, | |
| file: number, | |
| isEndGame: boolean | |
| ): number { | |
| const index = rank * 8 + file | |
| const flippedIndex = color === 'w' ? (7 - rank) * 8 + file : index | |
| let value = this.PIECE_VALUES[pieceType] | |
| switch (pieceType) { | |
| case 'p': | |
| value += this.PAWN_TABLE[flippedIndex] | |
| break | |
| case 'n': | |
| value += this.KNIGHT_TABLE[flippedIndex] | |
| break | |
| case 'b': | |
| value += this.BISHOP_TABLE[flippedIndex] | |
| break | |
| case 'r': | |
| value += this.ROOK_TABLE[flippedIndex] | |
| break | |
| case 'q': | |
| value += this.QUEEN_TABLE[flippedIndex] | |
| break | |
| case 'k': | |
| if (isEndGame) { | |
| value += this.KING_END_GAME_TABLE[flippedIndex] | |
| } else { | |
| value += this.KING_MIDDLE_GAME_TABLE[flippedIndex] | |
| } | |
| break | |
| } | |
| return value | |
| } | |
| private static isEndGame(board: Chess): boolean { | |
| let materialCount = 0 | |
| const squares = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] | |
| for (const file of squares) { | |
| for (let rank = 1; rank <= 8; rank++) { | |
| const square = (file + rank) as Square | |
| const piece = board.get(square) | |
| if (piece && piece.type !== 'k' && piece.type !== 'p') { | |
| materialCount += this.PIECE_VALUES[piece.type] | |
| } | |
| } | |
| } | |
| return materialCount < 2500 | |
| } | |
| private static evaluateKingSafety(board: Chess, color: Color): number { | |
| const kingSquare = this.findKing(board, color) | |
| if (!kingSquare) return 0 | |
| let safety = 0 | |
| const kingFile = kingSquare.charCodeAt(0) - 97 | |
| const kingRank = parseInt(kingSquare[1]) - 1 | |
| // check for pawn shield | |
| const pawnShieldRank = color === 'w' ? kingRank + 1 : kingRank - 1 | |
| if (pawnShieldRank >= 0 && pawnShieldRank < 8) { | |
| for (let file = Math.max(0, kingFile - 1); file <= Math.min(7, kingFile + 1); file++) { | |
| const shieldSquare = (String.fromCharCode(97 + file) + (pawnShieldRank + 1)) as Square | |
| const piece = board.get(shieldSquare) | |
| if (piece && piece.type === 'p' && piece.color === color) { | |
| safety += 30 | |
| } | |
| } | |
| } | |
| return safety | |
| } | |
| private static evaluatePawnStructure(board: Chess): number { | |
| let score = 0 | |
| const files = [0, 0, 0, 0, 0, 0, 0, 0] | |
| // count pawns | |
| for (let file = 0; file < 8; file++) { | |
| let whitePawns = 0 | |
| let blackPawns = 0 | |
| for (let rank = 0; rank < 8; rank++) { | |
| const square = (String.fromCharCode(97 + file) + (rank + 1)) as Square | |
| const piece = board.get(square) | |
| if (piece && piece.type === 'p') { | |
| if (piece.color === 'w') whitePawns++ | |
| else blackPawns++ | |
| } | |
| } | |
| // penalty for doubled pawns | |
| if (whitePawns > 1) score -= (whitePawns - 1) * 50 | |
| if (blackPawns > 1) score += (blackPawns - 1) * 50 | |
| files[file] = whitePawns - blackPawns | |
| } | |
| // penalty for isolated pawns | |
| for (let file = 0; file < 8; file++) { | |
| if (files[file] !== 0) { | |
| const leftFile = file > 0 ? files[file - 1] : 0 | |
| const rightFile = file < 7 ? files[file + 1] : 0 | |
| if (leftFile === 0 && rightFile === 0) { | |
| score -= Math.abs(files[file]) * 20 | |
| } | |
| } | |
| } | |
| return score | |
| } | |
| private static evaluateCenterControl(board: Chess): number { | |
| let score = 0 | |
| const centerSquares = ['d4', 'd5', 'e4', 'e5'] | |
| for (const square of centerSquares) { | |
| const piece = board.get(square as Square) | |
| if (piece) { | |
| const value = piece.type === 'p' ? 20 : 10 | |
| score += piece.color === 'w' ? value : -value | |
| } | |
| } | |
| return score | |
| } | |
| private static findKing(board: Chess, color: Color): Square | null { | |
| const squares = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] | |
| for (const file of squares) { | |
| for (let rank = 1; rank <= 8; rank++) { | |
| const square = (file + rank) as Square | |
| const piece = board.get(square) | |
| if (piece && piece.type === 'k' && piece.color === color) { | |
| return square | |
| } | |
| } | |
| } | |
| return null | |
| } | |
| private static calculateMobility(board: Chess): { whiteMobility: number, blackMobility: number } { | |
| const originalFen = board.fen() | |
| const currentTurn = board.turn() | |
| let whiteMobility = 0 | |
| let blackMobility = 0 | |
| if (currentTurn === 'w') { | |
| whiteMobility = board.moves().length | |
| const fenParts = originalFen.split(' ') | |
| fenParts[1] = 'b' | |
| try { | |
| const blackTurnFen = fenParts.join(' ') | |
| const blackBoard = new Chess(blackTurnFen) | |
| blackMobility = blackBoard.moves().length | |
| } catch { | |
| blackMobility = 0 | |
| } | |
| } else { | |
| blackMobility = board.moves().length | |
| const fenParts = originalFen.split(' ') | |
| fenParts[1] = 'w' | |
| try { | |
| const whiteTurnFen = fenParts.join(' ') | |
| const whiteBoard = new Chess(whiteTurnFen) | |
| whiteMobility = whiteBoard.moves().length | |
| } catch { | |
| whiteMobility = 0 | |
| } | |
| } | |
| return { whiteMobility, blackMobility } | |
| } | |
| public static evaluateForColor(board: Chess, color: Color): number { | |
| const evaluation = this.evaluatePosition(board) | |
| return color === 'w' ? evaluation : -evaluation | |
| } | |
| public static getInitiative(board: Chess, color: Color): number { | |
| const evaluation = this.evaluateForColor(board, color) | |
| const gameActivity = this.calculateGameActivity(board) | |
| const baseInitiative = Math.max(0, Math.min(1, (evaluation + 500) / 1000)) | |
| return baseInitiative * gameActivity | |
| } | |
| private static calculateGameActivity(board: Chess): number { | |
| let activity = 0 | |
| // count pieces not on starting squares | |
| const developmentScore = this.calculateDevelopment(board) | |
| activity += Math.min(0.4, developmentScore / 8) | |
| // count tactical opportunities | |
| const tacticalScore = this.calculateTacticalActivity(board) | |
| activity += Math.min(0.3, tacticalScore / 10) | |
| // calc material imbalance | |
| const materialImbalance = Math.abs(this.calculateMaterialBalance(board)) | |
| activity += Math.min(0.2, materialImbalance / 500) | |
| // count king threats | |
| const kingSafety = this.calculateKingThreats(board) | |
| activity += Math.min(0.1, kingSafety / 5) | |
| return Math.min(1, activity) | |
| } | |
| private static calculateDevelopment(board: Chess): number { | |
| let development = 0 | |
| const startingPositions = { | |
| 'b1': 'n', 'g1': 'n', 'c1': 'b', 'f1': 'b', 'd1': 'q', // white | |
| 'b8': 'n', 'g8': 'n', 'c8': 'b', 'f8': 'b', 'd8': 'q' // black | |
| } | |
| for (const [square, expectedPiece] of Object.entries(startingPositions)) { | |
| const piece = board.get(square as Square) | |
| if (!piece || piece.type !== expectedPiece) { | |
| development++ | |
| } | |
| } | |
| return development | |
| } | |
| private static calculateTacticalActivity(board: Chess): number { | |
| let tactical = 0 | |
| const moves = board.moves({ verbose: true }) | |
| for (const move of moves) { | |
| if (move.captured) tactical += 2 // captures | |
| if (move.san.includes('+')) tactical += 1 // checks | |
| if (move.san.includes('#')) tactical += 3 // checkmate | |
| if (move.promotion) tactical += 2 // promotions | |
| } | |
| return tactical | |
| } | |
| private static calculateMaterialBalance(board: Chess): number { | |
| let balance = 0 | |
| const squares = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] | |
| for (const file of squares) { | |
| for (let rank = 1; rank <= 8; rank++) { | |
| const square = (file + rank) as Square | |
| const piece = board.get(square) | |
| if (piece && piece.type !== 'k') { | |
| const value = this.PIECE_VALUES[piece.type] | |
| balance += piece.color === 'w' ? value : -value | |
| } | |
| } | |
| } | |
| return balance | |
| } | |
| private static calculateKingThreats(board: Chess): number { | |
| let threats = 0 | |
| const whiteKing = this.findKing(board, 'w') | |
| const blackKing = this.findKing(board, 'b') | |
| if (whiteKing) threats += this.countAttacksNearSquare(board, whiteKing, 'b') | |
| if (blackKing) threats += this.countAttacksNearSquare(board, blackKing, 'w') | |
| return threats | |
| } | |
| private static countAttacksNearSquare(board: Chess, square: Square, attackingColor: Color): number { | |
| let attacks = 0 | |
| const file = square.charCodeAt(0) - 97 | |
| const rank = parseInt(square[1]) - 1 | |
| for (let f = Math.max(0, file - 1); f <= Math.min(7, file + 1); f++) { | |
| for (let r = Math.max(0, rank - 1); r <= Math.min(7, rank + 1); r++) { | |
| const checkSquare = (String.fromCharCode(97 + f) + (r + 1)) as Square | |
| try { | |
| const moves = board.moves({ square: checkSquare, verbose: true }) | |
| const hasAttack = moves.some((move: any) => { | |
| const piece = board.get(move.from) | |
| return piece && piece.color === attackingColor && | |
| Math.abs(move.to.charCodeAt(0) - square.charCodeAt(0)) <= 1 && | |
| Math.abs(parseInt(move.to[1]) - parseInt(square[1])) <= 1 | |
| }) | |
| if (hasAttack) attacks++ | |
| } catch { | |
| // ignore invalid moves | |
| } | |
| } | |
| } | |
| return attacks | |
| } | |
| } |