use super::EditorState;
use crate::app::i18n::fill;
use crate::app::settings::GameSettings;
use crate::sim::{Board, Level, LevelKind, SolveOutcome, TileKind, solve};
use bevy::prelude::*;
use std::sync::{Arc, Mutex};
pub(super) type SolverSlot = Arc<Mutex<Option<SolveOutcome>>>;
pub(super) struct Validation {
pub(super) slot: SolverSlot,
pub(super) posts: u8,
}
pub(super) fn arena_report(board: &Board, tr: &crate::app::i18n::Tr) -> String {
let seats = board.castle_seats();
let holes = board
.tiles()
.filter(|(_, _, kind)| matches!(kind, TileKind::Spawner(_)))
.count();
if seats < 2 {
return fill(tr.ed_arena_needs_seats, &[("n", &seats.to_string())]);
}
if holes == 0 && board.crabs().is_empty() {
return tr.ed_arena_no_crabs.to_string();
}
fill(
tr.ed_arena_ok,
&[("n", &seats.to_string()), ("h", &holes.to_string())],
)
}
pub(super) fn orphan_warning(
state: &EditorState,
level: &Level,
tr: &crate::app::i18n::Tr,
) -> Option<String> {
match state.kind {
LevelKind::Arena if level.seats() < 2 => Some(fill(
tr.ed_arena_needs_seats,
&[("n", &level.seats().to_string())],
)),
LevelKind::Puzzle if level.crab_count() == 0 => Some(tr.ed_puzzle_no_crabs.to_string()),
LevelKind::Arena | LevelKind::Puzzle => None,
}
}
pub(super) fn start_validation(state: &mut EditorState, level: Level) {
let slot: SolverSlot = Arc::new(Mutex::new(None));
let thread_slot = Arc::clone(&slot);
let posts = level.posts;
std::thread::spawn(move || {
*thread_slot.lock().unwrap() = Some(solve(&level));
});
state.solver = Some(Validation { slot, posts });
}
pub fn poll_solver(settings: Res<GameSettings>, mut state: ResMut<EditorState>) {
let tr = settings.tr();
let Some(Validation { slot, posts }) = &state.solver else {
return;
};
let posts = *posts;
let result = slot.lock().unwrap().take();
match result {
None => return, Some(SolveOutcome::Found(solution)) => {
let described: Vec<String> = solution
.iter()
.map(|(x, y, dir)| format!("({x},{y} {dir:?})"))
.collect();
state.feedback = if solution.is_empty() {
tr.ed_solvable_free.into()
} else {
fill(tr.ed_solvable, &[("placements", &described.join(" "))])
};
}
Some(SolveOutcome::Unsolvable) => {
state.feedback = fill(tr.ed_not_solvable, &[("n", &posts.to_string())]);
}
Some(SolveOutcome::GaveUp) => {
state.feedback = tr.ed_solver_gave_up.into();
}
}
state.solver = None;
}
#[cfg(test)]
mod tests {
use super::super::brush::SPAWNER_PERIOD;
use super::super::level_here;
use super::*;
use crate::sim::{CrabKind, Direction, Handedness, Spawner};
fn sand() -> Board {
Board::new(6, 5, 3)
}
fn verdict(outcome: Option<SolveOutcome>) -> (String, bool) {
let mut app = App::new();
app.insert_resource(GameSettings::default());
app.insert_resource(EditorState {
posts: 5,
solver: Some(Validation {
slot: Arc::new(Mutex::new(outcome)),
posts: 2,
}),
..EditorState::default()
});
app.add_systems(Update, poll_solver);
app.update();
let state = app.world().resource::<EditorState>();
(state.feedback.clone(), state.solver.is_some())
}
#[test]
fn a_solution_is_spelled_out_or_called_free() {
use crate::app::i18n::EN;
let (said, pending) = verdict(Some(SolveOutcome::Found(vec![(1, 2, Direction::Up)])));
assert!(!pending, "the answer is taken");
assert_eq!(said, EN.ed_solvable.replace("{placements}", "(1,2 Up)"));
let (said, _) = verdict(Some(SolveOutcome::Found(Vec::new())));
assert_eq!(said, EN.ed_solvable_free);
}
#[test]
fn no_solution_and_no_answer_are_told_apart() {
use crate::app::i18n::EN;
let (said, _) = verdict(Some(SolveOutcome::Unsolvable));
assert_eq!(said, EN.ed_not_solvable.replace("{n}", "2"));
let (said, _) = verdict(Some(SolveOutcome::GaveUp));
assert_eq!(said, EN.ed_solver_gave_up);
let (said, pending) = verdict(None);
assert!(pending, "still searching");
assert!(said.is_empty(), "and nothing said yet: {said}");
}
#[test]
fn a_save_that_lands_nowhere_says_so() {
use crate::app::i18n::EN;
let mut board = sand();
let state = |kind| EditorState {
posts: 3,
kind,
..EditorState::default()
};
board.set_tile(0, 0, TileKind::Castle(0));
let arena = state(LevelKind::Arena);
let level = level_here(&arena, &board, "Half a Beach");
let complaint = orphan_warning(&arena, &level, &EN).expect("nowhere to go");
assert!(complaint.contains('1'), "{complaint}");
let puzzle = state(LevelKind::Puzzle);
let level = level_here(&puzzle, &board, "Empty Stage");
assert_eq!(
orphan_warning(&puzzle, &level, &EN).as_deref(),
Some(EN.ed_puzzle_no_crabs)
);
board.spawn_crab(2, 2, Direction::Right, Handedness::Left, CrabKind::Common);
let level = level_here(&puzzle, &board, "A Stage");
assert_eq!(orphan_warning(&puzzle, &level, &EN), None);
board.set_tile(5, 4, TileKind::Castle(1));
let level = level_here(&arena, &board, "A Beach");
assert_eq!(orphan_warning(&arena, &level, &EN), None);
}
#[test]
fn checking_a_beach_reads_its_castles() {
use crate::app::i18n::EN;
let mut board = sand();
assert!(arena_report(&board, &EN).contains('0'), "bare sand seats 0");
board.set_tile(0, 0, TileKind::Castle(0));
let one = arena_report(&board, &EN);
assert!(one.contains('1'), "one castle is not a match: {one}");
board.set_tile(5, 4, TileKind::Castle(1));
assert_eq!(
arena_report(&board, &EN),
EN.ed_arena_no_crabs,
"castles but nothing to fight over"
);
board.set_tile(
2,
2,
TileKind::Spawner(Spawner {
dir: Direction::Right,
period: SPAWNER_PERIOD,
}),
);
let ok = arena_report(&board, &EN);
assert!(ok.contains('2'), "two seats: {ok}");
assert!(!ok.contains("wants"), "no complaint left: {ok}");
}
#[test]
fn a_started_validation_leaves_a_verdict_in_the_slot() {
fn verdict(level: Level) -> SolveOutcome {
let mut state = EditorState::default();
start_validation(&mut state, level);
let slot = state.solver.expect("a validation is running").slot;
loop {
if let Some(outcome) = slot.lock().unwrap().take() {
return outcome;
}
std::thread::yield_now();
}
}
let mut easy = sand();
easy.set_tile(4, 2, TileKind::Castle(0));
easy.spawn_crab(1, 2, Direction::Right, Handedness::Left, CrabKind::Common);
assert_eq!(
verdict(Level::from_board("Custom", 1, easy)),
SolveOutcome::Found(Vec::new())
);
let mut sealed = sand();
sealed.set_tile(4, 2, TileKind::Castle(0));
sealed.spawn_crab(1, 2, Direction::Right, Handedness::Left, CrabKind::Common);
for dir in Direction::ALL {
sealed.set_wall(4, 2, dir, true);
}
assert_eq!(
verdict(Level::from_board("Custom", 1, sealed)),
SolveOutcome::Unsolvable
);
}
}