Skip to main content

subtr_actor/stats/accumulators/
kickoff.rs

1use super::*;
2
3/// Match-wide accumulated kickoff win/possession/goal tallies.
4#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
5#[ts(export)]
6pub struct KickoffStats {
7    pub count: u32,
8    pub team_zero_wins: u32,
9    pub team_one_wins: u32,
10    pub neutral_outcomes: u32,
11    pub team_zero_kickoff_possessions: u32,
12    pub team_one_kickoff_possessions: u32,
13    pub team_zero_kickoff_possession_advantages: u32,
14    pub team_one_kickoff_possession_advantages: u32,
15    pub contested_kickoff_possessions: u32,
16    pub kickoff_goal_count: u32,
17    pub team_zero_kickoff_goals: u32,
18    pub team_one_kickoff_goals: u32,
19    pub win_strength_sample_count: u32,
20    pub cumulative_win_strength: f32,
21    pub boost_after_sample_count: u32,
22    pub cumulative_boost_after: f32,
23    pub fake_count: u32,
24    pub missed_count: u32,
25    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
26    pub labeled_event_counts: LabeledCounts,
27    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
28    pub labeled_player_counts: LabeledCounts,
29}
30
31/// Per-player accumulated kickoff stats.
32#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
33#[ts(export)]
34pub struct KickoffPlayerStats {
35    pub count: u32,
36    pub touches: u32,
37    pub fakes: u32,
38    pub misses: u32,
39    pub support_go_for_boosts: u32,
40    pub support_cheats: u32,
41    pub support_other: u32,
42    /// Kickoffs on which this player was a non-taker (support) and contributed
43    /// a `start_distance_from_center` sample.
44    pub support_distance_sample_count: u32,
45    /// Sum of support spawn distances from field center, for averaging.
46    pub cumulative_support_distance_from_center: f32,
47    pub kickoff_goal_count: u32,
48    pub boost_after_sample_count: u32,
49    pub cumulative_boost_after: f32,
50    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
51    pub labeled_event_counts: LabeledCounts,
52}
53
54/// Per-team accumulated kickoff stats.
55#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
56#[ts(export)]
57pub struct KickoffTeamStats {
58    pub count: u32,
59    pub wins: u32,
60    pub losses: u32,
61    pub neutral_outcomes: u32,
62    pub kickoff_possessions: u32,
63    pub opponent_kickoff_possessions: u32,
64    pub kickoff_possession_advantages: u32,
65    pub opponent_kickoff_possession_advantages: u32,
66    pub contested_kickoff_possessions: u32,
67    pub kickoff_goal_count: u32,
68    pub kickoff_goals_for: u32,
69    pub kickoff_goals_against: u32,
70    pub win_strength_sample_count: u32,
71    pub cumulative_win_strength: f32,
72    pub boost_after_sample_count: u32,
73    pub cumulative_boost_after: f32,
74    pub fake_count: u32,
75    pub missed_count: u32,
76}
77
78impl KickoffStats {
79    pub fn average_win_strength(&self) -> f32 {
80        if self.win_strength_sample_count == 0 {
81            0.0
82        } else {
83            self.cumulative_win_strength / self.win_strength_sample_count as f32
84        }
85    }
86
87    pub fn average_boost_after(&self) -> f32 {
88        if self.boost_after_sample_count == 0 {
89            0.0
90        } else {
91            self.cumulative_boost_after / self.boost_after_sample_count as f32
92        }
93    }
94
95    pub(crate) fn record_event(&mut self, event: &KickoffEvent) {
96        self.count += 1;
97        self.labeled_event_counts.increment(event.labels());
98        match event.outcome {
99            KickoffOutcome::TeamZeroWin => self.team_zero_wins += 1,
100            KickoffOutcome::TeamOneWin => self.team_one_wins += 1,
101            KickoffOutcome::Neutral => self.neutral_outcomes += 1,
102            KickoffOutcome::Unknown => {}
103        }
104        match event.kickoff_possession_outcome {
105            KickoffPossessionOutcome::TeamZeroPossession => self.team_zero_kickoff_possessions += 1,
106            KickoffPossessionOutcome::TeamOnePossession => self.team_one_kickoff_possessions += 1,
107            KickoffPossessionOutcome::TeamZeroAdvantage => {
108                self.team_zero_kickoff_possession_advantages += 1
109            }
110            KickoffPossessionOutcome::TeamOneAdvantage => {
111                self.team_one_kickoff_possession_advantages += 1
112            }
113            KickoffPossessionOutcome::Contested => self.contested_kickoff_possessions += 1,
114        }
115        if event.kickoff_goal {
116            self.kickoff_goal_count += 1;
117            match event.scoring_team_is_team_0 {
118                Some(true) => self.team_zero_kickoff_goals += 1,
119                Some(false) => self.team_one_kickoff_goals += 1,
120                None => {}
121            }
122        }
123        if let Some(win_strength) = event.win_strength {
124            self.win_strength_sample_count += 1;
125            self.cumulative_win_strength += win_strength;
126        }
127        for player in event.player_events() {
128            self.labeled_player_counts.increment(player.labels());
129            if let Some(taker) = player.as_taker() {
130                if let Some(boost_after) = taker.boost_after {
131                    self.boost_after_sample_count += 1;
132                    self.cumulative_boost_after += boost_after;
133                }
134                match taker.outcome {
135                    KickoffTakerOutcome::Fake => self.fake_count += 1,
136                    KickoffTakerOutcome::Missed => self.missed_count += 1,
137                    _ => {}
138                }
139            }
140        }
141    }
142
143    pub fn complete_labeled_event_counts(&self) -> LabeledCounts {
144        LabeledCounts::complete_from_label_sets(
145            &[
146                &KICKOFF_OUTCOME_LABELS,
147                &KICKOFF_TYPE_LABELS,
148                &KICKOFF_DIRECTION_LABELS,
149                &KICKOFF_WIN_STRENGTH_LABELS,
150                &KICKOFF_POSSESSION_OUTCOME_LABELS,
151                &KICKOFF_GOAL_LABELS,
152                &KICKOFF_ADVANTAGE_LABELS,
153            ],
154            &self.labeled_event_counts,
155        )
156    }
157
158    pub fn complete_labeled_player_counts(&self) -> LabeledCounts {
159        LabeledCounts::complete_from_label_sets(
160            &[
161                &KICKOFF_SPAWN_LABELS,
162                &KICKOFF_TAKER_OUTCOME_LABELS,
163                &KICKOFF_APPROACH_LABELS,
164                &KICKOFF_FLIP_DIRECTION_LABELS,
165                &KICKOFF_SUPPORT_BEHAVIOR_LABELS,
166                &KICKOFF_BALL_DIRECTION_LABELS,
167            ],
168            &self.labeled_player_counts,
169        )
170    }
171
172    pub fn for_team(&self, is_team_zero: bool) -> KickoffTeamStats {
173        let wins = if is_team_zero {
174            self.team_zero_wins
175        } else {
176            self.team_one_wins
177        };
178        let losses = if is_team_zero {
179            self.team_one_wins
180        } else {
181            self.team_zero_wins
182        };
183        let kickoff_possessions = if is_team_zero {
184            self.team_zero_kickoff_possessions
185        } else {
186            self.team_one_kickoff_possessions
187        };
188        let opponent_kickoff_possessions = if is_team_zero {
189            self.team_one_kickoff_possessions
190        } else {
191            self.team_zero_kickoff_possessions
192        };
193        let kickoff_possession_advantages = if is_team_zero {
194            self.team_zero_kickoff_possession_advantages
195        } else {
196            self.team_one_kickoff_possession_advantages
197        };
198        let opponent_kickoff_possession_advantages = if is_team_zero {
199            self.team_one_kickoff_possession_advantages
200        } else {
201            self.team_zero_kickoff_possession_advantages
202        };
203        let kickoff_goals_for = if is_team_zero {
204            self.team_zero_kickoff_goals
205        } else {
206            self.team_one_kickoff_goals
207        };
208        let kickoff_goals_against = if is_team_zero {
209            self.team_one_kickoff_goals
210        } else {
211            self.team_zero_kickoff_goals
212        };
213        KickoffTeamStats {
214            count: self.count,
215            wins,
216            losses,
217            neutral_outcomes: self.neutral_outcomes,
218            kickoff_possessions,
219            opponent_kickoff_possessions,
220            kickoff_possession_advantages,
221            opponent_kickoff_possession_advantages,
222            contested_kickoff_possessions: self.contested_kickoff_possessions,
223            kickoff_goal_count: self.kickoff_goal_count,
224            kickoff_goals_for,
225            kickoff_goals_against,
226            win_strength_sample_count: self.win_strength_sample_count,
227            cumulative_win_strength: self.cumulative_win_strength,
228            boost_after_sample_count: self.boost_after_sample_count,
229            cumulative_boost_after: self.cumulative_boost_after,
230            fake_count: self.fake_count,
231            missed_count: self.missed_count,
232        }
233    }
234}
235
236impl KickoffPlayerStats {
237    pub(crate) fn record_event(&mut self, event: &KickoffEvent, player: KickoffPlayerEventRef<'_>) {
238        self.count += 1;
239        self.labeled_event_counts.increment(player.labels());
240        if let Some(taker) = player.as_taker() {
241            match taker.outcome {
242                KickoffTakerOutcome::Touched => self.touches += 1,
243                KickoffTakerOutcome::Fake => self.fakes += 1,
244                KickoffTakerOutcome::Missed => self.misses += 1,
245                KickoffTakerOutcome::Unknown => {}
246            }
247        }
248        if let Some(support) = player.as_support() {
249            if support.first_touch_time.is_some() {
250                self.touches += 1;
251            }
252            self.support_distance_sample_count += 1;
253            self.cumulative_support_distance_from_center += support.start_distance_from_center;
254            match support.support_behavior {
255                KickoffSupportBehavior::GoForBoost => self.support_go_for_boosts += 1,
256                KickoffSupportBehavior::Cheat => self.support_cheats += 1,
257                KickoffSupportBehavior::Other => self.support_other += 1,
258                KickoffSupportBehavior::Unknown => {}
259            }
260        }
261        if event.kickoff_goal && event.scoring_team_is_team_0 == Some(player.is_team_0()) {
262            self.kickoff_goal_count += 1;
263        }
264        if let Some(boost_after) = player.boost_after() {
265            self.boost_after_sample_count += 1;
266            self.cumulative_boost_after += boost_after;
267        }
268    }
269
270    pub fn average_boost_after(&self) -> f32 {
271        if self.boost_after_sample_count == 0 {
272            0.0
273        } else {
274            self.cumulative_boost_after / self.boost_after_sample_count as f32
275        }
276    }
277
278    /// Mean spawn distance from field center across kickoffs on which this
279    /// player was support.
280    pub fn average_support_distance_from_center(&self) -> f32 {
281        if self.support_distance_sample_count == 0 {
282            0.0
283        } else {
284            self.cumulative_support_distance_from_center / self.support_distance_sample_count as f32
285        }
286    }
287}
288
289impl KickoffTeamStats {
290    pub fn average_win_strength(&self) -> f32 {
291        if self.win_strength_sample_count == 0 {
292            0.0
293        } else {
294            self.cumulative_win_strength / self.win_strength_sample_count as f32
295        }
296    }
297
298    pub fn average_boost_after(&self) -> f32 {
299        if self.boost_after_sample_count == 0 {
300            0.0
301        } else {
302            self.cumulative_boost_after / self.boost_after_sample_count as f32
303        }
304    }
305}
306
307/// Accumulates kickoff stats over the replay from kickoff events.
308#[derive(Debug, Clone, Default, PartialEq)]
309pub struct KickoffStatsAccumulator {
310    stats: KickoffStats,
311    player_stats: HashMap<PlayerId, KickoffPlayerStats>,
312}
313
314impl KickoffStatsAccumulator {
315    pub fn new() -> Self {
316        Self::default()
317    }
318
319    pub fn stats(&self) -> &KickoffStats {
320        &self.stats
321    }
322
323    pub fn player_stats(&self) -> &HashMap<PlayerId, KickoffPlayerStats> {
324        &self.player_stats
325    }
326
327    pub fn apply_event(&mut self, event: &KickoffEvent) {
328        self.stats.record_event(event);
329        for player in event.player_events() {
330            self.player_stats
331                .entry(player.player().clone())
332                .or_default()
333                .record_event(event, player);
334        }
335    }
336}