From b26744f30fe8e3582d1aa1f9d146ba4244ff399b Mon Sep 17 00:00:00 2001 From: atomflunder <80397293+atomflunder@users.noreply.github.com> Date: Sun, 23 Oct 2022 00:29:37 +0200 Subject: [PATCH] Major restructuring --- .rustfmt.toml | 1 + CHANGELOG.md | 6 + Cargo.toml | 2 +- README.md | 28 +- benches/benchmarks/elo_bench.rs | 6 +- benches/benchmarks/glicko2_bench.rs | 6 +- benches/benchmarks/trueskill_bench.rs | 5 +- benches/benchmarks/weng_lin_bench.rs | 5 +- src/config.rs | 335 ---------------- src/dwz.rs | 112 ++++-- src/egf.rs | 87 ++++- src/elo.rs | 108 ++++- src/glicko.rs | 119 +++++- src/glicko2.rs | 190 ++++++--- src/glicko_boost.rs | 189 +++++++-- src/ingo.rs | 94 +++-- src/lib.rs | 58 ++- src/outcomes.rs | 31 -- src/rating.rs | 543 -------------------------- src/sticko.rs | 159 +++++++- src/trueskill.rs | 129 ++++-- src/uscf.rs | 119 +++++- src/weng_lin.rs | 136 +++++-- 23 files changed, 1243 insertions(+), 1225 deletions(-) create mode 100644 .rustfmt.toml delete mode 100644 src/config.rs delete mode 100644 src/outcomes.rs delete mode 100644 src/rating.rs diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..16bdde9 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +format_code_in_doc_comments = true diff --git a/CHANGELOG.md b/CHANGELOG.md index cde7d18..b401909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ This is a broad overview of the changes that have been made over the lifespan of this library. +## v0.18.0 - 2022-10-23 + +- Major restructuring of ratings, configs and outcomes + - Ratings and Configs now reside in the rating algorithms files + - Outcomes now reside in the lib.rs file + ## v0.17.0 - 2022-10-21 - Added USCF rating algorithms diff --git a/Cargo.toml b/Cargo.toml index 3d94f61..37b84fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skillratings" -version = "0.17.0" +version = "0.18.0" edition = "2021" description = "Calculate a player's skill rating using algorithms like Elo, Glicko, Glicko-2, TrueSkill and many more." readme = "README.md" diff --git a/README.md b/README.md index 805d787..43c1141 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Alternatively, you can add the following to your `Cargo.toml` file manually: ```toml [dependencies] -skillratings = "0.17" +skillratings = "0.18" ``` ## Usage and Examples @@ -54,7 +54,8 @@ We use *Glicko-2* in this example here. ```rust use skillratings::{ - glicko2::glicko2, outcomes::Outcomes, rating::Glicko2Rating, config::Glicko2Config + glicko2::{glicko2, Glicko2Config, Glicko2Rating}, + Outcomes, }; // Initialise a new player rating. @@ -64,7 +65,7 @@ let player_one = Glicko2Rating::new(); // Or you can initialise it with your own values of course. // Imagine these numbers being pulled from a database. let (some_rating, some_deviation, some_volatility) = (1325.0, 230.0, 0.05932); -let player_two = Glicko2Rating{ +let player_two = Glicko2Rating { rating: some_rating, deviation: some_deviation, volatility: some_volatility, @@ -90,10 +91,8 @@ This example shows a 3v3 game using *TrueSkill*. ```rust use skillratings::{ - trueskill::trueskill_teams, - outcomes::Outcomes, - rating::TrueSkillRating, - config::TrueSkillConfig, + trueskill::{trueskill_teams, TrueSkillConfig, TrueSkillRating}, + Outcomes, }; // We initialise Team One as a Vec of multiple TrueSkillRatings. @@ -139,14 +138,14 @@ Every rating algorithm has an `expected_score` function that you can use to pred This example is using *Glicko* (*not Glicko-2!*) to demonstrate. ```rust -use skillratings::{glicko::expected_score, rating::GlickoRating}; +use skillratings::glicko::{expected_score, GlickoRating}; // Initialise a new player rating. // The default values are: 1500.0, and 350.0. let player_one = GlickoRating::new(); // Initialising a new rating with custom numbers. -let player_two = GlickoRating{ +let player_two = GlickoRating { rating: 1812.0, deviation: 195.0, }; @@ -170,21 +169,20 @@ We are using the *Elo* rating algorithm in this example. ```rust use skillratings::{ - elo::elo_rating_period, outcomes::Outcomes, rating::EloRating, config::EloConfig + elo::{elo_rating_period, EloConfig, EloRating}, + Outcomes, }; // We initialise a new Elo Rating here. -let player = EloRating { - rating: 1402.1, -}; +let player = EloRating { rating: 1402.1 }; // We need a list of results to pass to the elo_rating_period function. let mut results = Vec::new(); -// And then we populate the list with tuples containing the opponent, +// And then we populate the list with tuples containing the opponent, // and the outcome of the match from our perspective. results.push((EloRating::new(), Outcomes::WIN)); -results.push((EloRating {rating: 954.0}, Outcomes::DRAW)); +results.push((EloRating { rating: 954.0 }, Outcomes::DRAW)); results.push((EloRating::new(), Outcomes::LOSS)); // The elo_rating_period function calculates the new rating for the player and returns it. diff --git a/benches/benchmarks/elo_bench.rs b/benches/benchmarks/elo_bench.rs index 9a3429f..bf41729 100644 --- a/benches/benchmarks/elo_bench.rs +++ b/benches/benchmarks/elo_bench.rs @@ -1,8 +1,6 @@ use skillratings::{ - config::EloConfig, - elo::{elo, elo_rating_period, expected_score}, - outcomes::Outcomes, - rating::EloRating, + elo::{elo, elo_rating_period, expected_score, EloConfig, EloRating}, + Outcomes, }; use criterion::{black_box, criterion_group, criterion_main, Criterion}; diff --git a/benches/benchmarks/glicko2_bench.rs b/benches/benchmarks/glicko2_bench.rs index b402caa..d40205b 100644 --- a/benches/benchmarks/glicko2_bench.rs +++ b/benches/benchmarks/glicko2_bench.rs @@ -1,8 +1,6 @@ use skillratings::{ - config::Glicko2Config, - glicko2::{expected_score, glicko2, glicko2_rating_period}, - outcomes::Outcomes, - rating::Glicko2Rating, + glicko2::{expected_score, glicko2, glicko2_rating_period, Glicko2Config, Glicko2Rating}, + Outcomes, }; use criterion::{black_box, criterion_group, criterion_main, Criterion}; diff --git a/benches/benchmarks/trueskill_bench.rs b/benches/benchmarks/trueskill_bench.rs index 75d3e99..1b1f2bb 100644 --- a/benches/benchmarks/trueskill_bench.rs +++ b/benches/benchmarks/trueskill_bench.rs @@ -1,10 +1,9 @@ use skillratings::{ - config::TrueSkillConfig, - outcomes::Outcomes, - rating::TrueSkillRating, trueskill::{ expected_score, expected_score_teams, trueskill, trueskill_rating_period, trueskill_teams, + TrueSkillConfig, TrueSkillRating, }, + Outcomes, }; use criterion::{black_box, criterion_group, criterion_main, Criterion}; diff --git a/benches/benchmarks/weng_lin_bench.rs b/benches/benchmarks/weng_lin_bench.rs index c5af63a..ced2d74 100644 --- a/benches/benchmarks/weng_lin_bench.rs +++ b/benches/benchmarks/weng_lin_bench.rs @@ -1,10 +1,9 @@ use skillratings::{ - config::WengLinConfig, - outcomes::Outcomes, - rating::WengLinRating, weng_lin::{ expected_score, expected_score_teams, weng_lin, weng_lin_rating_period, weng_lin_teams, + WengLinConfig, WengLinRating, }, + Outcomes, }; use criterion::{black_box, criterion_group, criterion_main, Criterion}; diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 9809042..0000000 --- a/src/config.rs +++ /dev/null @@ -1,335 +0,0 @@ -//! Contains structs to configure key variables used in the different rating algorithms. -//! -//! Not every algorithm needs a config for its calculations. - -#[derive(Clone, Copy, Debug)] -/// Constants used in the Elo calculations. -pub struct EloConfig { - /// The k-value is the maximum amount of rating change from a single match. - /// In chess, k-values from 40 to 10 are used, with the most common being 32, 24, 16 or 10. - /// The higher the number, the more volatile the ranking. - /// Here the default is 32. - pub k: f64, -} - -impl EloConfig { - #[must_use] - /// Initialize a new `EloConfig` with a k value of `32.0`. - pub const fn new() -> Self { - Self { k: 32.0 } - } -} - -impl Default for EloConfig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the Glicko calculations. -pub struct GlickoConfig { - /// The c value describes how much the rating deviation should decay in each step. - /// The higher the value, the more the rating deviation will decay. - /// In [the paper](http://www.glicko.net/glicko/glicko.pdf) a value of - /// `63.2` seems to be a suggested value, so that is the default here. - pub c: f64, -} - -impl GlickoConfig { - #[must_use] - /// Initialize a new `GlickoConfig` with a c value of `63.2` - pub const fn new() -> Self { - Self { c: 63.2 } - } -} - -impl Default for GlickoConfig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the Glicko-2 calculations. -pub struct Glicko2Config { - /// The tau constant constrains the change in volatility over time. - /// To cite Mark Glickman himself: "Reasonable choices are between 0.3 and 1.2". - /// Smaller values mean less change in volatility and vice versa. - /// The default value here is `0.5`. - pub tau: f64, - /// The convergence tolerance value, the smaller the value the more accurate the volatility calculations. - /// The default value is `0.000_001`, as suggested in [the paper (page 3)](http://www.glicko.net/glicko/glicko2.pdf). - /// Do not set this to a negative value. - pub convergence_tolerance: f64, -} - -impl Glicko2Config { - #[must_use] - /// Initialize a new `Glicko2Config` with a tau value of `0.5` and a convergence tolerance of `0.000_001`. - pub const fn new() -> Self { - Self { - tau: 0.5, - convergence_tolerance: 0.000_001, - } - } -} - -impl Default for Glicko2Config { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the TrueSkill calculations. -pub struct TrueSkillConfig { - /// The probability of draws occurring in match. - /// The higher the probability, the bigger the updates to the ratings in a non-drawn outcome. - /// By default set to `0.1`, meaning 10% chance of a draw. - /// Increase or decrease the value to match the values occurring in your game. - pub draw_probability: f64, - /// The skill-class width, aka the number of difference in rating points - /// needed to have an 80% win probability against another player. - /// By default set to (25 / 3) * 0.5 ≈ `4.167`. - /// If your game is more reliant on pure skill, decrease this value, - /// if there are more random factors, increase it. - pub beta: f64, - /// The additive dynamics factor. - /// It determines how easy it will be for a player to move up and down a leaderboard. - /// A larger value will tend to cause more volatility of player positions. - /// By default set to 25 / 300 ≈ `0.0833`. - pub default_dynamics: f64, -} - -impl TrueSkillConfig { - #[must_use] - /// Initialize a new `TrueSkillConfig` with a draw probability of `0.1`, - /// a beta value of `(25 / 3) * 0.5 ≈ 4.167` and a default dynamics value of 25 / 300 ≈ `0.0833`. - 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() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the Weng-Lin calculations. -pub struct WengLinConfig { - /// The skill-class width, aka the number of difference in rating points - /// needed to have an 80% win probability against another player. - /// By default set to 25 / 6 ≈ `4.167`. - /// If your game is more reliant on pure skill, decrease this value, - /// if there are more random factors, increase it. - pub beta: f64, - /// The lower ceiling of the sigma value, in the uncertainty calculations. - /// The lower this value, the lower the possible uncertainty values. - /// By default set to 0.000_001. - /// Do not set this to a negative value. - pub uncertainty_tolerance: f64, -} - -impl WengLinConfig { - #[must_use] - /// Initialize a new `WengLinConfig` with a beta value of 25 / 6 ≈ `4.167` - /// and an uncertainty tolerance of `0.000_001`. - pub fn new() -> Self { - Self { - beta: 25.0 / 6.0, - uncertainty_tolerance: 0.000_001, - } - } -} - -impl Default for WengLinConfig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the Sticko calculations. -/// -/// If the `h`, `beta`, `lambda` and `gamma` parameters are set to `0.0`, -/// this will behave exactly like the [`Glicko`](crate::glicko::glicko) calculations. -pub struct StickoConfig { - /// Controls player deviations across time. - /// The higher this number, the higher the deviation is going to be. - /// By default set to `10.0`. - /// If you want to mimic the [`GlickoConfig`], set this to `0.0`. - /// Do not set this to a negative value. - pub h: f64, - /// A bonus parameter, which gives a rating boost for just participating. - /// Note that setting this to a positive number will create rating inflation over time. - /// By default set to `0.0`. - /// If you want to mimic the [`GlickoConfig`], set this to `0.0`. - /// Do not set this to a negative value. - pub beta: f64, - /// The neighborhood parameter, which shrinks player ratings towards their opponent. - /// By default set to `2.0`. - /// If you want to mimic the [`GlickoConfig`], set this to `0.0`. - /// Do not set this to a negative value. - pub lambda: f64, - /// The advantage parameter of the first player. - /// If your game is biased towards player one set this to a positive number, - /// or set this to a negative number if the second player has an advantage. - /// With this you could represent the advantage of playing white in chess, - /// or home-team advantage in sports like football and so on. - /// In chess, a value of `30.0` seems to be about correct. - /// By default set to `0.0`. - /// If you want to mimic the [`GlickoConfig`], set this to `0.0`. - pub gamma: f64, - /// The c value describes how much the rating deviation should decay in each step. - /// The higher the value, the more the rating deviation will decay. - /// This is similar to the c value in [`GlickoConfig`]. - /// Keep in mind this needs to be set lower than the c in the [`GlickoConfig`] if the h value here is not equal to zero. - /// By default set to `10.0`. - /// If you want to mimic the [`GlickoConfig`] set this to `63.2`. - pub c: f64, -} - -impl StickoConfig { - #[must_use] - /// Initialize a new `StickoConfig` with a h value of `10.0`, a beta value of `0.0`, - /// a lambda value of `2.0` and a gamma value of `0.0`. - pub const fn new() -> Self { - Self { - h: 10.0, - beta: 0.0, - lambda: 2.0, - gamma: 0.0, - c: 10.0, - } - } -} - -impl Default for StickoConfig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the EGF Calculations. -pub struct EGFConfig { - /// The [handicap](https://en.wikipedia.org/wiki/Handicapping_in_Go), of the perspective of player one. - /// As a general rule, one handicap point is about equal to a 100 rating point difference. - /// - /// If player one has a handicap in the game, - /// you can set this number to the amount of handicap stones given to the opponent. - /// If player two is the one with the handicap, set this number to the negative amount of stones given. - /// If an equal game is played, this value should be 0.0. - /// For example, if player two has a handicap of 4 points (player one starts with 4 stones), set this number to -4.0. - /// - /// The maximum number should not exceed 9.0 or -9.0. - /// By default set to 0.0. - pub handicap: f64, -} - -impl EGFConfig { - #[must_use] - /// Initializes a new `EGFConfig` with a handicap value of `0.0`. - pub const fn new() -> Self { - Self { handicap: 0.0 } - } -} - -impl Default for EGFConfig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the Glicko-Boost calculations. -/// -/// If the `eta` parameter is set to `0.0`, -/// this will behave exactly like the [`Glicko`](crate::glicko::glicko) calculations. -pub struct GlickoBoostConfig { - /// The advantage parameter of the first player. - /// If your game is biased towards player one set this to a positive number, - /// or set this to a negative number if the second player has an advantage. - /// With this you could represent the advantage of playing white in chess, - /// or home-team advantage in sports like football and so on. - /// In chess, a value of `30.0` seems to be about correct. - /// By default set to `0.0`. - /// If you want to mimic the [`GlickoConfig`], set this to `0.0`. - pub eta: f64, - /// The "exceptional performance" threshold. - /// For outstanding performances, the rating deviation of the player will get boosted by the b values. - /// By default set to `1.96`, which is approximately equal to 2.5% of performances. - /// The higher this value, the harder it is to reach the threshold. - /// If you want to mimic the [`GlickoConfig`], set this to `0.0`. - pub k: f64, - /// The rating deviation boost factors. A tuple of 2 [`f64`]s. - /// The first value is multiplicative, the second additive. - /// By default set to 0.20139 and 17.5. - /// If k is set to 0, these will do nothing. - /// If you want to mimic the [`GlickoConfig`], set both of these to `0.0`. - pub b: (f64, f64), - /// The rating deviation increase factors. A tuple of 5 [`f64`]s. - /// These values regulate the rating deviation increase of player's who have not played in a rating period. - /// By default set to 5.83733, -1.75374e-04, -7.080124e-05, 0.001733792, and 0.00026706. - pub alpha: (f64, f64, f64, f64, f64), -} - -impl GlickoBoostConfig { - #[must_use] - /// Initialize a new `GlickoBoostConfig` with a eta value of 30.0, a k value of 1.96, - /// b values of 0.20139 and 17.5, and alpha values of 5.83733, -1.75374e-04, -7.080124e-05, 0.001733792, 0.00026706. - pub const fn new() -> Self { - Self { - eta: 30.0, - k: 1.96, - b: (0.20139, 17.5), - alpha: ( - 5.837_33, - -1.753_74e-04, - -7.080_124e-05, - 0.001_733_792, - 0.000_267_06, - ), - } - } -} - -impl Default for GlickoBoostConfig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy, Debug)] -/// Constants used in the USCF Rating calculations. -pub struct USCFConfig { - /// The t value controls the difficulty of earning bonus rating points. - /// The higher the t value, the more difficult it is. - /// - /// The USCF changes this value periodically. - /// As of 2022, the last change was in May 2017 where this was set from 12 to 14. - /// The lowest value was 6, from 2008 to 2012. - /// By default set to 14.0. - pub t: f64, -} - -impl USCFConfig { - #[must_use] - /// Initialize a new `USCFConfig` with a t value of 14.0. - pub const fn new() -> Self { - Self { t: 14.0 } - } -} - -impl Default for USCFConfig { - fn default() -> Self { - Self::new() - } -} diff --git a/src/dwz.rs b/src/dwz.rs index 891392e..7a4b375 100644 --- a/src/dwz.rs +++ b/src/dwz.rs @@ -13,7 +13,8 @@ //! //! ``` //! use skillratings::{ -//! dwz::dwz, outcomes::Outcomes, rating::DWZRating +//! dwz::{dwz, DWZRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -24,7 +25,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_index, some_age) = (1325.0, 51, 27); -//! let player_two = DWZRating{ +//! let player_two = DWZRating { //! rating: some_rating, //! index: some_index, //! age: some_age, @@ -47,7 +48,53 @@ use std::collections::HashMap; -use crate::{outcomes::Outcomes, rating::DWZRating}; +use crate::{elo::EloRating, Outcomes}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The DWZ (Deutsche Wertungszahl) rating for a player. +/// +/// The age is the actual age of the player, if unsure or unavailable set this to `>25`. +/// Converting from an `EloRating` or using `DWZRating::default()` will set the age to 26. +/// +/// The default rating is 1000.0. +pub struct DWZRating { + /// The player's DWZ rating number, by default 1000.0. + pub rating: f64, + /// The player's DWZ index, how many "events" they have completed. + pub index: usize, + /// The age of the player, if uncertain or unavailable set this to `>25`. + pub age: usize, +} + +impl DWZRating { + #[must_use] + /// Initialize a new `DWZRating` with a rating of 1000.0, an index of 1 and the specified age. + /// The age is the actual age of the player, if unsure or unavailable set this to `>25`. + pub const fn new(age: usize) -> Self { + Self { + rating: 1000.0, + index: 1, + age, + } + } +} + +impl Default for DWZRating { + fn default() -> Self { + Self::new(26) + } +} + +impl From for DWZRating { + fn from(e: EloRating) -> Self { + Self { + rating: e.rating, + // Recommended according to Wikipedia. + index: 6, + ..Default::default() + } + } +} #[must_use] /// Calculates new [`DWZRating`] of two players based on their old rating, index, age and outcome of the game. @@ -66,7 +113,10 @@ use crate::{outcomes::Outcomes, rating::DWZRating}; /// /// # Examples /// ``` -/// use skillratings::{dwz::dwz, outcomes::Outcomes, rating::DWZRating}; +/// use skillratings::{ +/// dwz::{dwz, DWZRating}, +/// Outcomes, +/// }; /// /// let player_one = DWZRating { /// rating: 1500.0, @@ -81,13 +131,13 @@ use crate::{outcomes::Outcomes, rating::DWZRating}; /// /// let outcome = Outcomes::WIN; /// -/// let (player_one_new, player_two_new) = dwz(&player_one, &player_two, &outcome); +/// let (new_one, new_two) = dwz(&player_one, &player_two, &outcome); /// -/// assert!((player_one_new.rating.round() - 1519.0).abs() < f64::EPSILON); -/// assert_eq!(player_one_new.index, 43); +/// assert!((new_one.rating.round() - 1519.0).abs() < f64::EPSILON); +/// assert_eq!(new_one.index, 43); /// -/// assert!((player_two_new.rating.round() - 1464.0).abs() < f64::EPSILON); -/// assert_eq!(player_two_new.index, 13); +/// assert!((new_two.rating.round() - 1464.0).abs() < f64::EPSILON); +/// assert_eq!(new_two.index, 13); /// ``` pub fn dwz( player_one: &DWZRating, @@ -153,7 +203,10 @@ pub fn dwz( /// /// # Examples /// ``` -/// use skillratings::{dwz::dwz_rating_period, outcomes::Outcomes, rating::DWZRating}; +/// use skillratings::{ +/// dwz::{dwz_rating_period, DWZRating}, +/// Outcomes, +/// }; /// /// let player = DWZRating { /// rating: 1530.0, @@ -215,7 +268,7 @@ pub fn dwz_rating_period(player: &DWZRating, results: &Vec<(DWZRating, Outcomes) /// /// # Examples /// ``` -/// use skillratings::{dwz::expected_score, rating::DWZRating}; +/// use skillratings::dwz::{expected_score, DWZRating}; /// /// let player_one = DWZRating { /// rating: 1900.0, @@ -230,7 +283,6 @@ pub fn dwz_rating_period(player: &DWZRating, results: &Vec<(DWZRating, Outcomes) /// /// let (exp_one, exp_two) = expected_score(&player_one, &player_two); /// -/// /// assert!(((exp_one * 100.0).round() - 91.0).abs() < f64::EPSILON); /// assert!(((exp_two * 100.0).round() - 9.0).abs() < f64::EPSILON); /// ``` @@ -247,7 +299,7 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f /// Gets a proper first [`DWZRating`]. /// /// In the case that you do not have enough opponents to rate a player against, -/// consider using [`DWZRating::from()`](DWZRating) if you have an [`EloRating`](crate::rating::EloRating) +/// consider using [`DWZRating::from()`](DWZRating) if you have an [`EloRating`](crate::elo::EloRating) /// or [`DWZRating::new()`](DWZRating) if not. /// /// Takes in the player's age and their results as a Vec of tuples containing the opponent and the outcome. @@ -258,29 +310,32 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f /// /// # Examples /// ``` -/// use skillratings::{dwz::get_first_dwz, outcomes::Outcomes, rating::DWZRating}; +/// use skillratings::{ +/// dwz::{get_first_dwz, DWZRating}, +/// Outcomes, +/// }; /// -/// let o1 = DWZRating { +/// let opponent1 = DWZRating { /// rating: 1300.0, /// index: 23, /// age: 17, /// }; -/// let o2 = DWZRating { +/// let opponent2 = DWZRating { /// rating: 1540.0, /// index: 2, /// age: 29, /// }; -/// let o3 = DWZRating { +/// let opponent3 = DWZRating { /// rating: 1200.0, /// index: 10, /// age: 7, /// }; -/// let o4 = DWZRating { +/// let opponent4 = DWZRating { /// rating: 1290.0, /// index: 76, /// age: 55, /// }; -/// let o5 = DWZRating { +/// let opponent5 = DWZRating { /// rating: 1400.0, /// index: 103, /// age: 11, @@ -289,11 +344,11 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f /// let player = get_first_dwz( /// 26, /// &vec![ -/// (o1, Outcomes::WIN), -/// (o2, Outcomes::DRAW), -/// (o3, Outcomes::LOSS), -/// (o4, Outcomes::WIN), -/// (o5, Outcomes::WIN), +/// (opponent1, Outcomes::WIN), +/// (opponent2, Outcomes::DRAW), +/// (opponent3, Outcomes::LOSS), +/// (opponent4, Outcomes::WIN), +/// (opponent5, Outcomes::WIN), /// ], /// ) /// .unwrap(); @@ -400,10 +455,6 @@ pub fn get_first_dwz(player_age: usize, results: &Vec<(DWZRating, Outcomes)>) -> }) } -fn e0_value(rating: f64, j: f64) -> f64 { - (rating / 1000.0).powi(4) + j -} - fn e_value(rating: f64, age: usize, score: f64, expected_score: f64, index: usize) -> f64 { // The variable j is dependent on the age of the player. From wikipedia: // "Teenagers up to 20 years: `j = 5.0`, junior adults (21 – 25 years): `j = 10.0`, over-25-year-olds: `j = 15.0`" @@ -413,7 +464,8 @@ fn e_value(rating: f64, age: usize, score: f64, expected_score: f64, index: usiz _ => 15.0, }; - let e0 = e0_value(rating, j); + // The base value of the development coefficient. + let e0 = (rating / 1000.0).powi(4) + j; // The acceleration factor allows young, over-achieving players to gain rating more quickly. let a = if age < 20 && score >= expected_score { @@ -780,8 +832,6 @@ mod tests { #[test] fn elo_conversion() { - use crate::rating::EloRating; - let player_one = EloRating { rating: 1200.0 }; let player_one_dwz = DWZRating::from(player_one); diff --git a/src/egf.rs b/src/egf.rs index 3000d63..75c6325 100644 --- a/src/egf.rs +++ b/src/egf.rs @@ -16,7 +16,8 @@ //! //! ``` //! use skillratings::{ -//! egf::egf, outcomes::Outcomes, rating::EGFRating, config::EGFConfig +//! egf::{egf, EGFConfig, EGFRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating with a rating of 0. @@ -25,7 +26,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let some_rating = 325.0; -//! let player_two = EGFRating{ +//! let player_two = EGFRating { //! rating: some_rating, //! }; //! @@ -51,7 +52,66 @@ //! - [Sensei's library](https://senseis.xmp.net/?GoR) //! - [Handicapping in Go](https://en.wikipedia.org/wiki/Handicapping_in_Go) -use crate::{config::EGFConfig, outcomes::Outcomes, rating::EGFRating}; +use crate::Outcomes; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The EGF (European Go Federation) Rating for a player. +/// +/// If the player has a Go rank or similar, +/// you can set the rating value manually approximately according to +/// [this inofficial comparison table](https://forums.online-go.com/t/go-ranks-vs-chess-ratings/41594/42). +/// Keep in mind that here, the lowest possible rating is -900.0. +/// +/// The default rating is 0.0. +pub struct EGFRating { + /// The player's EGF rating number, by default 0.0. + pub rating: f64, +} + +impl EGFRating { + #[must_use] + /// Initialize a new `EGFRating` with a rating of 0.0. + pub const fn new() -> Self { + Self { rating: 0.0 } + } +} + +impl Default for EGFRating { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the EGF Calculations. +pub struct EGFConfig { + /// The [handicap](https://en.wikipedia.org/wiki/Handicapping_in_Go), of the perspective of player one. + /// As a general rule, one handicap point is about equal to a 100 rating point difference. + /// + /// If player one has a handicap in the game, + /// you can set this number to the amount of handicap stones given to the opponent. + /// If player two is the one with the handicap, set this number to the negative amount of stones given. + /// If an equal game is played, this value should be 0.0. + /// For example, if player two has a handicap of 4 points (player one starts with 4 stones), set this number to -4.0. + /// + /// The maximum number should not exceed 9.0 or -9.0. + /// By default set to 0.0. + pub handicap: f64, +} + +impl EGFConfig { + #[must_use] + /// Initializes a new `EGFConfig` with a handicap value of `0.0`. + pub const fn new() -> Self { + Self { handicap: 0.0 } + } +} + +impl Default for EGFConfig { + fn default() -> Self { + Self::new() + } +} #[must_use] /// Calculates the [`EGFRating`]s of two players based on their old ratings and the outcome of the game. @@ -65,7 +125,8 @@ use crate::{config::EGFConfig, outcomes::Outcomes, rating::EGFRating}; /// /// ``` /// use skillratings::{ -/// egf::egf, outcomes::Outcomes, rating::EGFRating, config::EGFConfig +/// egf::{egf, EGFConfig, EGFRating}, +/// Outcomes, /// }; /// /// let player_one = EGFRating { rating: 950.0 }; @@ -75,10 +136,10 @@ use crate::{config::EGFConfig, outcomes::Outcomes, rating::EGFRating}; /// /// let config = EGFConfig::new(); /// -/// let (player_one_new, player_two_new) = egf(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = egf(&player_one, &player_two, &outcome, &config); /// -/// assert!((player_one_new.rating.round() - 989.0).abs() < f64::EPSILON); -/// assert!((player_two_new.rating.round() - 1173.0).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 989.0).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 1173.0).abs() < f64::EPSILON); /// ``` pub fn egf( player_one: &EGFRating, @@ -137,7 +198,8 @@ pub fn egf( /// # Examples /// ``` /// use skillratings::{ -/// egf::egf_rating_period, outcomes::Outcomes, rating::EGFRating, config::EGFConfig +/// egf::{egf_rating_period, EGFConfig, EGFRating}, +/// Outcomes, /// }; /// /// let player = EGFRating { rating: 220.0 }; @@ -195,7 +257,8 @@ pub fn egf_rating_period( /// # Examples /// ``` /// use skillratings::{ -/// egf::expected_score, rating::EGFRating, config::EGFConfig +/// egf::{expected_score, EGFConfig, EGFRating}, +/// Outcomes, /// }; /// /// let player_one = EGFRating { rating: 1320.0 }; @@ -203,10 +266,10 @@ pub fn egf_rating_period( /// /// let config = EGFConfig::new(); /// -/// let (winner_exp, loser_exp) = expected_score(&player_one, &player_two, &config); +/// let (exp1, exp2) = expected_score(&player_one, &player_two, &config); /// -/// assert!(((winner_exp * 100.0).round() - 59.0).abs() < f64::EPSILON); -/// assert!(((loser_exp * 100.0).round() - 41.0).abs() < f64::EPSILON); +/// assert!(((exp1 * 100.0).round() - 59.0).abs() < f64::EPSILON); +/// assert!(((exp2 * 100.0).round() - 41.0).abs() < f64::EPSILON); /// ``` pub fn expected_score( player_one: &EGFRating, diff --git a/src/elo.rs b/src/elo.rs index 230240d..af1ae19 100644 --- a/src/elo.rs +++ b/src/elo.rs @@ -11,7 +11,8 @@ //! //! ``` //! use skillratings::{ -//! elo::elo, outcomes::Outcomes, rating::EloRating, config::EloConfig +//! elo::{elo, EloConfig, EloRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -20,7 +21,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let some_rating = 1325.0; -//! let player_two = EloRating{ +//! let player_two = EloRating { //! rating: some_rating, //! }; //! @@ -30,7 +31,7 @@ //! // The config allows you to specify certain values in the Elo calculation. //! // Here we modify the k-value to be 20.0, instead of the usual 32.0. //! // To simplify massively: This means the ratings will not change as much. -//! let config = EloConfig { k: 20.0 } ; +//! let config = EloConfig { k: 20.0 }; //! //! // The elo function will calculate the new ratings for both players and return them. //! let (new_player_one, new_player_two) = elo(&player_one, &player_two, &outcome, &config); @@ -43,7 +44,82 @@ //! - [FIDE Ratings](https://ratings.fide.com/) //! - [FIFA Ratings](https://www.fifa.com/fifa-world-ranking) -use crate::{config::EloConfig, outcomes::Outcomes, rating::EloRating}; +use crate::{dwz::DWZRating, ingo::IngoRating, uscf::USCFRating, Outcomes}; + +/// The Elo rating of a player. +/// +/// The default rating is 1000.0. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct EloRating { + /// The player's Elo rating number, by default 1000.0. + pub rating: f64, +} + +impl EloRating { + /// Initialize a new `EloRating` with a rating of 1000.0. + #[must_use] + pub const fn new() -> Self { + Self { rating: 1000.0 } + } +} + +impl Default for EloRating { + fn default() -> Self { + Self::new() + } +} + +impl From for EloRating { + fn from(i: IngoRating) -> Self { + Self { + rating: 2840.0 - 8.0 * i.rating, + } + } +} + +impl From for EloRating { + fn from(d: DWZRating) -> Self { + Self { rating: d.rating } + } +} + +impl From for EloRating { + fn from(u: USCFRating) -> Self { + if u.rating > 2060.0 { + Self { + rating: (u.rating - 180.0) / 0.94, + } + } else { + Self { + rating: (u.rating - 20.0) / 1.02, + } + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the Elo calculations. +pub struct EloConfig { + /// The k-value is the maximum amount of rating change from a single match. + /// In chess, k-values from 40 to 10 are used, with the most common being 32, 24, 16 or 10. + /// The higher the number, the more volatile the ranking. + /// Here the default is 32. + pub k: f64, +} + +impl EloConfig { + #[must_use] + /// Initialize a new `EloConfig` with a k value of `32.0`. + pub const fn new() -> Self { + Self { k: 32.0 } + } +} + +impl Default for EloConfig { + fn default() -> Self { + Self::new() + } +} /// Calculates the [`EloRating`]s of two players based on their old ratings and the outcome of the game. /// @@ -54,7 +130,10 @@ use crate::{config::EloConfig, outcomes::Outcomes, rating::EloRating}; /// /// # Examples /// ``` -/// use skillratings::{elo::elo, outcomes::Outcomes, rating::EloRating, config::EloConfig}; +/// use skillratings::{ +/// elo::{elo, EloConfig, EloRating}, +/// Outcomes, +/// }; /// /// let player_one = EloRating { rating: 600.0 }; /// let player_two = EloRating { rating: 711.0 }; @@ -63,10 +142,10 @@ use crate::{config::EloConfig, outcomes::Outcomes, rating::EloRating}; /// /// let config = EloConfig::new(); /// -/// let (player_one_new, player_two_new) = elo(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = elo(&player_one, &player_two, &outcome, &config); /// -/// assert!((player_one_new.rating.round() - 621.0).abs() < f64::EPSILON); -/// assert!((player_two_new.rating.round() - 690.0).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 621.0).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 690.0).abs() < f64::EPSILON); /// ``` #[must_use] pub fn elo( @@ -104,7 +183,10 @@ pub fn elo( /// /// # Examples /// ``` -/// use skillratings::{elo::elo_rating_period, outcomes::Outcomes, rating::EloRating, config::EloConfig}; +/// use skillratings::{ +/// elo::{elo_rating_period, EloConfig, EloRating}, +/// Outcomes, +/// }; /// /// let player = EloRating { rating: 1204.0 }; /// @@ -158,15 +240,15 @@ pub fn elo_rating_period( /// /// # Examples /// ``` -/// use skillratings::{elo::expected_score, rating::EloRating}; +/// use skillratings::elo::{expected_score, EloRating}; /// /// let player_one = EloRating { rating: 1320.0 }; /// let player_two = EloRating { rating: 1217.0 }; /// -/// let (winner_exp, loser_exp) = expected_score(&player_one, &player_two); +/// let (exp1, exp2) = expected_score(&player_one, &player_two); /// -/// assert!(((winner_exp * 100.0).round() - 64.0).abs() < f64::EPSILON); -/// assert!(((loser_exp * 100.0).round() - 36.0).abs() < f64::EPSILON); +/// assert!(((exp1 * 100.0).round() - 64.0).abs() < f64::EPSILON); +/// assert!(((exp2 * 100.0).round() - 36.0).abs() < f64::EPSILON); /// ``` #[must_use] pub fn expected_score(player_one: &EloRating, player_two: &EloRating) -> (f64, f64) { diff --git a/src/glicko.rs b/src/glicko.rs index e812b15..09a847d 100644 --- a/src/glicko.rs +++ b/src/glicko.rs @@ -15,7 +15,8 @@ //! //! ``` //! use skillratings::{ -//! glicko::glicko, outcomes::Outcomes, rating::GlickoRating +//! glicko::{glicko, GlickoRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -24,7 +25,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_deviation) = (1325.0, 230.0); -//! let player_two = GlickoRating{ +//! let player_two = GlickoRating { //! rating: some_rating, //! deviation: some_deviation, //! }; @@ -42,9 +43,93 @@ //! - [Original Paper by Mark Glickman](http://www.glicko.net/glicko/glicko.pdf) //! - [Glicko Calculator](http://www.bjcox.com/?page_id=2) -use crate::{config::GlickoConfig, outcomes::Outcomes, rating::GlickoRating}; +use crate::{ + glicko2::Glicko2Rating, glicko_boost::GlickoBoostRating, sticko::StickoRating, Outcomes, +}; use std::f64::consts::PI; +#[derive(Copy, Clone, Debug, PartialEq)] +/// The Glicko rating for a player. +/// +/// For the Glicko-2 rating, please see [`Glicko2Rating`]. +/// +/// The default rating is 1500.0. +/// The default deviation is 350.0. +pub struct GlickoRating { + /// The player's Glicko rating number, by default 1500.0. + pub rating: f64, + /// The player's Glicko deviation number, by default 350.0. + pub deviation: f64, +} + +impl GlickoRating { + #[must_use] + /// Initialize a new `GlickoRating` with a rating of 1500.0 and a deviation of 350.0. + pub const fn new() -> Self { + Self { + rating: 1500.0, + deviation: 350.0, + } + } +} + +impl Default for GlickoRating { + fn default() -> Self { + Self::new() + } +} + +impl From for GlickoRating { + fn from(g: Glicko2Rating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +impl From for GlickoRating { + fn from(g: GlickoBoostRating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +impl From for GlickoRating { + fn from(s: StickoRating) -> Self { + Self { + rating: s.rating, + deviation: s.deviation, + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the Glicko calculations. +pub struct GlickoConfig { + /// The c value describes how much the rating deviation should decay in each step. + /// The higher the value, the more the rating deviation will decay. + /// In [the paper](http://www.glicko.net/glicko/glicko.pdf) a value of + /// `63.2` seems to be a suggested value, so that is the default here. + pub c: f64, +} + +impl GlickoConfig { + #[must_use] + /// Initialize a new `GlickoConfig` with a c value of `63.2` + pub const fn new() -> Self { + Self { c: 63.2 } + } +} + +impl Default for GlickoConfig { + fn default() -> Self { + Self::new() + } +} + #[must_use] /// Calculates the [`GlickoRating`]s of two players based on their old ratings, deviations, and the outcome of the game. /// @@ -63,7 +148,10 @@ use std::f64::consts::PI; /// /// # Examples /// ``` -/// use skillratings::{glicko::glicko, outcomes::Outcomes, rating::GlickoRating}; +/// use skillratings::{ +/// glicko::{glicko, GlickoRating}, +/// Outcomes, +/// }; /// /// let player_one = GlickoRating { /// rating: 1500.0, @@ -76,13 +164,13 @@ use std::f64::consts::PI; /// /// let outcome = Outcomes::WIN; /// -/// let (player_one_new, player_two_new) = glicko(&player_one, &player_two, &outcome); +/// let (new_one, new_two) = glicko(&player_one, &player_two, &outcome); /// -/// assert!((player_one_new.rating.round() - 1662.0).abs() < f64::EPSILON); -/// assert!((player_one_new.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 1662.0).abs() < f64::EPSILON); +/// assert!((new_one.deviation.round() - 290.0).abs() < f64::EPSILON); /// -/// assert!((player_two_new.rating.round() - 1338.0).abs() < f64::EPSILON); -/// assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 1338.0).abs() < f64::EPSILON); +/// assert!((new_two.deviation.round() - 290.0).abs() < f64::EPSILON); /// ``` pub fn glicko( player_one: &GlickoRating, @@ -153,7 +241,10 @@ pub fn glicko( /// /// # Examples /// ``` -/// use skillratings::{glicko::glicko_rating_period, outcomes::Outcomes, rating::GlickoRating, config::GlickoConfig}; +/// use skillratings::{ +/// glicko::{glicko_rating_period, GlickoConfig, GlickoRating}, +/// Outcomes, +/// }; /// /// let player = GlickoRating { /// rating: 1500.0, @@ -247,7 +338,7 @@ pub fn glicko_rating_period( /// /// # Examples /// ``` -/// use skillratings::{glicko::expected_score, rating::GlickoRating}; +/// use skillratings::glicko::{expected_score, GlickoRating}; /// /// let player_one = GlickoRating { /// rating: 2500.0, @@ -280,7 +371,7 @@ pub fn expected_score(player_one: &GlickoRating, player_two: &GlickoRating) -> ( /// /// # Examples /// ``` -/// use skillratings::{glicko::decay_deviation, rating::GlickoRating, config::GlickoConfig}; +/// use skillratings::glicko::{decay_deviation, GlickoConfig, GlickoRating}; /// /// let player_one = GlickoRating { /// rating: 2720.0, @@ -310,8 +401,8 @@ pub fn decay_deviation(player: &GlickoRating, config: &GlickoConfig) -> GlickoRa /// Takes in a player as a [`GlickoRating`] and returns two [`f64`]s that describe the lowest and highest rating. /// /// # Examples -/// ```rust -/// use skillratings::{rating::GlickoRating, glicko::confidence_interval}; +/// ``` +/// use skillratings::glicko::{confidence_interval, GlickoRating}; /// /// let player = GlickoRating { /// rating: 2250.0, diff --git a/src/glicko2.rs b/src/glicko2.rs index fb9c444..776dc10 100644 --- a/src/glicko2.rs +++ b/src/glicko2.rs @@ -13,7 +13,8 @@ //! //! ``` //! use skillratings::{ -//! glicko2::glicko2, outcomes::Outcomes, rating::Glicko2Rating, config::Glicko2Config +//! glicko2::{glicko2, Glicko2Config, Glicko2Rating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -22,7 +23,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_deviation, some_volatility) = (1325.0, 230.0, 0.05932); -//! let player_two = Glicko2Rating{ +//! let player_two = Glicko2Rating { //! rating: some_rating, //! deviation: some_deviation, //! volatility: some_volatility, @@ -50,9 +51,107 @@ //! - [Original Paper by Mark Glickman](http://www.glicko.net/glicko/glicko2.pdf) //! - [Glicko-2 Calculator](https://fsmosca-glicko2calculator-glicko2calculator-vik8k0.streamlitapp.com/) -use crate::{config::Glicko2Config, outcomes::Outcomes, rating::Glicko2Rating}; +use crate::{ + glicko::GlickoRating, glicko_boost::GlickoBoostRating, sticko::StickoRating, Outcomes, +}; use std::f64::consts::PI; +/// The Glicko-2 rating of a player. +/// +/// For the Glicko rating, please see [`GlickoRating`]. +/// +/// The default rating is 1500.0. +/// The default deviation is 350.0. +/// The default volatility is 0.06. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Glicko2Rating { + /// The player's Glicko-2 rating number, by default 1500.0. + pub rating: f64, + /// The player's Glicko-2 deviation number, by default 350.0. + pub deviation: f64, + /// The player's Glicko-2 volatility number, by default 0.06. + pub volatility: f64, +} + +impl Glicko2Rating { + /// Initialize a new `Glicko2Rating` with a rating of 1500.0, a deviation of 350.0 and a volatility of 0.06. + #[must_use] + pub const fn new() -> Self { + Self { + rating: 1500.0, + deviation: 350.0, + volatility: 0.06, + } + } +} + +impl Default for Glicko2Rating { + fn default() -> Self { + Self::new() + } +} + +impl From for Glicko2Rating { + fn from(g: GlickoRating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + ..Default::default() + } + } +} + +impl From for Glicko2Rating { + fn from(g: GlickoBoostRating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + ..Default::default() + } + } +} + +impl From for Glicko2Rating { + fn from(s: StickoRating) -> Self { + Self { + rating: s.rating, + deviation: s.deviation, + ..Default::default() + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the Glicko-2 calculations. +pub struct Glicko2Config { + /// The tau constant constrains the change in volatility over time. + /// To cite Mark Glickman himself: "Reasonable choices are between 0.3 and 1.2". + /// Smaller values mean less change in volatility and vice versa. + /// The default value here is `0.5`. + pub tau: f64, + /// The convergence tolerance value, the smaller the value the more accurate the volatility calculations. + /// The default value is `0.000_001`, as suggested in [the paper (page 3)](http://www.glicko.net/glicko/glicko2.pdf). + /// Do not set this to a negative value. + pub convergence_tolerance: f64, +} + +impl Glicko2Config { + #[must_use] + /// Initialize a new `Glicko2Config` with a tau value of `0.5` and a convergence tolerance of `0.000_001`. + pub const fn new() -> Self { + Self { + tau: 0.5, + convergence_tolerance: 0.000_001, + } + } +} + +impl Default for Glicko2Config { + fn default() -> Self { + Self::new() + } +} + /// Calculates the [`Glicko2Rating`]s of two players based on their old ratings, deviations, volatilities, and the outcome of the game. /// /// For the original version, please see [`Glicko`](crate::glicko). @@ -70,7 +169,10 @@ use std::f64::consts::PI; /// /// # Examples /// ``` -/// use skillratings::{glicko2::glicko2, outcomes::Outcomes, rating::Glicko2Rating, config::Glicko2Config}; +/// use skillratings::{ +/// glicko2::{glicko2, Glicko2Config, Glicko2Rating}, +/// Outcomes, +/// }; /// /// let player_one = Glicko2Rating { /// rating: 1500.0, @@ -87,15 +189,15 @@ use std::f64::consts::PI; /// /// let config = Glicko2Config::new(); /// -/// let (player_one_new, player_two_new) = glicko2(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = glicko2(&player_one, &player_two, &outcome, &config); /// -/// assert!((player_one_new.rating.round() - 1662.0).abs() < f64::EPSILON); -/// assert!((player_one_new.deviation.round() - 290.0).abs() < f64::EPSILON); -/// assert!((player_one_new.volatility - 0.05999967537233814).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 1662.0).abs() < f64::EPSILON); +/// assert!((new_one.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_one.volatility - 0.05999967537233814).abs() < f64::EPSILON); /// -/// assert!((player_two_new.rating.round() - 1338.0).abs() < f64::EPSILON); -/// assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON); -/// assert!((player_two_new.volatility - 0.05999967537233814).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 1338.0).abs() < f64::EPSILON); +/// assert!((new_two.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_two.volatility - 0.05999967537233814).abs() < f64::EPSILON); /// ``` #[must_use] pub fn glicko2( @@ -127,29 +229,23 @@ pub fn glicko2( let player_one_new_volatility = new_volatility( player_one.volatility, - delta_value(outcome1, v1, g1, e1), - player_one_deviation, + delta_value(outcome1, v1, g1, e1).powi(2), + player_one_deviation.powi(2), v1, config.tau, config.convergence_tolerance, ); let player_two_new_volatility = new_volatility( player_two.volatility, - delta_value(outcome2, v2, g2, e2), - player_two_deviation, + delta_value(outcome2, v2, g2, e2).powi(2), + player_two_deviation.powi(2), v2, config.tau, config.convergence_tolerance, ); - let new_deviation1 = new_deviation( - new_pre_deviation(player_one_deviation, player_one_new_volatility), - v1, - ); - let new_deviation2 = new_deviation( - new_pre_deviation(player_two_deviation, player_two_new_volatility), - v2, - ); + let new_deviation1 = new_deviation(player_one_deviation, player_one_new_volatility, v1); + let new_deviation2 = new_deviation(player_two_deviation, player_two_new_volatility, v2); let new_rating1 = new_rating(player_one_rating, new_deviation1, outcome1, g1, e1); let new_rating2 = new_rating(player_two_rating, new_deviation2, outcome2, g2, e2); @@ -185,7 +281,10 @@ pub fn glicko2( /// /// # Examples /// ``` -/// use skillratings::{glicko2::glicko2_rating_period, outcomes::Outcomes, rating::Glicko2Rating, config::Glicko2Config}; +/// use skillratings::{ +/// glicko2::{glicko2_rating_period, Glicko2Config, Glicko2Rating}, +/// Outcomes, +/// }; /// /// let player = Glicko2Rating { /// rating: 1500.0, @@ -193,28 +292,28 @@ pub fn glicko2( /// volatility: 0.06, /// }; /// -/// let opponent_one = Glicko2Rating { +/// let opponent1 = Glicko2Rating { /// rating: 1400.0, /// deviation: 30.0, /// volatility: 0.06, /// }; /// -/// let opponent_two = Glicko2Rating { +/// let opponent2 = Glicko2Rating { /// rating: 1550.0, /// deviation: 100.0, /// volatility: 0.06, /// }; /// -/// let opponent_three = Glicko2Rating { +/// let opponent3 = Glicko2Rating { /// rating: 1700.0, /// deviation: 300.0, /// volatility: 0.06, /// }; /// /// let results = vec![ -/// (opponent_one, Outcomes::WIN), -/// (opponent_two, Outcomes::LOSS), -/// (opponent_three, Outcomes::LOSS), +/// (opponent1, Outcomes::WIN), +/// (opponent2, Outcomes::LOSS), +/// (opponent3, Outcomes::LOSS), /// ]; /// /// let new_player = glicko2_rating_period(&player, &results, &Glicko2Config::new()); @@ -263,16 +362,14 @@ pub fn glicko2_rating_period( let new_volatility = new_volatility( player.volatility, - delta, - player_deviation, + delta.powi(2), + player_deviation.powi(2), v, config.tau, config.convergence_tolerance, ); - let new_pre_deviation = new_pre_deviation(player_deviation, new_volatility); - - let new_deviation = new_deviation(new_pre_deviation, v); + let new_deviation = new_deviation(player_deviation, new_volatility, v); let new_rating = new_deviation.powi(2).mul_add(scores, player_rating); @@ -291,7 +388,7 @@ pub fn glicko2_rating_period( /// /// # Examples /// ``` -/// use skillratings::{glicko2::expected_score, rating::Glicko2Rating}; +/// use skillratings::glicko2::{expected_score, Glicko2Rating}; /// /// let player_one = Glicko2Rating { /// rating: 2500.0, @@ -334,7 +431,7 @@ pub fn expected_score(player_one: &Glicko2Rating, player_two: &Glicko2Rating) -> /// /// # Examples /// ``` -/// use skillratings::{glicko2::decay_deviation, rating::Glicko2Rating}; +/// use skillratings::glicko2::{decay_deviation, Glicko2Rating}; /// /// let player_one = Glicko2Rating { /// rating: 2720.0, @@ -366,8 +463,8 @@ pub fn decay_deviation(player: &Glicko2Rating) -> Glicko2Rating { /// Takes in a player as a [`Glicko2Rating`] and returns two [`f64`]s that describe the lowest and highest rating. /// /// # Examples -/// ```rust -/// use skillratings::{rating::Glicko2Rating, glicko2::confidence_interval}; +/// ``` +/// use skillratings::glicko2::{confidence_interval, Glicko2Rating}; /// /// let player = Glicko2Rating { /// rating: 2250.0, @@ -424,15 +521,12 @@ fn f_value( fn new_volatility( old_volatility: f64, - delta: f64, - deviation: f64, + delta_squared: f64, + deviation_squared: f64, v: f64, tau: f64, convergence_tolerance: f64, ) -> f64 { - let delta_squared = delta.powi(2); - let deviation_squared = deviation.powi(2); - let mut a = old_volatility.powi(2).ln(); let mut b = if delta_squared > deviation_squared + v { (delta_squared - deviation_squared - v).ln() @@ -474,11 +568,9 @@ fn new_volatility( (a / 2.0).exp() } -fn new_pre_deviation(deviation: f64, new_volatility: f64) -> f64 { - deviation.hypot(new_volatility) -} +fn new_deviation(deviation: f64, new_volatility: f64, v: f64) -> f64 { + let pre_deviation = deviation.hypot(new_volatility); -fn new_deviation(pre_deviation: f64, v: f64) -> f64 { ((pre_deviation.powi(2).recip()) + (v.recip())) .sqrt() .recip() @@ -835,8 +927,6 @@ mod tests { #[test] fn glicko_conversion() { - use crate::rating::GlickoRating; - let glicko2_player = Glicko2Rating::new(); let glicko1_player = GlickoRating::from(glicko2_player); diff --git a/src/glicko_boost.rs b/src/glicko_boost.rs index 2398b7b..fe04b4b 100644 --- a/src/glicko_boost.rs +++ b/src/glicko_boost.rs @@ -26,10 +26,8 @@ //! //! ``` //! use skillratings::{ -//! glicko_boost::glicko_boost, -//! outcomes::Outcomes, -//! rating::GlickoBoostRating, -//! config::GlickoBoostConfig, +//! glicko_boost::{glicko_boost, GlickoBoostConfig, GlickoBoostRating}, +//! Outcomes, //! }; //! //! // Initialize a new player rating. @@ -38,7 +36,7 @@ //! // Or you can initialize it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_deviation) = (1325.0, 230.0); -//! let player_two = GlickoBoostRating{ +//! let player_two = GlickoBoostRating { //! rating: some_rating, //! deviation: some_deviation, //! }; @@ -59,7 +57,8 @@ //! }; //! //! // The glicko_boost function will calculate the new ratings for both players and return them. -//! let (new_one, new_two) = glicko_boost(&player_one, &player_two, &outcome, &config); +//! let (new_player_one, new_player_two) = +//! glicko_boost(&player_one, &player_two, &outcome, &config); //! ``` //! //! # More Information @@ -70,7 +69,124 @@ use std::f64::consts::PI; -use crate::{config::GlickoBoostConfig, outcomes::Outcomes, rating::GlickoBoostRating}; +use crate::{glicko::GlickoRating, glicko2::Glicko2Rating, sticko::StickoRating, Outcomes}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The Glicko-Boost rating of a player. +/// +/// Similar to [`GlickoRating`]. +/// +/// The default rating is 1500.0. +/// The default deviation is 350.0. +pub struct GlickoBoostRating { + /// The player's Glicko-Boost rating number, by default 1500.0. + pub rating: f64, + /// The player's Glicko-Boost deviation number, by default 350.0. + pub deviation: f64, +} + +impl GlickoBoostRating { + #[must_use] + /// Initialize a new `GlickoBoostRating` with a rating of 1500.0 and a deviation of 350.0. + pub const fn new() -> Self { + Self { + rating: 1500.0, + deviation: 350.0, + } + } +} + +impl Default for GlickoBoostRating { + fn default() -> Self { + Self::new() + } +} + +impl From for GlickoBoostRating { + fn from(g: GlickoRating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +impl From for GlickoBoostRating { + fn from(g: Glicko2Rating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +impl From for GlickoBoostRating { + fn from(s: StickoRating) -> Self { + Self { + rating: s.rating, + deviation: s.deviation, + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the Glicko-Boost calculations. +/// +/// If the `eta` parameter is set to `0.0`, +/// this will behave exactly like the [`Glicko`](crate::glicko::glicko) calculations. +pub struct GlickoBoostConfig { + /// The advantage parameter of the first player. + /// If your game is biased towards player one set this to a positive number, + /// or set this to a negative number if the second player has an advantage. + /// With this you could represent the advantage of playing white in chess, + /// or home-team advantage in sports like football and so on. + /// In chess, a value of `30.0` seems to be about correct. + /// By default set to `0.0`. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set this to `0.0`. + pub eta: f64, + /// The "exceptional performance" threshold. + /// For outstanding performances, the rating deviation of the player will get boosted by the b values. + /// By default set to `1.96`, which is approximately equal to 2.5% of performances. + /// The higher this value, the harder it is to reach the threshold. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set this to `0.0`. + pub k: f64, + /// The rating deviation boost factors. A tuple of 2 [`f64`]s. + /// The first value is multiplicative, the second additive. + /// By default set to 0.20139 and 17.5. + /// If k is set to 0, these will do nothing. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set both of these to `0.0`. + pub b: (f64, f64), + /// The rating deviation increase factors. A tuple of 5 [`f64`]s. + /// These values regulate the rating deviation increase of player's who have not played in a rating period. + /// By default set to 5.83733, -1.75374e-04, -7.080124e-05, 0.001733792, and 0.00026706. + pub alpha: (f64, f64, f64, f64, f64), +} + +impl GlickoBoostConfig { + #[must_use] + /// Initialize a new `GlickoBoostConfig` with a eta value of 30.0, a k value of 1.96, + /// b values of 0.20139 and 17.5, and alpha values of 5.83733, -1.75374e-04, -7.080124e-05, 0.001733792, 0.00026706. + pub const fn new() -> Self { + Self { + eta: 30.0, + k: 1.96, + b: (0.20139, 17.5), + alpha: ( + 5.837_33, + -1.753_74e-04, + -7.080_124e-05, + 0.001_733_792, + 0.000_267_06, + ), + } + } +} + +impl Default for GlickoBoostConfig { + fn default() -> Self { + Self::new() + } +} #[must_use] /// Calculates the [`GlickoBoostRating`]s of two players based on their old ratings, deviations, and the outcome of the game. @@ -94,10 +210,8 @@ use crate::{config::GlickoBoostConfig, outcomes::Outcomes, rating::GlickoBoostRa /// # Examples /// ``` /// use skillratings::{ -/// glicko_boost::glicko_boost, -/// outcomes::Outcomes, -/// rating::GlickoBoostRating, -/// config::GlickoBoostConfig +/// glicko_boost::{glicko_boost, GlickoBoostConfig, GlickoBoostRating}, +/// Outcomes, /// }; /// /// let player_one = GlickoBoostRating { @@ -117,13 +231,13 @@ use crate::{config::GlickoBoostConfig, outcomes::Outcomes, rating::GlickoBoostRa /// ..Default::default() /// }; /// -/// let (player_one_new, player_two_new) = glicko_boost(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = glicko_boost(&player_one, &player_two, &outcome, &config); /// -/// assert!((player_one_new.rating.round() - 1672.0).abs() < f64::EPSILON); -/// assert!((player_one_new.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 1672.0).abs() < f64::EPSILON); +/// assert!((new_one.deviation.round() - 290.0).abs() < f64::EPSILON); /// -/// assert!((player_two_new.rating.round() - 1328.0).abs() < f64::EPSILON); -/// assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 1328.0).abs() < f64::EPSILON); +/// assert!((new_two.deviation.round() - 290.0).abs() < f64::EPSILON); /// ``` pub fn glicko_boost( player_one: &GlickoBoostRating, @@ -208,10 +322,8 @@ pub fn glicko_boost( /// # Examples /// ``` /// use skillratings::{ -/// glicko_boost::glicko_boost_rating_period, -/// outcomes::Outcomes, -/// rating::GlickoBoostRating, -/// config::GlickoBoostConfig +/// glicko_boost::{glicko_boost_rating_period, GlickoBoostConfig, GlickoBoostRating}, +/// Outcomes, /// }; /// /// let player = GlickoBoostRating { @@ -321,9 +433,7 @@ pub fn glicko_boost_rating_period( /// /// # Examples /// ``` -/// use skillratings::{ -/// glicko_boost::expected_score, rating::GlickoBoostRating, config::GlickoBoostConfig -/// }; +/// use skillratings::glicko_boost::{expected_score, GlickoBoostConfig, GlickoBoostRating}; /// /// let player_one = GlickoBoostRating { /// rating: 2500.0, @@ -370,9 +480,7 @@ pub fn expected_score( /// /// # Examples /// ``` -/// use skillratings::{ -/// glicko_boost::decay_deviation, rating::GlickoBoostRating, config::GlickoBoostConfig -/// }; +/// use skillratings::glicko_boost::{decay_deviation, GlickoBoostConfig, GlickoBoostRating}; /// /// let player_one = GlickoBoostRating { /// rating: 2720.0, @@ -424,8 +532,8 @@ pub fn decay_deviation( /// Takes in a player as a [`GlickoBoostRating`] and returns two [`f64`]s that describe the lowest and highest rating. /// /// # Examples -/// ```rust -/// use skillratings::{rating::GlickoBoostRating, glicko_boost::confidence_interval}; +/// ``` +/// use skillratings::glicko_boost::{confidence_interval, GlickoBoostRating}; /// /// let player = GlickoBoostRating { /// rating: 2250.0, @@ -460,6 +568,19 @@ fn new_rating(old_rating: f64, deviation: f64, score: f64, q: f64, g: f64, e: f6 (deviation.powi(2) * q * g).mul_add(score - e, old_rating) } +fn z_value(g: f64, e: f64, score: f64) -> f64 { + (g * (score - e)) / (g.powi(2) * e * (1.0 - e)).sqrt() +} + +fn boost_rd(z: f64, deviation: f64, config: &GlickoBoostConfig) -> f64 { + (z - config.k) + .mul_add(config.b.0, 1.0) + .mul_add(deviation, config.b.1) +} + +// The functions below are very similar to the normal glicko functions, +// but with the advantage parameters. + fn g_value(q: f64, opponent_deviation: f64) -> f64 { (1.0 + ((3.0 * q.powi(2) * opponent_deviation.powi(2)) / (PI.powi(2)))) .sqrt() @@ -475,16 +596,6 @@ fn d_value(q: f64, g: f64, e: f64) -> f64 { (q.powi(2) * g.powi(2) * e * (1.0 - e)).powi(-1) } -fn z_value(g: f64, e: f64, score: f64) -> f64 { - (g * (score - e)) / (g.powi(2) * e * (1.0 - e)).sqrt() -} - -fn boost_rd(z: f64, deviation: f64, config: &GlickoBoostConfig) -> f64 { - (z - config.k) - .mul_add(config.b.0, 1.0) - .mul_add(deviation, config.b.1) -} - #[cfg(test)] mod tests { use super::*; @@ -664,8 +775,6 @@ mod tests { #[test] #[allow(clippy::similar_names)] fn test_glicko_conv() { - use crate::rating::{Glicko2Rating, GlickoRating}; - let glickob = GlickoBoostRating::new(); let glicko_conv = GlickoRating::from(glickob); diff --git a/src/ingo.rs b/src/ingo.rs index 875cfd9..5d7e0b7 100644 --- a/src/ingo.rs +++ b/src/ingo.rs @@ -12,7 +12,8 @@ //! //! ``` //! use skillratings::{ -//! ingo::ingo, outcomes::Outcomes, rating::IngoRating +//! ingo::{ingo, IngoRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -23,7 +24,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_age) = (150.4, 23); -//! let player_two = IngoRating{ +//! let player_two = IngoRating { //! rating: some_rating, //! age: some_age, //! }; @@ -41,7 +42,49 @@ //! - [Archive of Ingo Ratings (German)](https://www.schachbund.de/ingo-spiegel.html) //! - [Ingo Rules (German, PDF Download)](https://www.schachbund.de/ingo-spiegel.html?file=files/dsb/historie/ingo-spiegel/Ingo-Regeln.pdf&cid=28120) -use crate::{outcomes::Outcomes, rating::IngoRating}; +use crate::{elo::EloRating, Outcomes}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The Ingo rating of a player. +/// +/// Note that unlike in the other systems, a lower score is better than a higher score. +/// Negative values are possible. +/// +/// The age is the actual age of the player, if unsure or unavailable set this to `>25`. +/// Converting from an `EloRating` or using `IngoRating::default()` will set the age to 26. +/// +/// The default rating is 230.0. +pub struct IngoRating { + /// The rating value for a player, by default 230.0. + /// Note that a lower rating is more desirable. + pub rating: f64, + /// The age of the player, if uncertain or unavailable set this to `>25`. + pub age: usize, +} + +impl IngoRating { + #[must_use] + /// Initialize a new `IngoRating` with a rating of 230.0 and the given age. + /// The age is the actual age of the player, if unsure or unavailable set this to `>25`. + pub const fn new(age: usize) -> Self { + Self { rating: 230.0, age } + } +} + +impl Default for IngoRating { + fn default() -> Self { + Self::new(26) + } +} + +impl From for IngoRating { + fn from(e: EloRating) -> Self { + Self { + rating: 355.0 - (e.rating / 8.0), + ..Default::default() + } + } +} #[must_use] /// Calculates the [`IngoRating`]s of two players based on their ratings, and the outcome of the game. @@ -60,7 +103,10 @@ use crate::{outcomes::Outcomes, rating::IngoRating}; /// /// # Examples /// ``` -/// use skillratings::{ingo::ingo, outcomes::Outcomes, rating::IngoRating}; +/// use skillratings::{ +/// ingo::{ingo, IngoRating}, +/// Outcomes, +/// }; /// /// let player_one = IngoRating { /// rating: 130.0, @@ -71,10 +117,10 @@ use crate::{outcomes::Outcomes, rating::IngoRating}; /// age: 40, /// }; /// -/// let (p1, p2) = ingo(&player_one, &player_two, &Outcomes::WIN); +/// let (new_one, new_two) = ingo(&player_one, &player_two, &Outcomes::WIN); /// -/// assert!((p1.rating.round() - 129.0).abs() < f64::EPSILON); -/// assert!((p2.rating.round() - 161.0).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 129.0).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 161.0).abs() < f64::EPSILON); /// ``` pub fn ingo( player_one: &IngoRating, @@ -116,43 +162,46 @@ pub fn ingo( /// /// # Examples /// ``` -/// use skillratings::{ingo::ingo_rating_period, rating::IngoRating, outcomes::Outcomes}; +/// use skillratings::{ +/// ingo::{ingo_rating_period, IngoRating}, +/// Outcomes, +/// }; /// -/// let player_one = IngoRating { +/// let player = IngoRating { /// rating: 130.0, /// age: 40, /// }; /// -/// let player_two = IngoRating { +/// let opponent1 = IngoRating { /// rating: 160.0, /// age: 40, /// }; /// -/// let player_three = IngoRating { +/// let opponent2 = IngoRating { /// rating: 160.0, /// age: 40, /// }; /// -/// let player_four = IngoRating { +/// let opponent3 = IngoRating { /// rating: 55.0, /// age: 40, /// }; /// -/// let player_five = IngoRating { +/// let opponent4 = IngoRating { /// rating: 90.0, /// age: 40, /// }; /// /// let results = vec![ -/// (player_two, Outcomes::WIN), -/// (player_three, Outcomes::DRAW), -/// (player_four, Outcomes::WIN), -/// (player_five, Outcomes::LOSS), +/// (opponent1, Outcomes::WIN), +/// (opponent2, Outcomes::DRAW), +/// (opponent3, Outcomes::WIN), +/// (opponent4, Outcomes::LOSS), /// ]; /// -/// let p1 = ingo_rating_period(&player_one, &results); +/// let new_player = ingo_rating_period(&player, &results); /// -/// assert!((p1.rating.round() - 126.0).abs() < f64::EPSILON); +/// assert!((new_player.rating.round() - 126.0).abs() < f64::EPSILON); /// ``` pub fn ingo_rating_period( player: &IngoRating, @@ -187,7 +236,10 @@ pub fn ingo_rating_period( /// /// # Examples /// ``` -/// use skillratings::{ingo::expected_score, outcomes::Outcomes, rating::IngoRating}; +/// use skillratings::{ +/// ingo::{expected_score, IngoRating}, +/// Outcomes, +/// }; /// /// let player_one = IngoRating { /// rating: 130.0, @@ -227,8 +279,6 @@ const fn age_to_devcoefficent(age: usize) -> f64 { #[cfg(test)] mod tests { - use crate::rating::EloRating; - use super::*; #[test] diff --git a/src/lib.rs b/src/lib.rs index 72b74b3..1c1e076 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ //! //! ```toml //! [dependencies] -//! skillratings = "0.17" +//! skillratings = "0.18" //! ``` //! //! # Usage and Examples @@ -60,7 +60,8 @@ //! //! ```rust //! use skillratings::{ -//! glicko2::glicko2, outcomes::Outcomes, rating::Glicko2Rating, config::Glicko2Config +//! glicko2::{glicko2, Glicko2Config, Glicko2Rating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -70,7 +71,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_deviation, some_volatility) = (1325.0, 230.0, 0.05932); -//! let player_two = Glicko2Rating{ +//! let player_two = Glicko2Rating { //! rating: some_rating, //! deviation: some_deviation, //! volatility: some_volatility, @@ -96,10 +97,8 @@ //! //! ```rust //! use skillratings::{ -//! trueskill::trueskill_teams, -//! outcomes::Outcomes, -//! rating::TrueSkillRating, -//! config::TrueSkillConfig, +//! trueskill::{trueskill_teams, TrueSkillConfig, TrueSkillRating}, +//! Outcomes, //! }; //! //! // We initialise Team One as a Vec of multiple TrueSkillRatings. @@ -145,14 +144,14 @@ //! This example is using *Glicko* (*not Glicko-2!*) to demonstrate. //! //! ```rust -//! use skillratings::{glicko::expected_score, rating::GlickoRating}; +//! use skillratings::glicko::{expected_score, GlickoRating}; //! //! // Initialise a new player rating. //! // The default values are: 1500.0, and 350.0. //! let player_one = GlickoRating::new(); //! //! // Initialising a new rating with custom numbers. -//! let player_two = GlickoRating{ +//! let player_two = GlickoRating { //! rating: 1812.0, //! deviation: 195.0, //! }; @@ -176,13 +175,12 @@ //! //! ```rust //! use skillratings::{ -//! elo::elo_rating_period, outcomes::Outcomes, rating::EloRating, config::EloConfig +//! elo::{elo_rating_period, EloConfig, EloRating}, +//! Outcomes, //! }; //! //! // We initialise a new Elo Rating here. -//! let player = EloRating { -//! rating: 1402.1, -//! }; +//! let player = EloRating { rating: 1402.1 }; //! //! // We need a list of results to pass to the elo_rating_period function. //! let mut results = Vec::new(); @@ -190,7 +188,7 @@ //! // And then we populate the list with tuples containing the opponent, //! // and the outcome of the match from our perspective. //! results.push((EloRating::new(), Outcomes::WIN)); -//! results.push((EloRating {rating: 954.0}, Outcomes::DRAW)); +//! results.push((EloRating { rating: 954.0 }, Outcomes::DRAW)); //! results.push((EloRating::new(), Outcomes::LOSS)); //! //! // The elo_rating_period function calculates the new rating for the player and returns it. @@ -200,7 +198,6 @@ //! assert_eq!(new_player.rating.round(), 1362.0); //! ``` -pub mod config; pub mod dwz; pub mod egf; pub mod elo; @@ -208,9 +205,36 @@ pub mod glicko; pub mod glicko2; pub mod glicko_boost; pub mod ingo; -pub mod outcomes; -pub mod rating; pub mod sticko; pub mod trueskill; pub mod uscf; pub mod weng_lin; + +/// The possible outcomes for a match: Win, Draw, Loss. +/// +/// Note that this is always from the perspective of player one. +/// That means a win is a win for player one and a loss is a win for player two. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Outcomes { + /// A win, from player_one's perspective. + WIN, + /// A loss, from player_one's perspective. + LOSS, + /// A draw. + DRAW, +} + +impl Outcomes { + #[must_use] + /// Converts the outcome of the match into the points used in chess (1 = Win, 0.5 = Draw, 0 = Loss). + /// + /// Used internally in several rating algorithms, but some, like TrueSkill, have their own conversion. + pub const fn to_chess_points(self) -> f64 { + // Could set the visibility to crate level, but maybe someone has a use for it, who knows. + match self { + Self::WIN => 1.0, + Self::DRAW => 0.5, + Self::LOSS => 0.0, + } + } +} diff --git a/src/outcomes.rs b/src/outcomes.rs deleted file mode 100644 index 6cbb786..0000000 --- a/src/outcomes.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! The possible outcomes of a match. - -/// The possible outcomes for a match: Win, Draw, Loss. -/// -/// Note that this is always from the perspective of player one. -/// That means a win is a win for player one and a loss is a win for player two. - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Outcomes { - /// A win, from player_one's perspective. - WIN, - /// A loss, from player_one's perspective. - LOSS, - /// A draw. - DRAW, -} - -impl Outcomes { - #[must_use] - /// Converts the outcome of the match into the points used in chess (1 = Win, 0.5 = Draw, 0 = Loss). - /// - /// Used internally in several rating algorithms, but some, like TrueSkill, have their own conversion. - pub const fn to_chess_points(self) -> f64 { - // Could set the visibility to crate level, but maybe someone has a use for it, who knows. - match self { - Self::WIN => 1.0, - Self::DRAW => 0.5, - Self::LOSS => 0.0, - } - } -} diff --git a/src/rating.rs b/src/rating.rs deleted file mode 100644 index b8509ec..0000000 --- a/src/rating.rs +++ /dev/null @@ -1,543 +0,0 @@ -//! Structs for initializing a player's rating for the different rating algorithms used. - -/// The Elo rating of a player. -/// -/// The default rating is 1000.0. -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct EloRating { - /// The player's Elo rating number, by default 1000.0. - pub rating: f64, -} - -impl EloRating { - /// Initialize a new `EloRating` with a rating of 1000.0. - #[must_use] - pub const fn new() -> Self { - Self { rating: 1000.0 } - } -} - -impl Default for EloRating { - fn default() -> Self { - Self::new() - } -} - -impl From for EloRating { - fn from(i: IngoRating) -> Self { - Self { - rating: 2840.0 - 8.0 * i.rating, - } - } -} - -impl From for EloRating { - fn from(d: DWZRating) -> Self { - Self { rating: d.rating } - } -} - -impl From for EloRating { - fn from(u: USCFRating) -> Self { - if u.rating > 2060.0 { - Self { - rating: (u.rating - 180.0) / 0.94, - } - } else { - Self { - rating: (u.rating - 20.0) / 1.02, - } - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The Glicko rating for a player. -/// -/// For the Glicko-2 rating, please see [`Glicko2Rating`]. -/// -/// The default rating is 1500.0. -/// The default deviation is 350.0. -pub struct GlickoRating { - /// The player's Glicko rating number, by default 1500.0. - pub rating: f64, - /// The player's Glicko deviation number, by default 350.0. - pub deviation: f64, -} - -impl GlickoRating { - #[must_use] - /// Initialize a new `GlickoRating` with a rating of 1500.0 and a deviation of 350.0. - pub const fn new() -> Self { - Self { - rating: 1500.0, - deviation: 350.0, - } - } -} - -impl Default for GlickoRating { - fn default() -> Self { - Self::new() - } -} - -impl From for GlickoRating { - fn from(g: Glicko2Rating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -impl From for GlickoRating { - fn from(g: GlickoBoostRating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -impl From for GlickoRating { - fn from(s: StickoRating) -> Self { - Self { - rating: s.rating, - deviation: s.deviation, - } - } -} - -/// The Glicko-2 rating of a player. -/// -/// For the Glicko rating, please see [`GlickoRating`]. -/// -/// The default rating is 1500.0. -/// The default deviation is 350.0. -/// The default volatility is 0.06. -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct Glicko2Rating { - /// The player's Glicko-2 rating number, by default 1500.0. - pub rating: f64, - /// The player's Glicko-2 deviation number, by default 350.0. - pub deviation: f64, - /// The player's Glicko-2 volatility number, by default 0.06. - pub volatility: f64, -} - -impl Glicko2Rating { - /// Initialize a new `Glicko2Rating` with a rating of 1500.0, a deviation of 350.0 and a volatility of 0.06. - #[must_use] - pub const fn new() -> Self { - Self { - rating: 1500.0, - deviation: 350.0, - volatility: 0.06, - } - } -} - -impl Default for Glicko2Rating { - fn default() -> Self { - Self::new() - } -} - -impl From for Glicko2Rating { - fn from(g: GlickoRating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - ..Default::default() - } - } -} - -impl From for Glicko2Rating { - fn from(g: GlickoBoostRating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - ..Default::default() - } - } -} - -impl From for Glicko2Rating { - fn from(s: StickoRating) -> Self { - Self { - rating: s.rating, - deviation: s.deviation, - ..Default::default() - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The DWZ (Deutsche Wertungszahl) rating for a player. -/// -/// The age is the actual age of the player, if unsure or unavailable set this to `>25`. -/// Converting from an `EloRating` or using `DWZRating::default()` will set the age to 26. -/// -/// The default rating is 1000.0. -pub struct DWZRating { - /// The player's DWZ rating number, by default 1000.0. - pub rating: f64, - /// The player's DWZ index, how many "events" they have completed. - pub index: usize, - /// The age of the player, if uncertain or unavailable set this to `>25`. - pub age: usize, -} - -impl DWZRating { - #[must_use] - /// Initialize a new `DWZRating` with a rating of 1000.0, an index of 1 and the specified age. - /// The age is the actual age of the player, if unsure or unavailable set this to `>25`. - pub const fn new(age: usize) -> Self { - Self { - rating: 1000.0, - index: 1, - age, - } - } -} - -impl Default for DWZRating { - fn default() -> Self { - Self::new(26) - } -} - -impl From for DWZRating { - fn from(e: EloRating) -> Self { - Self { - rating: e.rating, - // Recommended according to Wikipedia. - index: 6, - ..Default::default() - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The TrueSkill rating of a player. -/// -/// The default rating is 25.0. -/// The default uncertainty is 25/3 ≈ 8.33. -pub struct TrueSkillRating { - /// The rating value (mu) of the TrueSkilLRating, by default 25.0. - pub rating: f64, - /// The uncertainty value (sigma) of the TrueSkillRating, by default 25/3 ≈ 8.33. - pub uncertainty: f64, -} - -impl TrueSkillRating { - #[must_use] - /// Initialize a new TrueSkillRating with a rating of 25.0, and an uncertainty of 25/3 ≈ 8.33. - pub fn new() -> Self { - Self { - rating: 25.0, - uncertainty: 25.0 / 3.0, - } - } -} - -impl Default for TrueSkillRating { - fn default() -> Self { - Self::new() - } -} - -impl From for TrueSkillRating { - fn from(w: WengLinRating) -> Self { - Self { - rating: w.rating, - uncertainty: w.uncertainty, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The Ingo rating of a player. -/// -/// Note that unlike in the other systems, a lower score is better than a higher score. -/// Negative values are possible. -/// -/// The age is the actual age of the player, if unsure or unavailable set this to `>25`. -/// Converting from an `EloRating` or using `IngoRating::default()` will set the age to 26. -/// -/// The default rating is 230.0. -pub struct IngoRating { - /// The rating value for a player, by default 230.0. - /// Note that a lower rating is more desirable. - pub rating: f64, - /// The age of the player, if uncertain or unavailable set this to `>25`. - pub age: usize, -} - -impl IngoRating { - #[must_use] - /// Initialize a new `IngoRating` with a rating of 230.0 and the given age. - /// The age is the actual age of the player, if unsure or unavailable set this to `>25`. - pub const fn new(age: usize) -> Self { - Self { rating: 230.0, age } - } -} - -impl Default for IngoRating { - fn default() -> Self { - Self::new(26) - } -} - -impl From for IngoRating { - fn from(e: EloRating) -> Self { - Self { - rating: 355.0 - (e.rating / 8.0), - ..Default::default() - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The Weng-Lin rating of a player. -/// -/// Similar to [`TrueSkillRating`]. -/// -/// The default rating is 25.0. -/// The default uncertainty is 25/3 ≈ 8.33. -pub struct WengLinRating { - /// The rating value (mu) of the WengLinRating, by default 25.0. - pub rating: f64, - /// The uncertainty value (sigma) of the WengLinRating, by default 25/3 ≈ 8.33. - pub uncertainty: f64, -} - -impl WengLinRating { - #[must_use] - /// Initialize a new WengLinRating with a rating of 25.0, and an uncertainty of 25/3 ≈ 8.33. - pub fn new() -> Self { - Self { - rating: 25.0, - uncertainty: 25.0 / 3.0, - } - } -} - -impl Default for WengLinRating { - fn default() -> Self { - Self::new() - } -} - -impl From for WengLinRating { - fn from(t: TrueSkillRating) -> Self { - Self { - rating: t.rating, - uncertainty: t.uncertainty, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The Sticko rating of a player. -/// -/// Similar to [`GlickoRating`]. -/// -/// The default rating is 1500.0. -/// The default deviation is 350.0. -pub struct StickoRating { - /// The player's Sticko rating number, by default 1500.0. - pub rating: f64, - /// The player's Sticko deviation number, by default 350.0. - pub deviation: f64, -} - -impl StickoRating { - #[must_use] - /// Initialize a new `StickoRating` with a rating of 1500.0 and a deviation of 350.0. - pub const fn new() -> Self { - Self { - rating: 1500.0, - deviation: 350.0, - } - } -} - -impl Default for StickoRating { - fn default() -> Self { - Self::new() - } -} - -impl From for StickoRating { - fn from(g: GlickoRating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -impl From for StickoRating { - fn from(g: Glicko2Rating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -impl From for StickoRating { - fn from(g: GlickoBoostRating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The EGF (European Go Federation) Rating for a player. -/// -/// If the player has a Go rank or similar, -/// you can set the rating value manually approximately according to -/// [this inofficial comparison table](https://forums.online-go.com/t/go-ranks-vs-chess-ratings/41594/42). -/// Keep in mind that here, the lowest possible rating is -900.0. -/// -/// The default rating is 0.0. -pub struct EGFRating { - /// The player's EGF rating number, by default 0.0. - pub rating: f64, -} - -impl EGFRating { - #[must_use] - /// Initialize a new `EGFRating` with a rating of 0.0. - pub const fn new() -> Self { - Self { rating: 0.0 } - } -} - -impl Default for EGFRating { - fn default() -> Self { - Self::new() - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The Glicko-Boost rating of a player. -/// -/// Similar to [`GlickoRating`]. -/// -/// The default rating is 1500.0. -/// The default deviation is 350.0. -pub struct GlickoBoostRating { - /// The player's Glicko-Boost rating number, by default 1500.0. - pub rating: f64, - /// The player's Glicko-Boost deviation number, by default 350.0. - pub deviation: f64, -} - -impl GlickoBoostRating { - #[must_use] - /// Initialize a new `GlickoBoostRating` with a rating of 1500.0 and a deviation of 350.0. - pub const fn new() -> Self { - Self { - rating: 1500.0, - deviation: 350.0, - } - } -} - -impl Default for GlickoBoostRating { - fn default() -> Self { - Self::new() - } -} - -impl From for GlickoBoostRating { - fn from(g: GlickoRating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -impl From for GlickoBoostRating { - fn from(g: Glicko2Rating) -> Self { - Self { - rating: g.rating, - deviation: g.deviation, - } - } -} - -impl From for GlickoBoostRating { - fn from(s: StickoRating) -> Self { - Self { - rating: s.rating, - deviation: s.deviation, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -/// The USCF (US Chess Federation) rating for a player. -/// -/// The age is the actual age of the player, -/// if unsure or unavailable the official guidelines say to set this to `26`, -/// if the player is inferred to be an adult, or to `15` if not. -/// -/// The default rating is dependent on the age of the player. -/// If the player is 26 or older this will be 1300.0, if the player is 15 the rating will be 750.0. -/// The minimum rating value is set to be 100.0. -pub struct USCFRating { - /// The player's USCF rating number. - pub rating: f64, - /// The player's completed games. - pub games: usize, -} - -impl USCFRating { - #[must_use] - /// Initialize a new `USCFRating` with a new rating dependent on the age of the player. - /// The age is the actual age of the player, if unsure or unavailable set this to `26`. - /// The rating of a 26 year old will be 1300.0. - 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 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, - } - } - } -} diff --git a/src/sticko.rs b/src/sticko.rs index 0f7b084..eeb0224 100644 --- a/src/sticko.rs +++ b/src/sticko.rs @@ -23,7 +23,8 @@ //! //! ``` //! use skillratings::{ -//! sticko::sticko, outcomes::Outcomes, rating::StickoRating, config::StickoConfig, +//! sticko::{sticko, StickoConfig, StickoRating}, +//! Outcomes, //! }; //! //! // Initialize a new player rating. @@ -32,7 +33,7 @@ //! // Or you can initialize it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_deviation) = (1325.0, 230.0); -//! let player_two = StickoRating{ +//! let player_two = StickoRating { //! rating: some_rating, //! deviation: some_deviation, //! }; @@ -64,7 +65,130 @@ use std::f64::consts::PI; -use crate::{config::StickoConfig, outcomes::Outcomes, rating::StickoRating}; +use crate::{ + glicko::GlickoRating, glicko2::Glicko2Rating, glicko_boost::GlickoBoostRating, Outcomes, +}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The Sticko rating of a player. +/// +/// Similar to [`GlickoRating`]. +/// +/// The default rating is 1500.0. +/// The default deviation is 350.0. +pub struct StickoRating { + /// The player's Sticko rating number, by default 1500.0. + pub rating: f64, + /// The player's Sticko deviation number, by default 350.0. + pub deviation: f64, +} + +impl StickoRating { + #[must_use] + /// Initialize a new `StickoRating` with a rating of 1500.0 and a deviation of 350.0. + pub const fn new() -> Self { + Self { + rating: 1500.0, + deviation: 350.0, + } + } +} + +impl Default for StickoRating { + fn default() -> Self { + Self::new() + } +} + +impl From for StickoRating { + fn from(g: GlickoRating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +impl From for StickoRating { + fn from(g: Glicko2Rating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +impl From for StickoRating { + fn from(g: GlickoBoostRating) -> Self { + Self { + rating: g.rating, + deviation: g.deviation, + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the Sticko calculations. +/// +/// If the `h`, `beta`, `lambda` and `gamma` parameters are set to `0.0`, +/// this will behave exactly like the [`Glicko`](crate::glicko::glicko) calculations. +pub struct StickoConfig { + /// Controls player deviations across time. + /// The higher this number, the higher the deviation is going to be. + /// By default set to `10.0`. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set this to `0.0`. + /// Do not set this to a negative value. + pub h: f64, + /// A bonus parameter, which gives a rating boost for just participating. + /// Note that setting this to a positive number will create rating inflation over time. + /// By default set to `0.0`. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set this to `0.0`. + /// Do not set this to a negative value. + pub beta: f64, + /// The neighborhood parameter, which shrinks player ratings towards their opponent. + /// By default set to `2.0`. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set this to `0.0`. + /// Do not set this to a negative value. + pub lambda: f64, + /// The advantage parameter of the first player. + /// If your game is biased towards player one set this to a positive number, + /// or set this to a negative number if the second player has an advantage. + /// With this you could represent the advantage of playing white in chess, + /// or home-team advantage in sports like football and so on. + /// In chess, a value of `30.0` seems to be about correct. + /// By default set to `0.0`. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig), set this to `0.0`. + pub gamma: f64, + /// The c value describes how much the rating deviation should decay in each step. + /// The higher the value, the more the rating deviation will decay. + /// This is similar to the c value in [`GlickoConfig`](crate::glicko::GlickoConfig). + /// Keep in mind this needs to be set lower than the c in the [`GlickoConfig`](crate::glicko::GlickoConfig) + /// if the h value here is not equal to zero. + /// By default set to `10.0`. + /// If you want to mimic the [`GlickoConfig`](crate::glicko::GlickoConfig) set this to `63.2`. + pub c: f64, +} + +impl StickoConfig { + #[must_use] + /// Initialize a new `StickoConfig` with a h value of `10.0`, a beta value of `0.0`, + /// a lambda value of `2.0` and a gamma value of `0.0`. + pub const fn new() -> Self { + Self { + h: 10.0, + beta: 0.0, + lambda: 2.0, + gamma: 0.0, + c: 10.0, + } + } +} + +impl Default for StickoConfig { + fn default() -> Self { + Self::new() + } +} #[must_use] /// Calculates the [`StickoRating`]s of two players based on their old ratings, deviations, and the outcome of the game. @@ -88,7 +212,8 @@ use crate::{config::StickoConfig, outcomes::Outcomes, rating::StickoRating}; /// # Examples /// ``` /// use skillratings::{ -/// sticko::sticko, outcomes::Outcomes, rating::StickoRating, config::StickoConfig +/// sticko::{sticko, StickoConfig, StickoRating}, +/// Outcomes, /// }; /// /// let player_one = StickoRating { @@ -104,13 +229,13 @@ use crate::{config::StickoConfig, outcomes::Outcomes, rating::StickoRating}; /// /// let config = StickoConfig::new(); /// -/// let (player_one_new, player_two_new) = sticko(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = sticko(&player_one, &player_two, &outcome, &config); /// -/// assert!((player_one_new.rating.round() - 1662.0).abs() < f64::EPSILON); -/// assert!((player_one_new.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_one.rating.round() - 1662.0).abs() < f64::EPSILON); +/// assert!((new_one.deviation.round() - 290.0).abs() < f64::EPSILON); /// -/// assert!((player_two_new.rating.round() - 1338.0).abs() < f64::EPSILON); -/// assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON); +/// assert!((new_two.rating.round() - 1338.0).abs() < f64::EPSILON); +/// assert!((new_two.deviation.round() - 290.0).abs() < f64::EPSILON); /// ``` pub fn sticko( player_one: &StickoRating, @@ -217,7 +342,8 @@ pub fn sticko( /// # Examples /// ``` /// use skillratings::{ -/// sticko::sticko_rating_period, outcomes::Outcomes, rating::StickoRating, config::StickoConfig +/// sticko::{sticko_rating_period, StickoConfig, StickoRating}, +/// Outcomes, /// }; /// /// let player = StickoRating { @@ -337,7 +463,7 @@ pub fn sticko_rating_period( /// /// # Examples /// ``` -/// use skillratings::{sticko::expected_score, rating::StickoRating, config::StickoConfig}; +/// use skillratings::sticko::{expected_score, StickoConfig, StickoRating}; /// /// let player_one = StickoRating { /// rating: 1830.0, @@ -384,7 +510,7 @@ pub fn expected_score( /// /// # Examples /// ``` -/// use skillratings::{sticko::decay_deviation, rating::StickoRating, config::StickoConfig}; +/// use skillratings::sticko::{decay_deviation, StickoConfig, StickoRating}; /// /// let player_one = StickoRating { /// rating: 2720.0, @@ -414,8 +540,8 @@ pub fn decay_deviation(player: &StickoRating, config: &StickoConfig) -> StickoRa /// Takes in a player as a [`StickoRating`] and returns two [`f64`]s that describe the lowest and highest rating. /// /// # Examples -/// ```rust -/// use skillratings::{rating::StickoRating, sticko::confidence_interval}; +/// ``` +/// use skillratings::sticko::{confidence_interval, StickoRating}; /// /// let player = StickoRating { /// rating: 2250.0, @@ -457,6 +583,9 @@ fn new_rating( + lambda } +// The functions below are very similar to the normal glicko functions, +// but with the advantage parameters. + fn g_value(q: f64, opponent_deviation: f64) -> f64 { (1.0 + ((3.0 * q.powi(2) * opponent_deviation.powi(2)) / (PI.powi(2)))) .sqrt() @@ -652,8 +781,6 @@ mod tests { #[test] #[allow(clippy::similar_names)] fn sticko_glicko_conversions() { - use crate::rating::{Glicko2Rating, GlickoBoostRating, GlickoRating}; - let sticko = StickoRating::new(); let glicko_conv = GlickoRating::from(sticko); diff --git a/src/trueskill.rs b/src/trueskill.rs index e4a716f..445eee6 100644 --- a/src/trueskill.rs +++ b/src/trueskill.rs @@ -28,7 +28,8 @@ //! //! ``` //! use skillratings::{ -//! trueskill::trueskill, outcomes::Outcomes, rating::TrueSkillRating, config::TrueSkillConfig +//! trueskill::{trueskill, TrueSkillConfig, TrueSkillRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -37,7 +38,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_uncertainty) = (34.2, 2.3); -//! let player_two = TrueSkillRating{ +//! let player_two = TrueSkillRating { //! rating: some_rating, //! uncertainty: some_uncertainty, //! }; @@ -68,7 +69,86 @@ use std::f64::consts::{FRAC_1_SQRT_2, PI, SQRT_2}; -use crate::{config::TrueSkillConfig, outcomes::Outcomes, rating::TrueSkillRating}; +use crate::{weng_lin::WengLinRating, Outcomes}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The TrueSkill rating of a player. +/// +/// The default rating is 25.0. +/// The default uncertainty is 25/3 ≈ 8.33. +pub struct TrueSkillRating { + /// The rating value (mu) of the TrueSkilLRating, by default 25.0. + pub rating: f64, + /// The uncertainty value (sigma) of the TrueSkillRating, by default 25/3 ≈ 8.33. + pub uncertainty: f64, +} + +impl TrueSkillRating { + #[must_use] + /// Initialize a new TrueSkillRating with a rating of 25.0, and an uncertainty of 25/3 ≈ 8.33. + pub fn new() -> Self { + Self { + rating: 25.0, + uncertainty: 25.0 / 3.0, + } + } +} + +impl Default for TrueSkillRating { + fn default() -> Self { + Self::new() + } +} + +impl From for TrueSkillRating { + fn from(w: WengLinRating) -> Self { + Self { + rating: w.rating, + uncertainty: w.uncertainty, + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the TrueSkill calculations. +pub struct TrueSkillConfig { + /// The probability of draws occurring in match. + /// The higher the probability, the bigger the updates to the ratings in a non-drawn outcome. + /// By default set to `0.1`, meaning 10% chance of a draw. + /// Increase or decrease the value to match the values occurring in your game. + pub draw_probability: f64, + /// The skill-class width, aka the number of difference in rating points + /// needed to have an 80% win probability against another player. + /// By default set to (25 / 3) * 0.5 ≈ `4.167`. + /// If your game is more reliant on pure skill, decrease this value, + /// if there are more random factors, increase it. + pub beta: f64, + /// The additive dynamics factor. + /// It determines how easy it will be for a player to move up and down a leaderboard. + /// A larger value will tend to cause more volatility of player positions. + /// By default set to 25 / 300 ≈ `0.0833`. + pub default_dynamics: f64, +} + +impl TrueSkillConfig { + #[must_use] + /// Initialize a new `TrueSkillConfig` with a draw probability of `0.1`, + /// a beta value of `(25 / 3) * 0.5 ≈ 4.167` and a default dynamics value of 25 / 300 ≈ `0.0833`. + 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] /// Calculates the [`TrueSkillRating`]s of two players based on their old ratings, uncertainties, and the outcome of the game. /// @@ -85,7 +165,10 @@ use crate::{config::TrueSkillConfig, outcomes::Outcomes, rating::TrueSkillRating /// /// # Examples /// ``` -/// use skillratings::{rating::TrueSkillRating, trueskill::trueskill, outcomes::Outcomes, config::TrueSkillConfig}; +/// use skillratings::{ +/// trueskill::{trueskill, TrueSkillConfig, TrueSkillRating}, +/// Outcomes, +/// }; /// /// let player_one = TrueSkillRating::new(); /// let player_two = TrueSkillRating { @@ -97,13 +180,13 @@ use crate::{config::TrueSkillConfig, outcomes::Outcomes, rating::TrueSkillRating /// /// let config = TrueSkillConfig::new(); /// -/// let (player_one, player_two) = trueskill(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = trueskill(&player_one, &player_two, &outcome, &config); /// -/// assert!(((player_one.rating * 100.0).round() - 4410.0).abs() < f64::EPSILON); -/// assert!(((player_one.uncertainty * 100.0).round() - 528.0).abs() < f64::EPSILON); +/// assert!(((new_one.rating * 100.0).round() - 4410.0).abs() < f64::EPSILON); +/// assert!(((new_one.uncertainty * 100.0).round() - 528.0).abs() < f64::EPSILON); /// -/// assert!(((player_two.rating * 100.0).round() - 4960.0).abs() < f64::EPSILON); -/// assert!(((player_two.uncertainty * 100.0).round() - 121.0).abs() < f64::EPSILON); +/// assert!(((new_two.rating * 100.0).round() - 4960.0).abs() < f64::EPSILON); +/// assert!(((new_two.uncertainty * 100.0).round() - 121.0).abs() < f64::EPSILON); /// ``` pub fn trueskill( player_one: &TrueSkillRating, @@ -198,7 +281,8 @@ pub fn trueskill( /// # Examples /// ``` /// use skillratings::{ -/// rating::TrueSkillRating, trueskill::trueskill_rating_period, outcomes::Outcomes, config::TrueSkillConfig +/// trueskill::{trueskill_rating_period, TrueSkillConfig, TrueSkillRating}, +/// Outcomes, /// }; /// /// let player_one = TrueSkillRating::new(); @@ -215,7 +299,7 @@ pub fn trueskill( /// uncertainty: 1.2, /// }; /// -/// let player = trueskill_rating_period( +/// let new_player = trueskill_rating_period( /// &player_one, /// &vec![ /// (player_two, Outcomes::WIN), @@ -225,8 +309,8 @@ pub fn trueskill( /// &TrueSkillConfig::new(), /// ); /// -/// assert!(((player.rating * 100.0).round() - 3277.0).abs() < f64::EPSILON); -/// assert!(((player.uncertainty * 100.0).round() - 566.0).abs() < f64::EPSILON); +/// assert!(((new_player.rating * 100.0).round() - 3277.0).abs() < f64::EPSILON); +/// assert!(((new_player.uncertainty * 100.0).round() - 566.0).abs() < f64::EPSILON); /// ``` pub fn trueskill_rating_period( player: &TrueSkillRating, @@ -302,7 +386,8 @@ pub fn trueskill_rating_period( /// # Examples /// ``` /// use skillratings::{ -/// trueskill::trueskill_teams, rating::TrueSkillRating, outcomes::Outcomes, config::TrueSkillConfig +/// trueskill::{trueskill_teams, TrueSkillConfig, TrueSkillRating}, +/// Outcomes, /// }; /// /// let player_one = TrueSkillRating { @@ -434,7 +519,7 @@ pub fn trueskill_teams( /// /// # Examples /// ``` -/// use skillratings::{rating::TrueSkillRating, trueskill::match_quality, config::TrueSkillConfig}; +/// use skillratings::trueskill::{match_quality, TrueSkillConfig, TrueSkillRating}; /// /// let player_one = TrueSkillRating::new(); /// let player_two = TrueSkillRating::new(); @@ -481,7 +566,7 @@ pub fn match_quality( /// /// # Examples /// ``` -/// use skillratings::{rating::TrueSkillRating, trueskill::match_quality_teams, config::TrueSkillConfig}; +/// use skillratings::trueskill::{match_quality_teams, TrueSkillConfig, TrueSkillRating}; /// /// let player_one = TrueSkillRating { /// rating: 20.0, @@ -549,7 +634,7 @@ pub fn match_quality_teams( /// /// # Examples /// ``` -/// use skillratings::{rating::TrueSkillRating, trueskill::expected_score, config::TrueSkillConfig}; +/// use skillratings::trueskill::{expected_score, TrueSkillConfig, TrueSkillRating}; /// /// let better_player = TrueSkillRating { /// rating: 44.0, @@ -564,7 +649,6 @@ pub fn match_quality_teams( /// /// let (exp1, exp2) = expected_score(&better_player, &worse_player, &config); /// -/// /// // Player one has an 80% chance to win and player two a 20% chance. /// assert!((exp1 * 100.0 - 80.0).round().abs() < f64::EPSILON); /// assert!((exp2 * 100.0 - 20.0).round().abs() < f64::EPSILON); @@ -605,7 +689,7 @@ pub fn expected_score( /// /// # Examples /// ``` -/// use skillratings::{rating::TrueSkillRating, trueskill::expected_score_teams, config::TrueSkillConfig}; +/// use skillratings::trueskill::{expected_score_teams, TrueSkillConfig, TrueSkillRating}; /// /// let player_one = TrueSkillRating { /// rating: 38.0, @@ -674,7 +758,7 @@ pub fn expected_score_teams( /// /// # Example /// ``` -/// use skillratings::{rating::TrueSkillRating, trueskill::get_rank}; +/// use skillratings::trueskill::{get_rank, TrueSkillRating}; /// /// let new_player = TrueSkillRating::new(); /// let older_player = TrueSkillRating { @@ -891,10 +975,9 @@ mod tests { use std::f64::{INFINITY, NEG_INFINITY}; #[test] + /// This example is taken from this presentation (Page 20): + /// https://ubm-twvideo01.s3.amazonaws.com/o1/vault/gdc2017/Presentations/Izquierdo_Mario_Ranking_Systems_Elo.pdf fn test_trueskill() { - // This example is taken from: - // - // (Page 20) let player_one = TrueSkillRating::new(); let player_two = TrueSkillRating { rating: 30.0, diff --git a/src/uscf.rs b/src/uscf.rs index dfe2baf..ef6968c 100644 --- a/src/uscf.rs +++ b/src/uscf.rs @@ -23,7 +23,8 @@ //! //! ``` //! use skillratings::{ -//! uscf::uscf, outcomes::Outcomes, rating::USCFRating, config::USCFConfig +//! uscf::{uscf, USCFConfig, USCFRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -34,7 +35,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_games) = (1325.0, 44); -//! let player_two = USCFRating{ +//! let player_two = USCFRating { //! rating: some_rating, //! games: some_games, //! }; @@ -46,9 +47,7 @@ //! // It determines how easy or hard it is to gain bonus rating points. //! // The recommended value changes periodically, as of right now it is 14.0. //! // Here we set it to 12.0, the recommended value from 2015 to 2017. -//! let config = USCFConfig { -//! t: 12.0, -//! }; +//! let config = USCFConfig { t: 12.0 }; //! //! // The uscf function will calculate the new ratings for both players and return them. //! let (new_player_one, new_player_two) = uscf(&player_one, &player_two, &outcome, &config); @@ -62,7 +61,92 @@ //! - [USCF Calculator](https://www.uschess.org/index.php/Players-Ratings/Do-NOT-edit-CLOSE-immediately.html) //! - [Wikipedia: USCF](https://en.wikipedia.org/wiki/United_States_Chess_Federation) -use crate::{config::USCFConfig, outcomes::Outcomes, rating::USCFRating}; +use crate::{elo::EloRating, Outcomes}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The USCF (US Chess Federation) rating for a player. +/// +/// The age is the actual age of the player, +/// if unsure or unavailable the official guidelines say to set this to `26`, +/// if the player is inferred to be an adult, or to `15` if not. +/// +/// The default rating is dependent on the age of the player. +/// If the player is 26 or older this will be 1300.0, if the player is 15 the rating will be 750.0. +/// The minimum rating value is set to be 100.0. +pub struct USCFRating { + /// The player's USCF rating number. + pub rating: f64, + /// The player's completed games. + pub games: usize, +} + +impl USCFRating { + #[must_use] + /// Initialize a new `USCFRating` with a new rating dependent on the age of the player. + /// The age is the actual age of the player, if unsure or unavailable set this to `26`. + /// The rating of a 26 year old will be 1300.0. + 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 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)] +/// Constants used in the USCF Rating calculations. +pub struct USCFConfig { + /// The t value controls the difficulty of earning bonus rating points. + /// The higher the t value, the more difficult it is. + /// + /// The USCF changes this value periodically. + /// As of 2022, the last change was in May 2017 where this was set from 12 to 14. + /// The lowest value was 6, from 2008 to 2012. + /// By default set to 14.0. + pub t: f64, +} + +impl USCFConfig { + #[must_use] + /// Initialize a new `USCFConfig` with a t value of 14.0. + pub const fn new() -> Self { + Self { t: 14.0 } + } +} + +impl Default for USCFConfig { + fn default() -> Self { + Self::new() + } +} #[must_use] /// Calculates the [`USCFRating`]s of two players based on their old ratings, deviations, and the outcome of the game. @@ -81,7 +165,8 @@ use crate::{config::USCFConfig, outcomes::Outcomes, rating::USCFRating}; /// # Examples /// ``` /// use skillratings::{ -/// uscf::uscf, outcomes::Outcomes, rating::USCFRating, config::USCFConfig, +/// uscf::{uscf, USCFConfig, USCFRating}, +/// Outcomes, /// }; /// /// let player_one = USCFRating { @@ -97,13 +182,13 @@ use crate::{config::USCFConfig, outcomes::Outcomes, rating::USCFRating}; /// /// let config = USCFConfig::new(); /// -/// let (player_one_new, player_two_new) = uscf(&player_one, &player_two, &outcome, &config); +/// let (new_one, new_two) = uscf(&player_one, &player_two, &outcome, &config); /// -/// assert!((player_one_new.rating.round() - 1289.0).abs() < f64::EPSILON); -/// assert_eq!(player_one_new.games, 31); +/// assert!((new_one.rating.round() - 1289.0).abs() < f64::EPSILON); +/// assert_eq!(new_one.games, 31); /// -/// assert!((player_two_new.rating.round() - 1344.0).abs() < f64::EPSILON); -/// assert_eq!(player_two_new.games, 10); +/// assert!((new_two.rating.round() - 1344.0).abs() < f64::EPSILON); +/// assert_eq!(new_two.games, 10); /// ``` pub fn uscf( player_one: &USCFRating, @@ -186,7 +271,8 @@ pub fn uscf( /// # Examples /// ``` /// use skillratings::{ -/// uscf::uscf_rating_period, outcomes::Outcomes, rating::USCFRating, config::USCFConfig +/// uscf::{uscf_rating_period, USCFConfig, USCFRating}, +/// Outcomes, /// }; /// /// let player = USCFRating { @@ -196,7 +282,7 @@ pub fn uscf( /// /// let opponent1 = USCFRating { /// rating: 1400.0, -/// games: 25, +/// games: 25, /// }; /// /// let opponent2 = USCFRating { @@ -291,7 +377,7 @@ pub fn uscf_rating_period( /// /// # Examples /// ``` -/// use skillratings::{uscf::expected_score, rating::USCFRating}; +/// use skillratings::uscf::{expected_score, USCFRating}; /// /// let player_one = USCFRating { /// rating: 1800.0, @@ -349,7 +435,6 @@ fn e_value(rating: f64, opponent_rating: f64) -> f64 { (10_f64.powf(-(rating - opponent_rating) / 400.0) + 1.0).recip() } -/// Calculates the effective game number of a player. fn effective_game_number(rating: f64, past_games: usize) -> f64 { if rating < 2355.0 { 50.0 / 0.000_007_39f64 @@ -378,8 +463,6 @@ fn get_boost_value(played_games: usize, k: f64, e: f64, score: f64, t: f64) -> f #[cfg(test)] mod tests { - use crate::rating::EloRating; - use super::*; #[test] diff --git a/src/weng_lin.rs b/src/weng_lin.rs index 8242fc4..9f3ee20 100644 --- a/src/weng_lin.rs +++ b/src/weng_lin.rs @@ -18,7 +18,8 @@ //! //! ``` //! use skillratings::{ -//! weng_lin::weng_lin, outcomes::Outcomes, rating::WengLinRating, config::WengLinConfig +//! weng_lin::{weng_lin, WengLinConfig, WengLinRating}, +//! Outcomes, //! }; //! //! // Initialise a new player rating. @@ -27,7 +28,7 @@ //! // Or you can initialise it with your own values of course. //! // Imagine these numbers being pulled from a database. //! let (some_rating, some_uncertainty) = (41.2, 2.12); -//! let player_two = WengLinRating{ +//! let player_two = WengLinRating { //! rating: some_rating, //! uncertainty: some_uncertainty, //! }; @@ -56,7 +57,81 @@ //! - [Approximate Bayesian computaion Wikipedia](https://en.wikipedia.org/wiki/Approximate_Bayesian_computation) //! - [Logistic distribution Wikipedia](https://en.wikipedia.org/wiki/Logistic_distribution) -use crate::{config::WengLinConfig, outcomes::Outcomes, rating::WengLinRating}; +use crate::{trueskill::TrueSkillRating, Outcomes}; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// The Weng-Lin rating of a player. +/// +/// Similar to [`TrueSkillRating`]. +/// +/// The default rating is 25.0. +/// The default uncertainty is 25/3 ≈ 8.33. +pub struct WengLinRating { + /// The rating value (mu) of the WengLinRating, by default 25.0. + pub rating: f64, + /// The uncertainty value (sigma) of the WengLinRating, by default 25/3 ≈ 8.33. + pub uncertainty: f64, +} + +impl WengLinRating { + #[must_use] + /// Initialize a new WengLinRating with a rating of 25.0, and an uncertainty of 25/3 ≈ 8.33. + pub fn new() -> Self { + Self { + rating: 25.0, + uncertainty: 25.0 / 3.0, + } + } +} + +impl Default for WengLinRating { + fn default() -> Self { + Self::new() + } +} + +impl From for WengLinRating { + fn from(t: TrueSkillRating) -> Self { + Self { + rating: t.rating, + uncertainty: t.uncertainty, + } + } +} + +#[derive(Clone, Copy, Debug)] +/// Constants used in the Weng-Lin calculations. +pub struct WengLinConfig { + /// The skill-class width, aka the number of difference in rating points + /// needed to have an 80% win probability against another player. + /// By default set to 25 / 6 ≈ `4.167`. + /// If your game is more reliant on pure skill, decrease this value, + /// if there are more random factors, increase it. + pub beta: f64, + /// The lower ceiling of the sigma value, in the uncertainty calculations. + /// The lower this value, the lower the possible uncertainty values. + /// By default set to 0.000_001. + /// Do not set this to a negative value. + pub uncertainty_tolerance: f64, +} + +impl WengLinConfig { + #[must_use] + /// Initialize a new `WengLinConfig` with a beta value of 25 / 6 ≈ `4.167` + /// and an uncertainty tolerance of `0.000_001`. + pub fn new() -> Self { + Self { + beta: 25.0 / 6.0, + uncertainty_tolerance: 0.000_001, + } + } +} + +impl Default for WengLinConfig { + fn default() -> Self { + Self::new() + } +} #[must_use] #[allow(clippy::needless_pass_by_value)] @@ -72,7 +147,8 @@ use crate::{config::WengLinConfig, outcomes::Outcomes, rating::WengLinRating}; /// # Examples /// ``` /// use skillratings::{ -/// rating::WengLinRating, weng_lin::weng_lin, outcomes::Outcomes, config::WengLinConfig +/// weng_lin::{weng_lin, WengLinConfig, WengLinRating}, +/// Outcomes, /// }; /// /// let player_one = WengLinRating { @@ -81,12 +157,17 @@ use crate::{config::WengLinConfig, outcomes::Outcomes, rating::WengLinRating}; /// }; /// let player_two = WengLinRating::new(); /// -/// let (player_one, player_two) = weng_lin(&player_one, &player_two, &Outcomes::WIN, &WengLinConfig::new()); +/// let (new_one, new_two) = weng_lin( +/// &player_one, +/// &player_two, +/// &Outcomes::WIN, +/// &WengLinConfig::new(), +/// ); /// -/// assert!(((player_one.rating * 100.0).round() - 4203.0).abs() < f64::EPSILON); -/// assert!(((player_one.uncertainty * 100.0).round() - 130.0).abs() < f64::EPSILON); -/// assert!(((player_two.rating * 100.0).round() - 2391.0).abs() < f64::EPSILON); -/// assert!(((player_two.uncertainty * 100.0).round() - 803.0).abs() < f64::EPSILON); +/// assert!(((new_one.rating * 100.0).round() - 4203.0).abs() < f64::EPSILON); +/// assert!(((new_one.uncertainty * 100.0).round() - 130.0).abs() < f64::EPSILON); +/// assert!(((new_two.rating * 100.0).round() - 2391.0).abs() < f64::EPSILON); +/// assert!(((new_two.uncertainty * 100.0).round() - 803.0).abs() < f64::EPSILON); /// ``` pub fn weng_lin( player_one: &WengLinRating, @@ -153,7 +234,8 @@ pub fn weng_lin( /// # Examples /// ``` /// use skillratings::{ -/// rating::WengLinRating, weng_lin::weng_lin_rating_period, outcomes::Outcomes, config::WengLinConfig +/// weng_lin::{weng_lin_rating_period, WengLinConfig, WengLinRating}, +/// Outcomes, /// }; /// /// let player = WengLinRating::new(); @@ -164,7 +246,7 @@ pub fn weng_lin( /// uncertainty: 4.2, /// }; /// -/// let player = weng_lin_rating_period( +/// let new_player = weng_lin_rating_period( /// &player, /// &vec![ /// (opponent_one, Outcomes::WIN), @@ -173,8 +255,8 @@ pub fn weng_lin( /// &WengLinConfig::new(), /// ); /// -/// assert!(((player.rating * 100.0).round() - 2578.0).abs() < f64::EPSILON); -/// assert!(((player.uncertainty * 100.0).round() - 780.0).abs() < f64::EPSILON); +/// assert!(((new_player.rating * 100.0).round() - 2578.0).abs() < f64::EPSILON); +/// assert!(((new_player.uncertainty * 100.0).round() - 780.0).abs() < f64::EPSILON); /// ``` pub fn weng_lin_rating_period( player: &WengLinRating, @@ -227,7 +309,8 @@ pub fn weng_lin_rating_period( /// # Examples /// ``` /// use skillratings::{ -/// rating::WengLinRating, weng_lin::weng_lin_teams, outcomes::Outcomes, config::WengLinConfig +/// weng_lin::{weng_lin_teams, WengLinConfig, WengLinRating}, +/// Outcomes, /// }; /// /// let team_one = vec![ @@ -254,16 +337,16 @@ pub fn weng_lin_rating_period( /// }, /// ]; /// -/// let (team_one, team_two) = +/// let (new_one, new_two) = /// weng_lin_teams(&team_one, &team_two, &Outcomes::WIN, &WengLinConfig::new()); /// -/// assert!(((team_one[0].rating * 100.0).round() - 2790.0).abs() < f64::EPSILON); -/// assert!(((team_one[1].rating * 100.0).round() - 3006.0).abs() < f64::EPSILON); -/// assert!(((team_one[2].rating * 100.0).round() - 2277.0).abs() < f64::EPSILON); +/// assert!(((new_one[0].rating * 100.0).round() - 2790.0).abs() < f64::EPSILON); +/// assert!(((new_one[1].rating * 100.0).round() - 3006.0).abs() < f64::EPSILON); +/// assert!(((new_one[2].rating * 100.0).round() - 2277.0).abs() < f64::EPSILON); /// -/// assert!(((team_two[0].rating * 100.0).round() - 2210.0).abs() < f64::EPSILON); -/// assert!(((team_two[1].rating * 100.0).round() - 4092.0).abs() < f64::EPSILON); -/// assert!(((team_two[2].rating * 100.0).round() - 1843.0).abs() < f64::EPSILON); +/// assert!(((new_two[0].rating * 100.0).round() - 2210.0).abs() < f64::EPSILON); +/// assert!(((new_two[1].rating * 100.0).round() - 4092.0).abs() < f64::EPSILON); +/// assert!(((new_two[2].rating * 100.0).round() - 1843.0).abs() < f64::EPSILON); /// ``` pub fn weng_lin_teams( team_one: &Vec, @@ -357,9 +440,7 @@ pub fn weng_lin_teams( /// /// # Examples /// ``` -/// use skillratings::{ -/// rating::WengLinRating, weng_lin::expected_score, config::WengLinConfig -/// }; +/// use skillratings::weng_lin::{expected_score, WengLinConfig, WengLinRating}; /// /// let p1 = WengLinRating { /// rating: 42.0, @@ -413,9 +494,7 @@ pub fn expected_score( /// /// # Examples /// ``` -/// use skillratings::{ -/// rating::WengLinRating, weng_lin::expected_score_teams, config::WengLinConfig -/// }; +/// use skillratings::weng_lin::{expected_score_teams, WengLinConfig, WengLinRating}; /// /// let team_one = vec![ /// WengLinRating { @@ -427,7 +506,6 @@ pub fn expected_score( /// rating: 12.0, /// uncertainty: 3.2, /// }, -/// /// ]; /// let team_two = vec![ /// WengLinRating { @@ -789,8 +867,6 @@ mod tests { #[test] fn trueskill_conversion() { - use crate::rating::TrueSkillRating; - let weng_lin_player = WengLinRating::new(); let trueskill_player = TrueSkillRating::from(weng_lin_player);