use super::{Phase, Playback, Screen, VersusPhase, editor, lobby, match_setup};
use bevy::prelude::*;
pub(super) fn board_screens(screen: Res<State<Screen>>) -> bool {
match screen.get() {
Screen::Menu => false,
Screen::Puzzle | Screen::Versus | Screen::Editor => true,
Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Interlude
| Screen::Language
| Screen::NewVersion => false,
}
}
pub(super) fn postcard_screens(screen: Res<State<Screen>>) -> bool {
super::postcard_screen(*screen.get())
}
pub(super) fn play_screens(screen: Res<State<Screen>>) -> bool {
matches!(screen.get(), Screen::Puzzle | Screen::Versus)
}
pub(super) fn keys_are_free(menu: Res<crate::app::pause::PauseMenu>) -> bool {
!menu.open
}
pub(super) fn text_entry_open(
lobby: Res<lobby::LobbyState>,
setup: Res<match_setup::MatchMenu>,
editor: Res<editor::EditorState>,
spectators: Res<crate::app::spectators::SpectatorChat>,
) -> bool {
lobby.typing.is_some() || setup.naming.is_some() || editor.is_naming() || spectators.open()
}
pub(super) fn not_watching(playback: Res<Playback>) -> bool {
playback.0.is_none()
}
pub(super) fn sim_should_run(
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
vphase: Res<State<VersusPhase>>,
editor: Res<editor::EditorState>,
) -> bool {
match screen.get() {
Screen::Menu => false,
Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Interlude
| Screen::Language
| Screen::NewVersion => false,
Screen::Puzzle => *phase.get() == Phase::Running,
Screen::Versus => *vphase.get() == VersusPhase::Running,
Screen::Editor => editor.is_testing(),
}
}
pub(super) fn versus_running(screen: Res<State<Screen>>, phase: Res<State<VersusPhase>>) -> bool {
*screen.get() == Screen::Versus && *phase.get() == VersusPhase::Running
}
pub(super) fn versus_over(screen: Res<State<Screen>>, phase: Res<State<VersusPhase>>) -> bool {
*screen.get() == Screen::Versus && *phase.get() == VersusPhase::Over
}
pub(super) fn puzzle_setup(screen: Res<State<Screen>>, phase: Res<State<Phase>>) -> bool {
*screen.get() == Screen::Puzzle && *phase.get() == Phase::Setup
}
pub(super) fn puzzle_running(screen: Res<State<Screen>>, phase: Res<State<Phase>>) -> bool {
*screen.get() == Screen::Puzzle && *phase.get() == Phase::Running
}
pub(super) fn puzzle_done(screen: Res<State<Screen>>, phase: Res<State<Phase>>) -> bool {
*screen.get() == Screen::Puzzle && matches!(*phase.get(), Phase::Won | Phase::Lost)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::{Screen, VersusPhase};
use bevy::ecs::system::RunSystemOnce;
use bevy::prelude::{State, World};
fn world(screen: Screen, phase: Phase, vphase: VersusPhase) -> World {
let mut world = World::new();
world.insert_resource(State::new(screen));
world.insert_resource(State::new(phase));
world.insert_resource(State::new(vphase));
world.insert_resource(editor::EditorState::default());
world.insert_resource(lobby::LobbyState::default());
world.insert_resource(match_setup::MatchMenu::default());
world.insert_resource(Playback::default());
world.insert_resource(crate::app::spectators::SpectatorChat::default());
world
}
const EVERY_SCREEN: [Screen; 14] = [
Screen::Menu,
Screen::Puzzle,
Screen::Versus,
Screen::Editor,
Screen::Lobby,
Screen::Settings,
Screen::Controls,
Screen::MatchSetup,
Screen::Achievements,
Screen::StageSelect,
Screen::Replays,
Screen::Interlude,
Screen::Language,
Screen::NewVersion,
];
fn ask(
screen: Screen,
phase: Phase,
vphase: VersusPhase,
what: fn(&mut World) -> bool,
) -> bool {
what(&mut world(screen, phase, vphase))
}
#[test]
fn a_board_is_drawn_on_the_three_screens_that_have_one() {
for screen in EVERY_SCREEN {
let wanted = matches!(screen, Screen::Puzzle | Screen::Versus | Screen::Editor);
let got = ask(screen, Phase::Setup, VersusPhase::Running, |w| {
w.run_system_once(board_screens).expect("ran")
});
assert_eq!(got, wanted, "{screen:?}");
}
}
#[test]
fn only_two_screens_are_a_round_in_progress() {
for screen in EVERY_SCREEN {
let wanted = matches!(screen, Screen::Puzzle | Screen::Versus);
let got = ask(screen, Phase::Running, VersusPhase::Running, |w| {
w.run_system_once(play_screens).expect("ran")
});
assert_eq!(got, wanted, "{screen:?}");
}
}
#[test]
fn the_postcard_stands_behind_the_screens_that_are_not_a_board() {
for screen in EVERY_SCREEN {
let board = ask(screen, Phase::Setup, VersusPhase::Running, |w| {
w.run_system_once(board_screens).expect("ran")
});
let postcard = ask(screen, Phase::Setup, VersusPhase::Running, |w| {
w.run_system_once(postcard_screens).expect("ran")
});
assert!(
!(board && postcard),
"{screen:?} draws a board *and* the postcard behind it"
);
}
}
#[test]
fn time_passes_on_exactly_the_screens_and_phases_that_are_playing() {
let run = |screen, phase, vphase| {
ask(screen, phase, vphase, |w| {
w.run_system_once(sim_should_run).expect("ran")
})
};
assert!(run(Screen::Puzzle, Phase::Running, VersusPhase::Running));
for phase in [Phase::Setup, Phase::Won, Phase::Lost] {
assert!(
!run(Screen::Puzzle, phase, VersusPhase::Running),
"a puzzle ticked in {phase:?}"
);
}
assert!(run(Screen::Versus, Phase::Setup, VersusPhase::Running));
assert!(!run(Screen::Versus, Phase::Setup, VersusPhase::Over));
for screen in EVERY_SCREEN {
if matches!(screen, Screen::Puzzle | Screen::Versus | Screen::Editor) {
continue;
}
assert!(
!run(screen, Phase::Running, VersusPhase::Running),
"{screen:?} is not a round and must not tick"
);
}
}
#[test]
fn the_editors_board_ticks_only_while_it_is_being_tested() {
let mut world = world(Screen::Editor, Phase::Setup, VersusPhase::Running);
assert!(
!world.run_system_once(sim_should_run).expect("ran"),
"a board being painted must not walk its crabs"
);
world.resource_mut::<editor::EditorState>().mode =
editor::Mode::Testing(Box::new(crate::sim::Board::new(9, 7, 1)));
assert!(
world.run_system_once(sim_should_run).expect("ran"),
"and must, the moment it is played"
);
}
#[test]
fn a_versus_round_is_either_running_or_over_and_never_both() {
for screen in EVERY_SCREEN {
for vphase in [VersusPhase::Running, VersusPhase::Over] {
let running = ask(screen, Phase::Setup, vphase, |w| {
w.run_system_once(versus_running).expect("ran")
});
let over = ask(screen, Phase::Setup, vphase, |w| {
w.run_system_once(versus_over).expect("ran")
});
assert!(!(running && over), "{screen:?} in {vphase:?} is both");
let on_the_beach = screen == Screen::Versus;
assert_eq!(running, on_the_beach && vphase == VersusPhase::Running);
assert_eq!(over, on_the_beach && vphase == VersusPhase::Over);
}
}
}
#[test]
fn the_puzzle_gates_partition_its_phases() {
for screen in EVERY_SCREEN {
for phase in [Phase::Setup, Phase::Running, Phase::Won, Phase::Lost] {
let gates = [
ask(screen, phase, VersusPhase::Running, |w| {
w.run_system_once(puzzle_setup).expect("ran")
}),
ask(screen, phase, VersusPhase::Running, |w| {
w.run_system_once(puzzle_running).expect("ran")
}),
ask(screen, phase, VersusPhase::Running, |w| {
w.run_system_once(puzzle_done).expect("ran")
}),
];
let open = gates.iter().filter(|gate| **gate).count();
if screen == Screen::Puzzle {
assert_eq!(open, 1, "{phase:?} matched {open} gates, not exactly one");
} else {
assert_eq!(open, 0, "{screen:?} answered a puzzle gate in {phase:?}");
}
}
}
}
#[test]
fn a_round_being_watched_is_not_a_round_being_played() {
let mut world = world(Screen::Versus, Phase::Running, VersusPhase::Running);
assert!(
world.run_system_once(not_watching).expect("ran"),
"an empty playback slot is live play"
);
let level = crate::sim::campaign_levels()
.first()
.expect("a campaign level")
.clone();
world.insert_resource(Playback(Some((crate::sim::Replay::new(level), 0))));
assert!(
!world.run_system_once(not_watching).expect("ran"),
"a loaded recording is being watched, not played"
);
}
#[test]
fn any_half_typed_name_takes_the_keyboard() {
let mut world = world(Screen::Lobby, Phase::Setup, VersusPhase::Running);
assert!(
!world.run_system_once(text_entry_open).expect("ran"),
"nobody is typing to begin with"
);
world.resource_mut::<match_setup::MatchMenu>().naming = Some(0);
assert!(
world.run_system_once(text_entry_open).expect("ran"),
"a seat being named holds the keyboard"
);
world.resource_mut::<match_setup::MatchMenu>().naming = None;
world.resource_mut::<editor::EditorState>().mode = editor::Mode::Naming;
assert!(
world.run_system_once(text_entry_open).expect("ran"),
"a beach being named holds the keyboard"
);
world.resource_mut::<editor::EditorState>().mode = editor::Mode::Painting;
world
.resource_mut::<crate::app::spectators::SpectatorChat>()
.0 = Some(String::new());
assert!(
world.run_system_once(text_entry_open).expect("ran"),
"and a line a spectator is saying to the table holds it too"
);
world
.resource_mut::<crate::app::spectators::SpectatorChat>()
.0 = None;
assert!(
!world.run_system_once(text_entry_open).expect("ran"),
"and the keyboard comes back when the typing stops"
);
}
}