use fforge_domain::{ClubId, Fixture, FixtureId};
pub fn double_round_robin(clubs: &[ClubId]) -> Vec<Fixture> {
let n = clubs.len();
assert!(n >= 2 && n.is_multiple_of(2), "need an even number of clubs, got {n}");
let rounds = (n - 1) as u8;
let mut fixtures = Vec::with_capacity(n * (n - 1));
let mut next_id = 0u32;
let mut ring: Vec<ClubId> = clubs[1..].to_vec();
let mut first_half: Vec<Vec<(ClubId, ClubId)>> = Vec::with_capacity(rounds as usize);
for round in 0..rounds {
let mut pairs = Vec::with_capacity(n / 2);
let (a, b) = (clubs[0], ring[0]);
if round % 2 == 0 {
pairs.push((a, b));
} else {
pairs.push((b, a));
}
for i in 1..n / 2 {
let x = ring[i];
let y = ring[n - 1 - i];
if round % 2 == 0 {
pairs.push((y, x));
} else {
pairs.push((x, y));
}
}
first_half.push(pairs);
ring.rotate_right(1);
}
for (round, pairs) in first_half.iter().enumerate() {
for &(home, away) in pairs {
fixtures.push(Fixture {
id: FixtureId(next_id),
matchday: round as u8 + 1,
home,
away,
});
next_id += 1;
}
}
for (round, pairs) in first_half.iter().enumerate() {
for &(home, away) in pairs {
fixtures.push(Fixture {
id: FixtureId(next_id),
matchday: rounds + round as u8 + 1,
home: away,
away: home,
});
next_id += 1;
}
}
fixtures
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn round_robin_properties() {
let clubs: Vec<ClubId> = (0..20).map(ClubId).collect();
let fixtures = double_round_robin(&clubs);
assert_eq!(fixtures.len(), 20 * 19);
let mut seen: BTreeMap<(ClubId, ClubId), u32> = BTreeMap::new();
for f in &fixtures {
assert_ne!(f.home, f.away);
*seen.entry((f.home, f.away)).or_default() += 1;
}
assert!(seen.values().all(|&c| c == 1));
assert_eq!(seen.len(), 20 * 19);
for md in 1..=38u8 {
let mut playing = Vec::new();
for f in fixtures.iter().filter(|f| f.matchday == md) {
playing.push(f.home);
playing.push(f.away);
}
playing.sort();
playing.dedup();
assert_eq!(playing.len(), 20, "matchday {md}");
}
}
}