Skip to main content

subtr_actor/stats/calculators/
live_play.rs

1use super::{FrameEventsState, GameplayState};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
5#[serde(rename_all = "snake_case")]
6#[ts(export)]
7pub enum GameplayPhase {
8    #[default]
9    Unknown,
10    KickoffCountdown,
11    KickoffWaitingForTouch,
12    ActivePlay,
13    PostGoal,
14}
15
16impl GameplayPhase {
17    pub fn is_live_play(self) -> bool {
18        matches!(self, Self::ActivePlay)
19    }
20
21    pub fn counts_toward_player_motion(self) -> bool {
22        matches!(self, Self::ActivePlay | Self::KickoffWaitingForTouch)
23    }
24
25    pub fn counts_toward_ball_position_stats(self) -> bool {
26        self.is_live_play()
27    }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub struct LivePlayState {
32    pub gameplay_phase: GameplayPhase,
33    pub is_live_play: bool,
34}
35
36impl LivePlayState {
37    pub fn counts_toward_player_motion(&self) -> bool {
38        self.gameplay_phase.counts_toward_player_motion()
39    }
40
41    pub fn counts_toward_ball_position_stats(&self) -> bool {
42        self.gameplay_phase.counts_toward_ball_position_stats()
43    }
44}
45
46#[derive(Debug, Clone, Default, PartialEq)]
47pub struct LivePlayTracker {
48    post_goal_phase_active: bool,
49    last_score: Option<(i32, i32)>,
50}
51
52impl LivePlayTracker {
53    fn gameplay_phase_internal(
54        &mut self,
55        gameplay: &GameplayState,
56        events: &FrameEventsState,
57    ) -> GameplayPhase {
58        let kickoff_phase_active = gameplay.kickoff_phase_active();
59        let score_changed = gameplay.current_score().zip(self.last_score).is_some_and(
60            |((team_zero_score, team_one_score), (last_team_zero, last_team_one))| {
61                team_zero_score > last_team_zero || team_one_score > last_team_one
62            },
63        );
64
65        if !events.goal_events.is_empty() || score_changed {
66            self.post_goal_phase_active = true;
67        }
68
69        if kickoff_phase_active {
70            self.post_goal_phase_active = false;
71        }
72
73        if let Some(score) = gameplay.current_score() {
74            self.last_score = Some(score);
75        }
76
77        if gameplay.game_state == Some(crate::stats::calculators::GAME_STATE_KICKOFF_COUNTDOWN)
78            || gameplay.kickoff_countdown_time.is_some_and(|time| time > 0)
79        {
80            GameplayPhase::KickoffCountdown
81        } else if gameplay.game_state
82            == Some(crate::stats::calculators::GAME_STATE_GOAL_SCORED_REPLAY)
83            || self.post_goal_phase_active
84        {
85            GameplayPhase::PostGoal
86        } else if gameplay.ball_has_been_hit == Some(false) {
87            GameplayPhase::KickoffWaitingForTouch
88        } else if gameplay.is_live_play() {
89            GameplayPhase::ActivePlay
90        } else {
91            GameplayPhase::Unknown
92        }
93    }
94
95    pub fn state_parts(
96        &mut self,
97        gameplay: &GameplayState,
98        events: &FrameEventsState,
99    ) -> LivePlayState {
100        let gameplay_phase = self.gameplay_phase_internal(gameplay, events);
101        LivePlayState {
102            gameplay_phase,
103            is_live_play: gameplay_phase.is_live_play(),
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::GoalEvent;
112
113    #[test]
114    fn kickoff_waiting_for_first_touch_is_not_live_play() {
115        let mut tracker = LivePlayTracker::default();
116        let gameplay = GameplayState {
117            ball_has_been_hit: Some(false),
118            ..Default::default()
119        };
120        let state = tracker.state_parts(&gameplay, &FrameEventsState::default());
121
122        assert_eq!(state.gameplay_phase, GameplayPhase::KickoffWaitingForTouch);
123        assert!(!state.is_live_play);
124        assert!(state.gameplay_phase.counts_toward_player_motion());
125    }
126
127    #[test]
128    fn goal_event_enters_post_goal_phase() {
129        let mut tracker = LivePlayTracker::default();
130        let gameplay = GameplayState::default();
131        let events = FrameEventsState {
132            goal_events: vec![GoalEvent {
133                time: 10.0,
134                frame: 1,
135                scoring_team_is_team_0: true,
136                player: None,
137                team_zero_score: None,
138                team_one_score: None,
139            }],
140            ..Default::default()
141        };
142
143        let state = tracker.state_parts(&gameplay, &events);
144
145        assert_eq!(state.gameplay_phase, GameplayPhase::PostGoal);
146        assert!(!state.is_live_play);
147    }
148}