use std::f64::consts::{FRAC_1_SQRT_2, PI, SQRT_2};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{weng_lin::WengLinRating, Outcomes};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TrueSkillRating {
pub rating: f64,
pub uncertainty: f64,
}
impl TrueSkillRating {
#[must_use]
pub fn new() -> Self {
Self {
rating: 25.0,
uncertainty: 25.0 / 3.0,
}
}
}
impl Default for TrueSkillRating {
fn default() -> Self {
Self::new()
}
}
impl From<(f64, f64)> for TrueSkillRating {
fn from((r, u): (f64, f64)) -> Self {
Self {
rating: r,
uncertainty: u,
}
}
}
impl From<WengLinRating> for TrueSkillRating {
fn from(w: WengLinRating) -> Self {
Self {
rating: w.rating,
uncertainty: w.uncertainty,
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TrueSkillConfig {
pub draw_probability: f64,
pub beta: f64,
pub default_dynamics: f64,
}
impl TrueSkillConfig {
#[must_use]
pub fn new() -> Self {
Self {
draw_probability: 0.1,
beta: (25.0 / 3.0) * 0.5,
default_dynamics: 25.0 / 300.0,
}
}
}
impl Default for TrueSkillConfig {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn trueskill(
player_one: &TrueSkillRating,
player_two: &TrueSkillRating,
outcome: &Outcomes,
config: &TrueSkillConfig,
) -> (TrueSkillRating, TrueSkillRating) {
let draw_margin = draw_margin(config.draw_probability, config.beta, 2.0);
let c = 2.0f64
.mul_add(
config.beta.powi(2),
player_one
.uncertainty
.mul_add(player_one.uncertainty, player_two.uncertainty.powi(2)),
)
.sqrt();
let rating_delta = match outcome {
Outcomes::WIN | Outcomes::DRAW => player_one.rating - player_two.rating,
Outcomes::LOSS => player_two.rating - player_one.rating,
};
let (v, w) = if outcome == &Outcomes::DRAW {
(
v_draw(rating_delta, draw_margin, c),
w_draw(rating_delta, draw_margin, c),
)
} else {
(
v_non_draw(rating_delta, draw_margin, c),
w_non_draw(rating_delta, draw_margin, c),
)
};
let (rank_multiplier1, rank_multiplier2) = match outcome {
Outcomes::WIN | Outcomes::DRAW => (1.0, -1.0),
Outcomes::LOSS => (-1.0, 1.0),
};
let new_rating1 = new_rating(
player_one.rating,
player_one.uncertainty,
v,
c,
config.default_dynamics,
rank_multiplier1,
);
let new_rating2 = new_rating(
player_two.rating,
player_two.uncertainty,
v,
c,
config.default_dynamics,
rank_multiplier2,
);
let new_uncertainty1 = new_uncertainty(player_one.uncertainty, c, w, config.default_dynamics);
let new_uncertainty2 = new_uncertainty(player_two.uncertainty, c, w, config.default_dynamics);
(
TrueSkillRating {
rating: new_rating1,
uncertainty: new_uncertainty1,
},
TrueSkillRating {
rating: new_rating2,
uncertainty: new_uncertainty2,
},
)
}
#[must_use]
pub fn trueskill_rating_period(
player: &TrueSkillRating,
results: &[(TrueSkillRating, Outcomes)],
config: &TrueSkillConfig,
) -> TrueSkillRating {
let mut player_rating = player.rating;
let mut player_uncertainty = player.uncertainty;
let draw_margin = draw_margin(config.draw_probability, config.beta, 2.0);
for (opponent, result) in results {
let c = 2.0f64
.mul_add(
config.beta.powi(2),
player_uncertainty.mul_add(player_uncertainty, opponent.uncertainty.powi(2)),
)
.sqrt();
let rating_delta = match result {
Outcomes::WIN | Outcomes::DRAW => player_rating - opponent.rating,
Outcomes::LOSS => opponent.rating - player_rating,
};
let (v, w) = if result == &Outcomes::DRAW {
(
v_draw(rating_delta, draw_margin, c),
w_draw(rating_delta, draw_margin, c),
)
} else {
(
v_non_draw(rating_delta, draw_margin, c),
w_non_draw(rating_delta, draw_margin, c),
)
};
let rank_multiplier = match result {
Outcomes::WIN | Outcomes::DRAW => 1.0,
Outcomes::LOSS => -1.0,
};
player_rating = new_rating(
player_rating,
player_uncertainty,
v,
c,
config.default_dynamics,
rank_multiplier,
);
player_uncertainty = new_uncertainty(player_uncertainty, c, w, config.default_dynamics);
}
TrueSkillRating {
rating: player_rating,
uncertainty: player_uncertainty,
}
}
#[must_use]
pub fn trueskill_two_teams(
team_one: &[TrueSkillRating],
team_two: &[TrueSkillRating],
outcome: &Outcomes,
config: &TrueSkillConfig,
) -> (Vec<TrueSkillRating>, Vec<TrueSkillRating>) {
if team_one.is_empty() || team_two.is_empty() {
return (team_one.to_vec(), team_two.to_vec());
}
let total_players = (team_one.len() + team_two.len()) as f64;
let draw_margin = draw_margin(config.draw_probability, config.beta, total_players);
let rating_one_sum: f64 = team_one.iter().map(|p| p.rating).sum();
let rating_two_sum: f64 = team_two.iter().map(|p| p.rating).sum();
let uncertainty_one_sum: f64 = team_one.iter().map(|p| p.uncertainty.powi(2)).sum();
let uncertainty_two_sum: f64 = team_two.iter().map(|p| p.uncertainty.powi(2)).sum();
let c = total_players
.mul_add(
config.beta.powi(2),
uncertainty_one_sum + uncertainty_two_sum,
)
.sqrt();
let rating_delta = match outcome {
Outcomes::WIN | Outcomes::DRAW => rating_one_sum - rating_two_sum,
Outcomes::LOSS => rating_two_sum - rating_one_sum,
};
let (v, w) = if outcome == &Outcomes::DRAW {
(
v_draw(rating_delta, draw_margin, c),
w_draw(rating_delta, draw_margin, c),
)
} else {
(
v_non_draw(rating_delta, draw_margin, c),
w_non_draw(rating_delta, draw_margin, c),
)
};
let (rank_multiplier1, rank_multiplier2) = match outcome {
Outcomes::WIN | Outcomes::DRAW => (1.0, -1.0),
Outcomes::LOSS => (-1.0, 1.0),
};
let mut new_team_one = Vec::new();
let mut new_team_two = Vec::new();
for player in team_one {
let new_rating = new_rating(
player.rating,
player.uncertainty,
v,
c,
config.default_dynamics,
rank_multiplier1,
);
let new_uncertainty = new_uncertainty(player.uncertainty, c, w, config.default_dynamics);
new_team_one.push(TrueSkillRating {
rating: new_rating,
uncertainty: new_uncertainty,
});
}
for player in team_two {
let new_rating = new_rating(
player.rating,
player.uncertainty,
v,
c,
config.default_dynamics,
rank_multiplier2,
);
let new_uncertainty = new_uncertainty(player.uncertainty, c, w, config.default_dynamics);
new_team_two.push(TrueSkillRating {
rating: new_rating,
uncertainty: new_uncertainty,
});
}
(new_team_one, new_team_two)
}
#[must_use]
pub fn match_quality(
player_one: &TrueSkillRating,
player_two: &TrueSkillRating,
config: &TrueSkillConfig,
) -> f64 {
let delta: f64 = player_one.rating - player_two.rating;
let a = ((2.0 * config.beta.powi(2))
/ player_two.uncertainty.mul_add(
player_two.uncertainty,
2.0f64.mul_add(config.beta.powi(2), player_one.uncertainty.powi(2)),
))
.sqrt();
let b = ((-delta.powi(2))
/ (2.0
* player_two.uncertainty.mul_add(
player_two.uncertainty,
2.0f64.mul_add(config.beta.powi(2), player_one.uncertainty.powi(2)),
)))
.exp();
a * b
}
#[must_use]
pub fn match_quality_two_teams(
team_one: &[TrueSkillRating],
team_two: &[TrueSkillRating],
config: &TrueSkillConfig,
) -> f64 {
let total_players = (team_one.len() + team_two.len()) as f64;
let rating_one_sum: f64 = team_one.iter().map(|p| p.rating).sum();
let rating_two_sum: f64 = team_two.iter().map(|p| p.rating).sum();
let uncertainty_one_sum: f64 = team_one.iter().map(|p| p.uncertainty.powi(2)).sum();
let uncertainty_two_sum: f64 = team_two.iter().map(|p| p.uncertainty.powi(2)).sum();
let a = ((total_players * config.beta.powi(2))
/ (total_players.mul_add(config.beta.powi(2), uncertainty_one_sum) + uncertainty_two_sum))
.sqrt();
let b = ((-(rating_one_sum - rating_two_sum).powi(2))
/ (2.0
* (total_players.mul_add(config.beta.powi(2), uncertainty_one_sum)
+ uncertainty_two_sum)))
.exp();
a * b
}
#[must_use]
pub fn expected_score(
player_one: &TrueSkillRating,
player_two: &TrueSkillRating,
config: &TrueSkillConfig,
) -> (f64, f64) {
let delta = player_one.rating - player_two.rating;
let denom = player_two
.uncertainty
.mul_add(
player_two.uncertainty,
2.0f64.mul_add(config.beta.powi(2), player_one.uncertainty.powi(2)),
)
.sqrt();
let exp_one = cdf(delta / denom, 0.0, 1.0);
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
#[must_use]
pub fn expected_score_two_teams(
team_one: &[TrueSkillRating],
team_two: &[TrueSkillRating],
config: &TrueSkillConfig,
) -> (f64, f64) {
let player_count = (team_one.len() + team_two.len()) as f64;
let rating_one_sum: f64 = team_one.iter().map(|p| p.rating).sum();
let rating_two_sum: f64 = team_two.iter().map(|p| p.rating).sum();
let uncertainty_one_sum: f64 = team_one.iter().map(|p| p.uncertainty.powi(2)).sum();
let uncertainty_two_sum: f64 = team_two.iter().map(|p| p.uncertainty.powi(2)).sum();
let delta = rating_one_sum - rating_two_sum;
let denom = (uncertainty_two_sum
+ player_count.mul_add(config.beta.powi(2), uncertainty_one_sum))
.sqrt();
let exp_one = cdf(delta / denom, 0.0, 1.0);
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
#[must_use]
pub fn get_rank(player: &TrueSkillRating) -> f64 {
player.rating - (player.uncertainty * 3.0)
}
fn draw_margin(draw_probability: f64, beta: f64, total_players: f64) -> f64 {
inverse_cdf(0.5 * (draw_probability + 1.0), 0.0, 1.0) * total_players.sqrt() * beta
}
fn v_non_draw(difference: f64, draw_margin: f64, c: f64) -> f64 {
let diff_c = difference / c;
let draw_c = draw_margin / c;
let norm = cdf(diff_c - draw_c, 0.0, 1.0);
if norm < 2.222_758_749e-162 {
-diff_c + draw_c
} else {
pdf(diff_c - draw_c, 0.0, 1.0) / norm
}
}
fn w_non_draw(difference: f64, draw_margin: f64, c: f64) -> f64 {
let diff_c = difference / c;
let draw_c = draw_margin / c;
let norm = cdf(diff_c - draw_c, 0.0, 1.0);
if norm < 2.222_758_749e-162 {
if diff_c < 0.0 {
return 1.0;
}
return 0.0;
}
let v = v_non_draw(difference, draw_margin, c);
v * (v + (diff_c) - (draw_c))
}
fn v_draw(difference: f64, draw_margin: f64, c: f64) -> f64 {
let diff_c = difference / c;
let draw_c = draw_margin / c;
let diff_c_abs = diff_c.abs();
let norm = cdf(draw_c - diff_c_abs, 0.0, 1.0) - cdf(-draw_c - diff_c_abs, 0.0, 1.0);
if norm < 2.222_758_749e-162 {
if diff_c < 0.0 {
return -diff_c - draw_c;
}
return -diff_c + draw_c;
}
let x = pdf(-draw_c - diff_c_abs, 0.0, 1.0) - pdf(draw_c - diff_c_abs, 0.0, 1.0);
if diff_c < 0.0 {
-x / norm
} else {
x / norm
}
}
fn w_draw(difference: f64, draw_margin: f64, c: f64) -> f64 {
let diff_c = difference / c;
let draw_c = draw_margin / c;
let diff_c_abs = diff_c.abs();
let norm = cdf(draw_c - diff_c_abs, 0.0, 1.0) - cdf(-draw_c - diff_c_abs, 0.0, 1.0);
if norm < 2.222_758_749e-162 {
return 1.0;
}
let v = v_draw(difference, draw_margin, c);
let p1 = pdf(draw_c - diff_c_abs, 0.0, 1.0);
let p2 = pdf(-draw_c - diff_c_abs, 0.0, 1.0);
v.mul_add(
v,
((draw_c - diff_c_abs) * p1 - (-draw_c - diff_c_abs) * p2) / norm,
)
}
fn new_rating(
rating: f64,
uncertainty: f64,
v: f64,
c: f64,
default_dynamics: f64,
rank_multiplier: f64,
) -> f64 {
let mean_multiplier = uncertainty.mul_add(uncertainty, default_dynamics.powi(2)) / c;
(rank_multiplier * mean_multiplier).mul_add(v, rating)
}
fn new_uncertainty(uncertainty: f64, c: f64, w: f64, default_dynamics: f64) -> f64 {
let variance = uncertainty.mul_add(uncertainty, default_dynamics.powi(2));
let dev_multiplier = variance / c.powi(2);
(variance * (1.0 - w * dev_multiplier)).sqrt()
}
fn erfc(x: f64) -> f64 {
let z = x.abs();
let t = (1.0 + z / 2.0).recip();
let r = t * t
.mul_add(
t.mul_add(
t.mul_add(
t.mul_add(
t.mul_add(
t.mul_add(
t.mul_add(
t.mul_add(t.mul_add(0.170_872_77, -0.822_152_23), 1.488_515_87),
-1.135_203_98,
),
0.278_868_07,
),
-0.186_288_06,
),
0.096_784_18,
),
0.374_091_96,
),
1.000_023_68,
),
-z * z - 1.265_512_23,
)
.exp();
if x < 0.0 {
2.0 - r
} else {
r
}
}
#[allow(clippy::excessive_precision)]
fn inverse_erfc(y: f64) -> f64 {
if y >= 2.0 {
return -100.0;
} else if y <= 0.0 {
return 100.0;
}
let zero_point = y < 1.0;
let y = if zero_point { y } else { 2.0 - y };
let t = (-2.0 * (y / 2.0).ln()).sqrt();
let mut x = -FRAC_1_SQRT_2
* (t.mul_add(0.27061, 2.30753) / t.mul_add(t.mul_add(0.04481, 0.99229), 1.0) - t);
for _ in 0..2 {
let err = erfc(x) - y;
x += err / (1.128_379_167_095_512_57 * (-(x.powi(2))).exp() - x * err);
}
if zero_point {
x
} else {
-x
}
}
fn cdf(x: f64, mu: f64, sigma: f64) -> f64 {
0.5 * erfc(-(x - mu) / (sigma * SQRT_2))
}
fn inverse_cdf(x: f64, mu: f64, sigma: f64) -> f64 {
mu - sigma * SQRT_2 * inverse_erfc(2.0 * x)
}
fn pdf(x: f64, mu: f64, sigma: f64) -> f64 {
((2.0 * PI).sqrt() * sigma.abs()).recip() * (-(((x - mu) / sigma.abs()).powi(2) / 2.0)).exp()
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::{INFINITY, NEG_INFINITY};
#[test]
fn test_trueskill() {
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating {
rating: 30.0,
uncertainty: 1.2,
};
let (p1, p2) = trueskill(
&player_one,
&player_two,
&Outcomes::WIN,
&TrueSkillConfig::new(),
);
assert!(((p1.rating * 100.0).round() - 3300.0).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 597.0).abs() < f64::EPSILON);
assert!(((p2.rating * 100.0).round() - 2983.0).abs() < f64::EPSILON);
assert!(((p2.uncertainty * 100.0).round() - 120.0).abs() < f64::EPSILON);
let (p1, p2) = trueskill(
&player_two,
&player_one,
&Outcomes::LOSS,
&TrueSkillConfig::new(),
);
assert!(((p2.rating * 100.0).round() - 3300.0).abs() < f64::EPSILON);
assert!(((p2.uncertainty * 100.0).round() - 597.0).abs() < f64::EPSILON);
assert!(((p1.rating * 100.0).round() - 2983.0).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 120.0).abs() < f64::EPSILON);
let player_two = TrueSkillRating::new();
let (p1, p2) = trueskill(
&player_one,
&player_two,
&Outcomes::WIN,
&TrueSkillConfig::new(),
);
assert!((p1.rating.round() - 29.0).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 717.0).abs() < f64::EPSILON);
assert!((p2.rating.round() - 21.0).abs() < f64::EPSILON);
assert!(((p2.uncertainty * 100.0).round() - 717.0).abs() < f64::EPSILON);
}
#[test]
fn test_trueskill_rating_period() {
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating {
rating: 30.0,
uncertainty: 1.2,
};
let player_three = TrueSkillRating {
rating: 12.0,
uncertainty: 1.9,
};
let player_four = TrueSkillRating {
rating: 49.0,
uncertainty: 1.2,
};
let player = trueskill_rating_period(
&player_one,
&[(player_two, Outcomes::WIN)],
&TrueSkillConfig::new(),
);
assert!(((player.rating * 100.0).round() - 3300.0).abs() < f64::EPSILON);
assert!(((player.uncertainty * 100.0).round() - 597.0).abs() < f64::EPSILON);
let player = trueskill_rating_period(
&player_one,
&[
(player_two, Outcomes::WIN),
(player_three, Outcomes::DRAW),
(player_four, Outcomes::LOSS),
],
&TrueSkillConfig::new(),
);
assert!(((player.rating * 100.0).round() - 2291.0).abs() < f64::EPSILON);
assert!(((player.uncertainty * 100.0).round() - 430.0).abs() < f64::EPSILON);
}
#[test]
fn test_draw() {
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating {
rating: 30.0,
uncertainty: 1.2,
};
let (p1, p2) = trueskill(
&player_one,
&player_two,
&Outcomes::DRAW,
&TrueSkillConfig::new(),
);
assert!((p1.rating - 28.282_523_394_245_658).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 488.0).abs() < f64::EPSILON);
assert!((p2.rating - 29.931_612_181_339_364).abs() < f64::EPSILON);
assert!(((p2.uncertainty * 100.0).round() - 119.0).abs() < f64::EPSILON);
let (p2, p1) = trueskill(
&player_two,
&player_one,
&Outcomes::DRAW,
&TrueSkillConfig::new(),
);
assert!((p1.rating - 28.282_523_394_245_658).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 488.0).abs() < f64::EPSILON);
assert!((p2.rating - 29.931_612_181_339_364).abs() < f64::EPSILON);
assert!(((p2.uncertainty * 100.0).round() - 119.0).abs() < f64::EPSILON);
let p1 = trueskill_rating_period(
&player_one,
&[(player_two, Outcomes::DRAW)],
&TrueSkillConfig::new(),
);
assert!((p1.rating - 28.282_523_394_245_658).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 488.0).abs() < f64::EPSILON);
}
#[test]
fn test_unlikely_values() {
let player_one = TrueSkillRating {
rating: -9.0,
uncertainty: -5.0,
};
let player_two = TrueSkillRating {
rating: 7000.0,
uncertainty: 6000.0,
};
let (p1, p2) = trueskill(
&player_one,
&player_two,
&Outcomes::WIN,
&TrueSkillConfig::new(),
);
assert!((p1.rating.round() - -9.0).abs() < f64::EPSILON);
assert!((p1.uncertainty.round() - 5.0).abs() < f64::EPSILON);
assert!((p2.rating.round() - -2969.0).abs() < f64::EPSILON);
assert!((p2.uncertainty.round() - 2549.0).abs() < f64::EPSILON);
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn test_teams() {
let player_one = TrueSkillRating {
rating: 20.0,
uncertainty: 8.0,
};
let player_two = TrueSkillRating {
rating: 25.0,
uncertainty: 6.0,
};
let player_three = TrueSkillRating {
rating: 35.0,
uncertainty: 7.0,
};
let player_four = TrueSkillRating {
rating: 40.0,
uncertainty: 5.0,
};
let (team_one, team_two) = trueskill_two_teams(
&[player_one, player_two],
&[player_three, player_four],
&Outcomes::WIN,
&TrueSkillConfig::new(),
);
assert!((team_one[0].rating - 29.698_800_676_796_665).abs() < f64::EPSILON);
assert!((team_one[1].rating - 30.456_035_750_156_31).abs() < f64::EPSILON);
assert!((team_two[0].rating - 27.574_109_105_332_1).abs() < f64::EPSILON);
assert!((team_two[1].rating - 36.210_764_756_738_115).abs() < f64::EPSILON);
assert!((team_one[0].uncertainty - 7.007_955_406_085_773).abs() < f64::EPSILON);
assert!((team_one[1].uncertainty - 5.594_025_202_259_947).abs() < f64::EPSILON);
assert!((team_two[0].uncertainty - 6.346_250_279_230_62).abs() < f64::EPSILON);
assert!((team_two[1].uncertainty - 4.767_945_180_134_836).abs() < f64::EPSILON);
let (team_two, team_one) = trueskill_two_teams(
&[player_three, player_four],
&[player_one, player_two],
&Outcomes::LOSS,
&TrueSkillConfig::new(),
);
assert!((team_one[0].rating - 29.698_800_676_796_665).abs() < f64::EPSILON);
assert!((team_one[1].rating - 30.456_035_750_156_31).abs() < f64::EPSILON);
assert!((team_two[0].rating - 27.574_109_105_332_1).abs() < f64::EPSILON);
assert!((team_two[1].rating - 36.210_764_756_738_115).abs() < f64::EPSILON);
assert!((team_one[0].uncertainty - 7.007_955_406_085_773).abs() < f64::EPSILON);
assert!((team_one[1].uncertainty - 5.594_025_202_259_947).abs() < f64::EPSILON);
assert!((team_two[0].uncertainty - 6.346_250_279_230_62).abs() < f64::EPSILON);
assert!((team_two[1].uncertainty - 4.767_945_180_134_836).abs() < f64::EPSILON);
let player_one = TrueSkillRating {
rating: 15.0,
uncertainty: 8.0,
};
let player_two = TrueSkillRating {
rating: 20.0,
uncertainty: 6.0,
};
let player_three = TrueSkillRating {
rating: 25.0,
uncertainty: 4.0,
};
let player_four = TrueSkillRating {
rating: 30.0,
uncertainty: 3.0,
};
let (team_one, team_two) = trueskill_two_teams(
&[player_one, player_two],
&[player_three, player_four],
&Outcomes::DRAW,
&TrueSkillConfig::new(),
);
assert!((team_one[0].rating - 21.571_213_060_731_655).abs() < f64::EPSILON);
assert!((team_one[1].rating - 23.696_619_260_051_385).abs() < f64::EPSILON);
assert!((team_two[0].rating - 23.356_662_026_148_804).abs() < f64::EPSILON);
assert!((team_two[1].rating - 29.075_310_476_318_872).abs() < f64::EPSILON);
assert!((team_one[0].uncertainty - 6.555_663_733_192_404).abs() < f64::EPSILON);
assert!((team_one[1].uncertainty - 5.417_723_612_401_869).abs() < f64::EPSILON);
assert!((team_two[0].uncertainty - 3.832_975_356_683_128).abs() < f64::EPSILON);
assert!((team_two[1].uncertainty - 2.930_957_525_591_959_5).abs() < f64::EPSILON);
let (team_one, _) =
trueskill_two_teams(&[player_one], &[], &Outcomes::WIN, &TrueSkillConfig::new());
assert_eq!(team_one[0], player_one);
}
#[test]
fn test_solo_team() {
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating {
rating: 12.0,
uncertainty: 3.2,
};
let (p1, p2) = trueskill(
&player_one,
&player_two,
&Outcomes::WIN,
&TrueSkillConfig::new(),
);
let (tp1, tp2) = trueskill_two_teams(
&[player_one],
&[player_two],
&Outcomes::WIN,
&TrueSkillConfig::new(),
);
assert_eq!(p1, tp1[0]);
assert_eq!(p2, tp2[0]);
}
#[test]
fn test_match_quality_two_teams() {
let player_one = TrueSkillRating {
rating: 20.0,
uncertainty: 8.0,
};
let player_two = TrueSkillRating {
rating: 25.0,
uncertainty: 6.0,
};
let player_three = TrueSkillRating {
rating: 35.0,
uncertainty: 7.0,
};
let player_four = TrueSkillRating {
rating: 40.0,
uncertainty: 5.0,
};
let quality = match_quality_two_teams(
&[player_one, player_two],
&[player_three, player_four],
&TrueSkillConfig::new(),
);
let quality2 = match_quality_two_teams(
&[player_three, player_four],
&[player_one, player_two],
&TrueSkillConfig::new(),
);
assert!((quality - 0.084_108_145_418_343_24).abs() < f64::EPSILON);
assert!((quality - quality2).abs() < f64::EPSILON);
}
#[test]
fn test_expected_score_two_teams() {
let player_one = TrueSkillRating {
rating: 38.0,
uncertainty: 3.0,
};
let player_two = TrueSkillRating {
rating: 38.0,
uncertainty: 3.0,
};
let player_three = TrueSkillRating {
rating: 44.0,
uncertainty: 3.0,
};
let player_four = TrueSkillRating {
rating: 44.0,
uncertainty: 3.0,
};
let (exp1, exp2) = expected_score_two_teams(
&[player_one, player_two],
&[player_three, player_four],
&TrueSkillConfig::new(),
);
assert!((exp1 + exp2 - 1.0).abs() < f64::EPSILON);
assert!((exp1 - 0.121_280_517_547_482_7).abs() < f64::EPSILON);
}
#[test]
fn test_quality() {
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating::new();
let quality = match_quality(&player_one, &player_two, &TrueSkillConfig::new());
assert!(((quality * 1000.0).round() - 447.0).abs() < f64::EPSILON);
let player_one = TrueSkillRating {
rating: 48.0,
uncertainty: 1.2,
};
let player_two = TrueSkillRating {
rating: 12.0,
..Default::default()
};
let quality = match_quality(&player_one, &player_two, &TrueSkillConfig::new());
assert!(((quality * 10000.0).round() - 12.0).abs() < f64::EPSILON);
let quality2 = match_quality(&player_two, &player_one, &TrueSkillConfig::new());
assert!((quality - quality2).abs() < f64::EPSILON);
}
#[test]
fn test_expected_score() {
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating::new();
let (exp1, exp2) = expected_score(&player_one, &player_two, &TrueSkillConfig::new());
assert!((exp1 * 100.0 - 50.0).round().abs() < f64::EPSILON);
assert!((exp2 * 100.0 - 50.0).round().abs() < f64::EPSILON);
let better_player = TrueSkillRating {
rating: 44.0,
uncertainty: 3.0,
};
let worse_player = TrueSkillRating {
rating: 38.0,
uncertainty: 3.0,
};
let (exp1, exp2) =
expected_score(&better_player, &worse_player, &TrueSkillConfig::default());
assert!((exp1 * 100.0 - 80.0).round().abs() < f64::EPSILON);
assert!((exp2 * 100.0 - 20.0).round().abs() < f64::EPSILON);
assert!((exp1.mul_add(100.0, exp2 * 100.0).round() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_get_rank() {
let new_player = TrueSkillRating::new();
let older_player = TrueSkillRating {
rating: 43.1,
uncertainty: 1.92,
};
let new_rank = get_rank(&new_player);
let older_rank = get_rank(&older_player);
assert!((new_rank.round() - 0.0).abs() < f64::EPSILON);
assert!((older_rank.round() - 37.0).abs() < f64::EPSILON);
}
#[test]
fn test_erfc() {
let err = erfc(0.5);
assert!((err - 0.479_500).abs() < 0.000_001);
}
#[test]
fn test_inverse_erfc() {
let err = inverse_erfc(0.5);
assert!((err - 0.476_936).abs() < 0.000_001);
let err = inverse_erfc(3.0);
assert!((err + 100.0).abs() < f64::EPSILON);
let err = inverse_erfc(-1.0);
assert!((err - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_cdf() {
let dist = cdf(5.5, 12.0, 5.0);
assert!((dist - 0.096_800).abs() < 0.000_001);
let dist_inf = cdf(INFINITY, 0.0, 1.0);
assert!((dist_inf - 1.0).abs() < f64::EPSILON);
let dist_neg_inf = cdf(NEG_INFINITY, 0.0, 1.0);
assert!((dist_neg_inf - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_inverse_cdf() {
let dist = inverse_cdf(0.055, 12.0, 5.0);
assert!((dist - 4.009_034).abs() < 0.000_001);
}
#[test]
fn test_pdf() {
let p = pdf(2.5, 0.0, 1.0);
assert!((p - 0.017_528).abs() < 0.000_001);
}
#[test]
fn test_wv_edge_cases() {
let w = w_non_draw(NEG_INFINITY, 0.0, 1.0);
assert!((w - 1.0).abs() < f64::EPSILON);
let v = v_non_draw(NEG_INFINITY, 0.0, 1.0);
assert!(v == INFINITY);
let w2 = w_draw(NEG_INFINITY, 0.0, 1.0);
assert!((w2 - 1.0).abs() < f64::EPSILON);
let v2 = v_draw(NEG_INFINITY, 0.0, 1.0);
assert!(v2 == INFINITY);
let w3 = w_non_draw(1.0, INFINITY, 1.0);
assert!((w3 - 0.0).abs() < f64::EPSILON);
let v3 = v_draw(INFINITY, f64::MAX, 1.0);
assert!(v3 == NEG_INFINITY);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let player_one = TrueSkillRating::new();
let config = TrueSkillConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.beta - config.clone().beta).abs() < f64::EPSILON);
assert!(!format!("{:?}", player_one).is_empty());
assert!(!format!("{:?}", config).is_empty());
assert_eq!(player_one, TrueSkillRating::from((25.0, 25.0 / 3.0)));
}
}