skillratings/
lib.rs

1#![warn(
2    missing_docs,
3    clippy::pedantic,
4    clippy::nursery,
5    clippy::unwrap_used,
6    clippy::expect_used
7)]
8#![allow(
9    // This is turned off because of the rating values in the structs
10    clippy::module_name_repetitions,
11    // "TrueSkill" shows up as a false positive otherwise
12    clippy::doc_markdown,
13    // Need to cast usizes to f64s where precision is not that important, also there seems to be no good alternative.
14    clippy::cast_precision_loss,
15)]
16
17//! Skillratings provides a collection of well-known (and lesser known) skill rating algorithms, that allow you to assess a player's skill level instantly.  
18//! You can easily calculate skill ratings instantly in 1vs1 matches, Team vs Team matches, or in tournaments / rating periods.  
19//! This library is incredibly lightweight (no dependencies by default), user-friendly, and of course, *blazingly fast*.
20//!
21//! Currently supported algorithms:
22//!
23//! - [Elo](https://docs.rs/skillratings/latest/skillratings/elo/)
24//! - [Glicko](https://docs.rs/skillratings/latest/skillratings/glicko/)
25//! - [Glicko-2](https://docs.rs/skillratings/latest/skillratings/glicko2/)
26//! - [TrueSkill](https://docs.rs/skillratings/latest/skillratings/trueskill/)
27//! - [Weng-Lin (OpenSkill)](https://docs.rs/skillratings/latest/skillratings/weng_lin/)
28//! - [FIFA Men's World Ranking](https://docs.rs/skillratings/latest/skillratings/fifa/)
29//! - [Sticko (Stephenson Rating System)](https://docs.rs/skillratings/latest/skillratings/sticko/)
30//! - [Glicko-Boost](https://docs.rs/skillratings/latest/skillratings/glicko_boost/)
31//! - [USCF (US Chess Federation Ratings)](https://docs.rs/skillratings/latest/skillratings/uscf/)
32//! - [EGF (European Go Federation Ratings)](https://docs.rs/skillratings/latest/skillratings/egf/)
33//! - [DWZ (Deutsche Wertungszahl)](https://docs.rs/skillratings/latest/skillratings/dwz/)
34//! - [Ingo](https://docs.rs/skillratings/latest/skillratings/ingo/)
35//!
36//! Most of these are known from their usage in chess and various other games.  
37//! Click on the documentation for the modules linked above for more information about the specific rating algorithms, and their advantages and disadvantages.
38//!
39//! ## Table of Contents
40//!
41//! - [Installation](#installation)
42//!     - [Serde Support](#serde-support)
43//! - [Usage and Examples](#usage-and-examples)
44//!     - [Player vs. Player](#player-vs-player)
45//!     - [Team vs. Team](#team-vs-team)
46//!     - [Free-For-Alls and Multiple Teams](#free-for-alls-and-multiple-teams)
47//!     - [Expected Outcome](#expected-outcome)
48//!     - [Rating Period](#rating-period)
49//!     - [Switching between different rating systems](#switching-between-different-rating-systems)
50//! - [Contributing](#contributing)
51//! - [License](#license)
52//!
53//!
54//! ## Installation
55//!
56//! If you are on Rust 1.62 or higher use `cargo add` to install the latest version:
57//!
58//! ```bash
59//! cargo add skillratings
60//! ```
61//!
62//! Alternatively, you can add the following to your `Cargo.toml` file manually:
63//!
64//! ```toml
65//! [dependencies]
66//! skillratings = "0.27"
67//! ```
68//!
69//! ### Serde support
70//!
71//! Serde support is gated behind the `serde` feature. You can enable it like so:
72//!
73//! Using `cargo add`:
74//!
75//! ```bash
76//! cargo add skillratings --features serde
77//! ```
78//!
79//! By editing `Cargo.toml` manually:
80//!
81//! ```toml
82//! [dependencies]
83//! skillratings = {version = "0.27", features = ["serde"]}
84//! ```
85//!
86//! ## Usage and Examples
87//!
88//! Below you can find some basic examples of the use cases of this crate.  
89//! There are many more rating algorithms available with lots of useful functions that are not covered here.  
90//! For more information head over to the modules linked above or below.
91//!
92//! ### Player-vs-Player
93//!
94//! Every rating algorithm included here can be used to rate 1v1 games.  
95//! We use *Glicko-2* in this example here.
96//!
97//! ```rust
98//! use skillratings::{
99//!     glicko2::{glicko2, Glicko2Config, Glicko2Rating},
100//!     Outcomes,
101//! };
102//!
103//! // Initialise a new player rating.
104//! // The default values are: 1500, 350, and 0.06.
105//! let player_one = Glicko2Rating::new();
106//!
107//! // Or you can initialise it with your own values of course.
108//! // Imagine these numbers being pulled from a database.
109//! let (some_rating, some_deviation, some_volatility) = (1325.0, 230.0, 0.05932);
110//! let player_two = Glicko2Rating {
111//!     rating: some_rating,
112//!     deviation: some_deviation,
113//!     volatility: some_volatility,
114//! };
115//!
116//! // The outcome of the match is from the perspective of player one.
117//! let outcome = Outcomes::WIN;
118//!
119//! // The config allows you to specify certain values in the Glicko-2 calculation.
120//! let config = Glicko2Config::new();
121//!
122//! // The glicko2 function will calculate the new ratings for both players and return them.
123//! let (new_player_one, new_player_two) = glicko2(&player_one, &player_two, &outcome, &config);
124//!
125//! // The first players rating increased by ~112 points.
126//! assert_eq!(new_player_one.rating.round(), 1612.0);
127//! ```
128//!
129//! ### Team-vs-Team
130//!
131//! Some algorithms like TrueSkill or Weng-Lin allow you to rate team-based games as well.  
132//! This example shows a 3v3 game using *TrueSkill*.
133//!
134//! ```rust
135//! use skillratings::{
136//!     trueskill::{trueskill_two_teams, TrueSkillConfig, TrueSkillRating},
137//!     Outcomes,
138//! };
139//!
140//! // We initialise Team One as a Vec of multiple TrueSkillRatings.
141//! // The default values for the rating are: 25, 25/3 ≈ 8.33.
142//! let team_one = vec![
143//!     TrueSkillRating {
144//!         rating: 33.3,
145//!         uncertainty: 3.3,
146//!     },
147//!     TrueSkillRating {
148//!         rating: 25.1,
149//!         uncertainty: 1.2,
150//!     },
151//!     TrueSkillRating {
152//!         rating: 43.2,
153//!         uncertainty: 2.0,
154//!     },
155//! ];
156//!
157//! // Team Two will be made up of 3 new players, for simplicity.
158//! // Note that teams do not necessarily have to be the same size.
159//! let team_two = vec![
160//!     TrueSkillRating::new(),
161//!     TrueSkillRating::new(),
162//!     TrueSkillRating::new(),
163//! ];
164//!
165//! // The outcome of the match is from the perspective of team one.
166//! let outcome = Outcomes::LOSS;
167//!
168//! // The config allows you to specify certain values in the TrueSkill calculation.
169//! let config = TrueSkillConfig::new();
170//!
171//! // The trueskill_two_teams function will calculate the new ratings for both teams and return them.
172//! let (new_team_one, new_team_two) = trueskill_two_teams(&team_one, &team_two, &outcome, &config);
173//!
174//! // The rating of the first player on team one decreased by around ~1.2 points.
175//! assert_eq!(new_team_one[0].rating.round(), 32.0);
176//! ```
177//!
178//! ### Free-For-Alls and Multiple Teams
179//!
180//! The *Weng-Lin* algorithm supports rating matches with multiple Teams.  
181//! Here is an example showing a 3-Team game with 3 players each.
182//!
183//! ```rust
184//! use skillratings::{
185//!     weng_lin::{weng_lin_multi_team, WengLinConfig, WengLinRating},
186//!     MultiTeamOutcome,
187//! };
188//!
189//! // Initialise the teams as Vecs of WengLinRatings.
190//! // Note that teams do not necessarily have to be the same size.
191//! // The default values for the rating are: 25, 25/3 ≈ 8.33.
192//! let team_one = vec![
193//!     WengLinRating {
194//!         rating: 25.1,
195//!         uncertainty: 5.0,
196//!     },
197//!     WengLinRating {
198//!         rating: 24.0,
199//!         uncertainty: 1.2,
200//!     },
201//!     WengLinRating {
202//!         rating: 18.0,
203//!         uncertainty: 6.5,
204//!     },
205//! ];
206//!
207//! let team_two = vec![
208//!     WengLinRating {
209//!         rating: 44.0,
210//!         uncertainty: 1.2,
211//!     },
212//!     WengLinRating {
213//!         rating: 32.0,
214//!         uncertainty: 2.0,
215//!     },
216//!     WengLinRating {
217//!         rating: 12.0,
218//!         uncertainty: 3.2,
219//!     },
220//! ];
221//!
222//! // Using the default rating for team three for simplicity.
223//! let team_three = vec![
224//!     WengLinRating::new(),
225//!     WengLinRating::new(),
226//!     WengLinRating::new(),
227//! ];
228//!
229//! // Every team is assigned a rank, depending on their placement. The lower the rank, the better.
230//! // If two or more teams tie with each other, assign them the same rank.
231//! let rating_groups = vec![
232//!     (&team_one[..], MultiTeamOutcome::new(1)),      // team one takes the 1st place.
233//!     (&team_two[..], MultiTeamOutcome::new(3)),      // team two takes the 3rd place.
234//!     (&team_three[..], MultiTeamOutcome::new(2)),    // team three takes the 2nd place.
235//! ];
236//!
237//! // The weng_lin_multi_team function will calculate the new ratings for all teams and return them.
238//! let new_teams = weng_lin_multi_team(&rating_groups, &WengLinConfig::new());
239//!
240//! // The rating of the first player of team one increased by around ~2.9 points.
241//! assert_eq!(new_teams[0][0].rating.round(), 28.0);
242//! ```
243//!
244//! ### Expected outcome
245//!
246//! Every rating algorithm has an `expected_score` function that you can use to predict the outcome of a game.  
247//! This example is using *Glicko* (*not Glicko-2!*) to demonstrate.
248//!
249//! ```rust
250//! use skillratings::glicko::{expected_score, GlickoRating};
251//!
252//! // Initialise a new player rating.
253//! // The default values are: 1500, and 350.
254//! let player_one = GlickoRating::new();
255//!
256//! // Initialising a new rating with custom numbers.
257//! let player_two = GlickoRating {
258//!     rating: 1812.0,
259//!     deviation: 195.0,
260//! };
261//!
262//! // The expected_score function will return two floats between 0 and 1 for each player.
263//! // A value of 1 means guaranteed victory, 0 means certain loss.
264//! // Values near 0.5 mean draws are likely to occur.
265//! let (exp_one, exp_two) = expected_score(&player_one, &player_two);
266//!
267//! // The expected score for player one is ~0.25.
268//! // If these players would play 100 games, player one is expected to score around 25 points.
269//! // (Win = 1 point, Draw = 0.5, Loss = 0)
270//! assert_eq!((exp_one * 100.0).round(), 25.0);
271//! ```
272//!
273//! ### Rating period
274//!
275//! Every rating algorithm included here has a `..._rating_period` that allows you to calculate a player's new rating using a list of results.  
276//! This can be useful in tournaments, or if you only update ratings at the end of a certain rating period, as the name suggests.  
277//! We are using the *Elo* rating algorithm in this example.
278//!
279//! ```rust
280//! use skillratings::{
281//!     elo::{elo_rating_period, EloConfig, EloRating},
282//!     Outcomes,
283//! };
284//!
285//! // We initialise a new Elo Rating here.
286//! // The default rating value is 1000.
287//! let player = EloRating { rating: 1402.1 };
288//!
289//! // We need a list of results to pass to the elo_rating_period function.
290//! let mut results = Vec::new();
291//!
292//! // And then we populate the list with tuples containing the opponent,
293//! // and the outcome of the match from our perspective.
294//! results.push((EloRating::new(), Outcomes::WIN));
295//! results.push((EloRating { rating: 954.0 }, Outcomes::DRAW));
296//! results.push((EloRating::new(), Outcomes::LOSS));
297//!
298//! // The elo_rating_period function calculates the new rating for the player and returns it.
299//! let new_player = elo_rating_period(&player, &results, &EloConfig::new());
300//!
301//! // The rating of the player decreased by around ~40 points.
302//! assert_eq!(new_player.rating.round(), 1362.0);
303//! ```
304//!
305//! ### Switching between different rating systems
306//!
307//! If you want to switch between different rating systems, for example to compare results or to do scientific analyisis,
308//! we provide Traits to make switching as easy and fast as possible.  
309//! All you have to do is provide the right Config for your rating system.
310//!
311//! _**Disclaimer:**_ For more accurate and fine-tuned calculations it is recommended that you use the rating system modules directly.  
312//! The Traits are primarily meant to be used for comparisions between systems.
313//!
314//! In the following example, we are using the `RatingSystem` (1v1) Trait with Glicko-2:
315//!
316//! ```rust
317//! use skillratings::{
318//!     glicko2::{Glicko2, Glicko2Config},
319//!     Outcomes, Rating, RatingSystem,
320//! };
321//!
322//! // Initialise a new player rating with a rating value and uncertainty value.
323//! // Not every rating system has an uncertainty value, so it may be discarded.
324//! // Some rating systems might consider other values too (volatility, age, matches played etc.).
325//! // If that is the case, we will use the default values for those.
326//! let player_one = Rating::new(Some(1200.0), Some(120.0));
327//! // Some rating systems might use widely different scales for measuring a player's skill.
328//! // So if you always want the default values for every rating system, use None instead.
329//! let player_two = Rating::new(None, None);
330//!
331//! // The config needs to be specific to the rating system.
332//! // When you swap rating systems, make sure to update the config.
333//! let config = Glicko2Config::new();
334//!
335//! // For 1v1 matches we are using the `RatingSystem` trait with the provided config.
336//! // If no config is available for the rating system, pass in empty brackets.
337//! // You may also need to use a type annotation here for the compiler.
338//! let rating_system: Glicko2 = RatingSystem::new(config);
339//!
340//! // The outcome of the match is from the perspective of player one.
341//! let outcome = Outcomes::WIN;
342//!
343//! // Calculate the expected score of the match.
344//! let expected_score = rating_system.expected_score(&player_one, &player_two);
345//! // Calculate the new ratings.
346//! let (new_one, new_two) = rating_system.rate(&player_one, &player_two, &outcome);
347//!
348//! // After that, access new ratings and uncertainties with the functions below.
349//! assert_eq!(new_one.rating().round(), 1241.0);
350//! // Note that because not every rating system has an uncertainty value,
351//! // the uncertainty function returns an Option<f64>.
352//! assert_eq!(new_one.uncertainty().unwrap().round(), 118.0);
353//! ```
354//!
355//! ## Contributing
356//!
357//! Contributions of any kind are always welcome!  
358//!
359//! Found a bug or have a feature request? [Submit a new issue](https://github.com/atomflunder/skillratings/issues/).  
360//! Alternatively, [open a pull request](https://github.com/atomflunder/skillratings/pulls) if you want to add features or fix bugs.  
361//! Leaving other feedback is of course also appreciated.
362//!
363//! Thanks to everyone who takes their time to contribute.
364//!
365//! ## License
366//!
367//! This project is licensed under either the [MIT License](/LICENSE-MIT), or the [Apache License, Version 2.0](/LICENSE-APACHE).
368
369#[cfg(feature = "serde")]
370use serde::de::DeserializeOwned;
371#[cfg(feature = "serde")]
372use serde::{Deserialize, Serialize};
373
374pub mod dwz;
375pub mod egf;
376pub mod elo;
377pub mod fifa;
378pub mod glicko;
379pub mod glicko2;
380pub mod glicko_boost;
381pub mod ingo;
382pub mod sticko;
383pub mod trueskill;
384pub mod uscf;
385pub mod weng_lin;
386
387/// The possible outcomes for a match: Win, Draw, Loss.
388///
389/// Note that this is always from the perspective of player one.  
390/// That means a win is a win for player one and a loss is a win for player two.
391#[derive(Clone, Copy, Debug, PartialEq, Eq)]
392#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
393pub enum Outcomes {
394    /// A win, from player_one's perspective.
395    WIN,
396    /// A loss, from player_one's perspective.
397    LOSS,
398    /// A draw.
399    DRAW,
400}
401
402impl Outcomes {
403    #[must_use]
404    /// Converts the outcome of the match into the points used in chess (1 = Win, 0.5 = Draw, 0 = Loss).
405    ///
406    /// Used internally in several rating algorithms, but some, like TrueSkill, have their own conversion.
407    pub const fn to_chess_points(self) -> f64 {
408        // Could set the visibility to crate level, but maybe someone has a use for it, who knows.
409        match self {
410            Self::WIN => 1.0,
411            Self::DRAW => 0.5,
412            Self::LOSS => 0.0,
413        }
414    }
415}
416
417/// Outcome for a free-for-all match or a match that involves more than two teams.
418///
419/// Every team is assigned a rank, depending on their placement. The lower the rank, the better.  
420/// If two or more teams tie with each other, assign them the same rank.
421///
422/// For example: Team A takes 1st place, Team C takes 2nd place, Team B takes 3rd place,
423/// and Teams D and E tie with each other and both take the 4th place.  
424/// In that case you would assign Team A = 1, Team B = 3, Team C = 2, Team D = 4, and Team E = 4.
425#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
426#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
427pub struct MultiTeamOutcome(usize);
428
429impl MultiTeamOutcome {
430    #[must_use]
431    #[inline]
432    /// Makes a new `MultiTeamOutcome` from a given rank.
433    pub const fn new(rank: usize) -> Self {
434        Self(rank)
435    }
436
437    #[must_use]
438    #[inline]
439    /// Returns the rank that corresponds to this `MultiTeamOutcome`.
440    pub const fn rank(self) -> usize {
441        self.0
442    }
443}
444
445impl From<usize> for MultiTeamOutcome {
446    #[must_use]
447    #[inline]
448    fn from(v: usize) -> Self {
449        Self(v)
450    }
451}
452
453impl From<MultiTeamOutcome> for usize {
454    #[must_use]
455    #[inline]
456    fn from(v: MultiTeamOutcome) -> Self {
457        v.0
458    }
459}
460
461/// Measure of player's skill.
462///
463/// 📌 _**Important note:**_ Please keep in mind that some rating systems use widely different scales for measuring ratings.  
464/// Please check out the documentation for each rating system for more information, or use `None` to always use default values.
465///
466/// Some rating systems might consider other values too (volatility, age, matches played etc.).
467/// If that is the case, we will use the default values for those.
468pub trait Rating {
469    /// A single value for player's skill
470    fn rating(&self) -> f64;
471    /// A value for the uncertainty of a players rating.
472    /// If the algorithm does not include an uncertainty value, this will return `None`.
473    fn uncertainty(&self) -> Option<f64>;
474    /// Initialise a `Rating` with provided score and uncertainty, if `None` use default.
475    /// If the algorithm does not include an uncertainty value it will get dismissed.
476    fn new(rating: Option<f64>, uncertainty: Option<f64>) -> Self;
477}
478
479/// Rating system for 1v1 matches.
480///
481/// 📌 _**Important note:**_ The RatingSystem Trait only implements the `rate` and `expected_score` functions.  
482/// Some rating systems might also implement additional functions (confidence interval, match quality, etc.) which you can only access by using those directly.
483pub trait RatingSystem {
484    #[cfg(feature = "serde")]
485    type RATING: Rating + Copy + std::fmt::Debug + DeserializeOwned + Serialize;
486    #[cfg(not(feature = "serde"))]
487    /// Rating type rating system.
488    type RATING: Rating + Copy + std::fmt::Debug;
489    /// Config type for rating system.
490    type CONFIG;
491    /// Initialise rating system with provided config. If the rating system does not require a config, leave empty brackets.
492    fn new(config: Self::CONFIG) -> Self;
493    /// Calculate ratings for two players based on provided ratings and outcome.
494    fn rate(
495        &self,
496        player_one: &Self::RATING,
497        player_two: &Self::RATING,
498        outcome: &Outcomes,
499    ) -> (Self::RATING, Self::RATING);
500    /// Calculate expected outcome of two players. Returns probability of player winning from 0.0 to 1.0.
501    fn expected_score(&self, player_one: &Self::RATING, player_two: &Self::RATING) -> (f64, f64);
502}
503
504/// Rating system for rating periods.
505///
506/// 📌 _**Important note:**_ The RatingPeriodSystem Trait only implements the `rate` and `expected_score` functions.  
507/// Some rating systems might also implement additional functions which you can only access by using those directly.
508pub trait RatingPeriodSystem {
509    #[cfg(feature = "serde")]
510    type RATING: Rating + Copy + std::fmt::Debug + DeserializeOwned + Serialize;
511    #[cfg(not(feature = "serde"))]
512    /// Rating type rating system.
513    type RATING: Rating + Copy + std::fmt::Debug;
514    /// Config type for rating system.
515    type CONFIG;
516    /// Initialise rating system with provided config. If the rating system does not require a config, leave empty brackets.
517    fn new(config: Self::CONFIG) -> Self;
518    /// Calculate ratings for a player based on provided list of opponents and outcomes.
519    fn rate(&self, player: &Self::RATING, results: &[(Self::RATING, Outcomes)]) -> Self::RATING;
520    /// Calculate expected scores for a player and a list of opponents. Returns probabilities of the player winning from 0.0 to 1.0.
521    fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64>;
522}
523
524/// Rating system for two teams.
525///
526/// 📌 _**Important note:**_ The TeamRatingSystem Trait only implements the `rate` and `expected_score` functions.  
527/// Some rating systems might also implement additional functions which you can only access by using those directly.
528pub trait TeamRatingSystem {
529    #[cfg(feature = "serde")]
530    type RATING: Rating + Copy + std::fmt::Debug + DeserializeOwned + Serialize;
531    #[cfg(not(feature = "serde"))]
532    /// Rating type rating system.
533    type RATING: Rating + Copy + std::fmt::Debug;
534    /// Config type for rating system.
535    type CONFIG;
536    /// Initialise rating system with provided config. If the rating system does not require a config, leave empty brackets.
537    fn new(config: Self::CONFIG) -> Self;
538    /// Calculate ratings for two teams based on provided ratings and outcome.
539    fn rate(
540        &self,
541        team_one: &[Self::RATING],
542        team_two: &[Self::RATING],
543        outcome: &Outcomes,
544    ) -> (Vec<Self::RATING>, Vec<Self::RATING>);
545    /// Calculate expected outcome of two teams. Returns probability of team winning from 0.0 to 1.0.
546    fn expected_score(&self, team_one: &[Self::RATING], team_two: &[Self::RATING]) -> (f64, f64);
547}
548
549/// Rating system for more than two teams.
550///
551/// 📌 _**Important note:**_ The MultiTeamRatingSystem Trait only implements the `rate` and `expected_score` functions.  
552/// Some rating systems might also implement additional functions which you can only access by using those directly.
553pub trait MultiTeamRatingSystem {
554    #[cfg(feature = "serde")]
555    type RATING: Rating + Copy + std::fmt::Debug + DeserializeOwned + Serialize;
556    #[cfg(not(feature = "serde"))]
557    /// Rating type rating system
558    type RATING: Rating + Copy + std::fmt::Debug;
559    /// Config type for rating system.
560    type CONFIG;
561    /// Initialise rating system with provided config. If the rating system does not require a config, leave empty brackets.
562    fn new(config: Self::CONFIG) -> Self;
563    /// Calculate ratings for multiple teams based on provided ratings and outcome.
564    fn rate(
565        &self,
566        teams_and_ranks: &[(&[Self::RATING], MultiTeamOutcome)],
567    ) -> Vec<Vec<Self::RATING>>;
568    /// Calculate expected outcome of multiple teams. Returns probability of team winning from 0.0 to 1.0.
569    fn expected_score(&self, teams: &[&[Self::RATING]]) -> Vec<f64>;
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn test_outcomes_to_chess_points() {
578        assert!((Outcomes::WIN.to_chess_points() - 1.0).abs() < f64::EPSILON);
579        assert!((Outcomes::DRAW.to_chess_points() - 0.5).abs() < f64::EPSILON);
580        assert!((Outcomes::LOSS.to_chess_points() - 0.0).abs() < f64::EPSILON);
581    }
582
583    #[test]
584    fn test_multi_team_outcome() {
585        let outcome = MultiTeamOutcome::new(1);
586        assert_eq!(outcome.rank(), 1);
587        assert_eq!(outcome, MultiTeamOutcome::from(1));
588        assert_eq!(outcome, 1.into());
589        assert_eq!(usize::from(MultiTeamOutcome::from(1)), 1);
590    }
591
592    #[test]
593    fn test_derives() {
594        let outcome = Outcomes::WIN;
595
596        assert_eq!(outcome, outcome.clone());
597        assert!(!format!("{outcome:?}").is_empty());
598
599        let multi_team_outcome = MultiTeamOutcome::new(1);
600        assert_eq!(multi_team_outcome, multi_team_outcome.clone());
601        assert!(!format!("{multi_team_outcome:?}").is_empty());
602        assert!(MultiTeamOutcome::new(1) < MultiTeamOutcome::new(2));
603    }
604}