Spaces:
Running on Zero
Running on Zero
File size: 7,451 Bytes
ab93d31 | 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 | """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),
]
|