File size: 3,030 Bytes
a1b0768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""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",
)