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"]
|