fbsim_core/game/play/
context.rs

1#![doc = include_str!("../../../docs/game/play/context.md")]
2#[cfg(feature = "rocket_okapi")]
3use rocket_okapi::okapi::schemars;
4#[cfg(feature = "rocket_okapi")]
5use rocket_okapi::okapi::schemars::JsonSchema;
6use serde::{Serialize, Deserialize};
7
8use crate::game::context::GameContext;
9
10/// # `PlayContext` struct
11///
12/// A `PlayContext` represents a play scenario
13#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
14#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Default, Serialize, Deserialize)]
15pub struct PlayContext {
16    quarter: u32,
17    half_seconds: u32,
18    down: u32,
19    distance: u32,
20    yard_line: u32,
21    score_diff: i32,
22    off_timeouts: u32,
23    def_timeouts: u32,
24    clock_running: bool
25}
26
27impl From<&GameContext> for PlayContext {
28    /// Initialize a PlayContext from a borrowed GameContext
29    ///
30    /// ### Example
31    /// ```
32    /// use fbsim_core::game::context::GameContext;
33    /// use fbsim_core::game::play::context::PlayContext;
34    /// 
35    /// let game_context = GameContext::new();
36    /// let play_context = PlayContext::from(&game_context);
37    /// ```
38    fn from(item: &GameContext) -> PlayContext {
39        // Determine score diff and timeouts based on possession
40        let score_diff: i32 = if item.home_possession() {
41            item.home_score() as i32 - item.away_score() as i32
42        } else {
43            item.away_score() as i32 - item.home_score() as i32
44        };
45        let off_timeouts: u32 = if item.home_possession() {
46            item.home_timeouts()
47        } else {
48            item.away_timeouts()
49        };
50        let def_timeouts: u32 = if item.home_possession() {
51            item.away_timeouts()
52        } else {
53            item.home_timeouts()
54        };
55
56        // Determine yard line based on possession and direction
57        let yard_line: u32 = if item.home_possession() ^ item.home_positive_direction() {
58            u32::try_from(100_i32 - item.yard_line() as i32).unwrap_or_default()
59        } else {
60            item.yard_line()
61        };
62
63        // Construct the play context
64        PlayContext{
65            quarter: item.quarter(),
66            half_seconds: item.half_seconds(),
67            down: item.down(),
68            distance: item.distance(),
69            yard_line,
70            score_diff,
71            off_timeouts,
72            def_timeouts,
73            clock_running: item.clock_running()
74        }
75    }
76}
77
78impl PlayContext {
79    /// Whether the clock is running
80    ///
81    /// ### Example
82    /// ```
83    /// use fbsim_core::game::context::GameContext;
84    /// use fbsim_core::game::play::context::PlayContext;
85    /// 
86    /// let game_context = GameContext::new();
87    /// let play_context = PlayContext::from(&game_context);
88    /// let clock_running = play_context.clock_running();
89    /// assert!(!clock_running);
90    /// ```
91    pub fn clock_running(&self) -> bool {
92        self.clock_running
93    }
94
95    /// Gets the current down
96    ///
97    /// ### Example
98    /// ```
99    /// use fbsim_core::game::context::GameContext;
100    /// use fbsim_core::game::play::context::PlayContext;
101    /// 
102    /// let game_context = GameContext::new();
103    /// let play_context = PlayContext::from(&game_context);
104    /// let down = play_context.down();
105    /// assert!(down == 0);
106    /// ```
107    pub fn down(&self) -> u32 {
108        self.down
109    }
110
111    /// Gets the current distance
112    ///
113    /// ### Example
114    /// ```
115    /// use fbsim_core::game::context::GameContext;
116    /// use fbsim_core::game::play::context::PlayContext;
117    /// 
118    /// let game_context = GameContext::new();
119    /// let play_context = PlayContext::from(&game_context);
120    /// let distance = play_context.distance();
121    /// assert!(distance == 10);
122    /// ```
123    pub fn distance(&self) -> u32 {
124        self.distance
125    }
126
127    /// Gets the current yard line
128    ///
129    /// ### Example
130    /// ```
131    /// use fbsim_core::game::context::GameContext;
132    /// use fbsim_core::game::play::context::PlayContext;
133    /// 
134    /// let game_context = GameContext::new();
135    /// let play_context = PlayContext::from(&game_context);
136    /// let yard_line = play_context.yard_line();
137    /// assert!(yard_line == 35);
138    /// ```
139    pub fn yard_line(&self) -> u32 {
140        self.yard_line
141    }
142
143    /// Gets the number of timeouts the offense has
144    ///
145    /// ### Example
146    /// ```
147    /// use fbsim_core::game::context::GameContext;
148    /// use fbsim_core::game::play::context::PlayContext;
149    /// 
150    /// let game_context = GameContext::new();
151    /// let play_context = PlayContext::from(&game_context);
152    /// let offense_timeouts = play_context.offense_timeouts();
153    /// assert!(offense_timeouts == 3);
154    /// ```
155    pub fn offense_timeouts(&self) -> u32 {
156        self.off_timeouts
157    }
158
159    /// Gets the number of timeouts the defense has
160    ///
161    /// ### Example
162    /// ```
163    /// use fbsim_core::game::context::GameContext;
164    /// use fbsim_core::game::play::context::PlayContext;
165    /// 
166    /// let game_context = GameContext::new();
167    /// let play_context = PlayContext::from(&game_context);
168    /// let defense_timeouts = play_context.defense_timeouts();
169    /// assert!(defense_timeouts == 3);
170    /// ```
171    pub fn defense_timeouts(&self) -> u32 {
172        self.def_timeouts
173    }
174
175    /// Gets the current quarter
176    ///
177    /// ### Example
178    /// ```
179    /// use fbsim_core::game::context::GameContext;
180    /// use fbsim_core::game::play::context::PlayContext;
181    /// 
182    /// let game_context = GameContext::new();
183    /// let play_context = PlayContext::from(&game_context);
184    /// let quarter = play_context.quarter();
185    /// assert!(quarter == 1);
186    /// ```
187    pub fn quarter(&self) -> u32 {
188        self.quarter
189    }
190
191    /// Whether this is a drain-clock scenario for the offense
192    ///
193    /// ### Example
194    /// ```
195    /// use fbsim_core::game::context::GameContext;
196    /// use fbsim_core::game::play::context::PlayContext;
197    /// 
198    /// let game_context = GameContext::new();
199    /// let play_context = PlayContext::from(&game_context);
200    /// let drain_clock = play_context.drain_clock();
201    /// assert!(!drain_clock);
202    /// ```
203    pub fn drain_clock(&self) -> bool {
204        if self.score_diff <= 0 {
205            return false
206        }
207        let scores_up_by: f32 = self.score_diff as f32 / 8_f32;
208        let drain_threshold_sig: i32 = (scores_up_by * 4_f32 * 60_f32) as i32;
209        let drain_threshold: u32 = u32::try_from(drain_threshold_sig).unwrap_or_default();
210        if self.quarter >= 4 && self.half_seconds < drain_threshold {
211            return true
212        }
213        false
214    }
215
216    /// Whether this is an up-tempo scenario for the offense
217    ///
218    /// ### Example
219    /// ```
220    /// use fbsim_core::game::context::GameContext;
221    /// use fbsim_core::game::play::context::PlayContext;
222    /// 
223    /// let game_context = GameContext::new();
224    /// let play_context = PlayContext::from(&game_context);
225    /// let up_tempo = play_context.up_tempo();
226    /// assert!(!up_tempo);
227    /// ```
228    pub fn up_tempo(&self) -> bool {
229        self.quarter >= 4 && self.half_seconds <= 180 &&
230        self.score_diff < 0 && self.score_diff >= -17
231    }
232
233    /// Whether this is a critical down scenario
234    ///
235    /// ### Example
236    /// ```
237    /// use fbsim_core::game::context::GameContext;
238    /// use fbsim_core::game::play::context::PlayContext;
239    /// 
240    /// let game_context = GameContext::new();
241    /// let play_context = PlayContext::from(&game_context);
242    /// let critical_down = play_context.critical_down();
243    /// assert!(!critical_down);
244    /// ```
245    pub fn critical_down(&self) -> bool {
246        self.down == 3 && self.half_seconds <= 180 &&
247        self.score_diff < 9 && self.score_diff > -9
248    }
249
250    /// Whether this is a conserve-clock scenario for the offense
251    ///
252    /// ### Example
253    /// ```
254    /// use fbsim_core::game::context::GameContext;
255    /// use fbsim_core::game::play::context::PlayContext;
256    /// 
257    /// let game_context = GameContext::new();
258    /// let play_context = PlayContext::from(&game_context);
259    /// let conserve_clock = play_context.offense_conserve_clock();
260    /// assert!(!conserve_clock);
261    /// ```
262    pub fn offense_conserve_clock(&self) -> bool {
263        self.quarter >= 4 && self.half_seconds <= 180 &&
264        self.score_diff < 0 && self.score_diff > -18
265    }
266
267    /// Whether this is a conserve-clock scenario for the defense
268    ///
269    /// ### Example
270    /// ```
271    /// use fbsim_core::game::context::GameContext;
272    /// use fbsim_core::game::play::context::PlayContext;
273    /// 
274    /// let game_context = GameContext::new();
275    /// let play_context = PlayContext::from(&game_context);
276    /// let conserve_clock = play_context.defense_conserve_clock();
277    /// assert!(!conserve_clock);
278    /// ```
279    pub fn defense_conserve_clock(&self) -> bool {
280        self.quarter >= 4 && self.half_seconds <= 180 &&
281        self.score_diff > 0 && self.score_diff < 18
282    }
283
284    /// Whether this is the last play
285    ///
286    /// ### Example
287    /// ```
288    /// use fbsim_core::game::context::GameContext;
289    /// use fbsim_core::game::play::context::PlayContext;
290    /// 
291    /// let game_context = GameContext::new();
292    /// let play_context = PlayContext::from(&game_context);
293    /// let last_play = play_context.last_play();
294    /// assert!(!last_play);
295    /// ```
296    pub fn last_play(&self) -> bool {
297        self.half_seconds < 6
298    }
299
300    /// Whether the offense needs a touchdown on the last play
301    ///
302    /// ### Example
303    /// ```
304    /// use fbsim_core::game::context::GameContext;
305    /// use fbsim_core::game::play::context::PlayContext;
306    /// 
307    /// let game_context = GameContext::new();
308    /// let play_context = PlayContext::from(&game_context);
309    /// let last_play_need_td = play_context.last_play_need_td();
310    /// assert!(!last_play_need_td);
311    /// ```
312    pub fn last_play_need_td(&self) -> bool {
313        self.score_diff < -3
314    }
315
316    /// Whether the offense can kneel to end the game
317    ///
318    /// ### Example
319    /// ```
320    /// use fbsim_core::game::context::GameContext;
321    /// use fbsim_core::game::play::context::PlayContext;
322    /// 
323    /// let game_context = GameContext::new();
324    /// let play_context = PlayContext::from(&game_context);
325    /// let can_kneel = play_context.can_kneel();
326    /// assert!(!can_kneel);
327    /// ```
328    pub fn can_kneel(&self) -> bool {
329        let downs_remaining = 4 - self.down;
330        let runoff_seconds = 42 * downs_remaining.checked_sub(self.def_timeouts).unwrap_or_default();
331        runoff_seconds >= self.half_seconds
332    }
333
334    /// Whether this is a must-score scenario for 4th-down playcalling
335    ///
336    /// ### Example
337    /// ```
338    /// use fbsim_core::game::context::GameContext;
339    /// use fbsim_core::game::play::context::PlayContext;
340    /// 
341    /// let game_context = GameContext::new();
342    /// let play_context = PlayContext::from(&game_context);
343    /// let must_score = play_context.must_score();
344    /// assert!(!must_score);
345    /// ```
346    pub fn must_score(&self) -> bool {
347        if self.score_diff >= 0 {
348            return false
349        }
350        let timeout_drive_time = (42 * (3 - self.off_timeouts)) + 8;
351        if self.half_seconds <= timeout_drive_time {
352            return true
353        }
354        let non_timeout_drive_time = (42 * 3) + 8;
355        let timeout_drives_remaining: u32 = 1;
356        let non_timeout_drive_time_remaining = self.half_seconds.checked_sub(timeout_drive_time).unwrap_or_default();
357        let non_timeout_drives_remaining = (
358            non_timeout_drive_time_remaining as f32 / non_timeout_drive_time as f32
359        ).ceil() as u32;
360        let scores_needed = (self.score_diff as f32 / 8_f32).round().abs() as u32;
361        let drives_remaining = timeout_drives_remaining + non_timeout_drives_remaining;
362        drives_remaining <= scores_needed
363    }
364
365    /// Whether this is a go for it on 4th scenario
366    ///
367    /// ### Example
368    /// ```
369    /// use fbsim_core::game::context::GameContext;
370    /// use fbsim_core::game::play::context::PlayContext;
371    /// 
372    /// let game_context = GameContext::new();
373    /// let play_context = PlayContext::from(&game_context);
374    /// let can_go_for_it = play_context.can_go_for_it();
375    /// assert!(!can_go_for_it);
376    /// ```
377    pub fn can_go_for_it(&self) -> bool {
378        self.distance <= 4 && (
379            self.yard_line >= 80 ||
380            (self.yard_line >= 40 && self.yard_line <= 60)
381        )
382    }
383
384    /// Whether the offense is in field goal range
385    ///
386    /// ### Example
387    /// ```
388    /// use fbsim_core::game::context::GameContext;
389    /// use fbsim_core::game::play::context::PlayContext;
390    /// 
391    /// let game_context = GameContext::new();
392    /// let play_context = PlayContext::from(&game_context);
393    /// let in_field_goal_range = play_context.in_field_goal_range();
394    /// assert!(!in_field_goal_range);
395    /// ```
396    pub fn in_field_goal_range(&self) -> bool {
397        self.yard_line >= 45
398    }
399}
400
401impl std::fmt::Display for PlayContext {
402    /// Format a `PlayContext` as a string.
403    ///
404    /// ### Example
405    ///
406    /// ```
407    /// use fbsim_core::game::context::GameContext;
408    /// use fbsim_core::game::play::context::PlayContext;
409    ///
410    /// // Initialize a play context and display it
411    /// let game_context = GameContext::new();
412    /// let play_context = PlayContext::from(&game_context);
413    /// println!("{}", play_context);
414    /// ```
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        // Format the clock
417        let clock_total = if self.half_seconds < 900 || 
418            (self.half_seconds == 900 && self.quarter.is_multiple_of(2) && self.quarter <= 4) {
419            self.half_seconds
420        } else {
421            self.half_seconds - 900
422        };
423        let clock_mins = clock_total / 60;
424        let clock_secs = clock_total - (clock_mins * 60);
425        let clock_secs_str = if clock_secs < 10 {
426            format!("0{}", clock_secs)
427        } else {
428            format!("{}", clock_secs)
429        };
430        let clock_str = format!("{}:{}", clock_mins, &clock_secs_str);
431
432        // Format the quarter
433        let quarter_str = if self.quarter <= 4 {
434            format!("{}Q", self.quarter)
435        } else {
436            let num_ot = self.quarter - 4;
437            format!("{}OT", num_ot)
438        };
439
440        // Format the down & distance
441        let down_suf = match self.down {
442            1 => "st",
443            2 => "nd",
444            3 => "rd",
445            _ => "th"
446        };
447        let down_dist_str = if self.yard_line + self.distance >= 100 {
448            format!("{}{} & goal", self.down, down_suf)
449        } else {
450            format!("{}{} & {}", self.down, down_suf, self.distance)
451        };
452
453        // Format the yard line
454        let (yard, side_of_field) = if self.yard_line < 50 {
455            (self.yard_line, "OWN")
456        } else {
457            (100 - self.yard_line, "OPP")
458        };
459        let yard_str = format!("{} {}", side_of_field, yard);
460
461        // Format the play context
462        let context_str = format!(
463            "[{} {}] {} at {}",
464            clock_str,
465            quarter_str,
466            down_dist_str,
467            yard_str
468        );
469        f.write_str(&context_str)
470    }
471}