#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
glicko2::Glicko2Rating, glicko_boost::GlickoBoostRating, sticko::StickoRating, Outcomes,
};
use std::f64::consts::PI;
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GlickoRating {
pub rating: f64,
pub deviation: f64,
}
impl GlickoRating {
#[must_use]
pub const fn new() -> Self {
Self {
rating: 1500.0,
deviation: 350.0,
}
}
}
impl Default for GlickoRating {
fn default() -> Self {
Self::new()
}
}
impl From<(f64, f64)> for GlickoRating {
fn from((r, d): (f64, f64)) -> Self {
Self {
rating: r,
deviation: d,
}
}
}
impl From<Glicko2Rating> for GlickoRating {
fn from(g: Glicko2Rating) -> Self {
Self {
rating: g.rating,
deviation: g.deviation,
}
}
}
impl From<GlickoBoostRating> for GlickoRating {
fn from(g: GlickoBoostRating) -> Self {
Self {
rating: g.rating,
deviation: g.deviation,
}
}
}
impl From<StickoRating> for GlickoRating {
fn from(s: StickoRating) -> Self {
Self {
rating: s.rating,
deviation: s.deviation,
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GlickoConfig {
pub c: f64,
}
impl GlickoConfig {
#[must_use]
pub const fn new() -> Self {
Self { c: 63.2 }
}
}
impl Default for GlickoConfig {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn glicko(
player_one: &GlickoRating,
player_two: &GlickoRating,
outcome: &Outcomes,
config: &GlickoConfig,
) -> (GlickoRating, GlickoRating) {
let q = 10_f64.ln() / 400.0;
let outcome1 = outcome.to_chess_points();
let outcome2 = 1.0 - outcome1;
let g1 = g_value(q, player_two.deviation);
let g2 = g_value(q, player_one.deviation);
let e1 = e_value(g1, player_one.rating, player_two.rating);
let e2 = e_value(g2, player_two.rating, player_one.rating);
let d1 = d_value(q, g1, e1);
let d2 = d_value(q, g2, e2);
let player_one_pre_deviation = player_one.deviation.hypot(config.c).min(350.0);
let player_two_pre_deviation = player_two.deviation.hypot(config.c).min(350.0);
let player_one_new_rating = new_rating(
player_one.rating,
player_one_pre_deviation,
outcome1,
q,
g1,
e1,
d1,
);
let player_two_new_rating = new_rating(
player_two.rating,
player_two_pre_deviation,
outcome2,
q,
g2,
e2,
d2,
);
let player_one_new_deviation = new_deviation(player_one_pre_deviation, d1);
let player_two_new_deviation = new_deviation(player_two_pre_deviation, d2);
(
GlickoRating {
rating: player_one_new_rating,
deviation: player_one_new_deviation,
},
GlickoRating {
rating: player_two_new_rating,
deviation: player_two_new_deviation,
},
)
}
#[must_use]
pub fn glicko_rating_period(
player: &GlickoRating,
results: &[(GlickoRating, Outcomes)],
config: &GlickoConfig,
) -> GlickoRating {
let q = 10_f64.ln() / 400.0;
if results.is_empty() {
return decay_deviation(player, config);
}
let d_sq = (q.powi(2)
* results
.iter()
.map(|r| {
let g = g_value(q, r.0.deviation);
let e = e_value(g, player.rating, r.0.rating);
g.powi(2) * e * (1.0 - e)
})
.sum::<f64>())
.recip();
let m = results
.iter()
.map(|r| {
let g = g_value(q, r.0.deviation);
let e = e_value(g, player.rating, r.0.rating);
let s = r.1.to_chess_points();
g * (s - e)
})
.sum();
let pre_deviation = player.deviation.hypot(config.c).min(350.0);
let new_rating = (q / (pre_deviation.powi(2).recip() + d_sq.recip())).mul_add(m, player.rating);
let new_deviation = (pre_deviation.powi(2).recip() + d_sq.recip())
.recip()
.sqrt();
GlickoRating {
rating: new_rating,
deviation: new_deviation,
}
}
#[must_use]
pub fn expected_score(player_one: &GlickoRating, player_two: &GlickoRating) -> (f64, f64) {
let q = 10_f64.ln() / 400.0;
let g = g_value(q, player_one.deviation.hypot(player_two.deviation));
let exp_one = (1.0 + 10_f64.powf(-g * (player_one.rating - player_two.rating) / 400.0)).recip();
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
#[must_use]
pub fn decay_deviation(player: &GlickoRating, config: &GlickoConfig) -> GlickoRating {
let new_player_deviation = player.deviation.hypot(config.c).min(350.0);
GlickoRating {
rating: player.rating,
deviation: new_player_deviation,
}
}
#[must_use]
pub fn confidence_interval(player: &GlickoRating) -> (f64, f64) {
(
player.rating - 1.96 * player.deviation,
1.96f64.mul_add(player.deviation, player.rating),
)
}
fn new_deviation(pre_deviation: f64, d: f64) -> f64 {
(pre_deviation.powi(2).recip() + d.recip()).recip().sqrt()
}
fn new_rating(
old_rating: f64,
pre_deviation: f64,
score: f64,
q: f64,
g: f64,
e: f64,
d: f64,
) -> f64 {
((q / (pre_deviation.powi(2).recip() + d.recip())) * g).mul_add(score - e, old_rating)
}
fn g_value(q: f64, opponent_deviation: f64) -> f64 {
(1.0 + ((3.0 * q.powi(2) * opponent_deviation.powi(2)) / (PI.powi(2))))
.sqrt()
.recip()
}
fn e_value(g: f64, rating: f64, opponent_rating: f64) -> f64 {
(1.0 + (10_f64.powf(-g * (rating - opponent_rating) / 400.0))).recip()
}
fn d_value(q: f64, g: f64, e: f64) -> f64 {
(q.powi(2) * g.powi(2) * e * (1.0 - e)).powi(-1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glicko() {
let player1 = GlickoRating {
rating: 1500.0,
deviation: 200.0,
};
let opponent1 = GlickoRating {
rating: 1400.0,
deviation: 30.0,
};
let opponent2 = GlickoRating {
rating: 1550.0,
deviation: 100.0,
};
let opponent3 = GlickoRating {
rating: 1700.0,
deviation: 300.0,
};
let config = GlickoConfig::default();
let (player1, _) = glicko(&player1, &opponent1, &Outcomes::WIN, &config);
let (player1, _) = glicko(&player1, &opponent2, &Outcomes::LOSS, &config);
let (player1, _) = glicko(&player1, &opponent3, &Outcomes::LOSS, &config);
assert!((player1.rating.round() - 1449.0).abs() < f64::EPSILON);
assert!((player1.deviation - 171.684_472_141_285_57).abs() < f64::EPSILON);
}
#[test]
fn test_glicko_rating_period() {
let player = GlickoRating {
rating: 1500.0,
deviation: 189.751_837_935_762_84,
};
let opponent1 = GlickoRating {
rating: 1400.0,
deviation: 30.0,
};
let opponent2 = GlickoRating {
rating: 1550.0,
deviation: 100.0,
};
let opponent3 = GlickoRating {
rating: 1700.0,
deviation: 300.0,
};
let results = vec![
(opponent1, Outcomes::WIN),
(opponent2, Outcomes::LOSS),
(opponent3, Outcomes::LOSS),
];
let new_player = glicko_rating_period(&player, &results, &GlickoConfig::new());
assert!((new_player.rating.round() - 1464.0).abs() < f64::EPSILON);
assert!((new_player.deviation - 151.398_902_447_969_33).abs() < f64::EPSILON);
let player = GlickoRating {
rating: 1500.0,
deviation: 50.0,
};
let results: Vec<(GlickoRating, Outcomes)> = Vec::new();
let new_player = glicko_rating_period(&player, &results, &GlickoConfig::new());
assert!((new_player.deviation - 80.586_847_562_117_73).abs() < f64::EPSILON);
}
#[test]
fn test_single_rp() {
let player = GlickoRating {
rating: 1200.0,
deviation: 25.0,
};
let opponent = GlickoRating {
rating: 1500.0,
deviation: 34.0,
};
let config = GlickoConfig::new();
let (np, _) = glicko(&player, &opponent, &Outcomes::WIN, &config);
let rp = glicko_rating_period(&player, &[(opponent, Outcomes::WIN)], &config);
assert_eq!(rp, np);
}
#[test]
fn test_expected_score() {
let player_one = GlickoRating {
rating: 1400.0,
deviation: 40.0,
};
let player_two = GlickoRating {
rating: 1500.0,
deviation: 150.0,
};
let (exp_one, exp_two) = expected_score(&player_one, &player_two);
assert!((exp_one - 0.373_700_405_951_935).abs() < f64::EPSILON);
assert!((exp_two - 0.626_299_594_048_065).abs() < f64::EPSILON);
assert!((exp_one + exp_two - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_confidence_interval() {
let player = GlickoRating {
rating: 1500.0,
deviation: 30.0,
};
let ci = confidence_interval(&player);
assert!((ci.0.round() - 1441.0).abs() < f64::EPSILON);
assert!((ci.1.round() - 1559.0).abs() < f64::EPSILON);
}
#[test]
fn test_decay_deviation() {
let player = GlickoRating {
rating: 1500.0,
deviation: 50.0,
};
let mut player = decay_deviation(&player, &GlickoConfig::new());
assert!((player.deviation - 80.586_847_562_117_73).abs() < f64::EPSILON);
for _ in 0..29 {
player = decay_deviation(&player, &GlickoConfig::default());
}
assert!(((player.deviation * 1000.0).round() - 349_753.0).abs() < f64::EPSILON);
player = decay_deviation(&player, &GlickoConfig::new());
assert!((player.deviation - 350.0).abs() < f64::EPSILON);
}
#[test]
fn test_unequal_draws() {
let mut player = GlickoRating::new();
let mut opponent = GlickoRating {
rating: 2230.0,
deviation: 41.0,
};
(player, opponent) = glicko(
&player,
&opponent,
&Outcomes::DRAW,
&GlickoConfig::default(),
);
assert!((player.rating.round() - 1820.0).abs() < f64::EPSILON);
assert!((player.deviation.round() - 340.0).abs() < f64::EPSILON);
assert!((opponent.rating.round() - 2220.0).abs() < f64::EPSILON);
assert!((opponent.deviation.round() - 75.0).abs() < f64::EPSILON);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let player_new = GlickoRating::new();
let player_default = GlickoRating::default();
assert!((player_new.rating - player_default.rating).abs() < f64::EPSILON);
assert!((player_new.deviation - player_new.deviation).abs() < f64::EPSILON);
let player_one = GlickoRating::new();
let config = GlickoConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.c - config.clone().c).abs() < f64::EPSILON);
assert!(!format!("{:?}", player_one).is_empty());
assert!(!format!("{:?}", config).is_empty());
assert_eq!(player_one, GlickoRating::from((1500.0, 350.0)));
}
}