#![doc = include_str!("../../docs/game/score.md")]
pub mod freq;
use lazy_static::lazy_static;
use rand::Rng;
use rand_distr::{Normal, Distribution, Bernoulli};
#[cfg(feature = "rocket_okapi")]
use rocket_okapi::okapi::schemars;
#[cfg(feature = "rocket_okapi")]
use rocket_okapi::okapi::schemars::JsonSchema;
use serde::{Serialize, Deserialize, Deserializer};
use statrs::distribution::Categorical;
use crate::game::score::freq::ScoreFrequencyLookup;
use crate::team::{DEFAULT_TEAM_NAME};
const H_MEAN_COEF: f64 = 23.14578315_f64;
const H_MEAN_INTERCEPT: f64 = 10.9716991_f64;
const H_STD_INTERCEPT: f64 = 7.64006156_f64;
const H_STD_COEF_1: f64 = 5.72612946_f64;
const H_STD_COEF_2: f64 = -4.29283414_f64;
const A_MEAN_COEF: f64 = 22.14952374_f64;
const A_MEAN_INTERCEPT: f64 = 8.92113289_f64;
const A_STD_INTERCEPT: f64 = 6.47638621_f64;
const A_STD_COEF_1: f64 = 8.00861267_f64;
const A_STD_COEF_2: f64 = -5.589282_f64;
const P_TIE_COEF: f64 = -0.00752297_f64;
const P_TIE_INTERCEPT: f64 = 0.01055039_f64;
const P_TIE_BASE: f64 = 0.036_f64;
lazy_static!{
static ref SCORE_FREQ_LUT: ScoreFrequencyLookup = {
let mut tmp_lut = ScoreFrequencyLookup::new();
tmp_lut.create();
tmp_lut
};
}
pub trait ScoreSimulatable {
fn name(&self) -> &str { DEFAULT_TEAM_NAME }
fn defense_overall(&self) -> u32 { 50_u32 }
fn offense_overall(&self) -> u32 { 50_u32 }
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Default, Serialize, Deserialize)]
pub struct FinalScoreRaw {
home_team: String,
home_score: u32,
away_team: String,
away_score: u32
}
impl FinalScoreRaw {
pub fn validate(&self) -> Result<(), String> {
if self.home_team.len() > 64 {
return Err(
format!(
"Home team name is longer than 64 characters: {}",
self.home_team
)
)
}
if self.away_team.len() > 64 {
return Err(
format!(
"Away team name is longer than 64 characters: {}",
self.away_team
)
)
}
Ok(())
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize)]
pub struct FinalScore {
home_team: String,
home_score: u32,
away_team: String,
away_score: u32
}
impl TryFrom<FinalScoreRaw> for FinalScore {
type Error = String;
fn try_from(item: FinalScoreRaw) -> Result<Self, Self::Error> {
match item.validate() {
Ok(()) => (),
Err(error) => return Err(error),
};
Ok(
FinalScore{
home_team: item.home_team,
home_score: item.home_score,
away_team: item.away_team,
away_score: item.away_score
}
)
}
}
impl<'de> Deserialize<'de> for FinalScore {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = FinalScoreRaw::deserialize(deserializer)?;
FinalScore::try_from(raw).map_err(serde::de::Error::custom)
}
}
impl Default for FinalScore {
fn default() -> Self {
FinalScore {
home_team: String::from(DEFAULT_TEAM_NAME),
home_score: 0_u32,
away_team: String::from(DEFAULT_TEAM_NAME),
away_score: 0_u32
}
}
}
impl FinalScore {
pub fn new() -> FinalScore {
FinalScore::default()
}
pub fn home_score(&self) -> u32 {
self.home_score
}
pub fn away_score(&self) -> u32 {
self.away_score
}
}
impl std::fmt::Display for FinalScore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let score_str = format!(
"{} {} - {} {}",
self.home_team,
self.home_score,
self.away_team,
self.away_score
);
f.write_str(&score_str)
}
}
#[cfg_attr(feature = "rocket_okapi", derive(JsonSchema))]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize)]
pub struct FinalScoreBuilder {
home_team: String,
home_score: u32,
away_team: String,
away_score: u32
}
impl Default for FinalScoreBuilder {
fn default() -> Self {
FinalScoreBuilder {
home_team: String::from(DEFAULT_TEAM_NAME),
home_score: 0_u32,
away_team: String::from(DEFAULT_TEAM_NAME),
away_score: 0_u32
}
}
}
impl FinalScoreBuilder {
pub fn new() -> FinalScoreBuilder {
FinalScoreBuilder::default()
}
pub fn home_team(mut self, home_team: &str) -> Self {
self.home_team = String::from(home_team);
self
}
pub fn away_team(mut self, away_team: &str) -> Self {
self.away_team = String::from(away_team);
self
}
pub fn home_score(mut self, home_score: u32) -> Self {
self.home_score = home_score;
self
}
pub fn away_score(mut self, away_score: u32) -> Self {
self.away_score = away_score;
self
}
pub fn build(self) -> Result<FinalScore, String> {
let raw = FinalScoreRaw {
home_team: self.home_team,
home_score: self.home_score,
away_team: self.away_team,
away_score: self.away_score
};
FinalScore::try_from(raw)
}
}
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default)]
pub struct FinalScoreSimulator;
impl FinalScoreSimulator {
pub fn new() -> FinalScoreSimulator {
FinalScoreSimulator{}
}
fn get_mean_score(&self, norm_diff: f64, home: bool) -> f64 {
if home {
H_MEAN_INTERCEPT + (H_MEAN_COEF * norm_diff)
} else {
A_MEAN_INTERCEPT + (A_MEAN_COEF * norm_diff)
}
}
fn get_std_score(&self, norm_diff: f64, home: bool) -> f64 {
if home {
H_STD_INTERCEPT + (H_STD_COEF_1 * norm_diff) + (H_STD_COEF_2 * norm_diff.powi(2))
} else {
A_STD_INTERCEPT + (A_STD_COEF_1 * norm_diff) + (A_STD_COEF_2 * norm_diff.powi(2))
}
}
fn get_normal_params(&self, norm_diff: f64, home: bool) -> (f64, f64) {
(self.get_mean_score(norm_diff, home), self.get_std_score(norm_diff, home))
}
fn get_p_tie(&self, norm_diff: f64) -> f64 {
P_TIE_INTERCEPT + (P_TIE_COEF * norm_diff)
}
fn get_p_resim(&self, p_tie: f64) -> f64 {
(p_tie - P_TIE_BASE) / (P_TIE_BASE.powi(2) - P_TIE_BASE)
}
fn gen_away_score(&self, norm_diff: f64, rng: &mut impl Rng) -> u32 {
let (mean, std): (f64, f64) = self.get_normal_params(norm_diff, false);
let away_dist = Normal::new(mean, std).unwrap();
let away_score_float = away_dist.sample(rng);
u32::try_from(away_score_float.round() as i32).unwrap_or_default()
}
fn gen_home_score(&self, norm_diff: f64, rng: &mut impl Rng) -> u32 {
let (mean, std) = self.get_normal_params(norm_diff, true);
let home_dist = Normal::new(mean, std).unwrap();
let home_score_float = home_dist.sample(rng);
u32::try_from(home_score_float.round() as i32).unwrap_or_default()
}
fn gen_score(&self, ha_norm_diff: f64, ah_norm_diff: f64, rng: &mut impl Rng) -> Result<(u32, u32), String> {
if !(0.0_f64..=1.0_f64).contains(&ha_norm_diff) {
return Err(
format!(
"Home offense / away defense normalized skill differential not in range [0, 1]: {}",
ha_norm_diff
)
)
}
if !(0.0_f64..=1.0_f64).contains(&ah_norm_diff) {
return Err(
format!(
"Away offense / home defense normalized skill differential not in range [0, 1]: {}",
ah_norm_diff
)
)
}
Ok((self.gen_home_score(ha_norm_diff, rng), self.gen_away_score(ah_norm_diff, rng)))
}
fn filter_score(&self, score: u32, rng: &mut impl Rng) -> u32 {
if score == 0 {
return 0
}
let low = SCORE_FREQ_LUT.frequency(score - 1).unwrap();
let mid = SCORE_FREQ_LUT.frequency(score).unwrap();
let high = SCORE_FREQ_LUT.frequency(score + 1).unwrap();
let dist = Categorical::new(&[low as f64, mid as f64, high as f64]).unwrap();
let score_adjustment_r: f64 = dist.sample(rng);
let score_adjustment = (score_adjustment_r as i32) - 1_i32;
let adj_score = score as i32 + score_adjustment;
u32::try_from(adj_score).unwrap_or_default()
}
pub fn sim(&self, home_team: &impl ScoreSimulatable, away_team: &impl ScoreSimulatable, rng: &mut impl Rng) -> Result<FinalScore, String> {
let ha_norm_diff: f64 = (home_team.offense_overall() as i32 - away_team.defense_overall() as i32 + 100_i32) as f64 / 200_f64;
let ah_norm_diff: f64 = (away_team.offense_overall() as i32 - home_team.defense_overall() as i32 + 100_i32) as f64 / 200_f64;
let (home_score, away_score): (u32, u32) = self.gen_score(ha_norm_diff, ah_norm_diff, rng)?;
let adj_home_score = self.filter_score(home_score, rng);
let adj_away_score = self.filter_score(away_score, rng);
let final_score: FinalScore = FinalScoreBuilder::new()
.home_team(home_team.name())
.home_score(adj_home_score)
.away_team(away_team.name())
.away_score(adj_away_score)
.build()
.unwrap();
if adj_home_score != adj_away_score {
return Ok(final_score)
}
let avg_norm_diff: f64 = (ha_norm_diff + ah_norm_diff) / 2_f64;
let p_tie: f64 = self.get_p_tie(avg_norm_diff);
let p_res: f64 = self.get_p_resim(p_tie);
let dst_res: Bernoulli = Bernoulli::new(p_res).unwrap();
let res: bool = dst_res.sample(rng);
if res {
let (home_score_2, away_score_2): (u32, u32) = self.gen_score(ha_norm_diff, ah_norm_diff, rng)?;
let adj_home_score_2 = self.filter_score(home_score_2, rng);
let adj_away_score_2 = self.filter_score(away_score_2, rng);
let final_score_2: FinalScore = FinalScoreBuilder::new()
.home_team(home_team.name())
.home_score(adj_home_score_2)
.away_team(away_team.name())
.away_score(adj_away_score_2)
.build()
.unwrap();
return Ok(final_score_2)
}
Ok(final_score)
}
}