fbsim_core/game/
context.rs

1#![doc = include_str!("../../docs/game/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, Deserializer};
7
8use crate::game::play::context::PlayContext;
9use crate::game::play::result::{ScoreResult, PlayResult};
10
11/// # `GameContextRaw` struct
12///
13/// A `GameContextRaw` is a `GameContext` before its properties have been
14/// validated
15#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
16#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
17pub struct GameContextRaw {
18    home_team_short: String,
19    away_team_short: String,
20    quarter: u32,
21    half_seconds: u32,
22    down: u32,
23    distance: u32,
24    yard_line: u32,
25    home_score: u32,
26    away_score: u32,
27    home_timeouts: u32,
28    away_timeouts: u32,
29    home_positive_direction: bool,
30    home_opening_kickoff: bool,
31    home_possession: bool,
32    last_play_turnover: bool,
33    last_play_incomplete: bool,
34    last_play_out_of_bounds: bool,
35    last_play_timeout: bool,
36    last_play_kickoff: bool,
37    last_play_punt: bool,
38    next_play_extra_point: bool,
39    next_play_kickoff: bool,
40    end_of_half: bool,
41    game_over: bool
42}
43
44impl GameContextRaw {
45    pub fn validate(&self) -> Result<(), String> {
46        // Ensure each team acronym is no longer than 4 characters
47        if self.home_team_short.len() > 4 {
48            return Err(
49                format!(
50                    "Home team short name is longer than 4 characters: {}",
51                    self.home_team_short
52                )
53            )
54        }
55        if self.away_team_short.len() > 4 {
56            return Err(
57                format!(
58                    "Away team short name is longer than 4 characters: {}",
59                    self.away_team_short
60                )
61            )
62        }
63
64        // Ensure half seconds is no greater than 1800 (15 mins)
65        if self.half_seconds > 1800 {
66            return Err(
67                format!(
68                    "Half seconds is not in range [0, 1800]: {}",
69                    self.half_seconds
70                )
71            )
72        }
73
74        // Ensure half seconds is not less than 900 if quarter is odd and less than 4
75        if self.half_seconds < 900 && self.quarter % 2 == 1 && self.quarter < 4 {
76            return Err(
77                format!(
78                    "Half seconds is not in range [900, 1800] for quarter {}: {}",
79                    self.quarter,
80                    self.half_seconds
81                )
82            )
83        }
84
85        // Ensure half seconds is not greater than 900 if quarter is even or greater than 4
86        if self.half_seconds > 900 && (self.quarter.is_multiple_of(2) || self.quarter > 4) {
87            return Err(
88                format!(
89                    "Half seconds is not in range [0, 900] for quarter {}: {}",
90                    self.quarter,
91                    self.half_seconds
92                )
93            )
94        }
95
96        // Ensure down is no greater than 4
97        if self.down > 4 {
98            return Err(
99                format!(
100                    "Down is not in range [0, 4]: {}",
101                    self.down
102                )
103            )
104        }
105
106        // Ensure yard line is no greater than 100
107        if self.yard_line > 100 {
108            return Err(
109                format!(
110                    "Yard line is not in range [0, 100]: {}",
111                    self.yard_line
112                )
113            )
114        }
115
116        // Ensure distance is no greater than the remaining yards
117        let remaining_yards: u32 = if self.home_possession ^ self.home_positive_direction {
118            self.yard_line
119        } else {
120            100_u32 - self.yard_line
121        };
122        if self.distance > remaining_yards {
123            return Err(
124                format!(
125                    "Distance was greater than yards remaining to touchdown: {} > {}",
126                    self.distance,
127                    remaining_yards
128                )
129            )
130        }
131
132        // Ensure home and away timeouts are no greater than 3
133        if self.home_timeouts > 3 {
134            return Err(
135                format!(
136                    "Home timeouts is not in range [0, 3]: {}",
137                    self.home_timeouts
138                )
139            )
140        }
141        if self.away_timeouts > 3 {
142            return Err(
143                format!(
144                    "Away timeouts is not in range [0, 3]: {}",
145                    self.away_timeouts
146                )
147            )
148        }
149
150        // Ensure no invalid last play scenarios
151        if self.last_play_incomplete && self.last_play_out_of_bounds {
152            return Err(
153                String::from("Invalid combination of last play scenarios: Incomplete & out of bounds")
154            )
155        }
156        if self.last_play_kickoff && self.last_play_timeout {
157            return Err(
158                String::from("Invalid combination of last play scenarios: Kickoff & timeout")
159            )
160        }
161        if self.last_play_punt && self.last_play_timeout {
162            return Err(
163                String::from("Invalid combination of last play scenarios: Punt & timeout")
164            )
165        }
166        if self.last_play_punt && self.last_play_kickoff {
167            return Err(
168                String::from("Invalid combination of last play scenarios: Punt & kickoff")
169            )
170        }
171
172        // Ensure no invalid next play scenarios
173        if self.next_play_extra_point && self.next_play_kickoff {
174            return Err(
175                String::from("Invalid combination of next play scenarios: Kickoff & extra point")
176            )
177        }
178
179        // Ensure half is not over if quarter is odd and less than 4
180        if self.end_of_half && (self.quarter == 1 || (self.quarter == 3 && self.half_seconds < 1800)) {
181            return Err(
182                format!(
183                    "Cannot end half during quarter: {}",
184                    self.quarter
185                )
186            )
187        }
188
189        // Ensure half is not over if there is still time left
190        if self.end_of_half && self.half_seconds != 1800 && self.half_seconds != 600 && self.half_seconds > 0 {
191            return Err(
192                format!(
193                    "End of half but nonzero half seconds: {}",
194                    self.half_seconds
195                )
196            )
197        }
198
199        // Ensure game is not over if quarter is less than 4
200        if self.game_over && self.quarter < 4 {
201            return Err(
202                format!(
203                    "Cannot end game during quarter: {}",
204                    self.quarter
205                )
206            )
207        }
208
209        // Ensure game is not over if there is still time left
210        if self.game_over && self.half_seconds > 0 {
211            return Err(
212                format!(
213                    "End of game but nonzero half seconds: {}",
214                    self.half_seconds
215                )
216            )
217        }
218        Ok(())
219    }
220}
221
222/// # `GameContextUpdateOptions` struct
223///
224/// A `GameContextUpdateOptions` contains the parameters required to derive
225/// the next game context
226#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Default)]
227pub struct GameContextUpdateOptions {
228    pub duration: u32,
229    pub net_yards: i32,
230    pub off_score: ScoreResult,
231    pub def_score: ScoreResult,
232    pub turnover: bool,
233    pub touchback: bool,
234    pub kickoff_oob: bool,
235    pub off_timeout: bool,
236    pub def_timeout: bool,
237    pub next_play_extra_point: bool,
238    pub between_play: bool,
239    pub end_of_game: bool
240}
241
242/// # `GameContext` struct
243///
244/// A `GameContext` represents a game scenario
245#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
246#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize)]
247pub struct GameContext {
248    home_team_short: String,
249    away_team_short: String,
250    quarter: u32,
251    half_seconds: u32,
252    down: u32,
253    distance: u32,
254    yard_line: u32,
255    home_score: u32,
256    away_score: u32,
257    home_timeouts: u32,
258    away_timeouts: u32,
259    home_positive_direction: bool,
260    home_opening_kickoff: bool,
261    home_possession: bool,
262    last_play_turnover: bool,
263    last_play_incomplete: bool,
264    last_play_out_of_bounds: bool,
265    last_play_timeout: bool,
266    last_play_kickoff: bool,
267    last_play_punt: bool,
268    next_play_extra_point: bool,
269    next_play_kickoff: bool,
270    end_of_half: bool,
271    game_over: bool
272}
273
274impl Default for GameContext {
275    /// Default constructor for the GameContext class
276    ///
277    /// ### Example
278    /// ```
279    /// use fbsim_core::game::context::GameContext;
280    /// 
281    /// let my_context = GameContext::default();
282    /// ```
283    fn default() -> Self {
284        GameContext {
285            home_team_short: String::from("HOME"),
286            away_team_short: String::from("AWAY"),
287            quarter: 1,
288            half_seconds: 1800,
289            down: 0,
290            distance: 10,
291            yard_line: 35,
292            home_score: 0,
293            away_score: 0,
294            home_timeouts: 3,
295            away_timeouts: 3,
296            home_positive_direction: true,
297            home_opening_kickoff: true,
298            home_possession: true,
299            last_play_turnover: false,
300            last_play_incomplete: false,
301            last_play_out_of_bounds: false,
302            last_play_timeout: false,
303            last_play_kickoff: false,
304            last_play_punt: false,
305            next_play_extra_point: false,
306            next_play_kickoff: true,
307            end_of_half: false,
308            game_over: false
309        }
310    }
311}
312
313impl TryFrom<GameContextRaw> for GameContext {
314    type Error = String;
315
316    fn try_from(item: GameContextRaw) -> Result<Self, Self::Error> {
317        // Validate the raw game context
318        match item.validate() {
319            Ok(()) => (),
320            Err(error) => return Err(error),
321        };
322
323        // If valid, then convert
324        Ok(
325            GameContext{
326                home_team_short: item.home_team_short,
327                away_team_short: item.away_team_short,
328                quarter: item.quarter,
329                half_seconds: item.half_seconds,
330                down: item.down,
331                distance: item.distance,
332                yard_line: item.yard_line,
333                home_score: item.home_score,
334                away_score: item.away_score,
335                home_timeouts: item.home_timeouts,
336                away_timeouts: item.away_timeouts,
337                home_positive_direction: item.home_positive_direction,
338                home_opening_kickoff: item.home_opening_kickoff,
339                home_possession: item.home_possession,
340                last_play_turnover: item.last_play_turnover,
341                last_play_incomplete: item.last_play_incomplete,
342                last_play_out_of_bounds: item.last_play_out_of_bounds,
343                last_play_timeout: item.last_play_timeout,
344                last_play_kickoff: item.last_play_kickoff,
345                last_play_punt: item.last_play_punt,
346                next_play_extra_point: item.next_play_extra_point,
347                next_play_kickoff: item.next_play_kickoff,
348                end_of_half: item.end_of_half,
349                game_over: item.game_over
350            }
351        )
352    }
353}
354
355impl<'de> Deserialize<'de> for GameContext {
356    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
357    where
358        D: Deserializer<'de>,
359    {
360        // Only deserialize if the conversion from raw succeeds
361        let raw = GameContextRaw::deserialize(deserializer)?;
362        GameContext::try_from(raw).map_err(serde::de::Error::custom)
363    }
364}
365
366impl GameContext {
367    /// Constructor for the GameContext class where properties are defaulted
368    ///
369    /// ### Example
370    /// ```
371    /// use fbsim_core::game::context::GameContext;
372    /// 
373    /// let my_context = GameContext::new();
374    /// ```
375    pub fn new() -> GameContext {
376        GameContext::default()
377    }
378
379    /// Borrow the GameContext home team short property
380    ///
381    /// ### Example
382    /// ```
383    /// use fbsim_core::game::context::GameContext;
384    /// 
385    /// let my_context = GameContext::new();
386    /// let home_team_short = my_context.home_team_short();
387    /// assert!(home_team_short == "HOME");
388    /// ```
389    pub fn home_team_short(&self) -> &str {
390        &self.home_team_short
391    }
392
393    /// Borrow the GameContext away team short property
394    ///
395    /// ### Example
396    /// ```
397    /// use fbsim_core::game::context::GameContext;
398    /// 
399    /// let my_context = GameContext::new();
400    /// let away_team_short = my_context.away_team_short();
401    /// assert!(away_team_short == "AWAY");
402    /// ```
403    pub fn away_team_short(&self) -> &str {
404        &self.away_team_short
405    }
406
407    /// Borrow the GameContext quarter property
408    ///
409    /// ### Example
410    /// ```
411    /// use fbsim_core::game::context::GameContext;
412    /// 
413    /// let my_context = GameContext::new();
414    /// let quarter = my_context.quarter();
415    /// assert!(quarter == 1);
416    /// ```
417    pub fn quarter(&self) -> u32 {
418        self.quarter
419    }
420
421    /// Borrow the GameContext half_seconds property
422    ///
423    /// ### Example
424    /// ```
425    /// use fbsim_core::game::context::GameContext;
426    /// 
427    /// let my_context = GameContext::new();
428    /// let half_seconds = my_context.half_seconds();
429    /// assert!(half_seconds == 1800);
430    /// ```
431    pub fn half_seconds(&self) -> u32 {
432        self.half_seconds
433    }
434
435    /// Borrow the GameContext down property
436    ///
437    /// ### Example
438    /// ```
439    /// use fbsim_core::game::context::GameContext;
440    /// 
441    /// let my_context = GameContext::new();
442    /// let down = my_context.down();
443    /// assert!(down == 0);
444    /// ```
445    pub fn down(&self) -> u32 {
446        self.down
447    }
448
449    /// Borrow the GameContext distance property
450    ///
451    /// ### Example
452    /// ```
453    /// use fbsim_core::game::context::GameContext;
454    /// 
455    /// let my_context = GameContext::new();
456    /// let distance = my_context.distance();
457    /// assert!(distance == 10);
458    /// ```
459    pub fn distance(&self) -> u32 {
460        self.distance
461    }
462
463    /// Borrow the GameContext yard_line property
464    ///
465    /// ### Example
466    /// ```
467    /// use fbsim_core::game::context::GameContext;
468    /// 
469    /// let my_context = GameContext::new();
470    /// let yard_line = my_context.yard_line();
471    /// assert!(yard_line == 35);
472    /// ```
473    pub fn yard_line(&self) -> u32 {
474        self.yard_line
475    }
476
477    /// Borrow the GameContext home_score property
478    ///
479    /// ### Example
480    /// ```
481    /// use fbsim_core::game::context::GameContext;
482    /// 
483    /// let my_context = GameContext::new();
484    /// let home_score = my_context.home_score();
485    /// assert!(home_score == 0);
486    /// ```
487    pub fn home_score(&self) -> u32 {
488        self.home_score
489    }
490
491    /// Borrow the GameContext away_score property
492    ///
493    /// ### Example
494    /// ```
495    /// use fbsim_core::game::context::GameContext;
496    /// 
497    /// let my_context = GameContext::new();
498    /// let away_score = my_context.away_score();
499    /// assert!(away_score == 0);
500    /// ```
501    pub fn away_score(&self) -> u32 {
502        self.away_score
503    }
504
505    /// Borrow the GameContext home_timeouts property
506    ///
507    /// ### Example
508    /// ```
509    /// use fbsim_core::game::context::GameContext;
510    /// 
511    /// let my_context = GameContext::new();
512    /// let home_timeouts = my_context.home_timeouts();
513    /// assert!(home_timeouts == 3);
514    /// ```
515    pub fn home_timeouts(&self) -> u32 {
516        self.home_timeouts
517    }
518
519    /// Borrow the GameContext away_timeouts property
520    ///
521    /// ### Example
522    /// ```
523    /// use fbsim_core::game::context::GameContext;
524    /// 
525    /// let my_context = GameContext::new();
526    /// let away_timeouts = my_context.away_timeouts();
527    /// assert!(away_timeouts == 3);
528    /// ```
529    pub fn away_timeouts(&self) -> u32 {
530        self.away_timeouts
531    }
532
533    /// Borrow the GameContext home_possession property
534    ///
535    /// ### Example
536    /// ```
537    /// use fbsim_core::game::context::GameContext;
538    /// 
539    /// let my_context = GameContext::new();
540    /// let home_possession = my_context.home_possession();
541    /// assert!(home_possession);
542    /// ```
543    pub fn home_possession(&self) -> bool {
544        self.home_possession
545    }
546
547    /// Borrow the GameContext home_positive_direction property
548    ///
549    /// ### Example
550    /// ```
551    /// use fbsim_core::game::context::GameContext;
552    /// 
553    /// let my_context = GameContext::new();
554    /// let home_positive_direction = my_context.home_positive_direction();
555    /// assert!(home_positive_direction);
556    /// ```
557    pub fn home_positive_direction(&self) -> bool {
558        self.home_positive_direction
559    }
560
561    /// Borrow the GameContext home_opening_kickoff property
562    ///
563    /// ### Example
564    /// ```
565    /// use fbsim_core::game::context::GameContext;
566    /// 
567    /// let my_context = GameContext::new();
568    /// let home_opening_kickoff = my_context.home_opening_kickoff();
569    /// assert!(home_opening_kickoff);
570    /// ```
571    pub fn home_opening_kickoff(&self) -> bool {
572        self.home_opening_kickoff
573    }
574
575    /// Borrow the GameContext last_play_turnover property
576    ///
577    /// ### Example
578    /// ```
579    /// use fbsim_core::game::context::GameContext;
580    /// 
581    /// let my_context = GameContext::new();
582    /// let last_play_turnover = my_context.last_play_turnover();
583    /// assert!(!last_play_turnover);
584    /// ```
585    pub fn last_play_turnover(&self) -> bool {
586        self.last_play_turnover
587    }
588
589    /// Borrow the GameContext last_play_incomplete property
590    ///
591    /// ### Example
592    /// ```
593    /// use fbsim_core::game::context::GameContext;
594    /// 
595    /// let my_context = GameContext::new();
596    /// let last_play_incomplete = my_context.last_play_incomplete();
597    /// assert!(!last_play_incomplete);
598    /// ```
599    pub fn last_play_incomplete(&self) -> bool {
600        self.last_play_incomplete
601    }
602
603    /// Borrow the GameContext last_play_out_of_bounds property
604    ///
605    /// ### Example
606    /// ```
607    /// use fbsim_core::game::context::GameContext;
608    /// 
609    /// let my_context = GameContext::new();
610    /// let last_play_out_of_bounds = my_context.last_play_out_of_bounds();
611    /// assert!(!last_play_out_of_bounds);
612    /// ```
613    pub fn last_play_out_of_bounds(&self) -> bool {
614        self.last_play_out_of_bounds
615    }
616
617    /// Borrow the GameContext last_play_kickoff property
618    ///
619    /// ### Example
620    /// ```
621    /// use fbsim_core::game::context::GameContext;
622    /// 
623    /// let my_context = GameContext::new();
624    /// let last_play_kickoff = my_context.last_play_kickoff();
625    /// assert!(!last_play_kickoff);
626    /// ```
627    pub fn last_play_kickoff(&self) -> bool {
628        self.last_play_kickoff
629    }
630
631    /// Borrow the GameContext last_play_punt property
632    ///
633    /// ### Example
634    /// ```
635    /// use fbsim_core::game::context::GameContext;
636    /// 
637    /// let my_context = GameContext::new();
638    /// let last_play_punt = my_context.last_play_punt();
639    /// assert!(!last_play_punt);
640    /// ```
641    pub fn last_play_punt(&self) -> bool {
642        self.last_play_punt
643    }
644
645    /// Borrow the GameContext last_play_timeout property
646    ///
647    /// ### Example
648    /// ```
649    /// use fbsim_core::game::context::GameContext;
650    /// 
651    /// let my_context = GameContext::new();
652    /// let last_play_timeout = my_context.last_play_timeout();
653    /// assert!(!last_play_timeout);
654    /// ```
655    pub fn last_play_timeout(&self) -> bool {
656        self.last_play_timeout
657    }
658
659    /// Borrow the GameContext next_play_kickoff property
660    ///
661    /// ### Example
662    /// ```
663    /// use fbsim_core::game::context::GameContext;
664    /// 
665    /// let my_context = GameContext::new();
666    /// let next_play_kickoff = my_context.next_play_kickoff();
667    /// assert!(next_play_kickoff);
668    /// ```
669    pub fn next_play_kickoff(&self) -> bool {
670        self.next_play_kickoff
671    }
672
673    /// Borrow the GameContext next_play_extra_point property
674    ///
675    /// ### Example
676    /// ```
677    /// use fbsim_core::game::context::GameContext;
678    /// 
679    /// let my_context = GameContext::new();
680    /// let next_play_extra_point = my_context.next_play_extra_point();
681    /// assert!(!next_play_extra_point);
682    /// ```
683    pub fn next_play_extra_point(&self) -> bool {
684        self.next_play_extra_point
685    }
686
687    /// Borrow the GameContext end_of_half property
688    ///
689    /// ### Example
690    /// ```
691    /// use fbsim_core::game::context::GameContext;
692    /// 
693    /// let my_context = GameContext::new();
694    /// let end_of_half = my_context.end_of_half();
695    /// assert!(!end_of_half);
696    /// ```
697    pub fn end_of_half(&self) -> bool {
698        self.end_of_half
699    }
700
701    /// Borrow the GameContext game_over property
702    ///
703    /// ### Example
704    /// ```
705    /// use fbsim_core::game::context::GameContext;
706    /// 
707    /// let my_context = GameContext::new();
708    /// let game_over = my_context.game_over();
709    /// assert!(!game_over);
710    /// ```
711    pub fn game_over(&self) -> bool {
712        self.game_over
713    }
714
715    /// Get the number of timeouts the defense has left
716    ///
717    /// ### Example
718    /// ```
719    /// use fbsim_core::game::context::GameContext;
720    /// 
721    /// let my_context = GameContext::new();
722    /// let defense_timeouts = my_context.defense_timeouts();
723    /// assert!(defense_timeouts == 3);
724    /// ```
725    pub fn defense_timeouts(&self) -> u32 {
726        if self.home_possession {
727            self.away_timeouts
728        } else {
729            self.home_timeouts
730        }
731    }
732
733    /// Get the number of timeouts the offense has left
734    ///
735    /// ### Example
736    /// ```
737    /// use fbsim_core::game::context::GameContext;
738    /// 
739    /// let my_context = GameContext::new();
740    /// let offense_timeouts = my_context.offense_timeouts();
741    /// assert!(offense_timeouts == 3);
742    /// ```
743    pub fn offense_timeouts(&self) -> u32 {
744        if self.home_possession {
745            self.home_timeouts
746        } else {
747            self.away_timeouts
748        }
749    }
750
751    /// Determine whether the clock is running
752    ///
753    /// ### Example
754    /// ```
755    /// use fbsim_core::game::context::GameContext;
756    /// 
757    /// let my_context = GameContext::new();
758    /// let clock_running = my_context.clock_running();
759    /// assert!(!clock_running);
760    /// ```
761    pub fn clock_running(&self) -> bool {
762        !(
763            self.last_play_incomplete || self.last_play_timeout || self.next_play_extra_point ||
764            self.next_play_kickoff || self.last_play_kickoff || self.last_play_punt || self.last_play_turnover ||
765            (
766                self.last_play_out_of_bounds && (
767                    (self.quarter == 2 && self.half_seconds < 120) ||
768                    (self.quarter >= 4 && self.half_seconds < 300)
769                )
770            )
771        )
772    }
773
774    /// Get the yards remaining until the defense's goal line
775    ///
776    /// ### Example
777    /// ```
778    /// use fbsim_core::game::context::GameContext;
779    /// 
780    /// let my_context = GameContext::new();
781    /// let yards_to_touchdown = my_context.yards_to_touchdown();
782    /// assert!(yards_to_touchdown == 65_i32);
783    /// ```
784    pub fn yards_to_touchdown(&self) -> i32 {
785        if self.home_possession ^ self.home_positive_direction {
786            self.yard_line as i32
787        } else {
788            100_i32 - self.yard_line as i32
789        }
790    }
791
792    /// Get the yards remaining until the offense's goal line
793    ///
794    /// ### Example
795    /// ```
796    /// use fbsim_core::game::context::GameContext;
797    /// 
798    /// let my_context = GameContext::new();
799    /// let yards_to_safety = my_context.yards_to_safety();
800    /// assert!(yards_to_safety == -35_i32);
801    /// ```
802    pub fn yards_to_safety(&self) -> i32 {
803        let safety_yards = if self.home_possession ^ self.home_positive_direction {
804            100_i32 - self.yard_line as i32
805        } else {
806            self.yard_line as i32
807        };
808        -safety_yards
809    }
810
811    /// Get the updated home score
812    ///
813    /// ### Example
814    /// ```
815    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
816    /// use fbsim_core::game::play::result::ScoreResult;
817    /// 
818    /// let mut update_opts = GameContextUpdateOptions::default();
819    /// update_opts.off_score = ScoreResult::Touchdown;
820    /// let my_context = GameContext::new();
821    /// let home_score = my_context.next_home_score(&update_opts);
822    /// assert!(home_score == 6);
823    /// ```
824    pub fn next_home_score(&self, update_opts: &GameContextUpdateOptions) -> u32 {
825        if self.home_possession {
826            self.home_score + update_opts.off_score.points()
827        } else {
828            self.home_score + update_opts.def_score.points()
829        }
830    }
831
832    /// Get the updated away score
833    ///
834    /// ### Example
835    /// ```
836    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
837    /// use fbsim_core::game::play::result::ScoreResult;
838    /// 
839    /// let mut update_opts = GameContextUpdateOptions::default();
840    /// update_opts.def_score = ScoreResult::Safety;
841    /// let my_context = GameContext::new();
842    /// let away_score = my_context.next_away_score(&update_opts);
843    /// assert!(away_score == 2);
844    /// ```
845    pub fn next_away_score(&self, update_opts: &GameContextUpdateOptions) -> u32 {
846        if self.home_possession {
847            self.away_score + update_opts.def_score.points()
848        } else {
849            self.away_score + update_opts.off_score.points()
850        }
851    }
852
853    /// Determine whether the score is tied given the scoring results from the last play
854    ///
855    /// ### Example
856    /// ```
857    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
858    /// use fbsim_core::game::play::result::ScoreResult;
859    /// 
860    /// let update_opts = GameContextUpdateOptions::default();
861    /// let my_context = GameContext::new();
862    /// let score_tied = my_context.next_score_tied(&update_opts);
863    /// assert!(score_tied);
864    /// ```
865    pub fn next_score_tied(&self, update_opts: &GameContextUpdateOptions) -> bool {
866        let next_home_score = self.next_home_score(update_opts);
867        let next_away_score = self.next_away_score(update_opts);
868        next_home_score == next_away_score
869    }
870
871    /// Get the updated half seconds
872    ///
873    /// ### Example
874    /// ```
875    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
876    /// use fbsim_core::game::play::result::ScoreResult;
877    /// 
878    /// let mut update_opts = GameContextUpdateOptions::default();
879    /// update_opts.duration = 10;
880    /// let my_context = GameContext::new();
881    /// let half_seconds = my_context.next_half_seconds(&update_opts);
882    /// assert!(half_seconds == 1790);
883    /// ```
884    pub fn next_half_seconds(&self, update_opts: &GameContextUpdateOptions) -> u32 {
885        let next_clock = u32::try_from(self.half_seconds as i32 - update_opts.duration as i32).unwrap_or_default();
886        let end_of_half = self.next_end_of_half(update_opts) || (self.end_of_half && update_opts.between_play);
887
888        // If end of quarter, max out at 900 seconds
889        if (self.quarter == 1 || self.quarter == 3) && self.half_seconds > 900 && next_clock <= 900 {
890            return 900;
891        }
892
893        // If end of half, return 0 seconds
894        if end_of_half && !(update_opts.between_play || update_opts.end_of_game) {
895            return 0;
896        }
897
898        // If start of second half, return to 1800 seconds
899        if (end_of_half && update_opts.between_play && self.quarter < 4) || (self.end_of_half && self.quarter == 2) {
900            return 1800;
901        }
902
903        // Check if end of game
904        if self.quarter >= 4 && next_clock == 0 {
905            if !self.next_score_tied(update_opts) {
906                // If end of game, max out at 0 seconds
907                return 0;
908            } else {
909                // If overtime, return to 600 seconds
910                return 600;
911            }
912        }
913        next_clock
914    }
915
916    /// Get the updated end of half property
917    ///
918    /// ### Example
919    /// ```
920    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
921    /// use fbsim_core::game::play::result::ScoreResult;
922    /// 
923    /// let mut update_opts = GameContextUpdateOptions::default();
924    /// update_opts.duration = 10;
925    /// let my_context = GameContext::new();
926    /// let end_of_half = my_context.next_end_of_half(&update_opts);
927    /// assert!(!end_of_half);
928    /// ```
929    pub fn next_end_of_half(&self, update_opts: &GameContextUpdateOptions) -> bool {
930        let next_clock = u32::try_from(self.half_seconds as i32 - update_opts.duration as i32).unwrap_or_default();
931
932        // Check if end of half
933        if next_clock == 0 && (self.quarter == 2 || self.quarter >=4) &&
934            !(update_opts.off_score == ScoreResult::Touchdown || update_opts.def_score == ScoreResult::Touchdown) {
935            return true;
936        }
937        false
938    }
939
940    /// Get the updated game over property
941    ///
942    /// ### Example
943    /// ```
944    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
945    /// use fbsim_core::game::play::result::ScoreResult;
946    /// 
947    /// let mut update_opts = GameContextUpdateOptions::default();
948    /// update_opts.duration = 10;
949    /// let my_context = GameContext::new();
950    /// let game_over = my_context.next_game_over(&update_opts);
951    /// assert!(!game_over);
952    /// ```
953    pub fn next_game_over(&self, update_opts: &GameContextUpdateOptions) -> bool {
954        let next_clock = u32::try_from(self.half_seconds as i32 - update_opts.duration as i32).unwrap_or_default();
955        self.quarter >= 4 && next_clock == 0 && !self.next_score_tied(update_opts)
956    }
957
958    /// Get the updated quarter
959    ///
960    /// ### Example
961    /// ```
962    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
963    /// use fbsim_core::game::play::result::ScoreResult;
964    /// 
965    /// let mut update_opts = GameContextUpdateOptions::default();
966    /// update_opts.duration = 10;
967    /// let my_context = GameContext::new();
968    /// let quarter = my_context.next_quarter(&update_opts);
969    /// assert!(quarter == 1);
970    /// ```
971    pub fn next_quarter(&self, update_opts: &GameContextUpdateOptions) -> u32 {
972        let next_clock = u32::try_from(self.half_seconds as i32 - update_opts.duration as i32).unwrap_or_default();
973
974        // Don't increment quarter if extra point still needs to be kicked
975        if update_opts.off_score == ScoreResult::Touchdown || update_opts.def_score == ScoreResult::Touchdown {
976            return self.quarter
977        }
978
979        // If end of 1st - 3rd quarter, increment quarter regardless
980        // If end of 4th - OT, increment quarter only if tied
981        if ((self.quarter == 1 || self.quarter == 3) && self.half_seconds >= 900 && next_clock <= 900) ||
982            (self.quarter == 2 && next_clock == 0) ||
983            (self.quarter >= 4 && next_clock == 0 && self.next_score_tied(update_opts)) {
984            return self.quarter + 1;
985        }
986        self.quarter
987    }
988
989    /// Get the updated home team direction
990    ///
991    /// ### Example
992    /// ```
993    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
994    /// use fbsim_core::game::play::result::ScoreResult;
995    /// 
996    /// let mut update_opts = GameContextUpdateOptions::default();
997    /// update_opts.duration = 10;
998    /// let my_context = GameContext::new();
999    /// let next_home_positive_direction = my_context.next_home_positive_direction(&update_opts);
1000    /// assert!(next_home_positive_direction);
1001    /// ```
1002    pub fn next_home_positive_direction(&self, update_opts: &GameContextUpdateOptions) -> bool {
1003        let qtr = self.next_quarter(update_opts);
1004        let end_of_half = self.next_end_of_half(update_opts) || (self.end_of_half && update_opts.between_play);
1005
1006        // Flip the field if end of quarter
1007        let home_dir = self.home_positive_direction;
1008        if self.quarter != qtr || end_of_half {
1009            return !home_dir;
1010        }
1011        home_dir
1012    }
1013
1014    /// Get the updated down
1015    ///
1016    /// ### Example
1017    /// ```
1018    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
1019    /// use fbsim_core::game::play::result::ScoreResult;
1020    /// 
1021    /// let mut update_opts = GameContextUpdateOptions::default();
1022    /// update_opts.net_yards = 10;
1023    /// update_opts.turnover = true;
1024    /// let my_context = GameContext::new();
1025    /// let down = my_context.next_down(&update_opts);
1026    /// assert!(down == 1);
1027    /// ```
1028    pub fn next_down(&self, update_opts: &GameContextUpdateOptions) -> u32 {
1029        let end_of_half = self.next_end_of_half(update_opts) || (self.end_of_half && update_opts.between_play);
1030
1031        // If this is the end of the half, next play is a kickoff
1032        if end_of_half {
1033            return 0;
1034        }
1035
1036        // If the result was for an extra point or 2 point conversion, next play is always a kickoff
1037        if self.next_play_extra_point {
1038            return 0;
1039        }
1040
1041        // If the result was for a kickoff, check if a score occurred
1042        if self.next_play_kickoff {
1043            if !(update_opts.off_score == ScoreResult::None && update_opts.def_score == ScoreResult::None) {
1044                return 0;
1045            }
1046            return 1;
1047        }
1048
1049        // If a touchdown, safety, or field goal occurred then next play is a down-0 play
1050        let off_zero_down = matches!(
1051            update_opts.off_score,
1052            ScoreResult::Touchdown | ScoreResult::FieldGoal | ScoreResult::Safety
1053        );
1054        let def_zero_down = matches!(
1055            update_opts.def_score,
1056            ScoreResult::Touchdown | ScoreResult::FieldGoal | ScoreResult::Safety
1057        );
1058        if off_zero_down || def_zero_down {
1059            return 0;
1060        }
1061
1062        // If a turnover occurred then next play is first down
1063        if update_opts.turnover {
1064            return 1;
1065        }
1066
1067        // Check if a first down was reached
1068        if update_opts.net_yards >= self.distance as i32 {
1069            return 1;
1070        }
1071
1072        // Increment the down and check for a turnover on downs
1073        let next_down = self.down + 1;
1074        if next_down > 4 {
1075            return 1;
1076        }
1077        next_down
1078    }
1079
1080    /// Get the updated home possession property
1081    ///
1082    /// ### Example
1083    /// ```
1084    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
1085    /// use fbsim_core::game::play::result::ScoreResult;
1086    /// 
1087    /// let mut update_opts = GameContextUpdateOptions::default();
1088    /// update_opts.net_yards = 10;
1089    /// let my_context = GameContext::new();
1090    /// let next_home_possession = my_context.next_home_possession(&update_opts);
1091    /// assert!(!next_home_possession);
1092    /// ```
1093    pub fn next_home_possession(&self, update_opts: &GameContextUpdateOptions) -> bool {
1094        let end_of_half = self.next_end_of_half(update_opts) || (self.end_of_half && update_opts.between_play);
1095
1096        // If end of half, possession goes to whomever received the opening kickoff
1097        if end_of_half {
1098            return self.home_opening_kickoff;
1099        }
1100
1101        // Change possession on successful kickoffs, defensive TDs, turnovers
1102        if self.next_play_kickoff || update_opts.def_score == ScoreResult::Touchdown || update_opts.turnover {
1103            return !self.home_possession;
1104        }
1105
1106        // Maintain possession on first downs, offensive scores
1107        if update_opts.net_yards >= self.distance as i32 ||
1108            update_opts.off_score == ScoreResult::Touchdown ||
1109            update_opts.off_score == ScoreResult::FieldGoal ||
1110            update_opts.off_score == ScoreResult::ExtraPoint ||
1111            update_opts.off_score == ScoreResult::TwoPointConversion {
1112            return self.home_possession;
1113        }
1114
1115        // Change possession on turnovers on downs
1116        let next_down = self.down + 1;
1117        if next_down > 4 {
1118            return !self.home_possession;
1119        }
1120        self.home_possession
1121    }
1122
1123    /// Get the updated yard line
1124    ///
1125    /// ### Example
1126    /// ```
1127    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
1128    /// use fbsim_core::game::play::result::ScoreResult;
1129    /// 
1130    /// let mut update_opts = GameContextUpdateOptions::default();
1131    /// update_opts.net_yards = 10;
1132    /// let my_context = GameContext::new();
1133    /// let yard_line = my_context.next_yard_line(&update_opts);
1134    /// assert!(yard_line == 45);
1135    /// ```
1136    pub fn next_yard_line(&self, update_opts: &GameContextUpdateOptions) -> u32 {
1137        let end_of_half = self.next_end_of_half(update_opts) || (self.end_of_half && update_opts.between_play);
1138
1139        // Kickoff and flip the field at the end of the half
1140        if end_of_half {
1141            if self.home_opening_kickoff ^ self.home_positive_direction {
1142                return 35;
1143            }
1144            return 65;
1145        }
1146
1147        // Kickoff after PAT, field goals, safeties
1148        let qtr = self.next_quarter(update_opts);
1149        let end_of_quarter = qtr != self.quarter;
1150        if self.next_play_extra_point || update_opts.def_score == ScoreResult::Safety || update_opts.off_score == ScoreResult::FieldGoal {
1151            let next_yl = if self.home_possession ^ self.home_positive_direction {
1152                65
1153            } else {
1154                35
1155            };
1156            let eoq_yl = if end_of_quarter {
1157                100 - next_yl
1158            } else {
1159                next_yl
1160            };
1161            return eoq_yl;
1162        }
1163
1164        // Extra point after touchdowns
1165        if update_opts.off_score == ScoreResult::Touchdown {
1166            let next_yl = if self.home_possession ^ self.home_positive_direction {
1167                2
1168            } else {
1169                98
1170            };
1171            let eoq_yl = if end_of_quarter {
1172                100 - next_yl
1173            } else {
1174                next_yl
1175            };
1176            return eoq_yl;
1177        } else if update_opts.def_score == ScoreResult::Touchdown {
1178            let next_yl = if self.home_possession ^ self.home_positive_direction {
1179                98
1180            } else {
1181                2
1182            };
1183            let eoq_yl = if end_of_quarter {
1184                100 - next_yl
1185            } else {
1186                next_yl
1187            };
1188            return eoq_yl;
1189        }
1190
1191        // Touchbacks and kickoffs out of bounds
1192        if update_opts.touchback {
1193            let next_yl = if self.home_possession ^ self.home_positive_direction {
1194                25
1195            } else {
1196                75
1197            };
1198            let eoq_yl = if end_of_quarter {
1199                100 - next_yl
1200            } else {
1201                next_yl
1202            };
1203            return eoq_yl;
1204        } else if update_opts.kickoff_oob {
1205            let next_yl = if self.home_possession ^ self.home_positive_direction {
1206                35
1207            } else {
1208                65
1209            };
1210            let eoq_yl = if end_of_quarter {
1211                100 - next_yl
1212            } else {
1213                next_yl
1214            };
1215            return eoq_yl;
1216        }
1217
1218        // Increment the yard line
1219        if self.home_possession ^ self.home_positive_direction {
1220            let next_yl = u32::try_from(0.max(100.min(self.yard_line as i32 - update_opts.net_yards))).unwrap_or_default();
1221            if end_of_quarter {
1222                100 - next_yl
1223            } else {
1224                next_yl
1225            }
1226        } else {
1227            let next_yl = u32::try_from(0.max(100.min(self.yard_line as i32 + update_opts.net_yards))).unwrap_or_default();
1228            if end_of_quarter {
1229                100 - next_yl
1230            } else {
1231                next_yl
1232            }
1233        }
1234    }
1235
1236    /// Get the updated distance
1237    ///
1238    /// ### Example
1239    /// ```
1240    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
1241    /// use fbsim_core::game::play::result::ScoreResult;
1242    ///
1243    /// let mut update_opts = GameContextUpdateOptions::default();
1244    /// update_opts.net_yards = 10;
1245    /// let my_context = GameContext::new();
1246    /// let distance = my_context.next_distance(&update_opts);
1247    /// assert!(distance == 10);
1248    /// ```
1249    pub fn next_distance(&self, update_opts: &GameContextUpdateOptions) -> u32 {
1250        let end_of_half = if update_opts.between_play {
1251            self.end_of_half
1252        } else {
1253            self.next_end_of_half(update_opts)
1254        };
1255
1256        // Kickoff after PAT, field goals, safeties, end of half
1257        if self.next_play_extra_point || end_of_half ||
1258            update_opts.def_score == ScoreResult::Safety || update_opts.off_score == ScoreResult::FieldGoal {
1259            return 10;
1260        }
1261
1262        // Extra point after touchdowns
1263        if update_opts.off_score == ScoreResult::Touchdown || update_opts.def_score == ScoreResult::Touchdown {
1264            return 2;
1265        }
1266
1267        // If a turnover occurred, determine the distance based on the defense's direction
1268        // Note it will always be a first down after a turnover
1269        let qtr = self.next_quarter(update_opts);
1270        let end_of_quarter = qtr != self.quarter;
1271        let mut next_yl = self.next_yard_line(update_opts);
1272        next_yl = if end_of_quarter {
1273            100 - next_yl
1274        } else {
1275            next_yl
1276        };
1277        if update_opts.turnover || (self.next_play_kickoff && !update_opts.between_play) {
1278            if self.home_possession ^ self.home_positive_direction {
1279                return 0.max(10.min(100_i32 - next_yl as i32)) as u32;
1280            }
1281            return 10.min(next_yl);
1282        }
1283
1284        // If no turnover occurred, check for a first down
1285        if update_opts.net_yards >= self.distance as i32 {
1286            if self.home_possession ^ self.home_positive_direction {
1287                return 10.min(next_yl);
1288            }
1289            return 0.max(10.min(100_i32 - next_yl as i32)) as u32;
1290        } else if self.down == 4 && !update_opts.between_play {
1291            if self.home_possession ^ self.home_positive_direction {
1292                return 0.max(10.min(100_i32 - next_yl as i32)) as u32;
1293            }
1294            return 10.min(next_yl);
1295        }
1296        let next_dist = self.distance as i32 - update_opts.net_yards;
1297        u32::try_from(next_dist).unwrap_or_default()
1298    }
1299
1300    /// Get the updated home timetous
1301    ///
1302    /// ### Example
1303    /// ```
1304    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
1305    /// 
1306    /// let mut update_opts = GameContextUpdateOptions::default();
1307    /// update_opts.off_timeout = true;
1308    /// let my_context = GameContext::new();
1309    /// let next_home_timeouts = my_context.next_home_timeouts(&update_opts);
1310    /// assert!(next_home_timeouts == 2);
1311    /// ```
1312    pub fn next_home_timeouts(&self, update_opts: &GameContextUpdateOptions) -> u32 {
1313        if self.end_of_half {
1314            return 3; // Reset at end of half
1315        }
1316        let home_tos = self.home_timeouts;
1317        if self.home_possession {
1318            if update_opts.off_timeout {
1319                return 0.max(home_tos as i32 - 1_i32) as u32;
1320            }
1321            return home_tos;
1322        }
1323        if update_opts.def_timeout {
1324            return 0.max(home_tos as i32 - 1_i32) as u32;
1325        }
1326        home_tos
1327    }
1328
1329    /// Get the updated away timetous
1330    ///
1331    /// ### Example
1332    /// ```
1333    /// use fbsim_core::game::context::{GameContext, GameContextUpdateOptions};
1334    /// 
1335    /// let mut update_opts = GameContextUpdateOptions::default();
1336    /// update_opts.def_timeout = true;
1337    /// let my_context = GameContext::new();
1338    /// let next_away_timeouts = my_context.next_away_timeouts(&update_opts);
1339    /// assert!(next_away_timeouts == 2);
1340    /// ```
1341    pub fn next_away_timeouts(&self, update_opts: &GameContextUpdateOptions) -> u32 {
1342        if self.end_of_half {
1343            return 3; // Reset at end of half
1344        }
1345        let away_tos = self.away_timeouts;
1346        if self.home_possession && update_opts.def_timeout {
1347            return 0.max(away_tos as i32 - 1_i32) as u32;
1348        }
1349        if update_opts.off_timeout {
1350            return 0.max(away_tos as i32 - 1_i32) as u32;
1351        }
1352        away_tos
1353    }
1354
1355    /// Get the next context given the results of the previous play
1356    pub fn next_context(&self, result: &(impl PlayResult + ?Sized)) -> GameContext {
1357        let duration = result.play_duration();
1358        let off_score = result.offense_score();
1359        let def_score = result.defense_score();
1360        let off_timeout = result.offense_timeout();
1361        let def_timeout = result.defense_timeout();
1362        let next_play_extra_point = result.next_play_extra_point();
1363        let turnover = result.turnover();
1364        let update_opts = GameContextUpdateOptions{
1365            duration,
1366            net_yards: result.net_yards(),
1367            off_score,
1368            def_score,
1369            turnover,
1370            touchback: result.touchback(),
1371            kickoff_oob: result.kickoff() && result.out_of_bounds(),
1372            off_timeout,
1373            def_timeout,
1374            next_play_extra_point,
1375            between_play: false,
1376            end_of_game: false
1377        };
1378        let end_of_half = if self.end_of_half {
1379            false
1380        } else {
1381            self.next_end_of_half(&update_opts) && !next_play_extra_point
1382        };
1383        let next_quarter = if end_of_half {
1384            self.quarter()
1385        } else {
1386            self.next_quarter(&update_opts)
1387        };
1388        let raw = GameContextRaw{
1389            home_team_short: self.home_team_short.clone(),
1390            away_team_short: self.away_team_short.clone(),
1391            quarter: next_quarter,
1392            half_seconds: self.next_half_seconds(&update_opts),
1393            down: self.next_down(&update_opts),
1394            distance: self.next_distance(&update_opts),
1395            yard_line: self.next_yard_line(&update_opts),
1396            home_score: self.next_home_score(&update_opts),
1397            away_score: self.next_away_score(&update_opts),
1398            home_timeouts: self.next_home_timeouts(&update_opts),
1399            away_timeouts: self.next_away_timeouts(&update_opts),
1400            home_positive_direction: self.next_home_positive_direction(&update_opts),
1401            home_opening_kickoff: self.home_opening_kickoff,
1402            home_possession: self.next_home_possession(&update_opts),
1403            last_play_turnover: turnover,
1404            last_play_incomplete: result.incomplete(),
1405            last_play_out_of_bounds: result.out_of_bounds(),
1406            last_play_timeout: off_timeout || def_timeout,
1407            last_play_kickoff: result.kickoff(),
1408            last_play_punt: result.punt(),
1409            next_play_extra_point,
1410            next_play_kickoff: result.next_play_kickoff() || (end_of_half && !next_play_extra_point),
1411            end_of_half,
1412            game_over: self.next_game_over(&update_opts)
1413        };
1414        GameContext::try_from(raw).unwrap()
1415    }
1416}
1417
1418impl std::fmt::Display for GameContext {
1419    /// Format a `GameContext` as a string.
1420    ///
1421    /// ### Example
1422    ///
1423    /// ```
1424    /// use fbsim_core::game::context::GameContext;
1425    ///
1426    /// // Initialize a game context and display it
1427    /// let my_context = GameContext::new();
1428    /// println!("{}", my_context);
1429    /// ```
1430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1431        let play_context = PlayContext::from(self);
1432        let (home_team_str, away_team_str) = if self.home_possession {
1433            (format!("*{}", &self.home_team_short), String::from(&self.away_team_short))
1434        } else {
1435            (String::from(&self.home_team_short), format!("*{}", &self.away_team_short))
1436        };
1437        let context_str = format!(
1438            "{} ({} {} - {} {})",
1439            &play_context,
1440            &home_team_str,
1441            self.home_score,
1442            &away_team_str,
1443            self.away_score
1444        );
1445        f.write_str(&context_str)
1446    }
1447}
1448
1449/// # `GameContextBuilder` struct
1450///
1451/// A `GameContextBuilder` implements the builder pattern for the `GameContext`
1452/// struct
1453#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
1454#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
1455pub struct GameContextBuilder {
1456    home_team_short: String,
1457    away_team_short: String,
1458    quarter: u32,
1459    half_seconds: u32,
1460    down: u32,
1461    distance: u32,
1462    yard_line: u32,
1463    home_score: u32,
1464    away_score: u32,
1465    home_timeouts: u32,
1466    away_timeouts: u32,
1467    home_positive_direction: bool,
1468    home_opening_kickoff: bool,
1469    home_possession: bool,
1470    last_play_turnover: bool,
1471    last_play_incomplete: bool,
1472    last_play_out_of_bounds: bool,
1473    last_play_timeout: bool,
1474    last_play_kickoff: bool,
1475    last_play_punt: bool,
1476    next_play_extra_point: bool,
1477    next_play_kickoff: bool,
1478    end_of_half: bool,
1479    game_over: bool
1480}
1481
1482impl Default for GameContextBuilder {
1483    /// Default constructor for the GameContextBuilder class
1484    ///
1485    /// ### Example
1486    /// ```
1487    /// use fbsim_core::game::context::GameContextBuilder;
1488    /// 
1489    /// let my_context = GameContextBuilder::default();
1490    /// ```
1491    fn default() -> Self {
1492        GameContextBuilder {
1493            home_team_short: String::from("HOME"),
1494            away_team_short: String::from("AWAY"),
1495            quarter: 1,
1496            half_seconds: 1800,
1497            down: 0,
1498            distance: 10,
1499            yard_line: 35,
1500            home_score: 0,
1501            away_score: 0,
1502            home_timeouts: 3,
1503            away_timeouts: 3,
1504            home_positive_direction: true,
1505            home_opening_kickoff: true,
1506            home_possession: true,
1507            last_play_turnover: false,
1508            last_play_incomplete: false,
1509            last_play_out_of_bounds: false,
1510            last_play_timeout: false,
1511            last_play_kickoff: false,
1512            last_play_punt: false,
1513            next_play_extra_point: false,
1514            next_play_kickoff: true,
1515            end_of_half: false,
1516            game_over: false
1517        }
1518    }
1519}
1520
1521impl GameContextBuilder {
1522    /// Initialize a new game context builder
1523    ///
1524    /// ### Example
1525    /// ```
1526    /// use fbsim_core::game::context::GameContextBuilder;
1527    ///
1528    /// let mut my_context_builder = GameContextBuilder::new();
1529    /// ```
1530    pub fn new() -> GameContextBuilder {
1531        GameContextBuilder::default()
1532    }
1533
1534    /// Set the home team short name
1535    ///
1536    /// ### Example
1537    /// ```
1538    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1539    /// 
1540    /// let my_context = GameContextBuilder::new()
1541    ///     .home_team_short("TEST")
1542    ///     .build()
1543    ///     .unwrap();
1544    /// assert!(my_context.home_team_short() == "TEST");
1545    /// ```
1546    pub fn home_team_short(mut self, home_team_short: &str) -> Self {
1547        self.home_team_short = String::from(home_team_short);
1548        self
1549    }
1550
1551    /// Set the away team short name
1552    ///
1553    /// ### Example
1554    /// ```
1555    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1556    /// 
1557    /// let my_context = GameContextBuilder::new()
1558    ///     .away_team_short("TEST")
1559    ///     .build()
1560    ///     .unwrap();
1561    /// assert!(my_context.away_team_short() == "TEST");
1562    /// ```
1563    pub fn away_team_short(mut self, away_team_short: &str) -> Self {
1564        self.away_team_short = String::from(away_team_short);
1565        self
1566    }
1567
1568    /// Set the quarter
1569    ///
1570    /// ### Example
1571    /// ```
1572    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1573    /// 
1574    /// let my_context = GameContextBuilder::new()
1575    ///     .quarter(2)
1576    ///     .half_seconds(800)
1577    ///     .build()
1578    ///     .unwrap();
1579    /// assert!(my_context.quarter() == 2);
1580    /// ```
1581    pub fn quarter(mut self, quarter: u32) -> Self {
1582        self.quarter = quarter;
1583        self
1584    }
1585
1586    /// Set the half seconds
1587    ///
1588    /// ### Example
1589    /// ```
1590    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1591    /// 
1592    /// let my_context = GameContextBuilder::new()
1593    ///     .half_seconds(100)
1594    ///     .quarter(4)
1595    ///     .build()
1596    ///     .unwrap();
1597    /// assert!(my_context.half_seconds() == 100);
1598    /// ```
1599    pub fn half_seconds(mut self, half_seconds: u32) -> Self {
1600        self.half_seconds = half_seconds;
1601        self
1602    }
1603
1604    /// Set the down
1605    ///
1606    /// ### Example
1607    /// ```
1608    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1609    /// 
1610    /// let my_context = GameContextBuilder::new()
1611    ///     .down(4)
1612    ///     .build()
1613    ///     .unwrap();
1614    /// assert!(my_context.down() == 4);
1615    /// ```
1616    pub fn down(mut self, down: u32) -> Self {
1617        self.down = down;
1618        self
1619    }
1620
1621    /// Set the distance
1622    ///
1623    /// ### Example
1624    /// ```
1625    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1626    /// 
1627    /// let my_context = GameContextBuilder::new()
1628    ///     .distance(7)
1629    ///     .build()
1630    ///     .unwrap();
1631    /// assert!(my_context.distance() == 7);
1632    /// ```
1633    pub fn distance(mut self, distance: u32) -> Self {
1634        self.distance = distance;
1635        self
1636    }
1637
1638    /// Set the yard line
1639    ///
1640    /// ### Example
1641    /// ```
1642    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1643    /// 
1644    /// let my_context = GameContextBuilder::new()
1645    ///     .yard_line(50)
1646    ///     .build()
1647    ///     .unwrap();
1648    /// assert!(my_context.yard_line() == 50);
1649    /// ```
1650    pub fn yard_line(mut self, yard_line: u32) -> Self {
1651        self.yard_line = yard_line;
1652        self
1653    }
1654
1655    /// Set the home score
1656    ///
1657    /// ### Example
1658    /// ```
1659    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1660    /// 
1661    /// let my_context = GameContextBuilder::new()
1662    ///     .home_score(21)
1663    ///     .build()
1664    ///     .unwrap();
1665    /// assert!(my_context.home_score() == 21);
1666    /// ```
1667    pub fn home_score(mut self, home_score: u32) -> Self {
1668        self.home_score = home_score;
1669        self
1670    }
1671
1672    /// Set the away score
1673    ///
1674    /// ### Example
1675    /// ```
1676    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1677    /// 
1678    /// let my_context = GameContextBuilder::new()
1679    ///     .away_score(14)
1680    ///     .build()
1681    ///     .unwrap();
1682    /// assert!(my_context.away_score() == 14);
1683    /// ```
1684    pub fn away_score(mut self, away_score: u32) -> Self {
1685        self.away_score = away_score;
1686        self
1687    }
1688
1689    /// Set the home timeouts
1690    ///
1691    /// ### Example
1692    /// ```
1693    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1694    /// 
1695    /// let my_context = GameContextBuilder::new()
1696    ///     .home_timeouts(2)
1697    ///     .build()
1698    ///     .unwrap();
1699    /// assert!(my_context.home_timeouts() == 2);
1700    /// ```
1701    pub fn home_timeouts(mut self, home_timeouts: u32) -> Self {
1702        self.home_timeouts = home_timeouts;
1703        self
1704    }
1705
1706    /// Set the away timeouts
1707    ///
1708    /// ### Example
1709    /// ```
1710    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1711    /// 
1712    /// let my_context = GameContextBuilder::new()
1713    ///     .away_timeouts(2)
1714    ///     .build()
1715    ///     .unwrap();
1716    /// assert!(my_context.away_timeouts() == 2);
1717    /// ```
1718    pub fn away_timeouts(mut self, away_timeouts: u32) -> Self {
1719        self.away_timeouts = away_timeouts;
1720        self
1721    }
1722    
1723    /// Set the home positive direction property
1724    ///
1725    /// ### Example
1726    /// ```
1727    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1728    /// 
1729    /// let my_context = GameContextBuilder::new()
1730    ///     .home_positive_direction(false)
1731    ///     .build()
1732    ///     .unwrap();
1733    /// assert!(my_context.home_positive_direction() == false);
1734    /// ```
1735    pub fn home_positive_direction(mut self, home_positive_direction: bool) -> Self {
1736        self.home_positive_direction = home_positive_direction;
1737        self
1738    }
1739    
1740    /// Set the home opening kickoff property
1741    ///
1742    /// ### Example
1743    /// ```
1744    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1745    /// 
1746    /// let my_context = GameContextBuilder::new()
1747    ///     .home_opening_kickoff(false)
1748    ///     .build()
1749    ///     .unwrap();
1750    /// assert!(my_context.home_opening_kickoff() == false);
1751    /// ```
1752    pub fn home_opening_kickoff(mut self, home_opening_kickoff: bool) -> Self {
1753        self.home_opening_kickoff = home_opening_kickoff;
1754        self
1755    }
1756    
1757    /// Set the home opening kickoff property
1758    ///
1759    /// ### Example
1760    /// ```
1761    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1762    /// 
1763    /// let my_context = GameContextBuilder::new()
1764    ///     .home_possession(false)
1765    ///     .build()
1766    ///     .unwrap();
1767    /// assert!(my_context.home_possession() == false);
1768    /// ```
1769    pub fn home_possession(mut self, home_possession: bool) -> Self {
1770        self.home_possession = home_possession;
1771        self
1772    }
1773    
1774    /// Set the last play turnover property
1775    ///
1776    /// ### Example
1777    /// ```
1778    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1779    /// 
1780    /// let my_context = GameContextBuilder::new()
1781    ///     .last_play_turnover(true)
1782    ///     .build()
1783    ///     .unwrap();
1784    /// assert!(my_context.last_play_turnover() == true);
1785    /// ```
1786    pub fn last_play_turnover(mut self, last_play_turnover: bool) -> Self {
1787        self.last_play_turnover = last_play_turnover;
1788        self
1789    }
1790    
1791    /// Set the last play incomplete property
1792    ///
1793    /// ### Example
1794    /// ```
1795    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1796    /// 
1797    /// let my_context = GameContextBuilder::new()
1798    ///     .last_play_incomplete(true)
1799    ///     .build()
1800    ///     .unwrap();
1801    /// assert!(my_context.last_play_incomplete() == true);
1802    /// ```
1803    pub fn last_play_incomplete(mut self, last_play_incomplete: bool) -> Self {
1804        self.last_play_incomplete = last_play_incomplete;
1805        self
1806    }
1807    
1808    /// Set the last play out of bounds property
1809    ///
1810    /// ### Example
1811    /// ```
1812    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1813    /// 
1814    /// let my_context = GameContextBuilder::new()
1815    ///     .last_play_out_of_bounds(true)
1816    ///     .build()
1817    ///     .unwrap();
1818    /// assert!(my_context.last_play_out_of_bounds() == true);
1819    /// ```
1820    pub fn last_play_out_of_bounds(mut self, last_play_out_of_bounds: bool) -> Self {
1821        self.last_play_out_of_bounds = last_play_out_of_bounds;
1822        self
1823    }
1824    
1825    /// Set the last play timeout property
1826    ///
1827    /// ### Example
1828    /// ```
1829    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1830    /// 
1831    /// let my_context = GameContextBuilder::new()
1832    ///     .last_play_timeout(true)
1833    ///     .build()
1834    ///     .unwrap();
1835    /// assert!(my_context.last_play_timeout() == true);
1836    /// ```
1837    pub fn last_play_timeout(mut self, last_play_timeout: bool) -> Self {
1838        self.last_play_timeout = last_play_timeout;
1839        self
1840    }
1841    
1842    /// Set the last play kickoff property
1843    ///
1844    /// ### Example
1845    /// ```
1846    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1847    /// 
1848    /// let my_context = GameContextBuilder::new()
1849    ///     .last_play_kickoff(true)
1850    ///     .build()
1851    ///     .unwrap();
1852    /// assert!(my_context.last_play_kickoff() == true);
1853    /// ```
1854    pub fn last_play_kickoff(mut self, last_play_kickoff: bool) -> Self {
1855        self.last_play_kickoff = last_play_kickoff;
1856        self
1857    }
1858    
1859    /// Set the last play punt property
1860    ///
1861    /// ### Example
1862    /// ```
1863    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1864    /// 
1865    /// let my_context = GameContextBuilder::new()
1866    ///     .last_play_punt(true)
1867    ///     .build()
1868    ///     .unwrap();
1869    /// assert!(my_context.last_play_punt() == true);
1870    /// ```
1871    pub fn last_play_punt(mut self, last_play_punt: bool) -> Self {
1872        self.last_play_punt = last_play_punt;
1873        self
1874    }
1875    
1876    /// Set the next play extra point property
1877    ///
1878    /// ### Example
1879    /// ```
1880    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1881    /// 
1882    /// let my_context = GameContextBuilder::new()
1883    ///     .next_play_kickoff(false)
1884    ///     .next_play_extra_point(true)
1885    ///     .build()
1886    ///     .unwrap();
1887    /// assert!(my_context.next_play_extra_point() == true);
1888    /// ```
1889    pub fn next_play_extra_point(mut self, next_play_extra_point: bool) -> Self {
1890        self.next_play_extra_point = next_play_extra_point;
1891        self
1892    }
1893    
1894    /// Set the next play kickoff property
1895    ///
1896    /// ### Example
1897    /// ```
1898    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1899    /// 
1900    /// let my_context = GameContextBuilder::new()
1901    ///     .next_play_kickoff(false)
1902    ///     .build()
1903    ///     .unwrap();
1904    /// assert!(my_context.next_play_kickoff() == false);
1905    /// ```
1906    pub fn next_play_kickoff(mut self, next_play_kickoff: bool) -> Self {
1907        self.next_play_kickoff = next_play_kickoff;
1908        self
1909    }
1910    
1911    /// Set the end of half property
1912    ///
1913    /// ### Example
1914    /// ```
1915    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1916    /// 
1917    /// let my_context = GameContextBuilder::new()
1918    ///     .half_seconds(0)
1919    ///     .quarter(4)
1920    ///     .end_of_half(true)
1921    ///     .build()
1922    ///     .unwrap();
1923    /// assert!(my_context.end_of_half() == true);
1924    /// ```
1925    pub fn end_of_half(mut self, end_of_half: bool) -> Self {
1926        self.end_of_half = end_of_half;
1927        self
1928    }
1929    
1930    /// Set the game over property
1931    ///
1932    /// ### Example
1933    /// ```
1934    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1935    /// 
1936    /// let my_context = GameContextBuilder::new()
1937    ///     .half_seconds(0)
1938    ///     .quarter(4)
1939    ///     .game_over(true)
1940    ///     .build()
1941    ///     .unwrap();
1942    /// assert!(my_context.game_over() == true);
1943    /// ```
1944    pub fn game_over(mut self, game_over: bool) -> Self {
1945        self.game_over = game_over;
1946        self
1947    }
1948
1949    /// Build the game context
1950    ///
1951    /// ### Example
1952    /// ```
1953    /// use fbsim_core::game::context::{GameContext, GameContextBuilder};
1954    /// 
1955    /// let my_context = GameContextBuilder::new()
1956    ///     .home_team_short("NYM")
1957    ///     .away_team_short("CAR")
1958    ///     .quarter(2)
1959    ///     .half_seconds(700)
1960    ///     .down(3)
1961    ///     .distance(4)
1962    ///     .yard_line(14)
1963    ///     .home_score(0)
1964    ///     .away_score(3)
1965    ///     .home_timeouts(2)
1966    ///     .away_timeouts(3)
1967    ///     .home_positive_direction(true)
1968    ///     .home_possession(true)
1969    ///     .last_play_incomplete(true)
1970    ///     .last_play_out_of_bounds(false)
1971    ///     .last_play_timeout(false)
1972    ///     .last_play_punt(false)
1973    ///     .last_play_kickoff(false)
1974    ///     .next_play_extra_point(false)
1975    ///     .next_play_kickoff(false)
1976    ///     .game_over(false)
1977    ///     .build()
1978    ///     .unwrap();
1979    /// ```
1980    pub fn build(self) -> Result<GameContext, String> {
1981        let raw = GameContextRaw{
1982            home_team_short: self.home_team_short,
1983            away_team_short: self.away_team_short,
1984            quarter: self.quarter,
1985            half_seconds: self.half_seconds,
1986            down: self.down,
1987            distance: self.distance,
1988            yard_line: self.yard_line,
1989            home_score: self.home_score,
1990            away_score: self.away_score,
1991            home_timeouts: self.home_timeouts,
1992            away_timeouts: self.away_timeouts,
1993            home_positive_direction: self.home_positive_direction,
1994            home_opening_kickoff: self.home_opening_kickoff,
1995            home_possession: self.home_possession,
1996            last_play_turnover: self.last_play_turnover,
1997            last_play_incomplete: self.last_play_incomplete,
1998            last_play_out_of_bounds: self.last_play_out_of_bounds,
1999            last_play_timeout: self.last_play_timeout,
2000            last_play_kickoff: self.last_play_kickoff,
2001            last_play_punt: self.last_play_punt,
2002            next_play_extra_point: self.next_play_extra_point,
2003            next_play_kickoff: self.next_play_kickoff,
2004            end_of_half: self.end_of_half,
2005            game_over: self.game_over
2006        };
2007        GameContext::try_from(raw)
2008    }
2009}
2010
2011#[cfg(test)]
2012mod tests {
2013    use super::*;
2014    use crate::game::play::result::betweenplay::{BetweenPlayResult, BetweenPlayResultBuilder};
2015    use crate::game::play::result::kickoff::{KickoffResult, KickoffResultBuilder};
2016
2017    #[test]
2018    fn test_long_kickoff_return_fumble_result() {
2019        // Create a new context
2020        let context: GameContext = GameContext::new();
2021
2022        // Create a kickoff return result in which the return team returns
2023        // 60+ yards and then fumbles
2024        let kickoff_return: KickoffResult = KickoffResultBuilder::new()
2025            .kickoff_yards(49)
2026            .kick_return_yards(67)
2027            .play_duration(10)
2028            .fumble_return_yards(3)
2029            .touchback(false)
2030            .out_of_bounds(false)
2031            .fair_catch(false)
2032            .fumble(true)
2033            .touchdown(false)
2034            .build()
2035            .unwrap();
2036
2037        // Get the next context
2038        let next_context: GameContext = kickoff_return.next_context(&context);
2039
2040        // Assert the next distance is 10
2041        assert!(next_context.distance() == 10);
2042    }
2043
2044    #[test]
2045    fn test_end_of_game_next_yl_1() {
2046        // Create a context
2047        let context: GameContext = GameContextBuilder::default()
2048            .home_score(52)
2049            .away_score(34)
2050            .half_seconds(28)
2051            .quarter(4)
2052            .down(3)
2053            .distance(6)
2054            .yard_line(4)
2055            .home_possession(false)
2056            .home_positive_direction(false)
2057            .home_opening_kickoff(true)
2058            .build()
2059            .unwrap();
2060        
2061        // Create a between play result
2062        let between_play: BetweenPlayResult = BetweenPlayResultBuilder::new()
2063            .duration(30)
2064            .offense_timeout(false)
2065            .defense_timeout(false)
2066            .build()
2067            .unwrap();
2068        
2069        // Get the next context
2070        let next_context: GameContext = between_play.next_context(&context);
2071
2072        // Assert the correct context is derived
2073        assert!(next_context.home_possession());
2074        assert!(next_context.home_positive_direction());
2075        assert!(next_context.end_of_half());
2076        assert_eq!(next_context.yard_line(), 35);
2077    }
2078
2079    #[test]
2080    fn test_end_of_game_next_yl_2() {
2081        // Create a context
2082        let context: GameContext = GameContextBuilder::default()
2083            .home_score(52)
2084            .away_score(34)
2085            .half_seconds(23)
2086            .quarter(4)
2087            .down(4)
2088            .distance(2)
2089            .yard_line(96)
2090            .home_possession(true)
2091            .home_positive_direction(true)
2092            .home_opening_kickoff(true)
2093            .build()
2094            .unwrap();
2095
2096        // Create a between play result
2097        let between_play: BetweenPlayResult = BetweenPlayResultBuilder::new()
2098            .duration(30)
2099            .offense_timeout(false)
2100            .defense_timeout(false)
2101            .build()
2102            .unwrap();
2103
2104        // Get the next context
2105        let next_context: GameContext = between_play.next_context(&context);
2106
2107        // Assert the correct context is derived
2108        assert_eq!(next_context.down(), 0);
2109        assert!(next_context.home_possession());
2110        assert!(!next_context.home_positive_direction());
2111        assert!(next_context.end_of_half());
2112        assert_eq!(next_context.yard_line(), 65);
2113    }
2114}