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.is_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.test_running(),
}
}
pub(super) fn puzzle_undecided(
screen: Res<State<Screen>>,
campaign: Option<Res<super::Campaign>>,
sim: Option<Res<super::Sim>>,
editor: Option<Res<editor::EditorState>>,
) -> bool {
match (screen.get(), campaign, sim, editor) {
(Screen::Puzzle, Some(campaign), Some(sim), _) => {
campaign.current().outcome(&sim.0) == crate::sim::PuzzleOutcome::Running
}
(Screen::Editor, _, Some(sim), Some(editor)) => {
editor.test().is_none_or(|test| test.undecided(&sim.0))
}
_ => true,
}
}
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 board_cursor_free(
screen: Res<State<Screen>>,
editor: Res<editor::EditorState>,
pause: Res<crate::app::pause::PauseMenu>,
menu: Res<match_setup::MatchMenu>,
) -> bool {
*screen.get() != Screen::Menu
&& !editor.has_keys()
&& !pause.is_open()
&& !crate::app::next_map::page_up(&menu)
}
pub(super) fn versus_before_the_round(
screen: Res<State<Screen>>,
phase: Res<State<VersusPhase>>,
) -> bool {
match screen.get() {
Screen::Interlude => true,
Screen::Versus => *phase.get() == VersusPhase::Countdown,
Screen::Menu
| Screen::Puzzle
| Screen::Editor
| Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Language
| Screen::NewVersion => false,
}
}
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 {
in_puzzle_setup(screen.get(), phase.get())
}
pub(in crate::app) fn in_puzzle_setup(screen: &Screen, phase: &Phase) -> bool {
*screen == Screen::Puzzle && *phase == 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 {
puzzle_settled(screen.get(), phase.get())
}
pub(in crate::app) fn puzzle_settled(screen: &Screen, phase: &Phase) -> bool {
*screen == Screen::Puzzle && matches!(phase, 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
}
#[test]
fn a_decided_puzzle_takes_no_more_ticks() {
use crate::sim::{Level, PUZZLE_TICK_LIMIT};
let level = Level::parse(
"name: T\nposts: 0\ngoal: survive\nmap:\n+-+-+\n|. .|\n+ + +\n|. .|\n+-+-+\n",
)
.expect("level");
let mut board = level.board();
for _ in 1..PUZZLE_TICK_LIMIT {
board.tick_idle();
}
let mut w = world(Screen::Puzzle, Phase::Running, VersusPhase::Running);
w.insert_resource(crate::app::Campaign {
builtins: 1,
..crate::app::Campaign::tide_pool_of(vec![level.clone()])
});
w.insert_resource(crate::app::Sim(board.clone()));
assert!(w.run_system_once(puzzle_undecided).expect("ran"));
board.tick_idle();
w.insert_resource(crate::app::Sim(board));
assert!(
!w.run_system_once(puzzle_undecided).expect("ran"),
"decided"
);
let mut w = world(Screen::Versus, Phase::Running, VersusPhase::Running);
assert!(w.run_system_once(puzzle_undecided).expect("ran"));
}
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(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 { was: String::new() };
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"
);
}
}