github-actions[bot]
deploy prod from 06dbd16a01ddcfe02b2d936c681e3e4eaa9b141f
8e756fd
Raw
History Blame Contribute Delete
3.24 kB
"""Human-interpretable per-side position analysis (0–100, phase-independent)."""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from typing import Any
from .board import Board, board_from_fen
from .evaluate import PSQT
from .types import WHITE
from .features import RawPositionFeatures, extract_raw_features
from .normalize import COMPONENT_KEYS, normalize_position, normalize_side
@dataclass
class SideAnalysis:
closedness: float
openness: float
pawn_structure: float
king_safety: float
piece_activity: float
passed_pawn_pressure: float
space: float
tactical_tension: float
tactical_complexity: float
material: float
@classmethod
def from_dict(cls, d: dict[str, float]) -> SideAnalysis:
return cls(**{k: d[k] for k in COMPONENT_KEYS})
def to_dict(self) -> dict[str, float]:
return {k: getattr(self, k) for k in COMPONENT_KEYS}
@dataclass
class PositionAnalysis:
fen: str
side_to_move: str
phase_middlegame: float
white: SideAnalysis
black: SideAnalysis
raw: RawPositionFeatures | None = None
def to_dict(self, *, include_raw: bool = False) -> dict[str, Any]:
out: dict[str, Any] = {
"fen": self.fen,
"side_to_move": self.side_to_move,
"phase_middlegame": self.phase_middlegame,
"white": self.white.to_dict(),
"black": self.black.to_dict(),
}
if include_raw and self.raw is not None:
out["raw"] = {
"white": asdict(self.raw.white),
"black": asdict(self.raw.black),
"phase_index": self.raw.phase_index,
}
return out
def analyze_board(board: Board, *, fen: str = "") -> PositionAnalysis:
raw = extract_raw_features(board)
normed = normalize_position(raw)
return PositionAnalysis(
fen=fen,
side_to_move="white" if board.turn == WHITE else "black",
phase_middlegame=normed["phase_middlegame"],
white=SideAnalysis.from_dict(normed["white"]),
black=SideAnalysis.from_dict(normed["black"]),
raw=raw,
)
def analyze_fen(fen: str) -> PositionAnalysis:
board = board_from_fen(fen, PSQT)
return analyze_board(board, fen=fen)
def format_analysis(pa: PositionAnalysis) -> str:
"""Human-readable per-side 0–100 report."""
def side_block(label: str, s: SideAnalysis) -> list[str]:
lines = [f"--- {label} ---"]
for key in COMPONENT_KEYS:
title = key.replace("_", " ").title()
val = getattr(s, key)
lines.append(f" {title:24} {val:5.1f}")
return lines
lines = [
f"FEN: {pa.fen}",
f"Side to move: {pa.side_to_move}",
f"Phase (middlegame character): {pa.phase_middlegame:.1f} / 100",
"",
*side_block("White", pa.white),
"",
*side_block("Black", pa.black),
"",
"Scores are 0–100 per side, phase-independent (not search eval).",
]
return "\n".join(lines)
def format_analysis_json(pa: PositionAnalysis, *, include_raw: bool = False) -> str:
return json.dumps(pa.to_dict(include_raw=include_raw), indent=2)