use crate::sim::board::{Board, MAX_PLAYERS, PlayerId, Spawner, TICKS_PER_SECOND, TileKind};
use crate::sim::crab::{CrabKind, Handedness};
use crate::sim::direction::Direction;
use crate::sim::rng::Pcg32;
pub fn castle_spots(width: u8, height: u8) -> [(u8, u8); MAX_PLAYERS] {
let mid = (width - 1) / 2;
[
(1, 1),
(width - 2, height - 2),
(width - 2, 1),
(1, height - 2),
(mid, 1),
(width - 1 - mid, height - 2),
]
}
fn seats_the_long_edges_evenly(width: u8) -> bool {
!width.is_multiple_of(2)
}
fn side_spawners(width: u8, height: u8) -> [(u8, u8, Direction); 2] {
[
(0, height / 2, Direction::Right),
(width - 1, height / 2, Direction::Left),
]
}
fn end_spawners(width: u8, height: u8) -> [(u8, u8, Direction); 4] {
let col = width / 4;
[
(col, 0, Direction::Down),
(width - 1 - col, 0, Direction::Down),
(col, height - 1, Direction::Up),
(width - 1 - col, height - 1, Direction::Up),
]
}
fn quad(width: u8, height: u8, x: u8, y: u8) -> [(u8, u8); 4] {
[
(x, y),
(width - 1 - x, y),
(x, height - 1 - y),
(width - 1 - x, height - 1 - y),
]
}
const QUAD_FLIPS: [(bool, bool); 4] = [(false, false), (true, false), (false, true), (true, true)];
fn mirror_dir(dir: Direction, flip_x: bool, flip_y: bool) -> Direction {
let horizontal = matches!(dir, Direction::Left | Direction::Right);
if (flip_x && horizontal) || (flip_y && !horizontal) {
dir.reverse()
} else {
dir
}
}
pub(crate) fn mirrored_tile(kind: TileKind, flip_x: bool, flip_y: bool) -> TileKind {
let flipped = flip_x ^ flip_y; match kind {
TileKind::Spawner(spawner) => TileKind::Spawner(Spawner {
dir: mirror_dir(spawner.dir, flip_x, flip_y),
period: spawner.period,
}),
TileKind::Turnstile { next_right } => TileKind::Turnstile {
next_right: next_right ^ flipped,
},
TileKind::Empty
| TileKind::Rock
| TileKind::Castle(_)
| TileKind::Kelp
| TileKind::Pool => kind,
}
}
fn place_quad(board: &mut Board, x: u8, y: u8, kind: TileKind) -> bool {
let spots = quad(board.width(), board.height(), x, y);
if spots
.iter()
.any(|&(qx, qy)| board.tile_at(qx, qy) != TileKind::Empty)
{
return false;
}
for (&(qx, qy), (flip_x, flip_y)) in spots.iter().zip(QUAD_FLIPS) {
board.set_tile(qx, qy, mirrored_tile(kind, flip_x, flip_y));
}
true
}
fn wall_quad(width: u8, height: u8, x: u8, y: u8, dir: Direction) -> [(u8, u8); 4] {
let (mx, my) = match dir {
Direction::Down | Direction::Up => (width - 1 - x, height.saturating_sub(2) - y),
Direction::Left | Direction::Right => (width.saturating_sub(2) - x, height - 1 - y),
};
[(x, y), (mx, y), (x, my), (mx, my)]
}
fn place_wall_quad(board: &mut Board, x: u8, y: u8, dir: Direction) {
for (wx, wy) in wall_quad(board.width(), board.height(), x, y, dir) {
board.set_wall(wx, wy, dir, true);
}
}
pub fn classic_arena(preload_scores: bool, seats: u8) -> Board {
classic_arena_seeded(0x5EA51DE, preload_scores, seats)
}
pub fn classic_arena_seeded(seed: u64, preload_scores: bool, seats: u8) -> Board {
let mut board = Board::new(12, 9, seed);
for (seat, &(x, y)) in castle_spots(12, 9)
.iter()
.enumerate()
.take(seats.clamp(2, 4) as usize)
{
board.set_tile(x, y, TileKind::Castle(seat as PlayerId));
}
board.set_tile(
0,
4,
TileKind::Spawner(Spawner {
dir: Direction::Right,
period: 46,
}),
);
board.set_tile(
11,
4,
TileKind::Spawner(Spawner {
dir: Direction::Left,
period: 46,
}),
);
board.set_tile(5, 4, TileKind::Rock);
board.set_tile(6, 4, TileKind::Rock);
for (x, y, dir) in [
(2u8, 2u8, Direction::Down),
(3, 2, Direction::Down),
(9, 5, Direction::Down),
(8, 5, Direction::Down),
(5, 1, Direction::Right),
(5, 7, Direction::Right),
(8, 3, Direction::Right),
(2, 5, Direction::Right),
] {
board.set_wall(x, y, dir, true);
}
board.spawn_crab(3, 2, Direction::Right, Handedness::Left, CrabKind::Molting);
board.spawn_crab(8, 6, Direction::Left, Handedness::Left, CrabKind::Molting);
board.set_gull_period(240);
board.set_round_length(Some(3 * 60 * TICKS_PER_SECOND));
board.set_events_enabled(true);
if preload_scores {
board.set_score(0, 30);
board.set_score(1, 12);
}
board
}
pub fn generate_arena(seed: u64, seats: u8, width: u8, height: u8) -> Board {
let (width, height) = (width.max(9), height.max(7));
let width = if seats >= 5 && !seats_the_long_edges_evenly(width) {
width + 1
} else {
width
};
let mut rng = Pcg32::new(seed, 0x0a_2e4a);
let mut board = Board::new(width, height, seed);
for (seat, &(x, y)) in castle_spots(width, height)
.iter()
.enumerate()
.take(seats.clamp(2, MAX_PLAYERS as u8) as usize)
{
board.set_tile(x, y, TileKind::Castle(seat as PlayerId));
}
let area = u32::from(width) * u32::from(height);
let roll = rng.next_u32() % 2;
let sides = side_spawners(width, height);
let ends = end_spawners(width, height);
let spots: Vec<(u8, u8, Direction)> = if area >= 240 {
sides.iter().chain(ends.iter()).copied().collect()
} else if roll == 0 {
sides.to_vec()
} else {
ends.to_vec()
};
let period = (34 + rng.next_u32() % 26) * spots.len() as u32 / 2;
for (x, y, dir) in spots {
board.set_tile(x, y, TileKind::Spawner(Spawner { dir, period }));
}
let groups = area / 160 + 1 + rng.next_u32() % 2;
let (rock_w, rock_h) = (u32::from(width) - 6, u32::from(height) - 4);
let mut placed: Vec<(i32, i32)> = Vec::new();
let mut attempts = 0;
while (placed.len() as u32) < groups && attempts < 80 {
attempts += 1;
let x = 3 + (rng.next_u32() % rock_w) as i32;
let y = 2 + (rng.next_u32() % rock_h) as i32;
let images = quad(width, height, x as u8, y as u8);
if images.iter().any(|&(ix, iy)| {
placed
.iter()
.any(|&(px, py)| (px - i32::from(ix)).abs() + (py - i32::from(iy)).abs() <= 2)
}) {
continue;
}
if !place_quad(&mut board, x as u8, y as u8, TileKind::Rock) {
continue;
}
placed.extend(images.map(|(ix, iy)| (i32::from(ix), i32::from(iy))));
}
let wall_groups = 1 + rng.next_u32() % 2;
for _ in 0..wall_groups {
let x = 1 + (rng.next_u32() % u32::from(width - 3)) as u8;
let y = 1 + (rng.next_u32() % u32::from(height - 3)) as u8;
let dir = if rng.next_u32().is_multiple_of(2) {
Direction::Down
} else {
Direction::Right
};
place_wall_quad(&mut board, x, y, dir);
}
for kind in [TileKind::Pool, TileKind::Kelp] {
for _ in 0..8 {
let x = 2 + (rng.next_u32() % u32::from(width - 4)) as u8;
let y = 2 + (rng.next_u32() % u32::from(height - 4)) as u8;
if place_quad(&mut board, x, y, kind) {
break;
}
}
}
place_quad(
&mut board,
(width - 1) / 2 - 1,
(height - 1) / 2 - 1,
TileKind::Turnstile { next_right: true },
);
board.set_gull_period(200 + rng.next_u32() % 80);
board.set_round_length(Some(3 * 60 * TICKS_PER_SECOND));
board.set_events_enabled(true);
board
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classic_arena_seats_castles_in_join_order() {
for seats in 2..=4u8 {
let board = classic_arena(false, seats);
for seat in 0..4u8 {
let &(x, y) = &castle_spots(12, 9)[seat as usize];
let expect = if seat < seats {
TileKind::Castle(seat)
} else {
TileKind::Empty
};
assert_eq!(board.tile_at(x, y), expect, "{seats} seats, seat {seat}");
}
}
assert_eq!(
classic_arena(false, 2).state_hash(),
classic_arena(false, 2).state_hash()
);
assert_eq!(classic_arena(false, 2).scores()[0], 0);
assert_eq!(classic_arena(true, 2).scores()[0], 30);
}
#[test]
fn six_castles_mirror_both_ways() {
for &(w, h) in &[(9u8, 7u8), (12, 9), (16, 11), (20, 13)] {
let board = generate_arena(7, 6, w, h);
let (w, h) = (board.width(), board.height());
assert!(!w.is_multiple_of(2), "{w}x{h} has a centre column");
let spots = castle_spots(w, h);
let set: Vec<(u8, u8)> = spots.to_vec();
for &(x, y) in &spots {
assert!(set.contains(&(w - 1 - x, y)), "({x},{y}) flipped across");
assert!(set.contains(&(x, h - 1 - y)), "({x},{y}) flipped down");
}
for pair in 0..3usize {
let (x0, y0) = spots[pair * 2];
assert_eq!((w - 1 - x0, h - 1 - y0), spots[pair * 2 + 1], "{pair}");
}
assert_eq!(spots[4].0, spots[5].0, "{w}x{h} edge castles centred");
}
}
#[test]
fn same_seed_same_arena() {
let a = generate_arena(1234, 4, 12, 9);
let b = generate_arena(1234, 4, 12, 9);
assert_eq!(a.state_hash(), b.state_hash());
}
#[test]
fn different_seeds_differ() {
assert_ne!(
generate_arena(1, 4, 12, 9).state_hash(),
generate_arena(2, 4, 12, 9).state_hash()
);
}
#[test]
fn every_size_generates_sane_arenas() {
for &(w, h) in &[(9u8, 7u8), (12, 9), (16, 11), (20, 13)] {
for seed in 0..10u64 {
let board = generate_arena(seed, 4, w, h);
assert_eq!(board.width(), w);
assert_eq!(board.height(), h);
let mut castles = 0;
for y in 0..h {
for x in 0..w {
if let TileKind::Castle(_) = board.tile_at(x, y) {
castles += 1;
}
}
}
assert_eq!(castles, 4, "{w}x{h} seed {seed}");
}
}
}
#[test]
fn arenas_have_the_requested_castles_and_some_spawners() {
for seed in 0..20u64 {
let board = generate_arena(seed, 4, 12, 9);
let mut castles = 0;
let mut spawners = 0;
for y in 0..board.height() {
for x in 0..board.width() {
match board.tile_at(x, y) {
TileKind::Castle(_) => castles += 1,
TileKind::Spawner(_) => spawners += 1,
TileKind::Empty
| TileKind::Rock
| TileKind::Turnstile { .. }
| TileKind::Kelp
| TileKind::Pool => {}
}
}
}
assert_eq!(castles, 4, "seed {seed}");
assert!(spawners >= 2, "seed {seed}");
assert!(board.remaining_ticks().is_some());
}
}
#[test]
fn generated_arenas_mirror_both_ways() {
for &(w, h) in &[(9u8, 7u8), (12, 9), (16, 11), (20, 13)] {
for seed in 0..25u64 {
let board = generate_arena(seed, 4, w, h);
for y in 0..h {
for x in 0..w {
let here = board.tile_at(x, y);
let same = |other: TileKind, flip_x: bool, flip_y: bool| match (
mirrored_tile(here, flip_x, flip_y),
other,
) {
(TileKind::Castle(_), TileKind::Castle(_)) => true,
(a, b) => a == b,
};
assert!(
same(board.tile_at(w - 1 - x, y), true, false),
"{w}x{h} seed {seed}: ({x},{y}) breaks the left-right mirror"
);
assert!(
same(board.tile_at(x, h - 1 - y), false, true),
"{w}x{h} seed {seed}: ({x},{y}) breaks the top-bottom mirror"
);
assert_eq!(
board.wall_at(x, y, Direction::Down),
board.wall_at(w - 1 - x, y, Direction::Down),
"{w}x{h} seed {seed}: wall under ({x},{y})"
);
assert_eq!(
board.wall_at(x, y, Direction::Right),
board.wall_at(x, h - 1 - y, Direction::Right),
"{w}x{h} seed {seed}: wall right of ({x},{y})"
);
if y + 2 <= h {
assert_eq!(
board.wall_at(x, y, Direction::Down),
board.wall_at(x, h - 2 - y, Direction::Down),
"{w}x{h} seed {seed}: wall under ({x},{y}), flipped"
);
}
if x + 2 <= w {
assert_eq!(
board.wall_at(x, y, Direction::Right),
board.wall_at(w - 2 - x, y, Direction::Right),
"{w}x{h} seed {seed}: wall right of ({x},{y}), flipped"
);
}
}
}
let holes: Vec<(u8, u8, Spawner)> = (0..h)
.flat_map(|y| (0..w).map(move |x| (x, y)))
.filter_map(|(x, y)| match board.tile_at(x, y) {
TileKind::Spawner(s) => Some((x, y, s)),
TileKind::Empty
| TileKind::Rock
| TileKind::Castle(_)
| TileKind::Turnstile { .. }
| TileKind::Kelp
| TileKind::Pool => None,
})
.collect();
assert!(holes.len() >= 2, "{w}x{h} seed {seed}: too few spawners");
for &(x, y, hole) in &holes {
let twin = |flip_x: bool, flip_y: bool| Spawner {
dir: mirror_dir(hole.dir, flip_x, flip_y),
period: hole.period,
};
assert!(
holes.contains(&(w - 1 - x, y, twin(true, false)))
&& holes.contains(&(x, h - 1 - y, twin(false, true))),
"{w}x{h} seed {seed}: spawner ({x},{y}) has no mirror twin"
);
}
}
}
}
#[test]
fn generated_arenas_are_alive_with_bots() {
use crate::sim::{BotLevel, MAX_PLAYERS, PlayerAction, bot_action};
let mut board = generate_arena(777, 4, 12, 9);
for _ in 0..4000 {
let mut actions = [PlayerAction::None; MAX_PLAYERS];
for seat in 0..4u8 {
actions[seat as usize] = bot_action(&board, seat, BotLevel::Normal);
}
board.tick(&actions);
}
assert!(
board.crabs_spawned() > 40,
"spawners ran ({} spawned)",
board.crabs_spawned()
);
assert!(
board.crabs_banked() > 0,
"bots routed crabs home (scores {:?})",
board.scores()
);
}
#[test]
fn an_open_ocean_arena_plays_a_round() {
use crate::sim::{BotLevel, MAX_PLAYERS, PlayerAction, bot_action};
for seed in 0..6u64 {
let mut board = generate_arena(seed, 4, 16, 11);
board.set_wrap(true);
for _ in 0..4000 {
let mut actions = [PlayerAction::None; MAX_PLAYERS];
for seat in 0..4u8 {
actions[seat as usize] = bot_action(&board, seat, BotLevel::Normal);
}
board.tick(&actions);
}
assert!(board.crabs_spawned() > 40, "seed {seed} spawners ran");
assert!(
board.crabs_banked() > 0,
"seed {seed} bots routed crabs home across an open edge (scores {:?})",
board.scores()
);
let tiles = u16::from(board.width()) * u16::from(board.height());
assert!(board.gulls().iter().all(|g| g.tile < tiles), "seed {seed}");
}
}
}