File size: 7,439 Bytes
cb4ffbd 8d3b15c cb4ffbd 8d3b15c cb4ffbd 8d3b15c cb4ffbd 8d3b15c cb4ffbd | 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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | """Field layouts and shot/delivery normalization for the cricket simulator."""
from __future__ import annotations
from dataclasses import dataclass
FIELD_ZONES = {
"slips",
"gully",
"point",
"cover",
"mid_off",
"straight",
"mid_on",
"midwicket",
"square_leg",
"fine_leg",
"third_man",
"deep_cover",
"deep_midwicket",
"long_off",
"long_on",
"deep_square_leg",
"deep_fine_leg",
"deep_third_man",
}
INNER_ZONES = {
"slips",
"gully",
"point",
"cover",
"mid_off",
"straight",
"mid_on",
"midwicket",
"square_leg",
"fine_leg",
"third_man",
}
DEEP_ZONE_FOR = {
"cover": "deep_cover",
"point": "deep_cover",
"third_man": "deep_third_man",
"fine_leg": "deep_fine_leg",
"square_leg": "deep_square_leg",
"midwicket": "deep_midwicket",
"mid_on": "long_on",
"mid_off": "long_off",
"straight": "long_off",
}
SHOT_DEFAULT_ZONES = {
"leave": "slips",
"defensive": "straight",
"single": "midwicket",
"rotate": "square_leg",
"boundary": "deep_cover",
"six": "long_on",
}
SHOT_TO_ZONE_HINTS = {
"cover drive": "cover",
"cover": "cover",
"off side": "cover",
"off-side": "cover",
"off": "cover",
"cut": "point",
"point": "point",
"third": "third_man",
"edge": "slips",
"slip": "slips",
"straight": "straight",
"bowler": "straight",
"long off": "long_off",
"long-off": "long_off",
"long on": "long_on",
"long-on": "long_on",
"mid on": "mid_on",
"mid-on": "mid_on",
"leg side": "midwicket",
"leg-side": "midwicket",
"leg": "midwicket",
"midwicket": "midwicket",
"mid wicket": "midwicket",
"square": "square_leg",
"pull": "square_leg",
"fine": "fine_leg",
"glance": "fine_leg",
"sweep": "square_leg",
"boundary": "deep_cover",
"gap": "cover",
"gaps": "cover",
}
LINE_ALIASES = {
"outside off": "outside_off",
"outside_off": "outside_off",
"off": "outside_off",
"corridor": "outside_off",
"stumps": "stumps",
"middle": "stumps",
"middle stump": "stumps",
"off stump": "stumps",
"on pads": "pads",
"pads": "pads",
"leg": "pads",
"leg stump": "pads",
"wide": "wide",
"wide outside off": "wide",
"down leg": "wide",
}
LENGTH_ALIASES = {
"yorker": "yorker",
"full": "full",
"half volley": "full",
"overpitched": "full",
"good length": "good",
"good": "good",
"back of a length": "short",
"short": "short",
"bouncer": "bouncer",
"short ball": "bouncer",
}
VARIATION_ALIASES = {
"stock": "stock",
"normal": "stock",
"seam": "seam",
"swing": "swing",
"inswing": "swing",
"outswing": "swing",
"slower ball": "slower",
"slower": "slower",
"yorker": "yorker",
"bouncer": "bouncer",
"off spin": "off_spin",
"off_spin": "off_spin",
"leg spin": "leg_spin",
"leg_spin": "leg_spin",
"googly": "googly",
"doosra": "googly",
}
OUTFIELD_ZONES = {
"deep_cover", "deep_midwicket", "deep_square_leg",
"deep_fine_leg", "deep_third_man", "long_off", "long_on",
}
POWERPLAY_OUTFIELDER_CAP = 2 # T20: max 2 fielders beyond the 30-yard circle in PP
@dataclass(frozen=True)
class FieldLayout:
setting: str
positions: dict[str, int]
def count(self, zone: str) -> int:
return self.positions.get(zone, 0)
def total_fielders(self) -> int:
return sum(self.positions.values())
def outside_circle(self) -> int:
return sum(c for z, c in self.positions.items() if z in OUTFIELD_ZONES)
def is_legal_in_phase(self, phase: str) -> tuple[bool, str]:
"""Return (legal, reason) given the current match phase."""
if phase == "powerplay" and self.outside_circle() > POWERPLAY_OUTFIELDER_CAP:
return False, (
f"{self.setting} has {self.outside_circle()} fielders outside the 30-yard circle; "
f"powerplay cap is {POWERPLAY_OUTFIELDER_CAP}."
)
return True, ""
def zone_pressure(self, zone: str) -> float:
deep_zone = DEEP_ZONE_FOR.get(zone, zone)
count = self.count(zone) + self.count(deep_zone)
return min(1.0, count / 2.0)
def boundary_rider(self, zone: str) -> bool:
return self.count(DEEP_ZONE_FOR.get(zone, zone)) > 0
def close_catcher(self, zone: str) -> bool:
return zone in {"slips", "gully", "point", "fine_leg", "square_leg"} and self.count(zone) > 0
def describe(self) -> str:
populated = [f"{zone}:{count}" for zone, count in sorted(self.positions.items()) if count]
return f"{self.setting} field ({', '.join(populated)})"
# Each preset has exactly 9 outfielders (cricket: 11 minus bowler + keeper)
FIELD_PRESETS: dict[str, FieldLayout] = {
"Aggressive": FieldLayout(
"Aggressive",
{
"slips": 2,
"gully": 1,
"point": 1,
"cover": 1,
"mid_off": 1,
"mid_on": 1,
"square_leg": 1,
"fine_leg": 1,
},
),
"Balanced": FieldLayout(
"Balanced",
{
"slips": 1,
"point": 1,
"cover": 1,
"mid_off": 1,
"mid_on": 1,
"midwicket": 1,
"square_leg": 1,
"fine_leg": 1,
"deep_cover": 1,
},
),
"Defensive": FieldLayout(
"Defensive",
{
"point": 1,
"mid_off": 1,
"mid_on": 1,
"midwicket": 1,
"deep_cover": 1,
"deep_midwicket": 1,
"long_on": 1,
"long_off": 1,
"deep_fine_leg": 1,
},
),
}
def get_field_layout(setting: str) -> FieldLayout:
return FIELD_PRESETS.get(setting, FIELD_PRESETS["Balanced"])
def normalize_target_area(raw: object, shot_intent: str = "defensive") -> str:
text = str(raw or "").strip().lower().replace("_", " ")
if text in {"", "gap", "gaps"}:
return SHOT_DEFAULT_ZONES.get(shot_intent, "straight")
if text in FIELD_ZONES:
return text
for needle, zone in SHOT_TO_ZONE_HINTS.items():
if needle in text:
return zone
return SHOT_DEFAULT_ZONES.get(shot_intent, "straight")
def infer_trajectory(shot_intent: str, risk: object, raw: object = None) -> str:
text = str(raw or "").lower()
if text in {"ground", "lofted", "aerial", "edge"}:
return text
risk_text = str(risk or "").lower()
if shot_intent == "leave":
return "edge"
if shot_intent == "six" or risk_text == "high":
return "aerial"
if shot_intent == "boundary" and risk_text in {"balanced", "medium"}:
return "lofted"
return "ground"
def normalize_line(raw: object) -> str:
text = str(raw or "outside off").strip().lower().replace("_", " ")
return LINE_ALIASES.get(text, "outside_off")
def normalize_length(raw: object) -> str:
text = str(raw or "good length").strip().lower().replace("_", " ")
return LENGTH_ALIASES.get(text, "good")
def normalize_variation(raw: object, bowler_type: str = "pace") -> str:
text = str(raw or "stock").strip().lower().replace("_", " ")
if text == "stock" and bowler_type == "spin":
return "off_spin"
return VARIATION_ALIASES.get(text, "stock")
|