use crate::app::match_setup::{MapChoice, MatchConfig};
use crate::app::net::Online;
use crate::app::tournament::Tournament;
use crate::app::{Screen, VersusPhase};
use bevy::prelude::*;
#[derive(Resource, Default, Debug)]
pub struct LocalTable {
set_here: bool,
opening: Option<(MapChoice, usize)>,
again: bool,
restart: crate::app::menu_ui::SecondPress,
}
impl LocalTable {
pub fn note_round(&mut self, set_here: bool, config: &MatchConfig, tournament: &Tournament) {
self.set_here = set_here;
self.again = false;
self.restart = crate::app::menu_ui::SecondPress::default();
if set_here && (!tournament.in_series() || tournament.round <= 1) {
self.opening = Some((config.map, config.custom));
}
}
pub fn keeps_the_table(&self) -> bool {
self.again
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Rematch {
Round,
Series,
SeriesAsked,
}
impl Rematch {
pub fn settled(self) -> Rematch {
match self {
Rematch::Round => Rematch::Round,
Rematch::Series | Rematch::SeriesAsked => Rematch::Series,
}
}
}
pub fn asks_first(rematch: Rematch, tournament: &Tournament) -> bool {
match rematch.settled() {
Rematch::Round => false,
Rematch::Series | Rematch::SeriesAsked => tournament.is_running(),
}
}
pub fn press_rematch(
table: &mut LocalTable,
rematch: Rematch,
tournament: &Tournament,
now: f32,
) -> Option<Rematch> {
let go = !asks_first(rematch, tournament) || table.restart.press(now, ());
go.then(|| rematch.settled())
}
pub fn settle_rematch_ask(table: &mut ResMut<LocalTable>, other_key: bool, now: f32) {
if !table.restart.asked() {
return;
}
let lapsed = table.bypass_change_detection().restart.lapse(now);
if lapsed {
table.set_changed();
} else if other_key {
table.restart.take_back();
}
}
pub fn rematch(table: &LocalTable, online: &Online, tournament: &Tournament) -> Option<Rematch> {
if !table.set_here || online.0.is_some() {
return None;
}
Some(match (tournament.in_series(), table.restart.asked()) {
(true, true) => Rematch::SeriesAsked,
(true, false) => Rematch::Series,
(false, _) => Rematch::Round,
})
}
pub fn restart_offered(
table: &LocalTable,
online: &Online,
tournament: &Tournament,
vphase: VersusPhase,
) -> bool {
let playing = match vphase {
VersusPhase::Countdown | VersusPhase::Running => true,
VersusPhase::Over => false,
};
playing && rematch(table, online, tournament).is_some()
}
pub fn rematch_key(tr: &crate::app::i18n::Tr, rematch: Rematch) -> &'static str {
match rematch {
Rematch::Round => tr.prompt_rematch,
Rematch::Series => tr.prompt_rematch_series,
Rematch::SeriesAsked => tr.prompt_rematch_series_confirm,
}
}
pub fn over_prompt(
tr: &crate::app::i18n::Tr,
online: &Online,
series_on: bool,
rematch: Option<Rematch>,
keep: crate::app::replays::KeepOffer,
) -> String {
let door = crate::app::play_input::enter_door(tr, online, series_on);
let again = rematch.map(|again| rematch_key(tr, again));
let keep = crate::app::replays::keep_words(tr, keep);
std::iter::once(door)
.chain(again)
.chain(keep)
.collect::<Vec<_>>()
.join(" | ")
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Again {
Rematch(Rematch),
Restart,
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct PlayAgain<'w> {
pub table: ResMut<'w, LocalTable>,
pub config: ResMut<'w, MatchConfig>,
pub next_vphase: ResMut<'w, NextState<VersusPhase>>,
pub beaches: Option<Res<'w, crate::app::match_setup::CustomBeaches>>,
}
impl PlayAgain<'_> {
pub fn let_go(&mut self) {
if std::mem::take(&mut self.table.again) {
self.config.armed = false;
}
}
pub fn ask(
&mut self,
again: Again,
tournament: &mut Tournament,
next_screen: &mut NextState<Screen>,
) {
if again == Again::Rematch(Rematch::Series) {
*tournament = Tournament::start(self.config.series);
if let Some((map, custom)) = self.table.opening {
self.config.map = map;
self.config.custom = custom;
}
}
if self.config.random && matches!(again, Again::Rematch(_)) {
let none = crate::app::match_setup::CustomBeaches::default();
crate::app::match_setup::draw_map(
&mut self.config,
self.beaches.as_deref().unwrap_or(&none),
crate::app::clock::fresh_seed(),
);
}
self.config.seed = None;
self.table.again = true;
self.config.armed = true;
next_screen.set(Screen::Versus);
self.next_vphase.set(VersusPhase::Countdown);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::tournament::SeriesLength;
use bevy::ecs::system::RunSystemOnce;
fn here() -> LocalTable {
let mut table = LocalTable::default();
table.note_round(true, &MatchConfig::default(), &Tournament::default());
table
}
#[test]
fn a_rematch_is_a_couch_tables_alone() {
let alone = Online::default();
let single = Tournament::default();
assert_eq!(rematch(&here(), &alone, &single), Some(Rematch::Round));
let series = Tournament::start(SeriesLength::BestOfThree);
assert_eq!(rematch(&here(), &alone, &series), Some(Rematch::Series));
let mut elsewhere = here();
elsewhere.note_round(false, &MatchConfig::default(), &single);
assert_eq!(rematch(&elsewhere, &alone, &single), None);
assert!(!restart_offered(
&elsewhere,
&alone,
&single,
VersusPhase::Running
));
assert!(restart_offered(
&here(),
&alone,
&single,
VersusPhase::Countdown
));
assert!(!restart_offered(
&here(),
&alone,
&single,
VersusPhase::Over
));
}
#[test]
fn restarting_a_series_midway_asks_first() {
use crate::app::menu_ui::SECOND_PRESS_SECS;
let alone = Online::default();
let tr = crate::app::i18n::Lang::En.tr();
let single = Tournament::default();
let mut table = here();
assert_eq!(
press_rematch(&mut table, Rematch::Round, &single, 0.0),
Some(Rematch::Round)
);
let series = Tournament::start(SeriesLength::BestOfThree);
let mut table = here();
let first = rematch(&table, &alone, &series).expect("a series to restart");
assert_eq!(rematch_key(tr, first), tr.prompt_rematch_series);
assert_eq!(press_rematch(&mut table, first, &series, 0.0), None);
let asked = rematch(&table, &alone, &series).expect("still one");
assert_eq!(asked, Rematch::SeriesAsked);
assert_eq!(rematch_key(tr, asked), tr.prompt_rematch_series_confirm);
assert_eq!(
press_rematch(&mut table, asked, &series, 1.0),
Some(Rematch::Series),
"R again restarts it"
);
let mut table = here();
assert_eq!(press_rematch(&mut table, first, &series, 0.0), None);
let mut world = World::new();
world.insert_resource(table);
let _ = world.run_system_once(move |mut table: ResMut<LocalTable>| {
settle_rematch_ask(&mut table, false, SECOND_PRESS_SECS + 0.5);
});
let mut table = world.remove_resource::<LocalTable>().expect("the table");
assert_eq!(rematch(&table, &alone, &series), Some(Rematch::Series));
assert_eq!(press_rematch(&mut table, first, &series, 5.0), None);
let mut world = World::new();
world.insert_resource(table);
let _ = world.run_system_once(|mut table: ResMut<LocalTable>| {
settle_rematch_ask(&mut table, true, 5.5);
});
let table = world.remove_resource::<LocalTable>().expect("the table");
assert_eq!(rematch(&table, &alone, &series), Some(Rematch::Series));
}
#[test]
fn a_series_rematch_starts_over_and_a_restart_does_not() {
let mut world = World::new();
world.init_resource::<NextState<Screen>>();
world.init_resource::<NextState<VersusPhase>>();
world.init_resource::<crate::app::match_setup::CustomBeaches>();
let config = MatchConfig {
series: SeriesLength::BestOfThree,
..MatchConfig::default()
};
let mut tournament = Tournament::start(SeriesLength::BestOfThree);
let mut table = LocalTable::default();
table.note_round(true, &config, &tournament);
world.insert_resource(table);
world.insert_resource(config);
tournament.round = 2;
tournament.wins[1] = 1;
world.resource_mut::<MatchConfig>().map = MapChoice::GenSmall;
world.resource_mut::<MatchConfig>().armed = false;
let ask = |world: &mut World, again: Again, tournament: &mut Tournament| {
let mut state: bevy::ecs::system::SystemState<(PlayAgain, ResMut<NextState<Screen>>)> =
bevy::ecs::system::SystemState::new(world);
let (mut play, mut next) = state.get_mut(world).expect("the params");
play.ask(again, tournament, &mut next);
};
ask(&mut world, Again::Restart, &mut tournament);
assert_eq!(tournament.round, 2, "a restart keeps the series' place");
assert_eq!(tournament.wins[1], 1);
assert_eq!(world.resource::<MatchConfig>().map, MapChoice::GenSmall);
assert!(world.resource::<MatchConfig>().armed);
assert!(world.resource::<LocalTable>().keeps_the_table());
ask(&mut world, Again::Rematch(Rematch::Series), &mut tournament);
assert_eq!(tournament.round, 1, "the series starts over");
assert_eq!(tournament.wins, [0; crate::sim::MAX_PLAYERS]);
assert!(tournament.is_running());
assert_eq!(world.resource::<MatchConfig>().map, MapChoice::Classic);
}
#[test]
fn a_random_rematch_draws_again() {
let mut world = World::new();
world.init_resource::<NextState<Screen>>();
world.init_resource::<NextState<VersusPhase>>();
world.init_resource::<crate::app::match_setup::CustomBeaches>();
world.init_resource::<LocalTable>();
world.insert_resource(MatchConfig {
seats: 5,
random: true,
map: MapChoice::GenOcean,
seed: Some(5),
..MatchConfig::default()
});
let ask = |world: &mut World, again: Again| {
let mut state: bevy::ecs::system::SystemState<(PlayAgain, ResMut<NextState<Screen>>)> =
bevy::ecs::system::SystemState::new(world);
let (mut play, mut next) = state.get_mut(world).expect("the params");
play.ask(again, &mut Tournament::default(), &mut next);
};
ask(&mut world, Again::Restart);
assert_eq!(world.resource::<MatchConfig>().map, MapChoice::GenOcean);
assert_eq!(world.resource::<MatchConfig>().seed, None, "a fresh seed");
let mut drawn = std::collections::BTreeSet::new();
for _ in 0..200 {
ask(&mut world, Again::Rematch(Rematch::Round));
let config = world.resource::<MatchConfig>();
assert!(crate::app::match_setup::holds(config.map, 5));
assert!(config.random);
drawn.insert(format!("{:?}", config.map));
}
assert!(drawn.len() > 1, "drawn again, not kept: {drawn:?}");
}
#[test]
fn a_rematch_comes_back_in_armed_and_on_the_count() {
use crate::app::session::{end_versus, forget_a_round_not_entered};
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.insert_state(Screen::Versus);
app.insert_state(VersusPhase::Running);
app.init_resource::<Online>();
app.init_resource::<crate::app::Playback>();
app.init_resource::<crate::app::Recorder>();
app.init_resource::<crate::app::session::ReelThread>();
app.init_resource::<crate::app::replays::RoundToKeep>();
app.init_resource::<crate::app::PendingActions>();
app.init_resource::<crate::app::Controllers>();
app.init_resource::<crate::app::Resuming>();
app.init_resource::<crate::app::Daily>();
app.init_resource::<Tournament>();
app.init_resource::<MatchConfig>();
app.init_resource::<crate::app::match_setup::CustomBeaches>();
let mut table = LocalTable::default();
table.note_round(true, &MatchConfig::default(), &Tournament::default());
app.insert_resource(table);
#[derive(Resource, Default)]
struct Entered(Vec<bool>);
app.init_resource::<Entered>();
app.add_systems(OnExit(Screen::Versus), end_versus);
app.add_systems(
OnEnter(Screen::Versus),
|config: Res<MatchConfig>, mut entered: ResMut<Entered>| entered.0.push(config.armed),
);
app.add_systems(
StateTransition,
forget_a_round_not_entered
.after(bevy::state::state::StateTransitionSystems::EnterSchedules),
);
app.update();
let ask = |app: &mut App, again: Again| {
app.world_mut()
.run_system_cached_with(
|In(again): In<Again>,
mut play: PlayAgain,
mut tournament: ResMut<Tournament>,
mut next: ResMut<NextState<Screen>>| {
play.ask(again, &mut tournament, &mut next);
},
again,
)
.expect("asked");
};
ask(&mut app, Again::Restart);
app.update();
assert_eq!(
app.world().resource::<Entered>().0,
[false, true],
"armed on the way back in"
);
assert_eq!(
*app.world().resource::<State<VersusPhase>>().get(),
VersusPhase::Countdown,
"and on the count in the same frame"
);
ask(&mut app, Again::Rematch(Rematch::Round));
app.world_mut()
.resource_mut::<NextState<Screen>>()
.set(Screen::Menu);
app.update();
assert!(!app.world().resource::<MatchConfig>().armed);
assert!(!app.world().resource::<LocalTable>().keeps_the_table());
}
}