#![deny(missing_docs)]
const CONVERGENCE_TOLERANCE: f64 = 0.000001;
#[derive(Clone, Copy, Debug)]
pub struct Glicko2Rating {
pub value: f64,
pub deviation: f64,
pub volatility: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct GlickoRating {
pub value: f64,
pub deviation: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct GameResult {
opponent_rating_value: f64,
opponent_rating_deviation: f64,
score: f64,
}
impl GameResult {
pub fn win<T: Into<Glicko2Rating>>(opponent_rating: T) -> GameResult {
let opponent_glicko2: Glicko2Rating = opponent_rating.into();
GameResult {
opponent_rating_value: opponent_glicko2.value,
opponent_rating_deviation: opponent_glicko2.deviation,
score: 1.0,
}
}
pub fn loss<T: Into<Glicko2Rating>>(opponent_rating: T) -> GameResult {
let opponent_glicko2: Glicko2Rating = opponent_rating.into();
GameResult {
opponent_rating_value: opponent_glicko2.value,
opponent_rating_deviation: opponent_glicko2.deviation,
score: 0.0,
}
}
pub fn draw<T: Into<Glicko2Rating>>(opponent_rating: T) -> GameResult {
let opponent_glicko2: Glicko2Rating = opponent_rating.into();
GameResult {
opponent_rating_value: opponent_glicko2.value,
opponent_rating_deviation: opponent_glicko2.deviation,
score: 0.5,
}
}
}
impl From<GlickoRating> for Glicko2Rating {
fn from(rating: GlickoRating) -> Glicko2Rating {
Glicko2Rating {
value: (rating.value - 1500.0) / 173.7178,
deviation: rating.deviation / 173.7178,
volatility: 0.06,
}
}
}
impl From<Glicko2Rating> for GlickoRating {
fn from(rating: Glicko2Rating) -> GlickoRating {
GlickoRating {
value: rating.value * 173.7178 + 1500.0,
deviation: rating.deviation * 173.7178,
}
}
}
impl Glicko2Rating {
pub fn unrated() -> Glicko2Rating {
Glicko2Rating::from(GlickoRating::unrated())
}
}
impl GlickoRating {
pub fn unrated() -> GlickoRating {
GlickoRating {
value: 1500.0,
deviation: 350.0,
}
}
}
impl Default for Glicko2Rating {
fn default() -> Glicko2Rating {
Glicko2Rating::unrated()
}
}
impl Default for GlickoRating {
fn default() -> GlickoRating {
GlickoRating::unrated()
}
}
fn g(rating_deviation: f64) -> f64 {
use std::f64::consts::PI;
let denom = 1.0 + ((3.0 * rating_deviation * rating_deviation) / (PI * PI));
denom.sqrt().recip()
}
fn e(rating: f64, other_rating: f64, other_rating_deviation: f64) -> f64 {
let base = -1.0 * g(other_rating_deviation) * (rating - other_rating);
(1.0 + base.exp()).recip()
}
fn f(x: f64, delta: f64, rating_deviation: f64, v: f64, volatility: f64, sys_constant: f64) -> f64 {
let fraction_one = {
let numer =
x.exp() * ((delta * delta) - (rating_deviation * rating_deviation) - v - x.exp());
let denom = 2.0 * (rating_deviation * rating_deviation + v + x.exp())
* (rating_deviation * rating_deviation + v + x.exp());
numer / denom
};
let fraction_two = {
let numer = x - (volatility * volatility).ln();
let denom = sys_constant * sys_constant;
numer / denom
};
fraction_one - fraction_two
}
pub fn new_rating(
prior_rating: Glicko2Rating,
results: &[GameResult],
sys_constant: f64,
) -> Glicko2Rating {
if !results.is_empty() {
let v: f64 = {
results
.iter()
.fold(0.0, |acc, result| {
acc
+ g(result.opponent_rating_deviation) * g(result.opponent_rating_deviation)
* e(
prior_rating.value,
result.opponent_rating_value,
result.opponent_rating_deviation,
)
* (1.0
- e(
prior_rating.value,
result.opponent_rating_value,
result.opponent_rating_deviation,
))
})
.recip()
};
let delta = {
v * results.iter().fold(0.0, |acc, result| {
acc
+ g(result.opponent_rating_deviation)
* (result.score
- e(
prior_rating.value,
result.opponent_rating_value,
result.opponent_rating_deviation,
))
})
};
let new_volatility = {
let mut a = (prior_rating.volatility * prior_rating.volatility).ln();
let delta_squared = delta * delta;
let rd_squared = prior_rating.deviation * prior_rating.deviation;
let mut b = if delta_squared > rd_squared + v {
delta_squared - rd_squared - v
} else {
let mut k = 1.0;
while f(
a - k * sys_constant,
delta,
prior_rating.deviation,
v,
prior_rating.volatility,
sys_constant,
) < 0.0
{
k += 1.0;
}
a - k * sys_constant
};
let mut fa = f(
a,
delta,
prior_rating.deviation,
v,
prior_rating.volatility,
sys_constant,
);
let mut fb = f(
b,
delta,
prior_rating.deviation,
v,
prior_rating.volatility,
sys_constant,
);
while (b - a).abs() > CONVERGENCE_TOLERANCE {
let c = a + ((a - b) * fa / (fb - fa));
let fc = f(
c,
delta,
prior_rating.deviation,
v,
prior_rating.volatility,
sys_constant,
);
if fc * fb < 0.0 {
a = b;
fa = fb;
} else {
fa /= 2.0;
}
b = c;
fb = fc;
}
(a / 2.0).exp()
};
let new_pre_rd = ((prior_rating.deviation * prior_rating.deviation)
+ (new_volatility * new_volatility))
.sqrt();
let new_rd = {
let subexpr_1 = (new_pre_rd * new_pre_rd).recip();
let subexpr_2 = v.recip();
(subexpr_1 + subexpr_2).sqrt().recip()
};
let new_rating = {
prior_rating.value + ((new_rd * new_rd) * results.iter().fold(0.0, |acc, &result| {
acc
+ g(result.opponent_rating_deviation)
* (result.score
- e(
prior_rating.value,
result.opponent_rating_value,
result.opponent_rating_deviation,
))
}))
};
Glicko2Rating {
value: new_rating,
deviation: new_rd,
volatility: new_volatility,
}
} else {
let new_rd = ((prior_rating.deviation * prior_rating.deviation)
+ (prior_rating.volatility * prior_rating.volatility))
.sqrt();
Glicko2Rating {
value: prior_rating.value,
deviation: new_rd,
volatility: prior_rating.volatility,
}
}
}
#[cfg(test)]
mod tests {
extern crate approx;
use self::approx::*;
use super::*;
#[test]
fn test_rating_update() {
let example_player_rating = Glicko2Rating::from(GlickoRating {
value: 1500.0,
deviation: 200.0,
});
let mut results = vec![];
results.push(GameResult::win(GlickoRating {
value: 1400.0,
deviation: 30.0,
}));
results.push(GameResult::loss(GlickoRating {
value: 1550.0,
deviation: 100.0,
}));
results.push(GameResult::loss(GlickoRating {
value: 1700.0,
deviation: 300.0,
}));
let new_rating = new_rating(example_player_rating, &results, 0.5);
assert!(Relative::default().epsilon(0.0001).eq(&new_rating.value, &-0.2069));
assert!(Relative::default().epsilon(0.0001).eq(&new_rating.deviation, &0.8722));
assert!(Relative::default().epsilon(0.0001).eq(&new_rating.volatility, &0.05999))
}
#[test]
fn test_glicko_glicko2_conversions() {
let example_player = GlickoRating {
value: 1500.0,
deviation: 200.0,
};
let glicko2_rating = Glicko2Rating::from(example_player);
assert!(Relative::default().epsilon(0.0001).eq(&glicko2_rating.value, &0.0));
assert!(Relative::default().epsilon(0.0001).eq(&glicko2_rating.deviation, &1.1513));
assert!(Relative::default().epsilon(0.0001).eq(&glicko2_rating.volatility, &0.06));
let glicko_rating = GlickoRating::from(glicko2_rating);
assert!(Relative::default().epsilon(0.0001).eq(&glicko_rating.value, &1500.0));
assert!(Relative::default().epsilon(0.0001).eq(&glicko_rating.deviation, &200.0));
}
}