1
// This file is part of hnefatafl-copenhagen.
2
//
3
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU Affero General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// hnefatafl-copenhagen is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU Affero General Public License for more details.
12
//
13
// You should have received a copy of the GNU Affero General Public License
14
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15

            
16
use std::{
17
    f64::consts::{LOG10_2, PI},
18
    fmt,
19
};
20

            
21
use serde::{Deserialize, Serialize};
22

            
23
/// ln 10 / 400
24
const Q: f64 = 0.005_756_5;
25
pub const CONFIDENCE_INTERVAL_95: f64 = 1.96;
26

            
27
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28
pub struct Rating {
29
    pub rating: f64,
30
    /// Ratings Deviation
31
    pub rd: f64,
32
}
33

            
34
impl Rating {
35
    #[must_use]
36
456
    pub fn rd_sq(&self) -> f64 {
37
456
        self.rd * self.rd
38
456
    }
39

            
40
    #[must_use]
41
    pub fn to_string_rounded(&self) -> String {
42
        format!(
43
            "{} ± {}",
44
            self.rating.round(),
45
            (CONFIDENCE_INTERVAL_95 * self.rd).round()
46
        )
47
    }
48

            
49
    /// This assumes 30 2 month periods must pass before one's rating
50
    /// deviation is the same as a new player and that a typical RD is 50.
51
24
    pub fn update_rd(&mut self) {
52
24
        let c = 63.2;
53

            
54
24
        let rd_new = f64::sqrt(self.rd_sq() + (c * c));
55
24
        self.rd = rd_new.clamp(30.0, 350.0);
56
24
    }
57

            
58
36
    pub fn update_rating(&mut self, rating: f64, outcome: &Outcome) {
59
36
        self.rating += (Q / (1.0 / self.rd_sq()) + (1.0 / self.d_sq(rating)))
60
36
            * self.g()
61
36
            * (outcome.score() - self.e(rating));
62

            
63
36
        self.rd = f64::sqrt(1.0 / ((1.0 / self.rd_sq()) + (1.0 / self.d_sq(rating))));
64
36
    }
65

            
66
    #[must_use]
67
72
    fn d_sq(&self, rating: f64) -> f64 {
68
72
        1.0 / ((Q * Q) * (self.g() * self.g()) * self.e(rating) * (1.0 - self.e(rating)))
69
72
    }
70

            
71
    #[must_use]
72
180
    fn e(&self, rating: f64) -> f64 {
73
180
        1.0 / (1.0 + exp10(-self.g() * ((self.rating - rating) / 400.0)))
74
180
    }
75

            
76
    #[must_use]
77
360
    fn g(&self) -> f64 {
78
360
        1.0 / f64::sqrt(1.0 + ((3.0 * Q * Q * self.rd_sq()) / (PI * PI)))
79
360
    }
80
}
81

            
82
impl Default for Rating {
83
54
    fn default() -> Self {
84
54
        Self {
85
54
            rating: 1_500.0,
86
54
            rd: 350.0,
87
54
        }
88
54
    }
89
}
90

            
91
impl fmt::Display for Rating {
92
    // Note: We use a FIGURE SPACE before and after the ± so
93
    // .split_ascii_whitespace() does not treat it as a space.
94
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95
        write!(f, "{} ± {}", self.rating, CONFIDENCE_INTERVAL_95 * self.rd)
96
    }
97
}
98

            
99
#[derive(Clone, Debug)]
100
pub enum Outcome {
101
    Draw,
102
    Loss,
103
    Win,
104
}
105

            
106
impl Outcome {
107
    #[must_use]
108
36
    pub fn score(&self) -> f64 {
109
36
        match self {
110
12
            Outcome::Draw => 0.5,
111
12
            Outcome::Loss => 0.0,
112
12
            Outcome::Win => 1.0,
113
        }
114
36
    }
115
}
116

            
117
#[must_use]
118
186
pub fn exp10(mut exponent: f64) -> f64 {
119
186
    exponent /= LOG10_2;
120
186
    exponent.exp2()
121
186
}
122

            
123
#[cfg(test)]
124
mod tests {
125
    use crate::glicko::Outcome;
126

            
127
    use super::{Rating, exp10};
128

            
129
    #[allow(clippy::float_cmp)]
130
    #[test]
131
6
    fn pow_10() {
132
6
        assert_eq!(exp10(2.0).round(), 100.0);
133
6
    }
134

            
135
    #[allow(clippy::float_cmp)]
136
    #[test]
137
6
    fn rd_increases() {
138
6
        let mut rating = Rating::default();
139
6
        assert_eq!(rating.rd, 350.0);
140

            
141
6
        rating.update_rd();
142
6
        assert_eq!(rating.rd, 350.0);
143

            
144
6
        rating.rd = 30.0;
145
6
        rating.update_rd();
146
6
        assert_eq!(rating.rd.round(), 70.0);
147

            
148
6
        rating.rd = 300.0;
149
6
        rating.update_rd();
150
6
        assert_eq!(rating.rd.round(), 307.0);
151
6
    }
152

            
153
    #[allow(clippy::float_cmp)]
154
    #[test]
155
6
    fn rating_and_rd_changes() {
156
6
        let rating = Rating::default();
157
6
        assert_eq!(rating.rating, 1_500.0);
158

            
159
6
        let mut rating_1 = rating.clone();
160
6
        rating_1.update_rating(1_600.0, &Outcome::Win);
161
6
        assert_eq!(rating_1.rating.round(), 1_781.0);
162

            
163
6
        let mut rating_2 = rating.clone();
164
6
        rating_2.update_rating(1_500.0, &Outcome::Win);
165
6
        assert_eq!(rating_2.rating.round(), 1_736.0);
166

            
167
6
        let mut rating_3 = rating.clone();
168
6
        rating_3.update_rating(1_500.0, &Outcome::Loss);
169
6
        assert_eq!(rating_3.rating.round(), 1_264.0);
170

            
171
6
        let mut rating_4 = rating.clone();
172
6
        rating_4.update_rating(1_500.0, &Outcome::Loss);
173
6
        assert_eq!(rating_4.rating.round(), 1_264.0);
174
6
        assert_eq!(rating_4.rd.round(), 299.0);
175
6
        rating_4.update_rd();
176
6
        assert_eq!(rating_4.rd.round(), 305.0);
177

            
178
6
        let mut rating_5 = rating.clone();
179
6
        rating_5.update_rating(1_500.0, &Outcome::Draw);
180
6
        assert_eq!(rating_5.rating.round(), 1_500.0);
181

            
182
6
        let mut rating_6 = rating.clone();
183
6
        rating_6.update_rating(1_600.0, &Outcome::Draw);
184
6
        assert_eq!(rating_6.rating.round(), 1545.0);
185
6
    }
186
}