Skip to content

Commit

Permalink
Add Weng-Lin calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
atomflunder committed Aug 26, 2022
1 parent cbfa35c commit cfa3a7f
Show file tree
Hide file tree
Showing 13 changed files with 816 additions and 53 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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

## v0.12.0 - 2022-08-26

- Add Weng-Lin (A Bayesian Approximation Method for Online Ranking) calculations
- Return original teams when a team is empty in `trueskill::trueskill_teams`

## v0.11.0 - 2022-08-26

- Add `new` and `default` implementations for `DWZRating`
Expand Down
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "skillratings"
version = "0.11.0"
version = "0.12.0"
edition = "2021"
description = "Calculate a player's skill rating using Elo, DWZ, Ingo, TrueSkill, Glicko and Glicko-2 algorithms."
description = "Calculate a player's skill rating using algorithms like Elo, Glicko, Glicko-2, TrueSkill and many more."
readme= "README.md"
repository = "https://github.com/atomflunder/skillratings"
license = "MIT"
keywords = ["elo", "glicko-2", "glicko", "dwz", "trueskill"]
keywords = ["elo", "glicko", "glicko-2", "trueskill", "rating"]
categories = ["game-development", "algorithms", "mathematics"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Currently supported algorithms:
- [Glicko](#glicko-rating-system)
- [Glicko-2](#glicko-2-rating-system)
- [TrueSkill](#trueskill-rating-system)
- [Weng-Lin (Bayesian Approxmation Method)](#weng-lin-rating-system)
- [DWZ (Deutsche Wertungszahl)](#dwz-deutsche-wertungszahl-rating-system)
- [Ingo](#ingo-rating-system)

Expand All @@ -24,7 +25,7 @@ Add the following to your `Cargo.toml` file:

```toml
[dependencies]
skillratings = "0.11.0"
skillratings = "0.12.0"
```

## Basic Usage
Expand Down Expand Up @@ -171,6 +172,41 @@ assert!(((p2.rating * 100.0).round() - 2983.0).abs() < f64::EPSILON);
assert!(((p2.uncertainty * 100.0).round() - 120.0).abs() < f64::EPSILON);
```

## Weng-Lin rating system

(A Bayesian Approximation Method for Online Ranking)

- [Documentation](https://docs.rs/skillratings/latest/skillratings/weng_lin/index.html)
- [Original Paper (PDF)](https://jmlr.csail.mit.edu/papers/volume12/weng11a/weng11a.pdf)

```rust
use skillratings::{
rating::WengLinRating, weng_lin::weng_lin, outcomes::Outcomes, config::WengLinConfig
};

let player_one = WengLinRating {
rating: 42.0,
uncertainty: 1.3,
};
let player_two = WengLinRating {
rating: 25.0,
uncertainty: 8.333,
};

// The config allows you to change certain adjustable values in the algorithms.
let config = WengLinConfig::new();

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

let (player_one, player_two) = weng_lin(player_one, player_two, outcome, &config);

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);
```

### DWZ (Deutsche Wertungszahl) rating system

- [Documentation](https://docs.rs/skillratings/latest/skillratings/dwz/index.html)
Expand Down
33 changes: 33 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,36 @@ impl Default for TrueSkillConfig {
Self::new()
}
}

/// Constants used in the Weng 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 `WengConfig` 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()
}
}
6 changes: 0 additions & 6 deletions src/dwz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ use crate::{outcomes::Outcomes, rating::DWZRating};
/// assert!((player_two_new.rating.round() - 1464.0).abs() < f64::EPSILON);
/// assert_eq!(player_two_new.index, 13);
/// ```
///
/// # More
/// [Wikipedia Article on DWZ](https://en.wikipedia.org/wiki/Deutsche_Wertungszahl)
pub fn dwz(
player_one: DWZRating,
player_two: DWZRating,
Expand Down Expand Up @@ -285,9 +282,6 @@ pub fn expected_score(player_one: DWZRating, player_two: DWZRating) -> (f64, f64
/// assert!((player.rating - 1491.0).abs() < f64::EPSILON);
/// assert_eq!(player.index, 1);
/// ```
///
/// # More
/// [Probability Table](https://www.schachbund.de/wertungsordnung-anhang-2-tabellen/articles/wertungsordnung-anhang-21-wahrscheinlichkeitstabelle.html)
pub fn get_first_dwz(player_age: usize, results: &Vec<(DWZRating, Outcomes)>) -> Option<DWZRating> {
if results.len() < 5 {
return None;
Expand Down
4 changes: 0 additions & 4 deletions src/elo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ use crate::{config::EloConfig, outcomes::Outcomes, rating::EloRating};
/// assert!((player_one_new.rating - 1016.0).abs() < f64::EPSILON);
/// assert!((player_two_new.rating - 984.0).abs() < f64::EPSILON);
/// ```
///
/// # More
/// [Wikipedia Article on the Elo system](https://en.wikipedia.org/wiki/Elo_rating_system)
/// [Elo Calculator](https://www.omnicalculator.com/sports/elo)
#[must_use]
pub fn elo(
player_one: EloRating,
Expand Down
4 changes: 0 additions & 4 deletions src/glicko.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ use std::f64::consts::PI;
/// assert!((player_two_new.rating.round() - 1338.0).abs() < f64::EPSILON);
/// assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON);
/// ```
///
/// # More
/// [Wikipedia Article on the Glicko system](https://en.wikipedia.org/wiki/Glicko_rating_system).
/// [Example of the Glicko system](http://www.glicko.net/glicko/glicko.pdf).
pub fn glicko(
player_one: GlickoRating,
player_two: GlickoRating,
Expand Down
4 changes: 0 additions & 4 deletions src/glicko2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ use std::f64::consts::PI;
/// assert!((player_two_new.deviation.round() - 290.0).abs() < f64::EPSILON);
/// assert!((player_two_new.volatility - 0.05999578094735206).abs() < f64::EPSILON);
/// ```
///
/// # More
/// [Wikipedia Article on the Glicko-2 system](https://en.wikipedia.org/wiki/Glicko-2).
/// [Example of the Glicko-2 system](http://www.glicko.net/glicko/glicko2.pdf).
#[must_use]
pub fn glicko2(
player_one: Glicko2Rating,
Expand Down
3 changes: 0 additions & 3 deletions src/ingo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ use crate::{outcomes::Outcomes, rating::IngoRating};
/// assert!((p1.rating.round() - 129.0).abs() < f64::EPSILON);
/// assert!((p2.rating.round() - 161.0).abs() < f64::EPSILON);
/// ```
///
/// # More:
/// [Wikipedia Article on the Ingo system (in german, no english version available)](https://de.wikipedia.org/wiki/Ingo-Zahl).
pub fn ingo(
player_one: IngoRating,
player_two: IngoRating,
Expand Down
12 changes: 7 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
//!
//! Currently we support these skill rating systems:
//! **[`Elo`](crate::elo)**,
//! **[`DWZ`](crate::dwz)**,
//! **[`Ingo`](crate::ingo)**,
//! **[`Glicko`](crate::glicko)**,
//! **[`Glicko-2`](crate::glicko2)**,
//! **[`TrueSkill`](crate::trueskill)**,
//! **[`Glicko`](crate::glicko)**
//! and **[`Glicko-2`](crate::glicko2)**.
//! **[`Weng-Lin`](crate::weng_lin)**,
//! **[`DWZ (Deutsche Wertungszahl)`](crate::dwz)**,
//! and **[`Ingo`](crate::ingo)**.
//!
//! You can use this crate to calculate results for two players instantly,
//! or for one player in a rating period with the algorithms mentioned above.
Expand All @@ -28,7 +29,7 @@
//! Add the following to your `Cargo.toml` file:
//! ```toml
//! [dependencies]
//! skillratings = "0.11.0"
//! skillratings = "0.12.0"
//! ```
//!
//! # Examples and Usage
Expand All @@ -44,3 +45,4 @@ pub mod ingo;
pub mod outcomes;
pub mod rating;
pub mod trueskill;
pub mod weng_lin;
33 changes: 32 additions & 1 deletion src/rating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub struct TrueSkillRating {

impl TrueSkillRating {
#[must_use]
/// Initialize a new `TrueSkillRating` with a rating of 25.0, and an uncertainty of 25/3 ≈ 8.33.
/// 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,
Expand Down Expand Up @@ -212,3 +212,34 @@ impl From<EloRating> for IngoRating {
}
}
}

#[derive(Copy, Clone, Debug, PartialEq)]
/// The Weng 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 WengRating, by default 25.0.
pub rating: f64,
/// The uncertainty value (sigma) of the WengRating, by default 25/3 ≈ 8.33.
pub uncertainty: f64,
}

impl WengLinRating {
#[must_use]
/// Initialize a new WengRating 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()
}
}
39 changes: 17 additions & 22 deletions src/trueskill.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! The TrueSkill rating algorithm, developed by Microsoft for Halo 3.
//! Used in the Halo games, the Forza Games, Tom Clancy's: Rainbow Six Siege, and most Xbox Live games.
//! Unlike the other rating algorithms, TrueSkill supports teams.
//! Used in the Halo games, the Forza Games, Tom Clancy's: Rainbow Six Siege, and most Xbox Live games.
//!
//! Developed specifically for online games with multiple teams and multiple players.
//!
//! **Caution:** TrueSkill is patented. If you have a commercial project, it is recommended to use another algorithm included here.
//!
Expand Down Expand Up @@ -65,11 +66,6 @@ use crate::{config::TrueSkillConfig, outcomes::Outcomes, rating::TrueSkillRating
/// assert!(((player_two.rating * 100.0).round() - 4960.0).abs() < f64::EPSILON);
/// assert!(((player_two.uncertainty * 100.0).round() - 121.0).abs() < f64::EPSILON);
/// ```
///
/// # More:
/// [Wikipedia Article about TrueSkill](https://en.wikipedia.org/wiki/TrueSkill).
/// [TrueSkill: A Bayesian Skill Rating System (PDF)](https://proceedings.neurips.cc/paper/2006/file/f44ee263952e65b3610b8ba51229d1f9-Paper.pdf).
/// [The math behind TrueSkill (PDF)](http://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf).
pub fn trueskill(
player_one: TrueSkillRating,
player_two: TrueSkillRating,
Expand Down Expand Up @@ -191,10 +187,6 @@ pub fn trueskill(
/// assert!(((player.rating * 100.0).round() - 3277.0).abs() < f64::EPSILON);
/// assert!(((player.uncertainty * 100.0).round() - 566.0).abs() < f64::EPSILON);
/// ```
/// # More:
/// [Wikipedia Article about TrueSkill](https://en.wikipedia.org/wiki/TrueSkill).
/// [TrueSkill: A Bayesian Skill Rating System (PDF)](https://proceedings.neurips.cc/paper/2006/file/f44ee263952e65b3610b8ba51229d1f9-Paper.pdf).
/// [The math behind TrueSkill (PDF)](http://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf).
pub fn trueskill_rating_period(
player: TrueSkillRating,
results: &Vec<(TrueSkillRating, Outcomes)>,
Expand Down Expand Up @@ -301,17 +293,16 @@ pub fn trueskill_rating_period(
/// assert!((team_two[0].rating - 27.574_109_105_332_1).abs() < f64::EPSILON);
/// assert!((team_two[1].rating - 36.210_764_756_738_115).abs() < f64::EPSILON);
/// ```
///
/// # More:
/// [Wikipedia Article about TrueSkill](https://en.wikipedia.org/wiki/TrueSkill).
/// [TrueSkill: A Bayesian Skill Rating System (PDF)](https://proceedings.neurips.cc/paper/2006/file/f44ee263952e65b3610b8ba51229d1f9-Paper.pdf).
/// [The math behind TrueSkill (PDF)](http://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf).
pub fn trueskill_teams(
team_one: Vec<TrueSkillRating>,
team_two: Vec<TrueSkillRating>,
outcome: Outcomes,
config: &TrueSkillConfig,
) -> (Vec<TrueSkillRating>, Vec<TrueSkillRating>) {
if team_one.is_empty() || team_two.is_empty() {
return (team_one, team_two);
}

let total_players = (team_one.len() + team_two.len()) as f64;

let draw_margin = draw_margin(config.draw_probability, config.beta, total_players);
Expand Down Expand Up @@ -524,9 +515,6 @@ pub fn match_quality_teams(
///
/// assert!((exp1.mul_add(100.0, exp2 * 100.0).round() - 100.0).abs() < f64::EPSILON);
/// ```
///
/// # More
/// <http://www.moserware.com/2010/03/computing-your-skill.html>
pub fn expected_score(
player_one: TrueSkillRating,
player_two: TrueSkillRating,
Expand Down Expand Up @@ -606,9 +594,6 @@ pub fn expected_score(
/// assert!(((exp1 * 100.0).round() - 12.0).abs() < f64::EPSILON);
/// assert!(((exp2 * 100.0).round() - 88.0).abs() < f64::EPSILON);
/// ```
///
/// # More
/// <http://www.moserware.com/2010/03/computing-your-skill.html>
pub fn expected_score_teams(
team_one: Vec<TrueSkillRating>,
team_two: Vec<TrueSkillRating>,
Expand Down Expand Up @@ -1060,6 +1045,7 @@ mod tests {
}

#[test]
#[allow(clippy::cognitive_complexity)]
/// This test is taken from:
/// <https://github.com/moserware/Skills/blob/master/UnitTests/TrueSkill/TrueSkillCalculatorTests.cs>
fn test_teams() {
Expand Down Expand Up @@ -1149,6 +1135,15 @@ mod tests {
assert!((team_one[1].uncertainty - 5.417_723_612_401_869).abs() < f64::EPSILON);
assert!((team_two[0].uncertainty - 3.832_975_356_683_128).abs() < f64::EPSILON);
assert!((team_two[1].uncertainty - 2.930_957_525_591_959_5).abs() < f64::EPSILON);

let (team_one, _) = trueskill_teams(
vec![player_one],
vec![],
Outcomes::WIN,
&TrueSkillConfig::new(),
);

assert_eq!(team_one[0], player_one);
}

#[test]
Expand Down
Loading

0 comments on commit cfa3a7f

Please sign in to comment.