Spaces:
Sleeping
Sleeping
| """Map raw features to 0–100 per-side analysis scores.""" | |
| from __future__ import annotations | |
| from .features import RawPositionFeatures, SideRawFeatures, _STARTING_MATERIAL_INDEX | |
| def _clamp100(x: float) -> float: | |
| return max(0.0, min(100.0, x)) | |
| def _linear(value: float, max_value: float) -> float: | |
| if max_value <= 0: | |
| return 0.0 | |
| return _clamp100(100.0 * value / max_value) | |
| def _quality_from_penalty(penalty: float, max_penalty: float) -> float: | |
| if max_penalty <= 0: | |
| return 100.0 | |
| return _clamp100(100.0 - 100.0 * penalty / max_penalty) | |
| def normalize_side(raw: SideRawFeatures) -> dict[str, float]: | |
| """Convert one side's raw features to named 0–100 scores (higher = better where noted).""" | |
| closedness = _linear(raw.closedness_index, 8.0) | |
| openness = _linear(raw.open_files, 8.0) | |
| pawn_penalty = ( | |
| raw.isolated_pawns * 14.0 | |
| + raw.backward_pawns * 12.0 | |
| + raw.stacked_pawns * 8.0 | |
| ) | |
| pawn_bonus = ( | |
| raw.connected_pawns * 4.0 | |
| + raw.candidate_passers * 10.0 | |
| + raw.passed_pawns * 18.0 | |
| ) | |
| pawn_structure = _clamp100(50.0 + pawn_bonus - pawn_penalty) | |
| king_penalty = ( | |
| raw.king_attacks * 4.0 | |
| + raw.king_attackers * 12.0 | |
| + raw.king_weak_squares * 10.0 | |
| ) | |
| king_bonus = raw.king_defenders * 6.0 + raw.shelter_mg / 500_000.0 | |
| king_safety = _quality_from_penalty(max(0.0, king_penalty - king_bonus), 80.0) | |
| mobility = ( | |
| raw.knight_mobility | |
| + raw.bishop_mobility | |
| + raw.rook_mobility | |
| + raw.queen_mobility | |
| ) | |
| piece_activity = _linear(mobility + raw.outposts * 3.0, 55.0) | |
| passed_pawn_pressure = _linear(raw.passed_pressure, 40.0) | |
| space = _linear(raw.center_control * 4.0 + raw.space_restricted, 24.0) | |
| tactical_tension = _linear(raw.threat_units + raw.pawn_push_threats, 18.0) | |
| tactical_complexity = _linear(raw.complexity_units, 20.0) | |
| material = _linear(raw.material_index, float(_STARTING_MATERIAL_INDEX)) | |
| return { | |
| "closedness": closedness, | |
| "openness": openness, | |
| "pawn_structure": pawn_structure, | |
| "king_safety": king_safety, | |
| "piece_activity": piece_activity, | |
| "passed_pawn_pressure": passed_pawn_pressure, | |
| "space": space, | |
| "tactical_tension": tactical_tension, | |
| "tactical_complexity": tactical_complexity, | |
| "material": material, | |
| } | |
| def normalize_position(raw: RawPositionFeatures) -> dict: | |
| """Full position analysis dict with per-side 0–100 components.""" | |
| white = normalize_side(raw.white) | |
| black = normalize_side(raw.black) | |
| phase_middlegame = _linear(raw.phase_index, 24.0) | |
| return { | |
| "phase_middlegame": phase_middlegame, | |
| "white": white, | |
| "black": black, | |
| } | |
| COMPONENT_KEYS = ( | |
| "closedness", | |
| "openness", | |
| "pawn_structure", | |
| "king_safety", | |
| "piece_activity", | |
| "passed_pawn_pressure", | |
| "space", | |
| "tactical_tension", | |
| "tactical_complexity", | |
| "material", | |
| ) | |