openskill 0.0.1

Rust implementation of OpenSkill (license free TrueSkill)
Documentation
use crate::{
    error::OpenSkillError,
    math::{self, normal},
    rating::{to_team_ratings, Rating},
    utils::zip_without_self,
    validate::validate_team,
};

use statrs::distribution::ContinuousCDF;

pub fn predict_win(teams: &Vec<Vec<Rating>>, beta: f64) -> Result<Vec<f64>, OpenSkillError> {
    let beta_squared = beta.powi(2);

    validate_team(&teams)?;

    let num_teams = teams.len();
    let team_ratings = to_team_ratings(teams, None);
    let denom = math::denominator(num_teams, 2);

    Ok(zip_without_self(&team_ratings)
        .map(|(team_i, others)| {
            others
                .iter()
                .map(|team_q| {
                    normal().cdf(
                        ((*team_i).mu - team_q.mu)
                            / math::sigma_bar(
                                team_i.sigma_sq,
                                team_q.sigma_sq,
                                beta_squared,
                                team_i.team_size() + team_q.team_size(),
                            ),
                    )
                })
                .sum::<f64>()
                / denom
        })
        .collect())
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::constant::DEFAULT_BETA;
    use approx::relative_eq;
    use std::iter::zip;

    fn predict_win_test(teams: Vec<Vec<Rating>>, want: Result<Vec<f64>, OpenSkillError>) {
        let got = predict_win(&teams, DEFAULT_BETA);
        match (got.clone(), want.clone()) {
            (Ok(got), Ok(want)) => {
                assert_eq!(got.len(), want.len());
                zip(got.iter(), want.iter()).for_each(|(g, w)| {
                    assert!(
                        relative_eq!(g, w, epsilon = 1e-6, max_relative = 1e-6),
                        "got: {g}, want: {w}",
                    )
                });
            }
            (Err(got), Err(want)) => {
                assert_eq!(got, want);
            }
            _ => assert!(false, "got: {got:?}, want: {want:?}"),
        };
    }

    #[test]
    fn predict_win_outcome_for_two_teams() {
        let teams = vec![
            vec![Rating::default(), Rating::new(32.444, 5.123)],
            vec![Rating::new(73.381, 1.421), Rating::new(25.188, 6.211)],
        ];
        let want = Ok(vec![0.0020706344961249385, 0.997929365503875]);
        predict_win_test(teams, want);
    }

    #[test]
    fn predict_win_outcome_for_multiple_asymmetric_teams() {
        let teams = vec![
            vec![Rating::default(), Rating::new(32.444, 5.123)],
            vec![Rating::new(73.381, 1.421), Rating::new(25.188, 6.211)],
            vec![Rating::new(32.444, 5.123)],
            vec![Rating::new(25.188, 6.211)],
        ];
        let want = Ok(vec![
            0.3273280055056081,
            0.49965489412719827,
            0.132583880271438,
            0.04043322009575564,
        ]);
        predict_win_test(teams, want);
    }

    #[test]
    fn predict_win_3_player_newbie_ffa() {
        let teams = vec![
            vec![Rating::default()],
            vec![Rating::default()],
            vec![Rating::default()],
        ];
        let want = Ok(vec![
            0.3333333333333333,
            0.3333333333333333,
            0.3333333333333333,
        ]);
        predict_win_test(teams, want);
    }

    #[test]
    fn predict_win_4_player_newbie_ffa() {
        let teams = vec![
            vec![Rating::default()],
            vec![Rating::default()],
            vec![Rating::default()],
            vec![Rating::default()],
        ];
        let want = Ok(vec![0.25, 0.25, 0.25, 0.25]);
        predict_win_test(teams, want);
    }

    #[test]
    fn predict_win_4_players_of_varying_skill() {
        let teams = vec![
            vec![Rating::new(1.0, 0.1)],
            vec![Rating::new(2.0, 0.1)],
            vec![Rating::new(3.0, 0.1)],
            vec![Rating::new(4.0, 0.1)],
        ];
        let want = Ok(vec![
            0.18420221980528928,
            0.22786446154550788,
            0.27213553845449207,
            0.31579778019471066,
        ]);
        predict_win_test(teams, want);
    }
}