""" Close call / event detector for referee system. Watches the trajectory model and triggers VAR when ball lands near a line. """ import time from typing import Optional, Tuple, List, Dict from .trajectory import TrajectoryModel # Court line definitions in meters (X=along court, Y=across court) # Full court: 13.4 x 6.1 COURT_LENGTH = 13.4 COURT_WIDTH = 6.1 HALF = COURT_LENGTH / 2 # 6.7 (net / center line) NVZ = 2.13 # non-volley zone (kitchen) depth from net COURT_LINES = { 'baseline_left': {'type': 'horizontal', 'x': 0, 'y_start': 0, 'y_end': COURT_WIDTH}, 'baseline_right': {'type': 'horizontal', 'x': COURT_LENGTH, 'y_start': 0, 'y_end': COURT_WIDTH}, 'sideline_near': {'type': 'vertical', 'y': 0, 'x_start': 0, 'x_end': COURT_LENGTH}, 'sideline_far': {'type': 'vertical', 'y': COURT_WIDTH, 'x_start': 0, 'x_end': COURT_LENGTH}, 'centerline': {'type': 'horizontal', 'x': HALF, 'y_start': 0, 'y_end': COURT_WIDTH}, 'kitchen_left': {'type': 'horizontal', 'x': HALF - NVZ, 'y_start': 0, 'y_end': COURT_WIDTH}, 'kitchen_right': {'type': 'horizontal', 'x': HALF + NVZ, 'y_start': 0, 'y_end': COURT_WIDTH}, 'center_service_left': {'type': 'vertical', 'y': COURT_WIDTH / 2, 'x_start': 0, 'x_end': HALF - NVZ}, 'center_service_right': {'type': 'vertical', 'y': COURT_WIDTH / 2, 'x_start': HALF + NVZ, 'x_end': COURT_LENGTH}, } class EventDetector: """Detects close calls by monitoring ball trajectory near court lines.""" def __init__(self, trigger_distance_m=0.3, cooldown_seconds=5.0): self.trigger_distance = trigger_distance_m self.cooldown = cooldown_seconds self.last_trigger_time = 0.0 self.events: List[Dict] = [] def check(self, trajectory: TrajectoryModel) -> Optional[Dict]: """Check if current trajectory warrants a VAR trigger. Conditions: 1. Ball is descending (vz < 0) or near ground (z < 0.3m) 2. Predicted landing point is within trigger_distance of a line 3. Cooldown has passed since last trigger """ now = time.time() if now - self.last_trigger_time < self.cooldown: return None if not trajectory.points or trajectory.velocity is None: return None last = trajectory.points[-1] vz = trajectory.velocity[2] # Ball must be descending or near ground if vz > 0 and last.z > 0.5: return None # Get predicted landing landing = trajectory.predict_landing() if landing is None: return None land_x, land_y, time_to_land = landing # Check distance to each line closest_line = None closest_dist = float('inf') for name, line in COURT_LINES.items(): dist = self._distance_to_line(land_x, land_y, line) if dist < closest_dist: closest_dist = dist closest_line = name if closest_dist <= self.trigger_distance: self.last_trigger_time = now event = { 'type': 'close_call', 'timestamp': now, 'line': closest_line, 'distance_m': closest_dist, 'landing_x': land_x, 'landing_y': land_y, 'time_to_land': time_to_land, 'ball_z': last.z, 'ball_speed': trajectory.get_speed(), } self.events.append(event) return event return None def _distance_to_line(self, x: float, y: float, line: Dict) -> float: if line['type'] == 'horizontal': lx = line['x'] y_start, y_end = line['y_start'], line['y_end'] if y_start <= y <= y_end: return abs(x - lx) return float('inf') else: # vertical ly = line['y'] x_start, x_end = line['x_start'], line['x_end'] if x_start <= x <= x_end: return abs(y - ly) return float('inf') def is_in_bounds(self, x: float, y: float) -> bool: return 0 <= x <= COURT_LENGTH and 0 <= y <= COURT_WIDTH