File size: 4,871 Bytes
d4de43f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
format_mapper.py — select the closest format rules (T5 / T20 / ODI) given
max_overs, and expose batter/bowler role helpers used by the environment and
heuristic opponent.

Usage:
    from server.format_mapper import get_format_rules, closest_batter_role, closest_bowler_role

    rules = get_format_rules(max_overs=7)   # returns T5 rules
    rules = get_format_rules(max_overs=20)  # returns T20 rules
    rules = get_format_rules(max_overs=50)  # returns ODI rules
"""

from __future__ import annotations

import json
import os
from functools import lru_cache
from typing import Any

_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "format_rules.json")


@lru_cache(maxsize=1)
def _load_rules() -> list[dict]:
    with open(_DATA_PATH) as f:
        return json.load(f)["formats"]


def get_format_rules(max_overs: int | None) -> dict:
    """Return the format rule block whose 'overs' is closest to max_overs."""
    formats = _load_rules()
    if max_overs is None:
        # Default: T20
        return next(f for f in formats if f["name"] == "T20")
    return min(formats, key=lambda f: abs(f["overs"] - max_overs))


def get_phase(over: int, max_overs: int | None) -> str:
    """Map an over number to the canonical 3-phase label for the given format."""
    rules = get_format_rules(max_overs)
    phases = rules["phases"]
    # Try each phase in priority order; last one with start<=over wins
    # Merge multi-phase ODI into 3-phase labels
    _canonical = {
        "powerplay":    "powerplay",
        "middle":       "middle",
        "middle_early": "middle",
        "middle_late":  "middle",
        "death":        "death",
    }
    matched = "middle"
    for phase_name, bounds in phases.items():
        start, end = bounds
        if start <= over <= end:
            matched = _canonical.get(phase_name, phase_name)
    return matched


def get_phase_aggression(over: int, max_overs: int | None) -> float:
    """Return the target aggression level for a given over and format."""
    rules = get_format_rules(max_overs)
    phase_agg = rules["batting"]["phase_aggression"]
    phase = get_phase(over, max_overs)
    # Try exact match, then fall back to canonical
    return phase_agg.get(phase, phase_agg.get("middle", 0.5))


def get_shot_weights(over: int, max_overs: int | None) -> dict[str, float]:
    """Return shot-intent probability weights for the current phase."""
    rules = get_format_rules(max_overs)
    weights_map = rules["batting"]["shot_weights"]
    phase = get_phase(over, max_overs)
    return weights_map.get(phase, weights_map.get("middle", {}))


def closest_batter_role(over: int, max_overs: int | None, wickets: int = 0) -> dict[str, Any]:
    """Pick the batter role whose active window contains the current over,
    preferring lower-aggression roles when wickets are scarce."""
    rules = get_format_rules(max_overs)
    roles = rules["batting"]["batter_roles"]
    candidates = [r for r in roles if r["overs_active"][0] <= over <= r["overs_active"][1]]
    if not candidates:
        # Closest by midpoint distance
        candidates = sorted(roles, key=lambda r: abs((r["overs_active"][0] + r["overs_active"][1]) / 2 - over))
    # Under wicket pressure, prefer anchors; otherwise pick highest aggression available
    if wickets >= 6:
        candidates.sort(key=lambda r: r["aggression"])
    else:
        candidates.sort(key=lambda r: -r["aggression"])
    return candidates[0]


def closest_bowler_role(over: int, max_overs: int | None, bowler_type: str = "pace") -> dict[str, Any]:
    """Pick the bowler role most appropriate for the current over and format."""
    rules = get_format_rules(max_overs)
    roles = rules["bowling"]["bowler_roles"]
    phase = get_phase(over, max_overs)

    # Prefer roles whose preferred_phases contains the current phase AND match bowler_type
    def score(role: dict) -> tuple[int, int]:
        phase_match = 1 if phase in role.get("preferred_phases", []) else 0
        type_match  = 1 if role.get("type") == bowler_type else 0
        return (-phase_match, -type_match)

    return min(roles, key=score)


def get_bowling_strategy(over: int, max_overs: int | None) -> dict[str, Any]:
    """Return the canonical bowling plan for the current over and format."""
    rules = get_format_rules(max_overs)
    strategies = rules["bowling"]["phase_strategy"]
    # Build fine-grained phase key (e.g. "middle_early" for ODI)
    phases = rules["phases"]
    matched_key = "middle"
    for phase_name, bounds in phases.items():
        start, end = bounds
        if start <= over <= end:
            matched_key = phase_name
            break
    return strategies.get(matched_key, strategies.get("middle", {}))


def max_spell_overs(max_overs: int | None) -> int:
    rules = get_format_rules(max_overs)
    return rules["bowling"]["max_spell_overs"]