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