Spaces:
Running
Running
| 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 | |