Skip to content

Commit

Permalink
Add config structs
Browse files Browse the repository at this point in the history
  • Loading branch information
atomflunder committed Aug 18, 2022
1 parent 3cb6b7b commit 1e511ea
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 156 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This is a broad overview of the changes that have been made over the lifespan of this library.

## v0.8.0 - 2022-08-19

- Add config structs: `EloConfig`, `GlickoConfig`, `Glicko2Config`, `TrueSkillConfig`
- These allow you to change some values used in the algorithm to further customise the behavior
- The following functions require a config now: `elo::elo`, `glicko::decay_deviation`, `glicko2::glicko2`, `trueskill::trueskill`, `trueskill::match_quality`, `trueskill::expected_score`
- Fix some spelling issues

## v0.7.2 - 2022-08-18

- Implement eq for Outcomes
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "skillratings"
version = "0.7.2"
version = "0.8.0"
edition = "2021"
description = "Calculate a player's skill rating in 1v1 matches instantly using Elo, DWZ, Ingo, TrueSkill, Glicko and Glicko-2 algorithms."
readme= "README.md"
Expand Down
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Add the following to your `Cargo.toml` file:

```toml
[dependencies]
skillratings = "0.7.2"
skillratings = "0.8.0"
```

## Usage
Expand All @@ -40,15 +40,20 @@ For a detailed guide on how to use this crate, head over [to the documentation](
```rust
extern crate skillratings;

use skillratings::{elo::elo, outcomes::Outcomes, rating::EloRating};
use skillratings::{
elo::elo, outcomes::Outcomes, rating::EloRating, config::EloConfig
};

let player_one = EloRating { rating: 1000.0 };
let player_two = EloRating { rating: 1000.0 };

// The outcome is from the perspective of player one.
let outcome = Outcomes::WIN;

let (player_one_new, player_two_new) = elo(player_one, player_two, outcome, 32.0);
// The config allows you to change certain adjustable values in the algorithms.
let config = EloConfig::new();

let (player_one_new, player_two_new) = elo(player_one, player_two, outcome, &config);
assert!((player_one_new.rating - 1016.0).abs() < f64::EPSILON);
assert!((player_two_new.rating - 984.0).abs() < f64::EPSILON);
```
Expand All @@ -58,7 +63,10 @@ assert!((player_two_new.rating - 984.0).abs() < f64::EPSILON);
[Wikipedia article](https://en.wikipedia.org/wiki/Glicko_rating_system)

```rust
use skillratings::{glicko::glicko, outcomes::Outcomes, rating::GlickoRating};
use skillratings::{
config::Glicko2Config, glicko::glicko, outcomes::Outcomes, rating::GlickoRating,
};


let player_one = GlickoRating {
rating: 1500.0,
Expand All @@ -71,7 +79,10 @@ let player_two = GlickoRating {

let outcome = Outcomes::WIN;

let (player_one_new, player_two_new) = glicko(player_one, player_two, outcome);
// The config allows you to change certain adjustable values in the algorithms.
let config = GlickoConfig::new();

let (player_one_new, player_two_new) = glicko(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);
Expand All @@ -87,7 +98,9 @@ assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON);
```rust
extern crate skillratings;

use skillratings::{glicko2::glicko2, outcomes::Outcomes, rating::Glicko2Rating};
use skillratings::{
glicko2::glicko2, outcomes::Outcomes, rating::Glicko2Rating, config::Glicko2Config
};

let player_one = Glicko2Rating {
rating: 1500.0,
Expand All @@ -102,7 +115,10 @@ let player_two = Glicko2Rating {

let outcome = Outcomes::WIN;

let (player_one_new, player_two_new) = glicko2(player_one, player_two, outcome, 0.5);
// The config allows you to change certain adjustable values in the algorithms.
let config = Glicko2Config::new();

let (player_one_new, player_two_new) = 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);
Expand All @@ -120,16 +136,20 @@ Microsoft permits only Xbox Live games or non-commercial projects to use TrueSki
If your project is commercial, you should use another rating system included here.

```rust
use skillratings::{trueskill::trueskill, outcomes::Outcomes, rating::TrueSkillRating};
use skillratings::{
trueskill::trueskill, outcomes::Outcomes, rating::TrueSkillRating, config::TrueSkillConfig
};

// Initialises a player with `rating` set to 25.0 and `uncertainty` set to (25.0 / 3).
let player_one = TrueSkillRating::new();
let player_two = TrueSkillRating {
rating: 30.0,
uncertainty: 1.2,
};

let (p1, p2) = trueskill(player_one, player_two, Outcomes::WIN);
// The config allows you to change certain adjustable values in the algorithms.
let config = TrueSkillConfig::new();

let (p1, p2) = trueskill(player_one, player_two, Outcomes::WIN, &config);

assert!(((p1.rating * 100.0).round() - 3300.0).abs() < f64::EPSILON);
assert!(((p1.uncertainty * 100.0).round() - 597.0).abs() < f64::EPSILON);
Expand All @@ -148,6 +168,8 @@ use skillratings::{dwz::dwz, outcomes::Outcomes, rating::DWZRating};
let player_one = DWZRating {
rating: 1500.0,
index: 42,
// The actual age of the player, if unavailable set this to >25.
// The lower the age, the more the rating will fluctuate.
age: 42,
};
let player_two = DWZRating {
Expand Down Expand Up @@ -177,6 +199,8 @@ use skillratings::{ingo::ingo, outcomes::Outcomes, rating::IngoRating};
let player_one = IngoRating {
// Note that a lower rating is more desirable.
rating: 130.0,
// The actual age of the player, if unavailable set this to >25.
// The lower the age, the more the rating will fluctuate.
age: 40,
};
let player_two = IngoRating {
Expand Down
114 changes: 114 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/// Constants used in the `Elo` calculation.
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 or 16.
/// 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()
}
}

/// Constants used in the `Glicko` calculation.
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()
}
}

/// Constants used in the `Glicko-2` calculation.
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()
}
}

/// Constants used in the `TrueSkill` calculation.
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()
}
}
14 changes: 8 additions & 6 deletions src/dwz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ pub fn expected_score(player_one: DWZRating, player_two: DWZRating) -> (f64, f64
/// If the actual player's age is unavailable or unknown, choose something `>25`.
///
/// This only returns a DWZ rating if the results include at least 5 matches,
/// and you dont have a 100% or a 0% win record. Otherwise it will return `None`.
/// and you don't have a 100% or a 0% win record. Otherwise it will return `None`.
///
/// # Example
/// ```
Expand Down Expand Up @@ -207,6 +207,7 @@ pub fn get_first_dwz(player_age: usize, results: &Vec<(DWZRating, Outcomes)>) ->
})
.sum::<f64>();

// If you have a 100% or 0% win rate, we return None.
if (points - results.len() as f64).abs() < f64::EPSILON || points == 0.0 {
return None;
}
Expand Down Expand Up @@ -294,16 +295,13 @@ pub fn get_first_dwz(player_age: usize, results: &Vec<(DWZRating, Outcomes)>) ->
})
}

/// Getting the fundamental e0 value.
///
/// 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`"
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`"
let j = match age {
usize::MIN..=20 => 5.0,
21..=25 => 10.0,
Expand All @@ -312,18 +310,22 @@ fn e_value(rating: f64, age: usize, score: f64, expected_score: f64, index: usiz

let e0 = e0_value(rating, j);

// The acceleration factor allows young, overachieving players to gain rating more quickly.
let a = if age < 20 && score >= expected_score {
rating / 2000.0
} else {
1.0
};

// The breaking value is applied to weak players that underperform in order to not decrease in rating too rapidly.
let b = if rating < 1300.0 && score <= expected_score {
((1300.0 - rating) / 150.0_f64).exp_m1()
} else {
0.0
};

// The development coefficient combines the acceleration and breaking values.
// It also depends on the number of entered tournaments (index).
let mut e = a.mul_add(e0, b);

if e <= 5.0 {
Expand Down
Loading

0 comments on commit 1e511ea

Please sign in to comment.