#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
dwz::DWZRating, fifa::FifaRating, ingo::IngoRating, uscf::USCFRating, Outcomes, Rating,
RatingPeriodSystem, RatingSystem,
};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EloRating {
pub rating: f64,
}
impl EloRating {
#[must_use]
pub const fn new() -> Self {
Self { rating: 1000.0 }
}
}
impl Default for EloRating {
fn default() -> Self {
Self::new()
}
}
impl Rating for EloRating {
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 EloRating {
fn from(r: f64) -> Self {
Self { rating: r }
}
}
impl From<IngoRating> for EloRating {
fn from(i: IngoRating) -> Self {
Self {
rating: 8.0f64.mul_add(-i.rating, 2840.0),
}
}
}
impl From<DWZRating> for EloRating {
fn from(d: DWZRating) -> Self {
Self { rating: d.rating }
}
}
impl From<USCFRating> for EloRating {
fn from(u: USCFRating) -> Self {
if u.rating > 2060.0 {
Self {
rating: (u.rating - 180.0) / 0.94,
}
} else {
Self {
rating: (u.rating - 20.0) / 1.02,
}
}
}
}
impl From<FifaRating> for EloRating {
fn from(f: FifaRating) -> Self {
Self { rating: f.rating }
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EloConfig {
pub k: f64,
}
impl EloConfig {
#[must_use]
pub const fn new() -> Self {
Self { k: 32.0 }
}
}
impl Default for EloConfig {
fn default() -> Self {
Self::new()
}
}
pub struct Elo {
config: EloConfig,
}
impl RatingSystem for Elo {
type RATING = EloRating;
type CONFIG = EloConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(
&self,
player_one: &EloRating,
player_two: &EloRating,
outcome: &Outcomes,
) -> (EloRating, EloRating) {
elo(player_one, player_two, outcome, &self.config)
}
fn expected_score(&self, player_one: &EloRating, player_two: &EloRating) -> (f64, f64) {
expected_score(player_one, player_two)
}
}
impl RatingPeriodSystem for Elo {
type RATING = EloRating;
type CONFIG = EloConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(&self, player: &EloRating, results: &[(EloRating, Outcomes)]) -> EloRating {
elo_rating_period(player, results, &self.config)
}
fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
expected_score_rating_period(player, opponents)
}
}
#[must_use]
pub fn elo(
player_one: &EloRating,
player_two: &EloRating,
outcome: &Outcomes,
config: &EloConfig,
) -> (EloRating, EloRating) {
let (one_expected, two_expected) = expected_score(player_one, player_two);
let outcome1 = outcome.to_chess_points();
let outcome2 = 1.0 - outcome1;
let one_new_elo = config.k.mul_add(outcome1 - one_expected, player_one.rating);
let two_new_elo = config.k.mul_add(outcome2 - two_expected, player_two.rating);
(
EloRating {
rating: one_new_elo,
},
EloRating {
rating: two_new_elo,
},
)
}
#[must_use]
pub fn elo_rating_period(
player: &EloRating,
results: &[(EloRating, Outcomes)],
config: &EloConfig,
) -> EloRating {
let mut player_rating = player.rating;
for (opponent, result) in results {
let exp = (1.0 + 10_f64.powf((opponent.rating - player_rating) / 400.0)).recip();
let outcome = result.to_chess_points();
player_rating = config.k.mul_add(outcome - exp, player_rating);
}
EloRating {
rating: player_rating,
}
}
#[must_use]
pub fn expected_score(player_one: &EloRating, player_two: &EloRating) -> (f64, f64) {
let exp_one = (1.0 + 10_f64.powf((player_two.rating - player_one.rating) / 400.0)).recip();
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
#[must_use]
pub fn expected_score_rating_period(player: &EloRating, opponents: &[EloRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| (1.0 + 10_f64.powf((o.rating - player.rating) / 400.0)).recip())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_elo() {
let (winner_new_elo, loser_new_elo) = elo(
&EloRating { rating: 1000.0 },
&EloRating { rating: 1000.0 },
&Outcomes::WIN,
&EloConfig::new(),
);
assert!((winner_new_elo.rating - 1016.0).abs() < f64::EPSILON);
assert!((loser_new_elo.rating - 984.0).abs() < f64::EPSILON);
let (winner_new_elo, loser_new_elo) = elo(
&EloRating { rating: 1000.0 },
&EloRating { rating: 1000.0 },
&Outcomes::LOSS,
&EloConfig::new(),
);
assert!((winner_new_elo.rating - 984.0).abs() < f64::EPSILON);
assert!((loser_new_elo.rating - 1016.0).abs() < f64::EPSILON);
let (winner_new_elo, loser_new_elo) = elo(
&EloRating { rating: 1000.0 },
&EloRating { rating: 1000.0 },
&Outcomes::DRAW,
&EloConfig::new(),
);
assert!((winner_new_elo.rating - 1000.0).abs() < f64::EPSILON);
assert!((loser_new_elo.rating - 1000.0).abs() < f64::EPSILON);
let (winner_new_elo, loser_new_elo) = elo(
&EloRating { rating: 500.0 },
&EloRating { rating: 1500.0 },
&Outcomes::WIN,
&EloConfig::default(),
);
assert!((winner_new_elo.rating.round() - 532.0).abs() < f64::EPSILON);
assert!((loser_new_elo.rating.round() - 1468.0).abs() < f64::EPSILON);
}
#[test]
fn test_elo_rating_period() {
let player = EloRating::new();
let opponent1 = EloRating::new();
let opponent2 = EloRating::new();
let opponent3 = EloRating::new();
let new_player = elo_rating_period(
&player,
&[
(opponent1, Outcomes::WIN),
(opponent2, Outcomes::DRAW),
(opponent3, Outcomes::LOSS),
],
&EloConfig::new(),
);
assert!((new_player.rating.round() - 999.0).abs() < f64::EPSILON);
}
#[test]
fn test_expected_score() {
let player_one = EloRating::new();
let player_two = EloRating::default();
let (winner_expected, loser_expected) = expected_score(&player_one, &player_two);
assert!((winner_expected - 0.5).abs() < f64::EPSILON);
assert!((loser_expected - 0.5).abs() < f64::EPSILON);
let player_one = EloRating { rating: 2251.0 };
let player_two = EloRating { rating: 1934.0 };
let (winner_expected, loser_expected) = expected_score(&player_one, &player_two);
assert!(((winner_expected * 100.0).round() - 86.0).abs() < f64::EPSILON);
assert!(((loser_expected * 100.0).round() - 14.0).abs() < f64::EPSILON);
assert!((winner_expected + loser_expected - 1.0).abs() < f64::EPSILON);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let player_one = EloRating::new();
let config = EloConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.k - config.clone().k).abs() < f64::EPSILON);
assert!(!format!("{player_one:?}").is_empty());
assert!(!format!("{config:?}").is_empty());
assert_eq!(player_one, EloRating::from(1000.0));
}
#[test]
fn test_traits() {
let player_one: EloRating = Rating::new(Some(240.0), Some(90.0));
let player_two: EloRating = Rating::new(Some(240.0), Some(90.0));
let rating_system: Elo = RatingSystem::new(EloConfig::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 - 256.0).abs() < f64::EPSILON);
assert!((new_player_two.rating - 224.0).abs() < f64::EPSILON);
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);
let rating_period_system: Elo = RatingPeriodSystem::new(EloConfig::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: EloRating = Rating::new(Some(240.0), Some(90.0));
let player_two: EloRating = Rating::new(Some(240.0), Some(90.0));
let rating_period: Elo = RatingPeriodSystem::new(EloConfig::new());
let new_player_one =
RatingPeriodSystem::rate(&rating_period, &player_one, &[(player_two, Outcomes::WIN)]);
assert!((new_player_one.rating - 256.0).abs() < f64::EPSILON);
}
}