instant_glicko_2/
algorithm.rs

1//! This module hosts the methods and types necessary to perform Glicko-2 calculations with fractional rating periods.
2
3use std::f64::consts::PI;
4use std::time::{Duration, SystemTime};
5
6use crate::{
7    constants, FromWithParameters, InternalRating, IntoWithParameters, Parameters, PublicRating,
8};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// A rating at a specific point in time.
14/// This is a *public* rating, meaning it is meant to be displayed to users,
15/// but it needs to be converted to an internal rating before use in rating calculations.
16///
17/// The timing of the rating is important because the deviation increases over the time no games are recorded.
18#[derive(Clone, Copy, PartialEq, Debug)]
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
20pub struct TimedPublicRating {
21    last_updated: SystemTime,
22    rating: PublicRating,
23}
24
25impl TimedPublicRating {
26    /// Creates a new [`TimedPublicRating`] at the given `last_updated` time with the given `rating`.
27    #[must_use]
28    pub fn new(last_updated: SystemTime, rating: PublicRating) -> Self {
29        TimedPublicRating {
30            last_updated,
31            rating,
32        }
33    }
34
35    /// The time this rating was last updated.
36    #[must_use]
37    pub fn last_updated(&self) -> SystemTime {
38        self.last_updated
39    }
40
41    /// The rating at the time it was last updated.
42    #[must_use]
43    pub fn raw_public_rating(&self) -> PublicRating {
44        self.rating
45    }
46
47    /// The rating with the deviation updated to the current time after no games were played since the last update.
48    /// Convenience for `self.public_rating_at(SystemTime::now(), parameters, rating_period_duration)`.
49    ///
50    /// # Panics
51    ///
52    /// This function panics if `last_updated` is in the future, or if the `rating_period_duration` is zero.
53    #[must_use]
54    pub fn public_rating_now(
55        &self,
56        parameters: Parameters,
57        rating_period_duration: Duration,
58    ) -> PublicRating {
59        self.public_rating_at(SystemTime::now(), parameters, rating_period_duration)
60    }
61
62    /// The rating with the deviation updated to the given time after no games were played since the last update.
63    ///
64    /// # Panics
65    ///
66    /// This function panics if `last_updated` is after `time`, or if the `rating_period_duration` is zero.
67    #[must_use]
68    pub fn public_rating_at(
69        &self,
70        time: SystemTime,
71        parameters: Parameters,
72        rating_period_duration: Duration,
73    ) -> PublicRating {
74        let internal_rating: InternalRating = self.rating.into_with_parameters(parameters);
75
76        let new_deviation = calculate_pre_rating_period_value(
77            internal_rating.volatility(),
78            internal_rating,
79            self.elapsed_rating_periods(time, rating_period_duration),
80        );
81
82        InternalRating {
83            deviation: new_deviation,
84            ..internal_rating
85        }
86        .into_with_parameters(parameters)
87    }
88
89    /// # Panics
90    ///
91    /// This function panics if `time` is **before** the last rating update, or if the `rating_period_duration` is zero.
92    #[must_use]
93    fn elapsed_rating_periods(&self, time: SystemTime, rating_period_duration: Duration) -> f64 {
94        time.duration_since(self.last_updated)
95            .expect("Player rating was updated after the game to rate")
96            .as_secs_f64()
97            / rating_period_duration.as_secs_f64()
98    }
99}
100
101impl FromWithParameters<TimedInternalRating> for TimedPublicRating {
102    fn from_with_parameters(internal: TimedInternalRating, parameters: Parameters) -> Self {
103        TimedPublicRating::new(
104            internal.last_updated,
105            internal.rating.into_with_parameters(parameters),
106        )
107    }
108}
109
110/// A rating at a specific point in time.
111/// This is an *internal* rating, meaning it can be used immediately in rating calculations,
112/// but should be converted to a public rating before displaying to users.
113///
114/// The timing of the rating is important because the deviation increases over the time no games are recorded.
115#[derive(Clone, Copy, PartialEq, Debug)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub struct TimedInternalRating {
118    last_updated: SystemTime,
119    rating: InternalRating,
120}
121
122impl TimedInternalRating {
123    /// Creates a new [`TimedInternalRating`] at the given `last_updated` time with the given `rating`.
124    #[must_use]
125    pub fn new(last_updated: SystemTime, rating: InternalRating) -> Self {
126        TimedInternalRating {
127            last_updated,
128            rating,
129        }
130    }
131
132    /// The time this rating was last updated.
133    #[must_use]
134    pub fn last_updated(&self) -> SystemTime {
135        self.last_updated
136    }
137
138    /// The rating at the time it was last updated.
139    #[must_use]
140    pub fn raw_internal_rating(&self) -> InternalRating {
141        self.rating
142    }
143
144    /// The rating with the deviation updated to the current time after no games were played since the last update.
145    /// Convenience for `self.public_rating_at(SystemTime::now(), parameters, rating_period_duration)`.
146    ///
147    /// # Panics
148    ///
149    /// This function panics if `last_updated` is in the future, or if the `rating_period_duration` is zero.
150    #[must_use]
151    pub fn internal_rating_now(&self, rating_period_duration: Duration) -> InternalRating {
152        self.internal_rating_at(SystemTime::now(), rating_period_duration)
153    }
154
155    /// The rating with the deviation updated to the given time after no games were played since the last update.
156    ///
157    /// # Panics
158    ///
159    /// This function panics if `last_updated` is after `time`, or if the `rating_period_duration` is zero.
160    #[must_use]
161    pub fn internal_rating_at(
162        &self,
163        time: SystemTime,
164        rating_period_duration: Duration,
165    ) -> InternalRating {
166        let new_deviation = calculate_pre_rating_period_value(
167            self.rating.volatility(),
168            self.rating,
169            self.elapsed_rating_periods(time, rating_period_duration),
170        );
171
172        InternalRating {
173            deviation: new_deviation,
174            ..self.rating
175        }
176    }
177
178    /// # Panics
179    ///
180    /// This function panics if `time` is **before** the last rating update, or if the `rating_period_duration` is zero.
181    #[must_use]
182    fn elapsed_rating_periods(&self, time: SystemTime, rating_period_duration: Duration) -> f64 {
183        time.duration_since(self.last_updated)
184            .expect("Player rating was updated after the game to rate")
185            .as_secs_f64()
186            / rating_period_duration.as_secs_f64()
187    }
188}
189
190impl FromWithParameters<TimedPublicRating> for TimedInternalRating {
191    fn from_with_parameters(public: TimedPublicRating, parameters: Parameters) -> Self {
192        TimedInternalRating::new(
193            public.last_updated,
194            public.rating.into_with_parameters(parameters),
195        )
196    }
197}
198
199/// Game information encompassing the opponent's rating at the time of the game
200/// as well as the game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
201///
202/// Keep in mind that this struct does not hold information about the player's rating, only the opponent's.
203/// This is because it is used in relation to registering games on and therefore update the player's rating struct.
204#[derive(Clone, Copy, PartialEq, Debug)]
205#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
206pub struct PublicGame {
207    opponent: PublicRating,
208    score: f64,
209}
210
211impl PublicGame {
212    /// Creates a new [`PublicGame`] with the given `opponent` and `score`.
213    /// `score` is a number between 0.0 (decisive opponent win) and `1.0` (decisive player win).
214    ///
215    /// # Panics
216    ///
217    /// This function panics if `score` is less than `0.0` or greater than `1.0`.
218    #[must_use]
219    pub fn new(opponent: PublicRating, score: f64) -> Self {
220        assert!((0.0..=1.0).contains(&score));
221
222        PublicGame { opponent, score }
223    }
224
225    /// The opponent's rating.
226    #[must_use]
227    pub fn opponent(&self) -> PublicRating {
228        self.opponent
229    }
230
231    /// The game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
232    #[must_use]
233    pub fn score(&self) -> f64 {
234        self.score
235    }
236
237    /// Converts a [`TimedPublicGame`] to a [`PublicGame`],
238    /// erasing the timing information and resolving the opponents rating to their rating at the time of the game.
239    ///
240    /// # Panics
241    ///
242    /// This function panics if `timed_game`'s opponent rating was updated after the game was recorded, or if the `rating_period_duration` is zero.
243    #[must_use]
244    pub fn from_timed_public_game(
245        timed_game: TimedPublicGame,
246        parameters: Parameters,
247        rating_period_duration: Duration,
248    ) -> Self {
249        timed_game.to_public_game(parameters, rating_period_duration)
250    }
251}
252
253impl FromWithParameters<InternalGame> for PublicGame {
254    fn from_with_parameters(internal: InternalGame, parameters: Parameters) -> Self {
255        PublicGame::new(
256            internal.opponent.into_with_parameters(parameters),
257            internal.score,
258        )
259    }
260}
261
262/// Game information encompassing the opponent's internal rating at the time of the game
263/// as well as the game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
264///
265/// Keep in mind that this struct does not hold information about the player's rating, only the opponent's.
266/// This is because it is used in relation to registering games on and therefore update the player's rating struct.
267#[derive(Clone, Copy, PartialEq, Debug)]
268#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
269pub struct InternalGame {
270    opponent: InternalRating,
271    score: f64,
272}
273
274impl InternalGame {
275    /// Creates a new [`InternalGame`] with the given `opponent` and `score`.
276    /// `score` is a number between 0.0 (decisive opponent win) and `1.0` (decisive player win).
277    ///
278    /// # Panics
279    ///
280    /// This function panics if `score` is less than `0.0` or greater than `1.0`.
281    #[must_use]
282    pub fn new(opponent: InternalRating, score: f64) -> Self {
283        assert!((0.0..=1.0).contains(&score));
284
285        InternalGame { opponent, score }
286    }
287
288    /// The opponent's rating.
289    #[must_use]
290    pub fn opponent(&self) -> InternalRating {
291        self.opponent
292    }
293
294    /// The game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
295    #[must_use]
296    pub fn score(&self) -> f64 {
297        self.score
298    }
299
300    /// Converts a [`TimedInternalGame`] to an [`InternalGame`],
301    /// erasing the timing information and resolving the opponents rating to their rating at the time of the game.
302    ///
303    /// # Panics
304    ///
305    /// This function panics if `timed_game`'s opponent rating was updated after the game was recorded, or if the `rating_period_duration` is zero.
306    #[must_use]
307    pub fn from_timed_internal_game(
308        timed_game: TimedInternalGame,
309        rating_period_duration: Duration,
310    ) -> Self {
311        timed_game.to_internal_game(rating_period_duration)
312    }
313}
314
315impl FromWithParameters<PublicGame> for InternalGame {
316    fn from_with_parameters(public: PublicGame, parameters: Parameters) -> Self {
317        InternalGame::new(
318            public.opponent.into_with_parameters(parameters),
319            public.score,
320        )
321    }
322}
323
324/// Game information encompassing
325/// - The time the game was recorded
326/// - The [`TimedPublicRating`] of the opponent
327/// - The score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win)
328///
329/// Keep in mind that this struct does not hold information about the player's rating, only the opponent's.
330/// This is because it is used to register games on and therefore update the player's rating struct.
331#[derive(Clone, Copy, PartialEq, Debug)]
332#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
333pub struct TimedPublicGame {
334    time: SystemTime,
335    opponent: TimedPublicRating,
336    score: f64,
337}
338
339impl TimedPublicGame {
340    /// Creates a new [`TimedPublicGame`] at the given `time` with the given `opponent` and `score`.
341    /// `score` is a number between 0.0 (decisive opponent win) and `1.0` (decisive player win).
342    ///
343    /// # Panics
344    ///
345    /// This function panics if `score` is less than `0.0` or greater than `1.0`.
346    #[must_use]
347    pub fn new(time: SystemTime, opponent: TimedPublicRating, score: f64) -> Self {
348        assert!((0.0..=1.0).contains(&score));
349
350        TimedPublicGame {
351            time,
352            opponent,
353            score,
354        }
355    }
356
357    /// The time this game was recorded.
358    #[must_use]
359    pub fn time(&self) -> SystemTime {
360        self.time
361    }
362
363    /// The opponent's rating.
364    #[must_use]
365    pub fn opponent(&self) -> TimedPublicRating {
366        self.opponent
367    }
368
369    /// The game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
370    #[must_use]
371    pub fn score(&self) -> f64 {
372        self.score
373    }
374
375    /// The game with timing information erased
376    /// and the opponent's rating resolved to their rating at the time of the last update.
377    #[must_use]
378    pub fn raw_public_game(&self) -> PublicGame {
379        PublicGame::new(self.opponent().raw_public_rating(), self.score())
380    }
381
382    /// Converts this [`TimedPublicGame`] to a [`PublicGame`],
383    /// erasing the timing information and resolving the opponent's rating to their rating at the time of the game.
384    ///
385    /// # Panics
386    ///
387    /// This function panics if the opponent rating was updated after the game was recorded, or if the `rating_period_duration` is zero.
388    #[must_use]
389    pub fn to_public_game(
390        &self,
391        parameters: Parameters,
392        rating_period_duration: Duration,
393    ) -> PublicGame {
394        self.public_game_at(self.time(), parameters, rating_period_duration)
395    }
396
397    /// Converts this [`TimedPublicGame`] to a [`PublicGame`],
398    /// erasing the timing information and resolving the opponent's rating to their rating at the given `time`.
399    ///
400    /// # Panics
401    ///
402    /// This function panics if the given `time` is before the opponent rating's last update, or if `rating_period_duration` is zero.
403    #[must_use]
404    pub fn public_game_at(
405        &self,
406        time: SystemTime,
407        parameters: Parameters,
408        rating_period_duration: Duration,
409    ) -> PublicGame {
410        let opponent = self
411            .opponent()
412            .public_rating_at(time, parameters, rating_period_duration);
413
414        PublicGame::new(opponent, self.score())
415    }
416}
417
418impl FromWithParameters<TimedInternalGame> for TimedPublicGame {
419    fn from_with_parameters(internal: TimedInternalGame, parameters: Parameters) -> Self {
420        TimedPublicGame::new(
421            internal.time,
422            internal.opponent.into_with_parameters(parameters),
423            internal.score,
424        )
425    }
426}
427
428/// Game information encompassing
429/// - The time the game was recorded
430/// - The [`TimedInternalRating`] of the opponent
431/// - The score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win)
432///
433/// Keep in mind that this struct does not hold information about the player's rating, only the opponent's.
434/// This is because it is used to register games on and therefore update the player's rating struct.
435#[derive(Clone, Copy, PartialEq, Debug)]
436#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
437pub struct TimedInternalGame {
438    time: SystemTime,
439    opponent: TimedInternalRating,
440    score: f64,
441}
442
443impl TimedInternalGame {
444    /// Creates a new [`TimedInternalGame`] at the given `time` with the given `opponent` and `score`.
445    /// `score` is a number between 0.0 (decisive opponent win) and `1.0` (decisive player win).
446    ///
447    /// # Panics
448    ///
449    /// This function panics if `score` is less than `0.0` or greater than `1.0`.
450    #[must_use]
451    pub fn new(time: SystemTime, opponent: TimedInternalRating, score: f64) -> Self {
452        assert!((0.0..=1.0).contains(&score));
453
454        TimedInternalGame {
455            time,
456            opponent,
457            score,
458        }
459    }
460
461    /// The time this game was recorded.
462    #[must_use]
463    pub fn time(&self) -> SystemTime {
464        self.time
465    }
466
467    /// The opponent's rating.
468    #[must_use]
469    pub fn opponent(&self) -> TimedInternalRating {
470        self.opponent
471    }
472
473    /// The game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
474    #[must_use]
475    pub fn score(&self) -> f64 {
476        self.score
477    }
478
479    /// The game with timing information erased
480    /// and the opponent's rating resolved to their rating at the time of the last update.
481    #[must_use]
482    pub fn raw_internal_game(&self) -> InternalGame {
483        InternalGame::new(self.opponent().raw_internal_rating(), self.score())
484    }
485
486    /// Converts this [`TimedInternalGame`] to an [`InternalGame`],
487    /// erasing the timing information and resolving the opponents rating to their rating at the time of the game.
488    ///
489    /// # Panics
490    ///
491    /// This function panics if the opponent rating was updated after the game was recorded, or if the `rating_period_duration` is zero.
492    #[must_use]
493    pub fn to_internal_game(&self, rating_period_duration: Duration) -> InternalGame {
494        self.internal_game_at(self.time(), rating_period_duration)
495    }
496
497    /// Converts this [`TimedInternalGame`] to an [`InternalGame`],
498    /// erasing the timing information and resolving the opponent's rating to their rating at the given `time`.
499    ///
500    /// # Panics
501    ///
502    /// This function panics if the given `time` is before the opponent rating's last update, or if `rating_period_duration` is zero.
503    #[must_use]
504    pub fn internal_game_at(
505        &self,
506        time: SystemTime,
507        rating_period_duration: Duration,
508    ) -> InternalGame {
509        let opponent = self
510            .opponent()
511            .internal_rating_at(time, rating_period_duration);
512
513        InternalGame::new(opponent, self.score)
514    }
515}
516
517impl FromWithParameters<TimedPublicGame> for TimedInternalGame {
518    fn from_with_parameters(public: TimedPublicGame, parameters: Parameters) -> Self {
519        TimedInternalGame::new(
520            public.time,
521            public.opponent.into_with_parameters(parameters),
522            public.score,
523        )
524    }
525}
526
527/// A match result where the opponent's rating is a [`TimedPublicRating`].
528/// The game itself is not timed.
529/// The struct holds only the opponent's rating and the game score from the player's perspective.
530#[derive(Clone, Copy, PartialEq, Debug)]
531#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
532pub struct TimedOpponentPublicGame {
533    opponent: TimedPublicRating,
534    score: f64,
535}
536
537impl TimedOpponentPublicGame {
538    /// Creates a new [`TimedOpponentPublicGame`] with the given `opponent` and the player's `score`.
539    /// `score` is a number between 0.0 (decisive opponent win) and `1.0` (decisive player win).
540    ///
541    /// # Panics
542    ///
543    /// This function panics if `score` is less than `0.0` or greater than `1.0`.
544    #[must_use]
545    pub fn new(opponent: TimedPublicRating, score: f64) -> Self {
546        assert!((0.0..=1.0).contains(&score));
547
548        TimedOpponentPublicGame { opponent, score }
549    }
550
551    /// The opponent's rating.
552    #[must_use]
553    pub fn opponent(&self) -> TimedPublicRating {
554        self.opponent
555    }
556
557    /// The game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
558    #[must_use]
559    pub fn score(&self) -> f64 {
560        self.score
561    }
562
563    /// Returns a [`TimedPublicGame`] which represents the given game happening at the given `time`.
564    #[must_use]
565    pub fn timed_public_game_at(&self, time: SystemTime) -> TimedPublicGame {
566        TimedPublicGame::new(time, self.opponent, self.score)
567    }
568
569    /// Returns a [`PublicGame`], resolving the opponent's rating to their rating at the given time.
570    #[must_use]
571    pub fn public_game_at(
572        &self,
573        time: SystemTime,
574        parameters: Parameters,
575        rating_period_duration: Duration,
576    ) -> PublicGame {
577        let opponent = self
578            .opponent
579            .public_rating_at(time, parameters, rating_period_duration);
580
581        PublicGame::new(opponent, self.score)
582    }
583}
584
585impl FromWithParameters<TimedOpponentInternalGame> for TimedOpponentPublicGame {
586    fn from_with_parameters(internal: TimedOpponentInternalGame, parameters: Parameters) -> Self {
587        TimedOpponentPublicGame::new(
588            internal.opponent.into_with_parameters(parameters),
589            internal.score,
590        )
591    }
592}
593
594/// A match result where the opponent's rating is a [`TimedInternalRating`].
595/// The game itself is not timed.
596/// The struct holds only the opponent's rating and the game score from the player's perspective.
597#[derive(Clone, Copy, PartialEq, Debug)]
598#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
599pub struct TimedOpponentInternalGame {
600    opponent: TimedInternalRating,
601    score: f64,
602}
603
604impl TimedOpponentInternalGame {
605    /// Creates a new [`TimedOpponentInternalGame`] with the given `opponent` and the player's `score`.
606    /// `score` is a number between 0.0 (decisive opponent win) and `1.0` (decisive player win).
607    ///
608    /// # Panics
609    ///
610    /// This function panics if `score` is less than `0.0` or greater than `1.0`.
611    #[must_use]
612    pub fn new(opponent: TimedInternalRating, score: f64) -> Self {
613        assert!((0.0..=1.0).contains(&score));
614
615        TimedOpponentInternalGame { opponent, score }
616    }
617
618    /// The opponent's rating.
619    #[must_use]
620    pub fn opponent(&self) -> TimedInternalRating {
621        self.opponent
622    }
623
624    /// The game score as a number between `0.0` (decisive opponent win) and `1.0` (decisive player win).
625    #[must_use]
626    pub fn score(&self) -> f64 {
627        self.score
628    }
629
630    /// Returns a [`TimedInternalGame`] which represents the given game happening at the given `time`.
631    #[must_use]
632    pub fn timed_internal_game_at(&self, time: SystemTime) -> TimedInternalGame {
633        TimedInternalGame::new(time, self.opponent, self.score)
634    }
635
636    /// Returns a [`InternalGame`], resolving the opponent's rating to their rating at the given time.
637    #[must_use]
638    pub fn internal_game_at(
639        &self,
640        time: SystemTime,
641        rating_period_duration: Duration,
642    ) -> InternalGame {
643        let opponent = self
644            .opponent
645            .internal_rating_at(time, rating_period_duration);
646
647        InternalGame::new(opponent, self.score)
648    }
649}
650
651impl FromWithParameters<TimedOpponentPublicGame> for TimedOpponentInternalGame {
652    fn from_with_parameters(public: TimedOpponentPublicGame, parameters: Parameters) -> Self {
653        TimedOpponentInternalGame::new(
654            public.opponent.into_with_parameters(parameters),
655            public.score,
656        )
657    }
658}
659
660/// A collection of [`TimedOpponentPublicGame`] where the games are
661/// all considered to be played at the same, known time.
662/// This struct does not hold information about the player, only about the opponents.
663#[derive(Clone, PartialEq, Debug)]
664#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
665pub struct TimedPublicGames {
666    time: SystemTime,
667    games: Vec<TimedOpponentPublicGame>,
668}
669
670impl TimedPublicGames {
671    /// Creates a new [`TimedPublicGames`] instance
672    /// where all `games` are considered to be played at the given `time`.
673    #[must_use]
674    pub fn new(time: SystemTime, games: Vec<TimedOpponentPublicGame>) -> Self {
675        TimedPublicGames { time, games }
676    }
677
678    /// Creates a new [`TimedPublicGames`] instance from a single `game`,
679    /// which is considered to be played at the given `time`.
680    #[must_use]
681    pub fn single(game: TimedPublicGame) -> Self {
682        TimedPublicGames::new(
683            game.time(),
684            vec![TimedOpponentPublicGame::new(game.opponent(), game.score())],
685        )
686    }
687
688    /// The time the games are considered to be played at.
689    #[must_use]
690    pub fn time(&self) -> SystemTime {
691        self.time
692    }
693
694    /// The games without information about when the games were played.
695    #[must_use]
696    pub fn games(&self) -> &[TimedOpponentPublicGame] {
697        &self.games
698    }
699
700    /// Iterator over the games with information about when they were played.
701    pub fn timed_games(&self) -> impl Iterator<Item = TimedPublicGame> + '_ {
702        self.games
703            .iter()
704            .map(|game| game.timed_public_game_at(self.time()))
705    }
706}
707
708impl From<TimedPublicGame> for TimedPublicGames {
709    fn from(game: TimedPublicGame) -> Self {
710        TimedPublicGames::single(game)
711    }
712}
713
714impl FromWithParameters<TimedInternalGames> for TimedPublicGames {
715    fn from_with_parameters(internal: TimedInternalGames, parameters: Parameters) -> Self {
716        let public_games = internal
717            .games()
718            .iter()
719            .map(|&game| game.into_with_parameters(parameters))
720            .collect();
721
722        TimedPublicGames::new(internal.time(), public_games)
723    }
724}
725
726/// A collection of [`TimedOpponentInternalGame`] where the games are
727/// all considered to be played at the same, known time.
728/// This struct does not hold information about the player, only about the opponents.
729#[derive(Clone, PartialEq, Debug)]
730#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
731pub struct TimedInternalGames {
732    time: SystemTime,
733    games: Vec<TimedOpponentInternalGame>,
734}
735
736impl TimedInternalGames {
737    /// Creates a new [`TimedInternalGames`] instance
738    /// where all `games` are considered to be played at the given `time`.
739    #[must_use]
740    pub fn new(time: SystemTime, games: Vec<TimedOpponentInternalGame>) -> Self {
741        TimedInternalGames { time, games }
742    }
743
744    /// Creates a new [`TimedInternalGames`] instance from a single `game`,
745    /// which is considered to be played at the given `time`.
746    #[must_use]
747    pub fn single(game: TimedInternalGame) -> Self {
748        TimedInternalGames::new(
749            game.time(),
750            vec![TimedOpponentInternalGame::new(
751                game.opponent(),
752                game.score(),
753            )],
754        )
755    }
756
757    /// The time the games are considered to be played at.
758    #[must_use]
759    pub fn time(&self) -> SystemTime {
760        self.time
761    }
762
763    /// The games without information about when the games were played.
764    #[must_use]
765    pub fn games(&self) -> &[TimedOpponentInternalGame] {
766        &self.games
767    }
768
769    /// Iterator over the games with information about when they were played.
770    pub fn timed_games(&self) -> impl Iterator<Item = TimedInternalGame> + '_ {
771        self.games
772            .iter()
773            .map(|game| game.timed_internal_game_at(self.time()))
774    }
775}
776
777impl From<TimedInternalGame> for TimedInternalGames {
778    fn from(game: TimedInternalGame) -> Self {
779        TimedInternalGames::single(game)
780    }
781}
782
783impl FromWithParameters<TimedPublicGames> for TimedInternalGames {
784    fn from_with_parameters(public: TimedPublicGames, parameters: Parameters) -> Self {
785        let internal_games = public
786            .games()
787            .iter()
788            .map(|&game| game.into_with_parameters(parameters))
789            .collect();
790
791        TimedInternalGames::new(public.time(), internal_games)
792    }
793}
794
795/// Calculates the new internal player rating after a `TimedInternalGame` using the Glicko-2 algorithm.
796///
797/// However, this function only provides an *approximation* for the actual new rating
798/// because it considers opponent ratings at the time of the game instead of at the time the player's rating was last updated.
799/// The errors this introduces are relatively small, and can be considered worth it for avoiding the bookkeeping
800/// that would come with tracking an older opponent rating for every possible player rating.
801// TODO: Can we work backwards to that rating to get a better approximation? That would be pretty cool. Glicko-2 compliant time travel. Imagine that.
802/// For a version more accurate to Glicko-2, see [`rate_games_untimed`].
803///
804/// # Panics
805///
806/// This function panics if the `player_rating` or any opponent ratings were updated after the game was played,
807/// or if the `rating_period_duration` is zero.
808///
809/// It can also panic if `parameters.convergence_tolerance()` is unreasonably low.
810#[must_use]
811pub fn rate_game(
812    player_rating: TimedInternalRating,
813    game: TimedInternalGame,
814    rating_period_duration: Duration,
815    parameters: Parameters,
816) -> TimedInternalRating {
817    rate_games(
818        player_rating,
819        &TimedInternalGames::single(game),
820        rating_period_duration,
821        parameters,
822    )
823}
824
825/// Calculates the new internal player rating after the given [`TimedInternalGames`] using the Glicko-2 algorithm.
826/// This is useful since Glicko-2 assumes all games within a rating period were played at the same point in time.
827///
828/// However, this function only provides an *approximation* for the actual new rating
829/// because it considers opponent ratings at the time of the game instead of at the time the player's rating was last updated.
830/// The errors this introduces are relatively small, and can be considered worth it for avoiding the bookkeeping
831/// that would come with tracking an older opponent rating for every possible player rating.
832// TODO: Can we work backwards to that rating to get a better approximation? That would be pretty cool. Glicko-2 compliant time travel. Imagine that.
833/// For a version more accurate to Glicko-2, see [`rate_games_untimed`].
834///
835/// # Panics
836///
837/// This function panics if the `player_rating` or any opponent ratings were updated after the games were played,
838/// or if `rating_period_duration` is zero.
839///
840/// It can also panic if `parameters.convergence_tolerance()` is unreasonably low.
841#[must_use]
842pub fn rate_games(
843    player_rating: TimedInternalRating,
844    games: &TimedInternalGames,
845    rating_period_duration: Duration,
846    parameters: Parameters,
847) -> TimedInternalRating {
848    // Step 1. (initialising) doesn't apply, we have already set the starting ratings.
849    // Step 2. (converting to internal scale) doesn't apply either, we get typed checked internal rating here
850
851    let game_time = games.time();
852
853    // If `games` is empty, only Step 6. applies, which TimedInternalRating does automatically
854    if games.games().is_empty() {
855        return player_rating;
856    }
857
858    // Raw rating because pre_rating_period_value will handle that
859    let player_rating = player_rating.internal_rating_at(game_time, rating_period_duration);
860
861    let internal_games: Vec<_> = games
862        .timed_games()
863        // Technically we should get internal game at time player_last_updated,
864        // but that would make all opponents being last updated before that a requirement,
865        // which is unreasonable. Errors because of this tend to be small.
866        .map(|game| game.to_internal_game(rating_period_duration))
867        .collect();
868
869    // elapsed_periods is 0.0 since we set last_updated to game_time (no time elapsed since game)
870    let new_rating = rate_games_untimed(player_rating, &internal_games, 0.0, parameters);
871
872    TimedInternalRating::new(
873        game_time,
874        InternalRating::new(
875            new_rating.rating(),
876            new_rating.deviation(),
877            new_rating.volatility(),
878        ),
879    )
880}
881
882/// Calculates the new internal player rating after the given [`InternalGame`]s were played
883/// and the given amount of rating periods `elapsed_periods` were elapsed using the Glicko-2 algorithm.
884///
885/// # Panics
886///
887/// This function panics if `elapsed_periods` is less than `0`.
888///
889/// It can also panic if `parameters.convergence_tolerance()` is unreasonably low.
890#[must_use]
891pub fn rate_games_untimed(
892    player_rating: InternalRating,
893    results: &[InternalGame],
894    elapsed_periods: f64,
895    parameters: Parameters,
896) -> InternalRating {
897    assert!(elapsed_periods >= 0.0);
898
899    // Step 1. (initialising) doesn't apply, we have already set the starting ratings.
900    // Step 2. (converting to internal scale) doesn't apply either, we get typed checked internal rating here
901
902    if results.iter().next().is_none() {
903        // If `results` is empty, only Step 6. applies
904        let new_deviation = calculate_pre_rating_period_value(
905            player_rating.volatility(),
906            player_rating,
907            elapsed_periods,
908        );
909
910        return InternalRating::new(
911            player_rating.rating(),
912            new_deviation,
913            player_rating.volatility(),
914        )
915        .into_with_parameters(parameters);
916    }
917
918    // Step 3.
919    let estimated_variance = calculate_estimated_variance(player_rating, results.iter().copied());
920
921    // Step 4.
922    let performance_sum = calculate_performance_sum(player_rating, results.iter().copied());
923    let estimated_improvement =
924        calculate_estimated_improvement(estimated_variance, performance_sum);
925
926    // Step 5.
927    let new_volatility = calculate_new_volatility(
928        estimated_improvement,
929        estimated_variance,
930        player_rating,
931        parameters.volatility_change(),
932        parameters.convergence_tolerance(),
933    );
934
935    // Step 6.
936    let pre_rating_period_value =
937        calculate_pre_rating_period_value(new_volatility, player_rating, elapsed_periods);
938
939    // Step 7.
940    let new_deviation = calculate_new_rating_deviation(pre_rating_period_value, estimated_variance);
941    let new_rating = calculate_new_rating(new_deviation, player_rating, performance_sum);
942
943    // Step 8. (converting back to public) doesn't apply
944    InternalRating::new(new_rating, new_deviation, new_volatility)
945}
946
947/// Step 3.
948///
949/// This function's return value and panic behaviuor is unspecified if the results iterator is empty.
950/// It will terminate.
951///
952/// # Panics
953///
954/// This function might panic if the results iterator is empty.
955#[must_use]
956fn calculate_estimated_variance(
957    player_rating: InternalRating,
958    games: impl IntoIterator<Item = InternalGame>,
959) -> f64 {
960    1.0 / games
961        .into_iter()
962        .map(|game| {
963            let opponent_rating = game.opponent();
964
965            let g = calculate_g(opponent_rating.deviation());
966            let e = calculate_e(g, player_rating.rating(), opponent_rating.rating());
967
968            g * g * e * (1.0 - e)
969        })
970        .sum::<f64>()
971}
972
973/// Calculates sum value for Steps 4. and 7.2.
974fn calculate_performance_sum(
975    player_rating: InternalRating,
976    games: impl IntoIterator<Item = InternalGame>,
977) -> f64 {
978    games
979        .into_iter()
980        .map(|game| {
981            let opponent_rating = game.opponent();
982
983            let g = calculate_g(opponent_rating.deviation());
984            let e = calculate_e(g, player_rating.rating(), opponent_rating.rating());
985
986            g * (game.score() - e)
987        })
988        .sum::<f64>()
989}
990
991/// Step 4.
992#[must_use]
993fn calculate_estimated_improvement(estimated_variance: f64, performance_sum: f64) -> f64 {
994    estimated_variance * performance_sum
995}
996
997// TODO: cached?
998// Optimizer is prolly smart enough to notice we call it with the same value twice
999// Even if not, like, ... this is likely not a bottleneck
1000#[must_use]
1001fn calculate_g(deviation: f64) -> f64 {
1002    1.0 / f64::sqrt(1.0 + 3.0 * deviation * deviation / (PI * PI))
1003}
1004
1005#[must_use]
1006fn calculate_e(g: f64, player_rating: f64, opponent_rating: f64) -> f64 {
1007    1.0 / (1.0 + f64::exp(-g * (player_rating - opponent_rating)))
1008}
1009
1010/// Step 5.
1011///
1012/// # Panics
1013///
1014/// This function might panic if `parameters.convergence_tolerance()` is unreasonably low.
1015#[must_use]
1016fn calculate_new_volatility(
1017    estimated_improvement: f64,
1018    estimated_variance: f64,
1019    player_rating: InternalRating,
1020    volatility_change: f64,
1021    convergence_tolerance: f64,
1022) -> f64 {
1023    let deviation = player_rating.deviation();
1024    let deviation_sq = deviation * deviation;
1025    let current_volatility = player_rating.volatility();
1026
1027    let estimated_improvement_sq = estimated_improvement * estimated_improvement;
1028
1029    // 1.
1030    let a = f64::ln(current_volatility * current_volatility);
1031
1032    let f = |x| {
1033        let x_exp = f64::exp(x);
1034
1035        let tmp_1 = x_exp * (estimated_improvement_sq - deviation_sq - estimated_variance - x_exp);
1036
1037        let tmp_2 = 2.0 * {
1038            let tmp = deviation_sq + estimated_variance + x_exp;
1039            tmp * tmp
1040        };
1041
1042        let tmp_3 = x - a;
1043
1044        let tmp_4 = volatility_change * volatility_change;
1045
1046        tmp_1 / tmp_2 - tmp_3 / tmp_4
1047    };
1048
1049    // 2.
1050    // Copy so the mutated value doesn't get captured by f
1051    let mut a = a;
1052
1053    let mut b = if estimated_improvement_sq > deviation_sq + estimated_variance {
1054        f64::ln(estimated_improvement_sq - deviation_sq - estimated_variance)
1055    } else {
1056        // (i)
1057        let mut k = 1.0;
1058
1059        loop {
1060            // (ii)
1061            let estimated_b = a - k * volatility_change;
1062
1063            if f(estimated_b) < 0.0 {
1064                k += 1.0;
1065            } else {
1066                break estimated_b;
1067            }
1068        }
1069    };
1070
1071    // 3.
1072    let mut f_a = f(a);
1073    let mut f_b = f(b);
1074
1075    // 4.
1076    let mut iteration = 0;
1077    while f64::abs(b - a) > convergence_tolerance {
1078        assert!(
1079            iteration <= constants::MAX_ITERATIONS,
1080            "Maximum number of iterations ({}) in converging loop algorithm exceeded. Is the convergence tolerance ({}) unreasonably low?",
1081            constants::MAX_ITERATIONS, convergence_tolerance
1082        );
1083
1084        // (a)
1085        let c = a + (a - b) * f_a / (f_b - f_a);
1086        let f_c = f(c);
1087
1088        // (b)
1089        if f_c * f_b <= 0.0 {
1090            a = b;
1091            f_a = f_b;
1092        } else {
1093            f_a /= 2.0;
1094        }
1095
1096        // (c)
1097        b = c;
1098        f_b = f_c;
1099
1100        iteration += 1;
1101        // (d) checked by loop
1102    }
1103
1104    // 5.
1105    f64::exp(a / 2.0)
1106}
1107
1108/// Step 6.
1109#[must_use]
1110fn calculate_pre_rating_period_value(
1111    new_volatility: f64,
1112    player_rating: InternalRating,
1113    elapsed_periods: f64,
1114) -> f64 {
1115    let current_deviation = player_rating.deviation();
1116
1117    // See Lichess' implementation: https://github.com/lichess-org/lila/blob/d6a175d25228b0f3d9053a30301fce90850ceb2d/modules/rating/src/main/java/glicko2/RatingCalculator.java#L316
1118    f64::sqrt(
1119        current_deviation * current_deviation + elapsed_periods * new_volatility * new_volatility,
1120    )
1121}
1122
1123/// Step 7.1.
1124#[must_use]
1125fn calculate_new_rating_deviation(pre_rating_period_value: f64, estimated_variance: f64) -> f64 {
1126    1.0 / f64::sqrt(
1127        1.0 / (pre_rating_period_value * pre_rating_period_value) + 1.0 / estimated_variance,
1128    )
1129}
1130
1131/// Step 7.2.
1132#[must_use]
1133fn calculate_new_rating(
1134    new_deviation: f64,
1135    player_rating: InternalRating,
1136    performance_sum: f64,
1137) -> f64 {
1138    player_rating.rating() + new_deviation * new_deviation * performance_sum
1139}
1140
1141#[cfg(test)]
1142mod test {
1143    use std::time::{Duration, SystemTime};
1144
1145    use crate::algorithm::{
1146        rate_games, rate_games_untimed, PublicGame, TimedOpponentPublicGame, TimedPublicGames,
1147        TimedPublicRating,
1148    };
1149    use crate::{FromWithParameters, IntoWithParameters, Parameters, PublicRating};
1150
1151    macro_rules! assert_approx_eq {
1152        ($a:expr, $b:expr, $tolerance:expr) => {{
1153            let a_val = $a;
1154            let b_val = $b;
1155
1156            assert!(
1157                (a_val - b_val).abs() <= $tolerance,
1158                "{} = {a_val} is not approximately equal to {} = {b_val}",
1159                stringify!($a),
1160                stringify!($b)
1161            )
1162        }};
1163    }
1164
1165    #[test]
1166    fn test_start_time() {
1167        let parameters = Parameters::default().with_volatility_change(0.5);
1168
1169        let start_time = SystemTime::UNIX_EPOCH;
1170        let rating_period_duration = Duration::from_secs(1);
1171
1172        let player = TimedPublicRating::new(start_time, PublicRating::new(1500.0, 200.0, 0.06));
1173        let rating_at_start =
1174            player.public_rating_at(SystemTime::UNIX_EPOCH, parameters, rating_period_duration);
1175
1176        assert_approx_eq!(rating_at_start.rating(), 1500.0, f64::EPSILON);
1177        assert_approx_eq!(rating_at_start.deviation(), 200.0, f64::EPSILON);
1178        assert_approx_eq!(rating_at_start.volatility(), 0.06, f64::EPSILON);
1179    }
1180
1181    /// This tests the example calculation in [Glickman's paper](http://www.glicko.net/glicko/glicko2.pdf).
1182    #[test]
1183    fn test_paper_example() {
1184        let parameters = Parameters::default().with_volatility_change(0.5);
1185
1186        let start_time = SystemTime::UNIX_EPOCH;
1187        let rating_period_duration = Duration::from_secs(1);
1188        let end_time = start_time + rating_period_duration;
1189
1190        let player = TimedPublicRating::new(start_time, PublicRating::new(1500.0, 200.0, 0.06));
1191
1192        // Volatility on opponents is not specified in the paper and doesn't matter in the calculation.
1193        // Constructor asserts it to be > 0.0
1194        let opponent_a = TimedPublicRating::new(
1195            start_time,
1196            PublicRating::new(1400.0, 30.0, parameters.start_rating().volatility()),
1197        );
1198        let opponent_b = TimedPublicRating::new(
1199            start_time,
1200            PublicRating::new(1550.0, 100.0, parameters.start_rating().volatility()),
1201        );
1202        let opponent_c = TimedPublicRating::new(
1203            start_time,
1204            PublicRating::new(1700.0, 300.0, parameters.start_rating().volatility()),
1205        );
1206
1207        let games = vec![
1208            TimedOpponentPublicGame::new(opponent_a, 1.0),
1209            TimedOpponentPublicGame::new(opponent_b, 0.0),
1210            TimedOpponentPublicGame::new(opponent_c, 0.0),
1211        ];
1212
1213        let games = TimedPublicGames::new(end_time, games);
1214
1215        let new_rating = rate_games(
1216            player.into_with_parameters(parameters),
1217            &games.into_with_parameters(parameters),
1218            rating_period_duration,
1219            parameters,
1220        );
1221
1222        // All games are considered to occur at the same time in the example
1223        let new_public_rating = TimedPublicRating::from_with_parameters(new_rating, parameters)
1224            .public_rating_at(end_time, parameters, rating_period_duration);
1225
1226        // However, we make an compromise for the opponent ratings to be able to be updated before the player ratings
1227        // which makes the algorithm a bit less accurate, thus the slightly higher tolerances
1228        assert_approx_eq!(new_public_rating.rating(), 1464.06, 0.05);
1229        assert_approx_eq!(new_public_rating.deviation(), 151.52, 0.05);
1230        assert_approx_eq!(new_public_rating.volatility(), 0.05999, 0.0001);
1231    }
1232
1233    /// This tests the example calculation in [Glickman's paper](http://www.glicko.net/glicko/glicko2.pdf).
1234    #[test]
1235    fn test_paper_example_untimed() {
1236        let parameters = Parameters::default().with_volatility_change(0.5);
1237
1238        let player = PublicRating::new(1500.0, 200.0, 0.06);
1239
1240        // Volatility on opponents is not specified in the paper and doesn't matter in the calculation.
1241        // Constructor asserts it to be > 0.0
1242        let opponent_a = PublicRating::new(1400.0, 30.0, parameters.start_rating().volatility());
1243        let opponent_b = PublicRating::new(1550.0, 100.0, parameters.start_rating().volatility());
1244        let opponent_c = PublicRating::new(1700.0, 300.0, parameters.start_rating().volatility());
1245
1246        let games = vec![
1247            PublicGame::new(opponent_a, 1.0).into_with_parameters(parameters),
1248            PublicGame::new(opponent_b, 0.0).into_with_parameters(parameters),
1249            PublicGame::new(opponent_c, 0.0).into_with_parameters(parameters),
1250        ];
1251
1252        let new_rating = rate_games_untimed(
1253            player.into_with_parameters(parameters),
1254            &games,
1255            1.0,
1256            parameters,
1257        );
1258
1259        let new_public_rating = PublicRating::from_with_parameters(new_rating, parameters);
1260
1261        assert_approx_eq!(new_public_rating.rating(), 1464.06, 0.01);
1262        assert_approx_eq!(new_public_rating.deviation(), 151.52, 0.01);
1263        assert_approx_eq!(new_public_rating.volatility(), 0.05999, 0.0001);
1264    }
1265}