#![doc = include_str!("../../../../docs/game/play/result/fieldgoal.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::{Distribution, Exp, SkewNormal};
use crate::game::context::GameContext;
use crate::game::play::PlaySimulatable;
use crate::game::play::result::{PlayResult, PlayTypeResult, PlayResultSimulator, ScoreResult};
const P_BLOCKED_SKILL_INTR: f64 = 0.013200206956159479_f64;
const P_BLOCKED_SKILL_COEF: f64 = 0.01919733_f64;
const P_BLOCKED_YARD_LINE_INTR: f64 = -5.320426815163247_f64;
const P_BLOCKED_YARD_LINE_COEF: f64 = 0.05875677_f64;
const P_FIELD_GOAL_MADE_SKILL_INTR: f64 = 0.44298810053776055_f64;
const P_FIELD_GOAL_MADE_SKILL_COEF: f64 = 0.57103524_f64;
const P_FIELD_GOAL_MADE_YARD_LINE_INTR: f64 = 0.9580405463949037_f64;
const P_FIELD_GOAL_MADE_YARD_LINE_COEF_1: f64 = 0.00399668_f64;
const P_FIELD_GOAL_MADE_YARD_LINE_COEF_2: f64 = -0.00035704_f64;
const FIELD_GOAL_BLOCKED_DURATION_MEAN: f64 = 9.843750_f64; const FIELD_GOAL_BLOCKED_DURATION_STD: f64 = 3.385612_f64;
const FIELD_GOAL_BLOCKED_DURATION_SKEW: f64 = 1.541247_f64;
const FIELD_GOAL_NOT_BLOCKED_DURATION_MEAN: f64 = 7.054470_f64; const FIELD_GOAL_NOT_BLOCKED_DURATION_STD: f64 = 1.001211_f64;
const FIELD_GOAL_NOT_BLOCKED_DURATION_SKEW: f64 = -0.440028_f64;
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
pub struct FieldGoalResultRaw {
field_goal_distance: i32,
return_yards: i32,
play_duration: u32,
made: bool,
blocked: bool,
touchdown: bool,
extra_point: bool
}
impl FieldGoalResultRaw {
pub fn validate(&self) -> Result<(), String> {
if self.made && self.touchdown {
return Err(
String::from("Field goal cannot both be made & TD scored")
)
}
if self.made && self.blocked {
return Err(
String::from("Field goal cannot both be made & blocked")
)
}
if self.touchdown && !self.blocked {
return Err(
String::from("Field goal cannot result in TD if not blocked")
)
}
if self.field_goal_distance > 117 {
return Err(
format!(
"Field goal distance is out of range [0, 117]: {}",
self.field_goal_distance
)
)
}
if self.play_duration > 100 {
return Err(
format!(
"Play duration is out of range [0, 100]: {}",
self.play_duration
),
)
}
if self.return_yards.abs() > 100 {
return Err(
format!(
"Return yards is out of range [-100, 100]: {}",
self.return_yards
)
)
}
if !self.blocked && self.return_yards != 0 {
return Err(
format!(
"Field goal was not blocked but return yards are nonzero: {}",
self.return_yards
)
)
}
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 FieldGoalResult {
field_goal_distance: i32,
return_yards: i32,
play_duration: u32,
made: bool,
blocked: bool,
touchdown: bool,
extra_point: bool
}
impl TryFrom<FieldGoalResultRaw> for FieldGoalResult {
type Error = String;
fn try_from(item: FieldGoalResultRaw) -> Result<Self, Self::Error> {
match item.validate() {
Ok(()) => (),
Err(error) => return Err(error),
};
Ok(
FieldGoalResult{
field_goal_distance: item.field_goal_distance,
return_yards: item.return_yards,
play_duration: item.play_duration,
made: item.made,
blocked: item.blocked,
touchdown: item.touchdown,
extra_point: item.extra_point
}
)
}
}
impl<'de> Deserialize<'de> for FieldGoalResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = FieldGoalResultRaw::deserialize(deserializer)?;
FieldGoalResult::try_from(raw).map_err(serde::de::Error::custom)
}
}
impl Default for FieldGoalResult {
fn default() -> Self {
FieldGoalResult{
field_goal_distance: 17,
return_yards: 0,
play_duration: 0,
made: true,
blocked: false,
touchdown: false,
extra_point: true
}
}
}
impl std::fmt::Display for FieldGoalResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let distance_str = format!("{} yard", self.field_goal_distance);
let fg_type_str = if self.extra_point {
"extra point"
} else {
"field goal"
};
let result_str = if self.made {
"is good."
} else if self.blocked {
"BLOCKED."
} else {
"NO GOOD."
};
let return_str = if self.blocked {
let return_prefix = format!("Returned {} yards", self.return_yards);
if self.touchdown {
format!("{}, TOUCHDOWN!", &return_prefix)
} else {
format!("{}.", return_prefix)
}
} else {
String::from("")
};
let fg_str = format!(
"{} {} {} {}",
&distance_str,
fg_type_str,
result_str,
&return_str
);
f.write_str(fg_str.trim())
}
}
impl PlayResult for FieldGoalResult {
fn next_context(&self, context: &GameContext) -> GameContext {
context.next_context(self)
}
fn play_duration(&self) -> u32 {
if self.extra_point {
0
} else {
self.play_duration
}
}
fn net_yards(&self) -> i32 {
-self.return_yards
}
fn turnover(&self) -> bool {
!self.extra_point && (self.blocked || !self.made)
}
fn offense_score(&self) -> ScoreResult {
if self.extra_point && self.made {
return ScoreResult::ExtraPoint;
} else if self.made {
return ScoreResult::FieldGoal;
}
ScoreResult::None
}
fn defense_score(&self) -> ScoreResult {
if self.extra_point && self.blocked && self.touchdown {
return ScoreResult::TwoPointConversion;
} else if self.blocked && self.touchdown {
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 { false }
fn kickoff(&self) -> bool { false }
fn next_play_kickoff(&self) -> bool {
self.extra_point || self.made
}
fn next_play_extra_point(&self) -> bool {
!self.extra_point && self.blocked && self.touchdown
}
}
impl FieldGoalResult {
pub fn new() -> FieldGoalResult {
FieldGoalResult::default()
}
pub fn field_goal_distance(&self) -> i32 {
self.field_goal_distance
}
pub fn return_yards(&self) -> i32 {
self.return_yards
}
pub fn play_duration(&self) -> u32 {
self.play_duration
}
pub fn made(&self) -> bool {
self.made
}
pub fn missed(&self) -> bool {
!(self.made || self.blocked)
}
pub fn blocked(&self) -> bool {
self.blocked
}
pub fn touchdown(&self) -> bool {
self.touchdown
}
pub fn extra_point(&self) -> bool {
self.extra_point
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize)]
pub struct FieldGoalResultBuilder {
field_goal_distance: i32,
return_yards: i32,
play_duration: u32,
made: bool,
blocked: bool,
touchdown: bool,
extra_point: bool
}
impl Default for FieldGoalResultBuilder {
fn default() -> Self {
FieldGoalResultBuilder{
field_goal_distance: 17,
return_yards: 0,
play_duration: 0,
made: true,
blocked: false,
touchdown: false,
extra_point: true
}
}
}
impl FieldGoalResultBuilder {
pub fn new() -> FieldGoalResultBuilder {
FieldGoalResultBuilder::default()
}
pub fn field_goal_distance(mut self, field_goal_distance: i32) -> Self {
self.field_goal_distance = field_goal_distance;
self
}
pub fn return_yards(mut self, return_yards: i32) -> Self {
self.return_yards = return_yards;
self
}
pub fn play_duration(mut self, play_duration: u32) -> Self {
self.play_duration = play_duration;
self
}
pub fn made(mut self, made: bool) -> Self {
self.made = made;
self
}
pub fn blocked(mut self, blocked: bool) -> Self {
self.blocked = blocked;
self
}
pub fn touchdown(mut self, touchdown: bool) -> Self {
self.touchdown = touchdown;
self
}
pub fn extra_point(mut self, extra_point: bool) -> Self {
self.extra_point = extra_point;
self
}
pub fn build(self) -> Result<FieldGoalResult, String> {
let raw = FieldGoalResultRaw{
field_goal_distance: self.field_goal_distance,
return_yards: self.return_yards,
play_duration: self.play_duration,
made: self.made,
blocked: self.blocked,
touchdown: self.touchdown,
extra_point: self.extra_point
};
FieldGoalResult::try_from(raw)
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Default, Serialize, Deserialize)]
pub struct FieldGoalResultSimulator {}
impl FieldGoalResultSimulator {
pub fn new() -> FieldGoalResultSimulator {
FieldGoalResultSimulator{}
}
fn blocked(&self, norm_diff_blocking: f64, yard_line: i32, rng: &mut impl Rng) -> bool {
let p_blocked_skill: f64 = P_BLOCKED_SKILL_INTR + (P_BLOCKED_SKILL_COEF * norm_diff_blocking);
let p_blocked_yardline: f64 = (P_BLOCKED_YARD_LINE_INTR + (P_BLOCKED_YARD_LINE_COEF * yard_line as f64)).exp();
let p_blocked: f64 = 1_f64.min(0_f64.max(
0.7_f64 * ((p_blocked_skill * 0.9_f64) + (p_blocked_yardline * 0.1_f64))
));
rng.gen::<f64>() < p_blocked
}
fn return_yards(&self, rng: &mut impl Rng) -> i32 {
Exp::new(1_f64).unwrap().sample(rng).round() as i32
}
fn made(&self, norm_kicking: f64, yard_line: i32, rng: &mut impl Rng) -> bool {
let p_made_skill: f64 = P_FIELD_GOAL_MADE_SKILL_INTR + (P_FIELD_GOAL_MADE_SKILL_COEF * norm_kicking);
let p_made_yardline: f64 = P_FIELD_GOAL_MADE_YARD_LINE_INTR + (P_FIELD_GOAL_MADE_YARD_LINE_COEF_1 * yard_line as f64) +
(P_FIELD_GOAL_MADE_YARD_LINE_COEF_2 * yard_line.pow(2) as f64);
let p_made: f64 = 0.9999_f64.min(
0.0001_f64.max(
(
1.18_f64 * ((p_made_skill * 0.5_f64) + (p_made_yardline * 0.5_f64))
).max(0.0001).ln() + 1.0
)
);
rng.gen::<f64>() < p_made
}
fn play_duration(&self, is_blocked: bool, rng: &mut impl Rng) -> u32 {
let duration_dist = if is_blocked {
SkewNormal::new(FIELD_GOAL_BLOCKED_DURATION_MEAN, FIELD_GOAL_BLOCKED_DURATION_STD, FIELD_GOAL_BLOCKED_DURATION_SKEW).unwrap()
} else {
SkewNormal::new(FIELD_GOAL_NOT_BLOCKED_DURATION_MEAN, FIELD_GOAL_NOT_BLOCKED_DURATION_STD, FIELD_GOAL_NOT_BLOCKED_DURATION_SKEW).unwrap()
};
u32::try_from(duration_dist.sample(rng).round() as i32).unwrap_or_default()
}
}
impl PlayResultSimulator for FieldGoalResultSimulator {
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_diff_blocking: f64 = 0.5_f64 + (
(
offense.offense().blocking_advantage(offense_advantage) as f64 -
defense.defense().blitzing_advantage(defense_advantage) as f64
) / 200_f64
);
let norm_kicking: f64 = offense.offense().field_goals_advantage(offense_advantage) as f64 / 100_f64;
let td_yards: i32 = context.yards_to_touchdown();
let safety_yards: i32 = context.yards_to_safety();
let blocked: bool = self.blocked(norm_diff_blocking, td_yards, rng);
let return_yards: i32 = if blocked {
self.return_yards(rng)
} else {
0
};
let made: bool = if !blocked {
self.made(norm_kicking, td_yards, rng)
} else {
false
};
let play_duration: u32 = self.play_duration(blocked, rng);
let touchdown: bool = blocked && (return_yards > safety_yards.abs());
let raw = FieldGoalResultRaw{
field_goal_distance: td_yards + 17,
return_yards,
play_duration,
made,
blocked,
touchdown,
extra_point: context.next_play_extra_point()
};
let fg_res = FieldGoalResult::try_from(raw).unwrap();
PlayTypeResult::FieldGoal(fg_res)
}
}