commit 6868a1fe65e1c2e2490d020cb18bc0871bc2cd5e
parent 0a61712c1fcab678dc05743ec256912efda93abe
Author: Santtu Lakkala <inz@inz.fi>
Date: Tue, 3 Feb 2026 09:56:47 +0200
Initial rust version
Diffstat:
| A | rust/Makefile | | | 15 | +++++++++++++++ |
| A | rust/elo.rs | | | 37 | +++++++++++++++++++++++++++++++++++++ |
| A | rust/rand.rs | | | 69 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | rust/simu.rs | | | 756 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 877 insertions(+), 0 deletions(-)
diff --git a/rust/Makefile b/rust/Makefile
@@ -0,0 +1,15 @@
+RUSTC = rustc
+RUSTFMT = rustfmt
+RUSTFLAGS = --edition 2024
+RUSTCFLAGS = --cfg 'feature="use_unsafe"'
+CLIPPY = clippy-driver
+SOURCES := simu.rs rand.rs elo.rs
+
+simu: simu.rs $(SOURCES) Makefile
+ $(RUSTC) $(RUSTFLAGS) $(RUSTCFLAGS) -O -o $@ $<
+
+fmt:
+ $(RUSTFMT) $(RUSTFLAGS) $(SOURCES)
+
+clippy:
+ $(CLIPPY) $(RUSTFLAGS) $(RUSTCFLAGS) -W clippy::pedantic simu.rs
diff --git a/rust/elo.rs b/rust/elo.rs
@@ -0,0 +1,37 @@
+#[derive(Clone, Copy, Debug)]
+pub struct Elo(pub f64);
+
+impl Default for Elo {
+ fn default() -> Self {
+ Self(2000.)
+ }
+}
+
+impl std::fmt::Display for Elo {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ write!(fmt, "{:.0}", self.0)
+ }
+}
+
+impl Elo {
+ pub fn expected(self, other: Self, adv: f64) -> f64 {
+ 1. / (1. + (10_f64).powf((other.0 - self.0 - adv) / 400.0))
+ }
+
+ pub fn update(&mut self, other: &mut Self, k: f64, actual: f64, adv: f64) {
+ let expected = self.expected(*other, adv);
+ self.0 += k * (actual - expected);
+ other.0 -= k * (actual - expected);
+ }
+
+ fn regress(&mut self, k: f64, factor: f64) {
+ self.0 += (Self::default().0 - self.0) * k * factor;
+ }
+
+ pub fn simulated(&mut self, other: &mut Self, adv: f64, k: f64, regress: f64) -> f64 {
+ let expected = self.expected(*other, adv);
+ self.regress(k, regress);
+ other.regress(k, regress);
+ expected
+ }
+}
diff --git a/rust/rand.rs b/rust/rand.rs
@@ -0,0 +1,69 @@
+pub trait RandOutput:
+ Copy + Ord + std::ops::Sub<Output = Self> + std::fmt::Debug + Default
+{
+}
+impl<T: Copy + Ord + std::ops::Sub<Output = Self> + std::fmt::Debug + Default> RandOutput for T {}
+
+pub trait Rand: Copy {
+ type Output: RandOutput;
+ const MAX: Self::Output;
+
+ fn rand(&mut self) -> Self::Output;
+}
+
+pub trait YesNo<T: RandOutput>: Rand<Output = T> {
+ fn yesno(&mut self, limit: T) -> (bool, bool) {
+ let r = self.rand();
+ (r < limit, Self::MAX - r < limit)
+ }
+}
+
+impl<T, R> YesNo<T> for R
+where
+ T: RandOutput,
+ R: Rand<Output = T>,
+{
+}
+
+#[derive(Clone, Copy)]
+pub struct Lehmer {
+ state: std::num::Wrapping<u128>,
+}
+
+impl Default for Lehmer {
+ fn default() -> Self {
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)
+ .expect("Failed to initialize random");
+ Self::new(u128::from(now.subsec_nanos()) << 64 | u128::from(now.as_secs()) | 1)
+ }
+}
+
+impl Lehmer {
+ pub fn new(seed: u128) -> Self {
+ Self {
+ state: std::num::Wrapping(seed),
+ }
+ }
+
+ pub fn new_with<T: Into<u128>>(other: &mut impl Rand<Output = T>) -> Self {
+ Self::new(
+ other.rand().into() << 96
+ ^ other.rand().into() << 64
+ ^ other.rand().into() << 32
+ ^ other.rand().into()
+ | 1,
+ )
+ }
+}
+
+impl Rand for Lehmer {
+ type Output = u32;
+ const MAX: Self::Output = Self::Output::MAX;
+
+ #[allow(clippy::cast_possible_truncation)]
+ fn rand(&mut self) -> Self::Output {
+ self.state *= 0xda94_2042_e4dd_58b5;
+ (self.state >> 64).0 as Self::Output
+ }
+}
diff --git a/rust/simu.rs b/rust/simu.rs
@@ -0,0 +1,756 @@
+mod elo;
+mod rand;
+
+use elo::Elo;
+use rand::{Lehmer, Rand, RandOutput, YesNo};
+
+pub type Error = Box<dyn std::error::Error>;
+pub type Result<T> = std::result::Result<T, Error>;
+
+trait Getter<T> {
+ fn idxmut(&mut self, idx: usize) -> &mut T;
+}
+
+#[cfg(not(feature = "use_unsafe"))]
+impl<T> Getter<T> for Vec<T> {
+ fn idxmut(&mut self, idx: usize) -> &mut T {
+ &mut self[idx]
+ }
+}
+
+#[cfg(feature = "use_unsafe")]
+impl<T> Getter<T> for Vec<T> {
+ fn idxmut(&mut self, idx: usize) -> &mut T {
+ unsafe { &mut *self.as_mut_ptr().add(idx) }
+ }
+}
+
+trait CloseEnough<T> {
+ fn lossy_from(other: T) -> Self;
+}
+
+impl CloseEnough<f64> for u32 {
+ #[allow(
+ clippy::cast_precision_loss,
+ clippy::cast_possible_truncation,
+ clippy::cast_sign_loss
+ )]
+ fn lossy_from(other: f64) -> Self {
+ other as Self
+ }
+}
+
+trait FirstLast {
+ type Item;
+
+ fn first_last<F: FnMut(Self::Item) -> bool>(self, f: F) -> Option<(usize, usize)>;
+}
+
+impl<T, I: std::iter::IntoIterator<Item = T>> FirstLast for I {
+ type Item = T;
+
+ fn first_last<F: FnMut(Self::Item) -> bool>(self, mut f: F) -> Option<(usize, usize)> {
+ self.into_iter().enumerate().fold(None, |acc, (i, e)| {
+ (f(e)).then_some((acc.map_or(i, |(a, _)| a), i)).or(acc)
+ })
+ }
+}
+
+#[derive(Debug)]
+struct Options {
+ pub threads: usize,
+ pub iterations: usize,
+ pub home_advantage: f64,
+ pub overtime_prob: f64,
+ pub elo_k: f64,
+ pub regression: f64,
+}
+
+impl Default for Options {
+ fn default() -> Self {
+ Self {
+ threads: 1,
+ iterations: 100_000,
+ home_advantage: 58.,
+ overtime_prob: 0.23,
+ elo_k: 32.,
+ regression: 0.005,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct PlayedGame {
+ pub home_id: usize,
+ pub away_id: usize,
+ pub home_score: usize,
+ pub away_score: usize,
+ pub overtime: bool,
+}
+
+impl std::str::FromStr for PlayedGame {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self> {
+ let mut parts = s.split_ascii_whitespace();
+ let home_id = parts.next().ok_or("Missing field")?.parse()?;
+ let away_id = parts.next().ok_or("Missing fields")?.parse()?;
+ let home_score = parts.next().ok_or("Missing fields")?.parse()?;
+ let away_score = parts.next().ok_or("Missing fields")?.parse()?;
+ let overtime = parts.next().is_some_and(|s| s == "OT" || s == "SO");
+
+ Ok(Self {
+ home_id,
+ away_id,
+ home_score,
+ away_score,
+ overtime,
+ })
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+struct Game {
+ pub home_id: usize,
+ pub away_id: usize,
+}
+
+#[derive(Clone, Copy, Debug)]
+struct UpcomingGame<R: Rand> {
+ pub home_id: usize,
+ pub away_id: usize,
+ pub expected: R::Output,
+ _phantom: std::marker::PhantomData<R>,
+}
+
+impl Game {
+ pub fn into_upcoming<R>(
+ self,
+ map: &[usize],
+ teams: &mut [Team],
+ opts: &Options,
+ ) -> Result<UpcomingGame<R>>
+ where
+ R: Rand,
+ R::Output: Into<f64> + CloseEnough<f64>,
+ {
+ if let Some(&home_id) = map.get(self.home_id)
+ && let Some(&away_id) = map.get(self.away_id)
+ {
+ let [home, away] = teams.get_disjoint_mut([home_id, away_id])?;
+ let ug = UpcomingGame {
+ home_id,
+ away_id,
+ expected: R::Output::lossy_from(
+ home.simulated(away, opts.home_advantage, opts.elo_k, opts.regression)
+ * R::MAX.into(),
+ ),
+ _phantom: std::marker::PhantomData,
+ };
+ Ok(ug)
+ } else {
+ Err(Error::from("Invalid team id"))
+ }
+ }
+}
+
+impl std::str::FromStr for Game {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self> {
+ let mut parts = s.split_ascii_whitespace();
+ let home_id = parts.next().ok_or("Missing fields")?.parse()?;
+ let away_id = parts.next().ok_or("Missing fields")?.parse()?;
+
+ Ok(Self { home_id, away_id })
+ }
+}
+
+#[derive(Default, Clone, Debug)]
+struct Team {
+ pub name: String,
+ pub id: usize,
+ pub points: u64,
+ pub games: u64,
+ pub wins: u64,
+ pub pointssum: u64,
+ pub gamessum: u64,
+ pub winssum: u64,
+ pub elo: Elo,
+ pub elo0: Elo,
+ pub poscounts: Vec<(usize, bool)>,
+}
+
+#[derive(Clone, Copy, Debug)]
+enum NormalizedProb {
+ Zero,
+ Epsilon,
+ Percentage(f64),
+ Always,
+}
+
+impl std::fmt::Display for NormalizedProb {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use NormalizedProb as N;
+ match self {
+ N::Zero => write!(f, "-"),
+ N::Epsilon => write!(f, "0"),
+ N::Percentage(p) => write!(f, "{p:e}"),
+ N::Always => write!(f, "+"),
+ }
+ }
+}
+
+#[allow(dead_code)]
+struct NormalizedTeam {
+ pub name: String,
+ pub points: u64,
+ pub games: u64,
+ pub wins: u64,
+ pub final_points: f64,
+ pub final_games: f64,
+ pub final_wins: f64,
+ pub elo: Elo,
+ pub poscounts: Vec<NormalizedProb>,
+}
+
+impl std::cmp::PartialEq for Team {
+ fn eq(&self, other: &Self) -> bool {
+ self.points * other.games == self.games * other.points
+ }
+}
+impl std::cmp::Eq for Team {}
+
+impl std::cmp::Ord for Team {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ (other.points * self.games)
+ .cmp(&(self.points * other.games))
+ .then_with(|| (other.wins * self.games).cmp(&(self.wins * other.games)))
+ }
+}
+
+impl std::cmp::PartialOrd for Team {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Team {
+ pub fn new(name: String, id: usize, n_teams: usize) -> Self {
+ Self {
+ name,
+ id,
+ poscounts: vec![(0, false); n_teams],
+ ..Default::default()
+ }
+ }
+
+ pub fn simulated(&mut self, other: &mut Self, adv: f64, elo_k: f64, regress: f64) -> f64 {
+ self.elo.simulated(&mut other.elo, adv, elo_k, regress)
+ }
+
+ pub fn game(&mut self, gf: usize, ga: usize, ot: bool) -> u64 {
+ self.games += 1;
+ let points = match (gf > ga, ot) {
+ (true, false) => {
+ self.wins += 1;
+ 3
+ }
+ (true, true) => 2,
+ (false, true) => 1,
+ (false, false) => 0,
+ };
+ self.points += points;
+ points
+ }
+
+ #[allow(clippy::cast_precision_loss)]
+ pub fn normalize(self, iterations: usize) -> NormalizedTeam {
+ let Team {
+ name,
+ points,
+ games,
+ wins,
+ pointssum,
+ gamessum,
+ winssum,
+ elo0: elo,
+ poscounts,
+ ..
+ } = self;
+ NormalizedTeam {
+ name,
+ points,
+ games,
+ wins,
+ elo,
+ final_points: pointssum as f64 / iterations as f64,
+ final_games: gamessum as f64 / iterations as f64,
+ final_wins: winssum as f64 / iterations as f64,
+ poscounts: poscounts
+ .into_iter()
+ .map(|count| match count {
+ (0, false) => NormalizedProb::Zero,
+ (0, true) => NormalizedProb::Epsilon,
+ (count, _) if count == iterations => NormalizedProb::Always,
+ (count, _) => {
+ NormalizedProb::Percentage(100. * (count - 1) as f64 / iterations as f64)
+ }
+ })
+ .collect(),
+ }
+ }
+
+ fn add_result(&mut self, rhs: &SimulationResult) {
+ for ((w, _), &r) in self.poscounts.iter_mut().zip(&rhs.position_counts) {
+ *w += r;
+ }
+ self.gamessum += rhs.games;
+ self.pointssum += rhs.points;
+ }
+
+ fn add_winlose_result(&mut self, rhs: &WinLoseSimulationResult) {
+ for ((_, p), &r) in self.poscounts.iter_mut().zip(&rhs.positions) {
+ *p |= r;
+ }
+ }
+}
+
+impl std::fmt::Display for NormalizedTeam {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let NormalizedTeam {
+ name,
+ games,
+ points,
+ final_games,
+ final_points,
+ elo,
+ poscounts,
+ ..
+ } = self;
+ write!(
+ f,
+ "{name} {games} {points} {final_games:.0} {final_points:.1} {elo:.0}"
+ )?;
+ for count in poscounts {
+ write!(f, " {count}")?;
+ }
+ Ok(())
+ }
+}
+
+trait IteratorArgExt {
+ fn get_arg<E, T>(&mut self) -> Result<T>
+ where
+ E: std::error::Error + 'static,
+ T: std::str::FromStr<Err = E>;
+}
+
+impl<S: AsRef<str>, I: Iterator<Item = S>> IteratorArgExt for I {
+ fn get_arg<E, T>(&mut self) -> Result<T>
+ where
+ E: std::error::Error + 'static,
+ T: std::str::FromStr<Err = E>,
+ {
+ Ok(self.next().ok_or("Missing argument")?.as_ref().parse()?)
+ }
+}
+
+impl Options {
+ fn from_args<T: IntoIterator<Item = String>>(iter: T) -> Result<Self> {
+ let mut iter = iter.into_iter();
+ let mut rv = Self::default();
+ while let Some(flags) = iter.next() {
+ let mut chars = flags.chars();
+ chars
+ .next()
+ .filter(|c| *c == '-')
+ .ok_or("Invalid argument")?;
+ for c in chars {
+ match c {
+ 'i' => rv.iterations = iter.get_arg()?,
+ 't' => rv.threads = iter.get_arg()?,
+ 'a' => rv.home_advantage = iter.get_arg()?,
+ 'o' => rv.overtime_prob = iter.get_arg()?,
+ 'k' => rv.elo_k = iter.get_arg()?,
+ 'r' => rv.regression = iter.get_arg()?,
+ _ => Err(format!("Invalid option {c}"))?,
+ }
+ }
+ }
+ Ok(rv)
+ }
+}
+
+#[derive(Clone, Debug, Copy)]
+struct SimulationTeam<R>
+where
+ R: Rand,
+ R::Output: Ord + Copy + std::fmt::Debug,
+{
+ id: usize,
+ points: u64,
+ games: u64,
+ wins: u64,
+ random: R::Output,
+}
+
+impl<R: Rand> std::cmp::PartialEq for SimulationTeam<R> {
+ fn eq(&self, other: &Self) -> bool {
+ self.points * other.games == other.points * self.games
+ && self.wins * other.games == other.wins * self.games
+ }
+}
+
+impl<R: Rand> std::cmp::Eq for SimulationTeam<R> {}
+
+impl<R: Rand> std::cmp::Ord for SimulationTeam<R> {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ (other.points * self.games)
+ .cmp(&(self.points * other.games))
+ .then_with(|| {
+ (other.wins * self.games)
+ .cmp(&(self.wins * other.games))
+ .then_with(|| self.random.cmp(&other.random))
+ })
+ }
+}
+
+impl<R: Rand> std::cmp::PartialOrd for SimulationTeam<R> {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl<R: Rand> SimulationTeam<R> {
+ pub fn copy_with(&self, rand: &mut R) -> (Self, Self) {
+ let r = rand.rand();
+ (
+ Self { random: r, ..*self },
+ Self {
+ random: R::MAX - r,
+ ..*self
+ },
+ )
+ }
+
+ pub fn game(&mut self, win: bool, ot: bool) {
+ self.games += 1;
+ self.points += (3 * u64::from(win)) ^ u64::from(ot);
+ }
+}
+
+impl<R: Rand> From<(usize, &Team)> for SimulationTeam<R> {
+ fn from((id, team): (usize, &Team)) -> Self {
+ Self {
+ id,
+ points: team.points,
+ games: team.games,
+ wins: team.wins,
+ random: Default::default(),
+ }
+ }
+}
+
+#[derive(Clone)]
+struct WinLoseSimulationResult {
+ positions: Vec<bool>,
+}
+
+#[derive(Clone)]
+struct SimulationResult {
+ position_counts: Vec<usize>,
+ games: u64,
+ points: u64,
+ wins: u64,
+}
+
+impl SimulationResult {
+ fn new(n_teams: usize) -> Self {
+ Self {
+ position_counts: vec![0; n_teams],
+ games: 0,
+ points: 0,
+ wins: 0,
+ }
+ }
+}
+
+fn simulate<T, R>(
+ teams: &[Team],
+ upcoming_games: &[UpcomingGame<R>],
+ rand: &mut R,
+ opts: &Options,
+) -> (usize, Vec<SimulationResult>)
+where
+ T: RandOutput + CloseEnough<f64> + Into<f64>,
+ R: YesNo<T> + Rand<Output = T>,
+{
+ let simulationteams0: Vec<SimulationTeam<R>> =
+ teams.iter().enumerate().map(From::from).collect();
+ let mut result: Vec<_> = (0..teams.len())
+ .map(|_| SimulationResult::new(teams.len()))
+ .collect();
+ let otprob = R::Output::lossy_from(opts.overtime_prob * R::MAX.into());
+ let mut simulationteams = simulationteams0.clone();
+ let mut antisimulationteams = simulationteams0.clone();
+ for _ in 0..opts.iterations / 2 {
+ for ((s, antis), s0) in simulationteams
+ .iter_mut()
+ .zip(antisimulationteams.iter_mut())
+ .zip(simulationteams0.iter())
+ {
+ (*s, *antis) = s0.copy_with(rand);
+ }
+
+ for game in upcoming_games {
+ let (res, antires) = rand.yesno(game.expected);
+ let (ot, antiot) = rand.yesno(otprob);
+
+ simulationteams.idxmut(game.home_id).game(res, ot);
+ simulationteams.idxmut(game.away_id).game(!res, ot);
+ antisimulationteams
+ .idxmut(game.home_id)
+ .game(antires, antiot);
+ antisimulationteams
+ .idxmut(game.away_id)
+ .game(!antires, antiot);
+ }
+
+ simulationteams.sort_unstable();
+ antisimulationteams.sort_unstable();
+
+ for (i, t) in simulationteams
+ .iter()
+ .enumerate()
+ .chain(antisimulationteams.iter().enumerate())
+ {
+ let res = result.idxmut(t.id);
+ res.position_counts[i] += 1;
+ res.games += t.games;
+ res.points += t.points;
+ res.wins += t.wins;
+ }
+ }
+
+ (opts.iterations & (usize::MAX - 1), result)
+}
+
+#[derive(Debug, Clone, Copy)]
+enum WinLose {
+ WinAll(usize),
+ LoseAll(usize),
+}
+
+enum Winner {
+ Undefined,
+ Home,
+ Away,
+}
+
+impl WinLose {
+ pub fn check<T>(&self, game: &UpcomingGame<impl Rand<Output = T>>) -> Winner {
+ use WinLose as WL;
+ use Winner as W;
+ match self {
+ WL::WinAll(id) if &game.home_id == id => W::Home,
+ WL::WinAll(id) if &game.away_id == id => W::Away,
+ WL::LoseAll(id) if &game.home_id == id => W::Away,
+ WL::LoseAll(id) if &game.away_id == id => W::Home,
+ _ => W::Undefined,
+ }
+ }
+}
+
+fn simulate_winall<T, R>(
+ teams: &[Team],
+ upcoming_games: &[UpcomingGame<R>],
+ rand: &mut R,
+ opts: &Options,
+ winlose: WinLose,
+) -> Vec<SimulationResult>
+where
+ T: RandOutput + CloseEnough<f64> + Into<f64>,
+ R: YesNo<T> + Rand<Output = T>,
+{
+ let mut teams = Vec::from(teams);
+ let upcoming: Vec<_> = upcoming_games
+ .iter()
+ .filter(|game| {
+ !match winlose.check(game) {
+ Winner::Home => Some((game.home_id, game.away_id)),
+ Winner::Away => Some((game.away_id, game.home_id)),
+ Winner::Undefined => None,
+ }
+ .is_some_and(|(winner, loser)| {
+ teams[winner].game(1, 0, false);
+ teams[loser].game(0, 1, false);
+ true
+ })
+ })
+ .copied()
+ .collect();
+ simulate(&teams, &upcoming, rand, opts).1
+}
+
+fn simulate_winloseall<T, R>(
+ teams: &[Team],
+ upcoming_games: &[UpcomingGame<R>],
+ rand: &mut R,
+ opts: &Options,
+) -> Vec<WinLoseSimulationResult>
+where
+ T: RandOutput + CloseEnough<f64> + Into<f64>,
+ R: YesNo<T> + Rand<Output = T>,
+{
+ let opts = Options {
+ iterations: opts.iterations / teams.len() / 2,
+ ..*opts
+ };
+ (0..teams.len())
+ .map(|idx| {
+ let (whigh, wlow) =
+ simulate_winall(teams, upcoming_games, rand, &opts, WinLose::WinAll(idx))[idx]
+ .position_counts
+ .iter()
+ .first_last(|&p| p != 0)
+ .unwrap();
+ let (lhigh, llow) =
+ simulate_winall(teams, upcoming_games, rand, &opts, WinLose::LoseAll(idx))[idx]
+ .position_counts
+ .iter()
+ .first_last(|&p| p != 0)
+ .unwrap();
+ WinLoseSimulationResult {
+ positions: (0..teams.len())
+ .map(|p| p >= whigh.min(lhigh) && p <= wlow.max(llow))
+ .collect(),
+ }
+ })
+ .collect()
+}
+
+fn simulate_parallel(
+ teams: &mut [Team],
+ games: &[UpcomingGame<Lehmer>],
+ rnd: &mut Lehmer,
+ opts: &Options,
+) -> usize {
+ let opts = Options {
+ iterations: opts.iterations / opts.threads,
+ ..*opts
+ };
+ let results = std::thread::scope(|s| {
+ (0..opts.threads)
+ .map(|_| {
+ let teams = &teams;
+ let games = &games;
+ let opts = &opts;
+ let mut rnd = Lehmer::new_with(rnd);
+
+ s.spawn(move || {
+ (
+ simulate(teams, games, &mut rnd, opts),
+ simulate_winloseall(teams, games, &mut rnd, opts),
+ )
+ })
+ })
+ .collect::<Vec<_>>()
+ .into_iter()
+ .map(|t| t.join().expect("Thread panicked"))
+ .collect::<Vec<_>>()
+ });
+ let mut iterations = 0;
+ for ((iters, simulated), simulatedwl) in results {
+ iterations += iters;
+ for (team, simu) in teams.iter_mut().zip(&simulated) {
+ team.add_result(simu);
+ }
+ for (team, simu) in teams.iter_mut().zip(&simulatedwl) {
+ team.add_winlose_result(simu);
+ }
+ }
+ iterations
+}
+
+fn main() -> Result<()> {
+ let opts = Options::from_args(std::env::args().skip(1))?;
+ let mut lines = std::io::stdin()
+ .lines()
+ .filter(|r| !r.as_ref().is_ok_and(|s| s.trim().is_empty()));
+
+ let n_teams: usize = lines.next().ok_or("Missing team count")??.trim().parse()?;
+ let mut teams = (0..n_teams)
+ .map(|id| {
+ Ok(Team::new(
+ lines.next().ok_or("Missing teams")??.trim().to_owned(),
+ id,
+ n_teams,
+ ))
+ })
+ .collect::<Result<Vec<_>>>()?;
+ let n_games: usize = lines.next().ok_or("Missing game count")??.trim().parse()?;
+ for game in (0..n_games).map(|_| lines.next().ok_or("Missing game")??.parse::<PlayedGame>()) {
+ let PlayedGame {
+ home_id,
+ away_id,
+ home_score,
+ away_score,
+ overtime,
+ } = game?;
+ let [home, away] = teams.get_disjoint_mut([home_id, away_id])?;
+
+ away.game(away_score, home_score, overtime);
+ #[allow(clippy::cast_precision_loss)]
+ let actual = home.game(home_score, away_score, overtime) as f64 / 3.;
+
+ home.elo
+ .update(&mut away.elo, opts.elo_k, actual, opts.home_advantage);
+ }
+
+ teams.sort();
+ let mut team_map = vec![0; n_teams];
+ for (i, team) in teams.iter_mut().enumerate() {
+ team_map[team.id] = i;
+ team.elo0 = team.elo;
+ }
+
+ let mut rnd = Lehmer::default();
+
+ let n_games: usize = lines.next().ok_or("Missing game count")??.trim().parse()?;
+ let games: Vec<_> = (0..n_games)
+ .map(|_| {
+ lines
+ .next()
+ .ok_or("Missing games")??
+ .parse::<Game>()?
+ .into_upcoming::<Lehmer>(&team_map, &mut teams, &opts)
+ })
+ .collect::<Result<_>>()?;
+
+ let iterations = if opts.threads == 1 {
+ let (iterations, simulated) = simulate(&teams, &games, &mut rnd, &opts);
+ let simulatedwl = simulate_winloseall(&teams, &games, &mut rnd, &opts);
+ for (team, simu) in teams.iter_mut().zip(&simulated) {
+ team.add_result(simu);
+ }
+ for (team, simu) in teams.iter_mut().zip(&simulatedwl) {
+ team.add_winlose_result(simu);
+ }
+ iterations
+ } else {
+ simulate_parallel(&mut teams, &games, &mut rnd, &opts)
+ };
+
+ teams.sort_by(|a, b| {
+ (b.pointssum * a.gamessum)
+ .cmp(&(a.pointssum * b.gamessum))
+ .then_with(|| (b.winssum * a.gamessum).cmp(&(a.winssum * a.gamessum)))
+ });
+
+ for team in teams.into_iter().map(|team| team.normalize(iterations)) {
+ println!("{team}");
+ }
+
+ Ok(())
+}