Skip to content

Commit fd1a6c1

Browse files
Require an angular turn for redirect touches, not speed alone.
Speed-only redirects let teammate fly-bys (e.g. #15#8 on 08fd33) complete a pass before the real interceptor settles; require a deflection angle so the turnover attributes to #3. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 48aa843 commit fd1a6c1

2 files changed

Lines changed: 74 additions & 12 deletions

File tree

sports/common/possession.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,23 @@ def _ball_touch_path_metrics(
562562
return angle_deg, speed_ratio
563563

564564

565+
def _redirect_signature(
566+
angle_deg: float,
567+
speed_ratio: float,
568+
*,
569+
min_angle_deg: float,
570+
min_speed_ratio: float,
571+
) -> bool:
572+
"""True when path change looks like a kick, not a speed blip on a fly-by.
573+
574+
Speed alone is not enough: a ball skimming a teammate mid-pass can briefly
575+
change measured speed without a real deflection. Require an angular turn.
576+
"""
577+
return min_angle_deg <= angle_deg <= 135.0 or (
578+
speed_ratio >= min_speed_ratio and angle_deg >= min_angle_deg
579+
)
580+
581+
565582
def ball_redirected_at_touch(
566583
frames_by_idx: dict[int, sv.Detections],
567584
touch_frame: int,
@@ -583,9 +600,11 @@ def ball_redirected_at_touch(
583600
if metrics is None:
584601
return False
585602
angle_deg, speed_ratio = metrics
586-
return (
587-
speed_ratio >= min_speed_ratio
588-
or (min_angle_deg <= angle_deg <= 135.0)
603+
return _redirect_signature(
604+
angle_deg,
605+
speed_ratio,
606+
min_angle_deg=min_angle_deg,
607+
min_speed_ratio=min_speed_ratio,
589608
)
590609

591610

@@ -640,16 +659,15 @@ def redirect_overrides_transit_flyby(
640659
if (
641660
release_gap_frames is not None
642661
and release_gap_frames >= config.gravity_flyby_min_release_gap_frames
662+
and speed_ratio > 4.0
643663
):
644-
if speed_ratio > 4.0:
645-
return False
646-
return (
647-
min_angle <= angle_deg <= 135.0
648-
or (speed_ratio >= min_ratio and angle_deg >= min_angle)
649-
)
650-
return (
651-
speed_ratio >= min_ratio
652-
or (min_angle <= angle_deg <= 135.0)
664+
# Teleport / sparse-detection speed spike during a long flight.
665+
return False
666+
return _redirect_signature(
667+
angle_deg,
668+
speed_ratio,
669+
min_angle_deg=min_angle,
670+
min_speed_ratio=min_ratio,
653671
)
654672

655673

tests/test_flyby_turnover.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,50 @@ def test_flyby_teammate_does_not_steal_opponent_turnover():
8383
)
8484

8585

86+
def test_speed_only_path_blip_is_not_a_redirect():
87+
"""A speed change without an angular turn must not count as a kick/redirect.
88+
89+
Regression for false #15→#8 on 08fd33: teammate skim had speed_ratio≈2.4 and
90+
angle≈11°, which the old speed-only rule treated as a redirect and completed
91+
a pass before opponent #3 secured the ball.
92+
"""
93+
from sports.common.possession import (
94+
ball_redirected_at_touch,
95+
redirect_overrides_transit_flyby,
96+
TouchValidationConfig,
97+
)
98+
99+
# Straight-ish flight with a mid-path deceleration (speed blip, tiny angle).
100+
ball_path = (
101+
[(100.0 + 25 * i, 200.0) for i in range(6)]
102+
+ [(250.0, 202.0), (255.0, 203.0), (260.0, 204.0)] # slower, ~tiny bend
103+
+ [(280.0 + 25 * i, 205.0) for i in range(6)]
104+
)
105+
frames_by_idx = {
106+
i: _frame(players=[(8, 0, (255.0, 220.0))], ball=ball)
107+
for i, ball in enumerate(ball_path, 1)
108+
}
109+
touch = 8 # around the deceleration
110+
assert ball_redirected_at_touch(
111+
frames_by_idx,
112+
touch,
113+
lookback=5,
114+
lookahead=5,
115+
min_angle_deg=28.0,
116+
min_speed_ratio=1.35,
117+
min_segment_px=10.0,
118+
) is False
119+
assert (
120+
redirect_overrides_transit_flyby(
121+
frames_by_idx,
122+
touch,
123+
config=TouchValidationConfig(),
124+
release_gap_frames=None,
125+
)
126+
is False
127+
)
128+
129+
86130
def test_fast_flyby_still_turnovers_to_interceptor():
87131
"""Default transit speed: ball flying past #6 still yields #15→#3 turnover."""
88132
ball_path = (

0 commit comments

Comments
 (0)