Skip to content

Commit

Permalink
Add some docs, tests and missing function
Browse files Browse the repository at this point in the history
  • Loading branch information
atomflunder committed Jun 8, 2024
1 parent 287559f commit ff2c293
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/dwz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ pub fn expected_score_rating_period(player: &DWZRating, opponents: &[DWZRating])
/// 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::elo::EloRating)
/// consider using [`DWZRating::from()`](DWZRating) if you have an [`EloRating`]
/// or [`DWZRating::new()`](DWZRating) if not.
///
/// Takes in the player's age and their results as a Slice of tuples containing the opponent and the outcome.
Expand Down
129 changes: 124 additions & 5 deletions src/trueskill/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
//!
//! | Match type | Matches needed |
//! | ---------- | -------------- |
//! | 8 Players Free-For-All | 3 |
//! | 4 Players Free-For-All | 5 |
//! | 4 Teams / 2 Players each | 10 |
//! | 1 Player vs 1 Player | 12 |
//! | 4 Teams / 4 Players each | 20 |
//! | 4 Players vs 4 Players | 46 |
//! | 8 Players vs 8 Players | 91 |
//!
Expand Down Expand Up @@ -297,10 +301,10 @@ impl MultiTeamRatingSystem for TrueSkill {
/// The outcome of the match is in the perspective of `player_one`.
/// This means [`Outcomes::WIN`] is a win for `player_one` and [`Outcomes::LOSS`] is a win for `player_two`.
///
/// Similar to [`trueskill_rating_period`], [`trueskill_two_teams`] and [`trueskill_multi_teams`].
/// Similar to [`trueskill_rating_period`], [`trueskill_two_teams`] and [`trueskill_multi_team`].
///
/// This algorithm uses some shortcuts to speed-up and simplify 1-vs-1 ratings. This is fine for 99.9% of use-cases,
/// but if you need maximum precision, consider using [`trueskill_multi_teams`].
/// but if you need maximum precision, consider using [`trueskill_multi_team`].
///
/// **Caution regarding usage of TrueSkill**:
/// Microsoft permits only Xbox Live games or non-commercial projects to use TrueSkill.
Expand Down Expand Up @@ -520,10 +524,10 @@ pub fn trueskill_rating_period(
/// The outcome of the match is in the perspective of `team_one`.
/// This means [`Outcomes::WIN`] is a win for `team_one` and [`Outcomes::LOSS`] is a win for `team_two`.
///
/// Similar to [`trueskill`] and [`trueskill_multi_teams`].
/// Similar to [`trueskill`] and [`trueskill_multi_team`].
///
/// This algorithm uses some shortcuts to speed-up and simplify Team-vs-Team ratings. This is fine for 99.9% of use-cases,
/// but if you need maximum precision, consider using [`trueskill_multi_teams`].
/// but if you need maximum precision, consider using [`trueskill_multi_team`].
///
/// **Caution regarding usage of TrueSkill**:
/// Microsoft permits only Xbox Live games or non-commercial projects to use TrueSkill(TM).
Expand Down Expand Up @@ -862,7 +866,7 @@ pub fn trueskill_multi_team(
///
/// Takes in two players as [`TrueSkillRating`]s and returns the probability of a draw occurring as an [`f64`] between 1.0 and 0.0.
///
/// Similar to [`match_quality_two_teams`] and [`match_quality_multi_team`].
/// Similar to [`match_quality_rating_period`], [`match_quality_two_teams`] and [`match_quality_multi_team`].
///
/// # Examples
/// ```
Expand Down Expand Up @@ -903,6 +907,47 @@ pub fn match_quality(
a * b
}

#[must_use]
/// Gets the quality of multiple matches, which is equal to the probability that the match will end in a draw.
/// The higher the Value, the better the quality of the match.
///
/// Takes in a player as a [`TrueSkillRating`], and a slice of opponents as [`TrueSkillRating`]s and returns the probabilities of a draw occurring as a Vec of [`f64`]s between 1.0 and 0.0.
///
/// Similar to [`match_quality`].
///
/// # Examples
/// ```
/// use skillratings::trueskill::{match_quality_rating_period, TrueSkillConfig, TrueSkillRating};
///
/// let player_one = TrueSkillRating::new();
/// let player_two = TrueSkillRating::new();
/// let player_three = TrueSkillRating {
/// rating: 13.0,
/// uncertainty: 6.1,
/// };
///
/// let config = TrueSkillConfig::new();
///
/// let qualities = match_quality_rating_period(&player_one, &[player_two, player_three], &config);
///
/// // According to TrueSkill, there is a 44.7% chance this first match will end in a draw.
/// assert!(((qualities[0] * 1000.0).round() - 447.0).abs() < f64::EPSILON);
/// // Similarly, the second match has a 29.8% chance of ending in a draw.
/// assert!(((qualities[1] * 1000.0).round() - 298.0).abs() < f64::EPSILON);
/// ```
pub fn match_quality_rating_period(
player: &TrueSkillRating,
results: &[TrueSkillRating],
config: &TrueSkillConfig,
) -> Vec<f64> {
// No shortcut, it's just a simple loop.
// But this is implemented here for consistency with the other functions.
results
.iter()
.map(|r| match_quality(player, r, config))
.collect()
}

#[must_use]
/// Gets the quality of the match, which is equal to the probability that the match will end in a draw.
/// The higher the Value, the better the quality of the match.
Expand Down Expand Up @@ -2645,6 +2690,80 @@ mod tests {
assert!((results[2][1].uncertainty - 1.976_314_792_712_798_2).abs() < f64::EPSILON);
}

#[test]
fn test_ffa() {
let p1 = TrueSkillRating {
rating: 41.023,
uncertainty: 2.1333,
};
let p2 = TrueSkillRating {
rating: 21.0,
uncertainty: 1.87,
};

let p3 = TrueSkillRating {
rating: 42.0,
uncertainty: 1.223,
};

let teams_and_ranks: &[(&[TrueSkillRating], MultiTeamOutcome)] = &[
(&[p1], MultiTeamOutcome::new(1)),
(&[p2], MultiTeamOutcome::new(3)),
(&[p3], MultiTeamOutcome::new(2)),
];

let results = trueskill_multi_team(teams_and_ranks, &TrueSkillConfig::new());

assert!((results[0][0].rating - 41.720_925_460_665).abs() < f64::EPSILON);
assert!((results[1][0].rating - 20.997_268_045_415_94).abs() < f64::EPSILON);
assert!((results[2][0].rating - 41.771_076_420_914_83).abs() < f64::EPSILON);

assert!((results[0][0].uncertainty - 2.050_533_079_246_658_7).abs() < f64::EPSILON);
assert!((results[1][0].uncertainty - 1.870_534_805_422_220_2).abs() < f64::EPSILON);
assert!((results[2][0].uncertainty - 1.209_939_281_670_434_9).abs() < f64::EPSILON);
}

#[test]
fn test_unlikely_ffa() {
let p1 = TrueSkillRating {
rating: 0.4,
uncertainty: 8.1333,
};
let p2 = TrueSkillRating {
rating: -21.0,
uncertainty: 1.87,
};

let p3 = TrueSkillRating {
rating: 122.0,
uncertainty: 0.01,
};

let p4 = TrueSkillRating {
rating: -1.0,
uncertainty: -1.223,
};

let teams_and_ranks: &[(&[TrueSkillRating], MultiTeamOutcome)] = &[
(&[p1], MultiTeamOutcome::new(1)),
(&[p2], MultiTeamOutcome::new(3)),
(&[p3], MultiTeamOutcome::new(2)),
(&[p4], MultiTeamOutcome::new(2)),
];

let results = trueskill_multi_team(teams_and_ranks, &TrueSkillConfig::new());

assert!((results[0][0].rating - 46.844_398_641_974_195).abs() < f64::EPSILON);
assert!((results[1][0].rating - -21.0).abs() < f64::EPSILON);
assert!((results[2][0].rating - 121.973_594_228_967_41).abs() < f64::EPSILON);
assert!((results[3][0].rating - 3.577_783_039_440_541_7).abs() < f64::EPSILON);

assert!((results[0][0].uncertainty - 4.453_979_220_473_841).abs() < f64::EPSILON);
assert!((results[1][0].uncertainty - 1.871_855_882_391_709_3).abs() < f64::EPSILON);
assert!((results[2][0].uncertainty - 0.083_922_196_135_183_51).abs() < f64::EPSILON);
assert!((results[3][0].uncertainty - 1.197_926_990_096_083_4).abs() < f64::EPSILON);
}

#[test]
fn test_multi_teams_empty() {
let res = trueskill_multi_team(&[], &TrueSkillConfig::new());
Expand Down

0 comments on commit ff2c293

Please sign in to comment.