subtr_actor/stats/accumulators/
touch.rs1use super::*;
2
3const TOUCH_KIND_LABEL_VALUES: [&str; 3] = ["control", "medium_hit", "hard_hit"];
4const TOUCH_SURFACE_LABEL_VALUES: [&str; 3] = ["ground", "air", "wall"];
5const TOUCH_DODGE_STATE_LABEL_VALUES: [&str; 2] = ["no_dodge", "dodge"];
6const TOUCH_ACTION_LABEL_VALUES: [&str; 5] = ["shot", "save", "clear", "boom", "pass"];
12const TOUCH_POSSESSION_LABEL_VALUES: [&str; 2] = ["control", "advance"];
13const TOUCH_RECEPTION_LABEL_VALUES: [&str; 2] = ["first_touch", "continuation"];
14
15fn touch_kind_label(value: &str) -> StatLabel {
16 match value {
17 "medium_hit" => StatLabel::new("kind", "medium_hit"),
18 "hard_hit" => StatLabel::new("kind", "hard_hit"),
19 _ => StatLabel::new("kind", "control"),
20 }
21}
22
23fn touch_height_band_label(value: &str) -> StatLabel {
24 match value {
25 "low_air" => StatLabel::new("height_band", "low_air"),
26 "high_air" => StatLabel::new("height_band", "high_air"),
27 _ => StatLabel::new("height_band", "ground"),
28 }
29}
30
31fn touch_surface_label(value: &str) -> StatLabel {
32 match value {
33 "air" => StatLabel::new("surface", "air"),
34 "wall" => StatLabel::new("surface", "wall"),
35 _ => StatLabel::new("surface", "ground"),
36 }
37}
38
39fn touch_dodge_state_label(value: &str) -> StatLabel {
40 match value {
41 "dodge" => StatLabel::new("dodge_state", "dodge"),
42 _ => StatLabel::new("dodge_state", "no_dodge"),
43 }
44}
45
46fn touch_action_label(value: &str) -> Option<StatLabel> {
47 let label = match value {
48 "shot" => StatLabel::new("action", "shot"),
49 "save" => StatLabel::new("action", "save"),
50 "clear" => StatLabel::new("action", "clear"),
51 "boom" => StatLabel::new("action", "boom"),
52 "pass" => StatLabel::new("action", "pass"),
53 _ => return None,
54 };
55 Some(label)
56}
57
58fn touch_possession_label(value: &str) -> Option<StatLabel> {
59 let label = match value {
60 "control" => StatLabel::new("possession", "control"),
61 "advance" => StatLabel::new("possession", "advance"),
62 _ => return None,
63 };
64 Some(label)
65}
66
67fn touch_reception_label(first_touch: bool) -> StatLabel {
68 StatLabel::new(
69 "reception",
70 if first_touch {
71 "first_touch"
72 } else {
73 "continuation"
74 },
75 )
76}
77
78#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
80#[ts(export)]
81pub struct TouchStats {
82 pub touch_count: u32,
83 pub control_touch_count: u32,
84 pub medium_hit_count: u32,
85 pub hard_hit_count: u32,
86 pub aerial_touch_count: u32,
87 pub high_aerial_touch_count: u32,
88 #[serde(default)]
89 pub wall_touch_count: u32,
90 #[serde(default)]
91 pub first_touch_count: u32,
92 pub is_last_touch: bool,
93 pub last_touch_time: Option<f32>,
94 pub last_touch_frame: Option<usize>,
95 pub time_since_last_touch: Option<f32>,
96 pub frames_since_last_touch: Option<usize>,
97 pub last_ball_speed_change: Option<f32>,
98 pub max_ball_speed_change: f32,
99 pub cumulative_ball_speed_change: f32,
100 #[serde(default)]
101 pub total_ball_travel_distance: f32,
102 #[serde(default)]
103 pub total_ball_advance_distance: f32,
104 #[serde(default)]
105 pub total_ball_retreat_distance: f32,
106 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
107 pub labeled_touch_counts: LabeledCounts,
108 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
109 pub labeled_action_counts: LabeledCounts,
110 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
111 pub labeled_possession_counts: LabeledCounts,
112 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
113 pub touch_counts_by_role: LabeledCounts,
114 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
115 pub touch_counts_by_play_depth: LabeledCounts,
116}
117
118impl TouchStats {
119 pub fn average_ball_speed_change(&self) -> f32 {
120 if self.touch_count == 0 {
121 0.0
122 } else {
123 self.cumulative_ball_speed_change / self.touch_count as f32
124 }
125 }
126
127 pub fn touch_count_with_labels(&self, labels: &[StatLabel]) -> u32 {
128 self.labeled_touch_counts.count_matching(labels)
129 }
130
131 pub fn dodge_touch_count(&self) -> u32 {
132 self.touch_count_with_labels(&[StatLabel::new("dodge_state", "dodge")])
133 }
134
135 pub fn dodge_hit_count(&self) -> u32 {
136 self.touch_count_with_labels(&[
137 StatLabel::new("dodge_state", "dodge"),
138 StatLabel::new("kind", "medium_hit"),
139 ]) + self.touch_count_with_labels(&[
140 StatLabel::new("dodge_state", "dodge"),
141 StatLabel::new("kind", "hard_hit"),
142 ])
143 }
144
145 pub fn action_count(&self, action: &str) -> u32 {
146 let Some(label) = touch_action_label(action) else {
147 return 0;
148 };
149 self.labeled_action_counts.count_matching(&[label])
150 }
151
152 pub fn first_touch_action_count(&self, action: &str) -> u32 {
153 let Some(label) = touch_action_label(action) else {
154 return 0;
155 };
156 self.labeled_action_counts
157 .count_matching(&[label, touch_reception_label(true)])
158 }
159
160 pub fn possession_count(&self, possession: &str) -> u32 {
161 let Some(label) = touch_possession_label(possession) else {
162 return 0;
163 };
164 self.labeled_possession_counts.count_matching(&[label])
165 }
166
167 pub fn first_touch_possession_count(&self, possession: &str) -> u32 {
168 let Some(label) = touch_possession_label(possession) else {
169 return 0;
170 };
171 self.labeled_possession_counts
172 .count_matching(&[label, touch_reception_label(true)])
173 }
174
175 fn complete_labeled_counts(
176 source: &LabeledCounts,
177 key: &'static str,
178 values: &[&'static str],
179 ) -> LabeledCounts {
180 let mut entries: Vec<_> = values
181 .iter()
182 .flat_map(|&value| {
183 TOUCH_RECEPTION_LABEL_VALUES
184 .into_iter()
185 .map(move |reception| {
186 let mut labels = vec![
187 StatLabel::new(key, value),
188 StatLabel::new("reception", reception),
189 ];
190 labels.sort();
191 LabeledCountEntry {
192 count: source.count_exact(&labels),
193 labels,
194 }
195 })
196 })
197 .collect();
198
199 entries.sort_by(|left, right| left.labels.cmp(&right.labels));
200
201 LabeledCounts { entries }
202 }
203
204 pub fn complete_labeled_action_counts(&self) -> LabeledCounts {
205 Self::complete_labeled_counts(
206 &self.labeled_action_counts,
207 "action",
208 &TOUCH_ACTION_LABEL_VALUES,
209 )
210 }
211
212 pub fn complete_labeled_possession_counts(&self) -> LabeledCounts {
213 Self::complete_labeled_counts(
214 &self.labeled_possession_counts,
215 "possession",
216 &TOUCH_POSSESSION_LABEL_VALUES,
217 )
218 }
219
220 pub fn touch_count_with_role(&self, role: RoleState) -> u32 {
221 self.touch_counts_by_role.count_exact(&[role.as_label()])
222 }
223
224 pub fn touch_count_with_play_depth(&self, play_depth: PlayDepthState) -> u32 {
225 self.touch_counts_by_play_depth
226 .count_exact(&[play_depth.as_label()])
227 }
228
229 pub fn touches_as_first_man(&self) -> u32 {
230 self.touch_count_with_role(RoleState::FirstMan)
231 }
232
233 pub fn touches_as_second_man(&self) -> u32 {
234 self.touch_count_with_role(RoleState::SecondMan)
235 }
236
237 pub fn touches_as_third_man(&self) -> u32 {
238 self.touch_count_with_role(RoleState::ThirdMan)
239 }
240
241 pub fn touches_behind_play(&self) -> u32 {
242 self.touch_count_with_play_depth(PlayDepthState::BehindPlay)
243 }
244
245 pub fn touches_ahead_of_play(&self) -> u32 {
246 self.touch_count_with_play_depth(PlayDepthState::AheadOfPlay)
247 }
248
249 pub fn complete_touch_counts_by_role(&self) -> LabeledCounts {
250 let mut entries: Vec<_> = ALL_ROLE_STATES
251 .into_iter()
252 .map(|role| {
253 let labels = vec![role.as_label()];
254 LabeledCountEntry {
255 count: self.touch_counts_by_role.count_exact(&labels),
256 labels,
257 }
258 })
259 .collect();
260 entries.sort_by(|left, right| left.labels.cmp(&right.labels));
261 LabeledCounts { entries }
262 }
263
264 pub fn complete_touch_counts_by_play_depth(&self) -> LabeledCounts {
265 let mut entries: Vec<_> = ALL_PLAY_DEPTH_STATES
266 .into_iter()
267 .map(|play_depth| {
268 let labels = vec![play_depth.as_label()];
269 LabeledCountEntry {
270 count: self.touch_counts_by_play_depth.count_exact(&labels),
271 labels,
272 }
273 })
274 .collect();
275 entries.sort_by(|left, right| left.labels.cmp(&right.labels));
276 LabeledCounts { entries }
277 }
278
279 pub fn complete_labeled_touch_counts(&self) -> LabeledCounts {
280 let mut entries: Vec<_> = ALL_PLAYER_VERTICAL_BANDS
281 .into_iter()
282 .flat_map(|height_band| {
283 TOUCH_SURFACE_LABEL_VALUES
284 .into_iter()
285 .flat_map(move |surface| {
286 TOUCH_DODGE_STATE_LABEL_VALUES
287 .into_iter()
288 .flat_map(move |dodge_state| {
289 TOUCH_KIND_LABEL_VALUES.into_iter().map(move |kind| {
290 let mut labels = vec![
291 StatLabel::new("kind", kind),
292 height_band.as_label(),
293 StatLabel::new("surface", surface),
294 StatLabel::new("dodge_state", dodge_state),
295 ];
296 labels.sort();
297 LabeledCountEntry {
298 count: self.labeled_touch_counts.count_exact(&labels),
299 labels,
300 }
301 })
302 })
303 })
304 })
305 .collect();
306
307 entries.sort_by(|left, right| left.labels.cmp(&right.labels));
308
309 LabeledCounts { entries }
310 }
311
312 pub fn with_complete_labeled_touch_counts(mut self) -> Self {
313 self.labeled_touch_counts = self.complete_labeled_touch_counts();
314 self.labeled_action_counts = self.complete_labeled_action_counts();
315 self.labeled_possession_counts = self.complete_labeled_possession_counts();
316 self
317 }
318}
319
320#[derive(Debug, Clone, Default, PartialEq)]
322pub struct TouchStatsAccumulator {
323 player_stats: HashMap<PlayerId, TouchStats>,
324 current_last_touch_player: Option<PlayerId>,
325}
326
327impl TouchStatsAccumulator {
328 pub fn new() -> Self {
329 Self::default()
330 }
331
332 pub fn player_stats(&self) -> &HashMap<PlayerId, TouchStats> {
333 &self.player_stats
334 }
335
336 pub fn begin_sample(&mut self, frame: &FrameInfo) {
337 for stats in self.player_stats.values_mut() {
338 stats.is_last_touch = false;
339 stats.time_since_last_touch = stats
340 .last_touch_time
341 .map(|time| (frame.time - time).max(0.0));
342 stats.frames_since_last_touch = stats
343 .last_touch_frame
344 .map(|last_frame| frame.frame_number.saturating_sub(last_frame));
345 }
346 }
347
348 pub fn apply_touch_event(&mut self, event: &TouchClassificationEvent, frame: &FrameInfo) {
349 let kind = event.tag("kind").unwrap_or("control");
356 let height_band = event.tag("height_band").unwrap_or("ground");
357 let surface = event.tag("surface").unwrap_or("ground");
358 let dodge_state = event.tag("dodge_state").unwrap_or("no_dodge");
359 let action = event.tag("action");
360 let possession = event.tag("possession");
361 let first_touch = event.tag("reception") == Some("first_touch");
362
363 let stats = self.player_stats.entry(event.player.clone()).or_default();
364 stats.touch_count += 1;
365 match height_band {
366 "low_air" => stats.aerial_touch_count += 1,
367 "high_air" => {
368 stats.aerial_touch_count += 1;
369 stats.high_aerial_touch_count += 1;
370 }
371 _ => {}
372 }
373 match kind {
374 "control" => stats.control_touch_count += 1,
375 "medium_hit" => stats.medium_hit_count += 1,
376 "hard_hit" => stats.hard_hit_count += 1,
377 _ => {}
378 }
379 if surface == "wall" {
380 stats.wall_touch_count += 1;
381 }
382 stats.labeled_touch_counts.increment([
383 touch_kind_label(kind),
384 touch_height_band_label(height_band),
385 touch_surface_label(surface),
386 touch_dodge_state_label(dodge_state),
387 ]);
388 if first_touch {
389 stats.first_touch_count += 1;
390 }
391 if let Some(label) = action.and_then(touch_action_label) {
392 stats
393 .labeled_action_counts
394 .increment([label, touch_reception_label(first_touch)]);
395 }
396 if let Some(label) = possession.and_then(touch_possession_label) {
397 stats
398 .labeled_possession_counts
399 .increment([label, touch_reception_label(first_touch)]);
400 }
401 stats
402 .touch_counts_by_role
403 .increment([event.role.as_label()]);
404 stats
405 .touch_counts_by_play_depth
406 .increment([event.play_depth.as_label()]);
407 stats.last_touch_time = Some(event.time);
408 stats.last_touch_frame = Some(event.frame);
409 stats.time_since_last_touch = Some((frame.time - event.time).max(0.0));
410 stats.frames_since_last_touch = Some(frame.frame_number.saturating_sub(event.frame));
411 stats.last_ball_speed_change = Some(event.ball_speed_change);
412 stats.max_ball_speed_change = stats.max_ball_speed_change.max(event.ball_speed_change);
413 stats.cumulative_ball_speed_change += event.ball_speed_change;
414 if let Some(movement) = event.ball_movement.as_ref() {
415 stats.total_ball_travel_distance += movement.travel_distance;
416 stats.total_ball_advance_distance += movement.advance_distance;
417 stats.total_ball_retreat_distance += movement.retreat_distance;
418 }
419 self.current_last_touch_player = Some(event.player.clone());
420 }
421
422 pub fn set_current_last_touch_player(&mut self, player: Option<PlayerId>) {
423 self.current_last_touch_player = player;
424 }
425
426 pub fn restore_current_last_touch_marker(&mut self) {
427 if let Some(player_id) = self.current_last_touch_player.as_ref() {
428 if let Some(stats) = self.player_stats.get_mut(player_id) {
429 stats.is_last_touch = true;
430 }
431 }
432 }
433}