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 | |
} | |
} |