#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{elo::EloRating, Outcomes, Rating, RatingPeriodSystem, RatingSystem};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FifaRating {
pub rating: f64,
}
impl FifaRating {
#[must_use]
pub const fn new() -> Self {
Self { rating: 1000.0 }
}
}
impl Default for FifaRating {
fn default() -> Self {
Self::new()
}
}
impl Rating for FifaRating {
fn rating(&self) -> f64 {
self.rating
}
fn uncertainty(&self) -> Option<f64> {
None
}
fn new(rating: Option<f64>, _uncertainty: Option<f64>) -> Self {
Self {
rating: rating.unwrap_or(1000.0),
}
}
}
impl From<f64> for FifaRating {
fn from(r: f64) -> Self {
Self { rating: r }
}
}
impl From<EloRating> for FifaRating {
fn from(e: EloRating) -> Self {
Self { rating: e.rating }
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FifaConfig {
pub importance: f64,
pub knockout: bool,
pub penalties: bool,
}
impl FifaConfig {
#[must_use]
pub const fn new() -> Self {
Self {
importance: 10.0,
knockout: false,
penalties: false,
}
}
}
impl Default for FifaConfig {
fn default() -> Self {
Self::new()
}
}
pub struct Fifa {
config: FifaConfig,
}
impl RatingSystem for Fifa {
type RATING = FifaRating;
type CONFIG = FifaConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(
&self,
player_one: &FifaRating,
player_two: &FifaRating,
outcome: &Outcomes,
) -> (FifaRating, FifaRating) {
fifa(player_one, player_two, outcome, &self.config)
}
fn expected_score(&self, player_one: &FifaRating, player_two: &FifaRating) -> (f64, f64) {
expected_score(player_one, player_two)
}
}
impl RatingPeriodSystem for Fifa {
type RATING = FifaRating;
type CONFIG = FifaConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(&self, player: &FifaRating, results: &[(FifaRating, Outcomes)]) -> FifaRating {
let new_results: Vec<(FifaRating, Outcomes, FifaConfig)> =
results.iter().map(|r| (r.0, r.1, self.config)).collect();
fifa_rating_period(player, &new_results[..])
}
fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
expected_score_rating_period(player, opponents)
}
}
#[must_use]
pub fn fifa(
player_one: &FifaRating,
player_two: &FifaRating,
outcome: &Outcomes,
config: &FifaConfig,
) -> (FifaRating, FifaRating) {
let (one_expected, two_expected) = expected_score(player_one, player_two);
let outcome1 = if config.penalties {
if outcome == &Outcomes::WIN {
0.75
} else {
0.5
}
} else {
outcome.to_chess_points()
};
let outcome2 = if config.penalties {
if outcome == &Outcomes::WIN {
0.75
} else {
0.5
}
} else {
1.0 - outcome1
};
let mut new_rating1 = config
.importance
.mul_add(outcome1 - one_expected, player_one.rating);
let mut new_rating2 = config
.importance
.mul_add(outcome2 - two_expected, player_two.rating);
if config.knockout && player_one.rating > new_rating1 {
new_rating1 = player_one.rating;
}
if config.knockout && player_two.rating > new_rating2 {
new_rating2 = player_two.rating;
}
(
FifaRating {
rating: new_rating1,
},
FifaRating {
rating: new_rating2,
},
)
}
#[must_use]
pub fn fifa_rating_period(
player: &FifaRating,
results: &[(FifaRating, Outcomes, FifaConfig)],
) -> FifaRating {
let mut player_rating = player.rating;
for (opponent, result, config) in results {
let exp = (1.0 + 10_f64.powf(-(player_rating - opponent.rating) / 600.0)).recip();
let outcome = if config.penalties {
if result == &Outcomes::WIN {
0.75
} else {
0.5
}
} else {
result.to_chess_points()
};
let new_rating = config.importance.mul_add(outcome - exp, player_rating);
if !(config.knockout && player_rating > new_rating) {
player_rating = new_rating;
}
}
FifaRating {
rating: player_rating,
}
}
#[must_use]
pub fn expected_score(player_one: &FifaRating, player_two: &FifaRating) -> (f64, f64) {
let exp_one = (1.0 + 10_f64.powf(-(player_one.rating - player_two.rating) / 600.0)).recip();
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
#[must_use]
pub fn expected_score_rating_period(player: &FifaRating, opponents: &[FifaRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| (1.0 + 10_f64.powf(-(player.rating - o.rating) / 600.0)).recip())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fifa() {
let team_a = FifaRating::from(1300.0);
let team_b = FifaRating { rating: 1500.0 };
let config = FifaConfig {
importance: 25.0,
knockout: false,
penalties: false,
};
let (new_a, new_b) = fifa(&team_a, &team_b, &Outcomes::WIN, &config);
assert!((new_a.rating.round() - 1317.0).abs() < f64::EPSILON);
assert!((new_b.rating.round() - 1483.0).abs() < f64::EPSILON);
let new = fifa_rating_period(&team_a, &[(team_b, Outcomes::WIN, config)]);
assert_eq!(new, new_a);
let new = fifa_rating_period(&team_b, &[(team_a, Outcomes::LOSS, config)]);
assert_eq!(new, new_b);
}
#[test]
fn test_fifa_rating_period() {
let mut team_a = FifaRating::new();
let mut results = Vec::new();
let team_b = FifaRating::default();
let team_c = FifaRating::from(1600.0);
let team_d = FifaRating::from(890.0);
let team_e = FifaRating::from(1300.0);
results.push((team_b, Outcomes::WIN, FifaConfig::new()));
results.push((team_c, Outcomes::DRAW, FifaConfig::default()));
results.push((
team_d,
Outcomes::LOSS,
FifaConfig {
importance: 60.0,
knockout: true,
penalties: true,
},
));
results.push((
team_e,
Outcomes::WIN,
FifaConfig {
importance: 25.0,
knockout: true,
penalties: true,
},
));
let new_team = fifa_rating_period(&team_a, &results);
assert!((new_team.rating.round() - 1022.0).abs() < f64::EPSILON);
for r in results {
(team_a, _) = fifa(&team_a, &r.0, &r.1, &r.2);
}
assert_eq!(team_a, new_team);
}
#[test]
#[allow(clippy::similar_names)]
fn test_draws() {
let team_a = FifaRating::from(1300.0);
let team_b = FifaRating::from(1500.0);
let config1 = FifaConfig {
importance: 25.0,
knockout: false,
penalties: false,
};
let config2 = FifaConfig {
importance: 25.0,
knockout: false,
penalties: true,
};
let (new_a, new_b) = fifa(&team_a, &team_b, &Outcomes::DRAW, &config1);
let (new_a2, new_b2) = fifa(&team_a, &team_b, &Outcomes::DRAW, &config2);
assert_eq!(new_a, new_a2);
assert_eq!(new_b, new_b2);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let team_a = FifaRating::from(1300.0);
let team_b = EloRating::from(team_a);
assert_eq!(team_a, FifaRating::from(team_b));
let player_one = FifaRating::new();
let config = FifaConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.importance - config.clone().importance).abs() < f64::EPSILON);
assert!(!format!("{player_one:?}").is_empty());
assert!(!format!("{config:?}").is_empty());
assert_eq!(player_one, FifaRating::from(1000.));
}
#[test]
fn test_traits() {
let player_one: FifaRating = Rating::new(Some(240.0), Some(90.0));
let player_two: FifaRating = Rating::new(Some(240.0), Some(90.0));
let rating_system: Fifa = RatingSystem::new(FifaConfig::new());
assert!((player_one.rating() - 240.0).abs() < f64::EPSILON);
assert_eq!(player_one.uncertainty(), None);
let (new_player_one, new_player_two) =
RatingSystem::rate(&rating_system, &player_one, &player_two, &Outcomes::WIN);
let (exp1, exp2) = RatingSystem::expected_score(&rating_system, &player_one, &player_two);
assert!((new_player_one.rating - 245.0).abs() < f64::EPSILON);
assert!((new_player_two.rating - 235.0).abs() < f64::EPSILON);
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);
let rating_period_system: Fifa = RatingPeriodSystem::new(FifaConfig::new());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);
let player_one: FifaRating = Rating::new(Some(240.0), Some(90.0));
let player_two: FifaRating = Rating::new(Some(240.0), Some(90.0));
let rating_period: Fifa = RatingPeriodSystem::new(FifaConfig::new());
let new_player_one =
RatingPeriodSystem::rate(&rating_period, &player_one, &[(player_two, Outcomes::WIN)]);
assert!((new_player_one.rating - 245.0).abs() < f64::EPSILON);
}
}