use std::f64::consts::PI;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{glicko::GlickoRating, glicko2::Glicko2Rating, sticko::StickoRating, Outcomes};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GlickoBoostRating {
pub rating: f64,
pub deviation: f64,
}
impl GlickoBoostRating {
#[must_use]
pub const fn new() -> Self {
Self {
rating: 1500.0,
deviation: 350.0,
}
}
}
impl Default for GlickoBoostRating {
fn default() -> Self {
Self::new()
}
}
impl From<(f64, f64)> for GlickoBoostRating {
fn from((r, d): (f64, f64)) -> Self {
Self {
rating: r,
deviation: d,
}
}
}
impl From<GlickoRating> for GlickoBoostRating {
fn from(g: GlickoRating) -> Self {
Self {
rating: g.rating,
deviation: g.deviation,
}
}
}
impl From<Glicko2Rating> for GlickoBoostRating {
fn from(g: Glicko2Rating) -> Self {
Self {
rating: g.rating,
deviation: g.deviation,
}
}
}
impl From<StickoRating> for GlickoBoostRating {
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 GlickoBoostConfig {
pub eta: f64,
pub k: f64,
pub b: (f64, f64),
pub alpha: (f64, f64, f64, f64, f64),
}
impl GlickoBoostConfig {
#[must_use]
pub const fn new() -> Self {
Self {
eta: 30.0,
k: 1.96,
b: (0.20139, 17.5),
alpha: (
5.837_33,
-1.753_74e-04,
-7.080_124e-05,
0.001_733_792,
0.000_267_06,
),
}
}
}
impl Default for GlickoBoostConfig {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn glicko_boost(
player_one: &GlickoBoostRating,
player_two: &GlickoBoostRating,
outcome: &Outcomes,
config: &GlickoBoostConfig,
) -> (GlickoBoostRating, GlickoBoostRating) {
let q = 10_f64.ln() / 400.0;
let outcome1 = outcome.to_chess_points();
let outcome2 = 1.0 - outcome1;
let colour1 = 1.0;
let colour2 = -1.0;
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,
config.eta,
colour1,
);
let e2 = e_value(
g2,
player_two.rating,
player_one.rating,
config.eta,
colour2,
);
let d1 = d_value(q, g1, e1);
let d2 = d_value(q, g2, e2);
let z1 = z_value(g1, e1, outcome1);
let z2 = z_value(g2, e2, outcome2);
let new_deviation1 = new_deviation(player_one.deviation, d1, z1, config);
let new_deviation2 = new_deviation(player_two.deviation, d2, z2, config);
let new_rating1 = new_rating(player_one.rating, new_deviation1, outcome1, q, g1, e1);
let new_rating2 = new_rating(player_two.rating, new_deviation2, outcome2, q, g2, e2);
let end_deviation1 = (new_deviation1
.mul_add(
new_deviation1,
config
.alpha
.4
.mul_add(
(new_rating1 / 1000.0).powi(2),
config.alpha.3.mul_add(
new_rating1 / 1000.0,
(config.alpha.2 * new_deviation1).mul_add(
new_rating1 / 1000.0,
config.alpha.1.mul_add(new_deviation1, config.alpha.0),
),
),
)
.exp(),
)
.sqrt())
.min(350.0);
let end_deviation2 = (new_deviation2
.mul_add(
new_deviation2,
config
.alpha
.4
.mul_add(
(new_rating2 / 1000.0).powi(2),
config.alpha.3.mul_add(
new_rating2 / 1000.0,
(config.alpha.2 * new_deviation2).mul_add(
new_rating2 / 1000.0,
config.alpha.1.mul_add(new_deviation2, config.alpha.0),
),
),
)
.exp(),
)
.sqrt())
.min(350.0);
(
GlickoBoostRating {
rating: new_rating1,
deviation: end_deviation1,
},
GlickoBoostRating {
rating: new_rating2,
deviation: end_deviation2,
},
)
}
#[must_use]
pub fn glicko_boost_rating_period(
player: &GlickoBoostRating,
results: &[(GlickoBoostRating, Outcomes, bool)],
config: &GlickoBoostConfig,
) -> GlickoBoostRating {
let q = 10_f64.ln() / 400.0;
if results.is_empty() {
return decay_deviation(player, config);
}
let d_sum: f64 = results
.iter()
.map(|r| {
let g = g_value(q, r.0.deviation);
let e = e_value(
g,
player.rating,
r.0.rating,
config.eta,
if r.2 { 1.0 } else { -1.0 },
);
g.powi(2) * e * (1.0 - e)
})
.sum();
let d_sq = (q.powi(2) * d_sum).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,
config.eta,
if r.2 { 1.0 } else { -1.0 },
);
let s = r.1.to_chess_points();
g * (s - e)
})
.sum();
let z = m / d_sum.sqrt();
let new_deviation = new_deviation(player.deviation, d_sq, z, config);
let new_rating = (new_deviation.powi(2) * q).mul_add(m, player.rating);
let end_deviation = (new_deviation
.mul_add(
new_deviation,
config
.alpha
.4
.mul_add(
(new_rating / 1000.0).powi(2),
config.alpha.3.mul_add(
new_rating / 1000.0,
(config.alpha.2 * new_deviation).mul_add(
new_rating / 1000.0,
config.alpha.1.mul_add(new_deviation, config.alpha.0),
),
),
)
.exp(),
)
.sqrt())
.min(350.0);
GlickoBoostRating {
rating: new_rating,
deviation: end_deviation,
}
}
#[must_use]
pub fn expected_score(
player_one: &GlickoBoostRating,
player_two: &GlickoBoostRating,
config: &GlickoBoostConfig,
) -> (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 + config.eta - player_two.rating) / 400.0))
.recip();
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
#[must_use]
pub fn decay_deviation(
player: &GlickoBoostRating,
config: &GlickoBoostConfig,
) -> GlickoBoostRating {
let decayed_deviation = (player
.deviation
.mul_add(
player.deviation,
config
.alpha
.4
.mul_add(
(player.rating / 1000.0).powi(2),
config.alpha.3.mul_add(
player.rating / 1000.0,
(config.alpha.2 * player.deviation).mul_add(
player.rating / 1000.0,
config.alpha.1.mul_add(player.deviation, config.alpha.0),
),
),
)
.exp(),
)
.sqrt())
.min(350.0);
GlickoBoostRating {
rating: player.rating,
deviation: decayed_deviation,
}
}
#[must_use]
pub fn confidence_interval(player: &GlickoBoostRating) -> (f64, f64) {
(
player.rating - 1.96 * player.deviation,
1.96f64.mul_add(player.deviation, player.rating),
)
}
fn new_deviation(old_deviation: f64, d: f64, z: f64, config: &GlickoBoostConfig) -> f64 {
if z > config.k && config.k != 0.0 {
let after_deviation = (old_deviation.powi(2).recip() + d.recip()).recip().sqrt();
let pre_deviation = boost_rd(z, after_deviation, config);
((pre_deviation.powi(2).recip() + d.recip()).recip().sqrt()).min(350.0)
} else {
((old_deviation.powi(2).recip() + d.recip()).recip().sqrt()).min(350.0)
}
}
fn new_rating(old_rating: f64, deviation: f64, score: f64, q: f64, g: f64, e: f64) -> f64 {
(deviation.powi(2) * q * g).mul_add(score - e, old_rating)
}
fn z_value(g: f64, e: f64, score: f64) -> f64 {
(g * (score - e)) / (g.powi(2) * e * (1.0 - e)).sqrt()
}
fn boost_rd(z: f64, deviation: f64, config: &GlickoBoostConfig) -> f64 {
(z - config.k)
.mul_add(config.b.0, 1.0)
.mul_add(deviation, config.b.1)
}
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, advantage: f64, colour: f64) -> f64 {
(1.0 + (10_f64.powf(-g * (colour.mul_add(advantage, 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_boost() {
let player_one = GlickoBoostRating {
rating: 1500.0,
deviation: 200.0,
};
let player_two = GlickoBoostRating {
rating: 1620.0,
deviation: 105.0,
};
let config = GlickoBoostConfig::new();
let (new_one, new_two) = glicko_boost(&player_one, &player_two, &Outcomes::WIN, &config);
assert!((new_one.rating.round() - 1606.0).abs() < f64::EPSILON);
assert!((new_two.rating.round() - 1589.0).abs() < f64::EPSILON);
assert!((new_one.deviation - 177.634_630_775_565_48).abs() < f64::EPSILON);
assert!((new_two.deviation - 103.511_394_589_339_77).abs() < f64::EPSILON);
}
#[test]
fn test_one_rp() {
let player = GlickoBoostRating {
rating: 1444.0,
deviation: 85.0,
};
let opponent = GlickoBoostRating {
rating: 1804.0,
deviation: 55.0,
};
let config = GlickoBoostConfig::new();
let (np, _) = glicko_boost(&player, &opponent, &Outcomes::WIN, &config);
let rp = glicko_boost_rating_period(&player, &[(opponent, Outcomes::WIN, true)], &config);
assert_eq!(np, rp);
}
#[test]
fn test_glicko_comparison() {
let config = GlickoBoostConfig {
eta: 0.0,
k: 0.0,
b: (0.0, 0.0),
alpha: (0.0, 0.0, 0.0, 0.0, 0.0),
};
let player = GlickoBoostRating {
rating: 1500.0,
deviation: 200.0,
};
let opponent1 = GlickoBoostRating {
rating: 1400.0,
deviation: 30.0,
};
let opponent2 = GlickoBoostRating {
rating: 1550.0,
deviation: 100.0,
};
let opponent3 = GlickoBoostRating {
rating: 1700.0,
deviation: 300.0,
};
let results = vec![
(opponent1, Outcomes::WIN, true),
(opponent2, Outcomes::LOSS, true),
(opponent3, Outcomes::LOSS, true),
];
let new_player = glicko_boost_rating_period(&player, &results, &config);
assert!((new_player.rating.round() - 1464.0).abs() < f64::EPSILON);
assert!((new_player.deviation - 151.402_204_945_799_04).abs() < f64::EPSILON);
}
#[test]
fn test_boost_rd() {
let rd = 98.6;
let z = 3.672_028_777_401_921_6;
let new_rd = boost_rd(z, rd, &GlickoBoostConfig::new());
assert!((new_rd - 150.095_847_882_423_93).abs() < f64::EPSILON);
}
#[test]
fn test_max_rd() {
let player_one = GlickoBoostRating::new();
let player_two = GlickoBoostRating {
rating: 3500.0,
deviation: 31.4,
};
let (np1, _) = glicko_boost(
&player_one,
&player_two,
&Outcomes::WIN,
&GlickoBoostConfig::new(),
);
assert!((np1.deviation - 350.0).abs() < f64::EPSILON);
}
#[test]
fn test_decay() {
let player = GlickoBoostRating {
rating: 1302.2,
deviation: 62.0,
};
let decayed_player = decay_deviation(&player, &GlickoBoostConfig::new());
let rp_player = glicko_boost_rating_period(&player, &[], &GlickoBoostConfig::new());
assert_eq!(decayed_player, rp_player);
assert!((decayed_player.deviation - 64.669_444_203_475_88).abs() < f64::EPSILON);
assert!((decayed_player.rating - player.rating).abs() < f64::EPSILON);
}
#[test]
fn test_confidence_interval() {
let player = GlickoBoostRating {
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_expected_score() {
let player_one = GlickoBoostRating {
rating: 1400.0,
deviation: 40.0,
};
let player_two = GlickoBoostRating {
rating: 1500.0,
deviation: 150.0,
};
let glicko_config = GlickoBoostConfig {
eta: 0.0,
..Default::default()
};
let boost_config = GlickoBoostConfig::new();
let (exp_one, exp_two) = expected_score(&player_one, &player_two, &glicko_config);
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);
let (exp_one, exp_two) = expected_score(&player_one, &player_two, &boost_config);
assert!((exp_one - 0.410_605_680_590_947_1).abs() < f64::EPSILON);
assert!((exp_two - 0.589_394_319_409_052_8).abs() < f64::EPSILON);
assert!((exp_one + exp_two - 1.0).abs() < f64::EPSILON);
}
#[test]
#[allow(clippy::similar_names)]
fn test_glicko_conv() {
let glickob = GlickoBoostRating::new();
let glicko_conv = GlickoRating::from(glickob);
let glicko2_conv = Glicko2Rating::from(glickob);
assert!((glicko_conv.rating - 1500.0).abs() < f64::EPSILON);
assert!((glicko2_conv.rating - 1500.0).abs() < f64::EPSILON);
let glicko2 = Glicko2Rating::new();
let glicko = GlickoRating::new();
assert_eq!(GlickoBoostRating::new(), GlickoBoostRating::from(glicko2));
assert_eq!(
GlickoBoostRating::default(),
GlickoBoostRating::from(glicko)
);
}
#[test]
#[allow(clippy::similar_names, clippy::too_many_lines)]
fn test_glicko_boost_rating_period() {
let player_a = GlickoBoostRating {
rating: 2300.0,
deviation: 140.0,
};
let player_b = GlickoBoostRating {
rating: 2295.0,
deviation: 80.0,
};
let player_c = GlickoBoostRating {
rating: 2280.0,
deviation: 150.0,
};
let player_d = GlickoBoostRating {
rating: 2265.0,
deviation: 70.0,
};
let player_e = GlickoBoostRating {
rating: 2260.0,
deviation: 90.0,
};
let player_f = GlickoBoostRating {
rating: 2255.0,
deviation: 200.0,
};
let player_g = GlickoBoostRating {
rating: 2250.0,
deviation: 50.0,
};
let player_h = GlickoBoostRating {
rating: 2075.0,
deviation: 120.0,
};
let config = GlickoBoostConfig::new();
let player_a_results = vec![
(player_b, Outcomes::LOSS, true),
(player_c, Outcomes::LOSS, false),
(player_e, Outcomes::LOSS, true),
(player_f, Outcomes::WIN, false),
(player_g, Outcomes::WIN, true),
(player_h, Outcomes::LOSS, false),
];
let player_b_results = vec![
(player_a, Outcomes::WIN, false),
(player_c, Outcomes::DRAW, true),
(player_d, Outcomes::WIN, true),
(player_e, Outcomes::DRAW, false),
(player_f, Outcomes::WIN, true),
(player_g, Outcomes::WIN, false),
];
let player_c_results = vec![
(player_a, Outcomes::WIN, true),
(player_b, Outcomes::DRAW, false),
(player_d, Outcomes::WIN, false),
(player_f, Outcomes::WIN, false),
(player_g, Outcomes::WIN, true),
(player_h, Outcomes::DRAW, true),
];
let player_d_results = vec![
(player_b, Outcomes::LOSS, false),
(player_c, Outcomes::LOSS, true),
(player_e, Outcomes::LOSS, true),
(player_f, Outcomes::DRAW, false),
(player_g, Outcomes::LOSS, true),
(player_h, Outcomes::LOSS, false),
];
let player_e_results = vec![
(player_a, Outcomes::WIN, false),
(player_b, Outcomes::DRAW, true),
(player_d, Outcomes::WIN, false),
(player_f, Outcomes::WIN, true),
(player_g, Outcomes::DRAW, false),
(player_h, Outcomes::LOSS, true),
];
let player_f_results = vec![
(player_a, Outcomes::LOSS, true),
(player_b, Outcomes::LOSS, false),
(player_c, Outcomes::LOSS, true),
(player_d, Outcomes::DRAW, true),
(player_e, Outcomes::LOSS, false),
(player_h, Outcomes::LOSS, false),
];
let player_g_results = vec![
(player_a, Outcomes::LOSS, false),
(player_b, Outcomes::LOSS, true),
(player_c, Outcomes::LOSS, false),
(player_d, Outcomes::WIN, false),
(player_e, Outcomes::DRAW, true),
(player_h, Outcomes::LOSS, true),
];
let player_h_results = vec![
(player_a, Outcomes::WIN, true),
(player_c, Outcomes::DRAW, false),
(player_d, Outcomes::WIN, true),
(player_e, Outcomes::WIN, false),
(player_f, Outcomes::WIN, true),
(player_g, Outcomes::WIN, false),
];
let new_a = glicko_boost_rating_period(&player_a, &player_a_results, &config);
let new_b = glicko_boost_rating_period(&player_b, &player_b_results, &config);
let new_c = glicko_boost_rating_period(&player_c, &player_c_results, &config);
let new_d = glicko_boost_rating_period(&player_d, &player_d_results, &config);
let new_e = glicko_boost_rating_period(&player_e, &player_e_results, &config);
let new_f = glicko_boost_rating_period(&player_f, &player_f_results, &config);
let new_g = glicko_boost_rating_period(&player_g, &player_g_results, &config);
let new_h = glicko_boost_rating_period(&player_h, &player_h_results, &config);
assert!((new_a.rating - 2_209.502_401_056_321_6).abs() < f64::EPSILON);
assert!((new_a.deviation - 105.846_722_882_642_35).abs() < f64::EPSILON);
assert!((new_b.rating - 2_343.331_676_741_51).abs() < f64::EPSILON);
assert!((new_b.deviation - 73.239_139_081_695_3).abs() < f64::EPSILON);
assert!((new_c.rating - 2_386.917_144_473_656_7).abs() < f64::EPSILON);
assert!((new_c.deviation - 109.595_379_480_830_47).abs() < f64::EPSILON);
assert!((new_d.rating - 2_204.280_099_158_658_7).abs() < f64::EPSILON);
assert!((new_d.deviation - 66.367_566_777_947_56).abs() < f64::EPSILON);
assert!((new_e.rating - 2_287.443_303_658_628_3).abs() < f64::EPSILON);
assert!((new_e.deviation - 79.993_286_777_344_75).abs() < f64::EPSILON);
assert!((new_f.rating - 2_051.583_061_993_349_5).abs() < f64::EPSILON);
assert!((new_f.deviation - 122.816_328_678_587_84).abs() < f64::EPSILON);
assert!((new_g.rating - 2_231.929_694_167_278).abs() < f64::EPSILON);
assert!((new_g.deviation - 51.016_495_555_231_394).abs() < f64::EPSILON);
assert!((new_h.rating - 2_348.033_407_382_116).abs() < f64::EPSILON);
assert!((new_h.deviation - 115.100_184_487_094_04).abs() < f64::EPSILON);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let player_one = GlickoBoostRating::new();
let config = GlickoBoostConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.eta - config.clone().eta).abs() < f64::EPSILON);
assert!(!format!("{:?}", player_one).is_empty());
assert!(!format!("{:?}", config).is_empty());
assert_eq!(player_one, GlickoBoostRating::from((1500.0, 350.0)));
}
}