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}