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),
]