Skip to main content

subtr_actor/stats/calculators/
ball_carry.rs

1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
4#[ts(export)]
5pub struct BallCarryStats {
6    pub carry_count: u32,
7    pub total_carry_time: f32,
8    pub total_straight_line_distance: f32,
9    pub total_path_distance: f32,
10    pub longest_carry_time: f32,
11    pub furthest_carry_distance: f32,
12    pub fastest_carry_speed: f32,
13    pub carry_speed_sum: f32,
14    pub average_horizontal_gap_sum: f32,
15    pub average_vertical_gap_sum: f32,
16}
17
18impl BallCarryStats {
19    fn pct_count_average(&self, value: f32) -> f32 {
20        if self.carry_count == 0 {
21            0.0
22        } else {
23            value / self.carry_count as f32
24        }
25    }
26
27    pub fn average_carry_time(&self) -> f32 {
28        self.pct_count_average(self.total_carry_time)
29    }
30
31    pub fn average_straight_line_distance(&self) -> f32 {
32        self.pct_count_average(self.total_straight_line_distance)
33    }
34
35    pub fn average_path_distance(&self) -> f32 {
36        self.pct_count_average(self.total_path_distance)
37    }
38
39    pub fn average_carry_speed(&self) -> f32 {
40        self.pct_count_average(self.carry_speed_sum)
41    }
42
43    pub fn average_horizontal_gap(&self) -> f32 {
44        self.pct_count_average(self.average_horizontal_gap_sum)
45    }
46
47    pub fn average_vertical_gap(&self) -> f32 {
48        self.pct_count_average(self.average_vertical_gap_sum)
49    }
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize)]
53pub struct BallCarryEvent {
54    pub player_id: PlayerId,
55    pub is_team_0: bool,
56    pub kind: BallCarryKind,
57    pub start_frame: usize,
58    pub end_frame: usize,
59    pub start_time: f32,
60    pub end_time: f32,
61    pub duration: f32,
62    pub straight_line_distance: f32,
63    pub path_distance: f32,
64    pub average_horizontal_gap: f32,
65    pub average_vertical_gap: f32,
66    pub average_speed: f32,
67    pub touch_count: u32,
68    pub air_touch_count: u32,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub air_dribble_origin: Option<AirDribbleOrigin>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
74#[ts(export)]
75#[serde(rename_all = "snake_case")]
76pub enum BallCarryKind {
77    Carry,
78    AirDribble,
79}
80
81#[derive(Debug, Clone, Default)]
82pub struct BallCarryCalculator {
83    player_stats: HashMap<PlayerId, BallCarryStats>,
84    player_air_dribble_stats: HashMap<PlayerId, AirDribbleStats>,
85    team_zero_stats: BallCarryStats,
86    team_one_stats: BallCarryStats,
87    team_zero_air_dribble_stats: AirDribbleStats,
88    team_one_air_dribble_stats: AirDribbleStats,
89    carry_events: Vec<BallCarryEvent>,
90    processed_control_sequence_count: usize,
91}
92
93impl BallCarryCalculator {
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    pub fn player_stats(&self) -> &HashMap<PlayerId, BallCarryStats> {
99        &self.player_stats
100    }
101
102    pub fn player_air_dribble_stats(&self) -> &HashMap<PlayerId, AirDribbleStats> {
103        &self.player_air_dribble_stats
104    }
105
106    pub fn team_zero_stats(&self) -> &BallCarryStats {
107        &self.team_zero_stats
108    }
109
110    pub fn team_one_stats(&self) -> &BallCarryStats {
111        &self.team_one_stats
112    }
113
114    pub fn team_zero_air_dribble_stats(&self) -> &AirDribbleStats {
115        &self.team_zero_air_dribble_stats
116    }
117
118    pub fn team_one_air_dribble_stats(&self) -> &AirDribbleStats {
119        &self.team_one_air_dribble_stats
120    }
121
122    pub fn carry_events(&self) -> &[BallCarryEvent] {
123        &self.carry_events
124    }
125
126    pub(crate) fn carry_frame_sample(
127        player: &PlayerSample,
128        ball: &BallSample,
129    ) -> Option<ContinuousBallControlSample<BallCarryKind>> {
130        let player_position = player.position()?;
131        let ball_position = ball.position();
132        let horizontal_gap = player_position
133            .truncate()
134            .distance(ball_position.truncate());
135        let vertical_gap = ball_position.z - player_position.z;
136
137        if AirDribblePolicy::is_sample(player_position, ball_position, horizontal_gap, vertical_gap)
138        {
139            return Some(ContinuousBallControlSample {
140                player_position,
141                kind: BallCarryKind::AirDribble,
142                horizontal_gap,
143                vertical_gap,
144                speed: player.speed().unwrap_or(0.0),
145            });
146        }
147
148        if player_is_on_wall(player_position) {
149            return None;
150        }
151
152        if !(BALL_CARRY_MIN_BALL_Z..=BALL_CARRY_MAX_BALL_Z).contains(&ball_position.z) {
153            return None;
154        }
155
156        if horizontal_gap > BALL_CARRY_MAX_HORIZONTAL_GAP {
157            return None;
158        }
159
160        if !(0.0..=BALL_CARRY_MAX_VERTICAL_GAP).contains(&vertical_gap) {
161            return None;
162        }
163
164        Some(ContinuousBallControlSample {
165            player_position,
166            kind: BallCarryKind::Carry,
167            horizontal_gap,
168            vertical_gap,
169            speed: player.speed().unwrap_or(0.0),
170        })
171    }
172
173    pub(crate) fn kind_requires_airborne(kind: BallCarryKind) -> bool {
174        AirDribblePolicy::kind_requires_airborne(kind)
175    }
176
177    pub(crate) fn control_player_statuses(
178        players: &PlayerFrameState,
179    ) -> Vec<ContinuousBallControlPlayerStatus> {
180        players
181            .players
182            .iter()
183            .filter_map(|player| {
184                Some(ContinuousBallControlPlayerStatus {
185                    player_id: player.player_id.clone(),
186                    is_airborne: AirDribblePolicy::is_air_touch_position(player.position()?),
187                })
188            })
189            .collect()
190    }
191
192    pub(crate) fn control_touches(
193        touch_state: &TouchState,
194        players: &PlayerFrameState,
195    ) -> Vec<ContinuousBallControlTouch> {
196        touch_state
197            .touch_events
198            .iter()
199            .filter_map(|touch| {
200                let player_id = touch.player.clone()?;
201                let player = players
202                    .players
203                    .iter()
204                    .find(|player| player.player_id == player_id)?;
205                Some(ContinuousBallControlTouch {
206                    player_id,
207                    is_airborne: AirDribblePolicy::is_air_touch_position(player.position()?),
208                })
209            })
210            .collect()
211    }
212
213    pub(crate) fn min_duration_for_kind(kind: BallCarryKind) -> f32 {
214        match kind {
215            BallCarryKind::Carry => BALL_CARRY_MIN_DURATION,
216            BallCarryKind::AirDribble => AIR_DRIBBLE_MIN_DURATION,
217        }
218    }
219
220    pub(crate) fn control_candidate(
221        ball: &BallFrameState,
222        players: &PlayerFrameState,
223        live_play: bool,
224        touch_state: &TouchState,
225    ) -> Option<ContinuousBallControlCandidate<BallCarryKind>> {
226        if !live_play {
227            return None;
228        }
229        let ball = ball.sample()?;
230        let player_id = touch_state.last_touch_player.as_ref()?;
231        let touch_count = touch_state
232            .touch_events
233            .iter()
234            .filter(|event| event.player.as_ref() == Some(player_id))
235            .count() as u32;
236        players
237            .players
238            .iter()
239            .find(|player| &player.player_id == player_id)
240            .and_then(|player| {
241                Self::carry_frame_sample(player, ball).map(|sample| {
242                    let air_touch_count =
243                        if AirDribblePolicy::is_air_touch_position(sample.player_position) {
244                            touch_count
245                        } else {
246                            0
247                        };
248                    ContinuousBallControlCandidate {
249                        player_id: player.player_id.clone(),
250                        is_team_0: player.is_team_0,
251                        touch_count,
252                        air_touch_count,
253                        sample,
254                    }
255                })
256            })
257    }
258
259    fn event_from_sequence(
260        sequence: CompletedBallControlSequence<BallCarryKind>,
261    ) -> BallCarryEvent {
262        let air_dribble_origin = (sequence.kind == BallCarryKind::AirDribble)
263            .then(|| AirDribblePolicy::origin(sequence.start_position));
264        BallCarryEvent {
265            player_id: sequence.player_id,
266            is_team_0: sequence.is_team_0,
267            kind: sequence.kind,
268            start_frame: sequence.start_frame,
269            end_frame: sequence.end_frame,
270            start_time: sequence.start_time,
271            end_time: sequence.end_time,
272            duration: sequence.duration,
273            straight_line_distance: sequence.straight_line_distance,
274            path_distance: sequence.path_distance,
275            average_horizontal_gap: sequence.average_horizontal_gap,
276            average_vertical_gap: sequence.average_vertical_gap,
277            average_speed: sequence.average_speed,
278            touch_count: sequence.touch_count,
279            air_touch_count: sequence.air_touch_count,
280            air_dribble_origin,
281        }
282    }
283
284    fn record_carry_event(&mut self, event: BallCarryEvent) {
285        match event.kind {
286            BallCarryKind::Carry => {
287                let player_stats = self
288                    .player_stats
289                    .entry(event.player_id.clone())
290                    .or_default();
291                Self::apply_carry_event(player_stats, &event);
292
293                let team_stats = if event.is_team_0 {
294                    &mut self.team_zero_stats
295                } else {
296                    &mut self.team_one_stats
297                };
298                Self::apply_carry_event(team_stats, &event);
299            }
300            BallCarryKind::AirDribble => {
301                let player_stats = self
302                    .player_air_dribble_stats
303                    .entry(event.player_id.clone())
304                    .or_default();
305                AirDribblePolicy::apply_event(player_stats, &event);
306
307                let team_stats = if event.is_team_0 {
308                    &mut self.team_zero_air_dribble_stats
309                } else {
310                    &mut self.team_one_air_dribble_stats
311                };
312                AirDribblePolicy::apply_event(team_stats, &event);
313            }
314        }
315        self.carry_events.push(event);
316    }
317
318    fn apply_carry_event(stats: &mut BallCarryStats, event: &BallCarryEvent) {
319        stats.carry_count += 1;
320        stats.total_carry_time += event.duration;
321        stats.total_straight_line_distance += event.straight_line_distance;
322        stats.total_path_distance += event.path_distance;
323        stats.longest_carry_time = stats.longest_carry_time.max(event.duration);
324        stats.furthest_carry_distance = stats
325            .furthest_carry_distance
326            .max(event.straight_line_distance);
327        stats.fastest_carry_speed = stats.fastest_carry_speed.max(event.average_speed);
328        stats.carry_speed_sum += event.average_speed;
329        stats.average_horizontal_gap_sum += event.average_horizontal_gap;
330        stats.average_vertical_gap_sum += event.average_vertical_gap;
331    }
332
333    pub fn update(&mut self, control_state: &ContinuousBallControlState) -> SubtrActorResult<()> {
334        for sequence in control_state
335            .completed_sequences
336            .iter()
337            .skip(self.processed_control_sequence_count)
338            .cloned()
339        {
340            if !AirDribblePolicy::is_valid_sequence(&sequence) {
341                continue;
342            }
343            self.record_carry_event(Self::event_from_sequence(sequence));
344        }
345        self.processed_control_sequence_count = control_state.completed_sequences.len();
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351#[path = "ball_carry_tests.rs"]
352mod tests;