use super::*;
use crate::app::i18n::fill;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Pick {
Nothing,
AskName(usize),
Take(usize),
Full(usize),
Gone,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) struct JoinAsk {
pub enter_on: Option<usize>,
pub intent: Option<Intent>,
pub auto_join: bool,
pub busy: bool,
pub named: bool,
pub to_watch: bool,
}
pub(super) fn which_beach(ask: JoinAsk, hosts: &[HostEntry]) -> Pick {
let JoinAsk {
enter_on,
intent,
auto_join,
busy,
named,
to_watch,
} = ask;
let asked = match (intent, auto_join) {
(Some(Intent::Join(addr)), _) => match hosts.iter().position(|host| host.addr == addr) {
Some(at) => Some(at),
None => return Pick::Gone,
},
(Some(Intent::Host | Intent::Dial(_) | Intent::JoinBot(_)), _) => return Pick::Nothing,
(None, true) => Some(0),
(None, false) => enter_on,
};
let Some(at) = asked.filter(|at| *at < hosts.len()) else {
return Pick::Nothing;
};
if busy {
return Pick::Nothing;
}
if hosts[at].full || (!to_watch && !hosts[at].has_room()) {
return Pick::Full(at);
}
match named || auto_join || intent.is_some() {
true => Pick::Take(at),
false => Pick::AskName(at),
}
}
pub(super) fn take_a_beach(
keys: &ButtonInput<KeyCode>,
settings: &GameSettings,
state: &mut LobbyState,
tr: &'static crate::app::i18n::Tr,
intent: Option<Intent>,
auto_join: bool,
) {
if let Some(Intent::Dial(addr)) = intent {
if !state.standing().at_a_beach() {
dial_at(state, settings, tr, addr);
}
return;
}
let enter_on = crate::app::menu_ui::enter(keys)
.then(|| state.selected_index())
.flatten();
let pick = which_beach(
JoinAsk {
enter_on,
intent,
auto_join,
busy: state.standing().at_a_beach(),
named: !settings.names[0].trim().is_empty(),
to_watch: state.watching(),
},
&state.hosts,
);
if auto_join && matches!(pick, Pick::Take(_)) {
state.auto_done = true;
}
match pick {
Pick::Nothing => {}
Pick::AskName(at) => {
state.typing = Some(
Typing::player_name(Intent::Join(state.hosts[at].addr), &settings.names[0])
.or_suggest(|| suggested_name(tr)),
);
}
Pick::Full(_) => state.feedback = tr.lobby_beach_full.to_string(),
Pick::Gone => state.feedback = tr.lobby_host_closed.to_string(),
Pick::Take(at) => dial(state, settings, tr, at),
}
}
fn dial(
state: &mut LobbyState,
settings: &GameSettings,
tr: &'static crate::app::i18n::Tr,
at: usize,
) {
debug_assert!(
at < state.hosts.len(),
"dialling row {at} of {}",
state.hosts.len()
);
let Some(host) = state.hosts.get(at) else {
return;
};
let (addr, id) = (host.addr, host.id);
dial_at(state, settings, tr, addr);
if let Some(joined) = state.joined_mut() {
joined.beach = (id != 0).then_some(id);
}
}
pub(super) const NO_ANSWER_AFTER: f32 = 6.0;
const _: () = {
assert!(NO_ANSWER_AFTER > HOST_TTL);
assert!(NO_ANSWER_AFTER > 3.0 * ANNOUNCE_EVERY);
};
fn answer_the_silence(
state: &mut LobbyState,
tr: &'static crate::app::i18n::Tr,
heard: bool,
delta: f32,
) -> bool {
let Some(joined) = state.joined_mut() else {
return false;
};
if heard {
let first_word = !joined.host_answered;
joined.host_answered = true;
joined.host_silence = 0.0;
let watching = joined.watching;
if first_word {
state.feedback = match watching {
true => tr.lobby_watching.to_string(),
false => tr.lobby_aboard.to_string(),
};
}
return false;
}
joined.host_silence += delta;
if joined.host_silence < NO_ANSWER_AFTER {
return false;
}
let called = joined
.transport
.peer_addr()
.map(|addr| addr.to_string())
.unwrap_or_default();
if joined.told_full {
state.let_go();
state.feedback = tr.lobby_no_room.to_string();
return true;
}
if let Some(gone) = state.joined_entry().map(|host| host.addr) {
state.hosts.retain(|host| host.addr != gone);
}
state.let_go();
state.feedback = fill(tr.lobby_no_answer, &[("a", &called)]);
true
}
pub(super) fn dial_at(
state: &mut LobbyState,
settings: &GameSettings,
tr: &'static crate::app::i18n::Tr,
addr: SocketAddr,
) {
dial_as(state, settings, tr, addr, None);
}
pub(super) fn dial_as(
state: &mut LobbyState,
settings: &GameSettings,
tr: &'static crate::app::i18n::Tr,
addr: SocketAddr,
bot: Option<String>,
) {
match UdpTransport::join(addr) {
Ok(transport) => {
let watching = state.watching() && bot.is_none();
let mut joined = Joined::dialled(transport, watching);
joined.bot = bot;
joined.transport.send(joined.greeting(&settings.names[0]));
state.standing = Standing::Joining(joined);
state.feedback = fill(tr.lobby_calling, &[("a", &addr.to_string())]);
}
Err(e) => state.feedback = fill(tr.lobby_could_not_join, &[("e", &e.to_string())]),
}
}
pub(super) fn a_fresh_invitation(played: Option<u64>, invitation: &Invitation) -> bool {
played != Some(invitation.terms.seed)
}
pub(super) fn accept_the_invitation(
state: &mut LobbyState,
online: &mut Online,
tournament: &mut crate::app::tournament::Tournament,
next_screen: &mut NextState<Screen>,
next_vphase: &mut NextState<VersusPhase>,
started: Option<Invitation>,
) {
if let Some(invitation) = started {
let terms = (invitation.terms, invitation.standing);
walk_into_the_arena(
state,
online,
tournament,
(next_screen, next_vphase),
terms,
|transport| OnlineSession::invited(transport, invitation),
);
}
}
fn catch_up_with_the_round(
state: &mut LobbyState,
online: &mut Online,
tournament: &mut crate::app::tournament::Tournament,
next_screen: &mut NextState<Screen>,
next_vphase: &mut NextState<VersusPhase>,
caught: crate::app::net::catch_up::CaughtUp,
) {
let terms = (caught.invitation.terms, caught.invitation.standing);
walk_into_the_arena(
state,
online,
tournament,
(next_screen, next_vphase),
terms,
|transport| OnlineSession::caught_up(transport, caught),
);
}
fn walk_into_the_arena(
state: &mut LobbyState,
online: &mut Online,
tournament: &mut crate::app::tournament::Tournament,
(next_screen, next_vphase): (&mut NextState<Screen>, &mut NextState<VersusPhase>),
(terms, standing): (MatchTerms, Option<crate::transport::SeriesStanding>),
session: impl FnOnce(UdpTransport) -> OnlineSession,
) {
let Standing::Joining(joined) = std::mem::take(&mut state.standing) else {
unreachable!("an invitation for a lobby that greeted nobody");
};
*tournament = crate::app::tournament::Tournament::from_terms(terms, standing);
let mut session = session(joined.transport);
session.home.from_lobby = true;
session.home.wants_to_watch = joined.watching;
session.bot = joined.bot;
online.0 = Some(session);
next_vphase.set(VersusPhase::Countdown);
next_screen.set(Screen::Versus);
}
fn queued_feedback(
tr: &'static crate::app::i18n::Tr,
watching: bool,
picking: bool,
ahead: u8,
) -> String {
match (watching, picking, ahead) {
(true, _, _) => tr.lobby_queued_watching.to_string(),
(false, true, _) => tr.lobby_queued_picking.to_string(),
(false, false, 0) => tr.lobby_queued_next.to_string(),
(false, false, n) => fill(tr.lobby_queued_behind, &[("n", &n.to_string())]),
}
}
fn tell_of_a_chair(
state: &mut LobbyState,
tr: &'static crate::app::i18n::Tr,
no_chair: bool,
picking: bool,
) {
let Some(joined) = state.joined_mut() else {
return;
};
let was_queued = joined.queued.take().is_some();
let no_chair = no_chair && !joined.watching;
if joined.no_chair == no_chair && joined.host_picking == picking && !was_queued {
return;
}
joined.no_chair = no_chair;
joined.host_picking = picking;
state.feedback = joiners_line(tr, joined).to_string();
}
pub(super) fn joiners_line(tr: &'static crate::app::i18n::Tr, joined: &Joined) -> &'static str {
match (joined.host_picking, joined.watching, joined.no_chair) {
(true, _, _) => tr.lobby_host_picking,
(false, true, _) => tr.lobby_watching,
(false, false, true) => tr.lobby_no_chair,
(false, false, false) => tr.lobby_aboard,
}
}
#[derive(Default)]
struct HostWord {
heard: bool,
started: Option<Invitation>,
caught: Option<crate::app::net::catch_up::CaughtUp>,
put_out: Option<PutOut>,
queued: Option<u8>,
no_chair: Option<bool>,
picking: bool,
said: Vec<(crate::transport::WireName, crate::transport::WireChat)>,
table: Option<(Vec<String>, [u8; MAX_PLAYERS])>,
host_terms: Option<MatchTerms>,
}
fn hear_the_host(joined: &mut Joined, hello: Option<NetMsg>, delta: f32) -> HostWord {
let mut word = HostWord::default();
joined.host_goodbye.age(delta);
let transport = &mut joined.transport;
if let Some(hello) = hello {
transport.send(hello);
}
for (msg, _) in transport.recv_all() {
if msg == NetMsg::Leaving {
joined.host_goodbye.spoke();
} else if msg == NetMsg::Full {
joined.told_full = true;
} else {
word.heard = true;
joined.told_full = false;
joined.host_goodbye.heard();
}
match msg {
NetMsg::Start(start) => word.started = Some(start),
NetMsg::Kicked => PutOut::hear(&mut word.put_out, PutOut::Kicked),
NetMsg::NoBots => PutOut::hear(&mut word.put_out, PutOut::NoBots),
NetMsg::Leaving | NetMsg::Full => {}
NetMsg::CatchUp {
frame,
part,
parts,
bytes,
} => {
if let Some(whole) = joined.catching_up.take(frame, part, parts, bytes) {
word.caught = Some(whole);
}
}
NetMsg::Incompatible { version } => {
PutOut::hear(&mut word.put_out, PutOut::Mismatch(version));
}
NetMsg::Queued { ahead } => word.queued = Some(ahead),
NetMsg::Chat { name, text } => word.said.push((name, text)),
NetMsg::SpectatorVote { .. }
| NetMsg::SpectatorTally { .. }
| NetMsg::SpectatorPick { .. }
| NetMsg::CrowdPicks { .. } => {}
NetMsg::Roster {
seats,
names,
terms,
kinds,
beach: _,
chair,
picking,
} => {
word.table = Some((
crate::transport::table_from_wire(&names)
.into_iter()
.take(usize::from(seats))
.collect(),
kinds,
));
word.host_terms = Some(terms);
word.no_chair = chair.map(|chair| !chair);
word.picking = picking;
}
NetMsg::Ping { stamp } => transport.send(NetMsg::Pong { stamp }),
NetMsg::Hello { .. }
| NetMsg::Watch { .. }
| NetMsg::Pong { .. }
| NetMsg::Clock { .. }
| NetMsg::Inputs(_)
| NetMsg::Hash { .. }
| NetMsg::Pause { .. }
| NetMsg::Abandoned { .. }
| NetMsg::Resume { .. } => {}
}
}
if joined.host_goodbye.stood(LOBBY_GOODBYE_GRACE) {
PutOut::hear(&mut word.put_out, PutOut::Closed);
}
word
}
pub(super) const LOBBY_GOODBYE_GRACE: f32 = 1.5;
pub fn join_tick(
time: Res<Time>,
settings: Res<GameSettings>,
mut state: ResMut<LobbyState>,
mut online: ResMut<Online>,
mut tournament: ResMut<crate::app::tournament::Tournament>,
mut next_screen: ResMut<NextState<Screen>>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
let Some(joined) = state.joined_mut() else {
return;
};
let say_hello = once_a_second(&mut joined.hello_in, time.delta_secs());
let hello = say_hello.then(|| joined.greeting(&settings.names[0]));
let word = hear_the_host(joined, hello, time.delta_secs());
if answer_the_silence(&mut state, settings.tr(), word.heard, time.delta_secs()) {
return;
}
if let Some(why) = word.put_out {
state.let_go();
state.feedback = why.feedback(settings.tr());
return;
}
let HostWord {
heard: _,
started,
caught,
put_out: _,
queued,
no_chair,
picking,
said,
table,
host_terms,
} = word;
if let Some((table, kinds)) = table {
state.table_kinds = kinds
.iter()
.take(table.len())
.map(|&byte| crate::transport::kind_from_byte(byte))
.collect();
if let Some(terms) = &host_terms {
state.seated = Seated::from_terms(terms, table.len() as u8);
}
let seated = state.seated;
state.table = (0..table.len())
.map(|place| {
let name = table.get(place).map_or("", String::as_str);
match name.is_empty() {
true => crate::app::seat_label(settings.tr(), seated.seat_of(place)),
false => name.to_string(),
}
})
.collect();
}
if let Some(terms) = host_terms
&& let Some(joined) = state.joined_mut()
{
joined.host_terms = Some(HostTerms { terms });
}
for (name, text) in said {
state.hear(&name, &text);
}
if let Some(no_chair) = no_chair {
tell_of_a_chair(&mut state, settings.tr(), no_chair, picking);
}
if let Some(ahead) = queued {
let watching = state.joined().is_some_and(|joined| joined.watching);
state.feedback = queued_feedback(settings.tr(), watching, picking, ahead);
if let Some(joined) = state.joined_mut() {
joined.queued = Some(ahead);
}
}
if let Some(caught) = caught {
catch_up_with_the_round(
&mut state,
&mut online,
&mut tournament,
&mut next_screen,
&mut next_vphase,
caught,
);
return;
}
let played = state.joined().and_then(Joined::played_seed);
let started = started.filter(|invitation| a_fresh_invitation(played, invitation));
accept_the_invitation(
&mut state,
&mut online,
&mut tournament,
&mut next_screen,
&mut next_vphase,
started,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bot_turned_away_hears_the_beach_is_not_taking_bots() {
let tr = &crate::app::i18n::EN;
for order in [
[PutOut::NoBots, PutOut::Kicked],
[PutOut::Kicked, PutOut::NoBots],
] {
let mut put_out = None;
for why in order {
PutOut::hear(&mut put_out, why);
}
assert_eq!(put_out, Some(PutOut::NoBots));
}
assert_eq!(PutOut::NoBots.feedback(tr), tr.lobby_no_bots);
let mut put_out = None;
PutOut::hear(&mut put_out, PutOut::Kicked);
assert_eq!(
put_out.map(|why| why.feedback(tr)),
Some(tr.lobby_kicked_you.to_string())
);
}
#[test]
fn a_nameless_place_does_not_cut_the_roster_short() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut joined = Joined::dialled(
UdpTransport::join(("127.0.0.1", port)).expect("join"),
false,
);
let settle = || std::thread::sleep(std::time::Duration::from_millis(20));
joined.transport.send(NetMsg::hello("Cy"));
settle();
let _ = host.recv_all();
let table = ["Anna".to_string(), String::new(), "Cy".to_string()];
host.send(NetMsg::Roster {
seats: 3,
names: crate::transport::wire_table(&table),
terms: MatchTerms::default(),
kinds: [0; MAX_PLAYERS],
beach: [0; crate::transport::WIRE_NAME],
chair: Some(true),
picking: false,
});
settle();
let word = hear_the_host(&mut joined, None, 0.0);
let (heard, _) = word.table.expect("a table");
assert_eq!(heard, table, "three places, the nameless one included");
}
#[test]
fn a_roster_says_whether_there_is_a_chair_and_ends_the_line() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut state = LobbyState {
standing: Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", port)).expect("join"),
false,
)),
..LobbyState::default()
};
let settle = || std::thread::sleep(std::time::Duration::from_millis(20));
state
.joined()
.expect("joined")
.transport
.send(NetMsg::hello("Gil"));
settle();
let _ = host.recv_all();
let roster = |chair| NetMsg::Roster {
seats: 6,
names: crate::transport::wire_table(&[]),
terms: MatchTerms::default(),
kinds: [0; MAX_PLAYERS],
beach: [0; crate::transport::WIRE_NAME],
chair,
picking: false,
};
let tr = &crate::app::i18n::EN;
let hear = |state: &mut LobbyState| {
settle();
let joined = state.joined_mut().expect("joined");
let word = hear_the_host(joined, None, 0.0);
if let Some(no_chair) = word.no_chair {
tell_of_a_chair(state, tr, no_chair, word.picking);
}
};
host.send(roster(Some(false)));
hear(&mut state);
assert!(state.joined().is_some_and(|joined| joined.no_chair));
assert_eq!(state.feedback, tr.lobby_no_chair);
host.send(roster(Some(true)));
hear(&mut state);
assert!(state.joined().is_some_and(|joined| !joined.no_chair));
assert_eq!(state.feedback, tr.lobby_aboard, "a chair came free");
if let Some(joined) = state.joined_mut() {
joined.queued = Some(0);
}
state.feedback = "next up".into();
host.send(roster(None));
hear(&mut state);
assert_eq!(state.joined().and_then(|joined| joined.queued), Some(0));
assert_eq!(state.feedback, "next up");
host.send(roster(Some(false)));
hear(&mut state);
assert_eq!(state.joined().and_then(|joined| joined.queued), None);
assert_eq!(state.feedback, tr.lobby_no_chair, "never told aboard");
}
#[test]
fn a_full_beach_is_called_until_a_place_frees() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut state = LobbyState {
standing: Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", port)).expect("join"),
false,
)),
..LobbyState::default()
};
let settle = || std::thread::sleep(std::time::Duration::from_millis(20));
let tr = &crate::app::i18n::EN;
state
.joined()
.expect("joined")
.transport
.send(NetMsg::hello("Gil"));
settle();
let _ = host.recv_all();
let tick = |state: &mut LobbyState, delta: f32| {
settle();
let joined = state.joined_mut().expect("joined");
let word = hear_the_host(joined, None, delta);
assert_eq!(word.put_out, None, "Full is not the end of the call");
answer_the_silence(state, tr, word.heard, delta)
};
host.send(NetMsg::Full);
assert!(!tick(&mut state, 1.0), "still calling");
host.send(NetMsg::Full);
assert!(!tick(&mut state, NO_ANSWER_AFTER - 2.0), "still calling");
host.send(NetMsg::chat("", "hi"));
assert!(!tick(&mut state, 0.5));
assert_eq!(state.feedback, tr.lobby_aboard);
let mut state = LobbyState {
standing: Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", port)).expect("join"),
false,
)),
..LobbyState::default()
};
state
.joined()
.expect("joined")
.transport
.send(NetMsg::hello("Hal"));
settle();
let _ = host.recv_all();
host.send(NetMsg::Full);
assert!(!tick(&mut state, NO_ANSWER_AFTER - 1.0));
host.send(NetMsg::Full);
assert!(tick(&mut state, 1.0), "given up on");
assert_eq!(state.feedback, tr.lobby_no_room);
}
#[test]
fn a_lobby_joiner_believes_a_goodbye_only_after_the_grace() {
let host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut joined = Joined::dialled(
UdpTransport::join(("127.0.0.1", port)).expect("join"),
false,
);
let mut host = host;
let settle = || std::thread::sleep(std::time::Duration::from_millis(20));
joined.transport.send(NetMsg::hello("Cy"));
settle();
assert_eq!(host.recv_all().len(), 1, "greeted");
host.send(NetMsg::Leaving);
settle();
assert!(hear_the_host(&mut joined, None, 0.0).put_out.is_none());
assert!(
hear_the_host(&mut joined, None, 1.0).put_out.is_none(),
"inside the grace"
);
host.send(NetMsg::chat("Sam", "hi"));
settle();
assert!(hear_the_host(&mut joined, None, 0.1).put_out.is_none());
assert!(
hear_the_host(&mut joined, None, LOBBY_GOODBYE_GRACE)
.put_out
.is_none(),
"taken back by the host's next word"
);
host.send(NetMsg::Leaving);
settle();
assert!(hear_the_host(&mut joined, None, 0.0).put_out.is_none());
assert_eq!(
hear_the_host(&mut joined, None, LOBBY_GOODBYE_GRACE).put_out,
Some(PutOut::Closed),
"and then the beach has closed"
);
}
fn asking() -> JoinAsk {
JoinAsk {
enter_on: None,
intent: None,
auto_join: false,
busy: false,
named: true,
to_watch: false,
}
}
fn beaches(spec: &[(u8, u8)]) -> Vec<HostEntry> {
spec.iter()
.enumerate()
.map(|(i, (taken, seats))| HostEntry {
addr: format!("10.0.0.{}:47777", i + 1).parse().expect("addr"),
id: i as u64 + 1,
name: format!("beach{i}"),
host: "Sam".to_string(),
taken: *taken,
seats: *seats,
running: false,
bots: false,
full: false,
age: 0.0,
heard: 1,
})
.collect()
}
fn open() -> Vec<HostEntry> {
beaches(&[(1, 6), (1, 6), (1, 6)])
}
#[test]
fn a_dialled_beach_hears_the_same_greeting_as_a_listed_one() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut state = LobbyState::default();
let mut settings = GameSettings::default();
settings.names[0] = "Cy".into();
let there: SocketAddr = format!("127.0.0.1:{port}").parse().expect("addr");
dial_at(&mut state, &settings, &crate::app::i18n::EN, there);
assert!(state.joined().is_some(), "the socket is open and greeting");
let mut greeted = None;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
for (msg, _) in host.recv_all() {
if let NetMsg::Hello { name, .. } = msg {
greeted = Some(crate::transport::name_from_wire(&name));
}
}
if greeted.is_some() {
break;
}
}
assert_eq!(greeted.as_deref(), Some("Cy"), "the host heard who called");
}
#[test]
fn a_key_picks_the_row_it_names_and_no_further() {
let hosts = open();
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(1),
..asking()
},
&hosts
),
Pick::Take(1)
);
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(6),
..asking()
},
&hosts
),
Pick::Nothing,
"a cursor past the end of the list"
);
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(2),
..asking()
},
&hosts
),
Pick::Take(2),
"Enter takes the cursor's beach"
);
assert_eq!(
which_beach(asking(), &hosts),
Pick::Nothing,
"and nothing pressed takes nothing"
);
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(0),
..asking()
},
&[]
),
Pick::Nothing,
"nor does a key with an empty hall"
);
}
#[test]
fn a_nameless_player_is_asked_first_and_lands_where_they_meant() {
let hosts = open();
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(2),
named: false,
..asking()
},
&hosts
),
Pick::AskName(2),
"asked, and asked about the right beach"
);
assert_eq!(
which_beach(
JoinAsk {
intent: Some(Intent::Join(hosts[2].addr)),
..asking()
},
&hosts
),
Pick::Take(2)
);
let mut resorted = open();
resorted.reverse();
assert_eq!(
which_beach(
JoinAsk {
intent: Some(Intent::Join(hosts[2].addr)),
..asking()
},
&resorted
),
Pick::Take(0)
);
assert_eq!(
which_beach(
JoinAsk {
intent: Some(Intent::Join(hosts[2].addr)),
..asking()
},
&hosts[..1]
),
Pick::Gone
);
}
#[test]
fn a_full_beach_is_refused_rather_than_queued_for() {
let hosts = beaches(&[(6, 6), (5, 6), (0, 0)]);
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(0),
..asking()
},
&hosts
),
Pick::Full(0)
);
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(1),
..asking()
},
&hosts
),
Pick::Take(1),
"one chair is still a chair"
);
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(2),
..asking()
},
&hosts
),
Pick::Take(2),
"a beach that described no table is not a full one"
);
}
#[test]
fn a_full_socket_is_full_for_a_watcher_too() {
let mut hosts = open();
hosts[0].full = true;
let ask = |to_watch| {
which_beach(
JoinAsk {
enter_on: Some(0),
to_watch,
..asking()
},
&hosts,
)
};
assert_eq!(ask(true), Pick::Full(0));
assert_eq!(ask(false), Pick::Full(0));
}
#[test]
fn keys_do_nothing_once_you_are_already_at_a_beach() {
let hosts = open();
for pressed in [Some(0), Some(1)] {
assert_eq!(
which_beach(
JoinAsk {
enter_on: pressed,
busy: true,
..asking()
},
&hosts
),
Pick::Nothing
);
}
assert_eq!(
which_beach(
JoinAsk {
intent: Some(Intent::Join(hosts[0].addr)),
busy: true,
..asking()
},
&hosts
),
Pick::Nothing
);
}
#[test]
fn the_intent_to_host_is_not_the_intent_to_join() {
let hosts = open();
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(1),
intent: Some(Intent::Host),
..asking()
},
&hosts
),
Pick::Nothing
);
}
#[test]
fn a_beach_that_never_answers_is_given_up_on() {
let mut state = LobbyState {
standing: Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", 47999)).expect("join"),
false,
)),
..LobbyState::default()
};
let mut listed = beaches(&[(1, 6), (1, 6)]);
listed[0].addr = "127.0.0.1:47999".parse().expect("addr");
state.hosts = listed;
let quiet = answer_the_silence(&mut state, &crate::app::i18n::EN, false, 1.0);
assert!(!quiet, "one second is a lost packet, not an empty address");
assert!(state.joined().is_some(), "still calling");
let quiet = answer_the_silence(&mut state, &crate::app::i18n::EN, false, NO_ANSWER_AFTER);
assert!(quiet, "and then it stops calling");
assert!(state.joined().is_none(), "the socket is let go");
assert!(
state.feedback.contains("127.0.0.1:47999"),
"and says which address went unanswered: {:?}",
state.feedback
);
assert!(
!state.standing().at_a_beach(),
"and the lobby is back to browsing"
);
let left: Vec<&str> = state.hosts.iter().map(|host| host.name.as_str()).collect();
assert_eq!(
left,
["beach1"],
"the beach that never answered is not offered again"
);
}
#[test]
fn a_watcher_in_the_queue_is_not_waiting_for_a_chair() {
let tr = &crate::app::i18n::EN;
assert_eq!(queued_feedback(tr, false, false, 0), tr.lobby_queued_next);
assert_eq!(
queued_feedback(tr, false, false, 3),
fill(tr.lobby_queued_behind, &[("n", "3")]),
"a player is told where in the line it stands"
);
for ahead in [0, 1, 9] {
for picking in [false, true] {
assert_eq!(
queued_feedback(tr, true, picking, ahead),
tr.lobby_queued_watching,
"with {ahead} ahead"
);
}
}
assert_ne!(
tr.lobby_queued_watching, tr.lobby_queued_next,
"and it is not the line a player is given"
);
}
#[test]
fn a_player_in_line_while_the_host_picks_watches_that_round() {
let tr = &crate::app::i18n::EN;
for ahead in [0, 2] {
assert_eq!(
queued_feedback(tr, false, true, ahead),
tr.lobby_queued_picking
);
}
}
#[test]
fn the_first_word_is_what_makes_it_a_beach() {
let mut state = LobbyState {
standing: Standing::Joining(Joined::dialled(
UdpTransport::join(("127.0.0.1", 47999)).expect("join"),
false,
)),
feedback: "calling...".into(),
..LobbyState::default()
};
answer_the_silence(&mut state, &crate::app::i18n::EN, true, 0.1);
assert_eq!(state.feedback, crate::app::i18n::EN.lobby_aboard);
assert!(state.joined().is_some_and(|joined| joined.host_answered));
answer_the_silence(
&mut state,
&crate::app::i18n::EN,
false,
NO_ANSWER_AFTER - 0.1,
);
answer_the_silence(&mut state, &crate::app::i18n::EN, true, 0.1);
let quiet = answer_the_silence(&mut state, &crate::app::i18n::EN, false, 1.0);
assert!(!quiet, "the clock restarted when the host spoke");
assert!(state.joined().is_some());
}
#[test]
fn a_dialled_address_is_not_a_row_of_the_list() {
let hosts = open();
let there: SocketAddr = "192.168.1.5:47777".parse().expect("addr");
assert_eq!(
which_beach(
JoinAsk {
enter_on: Some(1),
intent: Some(Intent::Dial(there)),
..asking()
},
&hosts
),
Pick::Nothing
);
assert_eq!(
which_beach(
JoinAsk {
intent: Some(Intent::Dial(there)),
..asking()
},
&[]
),
Pick::Nothing
);
}
#[test]
fn the_round_just_played_is_not_an_invitation_back_into_it() {
let invitation = |seed| Invitation {
seats: 2,
seat: Some(1),
terms: MatchTerms {
seed,
..MatchTerms::default()
},
..Invitation::blank()
};
assert!(
!a_fresh_invitation(Some(42), &invitation(42)),
"the host is repeating the round we just left"
);
assert!(
a_fresh_invitation(Some(42), &invitation(43)),
"a fresh seed is a fresh round, and this table is in it"
);
assert!(a_fresh_invitation(None, &invitation(42)));
}
#[test]
fn the_unattended_hook_takes_the_first_beach_without_being_asked() {
let hosts = open();
assert_eq!(
which_beach(
JoinAsk {
auto_join: true,
named: false,
..asking()
},
&hosts
),
Pick::Take(0),
"nameless, and taken anyway"
);
assert_eq!(
which_beach(
JoinAsk {
auto_join: true,
named: false,
..asking()
},
&[]
),
Pick::Nothing,
"but it cannot take what is not there"
);
}
}