#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
glicko2::Glicko2Rating, glicko_boost::GlickoBoostRating, sticko::StickoRating, Outcomes,
Rating, RatingPeriodSystem, RatingSystem,
};
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 Rating for GlickoRating {
fn rating(&self) -> f64 {
self.rating
}
fn uncertainty(&self) -> Option<f64> {
Some(self.deviation)
}
fn new(rating: Option<f64>, uncertainty: Option<f64>) -> Self {
Self {
rating: rating.unwrap_or(1500.0),
deviation: uncertainty.unwrap_or(350.0),
}
}
}
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()
}
}
pub struct Glicko {
config: GlickoConfig,
}
impl RatingSystem for Glicko {
type RATING = GlickoRating;
type CONFIG = GlickoConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(
&self,
player_one: &GlickoRating,
player_two: &GlickoRating,
outcome: &Outcomes,
) -> (GlickoRating, GlickoRating) {
glicko(player_one, player_two, outcome, &self.config)
}
fn expected_score(&self, player_one: &GlickoRating, player_two: &GlickoRating) -> (f64, f64) {
expected_score(player_one, player_two)
}
}
impl RatingPeriodSystem for Glicko {
type RATING = GlickoRating;
type CONFIG = GlickoConfig;
fn new(config: Self::CONFIG) -> Self {
Self { config }
}
fn rate(&self, player: &GlickoRating, results: &[(GlickoRating, Outcomes)]) -> GlickoRating {
glicko_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 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 expected_score_rating_period(player: &GlickoRating, opponents: &[GlickoRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| {
let q = 10_f64.ln() / 400.0;
let g = g_value(q, player.deviation.hypot(o.deviation));
(1.0 + 10_f64.powf(-g * (player.rating - o.rating) / 400.0)).recip()
})
.collect()
}
#[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) {
(
1.96f64.mul_add(-player.deviation, player.rating),
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)));
}
#[test]
fn test_traits() {
let player_one: GlickoRating = Rating::new(Some(240.0), Some(90.0));
let player_two: GlickoRating = Rating::new(Some(240.0), Some(90.0));
let rating_system: Glicko = RatingSystem::new(GlickoConfig::new());
assert!((player_one.rating() - 240.0).abs() < f64::EPSILON);
assert_eq!(player_one.uncertainty(), Some(90.0));
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 - 270.633_674_957_731_9).abs() < f64::EPSILON);
assert!((new_player_two.rating - 209.366_325_042_268_1).abs() < f64::EPSILON);
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);
let rating_period_system: Glicko = RatingPeriodSystem::new(GlickoConfig::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: GlickoRating = Rating::new(Some(240.0), Some(90.0));
let player_two: GlickoRating = Rating::new(Some(240.0), Some(90.0));
let rating_period: Glicko = RatingPeriodSystem::new(GlickoConfig::new());
let new_player_one =
RatingPeriodSystem::rate(&rating_period, &player_one, &[(player_two, Outcomes::WIN)]);
assert!((new_player_one.rating - 270.633_674_957_731_9).abs() < f64::EPSILON);
}
}