use super::naming::tidy_name;
use super::validate::{arena_report, start_check};
use super::{BoardSwap, EditorState, Offer, ask_first, level_here, replace_board, unsaved_work};
use crate::app::Sim;
use crate::app::i18n::fill;
use crate::sim::{Board, Level, LevelKind};
use bevy::prelude::*;
pub fn level_from_code(
pasted: Option<(crate::share::Kind, Vec<u8>)>,
tr: &crate::app::i18n::Tr,
) -> Result<Level, String> {
let text =
crate::app::codes::payload_text(pasted, tr, crate::share::Kind::Level, tr.code_level_bad)?;
Level::parse(&text).map_err(|e| fill(tr.code_level_bad, &[("e", &e)]))
}
fn is_from_elsewhere(pasted: &Level, bench: &Level, shelf: &[Level]) -> bool {
let text = pasted.to_text();
text != bench.to_text() && shelf.iter().all(|own| own.to_text() != text)
}
pub fn taken_from_elsewhere(pasted: &Level) -> bool {
let shelf = crate::app::campaign::load_custom_levels();
shelf.iter().all(|own| own.to_text() != pasted.to_text())
}
pub(super) fn take_level(
state: &mut EditorState,
level: &Level,
board: &Board,
tr: &crate::app::i18n::Tr,
) {
state.posts = level.hands().map_or(level.posts, |[p1, _]| p1);
state.second_hand = level.hands().map(|[_, p2]| p2);
state.kind = level.kind;
state.goal = level.goal;
state.name = tidy_name(&level.name, tr);
state.tables.clone_from(&level.tables);
state.roll = level.roll;
state.solution.clone_from(&level.solution);
state.hole_period = super::setting::first_hole_period(&level.board());
state.mine.clear();
state.opened = None;
state.filed = None;
state.symmetry = super::Symmetry::Off;
state.checked = None;
state.begin_history(board);
}
pub(super) fn copy_level(
state: &mut EditorState,
board: &Board,
clipboard: &mut Clipboard,
tr: &crate::app::i18n::Tr,
shared: &mut MessageWriter<crate::app::CodeShared>,
) {
let level = level_here(state, board, &state.name);
let (feedback, took) = crate::app::codes::copy_counted(
clipboard,
tr,
crate::share::Kind::Level,
level.to_text().as_bytes(),
tr.code_copied,
);
state.feedback = feedback;
if took {
shared.write(crate::app::CodeShared);
}
}
pub(super) fn paste_level(
state: &mut EditorState,
sim: &mut Sim,
pasted: Option<(crate::share::Kind, Vec<u8>)>,
(tr, device): (&crate::app::i18n::Tr, crate::app::gamepad::LastDevice),
taken: &mut MessageWriter<crate::app::CodeTaken>,
swap: &mut BoardSwap,
) -> bool {
match level_from_code(pasted, tr) {
Ok(level) => {
let at_stake = unsaved_work(state, &sim.0);
if !ask_first(
state,
Offer::Open,
at_stake,
Offer::Open.warning(tr, device),
) {
return false;
}
let bench = level_here(state, &sim.0, &state.name);
let shelf = crate::app::campaign::load_custom_levels();
if is_from_elsewhere(&level, &bench, &shelf) {
taken.write(crate::app::CodeTaken);
}
replace_board(level.board(), sim, state, swap);
take_level(state, &level, &sim.0, tr);
if level.kind == LevelKind::Arena {
state.feedback = arena_report(&sim.0, tr);
} else {
start_check(state, &sim.0);
state.feedback = tr.code_level_checking.into();
}
true
}
Err(complaint) => {
state.offer = None;
state.feedback = complaint;
false
}
}
}
#[cfg(test)]
mod tests {
use super::super::naming::NAME_MAX;
use super::*;
use crate::sim::{Direction, TileKind};
fn sand() -> Board {
Board::new(6, 5, 3)
}
#[test]
fn a_pasted_name_is_tidied_like_a_typed_one() {
let tr = &crate::app::i18n::EN;
let named = |name: &str| {
let mut state = EditorState::default();
let mut level = Level::from_board("x", 3, sand());
level.name = name.into();
take_level(&mut state, &level, &level.board(), tr);
state.name
};
assert_eq!(named(" Gull Alley "), "Gull Alley");
assert_eq!(named(""), tr.ed_default_name);
assert_eq!(named(" "), tr.ed_default_name);
let long = named(&"\u{e9}".repeat(NAME_MAX + 12));
assert_eq!(long.chars().count(), NAME_MAX, "cut on characters");
let spaced = format!("{} tail", "a".repeat(NAME_MAX - 1));
assert_eq!(named(&spaced), "a".repeat(NAME_MAX - 1));
}
#[test]
fn only_a_level_from_elsewhere_counts_as_taken() {
let mut board = sand();
board.set_tile(0, 0, TileKind::Castle(0));
let mine = Level::from_board("Mine", 3, board.clone());
board.set_tile(2, 2, TileKind::Rock);
let bench = Level::from_board("Bench", 3, board.clone());
let shelf = [mine.clone()];
assert!(!is_from_elsewhere(&bench, &bench, &shelf), "F3 then F4");
assert!(!is_from_elsewhere(&mine, &bench, &shelf), "my own, back");
board.set_tile(3, 3, TileKind::Rock);
let theirs = Level::from_board("Mine", 3, board);
assert!(
is_from_elsewhere(&theirs, &bench, &shelf),
"their edit of mine"
);
}
#[test]
fn a_pasted_level_brings_its_name() {
let mut state = EditorState {
posts: 3,
name: "My Beach".into(),
..EditorState::default()
};
state
.mine
.insert(super::super::shelf::save_path("My Beach"));
state
.mine
.insert(super::super::shelf::save_path("Gull Alley"));
let pasted = Level::from_board("Gull Alley", 5, sand()).with_kind(LevelKind::Arena);
take_level(&mut state, &pasted, &pasted.board(), &crate::app::i18n::EN);
assert_eq!(state.name, "Gull Alley");
assert_eq!((state.posts, state.kind), (5, LevelKind::Arena));
assert!(state.mine.is_empty(), "a save over any of them is asked");
}
#[test]
fn a_pasted_level_keeps_its_tables_roll_and_solution() {
let text = "name: Four Six\nposts: 3\nkind: arena\nwrap: on\nseats: 4 6\nroll: 3\n\
spawner: 1,1 R 60\nmap:\n\
+ + + + + + +\n 0 1 2 3 4 5 \n+ + + + + + +\n . . . . . . \n+ + + + + + +\n";
let pasted = Level::parse(text).expect("a beach");
assert_eq!((pasted.tables.as_slice(), pasted.roll), (&[4, 6][..], 3));
let mut state = EditorState::default();
take_level(&mut state, &pasted, &pasted.board(), &crate::app::i18n::EN);
let here = level_here(&state, &pasted.board(), &state.name);
assert_eq!(here.tables, [4, 6]);
assert_eq!(here.roll, 3);
let mut board = pasted.board();
board.set_tile(5, 0, TileKind::Empty);
board.set_wrap(false);
let here = level_here(&state, &board, &state.name);
assert_eq!((here.tables.as_slice(), here.roll), (&[4][..], 0));
assert!(Level::parse(&here.to_text()).is_ok(), "{}", here.to_text());
let mut stage = Level::from_board("Solved", 2, sand());
stage.solution = vec![(1, 1, Direction::Up, 0)];
take_level(&mut state, &stage, &stage.board(), &crate::app::i18n::EN);
let here = level_here(&state, &stage.board(), &state.name);
assert_eq!(here.solution, [(1, 1, Direction::Up, 0)]);
}
#[test]
fn a_pasted_goal_survives_the_bench() {
let mut board = sand();
board.set_tile(4, 3, TileKind::Castle(0));
let mut pasted = Level::from_board("Bank Two", 2, board.clone());
pasted.goal = crate::sim::Goal::Bank(2);
let pasted = Level::parse(&pasted.to_text()).expect("reads back");
assert_eq!(pasted.goal, crate::sim::Goal::Bank(2));
let mut state = EditorState::default();
take_level(&mut state, &pasted, &pasted.board(), &crate::app::i18n::EN);
let here = level_here(&state, &pasted.board(), &state.name);
assert_eq!(here.goal, crate::sim::Goal::Bank(2));
assert!(
here.to_text().contains("goal: bank 2"),
"{}",
here.to_text()
);
}
#[test]
fn a_level_travels_as_a_code_and_comes_back() {
let mut board = sand();
board.set_tile(2, 2, TileKind::Rock);
board.set_tile(4, 3, TileKind::Castle(0));
board.set_wall(1, 1, Direction::Right, true);
board.set_wrap(true);
let level = Level::from_board("Custom", 5, board.clone());
let code = crate::share::encode(crate::share::Kind::Level, level.to_text().as_bytes());
let back = level_from_code(crate::share::decode(&code), &crate::app::i18n::EN)
.expect("our own level reads back");
assert_eq!(back.posts, 5, "the signpost budget travels with it");
let rebuilt = back.board();
assert_eq!(rebuilt.tile_at(2, 2), TileKind::Rock);
assert_eq!(rebuilt.tile_at(4, 3), TileKind::Castle(0));
assert!(rebuilt.wall_at(1, 1, Direction::Right));
assert!(rebuilt.wrap(), "an open-ocean level stays open");
}
#[test]
fn what_is_not_a_level_says_which_way_it_is_not() {
let tr = &crate::app::i18n::EN;
assert_eq!(
level_from_code(None, tr).err(),
Some(tr.code_none_pasted.to_string())
);
let round = crate::share::encode(crate::share::Kind::Round, b"replay-v1\n");
let complaint = level_from_code(crate::share::decode(&round), tr).expect_err("not a level");
assert!(
complaint.contains("a round") && complaint.contains("a level"),
"{complaint}"
);
let junk = crate::share::encode(crate::share::Kind::Level, b"not a level at all");
assert!(level_from_code(crate::share::decode(&junk), tr).is_err());
}
}