#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{Outcomes, Rating, RatingPeriodSystem, RatingSystem};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EGFRating {
pub rating: f64,
}
impl EGFRating {
#[must_use]
pub const fn new() -> Self {
Self { rating: 0.0 }
}
}
impl Default for EGFRating {
fn default() -> Self {
Self::new()
}
}
impl Rating for EGFRating {
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(0.0),
}
}
}
impl From<f64> for EGFRating {
fn from(r: f64) -> Self {
Self { rating: r }
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EGFConfig {
pub handicap: f64,
}
impl EGFConfig {
#[must_use]
pub const fn new() -> Self {
Self { handicap: 0.0 }
}
}
impl Default for EGFConfig {
fn default() -> Self {
Self::new()
}
}
pub struct EGF {
config: EGFConfig,
}
impl RatingSystem for EGF {
type RATING = EGFRating;
type CONFIG = EGFConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(
&self,
player_one: &EGFRating,
player_two: &EGFRating,
outcome: &Outcomes,
) -> (EGFRating, EGFRating) {
egf(player_one, player_two, outcome, &self.config)
}
fn expected_score(&self, player_one: &EGFRating, player_two: &EGFRating) -> (f64, f64) {
expected_score(player_one, player_two, &self.config)
}
}
impl RatingPeriodSystem for EGF {
type RATING = EGFRating;
type CONFIG = EGFConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(&self, player: &EGFRating, results: &[(EGFRating, Outcomes)]) -> EGFRating {
let new_results: Vec<(EGFRating, Outcomes, EGFConfig)> =
results.iter().map(|r| (r.0, r.1, self.config)).collect();
egf_rating_period(player, &new_results[..])
}
fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
let new_opponents: Vec<(EGFRating, EGFConfig)> =
opponents.iter().map(|o| (*o, self.config)).collect();
expected_score_rating_period(player, &new_opponents)
}
}
#[must_use]
pub fn egf(
player_one: &EGFRating,
player_two: &EGFRating,
outcome: &Outcomes,
config: &EGFConfig,
) -> (EGFRating, EGFRating) {
let con1 = con(player_one.rating);
let con2 = con(player_two.rating);
let bonus1 = bonus(player_one.rating);
let bonus2 = bonus(player_two.rating);
let (exp1, exp2) = expected_score(player_one, player_two, config);
let outcome1 = outcome.to_chess_points();
let outcome2 = 1.0 - outcome1;
let new_rating1 = new_rating(player_one.rating, con1, outcome1, exp1, bonus1);
let new_rating2 = new_rating(player_two.rating, con2, outcome2, exp2, bonus2);
(
EGFRating {
rating: new_rating1,
},
EGFRating {
rating: new_rating2,
},
)
}
#[must_use]
pub fn egf_rating_period(
player: &EGFRating,
results: &[(EGFRating, Outcomes, EGFConfig)],
) -> EGFRating {
let mut rating_change = 0.0;
let con = con(player.rating);
let bonus = bonus(player.rating);
for (opponent, result, config) in results {
let (exp1, _) = expected_score(player, opponent, config);
let outcome = result.to_chess_points();
rating_change += new_rating(player.rating, con, outcome, exp1, bonus) - player.rating;
}
EGFRating {
rating: player.rating + rating_change,
}
}
#[must_use]
pub fn expected_score(
player_one: &EGFRating,
player_two: &EGFRating,
config: &EGFConfig,
) -> (f64, f64) {
let (h1, h2) = if config.handicap.is_sign_negative() {
(config.handicap.abs(), 0.0)
} else {
(0.0, config.handicap.abs())
};
let exp_one = (1.0 + (beta(player_two.rating, h2) - beta(player_one.rating, h1)).exp()).recip();
(exp_one, 1.0 - exp_one)
}
#[must_use]
pub fn expected_score_rating_period(
player: &EGFRating,
opponents: &[(EGFRating, EGFConfig)],
) -> Vec<f64> {
opponents
.iter()
.map(|o| {
let (h1, h2) = if o.1.handicap.is_sign_negative() {
(o.1.handicap.abs(), 0.0)
} else {
(0.0, o.1.handicap.abs())
};
(1.0 + (beta(o.0.rating, h2) - beta(player.rating, h1)).exp()).recip()
})
.collect()
}
fn new_rating(rating: f64, con: f64, score: f64, exp_score: f64, bonus: f64) -> f64 {
(con.mul_add(score - exp_score, rating) + bonus).max(-900.0)
}
fn con(rating: f64) -> f64 {
((3300.0 - rating) / 200.0).powf(1.6)
}
fn bonus(rating: f64) -> f64 {
((2300.0 - rating) / 80.0).exp().ln_1p() / 5.0
}
fn beta(rating: f64, handicap: f64) -> f64 {
let h = if handicap == 0.0 {
0.0
} else {
100.0 * (handicap - 0.5)
};
-7.0 * (3300.0 - h - rating).ln()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handicap() {
let config = EGFConfig { handicap: 0.0 };
let player_one = EGFRating { rating: 1000.0 };
let player_two = EGFRating { rating: 1300.0 };
let (new_one, new_two) = expected_score(&player_one, &player_two, &config);
assert!((new_one - 0.273_222_559_619_518_8).abs() < f64::EPSILON);
assert!((new_two - 0.726_777_440_380_481_2).abs() < f64::EPSILON);
assert!((new_one + new_two - 1.0).abs() < f64::EPSILON);
let handicap_config = EGFConfig { handicap: 1.0 };
let (new_one, new_two) = expected_score(&player_one, &player_two, &handicap_config);
assert!((new_one - 0.239_475_310_432_178_62).abs() < f64::EPSILON);
assert!((new_two - 0.760_524_689_567_821_4).abs() < f64::EPSILON);
assert!((new_one + new_two - 1.0).abs() < f64::EPSILON);
let other_handicap_config = EGFConfig { handicap: -1.0 };
let (new_one, new_two) = expected_score(&player_one, &player_two, &other_handicap_config);
assert!((new_one - 0.304_813_243_836_844_05).abs() < f64::EPSILON);
assert!((new_two - 0.695_186_756_163_156).abs() < f64::EPSILON);
assert!((new_one + new_two - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_egf() {
let player_one = EGFRating { rating: 1000.0 };
let player_two = EGFRating { rating: 1300.0 };
let config = EGFConfig::new();
let (new_one, new_two) = egf(&player_one, &player_two, &Outcomes::WIN, &config);
assert!((new_one.rating.round() - 1039.0).abs() < f64::EPSILON);
assert!((new_two.rating.round() - 1274.0).abs() < f64::EPSILON);
let player_one = EGFRating { rating: 0.0 };
let player_two = EGFRating::default();
let (new_one, new_two) = egf(&player_one, &player_two, &Outcomes::LOSS, &config);
assert!((new_one.rating.round() - -39.0).abs() < f64::EPSILON);
assert!((new_two.rating.round() - 50.0).abs() < f64::EPSILON);
let (new_one, new_two) = egf(&player_one, &player_two, &Outcomes::DRAW, &config);
assert!((new_one.rating.round() - 6.0).abs() < f64::EPSILON);
assert!((new_two.rating.round() - 6.0).abs() < f64::EPSILON);
}
#[test]
fn test_egf_rating_period() {
let player = EGFRating::new();
let config = EGFConfig::default();
let results = vec![
(EGFRating { rating: 20.0 }, Outcomes::WIN, config),
(EGFRating { rating: 40.0 }, Outcomes::DRAW, config),
(EGFRating { rating: 10.0 }, Outcomes::LOSS, config),
(EGFRating { rating: -40.0 }, Outcomes::WIN, config),
];
let new_player = egf_rating_period(&player, &results);
assert!((new_player.rating.round() - 69.0).abs() < f64::EPSILON);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let player_one = EGFRating::new();
let config = EGFConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.handicap - config.clone().handicap).abs() < f64::EPSILON);
assert!(!format!("{player_one:?}").is_empty());
assert!(!format!("{config:?}").is_empty());
assert_eq!(player_one, EGFRating::from(0.0));
}
#[test]
fn test_traits() {
let player_one: EGFRating = Rating::new(Some(240.0), Some(90.0));
let player_two: EGFRating = Rating::new(Some(240.0), Some(90.0));
let rating_system: EGF = RatingSystem::new(EGFConfig::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 - 284.457_578_792_560_87).abs() < f64::EPSILON);
assert!((new_player_two.rating - 205.842_421_207_441_73).abs() < f64::EPSILON);
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);
let rating_period_system: EGF = RatingPeriodSystem::new(EGFConfig::new());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);
let rating_period_system2: EGF = RatingPeriodSystem::new(EGFConfig { handicap: -0.0 });
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system2, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);
let player_one: EGFRating = Rating::new(Some(240.0), Some(90.0));
let player_two: EGFRating = Rating::new(Some(240.0), Some(90.0));
let rating_period: EGF = RatingPeriodSystem::new(EGFConfig::new());
let new_player_one =
RatingPeriodSystem::rate(&rating_period, &player_one, &[(player_two, Outcomes::WIN)]);
assert!((new_player_one.rating - 284.457_578_792_560_87).abs() < f64::EPSILON);
}
}