mod discovery;
mod entry;
mod hosting;
mod input;
mod joining;
#[cfg(test)]
mod load;
mod partners;
mod pick;
#[cfg(test)]
mod pick_tests;
mod standing;
mod terms;
mod ui;
pub use discovery::*;
pub use entry::*;
pub use hosting::*;
pub use input::*;
pub use joining::*;
pub use partners::{Seated, partner_rows, partners_open, seats_shown};
pub use pick::Picking;
pub use standing::*;
pub use terms::*;
pub use ui::*;
use crate::app::cycle::{Cycle, Turn};
use crate::app::i18n::fill;
use crate::app::match_setup::MatchConfig;
use crate::app::menu_ui::Nav;
use crate::app::net::{
HostedBot, Invitation, LobbyReturn, Online, OnlineSession, PeerBook, PutOut,
};
use crate::app::palette;
use crate::app::settings::GameSettings;
use crate::app::{Screen, VersusPhase};
use crate::sim::MAX_PLAYERS;
use crate::transport::{Announcer, Beacon, Discovery, MatchTerms, NetMsg, UdpTransport};
use bevy::ecs::relationship::RelatedSpawnerCommands;
use bevy::prelude::*;
use std::net::SocketAddr;
pub(crate) const ANNOUNCE_EVERY: f32 = 1.0;
pub(crate) fn once_a_second(clock: &mut f32, delta: f32) -> bool {
*clock -= delta;
if *clock > 0.0 {
return false;
}
*clock = ANNOUNCE_EVERY;
true
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Intent {
Host,
Join(SocketAddr),
Dial(SocketAddr),
JoinBot(SocketAddr),
}
#[derive(Default, Clone, PartialEq, Eq, Debug)]
pub struct Said {
pub who: String,
pub line: String,
}
impl Said {
pub fn is_notice(&self) -> bool {
self.who.is_empty()
}
}
#[derive(Resource, Default)]
pub struct LobbyState {
discovery: Option<Discovery>,
pub standing: Standing,
pub hosts: Vec<HostEntry>,
pub selected: Option<SocketAddr>,
pub scroll: usize,
rows: ListRows,
pub chat: Vec<Said>,
pub typing: Option<Typing>,
pub dial: usize,
pub table: Vec<String>,
pub table_kinds: Vec<crate::sim::SeatKind>,
pub seated: Seated,
pub game_name: String,
pub feedback: String,
auto_done: bool,
}
impl LobbyState {
fn start_over(&mut self) {
let game_name = std::mem::take(&mut self.game_name);
*self = LobbyState {
game_name,
..LobbyState::default()
};
}
pub fn hear(&mut self, name: &crate::transport::WireName, text: &crate::transport::WireChat) {
let who = crate::transport::name_from_wire(name);
let line = crate::transport::chat_from_wire(text);
self.say(&who, &line);
}
pub fn say(&mut self, who: &str, line: &str) {
if line.is_empty() {
return;
}
self.chat.push(Said {
who: who.to_string(),
line: line.to_string(),
});
let over = self.chat.len().saturating_sub(CHAT_LINES);
self.chat.drain(..over);
}
pub fn standing(&self) -> &Standing {
&self.standing
}
pub fn can_chat(&self) -> bool {
self.standing.at_a_beach()
}
pub fn hosting(&self) -> bool {
self.hosted().is_some()
}
pub fn hosted(&self) -> Option<&Hosted> {
match &self.standing {
Standing::Hosting(hosted) => Some(hosted),
Standing::Choosing { .. } | Standing::Joining(_) => None,
}
}
fn hosted_mut(&mut self) -> Option<&mut Hosted> {
match &mut self.standing {
Standing::Hosting(hosted) => Some(hosted),
Standing::Choosing { .. } | Standing::Joining(_) => None,
}
}
pub fn joined(&self) -> Option<&Joined> {
match &self.standing {
Standing::Joining(joined) => Some(joined),
Standing::Choosing { .. } | Standing::Hosting(_) => None,
}
}
fn joined_mut(&mut self) -> Option<&mut Joined> {
match &mut self.standing {
Standing::Joining(joined) => Some(joined),
Standing::Choosing { .. } | Standing::Hosting(_) => None,
}
}
fn transport(&self) -> Option<&UdpTransport> {
match &self.standing {
Standing::Hosting(hosted) => Some(&hosted.transport),
Standing::Joining(joined) => Some(&joined.transport),
Standing::Choosing { .. } => None,
}
}
pub fn watching(&self) -> bool {
match &self.standing {
Standing::Choosing { watching } => *watching,
Standing::Joining(joined) => joined.watching,
Standing::Hosting(_) => false,
}
}
fn set_watching(&mut self, on: bool) {
match &mut self.standing {
Standing::Choosing { watching } => *watching = on,
Standing::Joining(joined) => joined.watching = on,
Standing::Hosting(_) => {}
}
}
fn let_go(&mut self) {
let watching = self.watching();
self.standing = Standing::Choosing { watching };
self.chat.clear();
self.table.clear();
self.table_kinds.clear();
}
pub fn roster(&self, tr: &crate::app::i18n::Tr, me: &str) -> Vec<String> {
let nobody = PeerBook::default();
let peers = self.hosted().map_or(&nobody, |hosted| &hosted.peers);
table_of(peers, tr, me)
}
pub fn joined_entry(&self) -> Option<&HostEntry> {
let joined = self.joined()?;
match joined.beach {
Some(id) => self.hosts.iter().find(|host| host.id == id),
None => {
let there = joined.transport.peer_addr()?;
self.hosts.iter().find(|host| host.addr == there)
}
}
}
pub fn joining_beach(&self) -> Option<String> {
let host = &self.hosts[self.selected_index()?];
Some(match host.name.is_empty() {
true => host.addr.to_string(),
false => host.name.clone(),
})
}
pub fn roster_kinds(&self) -> Vec<crate::sim::SeatKind> {
let nobody = PeerBook::default();
let peers = self.hosted().map_or(&nobody, |hosted| &hosted.peers);
std::iter::once(crate::sim::SeatKind::Human)
.chain(peers.at_the_table().map(|(_, peer)| match peer.bot {
true => crate::sim::SeatKind::Bot,
false => crate::sim::SeatKind::Human,
}))
.collect()
}
pub fn selected_index(&self) -> Option<usize> {
let want = self.selected?;
self.hosts.iter().position(|host| host.addr == want)
}
fn settle_cursor(&mut self) {
if self.hosts.is_empty() {
self.selected = None;
self.scroll = 0;
return;
}
let at = match self.selected_index() {
Some(at) => at,
None => {
self.selected = self.hosts.first().map(|host| host.addr);
0
}
};
let rows = self.rows_shown();
self.scroll = self
.scroll
.min(at)
.max(at.saturating_sub(rows - 1))
.min(self.hosts.len().saturating_sub(rows));
}
pub fn rows_shown(&self) -> usize {
self.rows.0
}
pub fn listed(&self, row: usize) -> Option<&HostEntry> {
if self.standing().at_a_beach() || row >= self.rows_shown() {
return None;
}
self.hosts.get(self.scroll + row)
}
pub fn fit_rows(&mut self, rows: usize) {
let rows = rows.max(1);
if rows > self.rows.0 {
self.scroll = self.scroll.min(self.hosts.len().saturating_sub(rows));
}
self.rows = ListRows(rows);
self.settle_cursor();
}
pub(crate) fn point_at(&mut self, at: Option<usize>) {
let Some(host) = at.and_then(|at| self.hosts.get(at)) else {
return;
};
self.selected = Some(host.addr);
self.settle_cursor();
}
fn step_cursor(&mut self, nav: Nav) {
if self.hosts.is_empty() {
return;
}
let len = self.hosts.len();
let at = self.selected_index().unwrap_or(0);
let next = crate::app::menu_ui::step(nav, at, len);
self.selected = Some(self.hosts[next].addr);
self.settle_cursor();
}
}
pub(super) fn table_of(peers: &PeerBook, tr: &crate::app::i18n::Tr, me: &str) -> Vec<String> {
let mine = match me.trim().is_empty() {
true => crate::app::seat_label(tr, 0),
false => me.to_string(),
};
let mut table = vec![mine];
for (_, peer) in peers.at_the_table() {
table.push(match peer.name.is_empty() {
true => crate::app::seat_label(tr, table.len() as u8),
false => peer.name.clone(),
});
}
table
}
#[derive(Resource, Default)]
pub struct Homecoming(pub Option<LobbyReturn>);
fn settle_back_in(
state: &mut LobbyState,
returned: LobbyReturn,
tr: &'static crate::app::i18n::Tr,
) {
match returned {
LobbyReturn::Host {
announcer,
transport,
game_name,
mut peers,
played,
rounds_dealt,
arranged,
} => {
let Some(announcer) = announcer else { return };
peers.fit(transport.peer_count());
peers.hush();
state.game_name = game_name;
state.standing = Standing::Hosting(Hosted {
announcer,
transport,
peers,
announce_in: 0.0,
kicking: None,
bot_string: None,
tail: RoundTail::kept(played, rounds_dealt),
rounds_dealt,
picking: None,
partnering: false,
arranged,
});
}
LobbyReturn::Joiner {
transport,
watching,
played_seed,
bot,
} => {
state.standing =
Standing::Joining(Joined::returned(transport, watching, played_seed, bot));
state.feedback = match watching {
true => tr.lobby_watching,
false => tr.lobby_aboard,
}
.to_string();
}
}
}
pub fn enter_lobby(
mut commands: Commands,
mut state: ResMut<LobbyState>,
mut homecoming: ResMut<Homecoming>,
mut notice: ResMut<crate::app::RoundNotice>,
settings: Res<GameSettings>,
art: Res<crate::app::art::Art>,
) {
state.start_over();
spawn_lobby_ui(&mut commands, settings.tr(), &LobbyArt::from_art(&art));
if let Some(returned) = homecoming.0.take() {
settle_back_in(&mut state, returned, settings.tr());
}
let why = std::mem::take(&mut notice.0);
if !why.is_empty() {
state.feedback = why;
}
if state.hosting() {
return;
}
match Discovery::bind() {
Ok(discovery) => {
state.discovery = Some(discovery);
if state.feedback.is_empty() {
state.feedback = settings.tr().lobby_listening.into();
}
}
Err(e) => {
if state.feedback.is_empty() {
state.feedback = fill(
settings.tr().lobby_discovery_unavailable,
&[("e", &e.to_string())],
);
}
}
}
}
pub fn exit_lobby(
mut state: ResMut<LobbyState>,
dials: crate::app::match_setup::Dials,
mut menu: ResMut<crate::app::match_setup::MatchMenu>,
) {
say_goodbye(&state);
let crate::app::match_setup::Dials {
mut config,
beaches,
} = dials;
if let Some(picking) = state.hosted_mut().and_then(|hosted| hosted.picking.take()) {
pick::close_the_pick(
picking,
pick::Close::PutBack,
&mut config,
&mut menu,
&beaches,
);
}
state.start_over();
}
#[cfg(test)]
mod tests {
use super::*;
fn addr(last: u8) -> SocketAddr {
format!("10.0.0.{last}:47777").parse().expect("addr")
}
fn open(last: u8) -> (SocketAddr, Beacon) {
beach(last, "", 1, 6, false)
}
fn beach(last: u8, name: &str, taken: u8, seats: u8, running: bool) -> (SocketAddr, Beacon) {
(
addr(last),
Beacon::Here {
id: u64::from(last),
name: name.to_string(),
host: "Sam".to_string(),
taken,
seats,
running,
bots: false,
full: false,
},
)
}
#[test]
fn hosts_expire_when_they_stop_announcing() {
let mut hosts = Vec::new();
refresh_open(&mut hosts, &[open(7)], 0.0);
assert_eq!(hosts.len(), 1, "a new beach joins the list");
refresh_open(&mut hosts, &[open(7)], 1.0);
assert_eq!(hosts.len(), 1);
assert_eq!(hosts[0].age, 0.0);
refresh_open(&mut hosts, &[], HOST_TTL - 0.5);
assert_eq!(hosts.len(), 1, "given a moment's grace");
refresh_open(&mut hosts, &[], 1.0);
assert!(hosts.is_empty(), "a host that quit leaves the list");
}
#[test]
fn a_farewell_clears_a_host_without_waiting_out_the_timeout() {
let mut hosts = Vec::new();
refresh_open(&mut hosts, &[open(1), open(2)], 0.0);
assert_eq!(hosts.len(), 2);
refresh_open(&mut hosts, &[(addr(1), Beacon::Closing { id: 1 })], 0.0);
assert_eq!(hosts.len(), 1, "the one that said goodbye left");
assert_eq!(hosts[0].addr, addr(2), "and only that one");
refresh_open(&mut hosts, &[(addr(9), Beacon::Closing { id: 9 })], 0.0);
assert_eq!(hosts.len(), 1);
refresh_open(
&mut hosts,
&[open(5), (addr(5), Beacon::Closing { id: 5 })],
0.0,
);
assert!(
hosts.iter().all(|host| host.addr != addr(5)),
"a beach that opened and shut in one frame is not listed"
);
refresh_open(
&mut hosts,
&[(addr(5), Beacon::Closing { id: 5 }), open(5)],
0.0,
);
assert!(hosts.iter().any(|host| host.addr == addr(5)));
}
#[test]
fn the_gathering_line_counts_who_is_actually_there() {
use crate::app::i18n::EN;
let none = MatchConfig {
lobby_bots: 0,
..MatchConfig::default()
};
let ai = |config: &MatchConfig, aboard: usize| dealt(config, 1 + aboard as u8).bots;
let line = |config: &MatchConfig, aboard: usize, watching: usize| {
gathering_feedback(&EN, aboard, watching, ai(config, aboard), EN.key_enter)
};
let enter = [("k", EN.key_enter)];
assert_eq!(line(&none, 1, 0), fill(EN.lobby_rivals_one, &enter));
let onlookers = line(&none, 0, 1);
assert!(
onlookers.starts_with(&fill(EN.lobby_rivals_none, &enter)),
"{onlookers}"
);
assert!(
onlookers.contains(&fill(EN.lobby_ai_seats, &[("n", "1")])),
"{onlookers}"
);
assert!(line(&none, 3, 0).contains('3'));
let watched = line(&none, 2, 2);
assert!(watched.len() > line(&none, 2, 0).len());
let greedy = MatchConfig {
lobby_bots: 5,
..MatchConfig::default()
};
let full = line(&greedy, MAX_PLAYERS - 1, 0);
assert_eq!(
full,
line(&none, MAX_PLAYERS - 1, 0),
"no room for a bot, so no promise of one"
);
}
#[test]
fn a_hosting_lobby_announces_its_own_departure() {
let mut discovery = Discovery::bind().expect("bind lobby port");
let mut state = LobbyState::default();
let transport = UdpTransport::host(0).expect("game socket");
let port = transport.local_addr().expect("addr").port();
let mine = |heard: Vec<(SocketAddr, Beacon)>| {
heard
.into_iter()
.filter(|(addr, _)| addr.port() == port)
.collect::<Vec<_>>()
};
say_goodbye(&state);
std::thread::sleep(std::time::Duration::from_millis(30));
assert!(
mine(discovery.poll()).is_empty(),
"a joiner has no beach to close"
);
state.standing = Standing::hosting(Announcer::new(0xB0A7).expect("announcer"), transport);
say_goodbye(&state);
let mut heard = Vec::new();
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(10));
heard.extend(mine(discovery.poll()));
if !heard.is_empty() {
break;
}
}
let (_, beacon) = heard.first().expect("the farewell went out");
assert!(matches!(beacon, Beacon::Closing { .. }), "{beacon:?}");
}
#[test]
fn hosts_age_independently() {
let mut hosts = Vec::new();
refresh_open(&mut hosts, &[open(1), open(2)], 0.0);
assert_eq!(hosts.len(), 2);
refresh_open(&mut hosts, &[open(1)], HOST_TTL - 0.1);
assert_eq!(hosts.len(), 2, "the quiet one is on its last legs");
refresh_open(&mut hosts, &[open(1)], 0.2);
assert_eq!(hosts.len(), 1);
assert_eq!(hosts[0].addr, addr(1));
}
}
#[cfg(test)]
mod homecoming_tests {
use super::*;
use crate::app::i18n::EN;
use crate::sim::{DEFAULT_DELAY, Lockstep};
use crate::app::lobby::hosting::peers_named;
fn with_peers(peers: usize) -> (UdpTransport, Vec<UdpTransport>) {
let mut transport = UdpTransport::host(0).expect("game socket");
let port = transport.local_addr().expect("addr").port();
let mut joiners = Vec::new();
for want in 1..=peers {
let joiner = UdpTransport::join(("127.0.0.1", port)).expect("join");
joiner.send(NetMsg::hello("someone"));
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let _ = transport.recv_all();
if transport.peer_count() >= want {
break;
}
}
assert_eq!(transport.peer_count(), want, "peer {want} registered");
joiners.push(joiner);
}
(transport, joiners)
}
#[test]
fn the_beach_name_outlives_the_lobby() {
let mut state = LobbyState {
game_name: "Room 3".into(),
feedback: "something".into(),
dial: 4,
..LobbyState::default()
};
state.say("Ana", "hi");
state.start_over();
assert_eq!(state.game_name, "Room 3");
assert!(state.chat.is_empty());
assert!(state.feedback.is_empty());
assert_eq!(state.dial, 0);
}
#[test]
fn a_host_comes_home_still_hosting() {
let (transport, _joiners) = with_peers(2);
let port = transport.local_addr().expect("addr").port();
let mut state = LobbyState::default();
settle_back_in(
&mut state,
LobbyReturn::Host {
announcer: Some(Announcer::new(0xB0A7).expect("announcer")),
transport,
game_name: "Room 3".into(),
peers: peers_named(&["Bo", "Cy"], &[1]),
played: Box::new(Lockstep::new(0, vec![0, 1], DEFAULT_DELAY)),
rounds_dealt: 3,
arranged: None,
},
&EN,
);
assert!(state.hosting());
assert_eq!(state.hosted().map(|hosted| hosted.rounds_dealt), Some(3));
assert_eq!(state.game_name, "Room 3");
let hosted = state.hosted().expect("hosting");
assert_eq!(
hosted.transport.local_addr().expect("addr").port(),
port,
"the socket the table is still talking to"
);
assert_eq!(hosted.announce_in, 0.0, "back on the air this frame");
assert_eq!(
state.roster(&EN, "Anna"),
["Anna", "Bo"],
"the table as it stood, minus the one watching"
);
}
#[test]
fn the_lobby_repeats_the_rounds_end_for_a_while() {
use crate::sim::{InputMsg, PlayerAction};
use crate::transport::Batch;
let (transport, mut joiners) = with_peers(2);
let mut played = Lockstep::new(0, vec![0, 1], DEFAULT_DELAY);
while played.frame() < 300 {
if let Some(ours) = played.commit_local(PlayerAction::None) {
played.receive(InputMsg { player: 1, ..ours });
}
while played.due() && played.advance().is_some() {}
}
let rounds_dealt = 4;
let round = crate::transport::round_tag(rounds_dealt);
let frames = |joiner: &mut UdpTransport| -> Vec<u32> {
std::thread::sleep(std::time::Duration::from_millis(20));
let mut frames: Vec<u32> = joiner
.recv_all()
.into_iter()
.filter_map(|(msg, _)| {
let NetMsg::Inputs(batch) = msg else {
return None;
};
(batch.round == round).then_some(batch.inputs)
})
.flatten()
.map(|input| input.frame)
.collect();
frames.sort_unstable();
frames.dedup();
frames
};
let mut tail = RoundTail::kept(Box::new(played), rounds_dealt);
let behind = Batch {
round,
wants: Some([180; MAX_PLAYERS]),
..Batch::default()
};
tail.repeat(&transport, 0.1, false, &[(1, behind.clone())]);
assert!(
frames(&mut joiners[0]).is_empty(),
"only the peer that asked"
);
let sent = frames(&mut joiners[1]);
assert_eq!(sent.first(), Some(&180), "from where it stands: {sent:?}");
assert!(
sent.iter().all(|&frame| frame < 300),
"and none past the end"
);
let last = sent.last().copied().unwrap_or(0);
let mut wants = behind.clone();
wants.wants = Some([last + 1; MAX_PLAYERS]);
tail.repeat(&transport, 0.1, false, &[(1, wants)]);
assert_eq!(
frames(&mut joiners[1]).last(),
Some(&299),
"to the very end"
);
let stale = Batch {
round: round.wrapping_sub(1),
..behind.clone()
};
tail.repeat(&transport, 0.1, false, &[(1, stale)]);
assert!(frames(&mut joiners[1]).is_empty(), "the last round's");
tail.repeat(&transport, 0.1, true, &[]);
let everybody = [frames(&mut joiners[0]), frames(&mut joiners[1])];
assert!(
everybody.iter().all(|sent| sent.last() == Some(&299)),
"the tick: {everybody:?}"
);
tail.repeat(&transport, 60.0, true, &[(1, behind)]);
assert!(frames(&mut joiners[0]).is_empty() && frames(&mut joiners[1]).is_empty());
}
#[test]
fn a_short_name_list_is_padded_to_the_peers_it_indexes() {
let (transport, _joiners) = with_peers(2);
let mut state = LobbyState::default();
settle_back_in(
&mut state,
LobbyReturn::Host {
announcer: Some(Announcer::new(1).expect("announcer")),
transport,
game_name: String::new(),
peers: peers_named(&["Bo"], &[0]),
played: Box::new(Lockstep::new(0, vec![0, 1], DEFAULT_DELAY)),
rounds_dealt: 0,
arranged: None,
},
&EN,
);
let hosted = state.hosted().expect("hosting");
assert_eq!(
hosted.peers.len(),
hosted.transport.peer_count(),
"one row per peer the socket knows"
);
}
#[test]
fn a_joiner_comes_home_aboard() {
let mut state = LobbyState::default();
settle_back_in(
&mut state,
LobbyReturn::Joiner {
transport: UdpTransport::join(("127.0.0.1", 47999)).expect("join"),
watching: true,
played_seed: 42,
bot: None,
},
&EN,
);
let joined = state.joined().expect("joining");
assert!(joined.watching, "still watching");
assert_eq!(joined.played_seed(), Some(42));
assert!(joined.host_answered, "the host was talking a moment ago");
assert_eq!(state.feedback, EN.lobby_watching);
}
}
#[cfg(test)]
mod chat_tests {
use super::*;
#[test]
fn the_feed_keeps_the_last_few_lines() {
let mut state = LobbyState::default();
for n in 0..CHAT_LINES + 4 {
state.say("Anna", &format!("line {n}"));
}
assert_eq!(state.chat.len(), CHAT_LINES);
assert_eq!(state.chat[0].line, "line 4", "{:?}", state.chat);
assert_eq!(
state.chat.last().unwrap().who,
"Anna",
"each line remembers who said it"
);
state.say("Anna", "");
assert_eq!(state.chat.len(), CHAT_LINES);
}
#[test]
fn a_notice_has_nobody_behind_it() {
let mut state = LobbyState::default();
state.say("", "Anna joined");
state.say("Anna", "ready?");
assert!(state.chat[0].is_notice(), "nobody said it");
assert!(!state.chat[1].is_notice(), "Anna did");
}
#[test]
fn there_is_nobody_to_talk_to_until_there_is() {
let mut state = LobbyState::default();
assert!(!state.can_chat(), "browsing the list is not a conversation");
state.standing = hosting_lobby();
assert!(state.can_chat(), "a host has a table to address");
state.standing = Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", 47999)).expect("join"),
false,
));
assert!(state.can_chat(), "and a joiner has a host");
}
}
#[cfg(test)]
mod table_tests {
use super::*;
use crate::app::i18n::EN;
use crate::app::lobby::hosting::peers_named;
fn seated(names: &[&str], watchers: &[usize]) -> LobbyState {
let mut state = LobbyState {
standing: hosting_lobby(),
..LobbyState::default()
};
state.hosted_mut().expect("hosting").peers = peers_named(names, watchers);
state
}
#[test]
fn the_table_is_the_host_and_the_players() {
let state = seated(&["Bo", "Cy"], &[]);
assert_eq!(state.roster(&EN, "Anna"), ["Anna", "Bo", "Cy"]);
let state = seated(&["Bo", "Cy"], &[0]);
assert_eq!(state.roster(&EN, "Anna"), ["Anna", "Cy"]);
let state = seated(&[""], &[]);
let table = state.roster(&EN, "");
assert_eq!(table.len(), 2);
assert!(table.iter().all(|who| !who.is_empty()), "{table:?}");
let state = LobbyState::default();
assert_eq!(state.roster(&EN, "Anna"), ["Anna"]);
}
#[test]
fn forgetting_a_peer_moves_everything_that_indexed_it() {
let mut state = seated(&["Bo", "Cy", "Dee"], &[2]);
assert_eq!(
state.roster(&EN, "Anna"),
["Anna", "Bo", "Cy"],
"Dee watches"
);
state.hosted_mut().expect("hosting").forget_peer(0);
let hosted = state.hosted().expect("hosting");
let names: Vec<&str> = hosted.peers.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["Cy", "Dee"]);
assert_eq!(hosted.watchers_aboard(), 1, "the wish followed Dee down");
assert_eq!(hosted.players_aboard(), 1);
assert_eq!(
state.roster(&EN, "Anna"),
["Anna", "Cy"],
"Dee still watches"
);
state.hosted_mut().expect("hosting").forget_peer(1);
let hosted = state.hosted().expect("hosting");
assert_eq!(hosted.peers.len(), 1);
assert_eq!(hosted.watchers_aboard(), 0);
assert_eq!(state.roster(&EN, "Anna"), ["Anna", "Cy"]);
}
}
#[cfg(test)]
mod standing_tests {
use super::*;
#[test]
fn the_standing_carries_the_wish_to_watch_with_it() {
let mut state = LobbyState::default();
assert!(!state.standing().at_a_beach(), "nobody to talk to yet");
assert!(!state.watching());
state.set_watching(true);
assert!(state.watching());
assert!(!state.standing().at_a_beach());
let watching = state.watching();
state.standing = Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", 47999)).expect("join"),
watching,
));
assert!(state.joined().is_some());
assert!(state.watching(), "the wish went with the greeting");
assert!(state.standing().at_a_beach());
state.let_go();
assert!(state.joined().is_none());
assert!(state.watching());
state.standing = hosting_lobby();
assert!(state.hosting());
assert!(!state.watching(), "a host plays");
assert!(state.standing().at_a_beach());
}
#[test]
fn everything_that_asks_gets_the_same_answer() {
let mut state = LobbyState::default();
for watching in [false, true] {
state.set_watching(watching);
assert_eq!(state.can_chat(), state.standing().at_a_beach());
}
state.standing = Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", 47998)).expect("join"),
false,
));
assert_eq!(state.can_chat(), state.standing().at_a_beach());
}
}