#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{elo::EloRating, Outcomes};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct USCFRating {
pub rating: f64,
pub games: usize,
}
impl USCFRating {
#[must_use]
pub fn new(age: usize) -> Self {
Self {
rating: if age < 2 {
100.0
} else if age >= 26 {
1300.0
} else {
age as f64 * 50.0
},
games: 0,
}
}
}
impl Default for USCFRating {
fn default() -> Self {
Self::new(26)
}
}
impl From<(f64, usize)> for USCFRating {
fn from((r, g): (f64, usize)) -> Self {
Self {
rating: r,
games: g,
}
}
}
impl From<EloRating> for USCFRating {
fn from(e: EloRating) -> Self {
if e.rating > 2000.0 {
Self {
rating: 0.94f64.mul_add(e.rating, 180.0),
games: 10,
}
} else {
Self {
rating: 1.02f64.mul_add(e.rating, 20.0),
games: 5,
}
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct USCFConfig {
pub t: f64,
}
impl USCFConfig {
#[must_use]
pub const fn new() -> Self {
Self { t: 14.0 }
}
}
impl Default for USCFConfig {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn uscf(
player_one: &USCFRating,
player_two: &USCFRating,
outcome: &Outcomes,
config: &USCFConfig,
) -> (USCFRating, USCFRating) {
let outcome1 = outcome.to_chess_points();
let outcome2 = 1.0 - outcome1;
let e1 = e_value(player_one.rating, player_two.rating);
let e2 = e_value(player_two.rating, player_one.rating);
let new_rating1 = if player_one.games <= 8 {
new_rating_provisional(
player_one.rating,
player_one.games,
1,
player_two.rating,
i32::from(outcome == &Outcomes::WIN),
i32::from(outcome == &Outcomes::LOSS),
)
} else {
new_rating(
player_one.rating,
player_one.games,
1,
outcome1,
e1,
*config,
)
}
.max(100.0);
let new_rating2 = if player_two.games <= 8 {
new_rating_provisional(
player_two.rating,
player_two.games,
1,
player_one.rating,
i32::from(outcome == &Outcomes::LOSS),
i32::from(outcome == &Outcomes::WIN),
)
} else {
new_rating(
player_two.rating,
player_two.games,
1,
outcome2,
e2,
*config,
)
}
.max(100.0);
(
USCFRating {
rating: new_rating1,
games: player_one.games + 1,
},
USCFRating {
rating: new_rating2,
games: player_two.games + 1,
},
)
}
#[must_use]
pub fn uscf_rating_period(
player: &USCFRating,
results: &[(USCFRating, Outcomes)],
config: &USCFConfig,
) -> USCFRating {
if results.is_empty() {
return *player;
}
if player.games <= 8 {
let avg_opponent_rating =
results.iter().map(|r| r.0.rating).sum::<f64>() / results.len() as f64;
let wins = results
.iter()
.map(|r| i32::from(r.1 == Outcomes::WIN))
.sum();
let losses = results
.iter()
.map(|r| i32::from(r.1 == Outcomes::LOSS))
.sum();
let new_rating = new_rating_provisional(
player.rating,
player.games,
results.len(),
avg_opponent_rating,
wins,
losses,
);
return USCFRating {
rating: new_rating,
games: player.games + results.len(),
};
}
let score = results.iter().map(|r| r.1.to_chess_points()).sum();
let exp_sum = results
.iter()
.map(|r| e_value(player.rating, r.0.rating))
.sum();
let new_rating = new_rating(
player.rating,
player.games,
results.len(),
score,
exp_sum,
*config,
);
USCFRating {
rating: new_rating,
games: player.games + results.len(),
}
}
#[must_use]
pub fn expected_score(player_one: &USCFRating, player_two: &USCFRating) -> (f64, f64) {
let exp_one = e_value(player_one.rating, player_two.rating);
let exp_two = 1.0 - exp_one;
(exp_one, exp_two)
}
fn new_rating_provisional(
rating: f64,
past_games: usize,
played_games: usize,
opponent_rating: f64,
wins: i32,
losses: i32,
) -> f64 {
f64::from(wins - losses).mul_add(
400.0,
(past_games as f64).mul_add(rating, played_games as f64 * opponent_rating),
) / (past_games + played_games) as f64
}
fn new_rating(
rating: f64,
past_games: usize,
played_games: usize,
score: f64,
e: f64,
config: USCFConfig,
) -> f64 {
let ne = effective_game_number(rating, past_games);
let k = 800.0 / (ne + played_games as f64);
let boost = get_boost_value(played_games, k, e, score, config.t);
k.mul_add(score - e, rating) + boost
}
fn e_value(rating: f64, opponent_rating: f64) -> f64 {
(10_f64.powf(-(rating - opponent_rating) / 400.0) + 1.0).recip()
}
fn effective_game_number(rating: f64, past_games: usize) -> f64 {
if rating < 2355.0 {
50.0 / 0.000_007_39f64
.mul_add((2569.0 - rating).powi(2), 0.662)
.sqrt()
} else {
50.0
}
.min((past_games as f64).min(50.0))
}
fn get_boost_value(played_games: usize, k: f64, e: f64, score: f64, t: f64) -> f64 {
if played_games < 3 {
return 0.0;
}
let kse = k * (score - e);
let tm = t * (played_games.min(4) as f64).sqrt();
if tm >= kse {
0.0
} else {
kse - tm
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effective_game_number() {
let player = USCFRating {
rating: 1700.0,
games: 30,
};
let other_player = USCFRating {
rating: 2356.0,
games: 51,
};
let n1 = effective_game_number(player.rating, player.games);
let n2 = effective_game_number(other_player.rating, other_player.games);
assert!((n1 - 20.011_786_747_374_54).abs() < f64::EPSILON);
assert!((n2 - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_get_boost_value() {
let player = USCFRating {
rating: 1300.0,
games: 45,
};
let played_games = 4;
let ne = effective_game_number(player.rating, player.games);
let k = 800.0 / (ne + played_games as f64);
let e = [1250.0, 1400.0, 1500.0, 1550.0]
.iter()
.map(|r| e_value(player.rating, *r))
.sum();
let boost = get_boost_value(played_games, k, e, 3.5, 12.0);
assert!((boost - 70.402_444_130_859_96).abs() < f64::EPSILON);
let zero_boost = get_boost_value(10, k, e, 0.0, 12.0);
assert!((zero_boost - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_expected_score() {
let player_one = USCFRating {
rating: 1300.0,
games: 30,
};
let player_two = USCFRating {
rating: 1250.0,
games: 30,
};
let (exp_one, exp_two) = expected_score(&player_one, &player_two);
assert!((exp_one + exp_two - 1.0).abs() < f64::EPSILON);
assert!((exp_one - 0.571_463_117_408_381_4).abs() < f64::EPSILON);
}
#[test]
fn test_uscf_rating_period_provisional() {
let player = USCFRating {
rating: 1500.0,
games: 6,
};
let opponent1 = USCFRating {
rating: 1400.0,
games: 10,
};
let opponent2 = USCFRating {
rating: 1550.0,
games: 10,
};
let opponent3 = USCFRating {
rating: 1650.0,
games: 10,
};
let results = vec![
(opponent1, Outcomes::WIN),
(opponent2, Outcomes::LOSS),
(opponent3, Outcomes::DRAW),
];
let new_player = uscf_rating_period(&player, &results, &USCFConfig::default());
assert_eq!(new_player.games, 9);
assert!((new_player.rating - 1_511.111_111_111_111).abs() < f64::EPSILON);
}
#[test]
fn test_uscf_rating_period_proper() {
let player = USCFRating {
rating: 1300.0,
games: 45,
};
let opponent1 = USCFRating {
rating: 1250.0,
games: 10,
};
let opponent2 = USCFRating {
rating: 1400.0,
games: 10,
};
let opponent3 = USCFRating {
rating: 1500.0,
games: 10,
};
let opponent4 = USCFRating {
rating: 1550.0,
games: 10,
};
let results = vec![
(opponent1, Outcomes::WIN),
(opponent2, Outcomes::DRAW),
(opponent3, Outcomes::WIN),
(opponent4, Outcomes::WIN),
];
let config = USCFConfig { t: 12.0 };
let new_player = uscf_rating_period(&player, &results, &config);
assert_eq!(new_player.games, 49);
assert!((new_player.rating - 1_464.804_888_261_72).abs() < f64::EPSILON);
}
#[test]
fn test_empty_rp() {
let player = USCFRating {
rating: 3201.0,
games: 39,
};
let rp = uscf_rating_period(&player, &[], &USCFConfig::new());
assert_eq!(player, rp);
}
#[test]
fn test_one_rp() {
let player = USCFRating {
rating: 1500.0,
games: 40,
};
let opponent = USCFRating {
rating: 1700.0,
games: 40,
};
let config = USCFConfig::new();
let (np, _) = uscf(&player, &opponent, &Outcomes::WIN, &config);
let npr = uscf_rating_period(&player, &[(opponent, Outcomes::WIN)], &config);
assert_eq!(np, npr);
let player = USCFRating {
rating: 1500.0,
games: 5,
};
let opponent = USCFRating {
rating: 1700.0,
games: 5,
};
let (np, _) = uscf(&player, &opponent, &Outcomes::WIN, &config);
let npr = uscf_rating_period(&player, &[(opponent, Outcomes::WIN)], &config);
assert_eq!(np, npr);
}
#[test]
fn test_uscf() {
let player_one = USCFRating {
rating: 1300.0,
games: 32,
};
let player_two = USCFRating {
rating: 1200.0,
games: 32,
};
let config = USCFConfig::new();
let (new_one, new_two) = uscf(&player_one, &player_two, &Outcomes::WIN, &config);
assert_eq!(new_one.games, 33);
assert_eq!(new_two.games, 33);
assert!((new_one.rating - 1_319.060_726_612_988_7).abs() < f64::EPSILON);
assert!((new_two.rating - 1_179.614_576_196_810_5).abs() < f64::EPSILON);
let player_one = USCFRating {
rating: 1300.0,
games: 7,
};
let player_two = USCFRating {
rating: 1200.0,
games: 7,
};
let config = USCFConfig::new();
let (new_one, new_two) = uscf(&player_one, &player_two, &Outcomes::WIN, &config);
assert!((new_one.rating - 1337.5).abs() < f64::EPSILON);
assert!((new_two.rating - 1162.5).abs() < f64::EPSILON);
let (new_one, new_two) = uscf(&player_one, &player_two, &Outcomes::LOSS, &config);
assert!((new_one.rating - 1237.5).abs() < f64::EPSILON);
assert!((new_two.rating - 1262.5).abs() < f64::EPSILON);
}
#[test]
fn test_rating_conversion() {
let new = USCFRating::new(26);
let default = USCFRating::default();
assert_eq!(new, default);
let new2 = USCFRating::new(15);
assert_ne!(new2, default);
let new3 = USCFRating::new(1);
let elo = EloRating { rating: 1779.0 };
let uscf = USCFRating::from(elo);
assert!((uscf.rating - 1834.58).abs() < f64::EPSILON);
assert_eq!(uscf.games, 5);
assert_eq!(EloRating::from(uscf), elo);
let elo2 = EloRating { rating: 2235.0 };
let uscf2 = USCFRating::from(elo2);
assert!((uscf2.rating - 2280.9).abs() < f64::EPSILON);
assert_eq!(uscf2.games, 10);
assert_eq!(EloRating::from(uscf2), elo2);
assert!((new3.rating - 100.0).abs() < f64::EPSILON);
assert_eq!(new3.games, 0);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_misc_stuff() {
let player_one = USCFRating::new(55);
let config = USCFConfig::new();
assert_eq!(player_one, player_one.clone());
assert!((config.t - config.clone().t).abs() < f64::EPSILON);
assert!(!format!("{:?}", player_one).is_empty());
assert!(!format!("{:?}", config).is_empty());
assert_eq!(player_one, USCFRating::from((1300.0, 0)));
}
}