use super::EditorState;
use super::brush::SPAWNER_PERIOD;
use crate::sim::{Board, Goal, LevelKind, Spawner, TICKS_PER_SECOND, TileKind};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Setting {
#[default]
Signposts,
Players,
Goal,
Time,
Gulls,
Holes,
Wrap,
Mirror,
}
pub(super) const MAX_POSTS: u8 = 9;
pub(super) const GULL_PERIODS: [u32; 4] = [0, 480, 240, 120];
pub(super) const HOLE_PERIODS: [u32; 5] = [30, SPAWNER_PERIOD, 90, 120, 180];
pub(super) const TIME_STOPS: [u32; 6] = [30, 45, 60, 90, 120, 180];
pub(super) const GOALS: [Goal; 13] = [
Goal::AllCrabs,
Goal::Survive,
Goal::Golden,
Goal::Bank(1),
Goal::Bank(2),
Goal::Bank(3),
Goal::Bank(4),
Goal::Bank(5),
Goal::Bank(6),
Goal::Bank(8),
Goal::Bank(10),
Goal::Bank(15),
Goal::Bank(20),
];
impl Setting {
pub(super) fn rows(kind: LevelKind) -> &'static [Setting] {
match kind {
LevelKind::Puzzle => &[
Setting::Signposts,
Setting::Players,
Setting::Goal,
Setting::Time,
Setting::Gulls,
Setting::Holes,
Setting::Wrap,
Setting::Mirror,
],
LevelKind::Arena => &[Setting::Holes, Setting::Wrap, Setting::Mirror],
}
}
pub(super) fn chosen(state: &EditorState) -> Setting {
let rows = Setting::rows(state.kind);
match rows.contains(&state.setting) {
true => state.setting,
false => rows[0],
}
}
pub(super) fn neighbour(state: &EditorState, forward: bool) -> Setting {
let rows = Setting::rows(state.kind);
let at = rows
.iter()
.position(|row| *row == Setting::chosen(state))
.unwrap_or(0);
let len = rows.len();
rows[(at + if forward { 1 } else { len - 1 }) % len]
}
}
pub(super) fn step_stop_by<T: PartialEq + Copy, R: Ord>(
stops: &[T],
now: T,
forward: bool,
rank: impl Fn(T) -> R,
) -> T {
let len = stops.len();
match (stops.iter().position(|stop| *stop == now), forward) {
(Some(at), true) => stops[(at + 1) % len],
(Some(at), false) => stops[(at + len - 1) % len],
(None, true) => stops
.iter()
.copied()
.find(|&stop| rank(stop) > rank(now))
.unwrap_or(stops[0]),
(None, false) => stops
.iter()
.rev()
.copied()
.find(|&stop| rank(stop) < rank(now))
.unwrap_or(stops[len - 1]),
}
}
pub(super) fn step_stop<T: PartialEq + Copy + Ord>(stops: &[T], now: T, forward: bool) -> T {
step_stop_by(stops, now, forward, |stop| stop)
}
fn step_players(now: Option<u8>, forward: bool) -> Option<u8> {
match (now, forward) {
(None, true) => Some(1),
(None, false) => Some(MAX_POSTS),
(Some(n), true) if n >= MAX_POSTS => None,
(Some(n), true) => Some(n + 1),
(Some(n), false) if n <= 1 => None,
(Some(n), false) => Some(n - 1),
}
}
fn goal_rank(goal: Goal) -> (u8, u32) {
match goal {
Goal::AllCrabs => (0, 0),
Goal::Survive => (1, 0),
Goal::Golden => (2, 0),
Goal::Bank(n) => (3, n),
}
}
fn gull_rank(period: u32) -> (bool, std::cmp::Reverse<u32>) {
(period != 0, std::cmp::Reverse(period))
}
pub(super) fn hole_period(state: &EditorState) -> u32 {
state.hole_period.unwrap_or(SPAWNER_PERIOD)
}
pub(super) fn holes(board: &Board) -> Vec<(u8, u8, Spawner)> {
board
.tiles()
.filter_map(|(x, y, kind)| {
if let TileKind::Spawner(s) = kind {
Some((x, y, s))
} else {
None
}
})
.collect()
}
pub(super) fn first_hole_period(board: &Board) -> Option<u32> {
holes(board).first().map(|(_, _, s)| s.period)
}
pub(super) fn time_secs(board: &Board) -> u32 {
let ticks = crate::sim::Level::deadline(board);
u32::try_from(ticks / u64::from(TICKS_PER_SECOND)).unwrap_or(u32::MAX)
}
pub(super) fn step(
state: &mut EditorState,
board: &mut Board,
setting: Setting,
forward: bool,
) -> bool {
match setting {
Setting::Signposts => {
let was = state.posts;
if forward && state.posts < MAX_POSTS {
state.posts += 1;
} else if !forward {
state.posts = state.posts.saturating_sub(1);
}
state.posts != was
}
Setting::Players => {
state.second_hand = step_players(state.second_hand, forward);
true
}
Setting::Goal => {
state.goal = step_stop_by(&GOALS, state.goal, forward, goal_rank);
true
}
Setting::Time => {
let secs = step_stop(&TIME_STOPS, time_secs(board), forward);
let limit = crate::sim::PUZZLE_TICK_LIMIT / u64::from(TICKS_PER_SECOND);
board.rules_mut().round_length =
(u64::from(secs) != limit).then_some(secs * TICKS_PER_SECOND);
true
}
Setting::Gulls => {
let rules = board.rules_mut();
rules.gull_period = step_stop_by(&GULL_PERIODS, rules.gull_period, forward, gull_rank);
true
}
Setting::Holes => {
let period = step_stop(&HOLE_PERIODS, hole_period(state), forward);
state.hole_period = Some(period);
for (x, y, s) in holes(board) {
board.set_tile(x, y, TileKind::Spawner(Spawner { period, ..s }));
}
true
}
Setting::Wrap => {
board.set_wrap(!board.wrap());
true
}
Setting::Mirror => {
state.symmetry = step_stop_by(
&super::Symmetry::ALL,
state.symmetry,
forward,
super::Symmetry::index,
);
state.dirty = true;
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dial_wraps_both_ways_and_catches_a_value_off_it() {
let gulls = |now, forward| step_stop_by(&GULL_PERIODS, now, forward, gull_rank);
assert_eq!(gulls(0, true), 480);
assert_eq!(gulls(120, true), 0);
assert_eq!(gulls(0, false), 120);
assert_eq!(gulls(333, true), 240);
assert_eq!(gulls(333, false), 480);
assert_eq!(gulls(60, true), 0, "past the end, round");
assert_eq!(step_stop(&TIME_STOPS, 75, true), 90);
assert_eq!(step_stop(&TIME_STOPS, 75, false), 60);
let goal = |now, forward| step_stop_by(&GOALS, now, forward, goal_rank);
assert_eq!(goal(Goal::Bank(7), true), Goal::Bank(8));
assert_eq!(goal(Goal::Bank(7), false), Goal::Bank(6));
assert_eq!(goal(Goal::Bank(30), true), Goal::AllCrabs);
assert_eq!(super::super::resize::next_size((14, 10), false), (12, 9));
}
#[test]
fn the_time_dial_at_the_limit_is_no_round_line() {
let mut state = EditorState::default();
let mut board = Board::new(6, 5, 1);
assert_eq!(time_secs(&board), 60);
step(&mut state, &mut board, Setting::Time, true);
assert_eq!(time_secs(&board), 90);
assert_eq!(board.rules().round_length, Some(90 * TICKS_PER_SECOND));
step(&mut state, &mut board, Setting::Time, false);
assert_eq!(board.rules().round_length, None, "back to the limit");
step(&mut state, &mut board, Setting::Time, false);
assert_eq!(time_secs(&board), 45);
}
#[test]
fn the_players_dial_makes_a_coop_stage() {
let mut state = EditorState {
posts: 2,
..EditorState::default()
};
let mut board = Board::new(6, 5, 1);
board.set_tile(5, 2, TileKind::Castle(0));
assert!(step(&mut state, &mut board, Setting::Players, true));
assert_eq!(state.second_hand, Some(1));
let level = super::super::level_here(&state, &board, "Duo");
assert_eq!((level.hands(), level.posts), (Some([2, 1]), 3));
step(&mut state, &mut board, Setting::Players, false);
assert_eq!(state.second_hand, None);
let level = super::super::level_here(&state, &board, "Solo");
assert_eq!((level.hands(), level.posts), (None, 2));
assert_eq!(step_players(None, false), Some(MAX_POSTS));
assert_eq!(step_players(Some(MAX_POSTS), true), None);
}
#[test]
fn the_signposts_stop_at_nought_and_nine() {
let mut state = EditorState::default();
let mut board = Board::new(6, 5, 1);
assert!(!step(&mut state, &mut board, Setting::Signposts, false));
state.posts = MAX_POSTS;
assert!(!step(&mut state, &mut board, Setting::Signposts, true));
assert_eq!(state.posts, MAX_POSTS);
}
#[test]
fn the_holes_dial_turns_every_hole_and_rows_follow_the_kind() {
let mut state = EditorState::default();
let mut board = Board::new(6, 5, 1);
for x in [1, 3] {
let hole = Spawner {
dir: crate::sim::Direction::Right,
period: SPAWNER_PERIOD,
};
board.set_tile(x, 1, TileKind::Spawner(hole));
}
step(&mut state, &mut board, Setting::Holes, true);
let periods: Vec<u32> = holes(&board).iter().map(|(_, _, s)| s.period).collect();
assert_eq!(periods, [90, 90]);
state.setting = Setting::Goal;
state.kind = LevelKind::Arena;
assert_eq!(Setting::chosen(&state), Setting::Holes);
assert_eq!(Setting::neighbour(&state, true), Setting::Wrap);
assert_eq!(Setting::neighbour(&state, false), Setting::Mirror);
}
}