HAWK-ONE / rules.py
ageraustine's picture
init
ab93d31 verified
Raw History Blame Contribute Delete
7.45 kB
"""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),
]