use crate::app::teams::TeamMode;
use crate::sim::MAX_PLAYERS;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Who(pub [u8; MAX_PLAYERS]);
impl Default for Who {
fn default() -> Self {
Who(std::array::from_fn(|seat| seat as u8))
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Pick {
pub mates: Vec<u8>,
pub swaps: Vec<(u8, u8)>,
}
impl Pick {
pub fn moved(&self, seat: u8) -> u8 {
self.swaps
.iter()
.find_map(|&(a, b)| match seat {
s if s == a => Some(b),
s if s == b => Some(a),
_ => None,
})
.unwrap_or(seat)
}
}
pub fn picks(
mode: TeamMode,
seats: u8,
seat: u8,
fixed: &[u8],
pinned: &[u8],
) -> (Vec<Pick>, bool) {
if mode == TeamMode::Solo || !mode.fits(seats) || seat >= seats {
return (Vec::new(), false);
}
let size = usize::from(seats / mode.teams(seats));
let others: Vec<u8> = (0..seats).filter(|&other| other != seat).collect();
let stays = |other: u8| fixed.contains(&other) || pinned.contains(&other);
let stays_unpinned = |other: u8| fixed.contains(&other);
let mut out = Vec::new();
let mut held_back = false;
for mates in choose(&others, size - 1) {
match placed(mode, seats, seat, &mates, stays) {
Some(swaps) => out.push(Pick { mates, swaps }),
None => held_back |= placed(mode, seats, seat, &mates, stays_unpinned).is_some(),
}
}
(out, held_back)
}
pub fn mates_of(mode: TeamMode, seats: u8, seat: u8) -> Vec<u8> {
mode.seats_of(mode.team_of(seat), seats)
.into_iter()
.filter(|&other| other != seat)
.collect()
}
pub fn next_pick(
mode: TeamMode,
seats: u8,
seat: u8,
(fixed, pinned): (&[u8], &[u8]),
who: &[u8],
) -> Option<Pick> {
let (mut all, _) = picks(mode, seats, seat, fixed, pinned);
let people = |mates: &[u8]| {
let mut ids: Vec<u8> = mates
.iter()
.map(|&mate| who.get(usize::from(mate)).copied().unwrap_or(mate))
.collect();
ids.sort_unstable();
ids
};
all.sort_by_key(|pick| people(&pick.mates));
let now = mates_of(mode, seats, seat);
let at = all.iter().position(|pick| pick.mates == now)?;
let next = all.get((at + 1) % all.len())?;
(next.mates != now).then(|| next.clone())
}
pub fn reseated(who: &mut [u8], pick: &Pick) {
for &(a, b) in &pick.swaps {
who.swap(usize::from(a), usize::from(b));
}
}
fn placed(
mode: TeamMode,
seats: u8,
seat: u8,
mates: &[u8],
stays: impl Fn(u8) -> bool,
) -> Option<Vec<(u8, u8)>> {
let own = mode.team_of(seat);
let teams = std::iter::once(own).chain((0..mode.teams(seats)).filter(|&team| team != own));
let wanted: Vec<u8> = std::iter::once(seat).chain(mates.iter().copied()).collect();
for team in teams {
let members = mode.seats_of(team, seats);
let mut coming: Vec<u8> = wanted
.iter()
.copied()
.filter(|s| !members.contains(s))
.collect();
let going: Vec<u8> = members
.iter()
.copied()
.filter(|s| !wanted.contains(s))
.collect();
coming.sort_unstable();
if coming.iter().chain(&going).any(|&s| stays(s)) {
continue;
}
return Some(coming.into_iter().zip(going).collect());
}
None
}
fn choose(from: &[u8], n: usize) -> Vec<Vec<u8>> {
if n == 0 {
return vec![Vec::new()];
}
let mut out = Vec::new();
for (at, &first) in from.iter().enumerate() {
for mut rest in choose(&from[at + 1..], n - 1) {
rest.insert(0, first);
out.push(rest);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn swapped(mut table: Vec<char>, pick: &Pick) -> Vec<char> {
for &(a, b) in &pick.swaps {
table.swap(usize::from(a), usize::from(b));
}
table
}
#[test]
fn p1_picks_each_partner_in_turn() {
let mode = TeamMode::Pairs;
let mut table = vec!['a', 'b', 'c', 'd'];
let mut who = vec![0, 1, 2, 3];
let mut partners = Vec::new();
for _ in 0..6 {
let pick = next_pick(mode, 4, 0, (&[0], &[]), &who).expect("a pick");
assert!(
pick.swaps.iter().all(|&(a, b)| a != 0 && b != 0),
"P1 stays"
);
table = swapped(table, &pick);
reseated(&mut who, &pick);
partners.push(table[1]);
}
assert_eq!(partners, ['c', 'd', 'b', 'c', 'd', 'b'], "round the others");
}
#[test]
fn picking_p1_moves_the_picker() {
let mode = TeamMode::Pairs;
let (all, _) = picks(mode, 4, 3, &[0], &[]);
let with_p1 = all
.iter()
.find(|pick| pick.mates == [0])
.expect("P1 can be picked");
assert_eq!(with_p1.swaps, vec![(3, 1)]);
assert_eq!(with_p1.moved(3), 1, "the picker sits beside P1");
}
#[test]
fn every_pick_is_a_shuffle_of_whole_seats() {
for (mode, seats) in [
(TeamMode::Pairs, 4),
(TeamMode::Pairs, 6),
(TeamMode::Trios, 6),
] {
for seat in 0..seats {
let (all, held) = picks(mode, seats, seat, &[0], &[]);
assert!(!held);
for other in 0..seats {
if other != seat {
assert!(all.iter().any(|pick| pick.mates.contains(&other)));
}
}
for pick in &all {
let table: Vec<char> = (0..seats).map(|s| char::from(b'a' + s)).collect();
let after = swapped(table.clone(), pick);
let mut sorted = after.clone();
sorted.sort_unstable();
assert_eq!(sorted, table, "{mode:?} {seat} {pick:?}");
assert_eq!(after[0], 'a', "P1 never moves");
let at =
|who: char| after.iter().position(|&c| c == who).expect("seated") as u8;
let picker = at(table[usize::from(seat)]);
for &mate in &pick.mates {
let mate = at(table[usize::from(mate)]);
assert_eq!(mode.team_of(mate), mode.team_of(picker), "{pick:?}");
}
}
}
}
}
#[test]
fn trios_offer_every_two_teammates() {
let (all, _) = picks(TeamMode::Trios, 6, 0, &[0], &[]);
assert_eq!(all.len(), 10);
assert!(
all.iter()
.any(|pick| pick.mates == [2, 4] && pick.swaps.is_empty())
);
}
#[test]
fn a_pinned_seat_never_moves_and_says_so() {
let mode = TeamMode::Pairs;
let who = [0, 1, 2, 3, 4, 5];
assert_eq!(next_pick(mode, 4, 0, (&[0], &[1]), &who), None);
let (all, held) = picks(mode, 4, 2, &[0], &[1]);
assert!(held, "P1 and P2 held back");
assert_eq!(all.len(), 1, "P4, already the partner");
let pick = next_pick(mode, 6, 2, (&[0], &[1]), &who).expect("a pick");
assert!(pick.swaps.iter().all(|&(a, b)| a > 1 && b > 1), "{pick:?}");
let (all, held) = picks(TeamMode::Trios, 6, 0, &[0], &[1]);
assert!(held);
assert!(all.iter().all(|pick| !pick.mates.contains(&1)));
assert_eq!(all.len(), 6, "two of the four free seats");
}
#[test]
fn no_picks_without_teams() {
assert_eq!(picks(TeamMode::Solo, 4, 0, &[0], &[]), (Vec::new(), false));
assert_eq!(picks(TeamMode::Pairs, 5, 0, &[0], &[]), (Vec::new(), false));
assert_eq!(picks(TeamMode::Trios, 4, 0, &[0], &[]), (Vec::new(), false));
}
}