use crate::app::cycle::Cycle;
use crate::app::match_setup::TermsDials;
use crate::app::net;
use crate::app::settings::GameSettings;
use crate::sim::MAX_PLAYERS;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum TeamMode {
#[default]
Solo,
Pairs,
Trios,
}
impl TeamMode {
pub const ALL: [TeamMode; 3] = [TeamMode::Solo, TeamMode::Pairs, TeamMode::Trios];
pub fn fits(self, seats: u8) -> bool {
match self {
TeamMode::Solo => true,
TeamMode::Pairs => seats >= 4 && seats.is_multiple_of(2),
TeamMode::Trios => seats == 6,
}
}
pub fn played_by(self, seats: u8) -> TeamMode {
match self.fits(seats) {
true => self,
false => TeamMode::Solo,
}
}
pub fn fits_from(self, seats: u8) -> Option<u8> {
(seats.saturating_add(1)..=MAX_PLAYERS as u8).find(|&more| self.fits(more))
}
pub fn team_of(self, seat: u8) -> u8 {
match self {
TeamMode::Solo => seat,
TeamMode::Pairs => seat / 2,
TeamMode::Trios => seat % 2,
}
}
pub fn teams(self, seats: u8) -> u8 {
match self {
TeamMode::Solo => seats,
TeamMode::Pairs => seats / 2,
TeamMode::Trios => 2,
}
}
pub fn seats_of(self, team: u8, seats: u8) -> Vec<u8> {
(0..seats).filter(|&s| self.team_of(s) == team).collect()
}
pub fn index(self) -> usize {
Cycle::index(self)
}
pub fn from_index(index: usize) -> TeamMode {
<TeamMode as Cycle>::from_index(index)
}
pub fn key(self) -> &'static str {
match self {
TeamMode::Solo => "ffa",
TeamMode::Pairs => "pairs",
TeamMode::Trios => "trios",
}
}
pub fn from_key(key: &str) -> TeamMode {
match key {
"pairs" | "2v2" => TeamMode::Pairs,
"trios" => TeamMode::Trios,
_ => TeamMode::Solo,
}
}
}
pub fn row_value(tr: &crate::app::i18n::Tr, mode: TeamMode, seats: u8, dialled: bool) -> String {
let played = mode.played_by(seats);
match mode.fits_from(seats).filter(|_| dialled && played != mode) {
Some(from) => crate::app::i18n::fill(
tr.team_mode_waits,
&[
("m", tr.team_mode_names[mode.index()]),
("n", &from.to_string()),
],
),
None => tr.team_modes[played.index()].to_string(),
}
}
impl Cycle for TeamMode {
const VARIANTS: &'static [Self] = &TeamMode::ALL;
}
pub(crate) fn in_play(
local: TeamMode,
online: &net::Online,
replay: Option<&crate::sim::Replay>,
seats: u8,
) -> TeamMode {
let wanted = match (replay, &online.0) {
(Some(replay), _) => recorded(replay),
(None, Some(session)) => session.terms.teams(),
(None, None) => local,
};
wanted.played_by(seats)
}
pub(crate) fn local(settings: &GameSettings, daily: &crate::app::Daily) -> TeamMode {
if daily.active {
TeamMode::Solo
} else {
settings.team_mode
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub(crate) struct LocalScoring<'w> {
pub settings: bevy::prelude::Res<'w, GameSettings>,
pub daily: bevy::prelude::Res<'w, crate::app::Daily>,
}
impl LocalScoring<'_> {
pub fn mode(&self) -> TeamMode {
local(&self.settings, &self.daily)
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub(crate) struct Scoring<'w> {
pub local: LocalScoring<'w>,
pub online: bevy::prelude::Res<'w, net::Online>,
pub playback: bevy::prelude::Res<'w, crate::app::Playback>,
}
impl Scoring<'_> {
pub fn mode(&self, seats: u8) -> TeamMode {
let replay = self.playback.0.as_ref().map(|(replay, _)| replay);
in_play(self.local.mode(), &self.online, replay, seats)
}
pub fn live_mode(&self, seats: u8) -> TeamMode {
in_play(self.local.mode(), &self.online, None, seats)
}
pub fn leaders(&self, scores: &[u32; MAX_PLAYERS], seats: u8) -> [bool; MAX_PLAYERS] {
crate::app::seating::leading_seats(scores, seats, self.mode(seats))
}
}
pub fn recorded(replay: &crate::sim::Replay) -> TeamMode {
TeamMode::from_index(usize::from(replay.teams))
}
pub fn winner_label(
scores: &[u32; MAX_PLAYERS],
seats: u8,
mode: TeamMode,
label: impl Fn(u8) -> String,
) -> Option<String> {
let leaders = crate::app::seating::leading_seats(scores, seats, mode);
let seat = leaders.iter().position(|&led| led)? as u8;
Some(match mode {
TeamMode::Solo => label(seat),
TeamMode::Pairs | TeamMode::Trios => team_label(mode, mode.team_of(seat), seats, label),
})
}
pub fn team_label(mode: TeamMode, team: u8, seats: u8, label: impl Fn(u8) -> String) -> String {
mode.seats_of(team, seats)
.into_iter()
.map(label)
.collect::<Vec<_>>()
.join("+")
}
pub fn team_scores(scores: &[u32; MAX_PLAYERS], seats: u8, mode: TeamMode) -> Vec<u32> {
(0..mode.teams(seats))
.map(|team| {
mode.seats_of(team, seats)
.iter()
.fold(0u32, |total, &seat| {
total.saturating_add(scores[usize::from(seat)])
})
})
.collect()
}
pub fn top_total(scores: &[u32; MAX_PLAYERS], seats: u8, mode: TeamMode) -> u32 {
team_scores(scores, seats, mode)
.into_iter()
.max()
.unwrap_or(0)
}
pub fn placings(scores: &[u32; MAX_PLAYERS], seats: u8, mode: TeamMode) -> Vec<u32> {
let totals = team_scores(scores, seats, mode);
let places = crate::bots::seat::ranks(&totals);
(0..seats)
.map(|seat| places[usize::from(mode.team_of(seat))])
.collect()
}
pub fn label(
settings: &GameSettings,
names: &crate::app::SeatNames,
mode: TeamMode,
team: u8,
seats: u8,
) -> String {
team_label(mode, team, seats, |seat| names.label(settings.tr(), seat))
}
pub fn face_of(mode: TeamMode, team: u8, seats: u8) -> u8 {
mode.seats_of(team, seats).first().copied().unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::castle_spots;
#[test]
fn modes_only_offer_themselves_where_they_fit() {
assert!(TeamMode::Solo.fits(2) && TeamMode::Solo.fits(5));
assert!(!TeamMode::Pairs.fits(2), "one pair is not a match");
assert!(!TeamMode::Pairs.fits(5), "odd seats cannot pair up");
assert!(TeamMode::Pairs.fits(4) && TeamMode::Pairs.fits(6));
assert!(!TeamMode::Trios.fits(4) && TeamMode::Trios.fits(6));
assert_eq!(TeamMode::Pairs.teams(6), 3, "2v2v2");
assert_eq!(TeamMode::Trios.teams(6), 2, "3v3");
}
#[test]
fn every_split_is_symmetric_on_the_board() {
let pair_of = |seat: u8| seat / 2;
for seats in [4u8, 6] {
for team in 0..TeamMode::Pairs.teams(seats) {
let members = TeamMode::Pairs.seats_of(team, seats);
assert_eq!(members.len(), 2);
assert_eq!(
pair_of(members[0]),
pair_of(members[1]),
"a pair team must hold both ends of one axis"
);
}
}
let a = TeamMode::Trios.seats_of(0, 6);
let b = TeamMode::Trios.seats_of(1, 6);
assert_eq!(a, vec![0, 2, 4]);
assert_eq!(b, vec![1, 3, 5]);
for (x, y) in a.iter().zip(&b) {
assert_eq!(pair_of(*x), pair_of(*y), "{x} and {y} are not opposites");
}
let (w, h) = (20u8, 13);
let spots = castle_spots(w, h);
for pair in 0..3usize {
let (x0, y0) = spots[pair * 2];
let (x1, y1) = spots[pair * 2 + 1];
assert_eq!((w - 1 - x0, h - 1 - y0), (x1, y1), "pair {pair}");
}
}
#[test]
fn team_totals_add_up() {
let scores = [1, 2, 3, 4, 5, 6];
assert_eq!(team_scores(&scores, 6, TeamMode::Pairs), vec![3, 7, 11]);
assert_eq!(team_scores(&scores, 6, TeamMode::Trios), vec![9, 12]);
assert_eq!(team_scores(&scores, 4, TeamMode::Pairs), vec![3, 7]);
assert_eq!(
team_scores(&scores, 4, TeamMode::Solo),
vec![1, 2, 3, 4],
"solo teams are seats"
);
let most = [u32::MAX; MAX_PLAYERS];
assert_eq!(team_scores(&most, 6, TeamMode::Trios), vec![u32::MAX; 2]);
assert_eq!(top_total(&most, 4, TeamMode::Pairs), u32::MAX);
}
#[test]
fn keys_round_trip_and_the_old_name_still_loads() {
for mode in TeamMode::ALL {
assert_eq!(TeamMode::from_key(mode.key()), mode);
assert_eq!(TeamMode::from_index(mode.index()), mode);
}
assert_eq!(TeamMode::from_key("2v2"), TeamMode::Pairs, "old setting");
assert_eq!(TeamMode::from_key("nonsense"), TeamMode::Solo);
}
#[test]
fn the_daily_is_every_seat_for_itself_whatever_the_setting() {
let settings = GameSettings {
team_mode: TeamMode::Pairs,
..GameSettings::default()
};
let online = net::Online::default();
let mut daily = crate::app::Daily::default();
let mode = |daily: &crate::app::Daily| in_play(local(&settings, daily), &online, None, 4);
assert_eq!(mode(&daily), TeamMode::Pairs);
daily.active = true;
assert_eq!(mode(&daily), TeamMode::Solo);
}
}