use crate::sim::MAX_PLAYERS;
use crate::sim::{Board, CrabKind, Direction, Handedness, Spawner, TileKind};
use bevy::prelude::*;
pub(super) const SPAWNER_PERIOD: u32 = 60;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Brush {
#[default]
Sand,
Rock,
Castle,
Hole,
Log,
Weed,
Pool,
Crab,
Gull,
}
impl Brush {
pub const ALL: [Brush; 9] = [
Brush::Sand,
Brush::Rock,
Brush::Castle,
Brush::Hole,
Brush::Log,
Brush::Weed,
Brush::Pool,
Brush::Crab,
Brush::Gull,
];
pub fn key(self) -> KeyCode {
match self {
Brush::Sand => KeyCode::KeyX,
Brush::Rock => KeyCode::KeyR,
Brush::Castle => KeyCode::KeyC,
Brush::Hole => KeyCode::KeyH,
Brush::Log => KeyCode::KeyL,
Brush::Weed => KeyCode::KeyE,
Brush::Pool => KeyCode::KeyP,
Brush::Crab => KeyCode::KeyB,
Brush::Gull => KeyCode::KeyG,
}
}
pub fn letter(self) -> &'static str {
match self {
Brush::Sand => "X",
Brush::Rock => "R",
Brush::Castle => "C",
Brush::Hole => "H",
Brush::Log => "L",
Brush::Weed => "E",
Brush::Pool => "P",
Brush::Crab => "B",
Brush::Gull => "G",
}
}
pub fn label(self, tr: &crate::app::i18n::Tr) -> &'static str {
tr.ed_brushes[Brush::ALL.iter().position(|b| *b == self).unwrap_or(0)]
}
pub fn icon(self, art: &crate::app::art::Art) -> Handle<Image> {
match self {
Brush::Sand => art.sand_a.clone(),
Brush::Rock => art.rock.clone(),
Brush::Castle => art.castle.clone(),
Brush::Hole => art.hole.clone(),
Brush::Log => art.log.clone(),
Brush::Weed => art.kelp.clone(),
Brush::Pool => art.pool.clone(),
Brush::Crab => art.crab.clone(),
Brush::Gull => art.gull.clone(),
}
}
}
pub(super) fn cycle_castle(board: &mut Board, x: u8, y: u8) {
let kind = match board.tile_at(x, y) {
TileKind::Castle(owner) if usize::from(owner) + 1 < MAX_PLAYERS => {
TileKind::Castle(owner + 1)
}
TileKind::Castle(_) => TileKind::Empty,
TileKind::Empty
| TileKind::Rock
| TileKind::Spawner(_)
| TileKind::Turnstile { .. }
| TileKind::Kelp
| TileKind::Pool => {
board.remove_crabs_at(x, y);
board.remove_gulls_at(x, y);
TileKind::Castle(0)
}
};
board.set_tile(x, y, kind);
}
pub(super) fn paint(board: &mut Board, x: u8, y: u8, brush: Brush, hole_period: u32) {
match brush {
Brush::Castle => return cycle_castle(board, x, y),
Brush::Hole => {
let kind = match board.tile_at(x, y) {
TileKind::Spawner(s) => match next_dir(s.dir) {
Some(dir) => TileKind::Spawner(Spawner { dir, ..s }),
None => TileKind::Empty,
},
TileKind::Empty
| TileKind::Rock
| TileKind::Castle(_)
| TileKind::Turnstile { .. }
| TileKind::Kelp
| TileKind::Pool => {
board.remove_crabs_at(x, y);
board.remove_gulls_at(x, y);
TileKind::Spawner(Spawner {
dir: Direction::Right,
period: hole_period,
})
}
};
return board.set_tile(x, y, kind);
}
Brush::Crab => return cycle_crab(board, x, y),
Brush::Gull => {
let tile = board.index_of(x, y);
if board.gulls().iter().any(|g| g.tile == tile) {
board.remove_gulls_at(x, y);
} else if board.gull_may_stand(x, y) {
board.spawn_gull(x, y, Direction::Right);
}
return;
}
Brush::Sand | Brush::Rock | Brush::Log | Brush::Weed | Brush::Pool => {}
}
board.remove_crabs_at(x, y);
board.remove_gulls_at(x, y);
board.set_tile(
x,
y,
match brush {
Brush::Rock => TileKind::Rock,
Brush::Log => TileKind::Turnstile { next_right: true },
Brush::Weed => TileKind::Kelp,
Brush::Pool => TileKind::Pool,
Brush::Sand | Brush::Castle | Brush::Hole | Brush::Crab | Brush::Gull => {
TileKind::Empty
}
},
);
}
fn next_dir(dir: Direction) -> Option<Direction> {
match dir {
Direction::Right => Some(Direction::Down),
Direction::Down => Some(Direction::Left),
Direction::Left => Some(Direction::Up),
Direction::Up => None,
}
}
fn cycle_crab(board: &mut Board, x: u8, y: u8) {
const CYCLE: [(CrabKind, Handedness); 12] = [
(CrabKind::Common, Handedness::Left),
(CrabKind::Common, Handedness::Right),
(CrabKind::Juvenile, Handedness::Left),
(CrabKind::Juvenile, Handedness::Right),
(CrabKind::Giant, Handedness::Left),
(CrabKind::Giant, Handedness::Right),
(CrabKind::Molting, Handedness::Left),
(CrabKind::Molting, Handedness::Right),
(CrabKind::Golden, Handedness::Left),
(CrabKind::Golden, Handedness::Right),
(CrabKind::Sparkling, Handedness::Left),
(CrabKind::Sparkling, Handedness::Right),
];
if board.tile_at(x, y) != TileKind::Empty {
return; }
let tile = board.index_of(x, y);
let current = board
.crabs()
.iter()
.find(|c| c.tile == tile)
.map(|c| (c.kind, c.handed));
board.remove_crabs_at(x, y);
let next = match current {
None => Some(CYCLE[0]),
Some(cur) => CYCLE
.iter()
.position(|&e| e == cur)
.and_then(|i| CYCLE.get(i + 1))
.copied(),
};
if let Some((kind, handed)) = next {
board.spawn_crab(x, y, Direction::Right, handed, kind);
}
}
pub(super) fn stroke_onto(board: &mut Board, from: (u8, u8), to: (u8, u8), brush: Brush) {
let ((fx, fy), (x, y)) = (from, to);
let inside = |(x, y): (u8, u8)| x < board.width() && y < board.height();
if from == to || !inside(from) || !inside(to) {
return;
}
let sweep = |board: &mut Board| {
board.remove_crabs_at(x, y);
board.remove_gulls_at(x, y);
};
match brush {
Brush::Castle | Brush::Hole => {
let start = board.tile_at(fx, fy);
let same_layer = |kind: TileKind| match brush == Brush::Castle {
true => matches!(kind, TileKind::Castle(_)),
false => matches!(kind, TileKind::Spawner(_)),
};
if same_layer(start) {
sweep(board);
board.set_tile(x, y, start);
} else if same_layer(board.tile_at(x, y)) {
board.set_tile(x, y, TileKind::Empty);
}
}
Brush::Crab => {
let start = board.index_of(fx, fy);
let crab = board.crabs().iter().find(|c| c.tile == start).copied();
match crab {
Some(crab) if board.tile_at(x, y) == TileKind::Empty => {
board.remove_crabs_at(x, y);
board.spawn_crab(x, y, crab.dir, crab.handed, crab.kind);
}
Some(_) => {}
None => board.remove_crabs_at(x, y),
}
}
Brush::Gull => {
let start = board.index_of(fx, fy);
let here = board.index_of(x, y);
let gull = board
.gulls()
.iter()
.find(|g| g.tile == start)
.map(|g| g.dir);
let standing = board.gulls().iter().any(|g| g.tile == here);
match gull {
Some(dir) if !standing && board.gull_may_stand(x, y) => board.spawn_gull(x, y, dir),
Some(_) => {}
None => board.remove_gulls_at(x, y),
}
}
Brush::Sand | Brush::Rock | Brush::Log | Brush::Weed | Brush::Pool => {
paint(board, x, y, brush, SPAWNER_PERIOD);
}
}
}
pub(super) fn facing(board: &Board, x: u8, y: u8) -> Option<Direction> {
let tile = board.index_of(x, y);
if let Some(gull) = board.gulls().iter().find(|g| g.tile == tile) {
return Some(gull.dir);
}
if let Some(crab) = board.crabs().iter().find(|c| c.tile == tile) {
return Some(crab.dir);
}
if let TileKind::Spawner(s) = board.tile_at(x, y) {
return Some(s.dir);
}
None
}
pub(super) fn creature_at(board: &Board, x: u8, y: u8) -> bool {
let tile = board.index_of(x, y);
board.gulls().iter().any(|g| g.tile == tile) || board.crabs().iter().any(|c| c.tile == tile)
}
pub(super) fn face(board: &mut Board, x: u8, y: u8, dir: Direction) -> bool {
let tile = board.index_of(x, y);
if board.gulls().iter().any(|g| g.tile == tile) {
board.remove_gulls_at(x, y);
board.spawn_gull(x, y, dir);
return true;
}
let crabs: Vec<_> = board
.crabs()
.iter()
.filter(|c| c.tile == tile)
.copied()
.collect();
if !crabs.is_empty() {
board.remove_crabs_at(x, y);
for crab in crabs {
board.spawn_crab(x, y, dir, crab.handed, crab.kind);
}
return true;
}
if let TileKind::Spawner(s) = board.tile_at(x, y) {
board.set_tile(x, y, TileKind::Spawner(Spawner { dir, ..s }));
return true;
}
false
}
pub(super) fn describe(board: &Board, x: u8, y: u8, tr: &crate::app::i18n::Tr) -> String {
let arrow = |dir| {
format!(
"{}{}",
crate::app::i18n::WORD_JOINER,
super::validate::arrow(dir)
)
};
let tile = board.index_of(x, y);
let mut parts = Vec::new();
if let Some(gull) = board.gulls().iter().find(|g| g.tile == tile) {
parts.push(format!(
"{}\u{a0}{}",
Brush::Gull.label(tr),
arrow(gull.dir)
));
}
if let Some(crab) = board.crabs().iter().find(|c| c.tile == tile) {
let kind = tr.ed_crab_kinds[crab_index(crab.kind)];
let claw = match crab.handed {
Handedness::Left => tr.ed_claws[0],
Handedness::Right => tr.ed_claws[1],
};
let dir = arrow(crab.dir);
parts.push(crate::app::i18n::fill(
tr.ed_crab_desc,
&[("k", kind), ("c", claw), ("d", &dir)],
));
}
let ground = match board.tile_at(x, y) {
TileKind::Empty if parts.is_empty() => Some(tr.ed_sand.to_string()),
TileKind::Empty => None,
TileKind::Castle(owner) => {
let seat = crate::app::i18n::fill(tr.player_label, &[("p", &(owner + 1).to_string())]);
Some(format!("{} {seat}", Brush::Castle.label(tr)))
}
TileKind::Spawner(s) => Some(format!("{}\u{a0}{}", Brush::Hole.label(tr), arrow(s.dir))),
TileKind::Rock => Some(Brush::Rock.label(tr).to_string()),
TileKind::Turnstile { .. } => Some(Brush::Log.label(tr).to_string()),
TileKind::Kelp => Some(Brush::Weed.label(tr).to_string()),
TileKind::Pool => Some(Brush::Pool.label(tr).to_string()),
};
parts.extend(ground);
parts.join(" + ")
}
fn crab_index(kind: CrabKind) -> usize {
match kind {
CrabKind::Common => 0,
CrabKind::Juvenile => 1,
CrabKind::Giant => 2,
CrabKind::Molting => 3,
CrabKind::Golden => 4,
CrabKind::Sparkling => 5,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn turning_turns_every_crab_on_the_tile() {
let mut board = Board::new(6, 5, 1);
board.spawn_crab(2, 2, Direction::Right, Handedness::Left, CrabKind::Common);
board.spawn_crab(2, 2, Direction::Right, Handedness::Right, CrabKind::Giant);
assert!(face(&mut board, 2, 2, Direction::Down));
let crabs: Vec<_> = board
.crabs()
.iter()
.map(|c| (c.dir, c.handed, c.kind))
.collect();
assert_eq!(
crabs,
[
(Direction::Down, Handedness::Left, CrabKind::Common),
(Direction::Down, Handedness::Right, CrabKind::Giant),
]
);
}
#[test]
fn no_brush_takes_a_movement_key() {
use bevy::prelude::KeyCode;
let movement =
super::super::EditorKeys::of(&crate::app::settings::GameSettings::default()).moves;
let controls = [
KeyCode::KeyK, KeyCode::KeyO, KeyCode::KeyV, KeyCode::KeyT, KeyCode::KeyN, KeyCode::Tab, KeyCode::Space, KeyCode::Enter, KeyCode::Escape, KeyCode::F6, ];
for brush in Brush::ALL {
let key = brush.key();
assert!(!movement.contains(&key), "{brush:?} is on a movement key");
assert!(!controls.contains(&key), "{brush:?} is on a control key");
}
let mut keys: Vec<_> = Brush::ALL
.iter()
.map(|b| format!("{:?}", b.key()))
.collect();
keys.sort();
let before = keys.len();
keys.dedup();
assert_eq!(keys.len(), before, "two brushes on one key");
}
#[test]
fn letters_name_their_keys() {
for brush in Brush::ALL {
assert_eq!(
format!("{:?}", brush.key()),
format!("Key{}", brush.letter()),
"{brush:?} shows a letter that is not its key"
);
}
}
fn sand() -> Board {
Board::new(6, 5, 3)
}
#[test]
fn the_gull_key_refuses_rock_and_kelp() {
let mut board = sand();
board.set_tile(1, 1, TileKind::Rock);
board.set_tile(2, 2, TileKind::Kelp);
paint(&mut board, 1, 1, Brush::Gull, SPAWNER_PERIOD);
paint(&mut board, 2, 2, Brush::Gull, SPAWNER_PERIOD);
assert!(board.gulls().is_empty(), "nowhere for a gull to stand");
paint(&mut board, 3, 3, Brush::Gull, SPAWNER_PERIOD);
assert_eq!(board.gulls().len(), 1, "sand takes one");
}
#[test]
fn the_castle_key_walks_the_seats_then_clears() {
let mut board = sand();
board.spawn_crab(2, 2, Direction::Right, Handedness::Left, CrabKind::Common);
for owner in 0..MAX_PLAYERS as u8 {
cycle_castle(&mut board, 2, 2);
assert_eq!(board.tile_at(2, 2), TileKind::Castle(owner));
}
assert!(board.crabs().is_empty(), "the crab was swept aside");
cycle_castle(&mut board, 2, 2);
assert_eq!(board.tile_at(2, 2), TileKind::Empty, "and round to nothing");
}
#[test]
fn a_brush_replaces_and_sand_erases() {
let mut board = sand();
paint(&mut board, 1, 1, Brush::Weed, SPAWNER_PERIOD);
assert_eq!(board.tile_at(1, 1), TileKind::Kelp);
paint(&mut board, 1, 1, Brush::Weed, SPAWNER_PERIOD);
assert_eq!(board.tile_at(1, 1), TileKind::Kelp, "and again is the same");
paint(&mut board, 1, 1, Brush::Pool, SPAWNER_PERIOD);
assert_eq!(board.tile_at(1, 1), TileKind::Pool, "another replaces it");
paint(&mut board, 1, 1, Brush::Sand, SPAWNER_PERIOD);
assert_eq!(board.tile_at(1, 1), TileKind::Empty, "and sand clears it");
board.spawn_crab(3, 3, Direction::Right, Handedness::Left, CrabKind::Common);
paint(&mut board, 3, 3, Brush::Rock, SPAWNER_PERIOD);
assert_eq!(board.tile_at(3, 3), TileKind::Rock);
assert!(board.crabs().is_empty());
}
#[test]
fn the_palette_is_one_list() {
use std::collections::HashSet;
let keys: HashSet<KeyCode> = Brush::ALL.iter().map(|b| b.key()).collect();
assert_eq!(keys.len(), Brush::ALL.len(), "two brushes share a key");
let letters: HashSet<&str> = Brush::ALL.iter().map(|b| b.letter()).collect();
assert_eq!(
letters.len(),
Brush::ALL.len(),
"two brushes share a letter"
);
assert_eq!(
crate::app::i18n::EN.ed_brushes.len(),
Brush::ALL.len(),
"the palette and its labels have drifted apart"
);
}
#[test]
fn spawner_directions_run_out_after_four() {
let dirs: Vec<Direction> =
std::iter::successors(Some(Direction::Right), |d| next_dir(*d)).collect();
assert_eq!(
dirs,
[
Direction::Right,
Direction::Down,
Direction::Left,
Direction::Up
]
);
assert_eq!(next_dir(Direction::Up), None, "the cycle ends, and removes");
}
#[test]
fn the_crab_key_cycles_twelve_then_clears() {
let mut board = sand();
let mut seen = Vec::new();
for _ in 0..12 {
cycle_crab(&mut board, 3, 3);
let crab = board.crabs().first().copied().expect("a crab is standing");
seen.push((crab.kind, crab.handed));
}
seen.dedup();
assert_eq!(seen.len(), 12, "every kind, both hands: {seen:?}");
cycle_crab(&mut board, 3, 3);
assert!(board.crabs().is_empty(), "the thirteenth press clears it");
board.set_tile(4, 3, TileKind::Rock);
cycle_crab(&mut board, 4, 3);
assert!(board.crabs().is_empty(), "crabs start on open sand only");
}
}