"""HAWK ONE rules layer. One simulation core, two RuleProfiles. The engine never asks "is this NFL?"; it asks the active profile (profile.catch_feet_required, profile.dpi_mode, ...). Adding a level (e.g. high school) = adding one more profile. Every parameter marked verify=True in RULE_ROWS must be confirmed against the current-season rulebook by the coach before being treated as authoritative. """ from __future__ import annotations from dataclasses import dataclass from typing import Optional FIELD_LENGTH_YD = 120.0 # incl. two 10-yd end zones FIELD_WIDTH_YD = 160 / 3 # 53 1/3 yd, same at both levels @dataclass(frozen=True) class RuleProfile: key: str label: str hash_width_ft: float # distance between the two hash lines catch_feet_required: int # feet in bounds for a catch (or other body part) down_requires_contact: bool # runner must be touched to be down ineligible_downfield_yd: int # yards OL may be downfield before the pass dpi_mode: str # "spot" (spot foul) | "flat15" dpi_flat_yd: int = 15 dpi_auto_first_down: bool = True two_minute_warning: bool = False ot_summary: str = "" ot_two_point_from_period: Optional[int] = None kickoff_summary: str = "" targeting_summary: str = "" clock_summary: str = "" @property def hash_half_yd(self) -> float: return self.hash_width_ft / 3 / 2 @property def hash_width_yd(self) -> float: return self.hash_width_ft / 3 NFL = RuleProfile( key="NFL", label="NFL", hash_width_ft=18.5, catch_feet_required=2, down_requires_contact=True, ineligible_downfield_yd=1, dpi_mode="spot", two_minute_warning=True, ot_summary="Timed period (10 min regular season); both teams get a possession; sudden death after.", kickoff_summary="Dynamic kickoff: coverage team lined at receiving team's 40, receiving team in a setup zone; " "touchback spot depends on where the kick lands.", targeting_summary="No NCAA-style targeting rule; lowering-the-head / unnecessary-roughness fouls, " "ejection by officials' or league review.", clock_summary="Two-minute warning. Stoppage/restart table (out of bounds, incomplete, 10-sec runoff) -> ClockPolicy.", ) NCAA = RuleProfile( key="NCAA", label="College (NCAA)", hash_width_ft=40.0, catch_feet_required=1, down_requires_contact=False, ineligible_downfield_yd=3, dpi_mode="flat15", two_minute_warning=False, ot_summary="Untimed alternating possessions from the 25-yd line; 2-point tries only in later periods.", ot_two_point_from_period=3, kickoff_summary="Kick from own 35; touchback / fair-catch spot is the 25; traditional kickoff formations.", targeting_summary="Targeting: 15 yds + ejection, replay-reviewed; half-game carry-over suspension possible.", clock_summary="No two-minute warning. Different first-down / out-of-bounds restart treatment late in each half -> ClockPolicy.", ) PROFILES = {"NFL": NFL, "NCAA": NCAA} LEVEL_CHOICES = [("NFL", "NFL"), ("College (NCAA)", "NCAA")] def other(p: RuleProfile) -> RuleProfile: return NCAA if p.key == "NFL" else NFL # ---------------------------------------------------------------- rule functions def catch_is_complete(p: RuleProfile, control: bool, feet_in_bounds: int, other_body_part_in_bounds: bool = False) -> tuple[bool, str]: if not control: return False, "No secure control of the ball." if feet_in_bounds >= p.catch_feet_required: return True, f"Control + {feet_in_bounds} foot/feet in bounds (needs {p.catch_feet_required})." if other_body_part_in_bounds: return True, "Control + a body part other than the hands in bounds." return False, f"Needs {p.catch_feet_required} foot/feet in bounds; only {feet_in_bounds}." def runner_is_down(p: RuleProfile, body_down: bool, contacted: bool, slid_or_gave_up: bool = False) -> tuple[bool, str]: if not body_down: return False, "No knee/body part (other than hands/feet) on the ground." if not p.down_requires_contact: return True, "Knee/body down - ball is dead, contact not required." if contacted: return True, "Body down + contacted by a defender." if slid_or_gave_up: return True, "Runner slid / gave himself up." return False, "Down without contact: runner may get up and keep going." def dpi_enforcement(p: RuleProfile, foul_spot_yd: float, yds_to_goal: float) -> dict: """foul_spot_yd = yards downfield of LOS where the foul occurred.""" if p.dpi_mode == "spot": yards = min(foul_spot_yd, max(yds_to_goal - 1, 0)) # in end zone -> 1-yd line note = "Spot foul" + (" (placed at 1-yd line)" if foul_spot_yd >= yds_to_goal else "") else: yards = min(p.dpi_flat_yd, yds_to_goal / 2) # half-the-distance rule note = f"{p.dpi_flat_yd}-yd penalty from previous spot (half-distance to goal applies)" return {"yards": round(yards, 1), "first_down": p.dpi_auto_first_down, "note": note} def ineligible_downfield_ok(p: RuleProfile, yards_beyond_los: float) -> tuple[bool, str]: ok = yards_beyond_los <= p.ineligible_downfield_yd return ok, (f"Within the {p.ineligible_downfield_yd}-yd allowance." if ok else f"Beyond the {p.ineligible_downfield_yd}-yd allowance - ineligible downfield.") def overtime_plan(p: RuleProfile, period: int) -> str: if p.key == "NFL": return f"Period {period}: " + p.ot_summary if p.ot_two_point_from_period and period >= p.ot_two_point_from_period: return f"OT {period}: alternating 2-point tries from the 3-yd line (no drive)." return f"OT {period}: alternating possessions from the opponent's 25-yd line." # ---------------------------------------------------------------- comparison rows # (id, topic, NFL, NCAA, engine hook, status, verify) RULE_ROWS = [ (1, "Catch", "Control + two feet (or other body part) in bounds", "Control + one foot (or other body part) in bounds", "catch_is_complete()", "Live in Playground", False), (2, "Down by contact", "Runner must be touched (or slide / give up)", "Down when knee/body touches ground, no contact needed", "runner_is_down()", "Live in Playground", False), (3, "Hash marks", "18 ft 6 in apart (6.17 yd)", "40 ft apart (13.33 yd)", "profile.hash_half_yd -> field, spacing, alignments", "Live on field", False), (4, "Game clock", NFL.clock_summary, NCAA.clock_summary, "ClockPolicy (stop/start table per event)", "Phase 2", True), (5, "Pass interference (D)", "Spot foul (1-yd line if in end zone)", "15 yds (half-distance to goal); confirm flat vs capped", "dpi_enforcement()", "Live in Playground", True), (6, "Ineligible OL downfield", "1 yd past LOS before the pass", "3 yds past LOS before the pass", "ineligible_downfield_ok()", "Live on field + Playground", False), (7, "Overtime", NFL.ot_summary, NCAA.ot_summary, "overtime_plan() -> OTStateMachine", "Playground; state machine Phase 2", True), (8, "Kickoffs", NFL.kickoff_summary, NCAA.kickoff_summary, "KickoffFormation per profile", "Phase 2", True), (9, "Targeting / safety", NFL.targeting_summary, NCAA.targeting_summary, "PenaltyTable + EjectionPolicy", "Phase 2", True), ]