use crate::app::cycle::Cycle;
use crate::app::match_setup::{
CustomBeaches, MapChoice, MapPress, MatchConfig, MatchMenu, Step, TermsDials,
};
use crate::app::net::OnlineSession;
use bevy::prelude::*;
#[derive(Resource, Default)]
pub struct NextMap(Option<Open>);
struct Open {
before: MatchConfig,
fresh: bool,
}
impl NextMap {
pub fn is_up(&self) -> bool {
self.0.is_some()
}
}
pub fn page_up(menu: &MatchMenu) -> bool {
menu.online && menu.step == Step::Map
}
pub fn no_page_up(menu: Res<MatchMenu>) -> bool {
!page_up(&menu)
}
pub fn open_next_map(
next: &mut NextMap,
session: &mut OnlineSession,
config: &mut MatchConfig,
menu: &mut MatchMenu,
beaches: &CustomBeaches,
) {
let before = config.clone();
session.say_picking(true);
let seats = session.next_round_seats(config.lobby_bots);
let map = session.terms.map();
config.seats = seats;
config.clamp_bots();
config.random = false;
config.pick = None;
config.map = map;
config.custom = beaches
.shelf_of(&session.beach)
.filter(|_| map == MapChoice::Custom)
.and_then(|shelf| beaches.among_fitting(seats, shelf))
.unwrap_or(0);
next.0 = Some(Open {
before,
fresh: true,
});
menu.online = true;
menu.step = Step::Map;
menu.seed = crate::app::clock::fresh_seed();
menu.scroll = 0.0;
crate::app::match_setup::put_the_cursor_on_the_pick(menu, config, beaches);
}
fn follow_the_table(session: &OnlineSession, config: &mut MatchConfig) {
let seats = session.next_round_seats(config.lobby_bots);
if config.seats != seats {
config.seats = seats;
config.clamp_bots();
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Close {
Keep,
PutBack,
}
fn close(
next: &mut NextMap,
close: Close,
config: &mut MatchConfig,
menu: &mut MatchMenu,
beaches: &CustomBeaches,
) {
if let Some(open) = next.0.take() {
match close {
Close::PutBack => *config = open.before,
Close::Keep => crate::app::match_setup::reseat(config, open.before.seats, beaches),
}
}
menu.online = false;
menu.step = Step::Game;
}
fn picked_terms(
session: &mut OnlineSession,
config: &MatchConfig,
seed: u64,
beaches: &CustomBeaches,
) -> crate::transport::MatchTerms {
let mut table = config.clone();
if table.random {
crate::app::match_setup::draw_online_map(
&mut table,
beaches,
crate::app::clock::fresh_seed(),
);
}
let seats = table.seats;
session.beach = crate::app::match_setup::beach_bytes(&table, seats, beaches);
crate::transport::MatchTerms {
map: crate::app::lobby::map_for(&table, seats).index() as u8,
seed,
bots: table.lobby_bots,
..session.terms
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NextMapPress {
Nothing,
Back,
Called(Option<crate::transport::SeriesStanding>),
}
pub fn next_map_keys(
keys: &ButtonInput<KeyCode>,
next: &mut NextMap,
session: &mut OnlineSession,
config: &mut MatchConfig,
menu: &mut MatchMenu,
beaches: &CustomBeaches,
standing: Option<crate::transport::SeriesStanding>,
) -> NextMapPress {
let Some(open) = next.0.as_mut() else {
return NextMapPress::Nothing;
};
follow_the_table(session, config);
if std::mem::take(&mut open.fresh) {
return NextMapPress::Nothing;
}
match crate::app::match_setup::map_page_keys(keys, menu, config, beaches) {
MapPress::Nothing => NextMapPress::Nothing,
MapPress::Back => {
close(next, Close::PutBack, config, menu, beaches);
session.say_picking(false);
NextMapPress::Back
}
MapPress::Chosen(_) => {
let terms = picked_terms(session, config, menu.seed, beaches);
let standing = session.call_next_round(terms, standing);
close(next, Close::Keep, config, menu, beaches);
NextMapPress::Called(standing)
}
}
}
pub fn put_the_page_away(
mut next: ResMut<NextMap>,
mut config: ResMut<MatchConfig>,
mut menu: ResMut<MatchMenu>,
beaches: Res<CustomBeaches>,
) {
if next.is_up() {
close(&mut next, Close::PutBack, &mut config, &mut menu, &beaches);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::{DEFAULT_DELAY, Lockstep};
use crate::transport::{MatchTerms, NetMsg, UdpTransport};
fn series(seed: u64) -> MatchTerms {
MatchTerms {
seed,
series: 1,
..MatchTerms::default()
}
}
fn table(names: &[&str]) -> (OnlineSession, Vec<OnlineSession>) {
let mut host = OnlineSession::new(
UdpTransport::host(0).expect("host socket"),
Lockstep::new(0, (0..=names.len() as u8).collect(), DEFAULT_DELAY),
1 + names.len() as u8,
series(1),
);
let port = host.transport.local_addr().expect("addr").port();
let mut joiners = Vec::new();
for (at, name) in names.iter().enumerate() {
let seat = at as u8 + 1;
let joiner = OnlineSession::new(
UdpTransport::join(("127.0.0.1", port)).expect("join"),
Lockstep::new(seat, (0..=names.len() as u8).collect(), DEFAULT_DELAY),
1 + names.len() as u8,
series(1),
);
joiner.transport.send(NetMsg::hello(name));
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
host.poll_between_rounds(0.0);
if host.transport.peer_count() > at {
break;
}
}
joiners.push(joiner);
}
let plan: Vec<Option<u8>> = (1..=names.len() as u8).map(Some).collect();
host.peers.deal(&plan);
(host, joiners)
}
fn hear(joiners: &mut [OnlineSession]) {
for _ in 0..10 {
std::thread::sleep(std::time::Duration::from_millis(5));
for joiner in joiners.iter_mut() {
joiner.poll_between_rounds(0.0);
}
}
}
fn pick(
next: &mut NextMap,
host: &mut OnlineSession,
config: &mut MatchConfig,
menu: &mut MatchMenu,
card: crate::app::match_setup::Card,
) -> NextMapPress {
let shelf = CustomBeaches::default();
let keys = ButtonInput::<KeyCode>::default();
next_map_keys(&keys, next, host, config, menu, &shelf, None);
menu.slot = crate::app::match_setup::map_slots(&shelf)
.iter()
.position(|(_, slot)| *slot == crate::app::match_setup::Slot::Card(card))
.expect("a card");
let mut enter = ButtonInput::<KeyCode>::default();
enter.press(KeyCode::Enter);
next_map_keys(&enter, next, host, config, menu, &shelf, None)
}
#[test]
fn the_next_map_reaches_the_joiners() {
use crate::app::match_setup::Card;
let (mut host, mut joiners) = table(&["Bo"]);
let (mut next, mut config, mut menu) = (
NextMap::default(),
MatchConfig::default(),
MatchMenu::default(),
);
let shelf = CustomBeaches::default();
open_next_map(&mut next, &mut host, &mut config, &mut menu, &shelf);
assert_eq!(config.seats, 2, "greyed for the next round's table");
assert_eq!(
crate::app::match_setup::picked_card(&config, &shelf),
Card::Map(MapChoice::Classic),
"the cursor on the map just played"
);
hear(&mut joiners);
assert!(joiners[0].picking_map, "the table is told");
let seed = menu.seed;
let press = pick(
&mut next,
&mut host,
&mut config,
&mut menu,
Card::Map(MapChoice::TheMoat),
);
assert!(matches!(press, NextMapPress::Called(_)));
assert!(!next.is_up() && !page_up(&menu), "the page is down");
assert_eq!(
(host.terms.map(), host.terms.seed),
(MapChoice::TheMoat, seed)
);
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
joiners[0].poll_between_rounds(0.0);
if joiners[0].next_round {
break;
}
}
let joiner = &joiners[0];
assert!(joiner.next_round, "the call reached the joiner");
assert_eq!(joiner.terms, host.terms, "the same beach on every peer");
assert!(!joiner.picking_map, "and the wait is over");
let (name, _) = crate::app::lineup::map_words(&crate::app::i18n::EN, joiner);
assert_eq!(
name,
crate::app::match_setup::map_name(&crate::app::i18n::EN, MapChoice::TheMoat),
"the line-up names the new map"
);
}
#[test]
fn the_next_round_deals_its_ai_from_the_table_as_it_sits() {
use crate::app::match_setup::Card;
let mut host = OnlineSession::new(
UdpTransport::host(0).expect("host socket"),
Lockstep::new(0, vec![0], DEFAULT_DELAY),
2,
MatchTerms {
bots: 1,
..series(1)
},
);
let port = host.transport.local_addr().expect("addr").port();
let cy = UdpTransport::join(("127.0.0.1", port)).expect("join");
cy.send(NetMsg::hello("Cy"));
host_hears(&mut host);
assert_eq!(host.transport.peer_count(), 1, "Cy is in line");
let (mut next, mut config, mut menu) = (
NextMap::default(),
MatchConfig::default(),
MatchMenu::default(),
);
let shelf = CustomBeaches::default();
open_next_map(&mut next, &mut host, &mut config, &mut menu, &shelf);
assert_eq!(config.seats, 2, "the host and Cy, and no AI");
let press = pick(
&mut next,
&mut host,
&mut config,
&mut menu,
Card::Map(MapChoice::Classic),
);
assert!(matches!(press, NextMapPress::Called(_)));
assert_eq!(
(host.seats, host.terms.bots),
(2, 0),
"Cy in the AI's chair"
);
assert_eq!(host.peers.seat_of(0), Some(1));
}
#[test]
fn the_partners_picked_hold_into_the_next_round() {
use crate::app::match_setup::Card;
let (mut host, _joiners) = table(&["Bo"]);
host.terms.teams = crate::app::teams::TeamMode::Pairs.index() as u8;
host.arranged = Some(crate::app::net::order::Arranged {
bots: Vec::new(),
ai: vec![1, 3],
});
host.peers.row(0).seat_wish = Some(2);
let (mut next, mut config, mut menu) = (
NextMap::default(),
MatchConfig {
lobby_bots: 2,
..MatchConfig::default()
},
MatchMenu::default(),
);
let shelf = CustomBeaches::default();
open_next_map(&mut next, &mut host, &mut config, &mut menu, &shelf);
let press = pick(
&mut next,
&mut host,
&mut config,
&mut menu,
Card::Map(MapChoice::Classic),
);
assert!(matches!(press, NextMapPress::Called(_)));
assert_eq!(host.seats, 4);
assert_eq!(host.peers.seat_of(0), Some(2), "Bo where the pick put him");
assert_eq!(host.terms.ai_seats(4), [1, 3]);
assert_eq!(host.terms.player_seats(4), [0, 2]);
assert_eq!(host.names[2], "Bo");
}
fn first_frame(
next: &mut NextMap,
host: &mut OnlineSession,
config: &mut MatchConfig,
menu: &mut MatchMenu,
) {
let keys = ButtonInput::<KeyCode>::default();
let shelf = CustomBeaches::default();
next_map_keys(&keys, next, host, config, menu, &shelf, None);
}
fn host_hears(host: &mut OnlineSession) {
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(5));
host.poll_between_rounds(0.1);
}
}
#[test]
fn a_leaver_during_the_pick_greys_the_page_again() {
use crate::app::match_setup::{Card, card_note};
let (mut host, mut joiners) = table(&["Bo", "Cy", "Di"]);
let (mut next, mut config, mut menu) = (
NextMap::default(),
MatchConfig::default(),
MatchMenu::default(),
);
let shelf = CustomBeaches::default();
open_next_map(&mut next, &mut host, &mut config, &mut menu, &shelf);
first_frame(&mut next, &mut host, &mut config, &mut menu);
assert_eq!(config.seats, 4);
let gone = joiners.pop().expect("Di");
gone.transport.send(NetMsg::Leaving);
drop(gone);
host_hears(&mut host);
assert_eq!(host.next_round_seats(0), 3, "Di is gone");
let round_table = Card::Map(MapChoice::RoundTable);
let press = pick(&mut next, &mut host, &mut config, &mut menu, round_table);
assert_eq!(press, NextMapPress::Nothing, "a greyed card calls nothing");
assert!(next.is_up());
assert_eq!(config.seats, 3, "greyed for the table as it now is");
let note = card_note(&crate::app::i18n::EN, round_table, &config, &shelf, true);
assert!(note.contains("this table has 3"), "{note}");
let seed = menu.seed;
let press = pick(
&mut next,
&mut host,
&mut config,
&mut menu,
Card::Map(MapChoice::Classic),
);
assert!(matches!(press, NextMapPress::Called(_)));
assert_eq!(host.seats, 3, "dealt without Di");
assert_eq!(
(host.terms.map(), host.terms.seed),
(MapChoice::Classic, seed),
"on the pick, and the seed its picture was drawn with"
);
}
#[test]
fn a_newcomer_during_the_pick_plays_the_round_after() {
use crate::app::match_setup::Card;
let (mut host, _joiners) = table(&["Bo"]);
let (mut next, mut config, mut menu) = (
NextMap::default(),
MatchConfig::default(),
MatchMenu::default(),
);
let shelf = CustomBeaches::default();
open_next_map(&mut next, &mut host, &mut config, &mut menu, &shelf);
first_frame(&mut next, &mut host, &mut config, &mut menu);
let port = host.transport.local_addr().expect("addr").port();
let cy = UdpTransport::join(("127.0.0.1", port)).expect("join");
cy.send(NetMsg::hello("Cy"));
host_hears(&mut host);
assert_eq!(host.transport.peer_count(), 2, "Cy is on the socket");
assert_eq!(host.next_round_seats(0), 2, "but not at this table");
let press = pick(
&mut next,
&mut host,
&mut config,
&mut menu,
Card::Map(MapChoice::TheMoat),
);
assert!(matches!(press, NextMapPress::Called(_)));
assert_eq!(config.seats, 2);
assert_eq!(host.seats, 2, "dealt the table the map was picked for");
assert_eq!(host.terms.map(), MapChoice::TheMoat);
assert_eq!(host.peers.seat_of(1), None, "Cy watches this round");
assert_eq!(
host.peers.seat_plan(2),
[Some(1), Some(2)],
"and is dealt a chair in the next"
);
}
}