1#[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))]
62pub struct GlickoRating {
69 pub rating: f64,
71 pub deviation: f64,
73}
74
75impl GlickoRating {
76 #[must_use]
77 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))]
145pub struct GlickoConfig {
147 pub c: f64,
152}
153
154impl GlickoConfig {
155 #[must_use]
156 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
168pub 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]
213pub 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]
314pub 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]
418pub 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]
451pub 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]
495pub 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]
526pub 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 fn test_glicko_rating_period() {
627 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 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 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 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}