github-actions[bot]
deploy prod from 06dbd16a01ddcfe02b2d936c681e3e4eaa9b141f
8e756fd
Raw
History Blame Contribute Delete
6.32 kB
"""Per-component classical evaluation breakdown."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any
from . import bitboards as bb
from . import constants as C
from .board import Board
from .evaluate import (
EvalInfo,
PSQT,
evaluate_bishops,
evaluate_closedness,
evaluate_complexity,
evaluate_kings,
evaluate_kings_pawns,
evaluate_knights,
evaluate_passed,
evaluate_pawns,
evaluate_queens,
evaluate_rooks,
evaluate_scale_factor,
evaluate_space,
evaluate_threats,
init_eval_info,
score_eg,
score_mg,
)
from .types import BLACK, WHITE, make_score
@dataclass
class ComponentScore:
"""Middlegame / endgame contribution in centipawns (White − Black)."""
mg: int = 0
eg: int = 0
@classmethod
def from_packed(cls, packed: int) -> ComponentScore:
return cls(score_mg(packed), score_eg(packed))
def packed(self) -> int:
return make_score(self.mg, self.eg)
def __add__(self, other: ComponentScore) -> ComponentScore:
return ComponentScore(self.mg + other.mg, self.eg + other.eg)
def sum_components(*parts: ComponentScore) -> ComponentScore:
"""Sum MG/EG separately (correct way to combine tapered terms)."""
mg = sum(p.mg for p in parts)
eg = sum(p.eg for p in parts)
return ComponentScore(mg, eg)
@dataclass
class EvaluationBreakdown:
"""Full classical eval with intermediate terms (before phase taper)."""
fen: str
side_to_move: str
pawn_king_structure: ComponentScore
psqt_material: ComponentScore
knights: ComponentScore
bishops: ComponentScore
rooks: ComponentScore
queens: ComponentScore
kings: ComponentScore
passed_pawns: ComponentScore
threats: ComponentScore
space: ComponentScore
closedness: ComponentScore
complexity: ComponentScore
subtotal: ComponentScore
phase: int
scale_factor: int
tapered: int
tempo: int
final: int
def to_dict(self) -> dict[str, Any]:
d = asdict(self)
d["side_to_move"] = self.side_to_move
return d
def _diff(white_packed: int, black_packed: int) -> ComponentScore:
return ComponentScore.from_packed(white_packed - black_packed)
def evaluate_board_detailed(
board: Board,
*,
fen: str = "",
) -> EvaluationBreakdown:
"""Evaluate with per-component MG/EG breakdown and final tapered score."""
ei = EvalInfo()
init_eval_info(board, ei)
evaluate_pawns(ei, board, WHITE)
evaluate_pawns(ei, board, BLACK)
evaluate_kings_pawns(ei, board, WHITE)
evaluate_kings_pawns(ei, board, BLACK)
pawn_king = ComponentScore.from_packed(ei.pkeval[WHITE] - ei.pkeval[BLACK])
psqt = ComponentScore.from_packed(board.psqtmat)
knights = _diff(evaluate_knights(ei, board, WHITE), evaluate_knights(ei, board, BLACK))
bishops = _diff(evaluate_bishops(ei, board, WHITE), evaluate_bishops(ei, board, BLACK))
rooks = _diff(evaluate_rooks(ei, board, WHITE), evaluate_rooks(ei, board, BLACK))
queens = _diff(evaluate_queens(ei, board, WHITE), evaluate_queens(ei, board, BLACK))
kings = _diff(evaluate_kings(ei, board, WHITE), evaluate_kings(ei, board, BLACK))
passed = _diff(evaluate_passed(ei, board, WHITE), evaluate_passed(ei, board, BLACK))
threats = _diff(evaluate_threats(ei, board, WHITE), evaluate_threats(ei, board, BLACK))
space = _diff(evaluate_space(ei, board, WHITE), evaluate_space(ei, board, BLACK))
pieces = (
knights
+ bishops
+ rooks
+ queens
+ kings
+ passed
+ threats
+ space
)
closedness = ComponentScore.from_packed(evaluate_closedness(ei, board))
pre_complexity = sum_components(pawn_king, psqt, pieces, closedness)
complexity = ComponentScore.from_packed(
evaluate_complexity(board, pre_complexity.packed())
)
subtotal = pre_complexity + complexity
from .types import BISHOP, KNIGHT, QUEEN, ROOK
factor = evaluate_scale_factor(board, subtotal.packed())
phase = (
4 * bb.popcount(board.pieces[QUEEN])
+ 2 * bb.popcount(board.pieces[ROOK])
+ bb.popcount(board.pieces[KNIGHT] | board.pieces[BISHOP])
)
tapered = (
subtotal.mg * phase
+ subtotal.eg * (24 - phase) * factor // C.SCALE_NORMAL
) // 24
tempo = C.TEMPO
final = tempo + (tapered if board.turn == WHITE else -tapered)
return EvaluationBreakdown(
fen=fen,
side_to_move="white" if board.turn == WHITE else "black",
pawn_king_structure=pawn_king,
psqt_material=psqt,
knights=knights,
bishops=bishops,
rooks=rooks,
queens=queens,
kings=kings,
passed_pawns=passed,
threats=threats,
space=space,
closedness=closedness,
complexity=complexity,
subtotal=subtotal,
phase=phase,
scale_factor=factor,
tapered=tapered,
tempo=tempo,
final=final,
)
def format_breakdown(bd: EvaluationBreakdown) -> str:
"""Human-readable report for tests or CLI."""
def line(name: str, c: ComponentScore) -> str:
return f" {name:22} MG {c.mg:+6d} EG {c.eg:+6d}"
lines = [
f"FEN: {bd.fen}",
f"Side to move: {bd.side_to_move}",
"",
"Components (White - Black, centipawns):",
line("pawn/king structure", bd.pawn_king_structure),
line("psqt + material", bd.psqt_material),
line("knights", bd.knights),
line("bishops", bd.bishops),
line("rooks", bd.rooks),
line("queens", bd.queens),
line("kings", bd.kings),
line("passed pawns", bd.passed_pawns),
line("threats", bd.threats),
line("space", bd.space),
line("closedness", bd.closedness),
line("complexity", bd.complexity),
"",
line("subtotal (pre-taper)", bd.subtotal),
f" {'phase':22} {bd.phase:6d} (0=EG-heavy, 24=MG-heavy)",
f" {'scale_factor':22} {bd.scale_factor:6d} (/128 applied to EG taper)",
f" {'tapered':22} {bd.tapered:+6d}",
f" {'tempo':22} {bd.tempo:+6d}",
f" {'FINAL (White POV)':22} {bd.final:+6d}",
]
return "\n".join(lines)