Spaces:
Sleeping
Sleeping
| """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 | |
| 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 | |
| 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} | |
| 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) | |