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