musical-chess / src /engines /PositionEvaluator.ts
Maximus Powers
final
3568151
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
}
}