"""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)