skillratings/
glicko.rs

1//! The Glicko algorithm, developed by Mark Glickman as an improvement on Elo.  
2//! It is still being used in some games in favour Glicko-2, such as Pokémon Showdown, Chess.com and Quake Live.
3//!
4//! If you are looking for the updated Glicko-2 rating system, please see [`Glicko-2`](crate::glicko2).
5//!
6//! The main improvement over Elo is the rating deviation introduced,
7//! which decreases over time as the player plays more matches and the rating becomes more reliable.
8//! This allows players to rise and fall through the ranks quickly at the beginning,
9//! and not gain or lose as much rating points after completing more matches.
10//!
11//! # Quickstart
12//!
13//! This is the most basic example on how to use the Glicko Module.  
14//! Please take a look at the functions below to see more advanced use cases.
15//!
16//! ```
17//! use skillratings::{
18//!     glicko::{glicko, GlickoConfig, GlickoRating},
19//!     Outcomes,
20//! };
21//!
22//! // Initialise a new player rating with a rating of 1500 and a deviation of 350.
23//! let player_one = GlickoRating::new();
24//!
25//! // Or you can initialise it with your own values of course.
26//! // Imagine these numbers being pulled from a database.
27//! let (some_rating, some_deviation) = (1325.0, 230.0);
28//! let player_two = GlickoRating {
29//!     rating: some_rating,
30//!     deviation: some_deviation,
31//! };
32//!
33//! // The outcome of the match is from the perspective of player one.
34//! let outcome = Outcomes::WIN;
35//!
36//! // The config allows you to specify certain values in the Glicko calculation.
37//! // Here we set the c value to 23.75, instead of the default 63.2.
38//! // This will decrease the amount by which rating deviation increases per rating period.
39//! let config = GlickoConfig { c: 23.75 };
40//!
41//! // The glicko function will calculate the new ratings for both players and return them.
42//! let (new_player_one, new_player_two) = glicko(&player_one, &player_two, &outcome, &config);
43//! ```
44//!
45//! # More Information
46//!
47//! - [Wikipedia Article](https://en.wikipedia.org/wiki/Glicko_rating_system)
48//! - [Original Paper by Mark Glickman](http://www.glicko.net/glicko/glicko.pdf)
49//! - [Glicko Calculator](http://www.bjcox.com/?page_id=2)
50
51#[cfg(feature = "serde")]
52use serde::{Deserialize, Serialize};
53
54use crate::{
55    glicko2::Glicko2Rating, glicko_boost::GlickoBoostRating, sticko::StickoRating, Outcomes,
56    Rating, RatingPeriodSystem, RatingSystem,
57};
58use std::f64::consts::PI;
59
60#[derive(Copy, Clone, Debug, PartialEq)]
61#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
62/// The Glicko rating for a player.
63///
64/// For the Glicko-2 rating, please see [`Glicko2Rating`].
65///
66/// The default rating is 1500.0.  
67/// The default deviation is 350.0.
68pub struct GlickoRating {
69    /// The player's Glicko rating number, by default 1500.0.
70    pub rating: f64,
71    /// The player's Glicko deviation number, by default 350.0.
72    pub deviation: f64,
73}
74
75impl GlickoRating {
76    #[must_use]
77    /// Initialise a new `GlickoRating` with a rating of 1500.0 and a deviation of 350.0.
78    pub const fn new() -> Self {
79        Self {
80            rating: 1500.0,
81            deviation: 350.0,
82        }
83    }
84}
85
86impl Default for GlickoRating {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92impl Rating for GlickoRating {
93    fn rating(&self) -> f64 {
94        self.rating
95    }
96    fn uncertainty(&self) -> Option<f64> {
97        Some(self.deviation)
98    }
99    fn new(rating: Option<f64>, uncertainty: Option<f64>) -> Self {
100        Self {
101            rating: rating.unwrap_or(1500.0),
102            deviation: uncertainty.unwrap_or(350.0),
103        }
104    }
105}
106
107impl From<(f64, f64)> for GlickoRating {
108    fn from((r, d): (f64, f64)) -> Self {
109        Self {
110            rating: r,
111            deviation: d,
112        }
113    }
114}
115
116impl From<Glicko2Rating> for GlickoRating {
117    fn from(g: Glicko2Rating) -> Self {
118        Self {
119            rating: g.rating,
120            deviation: g.deviation,
121        }
122    }
123}
124
125impl From<GlickoBoostRating> for GlickoRating {
126    fn from(g: GlickoBoostRating) -> Self {
127        Self {
128            rating: g.rating,
129            deviation: g.deviation,
130        }
131    }
132}
133
134impl From<StickoRating> for GlickoRating {
135    fn from(s: StickoRating) -> Self {
136        Self {
137            rating: s.rating,
138            deviation: s.deviation,
139        }
140    }
141}
142
143#[derive(Clone, Copy, Debug)]
144#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
145/// Constants used in the Glicko calculations.
146pub struct GlickoConfig {
147    /// The c value describes how much the rating deviation should decay in each step.
148    /// The higher the value, the more the rating deviation will decay.  
149    /// In [the paper](http://www.glicko.net/glicko/glicko.pdf) a value of
150    /// `63.2` seems to be a suggested value, so that is the default here.
151    pub c: f64,
152}
153
154impl GlickoConfig {
155    #[must_use]
156    /// Initialise a new `GlickoConfig` with a c value of `63.2`
157    pub const fn new() -> Self {
158        Self { c: 63.2 }
159    }
160}
161
162impl Default for GlickoConfig {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168/// Struct to calculate ratings and expected score for [`GlickoRating`]
169pub struct Glicko {
170    config: GlickoConfig,
171}
172
173impl RatingSystem for Glicko {
174    type RATING = GlickoRating;
175    type CONFIG = GlickoConfig;
176
177    fn new(config: Self::CONFIG) -> Self {
178        Self { config }
179    }
180
181    fn rate(
182        &self,
183        player_one: &GlickoRating,
184        player_two: &GlickoRating,
185        outcome: &Outcomes,
186    ) -> (GlickoRating, GlickoRating) {
187        glicko(player_one, player_two, outcome, &self.config)
188    }
189
190    fn expected_score(&self, player_one: &GlickoRating, player_two: &GlickoRating) -> (f64, f64) {
191        expected_score(player_one, player_two)
192    }
193}
194
195impl RatingPeriodSystem for Glicko {
196    type RATING = GlickoRating;
197    type CONFIG = GlickoConfig;
198
199    fn new(config: Self::CONFIG) -> Self {
200        Self { config }
201    }
202
203    fn rate(&self, player: &GlickoRating, results: &[(GlickoRating, Outcomes)]) -> GlickoRating {
204        glicko_rating_period(player, results, &self.config)
205    }
206
207    fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
208        expected_score_rating_period(player, opponents)
209    }
210}
211
212#[must_use]
213/// Calculates the [`GlickoRating`]s of two players based on their old ratings, deviations, and the outcome of the game.
214///
215/// Please see [`Glicko-2`](crate::glicko2) for calculating with the improved version.
216///
217/// Takes in two players as [`GlickoRating`]s, and an [`Outcome`](Outcomes).
218///
219/// Instead of the traditional way of calculating the Glicko for only one player only using a list of results,
220/// we are calculating the Glicko rating for two players at once, like in the Elo calculation,
221/// to make it easier to see instant results.
222///
223/// For the traditional way of calculating a Glicko rating please see [`glicko_rating_period`].
224///
225/// The outcome of the match is in the perspective of `player_one`.
226/// This means [`Outcomes::WIN`] is a win for `player_one` and [`Outcomes::LOSS`] is a win for `player_two`.
227///
228/// # Examples
229/// ```
230/// use skillratings::{
231///     glicko::{glicko, GlickoConfig, GlickoRating},
232///     Outcomes,
233/// };
234///
235/// let player_one = GlickoRating {
236///     rating: 1500.0,
237///     deviation: 350.0,
238/// };
239/// let player_two = GlickoRating {
240///     rating: 1500.0,
241///     deviation: 350.0,
242/// };
243///
244/// let outcome = Outcomes::WIN;
245///
246/// let config = GlickoConfig::new();
247///
248/// let (new_one, new_two) = glicko(&player_one, &player_two, &outcome, &config);
249///
250/// assert!((new_one.rating.round() - 1662.0).abs() < f64::EPSILON);
251/// assert!((new_one.deviation.round() - 290.0).abs() < f64::EPSILON);
252///
253/// assert!((new_two.rating.round() - 1338.0).abs() < f64::EPSILON);
254/// assert!((new_two.deviation.round() - 290.0).abs() < f64::EPSILON);
255/// ```
256pub fn glicko(
257    player_one: &GlickoRating,
258    player_two: &GlickoRating,
259    outcome: &Outcomes,
260    config: &GlickoConfig,
261) -> (GlickoRating, GlickoRating) {
262    let q = 10_f64.ln() / 400.0;
263
264    let outcome1 = outcome.to_chess_points();
265    let outcome2 = 1.0 - outcome1;
266
267    let g1 = g_value(q, player_two.deviation);
268    let g2 = g_value(q, player_one.deviation);
269
270    let e1 = e_value(g1, player_one.rating, player_two.rating);
271    let e2 = e_value(g2, player_two.rating, player_one.rating);
272
273    let d1 = d_value(q, g1, e1);
274    let d2 = d_value(q, g2, e2);
275
276    let player_one_pre_deviation = player_one.deviation.hypot(config.c).min(350.0);
277    let player_two_pre_deviation = player_two.deviation.hypot(config.c).min(350.0);
278
279    let player_one_new_rating = new_rating(
280        player_one.rating,
281        player_one_pre_deviation,
282        outcome1,
283        q,
284        g1,
285        e1,
286        d1,
287    );
288    let player_two_new_rating = new_rating(
289        player_two.rating,
290        player_two_pre_deviation,
291        outcome2,
292        q,
293        g2,
294        e2,
295        d2,
296    );
297
298    let player_one_new_deviation = new_deviation(player_one_pre_deviation, d1);
299    let player_two_new_deviation = new_deviation(player_two_pre_deviation, d2);
300
301    (
302        GlickoRating {
303            rating: player_one_new_rating,
304            deviation: player_one_new_deviation,
305        },
306        GlickoRating {
307            rating: player_two_new_rating,
308            deviation: player_two_new_deviation,
309        },
310    )
311}
312
313#[must_use]
314/// The "traditional" way of calculating a [`GlickoRating`] of a player in a rating period.
315///
316/// Note that in this case, all of the matches are considered to be played at once.  
317/// This means that the player will not get updated in-between matches, as you might expect.  
318/// This will result in *slightly* different results than if you were to use the [`glicko`] function in a loop.
319///
320/// Takes in a player as an [`GlickoRating`] and their results as a Slice of tuples containing the opponent as an [`GlickoRating`],
321/// the outcome of the game as an [`Outcome`](Outcomes) and a [`GlickoConfig`].
322///
323/// The outcome of the match is in the perspective of the player.
324/// This means [`Outcomes::WIN`] is a win for the player and [`Outcomes::LOSS`] is a win for the opponent.
325///
326/// If the player's results are empty, the player's rating deviation will automatically be decayed using [`decay_deviation`].
327///
328/// # Examples
329/// ```
330/// use skillratings::{
331///     glicko::{glicko_rating_period, GlickoConfig, GlickoRating},
332///     Outcomes,
333/// };
334///
335/// let player = GlickoRating {
336///     rating: 1500.0,
337///     deviation: 200.0,
338/// };
339///
340/// let opponent1 = GlickoRating {
341///     rating: 1400.0,
342///     deviation: 30.0,
343/// };
344///
345/// let opponent2 = GlickoRating {
346///     rating: 1550.0,
347///     deviation: 100.0,
348/// };
349///
350/// let opponent3 = GlickoRating {
351///     rating: 1700.0,
352///     deviation: 300.0,
353/// };
354///
355/// let results = vec![
356///     (opponent1, Outcomes::WIN),
357///     (opponent2, Outcomes::LOSS),
358///     (opponent3, Outcomes::LOSS),
359/// ];
360///
361/// let config = GlickoConfig::new();
362///
363/// let new_player = glicko_rating_period(&player, &results, &config);
364///
365/// assert!((new_player.rating.round() - 1462.0).abs() < f64::EPSILON);
366/// assert!((new_player.deviation.round() - 155.0).abs() < f64::EPSILON);
367/// ```
368pub fn glicko_rating_period(
369    player: &GlickoRating,
370    results: &[(GlickoRating, Outcomes)],
371    config: &GlickoConfig,
372) -> GlickoRating {
373    let q = 10_f64.ln() / 400.0;
374
375    if results.is_empty() {
376        return decay_deviation(player, config);
377    }
378
379    let d_sq = (q.powi(2)
380        * results
381            .iter()
382            .map(|r| {
383                let g = g_value(q, r.0.deviation);
384
385                let e = e_value(g, player.rating, r.0.rating);
386
387                g.powi(2) * e * (1.0 - e)
388            })
389            .sum::<f64>())
390    .recip();
391
392    let m = results
393        .iter()
394        .map(|r| {
395            let g = g_value(q, r.0.deviation);
396
397            let e = e_value(g, player.rating, r.0.rating);
398
399            let s = r.1.to_chess_points();
400
401            g * (s - e)
402        })
403        .sum();
404
405    let pre_deviation = player.deviation.hypot(config.c).min(350.0);
406    let new_rating = (q / (pre_deviation.powi(2).recip() + d_sq.recip())).mul_add(m, player.rating);
407    let new_deviation = (pre_deviation.powi(2).recip() + d_sq.recip())
408        .recip()
409        .sqrt();
410
411    GlickoRating {
412        rating: new_rating,
413        deviation: new_deviation,
414    }
415}
416
417#[must_use]
418/// Calculates the expected outcome of two players based on glicko.
419///
420/// Takes in two players as [`GlickoRating`]s and returns the probability of victory for each player as an [`f64`] between 1.0 and 0.0.  
421/// 1.0 means a certain victory for the player, 0.0 means certain loss.
422/// Values near 0.5 mean a draw is likely to occur.
423///
424/// # Examples
425/// ```
426/// use skillratings::glicko::{expected_score, GlickoRating};
427///
428/// let player_one = GlickoRating {
429///     rating: 2500.0,
430///     deviation: 41.0,
431/// };
432/// let player_two = GlickoRating {
433///     rating: 1950.0,
434///     deviation: 320.0,
435/// };
436/// let (exp_one, exp_two) = expected_score(&player_one, &player_two);
437/// assert!(((exp_one * 100.0).round() - 90.0).abs() < f64::EPSILON);
438/// assert!(((exp_two * 100.0).round() - 10.0).abs() < f64::EPSILON);
439/// ```
440pub fn expected_score(player_one: &GlickoRating, player_two: &GlickoRating) -> (f64, f64) {
441    let q = 10_f64.ln() / 400.0;
442    let g = g_value(q, player_one.deviation.hypot(player_two.deviation));
443
444    let exp_one = (1.0 + 10_f64.powf(-g * (player_one.rating - player_two.rating) / 400.0)).recip();
445    let exp_two = 1.0 - exp_one;
446
447    (exp_one, exp_two)
448}
449
450#[must_use]
451/// Calculates the expected outcome of a player in a rating period or tournament.
452///
453/// Takes in a players as [`GlickoRating`] and a list of opponents as a slice of [`GlickoRating`]
454/// and returns the probability of victory for each match as an Vec of [`f64`] between 1.0 and 0.0 from the perspective of the player.  
455/// 1.0 means a certain victory for the player, 0.0 means certain loss.
456/// Values near 0.5 mean a draw is likely to occur.
457///
458/// # Examples
459/// ```
460/// use skillratings::glicko::{expected_score_rating_period, GlickoRating};
461///
462/// let player = GlickoRating {
463///     rating: 1900.0,
464///     deviation: 120.0,
465/// };
466///
467/// let opponent1 = GlickoRating {
468///     rating: 1930.0,
469///     deviation: 120.0,
470/// };
471///
472/// let opponent2 = GlickoRating {
473///     rating: 1730.0,
474///     deviation: 120.0,
475/// };
476///
477/// let exp = expected_score_rating_period(&player, &[opponent1, opponent2]);
478///
479/// assert_eq!((exp[0] * 100.0).round(), 46.0);
480/// assert_eq!((exp[1] * 100.0).round(), 70.0);
481/// ```
482pub fn expected_score_rating_period(player: &GlickoRating, opponents: &[GlickoRating]) -> Vec<f64> {
483    opponents
484        .iter()
485        .map(|o| {
486            let q = 10_f64.ln() / 400.0;
487            let g = g_value(q, player.deviation.hypot(o.deviation));
488
489            (1.0 + 10_f64.powf(-g * (player.rating - o.rating) / 400.0)).recip()
490        })
491        .collect()
492}
493
494#[must_use]
495/// Decays a Rating Deviation Value for a player, if they missed playing in a certain rating period.
496///
497/// The length of the rating period and thus the number of missed periods per player is something to decide and track yourself.
498///
499/// Takes in a player as a [`GlickoRating`] and a [`GlickoConfig`], that describes how much the rating should change, and returns the decayed [`GlickoRating`].
500///
501/// # Examples
502/// ```
503/// use skillratings::glicko::{decay_deviation, GlickoConfig, GlickoRating};
504///
505/// let player_one = GlickoRating {
506///     rating: 2720.0,
507///     deviation: 41.3,
508/// };
509///
510/// let config = GlickoConfig::new();
511///
512/// let player_one_decay = decay_deviation(&player_one, &config);
513///
514/// assert!((player_one_decay.deviation.round() - 75.0).abs() < f64::EPSILON);
515/// ```
516pub fn decay_deviation(player: &GlickoRating, config: &GlickoConfig) -> GlickoRating {
517    let new_player_deviation = player.deviation.hypot(config.c).min(350.0);
518
519    GlickoRating {
520        rating: player.rating,
521        deviation: new_player_deviation,
522    }
523}
524
525#[must_use]
526/// The 95% confidence interval of the lowest to highest rating.
527///
528/// The system is 95% sure that the "true skill" of the player is in-between these values.
529///
530/// Takes in a player as a [`GlickoRating`] and returns two [`f64`]s that describe the lowest and highest rating.
531///
532/// # Examples
533/// ```
534/// use skillratings::glicko::{confidence_interval, GlickoRating};
535///
536/// let player = GlickoRating {
537///     rating: 2250.0,
538///     deviation: 79.0,
539/// };
540///
541/// let (interval_low, interval_high) = confidence_interval(&player);
542///
543/// assert!(interval_low.round() - 2095.0 < f64::EPSILON);
544/// assert!(interval_high.round() - 2405.0 < f64::EPSILON);
545/// ```
546pub fn confidence_interval(player: &GlickoRating) -> (f64, f64) {
547    (
548        1.96f64.mul_add(-player.deviation, player.rating),
549        1.96f64.mul_add(player.deviation, player.rating),
550    )
551}
552
553fn new_deviation(pre_deviation: f64, d: f64) -> f64 {
554    (pre_deviation.powi(2).recip() + d.recip()).recip().sqrt()
555}
556
557fn new_rating(
558    old_rating: f64,
559    pre_deviation: f64,
560    score: f64,
561    q: f64,
562    g: f64,
563    e: f64,
564    d: f64,
565) -> f64 {
566    ((q / (pre_deviation.powi(2).recip() + d.recip())) * g).mul_add(score - e, old_rating)
567}
568
569fn g_value(q: f64, opponent_deviation: f64) -> f64 {
570    (1.0 + ((3.0 * q.powi(2) * opponent_deviation.powi(2)) / (PI.powi(2))))
571        .sqrt()
572        .recip()
573}
574
575fn e_value(g: f64, rating: f64, opponent_rating: f64) -> f64 {
576    (1.0 + (10_f64.powf(-g * (rating - opponent_rating) / 400.0))).recip()
577}
578
579fn d_value(q: f64, g: f64, e: f64) -> f64 {
580    (q.powi(2) * g.powi(2) * e * (1.0 - e)).powi(-1)
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[test]
588    fn test_glicko() {
589        let player1 = GlickoRating {
590            rating: 1500.0,
591            deviation: 200.0,
592        };
593
594        let opponent1 = GlickoRating {
595            rating: 1400.0,
596            deviation: 30.0,
597        };
598
599        let opponent2 = GlickoRating {
600            rating: 1550.0,
601            deviation: 100.0,
602        };
603
604        let opponent3 = GlickoRating {
605            rating: 1700.0,
606            deviation: 300.0,
607        };
608
609        let config = GlickoConfig::default();
610
611        let (player1, _) = glicko(&player1, &opponent1, &Outcomes::WIN, &config);
612
613        let (player1, _) = glicko(&player1, &opponent2, &Outcomes::LOSS, &config);
614
615        let (player1, _) = glicko(&player1, &opponent3, &Outcomes::LOSS, &config);
616
617        assert!((player1.rating.round() - 1449.0).abs() < f64::EPSILON);
618        assert!((player1.deviation - 171.684_472_141_285_57).abs() < f64::EPSILON);
619    }
620
621    #[test]
622    /// This test is taken directly from the official glicko example.  
623    /// <http://www.glicko.net/glicko/glicko.pdf>
624    /// The result will be slightly different from above,
625    /// because the games in a rating period are considered to be played at the same time.
626    fn test_glicko_rating_period() {
627        // This weird deviation compensates for official example
628        // not performing calculations from step 1.
629        let player = GlickoRating {
630            rating: 1500.0,
631            deviation: 189.751_837_935_762_84,
632        };
633
634        let opponent1 = GlickoRating {
635            rating: 1400.0,
636            deviation: 30.0,
637        };
638
639        let opponent2 = GlickoRating {
640            rating: 1550.0,
641            deviation: 100.0,
642        };
643
644        let opponent3 = GlickoRating {
645            rating: 1700.0,
646            deviation: 300.0,
647        };
648
649        let results = vec![
650            (opponent1, Outcomes::WIN),
651            (opponent2, Outcomes::LOSS),
652            (opponent3, Outcomes::LOSS),
653        ];
654
655        let new_player = glicko_rating_period(&player, &results, &GlickoConfig::new());
656
657        assert!((new_player.rating.round() - 1464.0).abs() < f64::EPSILON);
658        assert!((new_player.deviation - 151.398_902_447_969_33).abs() < f64::EPSILON);
659
660        let player = GlickoRating {
661            rating: 1500.0,
662            deviation: 50.0,
663        };
664
665        let results: Vec<(GlickoRating, Outcomes)> = Vec::new();
666
667        let new_player = glicko_rating_period(&player, &results, &GlickoConfig::new());
668
669        assert!((new_player.deviation - 80.586_847_562_117_73).abs() < f64::EPSILON);
670    }
671
672    #[test]
673    fn test_single_rp() {
674        let player = GlickoRating {
675            rating: 1200.0,
676            deviation: 25.0,
677        };
678        let opponent = GlickoRating {
679            rating: 1500.0,
680            deviation: 34.0,
681        };
682
683        let config = GlickoConfig::new();
684
685        let (np, _) = glicko(&player, &opponent, &Outcomes::WIN, &config);
686
687        let rp = glicko_rating_period(&player, &[(opponent, Outcomes::WIN)], &config);
688
689        assert_eq!(rp, np);
690    }
691
692    #[test]
693    /// This test is taken directly from the official glicko example.  
694    /// <http://www.glicko.net/glicko/glicko.pdf>
695    fn test_expected_score() {
696        let player_one = GlickoRating {
697            rating: 1400.0,
698            deviation: 40.0,
699        };
700
701        let player_two = GlickoRating {
702            rating: 1500.0,
703            deviation: 150.0,
704        };
705
706        let (exp_one, exp_two) = expected_score(&player_one, &player_two);
707
708        assert!((exp_one - 0.373_700_405_951_935).abs() < f64::EPSILON);
709        assert!((exp_two - 0.626_299_594_048_065).abs() < f64::EPSILON);
710        assert!((exp_one + exp_two - 1.0).abs() < f64::EPSILON);
711    }
712
713    #[test]
714    /// This test is taken directly from the official glicko example.  
715    /// <http://www.glicko.net/glicko/glicko.pdf>
716    fn test_confidence_interval() {
717        let player = GlickoRating {
718            rating: 1500.0,
719            deviation: 30.0,
720        };
721
722        let ci = confidence_interval(&player);
723
724        assert!((ci.0.round() - 1441.0).abs() < f64::EPSILON);
725        assert!((ci.1.round() - 1559.0).abs() < f64::EPSILON);
726    }
727
728    #[test]
729    /// This test is taken directly from the official glicko example.  
730    /// <http://www.glicko.net/glicko/glicko.pdf>
731    fn test_decay_deviation() {
732        let player = GlickoRating {
733            rating: 1500.0,
734            deviation: 50.0,
735        };
736
737        let mut player = decay_deviation(&player, &GlickoConfig::new());
738
739        assert!((player.deviation - 80.586_847_562_117_73).abs() < f64::EPSILON);
740
741        for _ in 0..29 {
742            player = decay_deviation(&player, &GlickoConfig::default());
743        }
744
745        assert!(((player.deviation * 1000.0).round() - 349_753.0).abs() < f64::EPSILON);
746
747        player = decay_deviation(&player, &GlickoConfig::new());
748
749        assert!((player.deviation - 350.0).abs() < f64::EPSILON);
750    }
751
752    #[test]
753    fn test_unequal_draws() {
754        let mut player = GlickoRating::new();
755
756        let mut opponent = GlickoRating {
757            rating: 2230.0,
758            deviation: 41.0,
759        };
760
761        (player, opponent) = glicko(
762            &player,
763            &opponent,
764            &Outcomes::DRAW,
765            &GlickoConfig::default(),
766        );
767
768        assert!((player.rating.round() - 1820.0).abs() < f64::EPSILON);
769        assert!((player.deviation.round() - 340.0).abs() < f64::EPSILON);
770
771        assert!((opponent.rating.round() - 2220.0).abs() < f64::EPSILON);
772        assert!((opponent.deviation.round() - 75.0).abs() < f64::EPSILON);
773    }
774
775    #[test]
776    #[allow(clippy::clone_on_copy)]
777    fn test_misc_stuff() {
778        let player_new = GlickoRating::new();
779        let player_default = GlickoRating::default();
780
781        assert!((player_new.rating - player_default.rating).abs() < f64::EPSILON);
782        assert!((player_new.deviation - player_new.deviation).abs() < f64::EPSILON);
783
784        let player_one = GlickoRating::new();
785        let config = GlickoConfig::new();
786
787        assert_eq!(player_one, player_one.clone());
788        assert!((config.c - config.clone().c).abs() < f64::EPSILON);
789
790        assert!(!format!("{player_one:?}").is_empty());
791        assert!(!format!("{config:?}").is_empty());
792
793        assert_eq!(player_one, GlickoRating::from((1500.0, 350.0)));
794    }
795
796    #[test]
797    fn test_traits() {
798        let player_one: GlickoRating = Rating::new(Some(240.0), Some(90.0));
799        let player_two: GlickoRating = Rating::new(Some(240.0), Some(90.0));
800
801        let rating_system: Glicko = RatingSystem::new(GlickoConfig::new());
802
803        assert!((player_one.rating() - 240.0).abs() < f64::EPSILON);
804        assert_eq!(player_one.uncertainty(), Some(90.0));
805
806        let (new_player_one, new_player_two) =
807            RatingSystem::rate(&rating_system, &player_one, &player_two, &Outcomes::WIN);
808
809        let (exp1, exp2) = RatingSystem::expected_score(&rating_system, &player_one, &player_two);
810
811        assert!((new_player_one.rating - 270.633_674_957_731_9).abs() < f64::EPSILON);
812        assert!((new_player_two.rating - 209.366_325_042_268_1).abs() < f64::EPSILON);
813        assert!((exp1 - 0.5).abs() < f64::EPSILON);
814        assert!((exp2 - 0.5).abs() < f64::EPSILON);
815
816        let rating_period_system: Glicko = RatingPeriodSystem::new(GlickoConfig::new());
817        let exp_rp =
818            RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
819        assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);
820
821        let player_one: GlickoRating = Rating::new(Some(240.0), Some(90.0));
822        let player_two: GlickoRating = Rating::new(Some(240.0), Some(90.0));
823
824        let rating_period: Glicko = RatingPeriodSystem::new(GlickoConfig::new());
825
826        let new_player_one =
827            RatingPeriodSystem::rate(&rating_period, &player_one, &[(player_two, Outcomes::WIN)]);
828
829        assert!((new_player_one.rating - 270.633_674_957_731_9).abs() < f64::EPSILON);
830    }
831}