#![doc = include_str!("../../../../docs/game/play/result/kickoff.md")]
use rand::Rng;
#[cfg(feature = "rocket_okapi")]
use rocket_okapi::okapi::schemars;
#[cfg(feature = "rocket_okapi")]
use rocket_okapi::okapi::schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
#[cfg(feature = "wasm")]
use tsify_next::Tsify;
use rand_distr::{Normal, Distribution, Exp, SkewNormal};
use crate::game::context::GameContext;
use crate::game::play::PlaySimulatable;
use crate::game::play::context::PlayContext;
use crate::game::play::result::{PlayResult, PlayTypeResult, PlayResultSimulator, ScoreResult};
const P_TOUCHBACK_INTR: f64 = 0.2528877428268531_f64;
const P_TOUCHBACK_COEF: f64 = 0.62457076_f64;
const P_OOB_INTR: f64 = 0.013879833381776598_f64;
const P_OOB_COEF: f64 = -0.01063523_f64;
const P_KICKOFF_INSIDE_20: f64 = 0.8_f64;
const MEAN_KICKOFF_INSIDE_20_DIST: f64 = 64.3_f64;
const STD_KICKOFF_INSIDE_20_DIST_INTR: f64 = 4.516109138481186_f64;
const STD_KICKOFF_INSIDE_20_DIST_COEF: f64 = 1.97369663_f64;
const SKEW_KICKOFF_INSIDE_20_DIST: f64 = -1.7_f64;
const MEAN_KICKOFF_OUTSIDE_20_DIST_INTR: f64 = 59.31943845056676_f64;
const MEAN_KICKOFF_OUTSIDE_20_DIST_COEF: f64 = -3.42944893_f64;
const STD_KICKOFF_OUTSIDE_20_DIST_INTR: f64 = 11.602550109235546_f64;
const STD_KICKOFF_OUTSIDE_20_DIST_COEF: f64 = 6.81862647_f64;
const SKEW_KICKOFF_OUTSIDE_20_DIST: f64 = -2_f64;
const P_FAIR_CATCH_INTR: f64 = 0.02694588730554516_f64;
const P_FAIR_CATCH_COEF: f64 = -0.03716183_f64;
const MEAN_KICKOFF_RETURN_YARDS_INTR: f64 = -0.6236115656913945_f64;
const MEAN_KICKOFF_RETURN_YARDS_COEF: f64 = 20.05077203_f64;
const STD_KICKOFF_RETURN_YARDS_INTR: f64 = 6.421970424325094_f64;
const STD_KICKOFF_RETURN_YARDS_COEF: f64 = 12.34550665_f64;
const SKEW_KICKOFF_RETURN_YARDS_INTR: f64 = 3.62041405111988_f64;
const SKEW_KICKOFF_RETURN_YARDS_COEF: f64 = -2.65709746_f64;
const P_ONSIDE_KICK_RECOVERY: f64 = 0.06_f64;
const P_KICKOFF_RETURN_FUMBLE: f64 = 0.007_f64;
const KICKOFF_RETURN_PLAY_DURATION_INTR: f64 = 0.11217103_f64;
const KICKOFF_RETURN_PLAY_DURATION_COEF: f64 = 1.20326252_f64;
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
pub struct KickoffResultRaw {
kickoff_yards: i32,
kick_return_yards: i32,
play_duration: u32,
fumble_return_yards: i32,
touchback: bool,
out_of_bounds: bool,
fair_catch: bool,
fumble: bool,
touchdown: bool,
onside_kick: bool
}
impl KickoffResultRaw {
pub fn validate(&self) -> Result<(), String> {
if self.kickoff_yards > 100 {
return Err(
format!(
"Kickoff yards is not in range [0, 100]: {}",
self.kickoff_yards
)
)
}
if self.kick_return_yards > 110 {
return Err(
format!(
"Kick return yards is not in range [0, 110]: {}",
self.kick_return_yards
)
)
}
if self.play_duration > 100 {
return Err(
format!(
"Play duration is not in range [0, 100]: {}",
self.play_duration
)
)
}
if self.fumble_return_yards > 100 {
return Err(
format!(
"Fubmle return yards is not in range [0, 100]: {}",
self.fumble_return_yards
)
)
}
if (self.out_of_bounds && (self.fair_catch || self.touchback)) ||
(self.fair_catch && self.touchback) {
return Err(
format!(
"Must have at most one true across touchback ({}), out of bounds ({}), and fair catch ({})",
self.touchback, self.out_of_bounds, self.fair_catch
)
)
}
if self.touchdown && (self.touchback || self.out_of_bounds || self.fair_catch) {
return Err(
format!(
"Cannot both score a touchdown and touchback ({}), out of bounds ({}), or fair catch ({})",
self.touchback, self.out_of_bounds, self.fair_catch
)
)
}
Ok(())
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize)]
pub struct KickoffResult {
kickoff_yards: i32,
kick_return_yards: i32,
play_duration: u32,
fumble_return_yards: i32,
touchback: bool,
out_of_bounds: bool,
fair_catch: bool,
fumble: bool,
touchdown: bool,
onside_kick: bool
}
impl TryFrom<KickoffResultRaw> for KickoffResult {
type Error = String;
fn try_from(item: KickoffResultRaw) -> Result<Self, Self::Error> {
match item.validate() {
Ok(()) => (),
Err(error) => return Err(error),
};
Ok(
KickoffResult{
kickoff_yards: item.kickoff_yards,
kick_return_yards: item.kick_return_yards,
play_duration: item.play_duration,
fumble_return_yards: item.fumble_return_yards,
touchback: item.touchback,
out_of_bounds: item.out_of_bounds,
fair_catch: item.fair_catch,
fumble: item.fumble,
touchdown: item.touchdown,
onside_kick: item.onside_kick
}
)
}
}
impl<'de> Deserialize<'de> for KickoffResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = KickoffResultRaw::deserialize(deserializer)?;
KickoffResult::try_from(raw).map_err(serde::de::Error::custom)
}
}
impl Default for KickoffResult {
fn default() -> Self {
KickoffResult{
kickoff_yards: 65,
kick_return_yards: 0,
play_duration: 0,
fumble_return_yards: 0,
touchback: true,
out_of_bounds: false,
fair_catch: false,
fumble: false,
touchdown: false,
onside_kick: false
}
}
}
impl std::fmt::Display for KickoffResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let distance_str = if self.onside_kick {
format!("Onside kick {} yards", self.kickoff_yards)
} else {
format!("Kickoff {} yards", self.kickoff_yards)
};
let landing_suffix = if self.touchback {
" for a touchback."
} else if self.out_of_bounds {
" out of bounds."
} else if self.fair_catch && !self.fumble {
" for a fair catch."
} else if self.onside_kick && ! self.fumble {
" recovered by the receiving team."
} else {
" fielded."
};
let kick_return_str = if !(
self.touchback || self.out_of_bounds ||
(self.fair_catch && !self.fumble) ||
(self.fumble && self.kick_return_yards == 0)
) {
format!(" Returned {} yards.", self.kick_return_yards)
} else {
String::from("")
};
let fumble_str = if self.fumble && !self.onside_kick {
format!(" FUMBLED recovered by the kicking team, returned {} yards.", self.fumble_return_yards)
} else if self.fumble && self.onside_kick {
format!(" RECOVERED by the kicking team, returned {} yards.", self.fumble_return_yards)
} else {
String::from("")
};
let touchdown_str = if self.touchdown {
" TOUCHDOWN!"
} else {
""
};
let kickoff_str = format!(
"{}{}{}{}{}",
&distance_str,
landing_suffix,
&kick_return_str,
&fumble_str,
&touchdown_str
);
f.write_str(kickoff_str.trim())
}
}
impl PlayResult for KickoffResult {
fn next_context(&self, context: &GameContext) -> GameContext {
context.next_context(self)
}
fn play_duration(&self) -> u32 {
self.play_duration
}
fn net_yards(&self) -> i32 {
self.kickoff_yards - self.kick_return_yards + self.fumble_return_yards
}
fn turnover(&self) -> bool {
!self.fumble
}
fn offense_score(&self) -> ScoreResult {
if self.touchdown && self.fumble {
return ScoreResult::Touchdown;
}
ScoreResult::None
}
fn defense_score(&self) -> ScoreResult {
if self.touchdown && !self.fumble {
return ScoreResult::Touchdown;
}
ScoreResult::None
}
fn offense_timeout(&self) -> bool { false }
fn defense_timeout(&self) -> bool { false }
fn incomplete(&self) -> bool { false }
fn out_of_bounds(&self) -> bool {
self.out_of_bounds
}
fn touchback(&self) -> bool {
self.touchback
}
fn kickoff(&self) -> bool { true }
fn next_play_kickoff(&self) -> bool { false }
fn next_play_extra_point(&self) -> bool {
self.touchdown
}
}
impl KickoffResult {
pub fn new() -> KickoffResult {
KickoffResult::default()
}
pub fn kickoff_yards(&self) -> i32 {
self.kickoff_yards
}
pub fn kick_return_yards(&self) -> i32 {
self.kick_return_yards
}
pub fn play_duration(&self) -> u32 {
self.play_duration
}
pub fn fumble_return_yards(&self) -> i32 {
self.fumble_return_yards
}
pub fn touchback(&self) -> bool {
self.touchback
}
pub fn out_of_bounds(&self) -> bool {
self.out_of_bounds
}
pub fn fair_catch(&self) -> bool {
self.fair_catch
}
pub fn fumble(&self) -> bool {
self.fumble
}
pub fn touchdown(&self) -> bool {
self.touchdown
}
pub fn onside_kick(&self) -> bool {
self.onside_kick
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize)]
pub struct KickoffResultBuilder {
kickoff_yards: i32,
kick_return_yards: i32,
play_duration: u32,
fumble_return_yards: i32,
touchback: bool,
out_of_bounds: bool,
fair_catch: bool,
fumble: bool,
touchdown: bool,
onside_kick: bool
}
impl Default for KickoffResultBuilder {
fn default() -> Self {
KickoffResultBuilder{
kickoff_yards: 65,
kick_return_yards: 0,
play_duration: 0,
fumble_return_yards: 0,
touchback: true,
out_of_bounds: false,
fair_catch: false,
fumble: false,
touchdown: false,
onside_kick: false
}
}
}
impl KickoffResultBuilder {
pub fn new() -> KickoffResultBuilder {
KickoffResultBuilder::default()
}
pub fn kickoff_yards(mut self, kickoff_yards: i32) -> Self {
self.kickoff_yards = kickoff_yards;
self
}
pub fn kick_return_yards(mut self, kick_return_yards: i32) -> Self {
self.kick_return_yards = kick_return_yards;
self
}
pub fn play_duration(mut self, play_duration: u32) -> Self {
self.play_duration = play_duration;
self
}
pub fn fumble_return_yards(mut self, fumble_return_yards: i32) -> Self {
self.fumble_return_yards = fumble_return_yards;
self
}
pub fn touchback(mut self, touchback: bool) -> Self {
self.touchback = touchback;
self
}
pub fn out_of_bounds(mut self, out_of_bounds: bool) -> Self {
self.out_of_bounds = out_of_bounds;
self
}
pub fn fair_catch(mut self, fair_catch: bool) -> Self {
self.fair_catch = fair_catch;
self
}
pub fn fumble(mut self, fumble: bool) -> Self {
self.fumble = fumble;
self
}
pub fn touchdown(mut self, touchdown: bool) -> Self {
self.touchdown = touchdown;
self
}
pub fn onside_kick(mut self, onside_kick: bool) -> Self {
self.onside_kick = onside_kick;
self
}
pub fn build(self) -> Result<KickoffResult, String> {
let raw = KickoffResultRaw{
kickoff_yards: self.kickoff_yards,
kick_return_yards: self.kick_return_yards,
play_duration: self.play_duration,
fumble_return_yards: self.fumble_return_yards,
touchback: self.touchback,
out_of_bounds: self.out_of_bounds,
fair_catch: self.fair_catch,
fumble: self.fumble,
touchdown: self.touchdown,
onside_kick: self.onside_kick
};
KickoffResult::try_from(raw)
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Default, Serialize, Deserialize)]
pub struct KickoffResultSimulator {}
impl KickoffResultSimulator {
pub fn new() -> KickoffResultSimulator {
KickoffResultSimulator{}
}
fn touchback(&self, norm_kicking: f64, rng: &mut impl Rng) -> bool {
let p_touchback: f64 = 1_f64.min(0_f64.max(
P_TOUCHBACK_INTR + (P_TOUCHBACK_COEF * norm_kicking)
));
rng.gen::<f64>() < p_touchback
}
fn out_of_bounds(&self, norm_kicking: f64, rng: &mut impl Rng) -> bool {
let p_oob: f64 = 1_f64.min(0_f64.max(
P_OOB_INTR + (P_OOB_COEF * norm_kicking)
));
rng.gen::<f64>() < p_oob
}
fn inside_20(&self, rng: &mut impl Rng) -> bool {
rng.gen::<f64>() < P_KICKOFF_INSIDE_20
}
fn distance(&self, norm_kicking: f64, inside_20: bool, rng: &mut impl Rng) -> i32 {
let mean_dist: f64 = if inside_20 {
MEAN_KICKOFF_INSIDE_20_DIST
} else {
MEAN_KICKOFF_OUTSIDE_20_DIST_INTR + (MEAN_KICKOFF_OUTSIDE_20_DIST_COEF * norm_kicking)
};
let std_dist: f64 = if inside_20 {
STD_KICKOFF_INSIDE_20_DIST_INTR + (STD_KICKOFF_INSIDE_20_DIST_COEF * norm_kicking)
} else {
STD_KICKOFF_OUTSIDE_20_DIST_INTR + (STD_KICKOFF_OUTSIDE_20_DIST_COEF * norm_kicking)
};
let skew_dist: f64 = if inside_20 {
SKEW_KICKOFF_INSIDE_20_DIST
} else {
SKEW_KICKOFF_OUTSIDE_20_DIST
};
let dist_dist = SkewNormal::new(mean_dist, std_dist, skew_dist).unwrap();
dist_dist.sample(rng).round() as i32
}
fn fair_catch(&self, norm_diff_returning: f64, rng: &mut impl Rng) -> bool {
let p_fair_catch: f64 = 1_f64.min(0_f64.max(
P_FAIR_CATCH_INTR + (P_FAIR_CATCH_COEF * norm_diff_returning)
));
rng.gen::<f64>() < p_fair_catch
}
fn return_yards(&self, norm_diff_returning: f64, rng: &mut impl Rng) -> i32 {
let mean_return_yards: f64 = MEAN_KICKOFF_RETURN_YARDS_INTR + (MEAN_KICKOFF_RETURN_YARDS_COEF * norm_diff_returning);
let std_return_yards: f64 = STD_KICKOFF_RETURN_YARDS_INTR + (STD_KICKOFF_RETURN_YARDS_COEF * norm_diff_returning);
let skew_return_yards: f64 = SKEW_KICKOFF_RETURN_YARDS_INTR + (SKEW_KICKOFF_RETURN_YARDS_COEF * norm_diff_returning);
let return_yards_dist = SkewNormal::new(mean_return_yards, std_return_yards, skew_return_yards).unwrap();
return_yards_dist.sample(rng).round() as i32
}
fn fumble(&self, rng: &mut impl Rng) -> bool {
rng.gen::<f64>() < P_KICKOFF_RETURN_FUMBLE
}
fn onside_kick_recovery(&self, rng: &mut impl Rng) -> bool {
rng.gen::<f64>() < P_ONSIDE_KICK_RECOVERY
}
fn fumble_return_yards(&self, rng: &mut impl Rng) -> i32 {
Exp::new(1_f64).unwrap().sample(rng).round() as i32
}
fn play_duration(&self, total_yards: u32, rng: &mut impl Rng) -> u32 {
let mean_duration: f64 = KICKOFF_RETURN_PLAY_DURATION_INTR + (KICKOFF_RETURN_PLAY_DURATION_COEF * total_yards as f64);
let duration_dist = Normal::new(mean_duration, 2_f64).unwrap();
u32::try_from(duration_dist.sample(rng).sqrt().round() as i32).unwrap_or_default()
}
}
impl PlayResultSimulator for KickoffResultSimulator {
fn sim(&self, offense: &impl PlaySimulatable, defense: &impl PlaySimulatable, context: &GameContext, rng: &mut impl Rng) -> PlayTypeResult {
let offense_advantage: bool = context.offense_advantage();
let defense_advantage: bool = context.defense_advantage();
let norm_kicking: f64 = offense.offense().kickoffs_advantage(offense_advantage) as f64 / 100_f64;
let norm_diff_returning: f64 = 0.5_f64 + (
(
defense.defense().kick_returning_advantage(defense_advantage) as f64 -
offense.offense().kick_return_defense_advantage(offense_advantage) as f64
) / 200_f64
);
let td_yards: i32 = context.yards_to_touchdown();
let safety_yards: i32 = context.yards_to_safety();
let play_context = PlayContext::from(context);
let onside_kick: bool = play_context.onside_kick();
let touchback: bool = if !onside_kick {
self.touchback(norm_kicking, rng)
} else {
false
};
let out_of_bounds: bool = if !touchback {
self.out_of_bounds(norm_kicking, rng)
} else {
false
};
let inside_20: bool = if !(touchback || onside_kick) {
self.inside_20(rng)
} else {
false
};
let kickoff_distance: i32 = if onside_kick {
10
} else if !touchback {
td_yards.min(self.distance(norm_kicking, inside_20, rng))
} else {
td_yards
};
let fair_catch: bool = if !(touchback || out_of_bounds || onside_kick) {
self.fair_catch(norm_diff_returning, rng)
} else {
false
};
let return_yards: i32 = if !(touchback || out_of_bounds || fair_catch || onside_kick) {
self.return_yards(norm_diff_returning, rng).min(safety_yards + kickoff_distance)
} else {
0
};
let fumble: bool = if onside_kick {
self.onside_kick_recovery(rng)
} else if !(touchback || out_of_bounds || fair_catch) {
self.fumble(rng)
} else {
false
};
let fumble_return_yards: i32 = if fumble {
self.fumble_return_yards(rng)
} else {
0
};
let total_yards: u32 = kickoff_distance.unsigned_abs() + return_yards.unsigned_abs() + fumble_return_yards.unsigned_abs();
let play_duration: u32 = if !(touchback || out_of_bounds || fair_catch) {
self.play_duration(total_yards, rng)
} else {
0
};
let touchdown: bool = if fumble {
kickoff_distance - return_yards + fumble_return_yards > td_yards
} else if !(touchback || out_of_bounds || fair_catch) {
kickoff_distance - return_yards < safety_yards
} else {
false
};
let raw = KickoffResultRaw{
kickoff_yards: kickoff_distance,
kick_return_yards: return_yards,
play_duration,
fumble_return_yards,
touchback,
out_of_bounds,
fair_catch,
fumble,
touchdown,
onside_kick
};
let kickoff_res = KickoffResult::try_from(raw).unwrap();
PlayTypeResult::Kickoff(kickoff_res)
}
}