use crate::app::match_setup::{CustomBeaches, MatchConfig};
use crate::app::settings::{GameSettings, SeatInput};
use crate::app::{Screen, SeatController};
use crate::sim::MAX_PLAYERS;
use bevy::prelude::*;
#[derive(Resource, Default)]
pub struct PadSeats {
claims: Vec<(Entity, u8)>,
own: Vec<Entity>,
benched: Vec<Entity>,
pilot: Option<Entity>,
p2_at_keys: bool,
start_asked: bool,
order: PlugOrder,
chairs: Chairs,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Rows([SeatInput; crate::app::binds::BOUND_SEATS]);
impl Rows {
fn choice(self, player: u8) -> Option<SeatInput> {
self.0.get(usize::from(player)).copied()
}
fn is_auto(self, player: u8) -> bool {
matches!(self.choice(player), Some(SeatInput::Auto) | None)
}
}
#[derive(Default)]
struct Chairs(Vec<(Entity, Chair)>);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Chair {
Ai(u8, SeatController),
Added,
}
impl Chair {
fn give_back(self, config: &mut MatchConfig, beaches: &CustomBeaches) {
match self {
Chair::Ai(seat, was) => {
if seat < config.seats && config.controller(seat) == SeatController::Local {
config.controllers[usize::from(seat)] = was;
}
config.clamp_bots();
}
Chair::Added => crate::app::match_setup::reseat(config, config.seats - 1, beaches),
}
}
}
#[derive(Default)]
struct PlugOrder(Vec<Entity>);
struct Lineup {
order: Vec<Entity>,
claims: Vec<(Entity, u8)>,
own: Vec<Entity>,
benched: Vec<Entity>,
pilot: Option<Entity>,
}
impl Lineup {
fn plugged(&self, n: u8) -> Option<Entity> {
self.order.get(usize::from(n)).copied()
}
fn naming(&self, rows: Rows, pad: Entity) -> Option<u8> {
rows.0
.iter()
.position(|choice| matches!(choice, SeatInput::Pad(n) if self.plugged(*n) == Some(pad)))
.map(|seat| seat as u8)
}
fn is_named(&self, rows: Rows, pad: Entity) -> bool {
self.naming(rows, pad).is_some()
}
fn is_claimed(&self, pad: Entity) -> bool {
self.claims.iter().any(|&(claimed, _)| claimed == pad) || self.own.contains(&pad)
}
fn pilot(&self, rows: Rows, players: &[u8]) -> Option<Entity> {
self.pilot.filter(|&pad| {
players.contains(&0)
&& rows.is_auto(0)
&& self.order.contains(&pad)
&& !self.is_named(rows, pad)
&& !self.is_claimed(pad)
&& !self.benched.contains(&pad)
})
}
fn pads(&self, rows: Rows, players: &[u8]) -> Vec<(u8, Option<Entity>)> {
if let [only] = players {
let pad = match rows.choice(0) {
Some(SeatInput::Keys) => None,
Some(SeatInput::Pad(n)) => self.plugged(n),
Some(SeatInput::Auto) | None => self
.pilot(rows, &[0])
.into_iter()
.chain(self.claims.iter().map(|&(pad, _)| pad))
.chain(
self.order
.iter()
.copied()
.filter(|pad| !self.benched.contains(pad)),
)
.next(),
};
return vec![(*only, pad)];
}
let reach = claim_seats(rows, players);
let mut held: Vec<Option<Entity>> = vec![None; reach.len()];
let mut waiting = Vec::new();
for &(pad, seat) in &self.claims {
if self.is_named(rows, pad) {
continue;
}
match reach.iter().position(|&reached| reached == seat) {
Some(at) if held[at].is_none() => held[at] = Some(pad),
Some(_) | None => waiting.push(pad),
}
}
waiting.extend(
self.own
.iter()
.copied()
.filter(|&pad| !self.is_named(rows, pad)),
);
let pilot = self.pilot(rows, players);
let spare = self.order.iter().copied().filter(|&pad| {
!self.is_claimed(pad)
&& !self.benched.contains(&pad)
&& !self.is_named(rows, pad)
&& Some(pad) != pilot
});
let mut open = reach
.iter()
.zip(&held)
.filter(|(_, pad)| pad.is_none())
.map(|(&seat, _)| seat);
let mut dealt: Vec<(u8, Entity)> = Vec::new();
for pad in waiting {
let Some(seat) = open.next() else { break };
dealt.push((seat, pad));
}
dealt.extend(pilot.map(|pad| (0, pad)));
let p1_free = players.contains(&0) && rows.is_auto(0) && pilot.is_none();
dealt.extend(open.chain(p1_free.then_some(0)).zip(spare));
players
.iter()
.map(|&player| {
let pad = match rows.choice(player) {
Some(SeatInput::Keys) => None,
Some(SeatInput::Pad(n)) => self.plugged(n),
Some(SeatInput::Auto) | None => reach
.iter()
.zip(&held)
.find(|(seat, _)| **seat == player)
.and_then(|(_, pad)| *pad)
.or_else(|| {
dealt
.iter()
.find(|(seat, _)| *seat == player)
.map(|(_, pad)| *pad)
}),
};
(player, pad)
})
.collect()
}
fn pad_of(&self, rows: Rows, players: &[u8], player: u8) -> Option<Entity> {
self.pads(rows, players)
.into_iter()
.find(|(seat, _)| *seat == player)
.and_then(|(_, pad)| pad)
}
fn seat_of(&self, rows: Rows, players: &[u8], pad: Entity) -> Option<u8> {
self.pads(rows, players)
.into_iter()
.find(|(_, held)| *held == Some(pad))
.map(|(seat, _)| seat)
}
fn number(&self, pad: Entity) -> Option<u8> {
self.order
.iter()
.position(|&plugged| plugged == pad)
.map(|at| at as u8 + 1)
}
}
fn claim_seats(rows: Rows, players: &[u8]) -> Vec<u8> {
let mut seats: Vec<u8> = players
.iter()
.copied()
.filter(|&player| player != 0 && rows.is_auto(player))
.collect();
seats.sort_unstable();
seats
}
impl PadSeats {
fn lineup(&self, live: impl IntoIterator<Item = Entity>) -> Lineup {
let PadSeats {
claims,
own,
benched,
pilot,
p2_at_keys: _,
start_asked: _,
order: PlugOrder(order),
chairs: _,
} = self;
let mut all = order.clone();
for pad in live {
if !all.contains(&pad) {
all.push(pad);
}
}
Lineup {
order: all,
claims: claims.clone(),
own: own.clone(),
benched: benched.clone(),
pilot: *pilot,
}
}
fn rows(&self, settings: &GameSettings) -> Rows {
let mut rows = settings.seat_input;
if self.p2_at_keys && rows[1] == SeatInput::Auto {
rows[1] = SeatInput::Keys;
}
Rows(rows)
}
pub fn setup_opened(&mut self, pilot: Option<Entity>) {
self.p2_at_keys = false;
self.pilot = pilot.filter(|pad| !self.benched.contains(pad));
}
pub fn p2_at_keys(&self) -> bool {
self.p2_at_keys
}
pub fn p2_sat_down(&mut self) {
self.p2_at_keys = true;
}
pub(super) fn pad_for<'a>(
&self,
pads: &'a Query<(Entity, &Gamepad)>,
settings: &GameSettings,
players: &[u8],
player: u8,
) -> Option<&'a Gamepad> {
let entity = self.pad_entity_for(pads, settings, players, player)?;
pads.get(entity).ok().map(|(_, pad)| pad)
}
pub(super) fn pad_entity_for(
&self,
pads: &Query<(Entity, &Gamepad)>,
settings: &GameSettings,
players: &[u8],
player: u8,
) -> Option<Entity> {
self.lineup(pads.iter().map(|(entity, _)| entity))
.pad_of(self.rows(settings), players, player)
.filter(|&entity| pads.contains(entity))
}
fn claimed(&self) -> impl Iterator<Item = Entity> + '_ {
self.claims
.iter()
.map(|&(pad, _)| pad)
.chain(self.own.iter().copied())
}
pub fn joined(&self, live: &[Entity], settings: &GameSettings, players: &[u8]) -> Vec<u8> {
self.seat_pads(live, settings, players)
.into_iter()
.filter(|seat| seat.joined)
.map(|seat| seat.seat)
.collect()
}
pub fn seat_pads(
&self,
live: &[Entity],
settings: &GameSettings,
players: &[u8],
) -> Vec<SeatPad> {
let lineup = self.lineup(live.iter().copied());
let mut out: Vec<SeatPad> = lineup
.pads(self.rows(settings), players)
.into_iter()
.filter_map(|(seat, pad)| {
let pad = pad.filter(|pad| live.contains(pad))?;
Some(SeatPad {
seat,
number: lineup.number(pad)?,
joined: lineup.is_claimed(pad),
})
})
.collect();
out.sort_by_key(|seat| seat.seat);
out
}
pub fn seats_with_no_controls(
&self,
live: &[Entity],
settings: &GameSettings,
players: &[u8],
) -> Vec<u8> {
self.lineup(live.iter().copied())
.pads(self.rows(settings), players)
.into_iter()
.filter(|(seat, pad)| {
usize::from(*seat) >= crate::app::binds::BOUND_SEATS
&& !pad.is_some_and(|pad| live.contains(&pad))
})
.map(|(seat, _)| seat)
.collect()
}
pub fn a_start_would_seat_someone(
&self,
live: &[Entity],
settings: &GameSettings,
players: &[u8],
) -> bool {
live.iter()
.any(|&pad| self.start_seats_a_friend(live, settings, players, pad))
}
pub fn start_seats_a_friend(
&self,
live: &[Entity],
settings: &GameSettings,
players: &[u8],
pad: Entity,
) -> bool {
let lineup = self.lineup(live.iter().copied());
let rows = self.rows(settings);
live.contains(&pad)
&& !lineup.is_claimed(pad)
&& lineup.naming(rows, pad) != Some(0)
&& lineup.pilot(rows, players) != Some(pad)
}
pub fn start_joins_here(
&self,
live: &[Entity],
settings: &GameSettings,
players: &[u8],
last: Option<Entity>,
) -> bool {
match last.filter(|pad| live.contains(pad)) {
Some(pad) => self.start_seats_a_friend(live, settings, players, pad),
None => self.a_start_would_seat_someone(live, settings, players),
}
}
pub fn take_start(&mut self) -> bool {
std::mem::take(&mut self.start_asked)
}
fn claim(
&mut self,
pad: Entity,
live: &[Entity],
settings: &GameSettings,
config: &mut MatchConfig,
beaches: &CustomBeaches,
) {
self.benched.retain(|&benched| benched != pad);
let rows = self.rows(settings);
let lineup = self.lineup(live.iter().copied());
if lineup.pilot(rows, &config.human_seats()) == Some(pad) {
return;
}
let named = lineup.naming(rows, pad);
match named {
Some(0) => return,
Some(_) => self.own.push(pad),
None => {
if self.claims.len() >= MAX_PLAYERS - 1 {
return;
}
}
}
let mut taken = Vec::new();
loop {
if named.is_none() {
let players = config.human_seats();
let reach = claim_seats(rows, &players);
let seats = self.lineup(live.iter().copied()).pads(rows, &players);
let dealt_here = seats
.iter()
.find(|&&(seat, held)| held == Some(pad) && reach.contains(&seat))
.map(|&(seat, _)| seat);
let free = dealt_here.or_else(|| {
reach
.iter()
.copied()
.find(|&seat| !self.claims.iter().any(|&(_, held)| held == seat))
});
if let Some(seat) = free {
self.claims.retain(|&(claimed, _)| claimed != pad);
self.claims.push((pad, seat));
}
}
let drives = self
.lineup(live.iter().copied())
.seat_of(rows, &config.human_seats(), pad)
.is_some_and(|seat| seat != 0);
if drives {
break;
}
let ai_chair = match named {
Some(seat) => (seat < config.seats
&& config.controller(seat) != SeatController::Local)
.then_some(seat),
None => {
(1..config.seats).find(|&seat| config.controller(seat) != SeatController::Local)
}
};
let chair = if let Some(seat) = ai_chair {
let was = config.controller(seat);
config.controllers[usize::from(seat)] = SeatController::Local;
Chair::Ai(seat, was)
} else if usize::from(config.seats) < MAX_PLAYERS {
let seats = config.seats + 1;
crate::app::match_setup::reseat(config, seats, beaches);
Chair::Added
} else {
for chair in taken.into_iter().rev() {
Chair::give_back(chair, config, beaches);
}
self.let_go(pad);
return;
};
taken.push(chair);
}
self.chairs
.0
.extend(taken.into_iter().map(|chair| (pad, chair)));
}
fn let_go_of_the_seatless(&mut self, live: &[Entity], settings: &GameSettings, players: &[u8]) {
let seats = self
.lineup(live.iter().copied())
.pads(self.rows(settings), players);
let drives = |pad: Entity| {
seats
.iter()
.find(|&&(seat, held)| held == Some(pad) && seat != 0)
.map(|&(seat, _)| seat)
};
for (pad, seat) in &mut self.claims {
if let Some(driven) = drives(*pad) {
*seat = driven;
}
}
let seatless: Vec<Entity> = self
.claimed()
.filter(|&pad| drives(pad).is_none())
.collect();
for pad in seatless {
self.let_go(pad);
self.chairs.0.retain(|&(claimed, _)| claimed != pad);
if !self.benched.contains(&pad) {
self.benched.push(pad);
}
}
}
pub fn swap_seats(
&mut self,
live: &[Entity],
settings: &GameSettings,
players: &[u8],
(a, b): (u8, u8),
) {
let lineup = self.lineup(live.iter().copied());
let seats = lineup.pads(self.rows(settings), players);
let pad_at = |seat: u8| {
seats
.iter()
.find(|&&(at, _)| at == seat)
.and_then(|&(_, pad)| pad)
};
let moving = [(pad_at(a), b), (pad_at(b), a)];
for (_, seat) in &mut self.claims {
if *seat == a {
*seat = b;
} else if *seat == b {
*seat = a;
}
}
for (pad, to) in moving {
if let Some(pad) = pad
&& !lineup.is_claimed(pad)
{
self.claims.push((pad, to));
}
}
self.table_set_by_hand();
}
pub fn table_set_by_hand(&mut self) {
self.chairs.0.clear();
}
fn let_go(&mut self, pad: Entity) {
self.claims.retain(|&(claimed, _)| claimed != pad);
self.own.retain(|&claimed| claimed != pad);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SeatPad {
pub seat: u8,
pub number: u8,
pub joined: bool,
}
pub(super) fn seated(players: impl Iterator<Item = u8>) -> Vec<u8> {
let mut players: Vec<u8> = players.collect();
players.sort_unstable();
players
}
pub fn keep_pad_order(
pads: Query<Entity, With<Gamepad>>,
screen: Res<State<Screen>>,
mut seats: ResMut<PadSeats>,
config: Option<ResMut<MatchConfig>>,
beaches: Option<Res<CustomBeaches>>,
settings: Option<Res<GameSettings>>,
) {
let rows = settings.as_deref().map(|settings| seats.rows(settings));
let PadSeats {
claims,
own,
benched,
pilot,
p2_at_keys: _,
start_asked: _,
order: PlugOrder(order),
chairs: Chairs(chairs),
} = &mut *seats;
for pad in &pads {
if order.contains(&pad) {
continue;
}
let dead = claims
.iter_mut()
.map(|(claimed, _)| claimed)
.chain(own.iter_mut())
.find(|claimed| !pads.contains(**claimed));
if let Some(claimed) = dead {
let gone = std::mem::replace(claimed, pad);
if !benched.contains(&gone) {
benched.push(gone);
}
for chair in chairs.iter_mut().filter(|(claimed, _)| *claimed == gone) {
chair.0 = pad;
}
if let Some(place) = order.iter().position(|&seen| seen == gone) {
order[place] = pad;
continue;
}
}
match order.iter().position(|&seen| !pads.contains(seen)) {
Some(empty) => order[empty] = pad,
None => order.push(pad),
}
}
let in_a_round = matches!(
screen.get(),
Screen::Puzzle | Screen::Versus | Screen::Interlude
);
if in_a_round {
return;
}
claims.retain(|&(claimed, _)| pads.contains(claimed));
own.retain(|&claimed| pads.contains(claimed));
benched.retain(|&gone| pads.contains(gone));
order.retain(|&seen| pads.contains(seen));
if pilot.is_some_and(|pad| !pads.contains(pad)) {
*pilot = None;
}
if *screen.get() != Screen::MatchSetup {
return;
}
let mut config = config;
let shelf = CustomBeaches::default();
let beaches = beaches.as_deref().unwrap_or(&shelf);
let mut given_back = false;
chairs.retain(|&(claimed, chair)| {
if claims.iter().any(|&(pad, _)| pad == claimed) || own.contains(&claimed) {
return true;
}
if let Some(config) = config.as_deref_mut() {
chair.give_back(config, beaches);
given_back = true;
}
false
});
if given_back && let (Some(config), Some(rows)) = (config.as_deref(), rows) {
let reach = claim_seats(rows, &config.human_seats());
for at in 0..claims.len() {
if reach.contains(&claims[at].1) {
continue;
}
let free = reach
.iter()
.copied()
.find(|&seat| !claims.iter().any(|&(_, held)| held == seat));
if let Some(seat) = free {
claims[at].1 = seat;
}
}
}
}
pub fn let_claims_go(mut seats: ResMut<PadSeats>) {
release_the_couch(&mut seats);
seats.pilot = None;
}
fn release_the_couch(seats: &mut PadSeats) {
let PadSeats {
claims,
own,
benched,
pilot: _,
p2_at_keys,
start_asked,
order: _,
chairs: _,
} = seats;
claims.clear();
own.clear();
benched.clear();
*p2_at_keys = false;
*start_asked = false;
}
pub fn forget_the_pilot(mut seats: ResMut<PadSeats>) {
seats.pilot = None;
}
pub fn let_a_lone_players_claims_go(
source: crate::app::session::RoundSource,
mut seats: ResMut<PadSeats>,
) {
if source.online.0.is_none() && crate::app::cursor::seats_played_here(&source).len() == 1 {
release_the_couch(&mut seats);
}
}
pub fn follow_the_setup_hands(
mut keyboard: MessageReader<bevy::input::keyboard::KeyboardInput>,
pads: Query<(Entity, &Gamepad)>,
settings: Res<GameSettings>,
menu: Res<crate::app::match_setup::MatchMenu>,
bots: Res<crate::app::bot_seats::BotSeats>,
mut seats: ResMut<PadSeats>,
) {
let typed: Vec<KeyCode> = keyboard
.read()
.filter(|press| press.state == bevy::input::ButtonState::Pressed)
.map(|press| press.key_code)
.collect();
let step_three = menu.step == crate::app::match_setup::Step::Players;
if menu.naming.is_some() || bots.door_for.card_up() || step_three {
return;
}
let p2_only = crate::app::match_setup::p2_only_keys(&settings);
if typed.iter().any(|key| !p2_only.contains(key)) {
if seats.pilot.is_some() {
seats.pilot = None;
}
return;
}
let live: Vec<Entity> = pads.iter().map(|(entity, _)| entity).collect();
if seats.pilot.is_some_and(|pilot| live.contains(&pilot)) {
return;
}
let rows = seats.rows(&settings);
let lineup = seats.lineup(live.iter().copied());
let setup = super::Face::MatchSetup {
step: crate::app::match_setup::Step::Players,
join: true,
partners: false,
};
let reached = pads.iter().find(|&(entity, pad)| {
let joins = super::scheme::pressed_on(&setup, super::scheme::Does::Join, pad);
pad.get_just_pressed().count() > usize::from(joins)
&& !lineup.is_claimed(entity)
&& !lineup.benched.contains(&entity)
&& !lineup.is_named(rows, entity)
});
if let Some((entity, _)) = reached {
seats.pilot = Some(entity);
}
}
pub fn pad_claim_seats(
pads: Query<(Entity, &Gamepad)>,
mut seats: ResMut<PadSeats>,
mut config: ResMut<MatchConfig>,
beaches: Res<CustomBeaches>,
settings: Res<GameSettings>,
(bots, menu): (
Res<crate::app::bot_seats::BotSeats>,
Res<crate::app::match_setup::MatchMenu>,
),
) {
if bots.door_for.card_up() {
return;
}
let live: Vec<Entity> = pads.iter().map(|(entity, _)| entity).collect();
seats.let_go_of_the_seatless(&live, &settings, &config.human_seats());
let setup = super::Face::MatchSetup {
step: crate::app::match_setup::Step::Players,
join: true,
partners: false,
};
let step_three = menu.step == crate::app::match_setup::Step::Players && menu.naming.is_none();
for (entity, pad) in &pads {
if !super::scheme::pressed_on(&setup, super::scheme::Does::Join, pad) {
continue;
}
let seats_one = seats.start_seats_a_friend(&live, &settings, &config.human_seats(), entity);
if step_three && !seats_one {
seats.start_asked = true;
} else if !seats.claimed().any(|c| c == entity) {
seats.claim(entity, &live, &settings, &mut config, &beaches);
}
}
}
#[cfg(test)]
mod tests {
fn dealt(n: u8) -> crate::app::match_setup::PlayedBy {
crate::app::match_setup::PlayedBy::Pad {
number: n,
joined: false,
}
}
fn joined(n: u8) -> crate::app::match_setup::PlayedBy {
crate::app::match_setup::PlayedBy::Pad {
number: n,
joined: true,
}
}
use super::*;
use crate::app::settings::{GameSettings, SeatInput};
use bevy::ecs::system::RunSystemOnce;
fn with(choices: [SeatInput; 2]) -> GameSettings {
GameSettings {
seat_input: choices,
..GameSettings::default()
}
}
fn lineup(n: usize, claims: &[Option<usize>]) -> (Lineup, Vec<Entity>) {
let mut world = World::new();
let pads: Vec<Entity> = (0..n).map(|_| world.spawn_empty().id()).collect();
let lineup = Lineup {
order: pads.clone(),
claims: claims
.iter()
.enumerate()
.filter_map(|(k, c)| c.map(|i| (pads[i], k as u8 + 1)))
.collect(),
own: Vec::new(),
benched: Vec::new(),
pilot: None,
};
(lineup, pads)
}
#[test]
fn pads_fill_from_p2_up_and_p1_last() {
let auto = Rows([SeatInput::Auto; 2]);
let (four, pads) = lineup(4, &[]);
assert_eq!(four.pad_of(auto, &[0, 1], 1), Some(pads[0]));
assert_eq!(four.pad_of(auto, &[0, 1], 0), Some(pads[1]));
assert_eq!(four.pad_of(auto, &[0, 1, 2, 3], 1), Some(pads[0]));
assert_eq!(four.pad_of(auto, &[0, 1, 2, 3], 3), Some(pads[2]));
assert_eq!(four.pad_of(auto, &[0, 1, 2, 3], 0), Some(pads[3]));
assert_eq!(four.pad_of(auto, &[2], 2), Some(pads[0]), "online");
assert_eq!(four.pad_of(auto, &[0, 1], 2), None, "no cursor, no pad");
}
#[test]
fn claims_hold_their_seats_and_spares_fill_the_rest() {
let auto = Rows([SeatInput::Auto; 2]);
let (lineup, pads) = lineup(4, &[Some(2), None, Some(3)]);
let seats = lineup.pads(auto, &[0, 1, 2, 3]);
assert_eq!(
seats,
vec![
(0, Some(pads[1])),
(1, Some(pads[2])),
(2, Some(pads[0])),
(3, Some(pads[3])),
],
"P2 and P4 claimed, a spare in the hole at P3, the last on P1"
);
}
#[test]
fn a_lone_cursor_plays_by_the_first_row() {
let (lineup, pads) = lineup(3, &[]);
let pad_for_p1 = Rows([SeatInput::Auto, SeatInput::Keys]);
assert_eq!(lineup.pad_of(pad_for_p1, &[1], 1), Some(pads[0]));
let keys_for_p1 = Rows([SeatInput::Keys, SeatInput::Auto]);
assert_eq!(lineup.pad_of(keys_for_p1, &[1], 1), None);
let named = Rows([SeatInput::Pad(2), SeatInput::Keys]);
assert_eq!(lineup.pad_of(named, &[3], 3), Some(pads[2]));
}
#[test]
fn a_named_controller_belongs_to_the_seat_that_named_it() {
let (lineup, pads) = lineup(2, &[]);
let mine = Rows([SeatInput::Pad(0), SeatInput::Auto]);
assert_eq!(lineup.pad_of(mine, &[0, 1], 0), Some(pads[0]));
assert_eq!(
lineup.pad_of(mine, &[0, 1], 1),
Some(pads[1]),
"P2 takes the next one free, not the named one"
);
}
#[test]
fn a_named_controller_is_counted_in_plug_order() {
let (lineup, pads) = lineup(2, &[Some(1)]);
let mine = Rows([SeatInput::Pad(0), SeatInput::Auto]);
assert_eq!(lineup.pad_of(mine, &[0, 1], 0), Some(pads[0]));
assert_eq!(lineup.pad_of(mine, &[0, 1], 1), Some(pads[1]));
}
#[test]
fn keyboard_only_gives_its_pad_up() {
let (lineup, pads) = lineup(1, &[]);
let quiet = Rows([SeatInput::Auto, SeatInput::Keys]);
assert_eq!(lineup.pad_of(quiet, &[0, 1], 1), None);
assert_eq!(lineup.pad_of(quiet, &[0, 1], 0), Some(pads[0]));
}
#[test]
fn two_seats_may_name_the_same_controller() {
let (lineup, pads) = lineup(2, &[]);
let both = Rows([SeatInput::Pad(1), SeatInput::Pad(1)]);
assert_eq!(lineup.pad_of(both, &[0, 1], 0), Some(pads[1]));
assert_eq!(lineup.pad_of(both, &[0, 1], 1), Some(pads[1]));
assert_eq!(lineup.pad_of(both, &[0, 1, 2], 2), Some(pads[0]));
}
fn setup_table(config: MatchConfig, pads: usize) -> (App, Vec<Entity>) {
use crate::app::match_setup::{MatchMenu, Step};
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.insert_resource(State::new(Screen::MatchSetup));
app.init_resource::<PadSeats>();
app.init_resource::<GameSettings>();
app.init_resource::<crate::app::bot_seats::BotSeats>();
app.init_resource::<CustomBeaches>();
app.insert_resource(config);
app.init_resource::<ButtonInput<KeyCode>>();
app.add_message::<bevy::input::keyboard::KeyboardInput>();
app.insert_resource(MatchMenu {
step: Step::Players,
..MatchMenu::default()
});
app.init_resource::<crate::app::tournament::Tournament>();
app.init_resource::<crate::app::settings::screen::SettingsMenu>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::progress::Progress>();
app.init_resource::<crate::app::net::Online>();
app.init_resource::<crate::app::Playback>();
app.init_resource::<crate::app::Daily>();
app.init_resource::<crate::app::Resuming>();
app.init_resource::<crate::app::rematch::LocalTable>();
app.insert_resource(crate::app::Sandbox(false));
app.init_resource::<super::super::PadFace>();
app.init_resource::<super::super::LastDevice>();
app.init_resource::<super::super::LastPad>();
app.init_resource::<super::super::Bridged>();
app.init_resource::<super::super::PadPick>();
crate::app::schedule::add_claim_release(&mut app);
app.add_systems(
OnEnter(Screen::MatchSetup),
crate::app::match_setup::enter_match_setup,
);
app.add_systems(OnEnter(Screen::Menu), forget_the_pilot);
let face = |screen: Res<State<Screen>>,
menu: Res<MatchMenu>,
(seats, settings, config, last_pad): (
Res<PadSeats>,
Res<GameSettings>,
Res<MatchConfig>,
Res<super::super::LastPad>,
),
pads: Query<Entity, With<Gamepad>>,
mut face: ResMut<super::super::PadFace>| {
let live: Vec<Entity> = pads.iter().collect();
face.0 = match screen.get() {
Screen::Menu => super::super::Face::Menu,
Screen::MatchSetup => super::super::Face::MatchSetup {
step: menu.step,
join: seats.start_joins_here(
&live,
&settings,
&config.human_seats(),
last_pad.0,
),
partners: crate::app::match_setup::setup_teams(&settings, &config)
!= crate::app::teams::TeamMode::Solo,
},
Screen::Settings => super::super::Face::Settings,
Screen::Puzzle
| Screen::Versus
| Screen::Editor
| Screen::Lobby
| Screen::Controls
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Interlude
| Screen::Language
| Screen::NewVersion => super::super::Face::Round,
};
};
app.add_systems(
Update,
(
keep_pad_order,
face,
super::super::track_last_device,
super::super::pad_menu_bridge,
super::super::pad_bridge_release,
(
follow_the_setup_hands,
pad_claim_seats,
crate::app::match_setup::match_setup_input,
)
.chain()
.run_if(in_state(Screen::MatchSetup)),
crate::app::settings::screen::settings_input.run_if(in_state(Screen::Settings)),
)
.chain(),
);
let plugged = (0..pads)
.map(|_| app.world_mut().spawn(Gamepad::default()).id())
.collect();
app.update();
(app, plugged)
}
fn press_start(app: &mut App, pad: Entity) {
press(app, pad, GamepadButton::Start);
}
fn press(app: &mut App, pad: Entity, button: GamepadButton) {
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("pad");
gamepad.digital_mut().press(button);
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.clear();
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("pad");
gamepad.digital_mut().clear();
gamepad.digital_mut().release(button);
gamepad.digital_mut().clear();
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.clear();
}
fn into_the_setup_by(app: &mut App, pad: Option<Entity>) {
use crate::app::match_setup::{MatchMenu, Step};
go(app, Screen::Menu);
match pad {
Some(pad) => press(app, pad, GamepadButton::DPadDown),
None => tap(app, KeyCode::ArrowDown),
}
go(app, Screen::MatchSetup);
app.world_mut().resource_mut::<MatchMenu>().step = Step::Players;
app.update();
}
fn tap(app: &mut App, key: KeyCode) {
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(key);
app.world_mut()
.write_message(bevy::input::keyboard::KeyboardInput {
key_code: key,
logical_key: bevy::input::keyboard::Key::Unidentified(
bevy::input::keyboard::NativeKey::Unidentified,
),
state: bevy::input::ButtonState::Pressed,
text: None,
repeat: false,
window: Entity::PLACEHOLDER,
});
app.update();
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.release(key);
keys.clear();
}
fn go(app: &mut App, screen: Screen) {
app.world_mut()
.resource_mut::<NextState<Screen>>()
.set(screen);
app.update();
}
fn dial_players(app: &mut App, right: bool) {
use crate::app::match_setup::{MatchMenu, Step};
let mut menu = app.world_mut().resource_mut::<MatchMenu>();
menu.step = Step::Game;
menu.game_row = 0;
tap(
app,
if right {
KeyCode::ArrowRight
} else {
KeyCode::ArrowLeft
},
);
app.world_mut().resource_mut::<MatchMenu>().step = Step::Players;
}
fn turn_seat(app: &mut App, seat: u8, right: bool) {
app.world_mut()
.resource_mut::<crate::app::match_setup::MatchMenu>()
.seat = seat;
tap(
app,
if right {
KeyCode::ArrowRight
} else {
KeyCode::ArrowLeft
},
);
}
fn p2_input_one_notch(app: &mut App) {
use crate::app::settings::screen::{Row, SettingsMenu};
go(app, Screen::Settings);
let at = Row::ALL
.iter()
.position(|r| *r == Row::InputP2)
.expect("the row");
app.world_mut().resource_mut::<SettingsMenu>().selected = at;
tap(app, KeyCode::ArrowRight);
go(app, Screen::MatchSetup);
}
fn told(app: &mut App) -> (u8, u8, Vec<u8>) {
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let config = app.world().resource::<MatchConfig>();
let settings = app.world().resource::<GameSettings>();
let joined =
app.world()
.resource::<PadSeats>()
.joined(&live, settings, &config.human_seats());
(config.seats, config.ai_seats(), joined)
}
fn rows(app: &mut App) -> Vec<crate::app::match_setup::PlayedBy> {
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let config = app.world().resource::<MatchConfig>();
let settings = app.world().resource::<GameSettings>();
let seats = app.world().resource::<PadSeats>();
let pads = seats.seat_pads(&live, settings, &config.human_seats());
(0..config.seats)
.map(|seat| {
crate::app::match_setup::played_by(
config,
seat,
&pads,
seats.p2_at_keys(),
settings,
)
})
.collect()
}
fn driving(app: &mut App, seat: u8) -> Option<Entity> {
let humans = app.world().resource::<MatchConfig>().human_seats();
app.world_mut()
.run_system_once(
move |pads: Query<(Entity, &Gamepad)>,
seats: Res<PadSeats>,
settings: Res<GameSettings>| {
seats.pad_entity_for(&pads, &settings, &humans, seat)
},
)
.expect("runs")
}
fn drives(app: &mut App, expected: &[(u8, Entity)]) {
for &(seat, pad) in expected {
assert_eq!(driving(app, seat), Some(pad), "P{} drives it", seat + 1);
}
assert!(
!expected
.iter()
.any(|&(_, pad)| driving(app, 0) == Some(pad)),
"a claim steers P1"
);
}
#[test]
fn two_friends_join_a_table_of_two_as_p2_and_p3() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
assert_eq!(told(&mut app), (2, 0, vec![]));
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 0, vec![1]), "P2's empty seat");
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]), "P2 kept, P3 added");
drives(&mut app, &[(1, pads[0]), (2, pads[1])]);
}
#[test]
fn friends_fill_the_seats_dialled_before_any_are_added() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(3, 0), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
drives(&mut app, &[(1, pads[0]), (2, pads[1])]);
}
#[test]
fn a_friend_takes_the_ais_chair_and_keeps_its_name() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 2);
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 0, vec![1]), "two people, no AI");
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]), "still P2, then P3");
drives(&mut app, &[(1, pads[0]), (2, pads[1])]);
}
#[test]
fn a_friends_start_never_steers_p1s_named_pad() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
app.insert_resource(with([SeatInput::Pad(0), SeatInput::Auto]));
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (2, 0, vec![1]), "the friend is P2");
assert_eq!(driving(&mut app, 0), Some(pads[0]), "P1's own pad");
assert_eq!(driving(&mut app, 1), Some(pads[1]));
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 0, vec![1]), "P1 was already here");
assert_eq!(driving(&mut app, 0), Some(pads[0]));
assert_eq!(driving(&mut app, 1), Some(pads[1]));
}
#[test]
fn a_seated_players_named_pad_takes_no_chair() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 1);
app.insert_resource(with([SeatInput::Pad(0), SeatInput::Auto]));
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 1, vec![]), "the AI still plays");
assert_eq!(driving(&mut app, 0), Some(pads[0]));
}
#[test]
fn a_named_pad_brings_its_seat_to_the_table() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 2);
app.insert_resource(with([SeatInput::Auto, SeatInput::Pad(1)]));
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (2, 0, vec![1]));
assert_eq!(driving(&mut app, 1), Some(pads[1]));
app.world_mut().entity_mut(pads[1]).remove::<Gamepad>();
app.update();
assert_eq!(told(&mut app), (2, 1, vec![]));
}
#[test]
fn a_keyboard_only_p2_sends_a_friend_to_p3() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 1);
app.insert_resource(with([SeatInput::Auto, SeatInput::Keys]));
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (3, 0, vec![2]));
drives(&mut app, &[(2, pads[0])]);
app.world_mut().entity_mut(pads[0]).remove::<Gamepad>();
app.update();
assert_eq!(told(&mut app), (2, 1, vec![]));
}
#[test]
fn a_full_table_takes_no_more_claims() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(4, 2), 6);
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (4, 2, vec![1]), "P2 was a human seat");
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (4, 1, vec![1, 2]), "the AI's chair");
for &pad in &pads[2..] {
press_start(&mut app, pad);
}
assert_eq!(told(&mut app), (6, 0, vec![1, 2, 3, 4, 5]));
assert!(
!app.world()
.resource::<PadSeats>()
.claimed()
.any(|c| c == pads[5])
);
drives(
&mut app,
&[
(1, pads[0]),
(2, pads[1]),
(3, pads[2]),
(4, pads[3]),
(5, pads[4]),
],
);
}
#[test]
fn no_claim_while_the_bot_card_is_up() {
use crate::app::bot_seats::{BotSeats, DoorFor};
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 1);
app.world_mut().resource_mut::<BotSeats>().door_for = DoorFor::Couch;
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 1, vec![]), "the bot's chair kept");
let seats = app.world().resource::<PadSeats>();
assert!(seats.claimed().next().is_none(), "no claim");
}
#[test]
fn a_table_grown_by_pads_is_moved_to_a_beach_that_seats_it() {
use crate::app::match_setup::{MapChoice, holds};
let (mut app, pads) = setup_table(
MatchConfig {
map: MapChoice::Classic,
..MatchConfig::with_ai(2, 0)
},
4,
);
for &pad in &pads {
press_start(&mut app, pad);
}
let config = app.world().resource::<MatchConfig>();
assert_eq!(config.seats, 5, "P2 filled, then P3 to P5 added");
assert!(holds(config.map, 5), "five on {:?}", config.map);
}
#[test]
fn a_start_that_moves_the_map_says_so_and_launches_it() {
use crate::app::i18n::EN;
use crate::app::match_setup::{
Card, MapChoice, MatchMenu, card_config, local_board, map_moved, moved_note,
picked_card,
};
let (mut app, pads) = setup_table(
MatchConfig {
seats: 4,
map: MapChoice::GenSmall,
..MatchConfig::default()
},
4,
);
app.world_mut().resource_mut::<MatchMenu>().seed = 0x5EED;
for &pad in &pads[..3] {
press_start(&mut app, pad);
}
let shelf = CustomBeaches::default();
let config = app.world().resource::<MatchConfig>();
assert_eq!((config.seats, config.map), (4, MapChoice::GenSmall));
assert_eq!(moved_note(&EN, config, &shelf), None);
press_start(&mut app, pads[3]);
let config = app.world().resource::<MatchConfig>().clone();
assert_eq!((config.seats, config.map), (5, MapChoice::GenLarge));
assert_eq!(
map_moved(&config, &shelf),
Some((
Card::Map(MapChoice::GenSmall),
Card::Map(MapChoice::GenLarge)
))
);
let note = moved_note(&EN, &config, &shelf).expect("step 3 says it");
assert!(note.contains("Small") && note.contains("Large") && note.contains('5'));
tap(&mut app, KeyCode::Enter);
let launched = app.world().resource::<MatchConfig>();
assert!(launched.armed);
assert_eq!(launched.seed, Some(0x5EED));
let shown = card_config(&config, picked_card(&config, &shelf), &shelf).expect("a card");
assert_eq!(shown.map, MapChoice::GenLarge, "the card the note names");
let played = local_board(launched, &shelf, 0x5EED);
let drawn = local_board(&shown, &shelf, 0x5EED);
assert_eq!(played.state_hash(), drawn.state_hash());
}
#[test]
fn a_pad_that_drops_and_rejoins_keeps_one_seat() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
for _ in 0..3 {
app.world_mut().entity_mut(pads[1]).remove::<Gamepad>();
app.update();
assert_eq!(told(&mut app), (2, 0, vec![1]), "the added seat goes");
app.world_mut()
.entity_mut(pads[1])
.insert(Gamepad::default());
app.update();
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
}
let (mut app, pads) = setup_table(MatchConfig::with_ai(3, 1), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
app.world_mut().entity_mut(pads[1]).remove::<Gamepad>();
app.update();
assert_eq!(told(&mut app), (3, 1, vec![1]), "the AI sits back down");
}
#[test]
fn a_friend_who_leaves_does_not_renumber_the_rest() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(3, 0), 3);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
app.world_mut().entity_mut(pads[0]).remove::<Gamepad>();
app.update();
assert_eq!(told(&mut app), (3, 0, vec![2]), "still P3");
assert_eq!(driving(&mut app, 2), Some(pads[1]));
press_start(&mut app, pads[2]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]), "the new one fills P2");
drives(&mut app, &[(1, pads[2]), (2, pads[1])]);
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
app.world_mut().entity_mut(pads[0]).remove::<Gamepad>();
app.update();
assert_eq!(
told(&mut app),
(3, 1, vec![2]),
"the AI back in the chair it gave up, the friend still at P3"
);
assert_eq!(driving(&mut app, 2), Some(pads[1]));
}
#[test]
fn a_seat_with_no_controls_is_named() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(4, 0), 2);
let idle = |app: &mut App| {
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let settings = app.world().resource::<GameSettings>();
app.world().resource::<PadSeats>().seats_with_no_controls(
&live,
settings,
&[0, 1, 2, 3],
)
};
assert_eq!(idle(&mut app), vec![3]);
app.world_mut().entity_mut(pads[1]).remove::<Gamepad>();
app.update();
assert_eq!(idle(&mut app), vec![2, 3]);
}
#[test]
fn a_puzzle_after_a_couch_match_follows_the_first_pad() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
press_start(&mut app, pads[1]);
let lone = |app: &mut App| {
app.world_mut()
.run_system_once(
|pads: Query<(Entity, &Gamepad)>,
seats: Res<PadSeats>,
settings: Res<GameSettings>| {
seats.pad_entity_for(&pads, &settings, &[0], 0)
},
)
.expect("runs")
};
assert_eq!(lone(&mut app), Some(pads[1]), "online takes the claim");
go(&mut app, Screen::Puzzle);
assert_eq!(lone(&mut app), Some(pads[0]));
}
#[test]
fn the_daily_after_a_couch_match_follows_the_first_pad() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (2, 0, vec![1]));
tap(&mut app, KeyCode::Enter);
app.update();
assert_eq!(
*app.world().resource::<State<Screen>>().get(),
Screen::Versus
);
drives(&mut app, &[(1, pads[1])]);
go(&mut app, Screen::Menu);
app.world_mut().resource_mut::<crate::app::Daily>().active = true;
go(&mut app, Screen::Versus);
let lone = app
.world_mut()
.run_system_once(
|pads: Query<(Entity, &Gamepad)>,
seats: Res<PadSeats>,
settings: Res<GameSettings>| {
seats.pad_entity_for(&pads, &settings, &[0], 0)
},
)
.expect("runs");
assert_eq!(lone, Some(pads[0]), "the daily's one player");
}
#[test]
fn p1s_dealt_pad_is_offered_to_join() {
let offered = |config: MatchConfig| {
let (mut app, _) = setup_table(config, 1);
app.update();
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let players = app.world().resource::<MatchConfig>().human_seats();
let settings = app.world().resource::<GameSettings>();
app.world()
.resource::<PadSeats>()
.a_start_would_seat_someone(&live, settings, &players)
};
assert!(offered(MatchConfig::with_ai(2, 1)), "P1 against the AI");
assert!(offered(MatchConfig::with_ai(2, 0)), "a seat for a friend");
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 1);
into_the_setup_by(&mut app, Some(pads[0]));
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let players = app.world().resource::<MatchConfig>().human_seats();
let settings = app.world().resource::<GameSettings>();
let seats = app.world().resource::<PadSeats>();
assert!(
!seats.a_start_would_seat_someone(&live, settings, &players),
"the pad working the setup"
);
}
#[test]
fn a_seated_players_named_pad_joins_as_that_seat() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
app.insert_resource(with([SeatInput::Auto, SeatInput::Pad(1)]));
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (2, 0, vec![1]), "P2 is here");
drives(&mut app, &[(1, pads[1])]);
tap(&mut app, KeyCode::Enter);
assert!(
app.world().resource::<MatchConfig>().armed,
"the match starts"
);
}
#[test]
fn p1s_own_pad_is_not_offered_to_join() {
use crate::app::match_setup::PlayedBy;
let (mut app, pads) = setup_table(MatchConfig::default(), 1);
app.insert_resource(with([SeatInput::Pad(0), SeatInput::Auto]));
app.update();
let offered = |app: &mut App| {
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let settings = app.world().resource::<GameSettings>();
let players = app.world().resource::<MatchConfig>().human_seats();
app.world()
.resource::<PadSeats>()
.a_start_would_seat_someone(&live, settings, &players)
};
assert!(!offered(&mut app), "no pad to press Start on");
use crate::app::match_setup::{MatchMenu, Step};
app.world_mut().resource_mut::<MatchMenu>().step = Step::Game;
press_start(&mut app, pads[0]);
app.world_mut().resource_mut::<MatchMenu>().step = Step::Players;
app.update();
assert_eq!(told(&mut app), (2, 0, vec![]));
assert_eq!(
rows(&mut app),
vec![dealt(1), PlayedBy::Open { keys: true }]
);
app.world_mut().spawn(Gamepad::default());
app.update();
assert!(offered(&mut app));
}
#[test]
fn a_friend_pushed_out_of_p2_by_settings_sits_down_again() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 1);
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 0, vec![1]));
p2_input_one_notch(&mut app);
assert_eq!(
app.world().resource::<GameSettings>().seat_input[1],
SeatInput::Keys
);
assert_eq!(told(&mut app), (2, 0, vec![]), "P2 is the keyboard's");
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (3, 0, vec![2]), "the friend at P3");
drives(&mut app, &[(2, pads[0])]);
assert_eq!(driving(&mut app, 0), None, "P1 on the keyboard");
}
#[test]
fn a_friend_dialled_off_the_table_sits_down_again() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
dial_players(&mut app, false);
app.update();
assert_eq!(told(&mut app), (2, 0, vec![1]), "the third seat went");
drives(&mut app, &[(1, pads[0])]);
assert_eq!(driving(&mut app, 0), None, "nor is P1 the friend's");
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]), "back at P3");
drives(&mut app, &[(1, pads[0]), (2, pads[1])]);
}
#[test]
fn a_table_set_by_the_dials_keeps_what_they_say() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(2, 1), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
turn_seat(&mut app, 2, true);
app.update();
assert_eq!(told(&mut app), (3, 1, vec![1]), "the AI has P3");
app.world_mut().entity_mut(pads[0]).remove::<Gamepad>();
app.update();
assert_eq!(told(&mut app), (3, 1, vec![]), "as the dials say");
}
#[test]
fn a_pad_back_after_its_claim_was_taken_over_drives_nothing() {
let (mut app, pads) = setup_table(MatchConfig::default(), 1);
press_start(&mut app, pads[0]);
tap(&mut app, KeyCode::Enter);
app.update();
assert_eq!(
*app.world().resource::<State<Screen>>().get(),
Screen::Versus
);
app.world_mut().entity_mut(pads[0]).remove::<Gamepad>();
let spare = app.world_mut().spawn(Gamepad::default()).id();
app.update();
drives(&mut app, &[(1, spare)]);
app.world_mut()
.entity_mut(pads[0])
.insert(Gamepad::default());
app.update();
drives(&mut app, &[(1, spare)]);
assert_eq!(driving(&mut app, 0), None, "P1 is the keyboard's");
go(&mut app, Screen::MatchSetup);
assert_eq!(driving(&mut app, 0), None, "still benched");
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (3, 0, vec![1, 2]));
drives(&mut app, &[(1, spare), (2, pads[0])]);
}
#[test]
fn a_pad_gone_in_the_lobby_changes_nothing_until_the_setup_screen() {
let (mut app, pads) = setup_table(MatchConfig::with_ai(4, 2), 2);
press_start(&mut app, pads[0]);
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (4, 1, vec![1, 2]));
go(&mut app, Screen::Lobby);
app.world_mut().resource_mut::<MatchConfig>().lobby_bots = 1;
app.world_mut().entity_mut(pads[1]).remove::<Gamepad>();
app.update();
let config = app.world().resource::<MatchConfig>();
assert_eq!(
(config.seats, config.ai_seats(), config.lobby_bots),
(4, 1, 1),
"the terms stand"
);
go(&mut app, Screen::MatchSetup);
let config = app.world().resource::<MatchConfig>();
assert_eq!(config.ai_seats(), 2, "owed, and given back to the AI there");
assert!(app.world().resource::<PadSeats>().chairs.0.is_empty());
}
#[test]
fn a_start_on_an_early_step_is_seated_on_step_three() {
use crate::app::match_setup::{MatchMenu, PlayedBy, Step};
for step in [Step::Game, Step::Map] {
let (mut app, pads) = setup_table(MatchConfig::with_ai(3, 1), 2);
app.world_mut().resource_mut::<MatchMenu>().step = step;
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (3, 1, vec![1]), "{step:?}");
app.world_mut().resource_mut::<MatchMenu>().step = Step::Players;
app.update();
assert_eq!(
rows(&mut app),
vec![
dealt(1),
joined(2),
PlayedBy::Ai(crate::sim::BotLevel::Normal),
],
"{step:?}: controller 2, the second plugged in"
);
drives(&mut app, &[(1, pads[1])]);
}
}
#[test]
fn open_seats_are_filled_with_the_ai_at_start() {
use crate::app::SeatController;
use crate::app::match_setup::PlayedBy;
let mut config = MatchConfig {
seats: 4,
..MatchConfig::default()
};
config.ai_level = crate::sim::BotLevel::Hard;
let (mut app, pads) = setup_table(config, 1);
press_start(&mut app, pads[0]);
assert_eq!(
rows(&mut app),
vec![
PlayedBy::Keys(0),
joined(1),
PlayedBy::Open { keys: false },
PlayedBy::Open { keys: false },
]
);
tap(&mut app, KeyCode::Enter);
let config = app.world().resource::<MatchConfig>();
assert!(config.armed, "Start match works with seats open");
assert_eq!(
config.controllers[..4],
[
SeatController::Local,
SeatController::Local,
SeatController::Ai(crate::sim::BotLevel::Hard),
SeatController::Ai(crate::sim::BotLevel::Hard),
]
);
drives(&mut app, &[(1, pads[0])]);
}
#[test]
fn a_pad_dealt_a_seat_plays_it_and_never_p1() {
use crate::app::SeatController;
use crate::app::match_setup::PlayedBy;
let (mut app, pads) = setup_table(MatchConfig::with_ai(3, 0), 2);
press_start(&mut app, pads[0]);
assert_eq!(
rows(&mut app),
vec![PlayedBy::Keys(0), joined(1), dealt(2)],
"the friend who pressed Start joined, the other pad was dealt"
);
tap(&mut app, KeyCode::Enter);
let config = app.world().resource::<MatchConfig>();
assert!(config.armed);
assert_eq!(config.controllers[..3], [SeatController::Local; 3]);
drives(&mut app, &[(1, pads[0]), (2, pads[1])]);
assert_eq!(driving(&mut app, 0), None, "P1 is the keyboard's");
}
#[test]
fn the_keyboard_and_a_pad_play_two_without_a_start() {
use crate::app::SeatController;
use crate::app::match_setup::PlayedBy;
let (mut app, pads) = setup_table(MatchConfig::default(), 1);
into_the_setup_by(&mut app, None);
assert_eq!(rows(&mut app), vec![PlayedBy::Keys(0), dealt(1)]);
tap(&mut app, KeyCode::Enter);
let config = app.world().resource::<MatchConfig>();
assert!(config.armed);
assert_eq!(config.controller(1), SeatController::Local, "not the AI");
drives(&mut app, &[(1, pads[0])]);
assert_eq!(driving(&mut app, 0), None, "P1 is the keyboard's");
let (mut app, pads) = setup_table(MatchConfig::default(), 1);
into_the_setup_by(&mut app, Some(pads[0]));
tap(&mut app, KeyCode::KeyS);
let pad_at_p1 = vec![dealt(1), PlayedBy::Open { keys: true }];
assert_eq!(rows(&mut app), pad_at_p1);
use crate::app::match_setup::{MatchMenu, Step};
app.world_mut().resource_mut::<MatchMenu>().step = Step::Game;
tap(&mut app, KeyCode::KeyS);
assert_eq!(rows(&mut app), vec![PlayedBy::Keys(0), dealt(1)]);
}
#[test]
fn a_lone_pad_working_the_setup_plays_p1_against_the_ai() {
use crate::app::SeatController;
use crate::app::match_setup::PlayedBy;
let (mut app, pads) = setup_table(MatchConfig::default(), 1);
into_the_setup_by(&mut app, Some(pads[0]));
assert_eq!(
rows(&mut app),
vec![dealt(1), PlayedBy::Open { keys: true }]
);
press_start(&mut app, pads[0]);
assert_eq!(
*app.world().resource::<State<Screen>>().get(),
Screen::Versus
);
let config = app.world().resource::<MatchConfig>();
assert_eq!(
config.controllers[..2],
[SeatController::Local, SeatController::Ai(config.ai_level)],
"P1 a person (the seat trophies count), P2 the AI"
);
assert_eq!(driving(&mut app, 0), Some(pads[0]), "the pad steers P1");
let (mut app, pads) = setup_table(MatchConfig::default(), 1);
into_the_setup_by(&mut app, Some(pads[0]));
tap(&mut app, KeyCode::KeyI);
assert_eq!(rows(&mut app), vec![dealt(1), PlayedBy::Keys(1)]);
}
#[test]
fn the_pad_working_the_setup_is_p1_and_the_other_p2() {
for (pilot, other) in [(1, 0), (0, 1)] {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
into_the_setup_by(&mut app, Some(pads[pilot]));
assert_eq!(
rows(&mut app),
vec![dealt(pilot as u8 + 1), dealt(other as u8 + 1)]
);
assert_eq!(driving(&mut app, 0), Some(pads[pilot]));
assert_eq!(driving(&mut app, 1), Some(pads[other]));
press_start(&mut app, pads[other]);
assert_eq!(told(&mut app), (2, 0, vec![1]));
assert_eq!(driving(&mut app, 0), Some(pads[pilot]));
assert_eq!(driving(&mut app, 1), Some(pads[other]));
}
}
#[test]
fn a_named_pads_start_leaves_p1_its_pad() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
app.insert_resource(with([SeatInput::Auto, SeatInput::Pad(1)]));
app.update();
assert_eq!(driving(&mut app, 0), Some(pads[0]));
assert_eq!(driving(&mut app, 1), Some(pads[1]));
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (2, 0, vec![1]), "P2 is here");
assert_eq!(driving(&mut app, 0), Some(pads[0]), "P1 keeps its pad");
assert_eq!(driving(&mut app, 1), Some(pads[1]));
}
#[test]
fn a_dealt_pads_start_leaves_p1_its_pad() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
assert_eq!(driving(&mut app, 1), Some(pads[0]));
assert_eq!(driving(&mut app, 0), Some(pads[1]));
press_start(&mut app, pads[0]);
assert_eq!(told(&mut app), (2, 0, vec![1]));
assert_eq!(driving(&mut app, 1), Some(pads[0]));
assert_eq!(driving(&mut app, 0), Some(pads[1]), "P1 keeps its pad");
}
#[test]
fn p2s_keys_send_the_pad_to_p3() {
use crate::app::SeatController;
use crate::app::match_setup::PlayedBy;
let (mut app, pads) = setup_table(MatchConfig::with_ai(3, 0), 1);
tap(&mut app, KeyCode::KeyI);
assert_eq!(
rows(&mut app),
vec![PlayedBy::Keys(0), PlayedBy::Keys(1), dealt(1)]
);
tap(&mut app, KeyCode::Enter);
let config = app.world().resource::<MatchConfig>();
assert!(config.armed);
assert_eq!(config.controllers[..3], [SeatController::Local; 3]);
assert_eq!(driving(&mut app, 1), None, "P2 on the keys alone");
assert_eq!(driving(&mut app, 2), Some(pads[0]));
}
#[test]
fn two_pads_play_two_and_step_three_names_p1s() {
let (mut app, pads) = setup_table(MatchConfig::default(), 2);
assert_eq!(rows(&mut app), vec![dealt(2), dealt(1)]);
drives(&mut app, &[(1, pads[0])]);
assert_eq!(driving(&mut app, 0), Some(pads[1]));
}
#[test]
fn p2s_keys_take_the_second_seat() {
use crate::app::SeatController;
use crate::app::match_setup::PlayedBy;
let (mut app, _) = setup_table(MatchConfig::default(), 0);
assert_eq!(rows(&mut app)[1], PlayedBy::Open { keys: true });
tap(&mut app, KeyCode::KeyI);
assert_eq!(rows(&mut app)[1], PlayedBy::Keys(1));
tap(&mut app, KeyCode::Enter);
let config = app.world().resource::<MatchConfig>();
assert!(config.armed);
assert_eq!(config.controller(1), SeatController::Local, "P2 plays");
let (mut app, _) = setup_table(MatchConfig::default(), 0);
tap(&mut app, KeyCode::Enter);
let config = app.world().resource::<MatchConfig>();
assert!(config.armed, "nobody at P2's keys: the AI plays P2");
assert_eq!(config.controller(1), SeatController::Ai(config.ai_level));
}
#[test]
fn turning_a_seat_to_the_ai_moves_nobody_else() {
let (mut app, pads) = setup_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
3,
);
for &pad in &pads {
press_start(&mut app, pad);
}
assert_eq!(told(&mut app), (4, 0, vec![1, 2, 3]));
turn_seat(&mut app, 2, true);
app.update();
assert_eq!(told(&mut app), (4, 1, vec![1, 3]), "P2 and P4 stay put");
drives(&mut app, &[(1, pads[0]), (3, pads[2])]);
turn_seat(&mut app, 2, false);
app.update();
assert_eq!(told(&mut app), (4, 0, vec![1, 3]), "P3 open again");
press_start(&mut app, pads[1]);
assert_eq!(told(&mut app), (4, 0, vec![1, 2, 3]));
drives(&mut app, &[(1, pads[0]), (2, pads[1]), (3, pads[2])]);
}
#[derive(Clone, Copy, Debug)]
enum Go {
Enter,
Pad(usize, GamepadButton),
}
#[test]
fn the_table_step_three_shows_is_the_table_launched() {
use crate::app::match_setup::PlayedBy;
use crate::app::teams::TeamMode;
let buttons = [GamepadButton::Start, GamepadButton::South];
let four = MatchConfig {
seats: 4,
..MatchConfig::default()
};
let tables = [
(MatchConfig::default(), TeamMode::Solo),
(MatchConfig::with_ai(2, 1), TeamMode::Solo),
(four, TeamMode::Pairs),
(MatchConfig::with_ai(4, 1), TeamMode::Pairs),
];
let p2_rows = [SeatInput::Auto, SeatInput::Keys, SeatInput::Pad(1)];
let mut cases = 0;
for (config, mode) in tables {
for pads in 1..=3 {
for p2 in p2_rows {
if p2 == SeatInput::Pad(1) && pads < 2 {
continue;
}
for pick in [false, true] {
if pick && mode == TeamMode::Solo {
continue;
}
for opener in [None, Some(0), Some(pads - 1)] {
let mut goes = vec![Go::Enter];
for pad in 0..pads {
goes.extend(buttons.map(|button| Go::Pad(pad, button)));
}
for go in goes {
let case = format!(
"{} seats {mode:?}, {pads} pads, P2 {p2:?}, pick {pick}, \
opener {opener:?}, {go:?}",
config.seats
);
launched_as_shown(
&case,
(config.clone(), mode),
(pads, p2),
(pick, opener, go),
);
cases += 1;
}
}
}
}
}
}
assert!(cases > 500, "{cases} cases");
fn launched_as_shown(
case: &str,
(config, mode): (MatchConfig, TeamMode),
(pads, p2): (usize, SeatInput),
(pick, opener, go): (bool, Option<usize>, Go),
) {
let (mut app, plugged) = teams_table(config, mode, pads);
app.world_mut().resource_mut::<GameSettings>().seat_input[1] = p2;
into_the_setup_by(&mut app, opener.map(|at| plugged[at]));
for &pad in &plugged[..pads - 1] {
press_start(&mut app, pad);
}
if p2 == SeatInput::Keys {
tap(&mut app, KeyCode::KeyI);
app.update();
}
if pick {
tap(&mut app, KeyCode::KeyP);
app.update();
}
let mut shown = rows(&mut app);
let mut named = names(&app);
for _ in 0..2 {
shown = rows(&mut app);
named = names(&app);
match go {
Go::Enter => tap(&mut app, KeyCode::Enter),
Go::Pad(at, button) => press(&mut app, plugged[at], button),
}
app.update();
if app.world().resource::<MatchConfig>().armed {
break;
}
}
assert!(
app.world().resource::<MatchConfig>().armed,
"{case}: started"
);
assert_eq!(names(&app), named, "{case}: the names step 3 gave");
for (seat, by) in shown.iter().enumerate() {
let seat = seat as u8;
let controller = app.world().resource::<MatchConfig>().controller(seat);
match *by {
PlayedBy::Pad { number, .. } => {
assert_eq!(
driving(&mut app, seat),
Some(plugged[usize::from(number) - 1]),
"{case}: P{} was shown on controller {number}: {shown:?}",
seat + 1
);
}
PlayedBy::Keys(_) => {
assert_eq!(controller, SeatController::Local, "{case}");
assert_eq!(
driving(&mut app, seat),
None,
"{case}: P{} was shown on the keys: {shown:?}",
seat + 1
);
}
PlayedBy::Open { .. } | PlayedBy::Ai(_) => assert!(
controller.ai().is_some(),
"{case}: P{} was the AI's: {shown:?}",
seat + 1
),
PlayedBy::Bot => {
assert_eq!(controller, SeatController::Bot, "{case}")
}
}
}
}
}
fn teams_table(
config: MatchConfig,
mode: crate::app::teams::TeamMode,
pads: usize,
) -> (App, Vec<Entity>) {
let (mut app, plugged) = setup_table(config, pads);
let mut settings = app.world_mut().resource_mut::<GameSettings>();
settings.team_mode = mode;
for (seat, name) in ["Anna", "Bo", "Cleo", "Dan", "Eve", "Finn"]
.iter()
.enumerate()
{
settings.names[seat] = (*name).to_string();
}
app.update();
(app, plugged)
}
fn names(app: &App) -> Vec<String> {
let config = app.world().resource::<MatchConfig>();
let saved = &app.world().resource::<GameSettings>().names;
config.seat_names(saved)[..usize::from(config.seats)].to_vec()
}
#[test]
fn p_picks_p1s_partner_and_the_pads_follow() {
use crate::app::teams::TeamMode;
let (mut app, pads) = teams_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
TeamMode::Pairs,
3,
);
for &pad in &pads {
press_start(&mut app, pad);
}
drives(&mut app, &[(1, pads[0]), (2, pads[1]), (3, pads[2])]);
let mut partners = Vec::new();
for _ in 0..3 {
tap(&mut app, KeyCode::KeyP);
partners.push(names(&app)[1].clone());
assert_eq!(names(&app)[0], "Anna", "P1 stays");
}
assert_eq!(partners, ["Cleo", "Dan", "Bo"], "each friend in turn");
tap(&mut app, KeyCode::KeyP);
assert_eq!(names(&app), ["Anna", "Cleo", "Dan", "Bo"]);
drives(&mut app, &[(1, pads[1]), (2, pads[2]), (3, pads[0])]);
let shown = rows(&mut app);
tap(&mut app, KeyCode::Enter);
assert!(app.world().resource::<MatchConfig>().armed);
assert_eq!(shown[1], joined(2), "Cleo's pad, controller 2");
drives(&mut app, &[(1, pads[1]), (2, pads[2]), (3, pads[0])]);
}
#[test]
fn the_ai_can_be_picked_as_a_partner() {
use crate::app::match_setup::PlayedBy;
use crate::app::teams::TeamMode;
let (mut app, pads) = teams_table(MatchConfig::with_ai(4, 2), TeamMode::Pairs, 1);
into_the_setup_by(&mut app, None);
assert_eq!(rows(&mut app)[1], dealt(1));
tap(&mut app, KeyCode::KeyP);
let level = app.world().resource::<MatchConfig>().ai_level;
assert_eq!(
rows(&mut app),
vec![
PlayedBy::Keys(0),
PlayedBy::Ai(level),
joined(1),
PlayedBy::Ai(level)
]
);
assert_eq!(names(&app)[2], "Bo", "the friend's name went with them");
tap(&mut app, KeyCode::Enter);
drives(&mut app, &[(2, pads[0])]);
let config = app.world().resource::<MatchConfig>();
assert_eq!(config.human_seats(), vec![0, 2]);
}
#[test]
fn the_keyboards_p2_is_never_moved() {
use crate::app::match_setup::{MatchMenu, p2_pin, partner_line};
use crate::app::teams::TeamMode;
let (mut app, _) = teams_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
TeamMode::Pairs,
0,
);
tap(&mut app, KeyCode::KeyI);
let before = names(&app);
tap(&mut app, KeyCode::KeyP);
assert_eq!(names(&app), before, "nobody moved");
let line = |app: &App, seat: u8| {
let config = app.world().resource::<MatchConfig>();
let settings = app.world().resource::<GameSettings>();
let seats = app.world().resource::<PadSeats>();
let pin = p2_pin(config, settings, &[], seats.p2_at_keys());
partner_line(
&crate::app::i18n::EN,
crate::app::gamepad::LastDevice::Keyboard,
(config, settings),
seat,
pin,
)
};
assert_eq!(
line(&app, 0).as_deref(),
Some("Bo plays on the keyboard, so their seat stays put")
);
app.world_mut().resource_mut::<MatchMenu>().seat = 2;
tap(&mut app, KeyCode::KeyP);
assert_eq!(names(&app), before, "Cleo cannot have P1 or P2 either");
let (mut app, _) = teams_table(MatchConfig::with_ai(6, 0), TeamMode::Pairs, 0);
tap(&mut app, KeyCode::KeyI);
app.world_mut().resource_mut::<MatchMenu>().seat = 2;
tap(&mut app, KeyCode::KeyP);
assert_eq!(names(&app), ["Anna", "Bo", "Cleo", "Eve", "Dan", "Finn"]);
}
#[test]
fn the_rows_go_team_by_team() {
use crate::app::match_setup::{MatchMenu, partner_line};
use crate::app::teams::TeamMode;
let (mut app, _) = teams_table(MatchConfig::with_ai(6, 5), TeamMode::Trios, 0);
let mut walked = Vec::new();
for _ in 0..6 {
tap(&mut app, KeyCode::KeyS);
walked.push(app.world().resource::<MatchMenu>().seat);
}
assert_eq!(walked, [2, 4, 1, 3, 5, 0]);
let (mut app, _) = teams_table(MatchConfig::with_ai(5, 4), TeamMode::Trios, 0);
let before = names(&app);
tap(&mut app, KeyCode::KeyP);
assert_eq!(names(&app), before);
let config = app.world().resource::<MatchConfig>();
let settings = app.world().resource::<GameSettings>();
let said = partner_line(
&crate::app::i18n::EN,
crate::app::gamepad::LastDevice::Keyboard,
(config, settings),
0,
None,
);
assert_eq!(said.as_deref(), Some("trios (free-for-all until 6)"));
}
#[test]
fn a_seat_wired_to_p2_stays_put_under_the_ai() {
use crate::app::match_setup::{MatchMenu, p2_pin, partner_line};
use crate::app::teams::TeamMode;
for (wired, said) in [
(
SeatInput::Keys,
"Bo's seat is kept for the keyboard, so it stays put",
),
(
SeatInput::Pad(1),
"Bo's seat is kept for controller 2 set in Settings, so it stays put",
),
] {
let (mut app, pads) = teams_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
TeamMode::Pairs,
2,
);
app.world_mut().resource_mut::<GameSettings>().seat_input[1] = wired;
let level = app.world().resource::<MatchConfig>().ai_level;
app.world_mut().resource_mut::<MatchConfig>().controllers[1] =
SeatController::Ai(level);
app.update();
press_start(&mut app, pads[0]);
app.update();
assert_eq!(driving(&mut app, 2), Some(pads[0]), "{wired:?}: Cleo in P3");
app.world_mut().resource_mut::<MatchMenu>().seat = 0;
tap(&mut app, KeyCode::KeyP);
app.update();
let config = app.world().resource::<MatchConfig>();
assert_eq!(config.controller(1), SeatController::Ai(level), "{wired:?}");
assert_eq!(driving(&mut app, 2), Some(pads[0]), "{wired:?}: Cleo stays");
let world = app.world_mut();
let live: Vec<Entity> = world
.query_filtered::<Entity, With<Gamepad>>()
.iter(world)
.collect();
let config = app.world().resource::<MatchConfig>();
let settings = app.world().resource::<GameSettings>();
let seats = app.world().resource::<PadSeats>();
let pin = p2_pin(
config,
settings,
&seats.seat_pads(&live, settings, &config.human_seats()),
seats.p2_at_keys(),
);
let line = partner_line(
&crate::app::i18n::EN,
crate::app::gamepad::LastDevice::Keyboard,
(config, settings),
0,
pin,
);
assert_eq!(line.as_deref(), Some(said));
}
}
#[test]
fn an_open_p2_seat_is_kept_for_the_keyboard() {
use crate::app::match_setup::{MatchMenu, PlayedBy};
use crate::app::teams::TeamMode;
let (mut app, pads) = teams_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
TeamMode::Pairs,
1,
);
into_the_setup_by(&mut app, None);
let level = app.world().resource::<MatchConfig>().ai_level;
app.world_mut().resource_mut::<MatchConfig>().controllers[1] = SeatController::Ai(level);
app.update();
press_start(&mut app, pads[0]);
app.world_mut().resource_mut::<MatchConfig>().controllers[1] = SeatController::Local;
app.update();
assert_eq!(rows(&mut app)[1], PlayedBy::Open { keys: true });
assert_eq!(driving(&mut app, 2), Some(pads[0]), "Cleo in P3");
app.world_mut().resource_mut::<MatchMenu>().seat = 0;
tap(&mut app, KeyCode::KeyP);
app.update();
assert_eq!(rows(&mut app)[1], PlayedBy::Open { keys: true }, "kept");
tap(&mut app, KeyCode::KeyI);
app.update();
assert_eq!(rows(&mut app)[1], PlayedBy::Keys(1));
assert_eq!(driving(&mut app, 2), Some(pads[0]), "Cleo still plays");
}
#[test]
fn a_pick_leaves_the_saved_names_alone() {
use crate::app::teams::TeamMode;
let (mut app, pads) = teams_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
TeamMode::Pairs,
3,
);
for &pad in &pads {
press_start(&mut app, pad);
}
tap(&mut app, KeyCode::KeyP);
app.update();
let saved = &app.world().resource::<GameSettings>().names;
assert_eq!(saved[..4], ["Anna", "Bo", "Cleo", "Dan"], "saved as typed");
assert_eq!(names(&app), ["Anna", "Cleo", "Bo", "Dan"], "the table's");
}
#[test]
fn a_pads_y_picks_partners_through_the_bridge() {
use crate::app::teams::TeamMode;
let (mut app, pads) = teams_table(
MatchConfig {
seats: 4,
..MatchConfig::default()
},
TeamMode::Pairs,
3,
);
for &pad in &pads {
press_start(&mut app, pad);
}
app.update();
press(&mut app, pads[1], GamepadButton::North);
app.update();
assert_eq!(names(&app), ["Anna", "Cleo", "Bo", "Dan"], "Y picked");
drives(&mut app, &[(1, pads[1]), (2, pads[0]), (3, pads[2])]);
}
}