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}