| """ |
| 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: |
| |
| 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"] |
| |
| |
| _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) |
| |
| 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: |
| |
| candidates = sorted(roles, key=lambda r: abs((r["overs_active"][0] + r["overs_active"][1]) / 2 - over)) |
| |
| 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) |
|
|
| |
| 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"] |
| |
| 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"] |
|
|