megadicing / modelling /turn_model.py
gloygum
Initial megadicing space
35465a0
Raw
History Blame Contribute Delete
18.6 kB
from collections import defaultdict
def split_by_kickoff_step_infos(
events_with_possession,
kickoff_step_infos,
*,
is_known_team_id,
is_kickoff_related_event,
step_stays_in_kickoff_phase,
kickoff_team_id=None,
fallback_team_id=None,
):
"""Split events into kickoff/team-turn sections using kickoff step boundaries."""
if not kickoff_step_infos:
sections = []
events_remaining = list(events_with_possession)
idx = 0
if is_known_team_id(kickoff_team_id):
while idx < len(events_remaining) and events_remaining[idx].get("effective_active_team_id") == kickoff_team_id:
idx += 1
if idx > 0:
sections.append({"phase": "kickoff", "team_id": kickoff_team_id, "events": events_remaining[:idx]})
events_remaining = events_remaining[idx:]
current_team = None
current_events = []
for event in events_remaining:
effective_active = event.get("effective_active_team_id")
if not is_known_team_id(effective_active):
current_events.append(event)
continue
if current_team is None:
current_team = effective_active
elif effective_active != current_team:
sections.append({"phase": "team_turn", "team_id": current_team, "events": current_events})
current_team = effective_active
current_events = []
current_events.append(event)
if current_events:
sections.append({"phase": "team_turn", "team_id": current_team if is_known_team_id(current_team) else None, "events": current_events})
if not sections:
sections.append({"phase": "team_turn", "team_id": fallback_team_id if is_known_team_id(fallback_team_id) else None, "events": events_with_possession})
return sections
ko_steps = sorted(kickoff_step_infos, key=lambda x: int(x[0]) if str(x[0]).isdigit() else 999999)
sections = []
pending_events = []
current_team = fallback_team_id if is_known_team_id(fallback_team_id) else None
ko_idx = 0
kickoff_open = False
last_kickoff_section_idx = None
def flush_team_section():
nonlocal pending_events
if pending_events:
sections.append(
{
"phase": "team_turn",
"team_id": current_team if is_known_team_id(current_team) else None,
"events": pending_events,
}
)
pending_events = []
def pop_kickoff_tail_events():
"""Detach trailing kickoff-related step suffix from pending team events."""
nonlocal pending_events
if not pending_events:
return []
pending_by_step = defaultdict(list)
for event in pending_events:
pending_by_step[event.get("step_number")].append(event)
pending_steps = sorted(
pending_by_step.keys(),
key=lambda value: int(value) if str(value).isdigit() else 999999,
)
kickoff_tail_steps = []
for step_num in reversed(pending_steps):
step_events = pending_by_step.get(step_num, [])
if step_stays_in_kickoff_phase(step_events):
kickoff_tail_steps.append(step_num)
continue
break
if not kickoff_tail_steps:
return []
kickoff_tail_steps = set(kickoff_tail_steps)
kickoff_tail_events = [event for event in pending_events if event.get("step_number") in kickoff_tail_steps]
pending_events = [event for event in pending_events if event.get("step_number") not in kickoff_tail_steps]
return kickoff_tail_events
def split_step_events_at_kickoff_boundary(step_events):
"""Split kickoff-step rows into pre-kickoff vs kickoff-boundary groups."""
kickoff_boundary_events = []
pre_kickoff_events = []
for event in step_events or []:
if str(event.get("roll_category") or "") == "touchdown":
pre_kickoff_events.append(event)
continue
if is_kickoff_related_event(event):
kickoff_boundary_events.append(event)
else:
pre_kickoff_events.append(event)
return pre_kickoff_events, kickoff_boundary_events
events_by_step = defaultdict(list)
for event in events_with_possession:
events_by_step[event.get("step_number")].append(event)
step_numbers = sorted(
events_by_step.keys(),
key=lambda value: int(value) if str(value).isdigit() else 999999,
)
for step_num_raw in step_numbers:
step_events = events_by_step[step_num_raw]
try:
step_num = int(step_num_raw)
except (TypeError, ValueError):
step_num = 999999
next_ko_step_num = None
next_ko_team_id = None
if ko_idx < len(ko_steps):
next_ko_step_raw, next_ko_team_id = ko_steps[ko_idx]
try:
next_ko_step_num = int(next_ko_step_raw)
except (TypeError, ValueError):
next_ko_step_num = 999999
while ko_idx < len(ko_steps):
ko_step_num_raw, ko_team_id = ko_steps[ko_idx]
try:
ko_step_num = int(ko_step_num_raw)
except (TypeError, ValueError):
ko_step_num = 999999
if step_num > ko_step_num:
kickoff_tail_events = pop_kickoff_tail_events()
flush_team_section()
sections.append({"phase": "kickoff", "team_id": ko_team_id, "events": kickoff_tail_events, "kickoff_step_number": ko_step_num})
last_kickoff_section_idx = len(sections) - 1
kickoff_open = True
receiver = "0" if ko_team_id == "1" else ("1" if ko_team_id == "0" else None)
current_team = receiver if is_known_team_id(receiver) else None
ko_idx += 1
else:
break
if (
not kickoff_open
and ko_idx < len(ko_steps)
and next_ko_step_num is not None
and step_num < next_ko_step_num
and any(is_kickoff_related_event(event) for event in step_events)
):
pre_kickoff_events, kickoff_boundary_events = split_step_events_at_kickoff_boundary(step_events)
if pre_kickoff_events:
pending_events.extend(pre_kickoff_events)
kickoff_tail_events = pop_kickoff_tail_events()
flush_team_section()
kickoff_events = kickoff_tail_events + (kickoff_boundary_events if kickoff_boundary_events else step_events)
sections.append({"phase": "kickoff", "team_id": next_ko_team_id, "events": kickoff_events, "kickoff_step_number": next_ko_step_num})
last_kickoff_section_idx = len(sections) - 1
kickoff_open = True
receiver = "0" if next_ko_team_id == "1" else ("1" if next_ko_team_id == "0" else None)
current_team = receiver if is_known_team_id(receiver) else None
continue
if ko_idx < len(ko_steps):
ko_step_num_raw, ko_team_id = ko_steps[ko_idx]
try:
ko_step_num = int(ko_step_num_raw)
except (TypeError, ValueError):
ko_step_num = 999999
if step_num == ko_step_num:
if kickoff_open:
if isinstance(last_kickoff_section_idx, int):
sections[last_kickoff_section_idx]["events"].extend(step_events)
else:
sections.append({"phase": "kickoff", "team_id": ko_team_id, "events": step_events})
last_kickoff_section_idx = len(sections) - 1
ko_idx += 1
continue
kickoff_tail_events = pop_kickoff_tail_events()
flush_team_section()
kickoff_events = kickoff_tail_events + step_events
sections.append({"phase": "kickoff", "team_id": ko_team_id, "events": kickoff_events, "kickoff_step_number": ko_step_num})
last_kickoff_section_idx = len(sections) - 1
kickoff_open = True
receiver = "0" if ko_team_id == "1" else ("1" if ko_team_id == "0" else None)
current_team = receiver if is_known_team_id(receiver) else None
ko_idx += 1
continue
if kickoff_open and step_stays_in_kickoff_phase(step_events):
if isinstance(last_kickoff_section_idx, int):
sections[last_kickoff_section_idx]["events"].extend(step_events)
else:
sections.append({"phase": "kickoff", "team_id": None, "events": step_events})
last_kickoff_section_idx = len(sections) - 1
continue
kickoff_open = False
if any(str(event.get("roll_category") or "") == "armour" for event in step_events):
effective_active = None
for event in step_events:
active = event.get("effective_active_team_id")
if is_known_team_id(active):
effective_active = active
break
if is_known_team_id(effective_active) and is_known_team_id(current_team) and effective_active != current_team:
flush_team_section()
current_team = effective_active
pending_events.extend(step_events)
continue
effective_active = None
for event in step_events:
active = event.get("effective_active_team_id")
if is_known_team_id(active):
effective_active = active
break
if current_team is None and is_known_team_id(effective_active):
current_team = effective_active
elif is_known_team_id(effective_active) and is_known_team_id(current_team) and effective_active != current_team:
flush_team_section()
current_team = effective_active
pending_events.extend(step_events)
while ko_idx < len(ko_steps):
ko_step_num_raw, ko_team_id = ko_steps[ko_idx]
try:
ko_step_num = int(ko_step_num_raw)
except (TypeError, ValueError):
ko_step_num = None
flush_team_section()
sections.append({"phase": "kickoff", "team_id": ko_team_id, "events": [], "kickoff_step_number": ko_step_num})
receiver = "0" if ko_team_id == "1" else ("1" if ko_team_id == "0" else None)
current_team = receiver if is_known_team_id(receiver) else None
ko_idx += 1
flush_team_section()
if not sections:
sections.append({"phase": "team_turn", "team_id": None, "events": events_with_possession})
return sections
def step_anchor_fingerprint(step_events, *, normalize_team_id, max_rows=4):
"""Create a stable signature for a step, tolerant to nearby row insertions."""
if not step_events:
return []
rows = []
for event in sorted(
step_events,
key=lambda e: (
str(e.get("roll_category")),
str(e.get("payload_type")),
str(e.get("result_name")),
),
):
rows.append(
{
"cat": str(event.get("roll_category") or "-"),
"payload": str(event.get("payload_type") or "-"),
"result": str(event.get("result_name") or "-"),
"roll_type": str(event.get("roll_type") or "-"),
"team": normalize_team_id(event.get("team_id")) or "-",
}
)
return rows[:max_rows]
def snapshot_turn_anchors(by_step, anchor_specs, *, fingerprint_builder):
"""Attach step fingerprints to anchor specs."""
result = []
for spec in anchor_specs or []:
if not isinstance(spec, dict):
continue
anchor = dict(spec)
step_num = anchor.get("step")
if isinstance(step_num, int):
anchor["fingerprint"] = fingerprint_builder(by_step.get(step_num, []))
result.append(anchor)
return result
def check_turn_anchors(
step_lookup,
event_rows,
by_step,
anchors,
*,
normalize_team_id,
event_matches_filter,
fingerprint_builder,
drift_window=20,
):
"""Validate anchors and track nearby moves within a drift window."""
known_steps = sorted(by_step.keys())
reports = []
for anchor in anchors or []:
if not isinstance(anchor, dict):
continue
name = str(anchor.get("name") or f"step-{anchor.get('step')}")
expected = {
"turn": str(anchor.get("expected_turn")) if anchor.get("expected_turn") is not None else None,
"phase": anchor.get("expected_phase"),
"team_id": normalize_team_id(anchor.get("expected_team")),
}
step_num = anchor.get("step")
fingerprint = anchor.get("fingerprint") or []
event_filter = anchor.get("event_filter")
def owner_matches(owner):
return (
(expected["turn"] is None or owner.get("turn") == expected["turn"])
and (expected["phase"] is None or owner.get("phase") == expected["phase"])
and (expected["team_id"] is None or owner.get("team_id") == expected["team_id"])
)
if isinstance(step_num, int) and isinstance(event_filter, dict):
exact_matches = [
row
for row in event_rows
if row.get("step") == step_num and event_matches_filter(row, event_filter)
]
exact_owner_match = next((row for row in exact_matches if owner_matches(row)), None)
if exact_owner_match is not None:
reports.append(
{
"name": name,
"status": "pass",
"anchor_step": step_num,
"actual_step": step_num,
"expected": expected,
"actual": {
"turn": exact_owner_match.get("turn"),
"phase": exact_owner_match.get("phase"),
"team_id": exact_owner_match.get("team_id"),
},
}
)
continue
moved_to = None
lo = step_num - int(drift_window)
hi = step_num + int(drift_window)
for candidate in known_steps:
if candidate < lo or candidate > hi:
continue
candidate_rows = [
row
for row in event_rows
if row.get("step") == candidate and event_matches_filter(row, event_filter)
]
if not candidate_rows:
continue
if any(owner_matches(row) for row in candidate_rows):
moved_to = candidate
break
if moved_to is not None:
moved_row = next(
row
for row in event_rows
if row.get("step") == moved_to and event_matches_filter(row, event_filter) and owner_matches(row)
)
reports.append(
{
"name": name,
"status": "moved",
"anchor_step": step_num,
"actual_step": moved_to,
"expected": expected,
"actual": {
"turn": moved_row.get("turn"),
"phase": moved_row.get("phase"),
"team_id": moved_row.get("team_id"),
},
}
)
else:
actual_owner = None
if exact_matches:
row = exact_matches[0]
actual_owner = {
"turn": row.get("turn"),
"phase": row.get("phase"),
"team_id": row.get("team_id"),
}
reports.append(
{
"name": name,
"status": "fail",
"anchor_step": step_num,
"actual_step": step_num,
"expected": expected,
"actual": actual_owner,
}
)
continue
actual = step_lookup.get(step_num) if isinstance(step_num, int) else None
if actual is not None and owner_matches(actual):
reports.append(
{
"name": name,
"status": "pass",
"anchor_step": step_num,
"actual_step": step_num,
"expected": expected,
"actual": actual,
}
)
continue
moved_to = None
if isinstance(step_num, int) and fingerprint:
lo = step_num - int(drift_window)
hi = step_num + int(drift_window)
for candidate in known_steps:
if candidate < lo or candidate > hi:
continue
candidate_owner = step_lookup.get(candidate)
if not candidate_owner or not owner_matches(candidate_owner):
continue
candidate_fingerprint = fingerprint_builder(by_step.get(candidate, []))
if candidate_fingerprint == fingerprint:
moved_to = candidate
break
if moved_to is not None:
reports.append(
{
"name": name,
"status": "moved",
"anchor_step": step_num,
"actual_step": moved_to,
"expected": expected,
"actual": step_lookup.get(moved_to),
}
)
else:
reports.append(
{
"name": name,
"status": "fail",
"anchor_step": step_num,
"actual_step": step_num,
"expected": expected,
"actual": actual,
}
)
return reports