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