use crate::sim::crab::{Crab, CrabKind, Handedness};
use crate::sim::direction::Direction;
use crate::sim::gull::{
EAT_RANGE, FLIGHT_MAX, FLIGHT_MIN, GULL_FLY_SPEED, GULL_WALK_SPEED, Gull, GullState,
TAKEOFF_MAX, TAKEOFF_MIN,
};
use crate::sim::hash::Fnv;
use crate::sim::rng::Pcg32;
pub const TIER_FLOORS: [u32; 4] = [0, 10, 25, 50];
pub const SPILL_CAP: u32 = 8;
pub const LURE_TICKS: u32 = 300;
pub const LURE_COOLDOWN: u32 = 300;
pub const SIGNPOST_LIFETIME: u32 = 300;
pub const TICKS_PER_SECOND: u32 = 30;
pub const EVENT_TICKS: u32 = 300;
pub const SURGE_TICKS: u32 = 900;
pub fn castle_tier(score: u32) -> u8 {
match score {
0..=9 => 0,
10..=24 => 1,
25..=49 => 2,
_ => 3,
}
}
fn sub_offset(dir: Direction, progress: u16) -> (i32, i32) {
let (dx, dy) = dir.offset();
(dx * i32::from(progress), dy * i32::from(progress))
}
pub type PlayerId = u8;
pub fn seat(player: PlayerId) -> Option<usize> {
let seat = usize::from(player);
(seat < MAX_PLAYERS).then_some(seat)
}
pub(crate) const CASTLE_RING: [(i32, i32); 8] = [
(0, -1),
(1, 0),
(0, 1),
(-1, 0),
(1, -1),
(1, 1),
(-1, 1),
(-1, -1),
];
pub const MAX_PLAYERS: usize = 6;
pub const MAX_SIGNPOSTS_PER_PLAYER: usize = 3;
pub const GULL_CAP: usize = 6;
pub const CRAB_CAP_TILES_PER_CRAB: usize = 3;
pub const SUBUNITS_PER_TILE: u16 = 256;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TileKind {
Empty,
Rock,
Castle(PlayerId),
Spawner(Spawner),
Turnstile {
next_right: bool,
},
Kelp,
Pool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Spawner {
pub dir: Direction,
pub period: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SignpostHealth {
Full,
Worn,
}
#[derive(Clone, Copy, Debug)]
pub struct Signpost {
pub dir: Direction,
pub owner: PlayerId,
pub health: SignpostHealth,
seq: u64,
pub placed: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum PlayerAction {
#[default]
None,
Place {
x: u8,
y: u8,
dir: Direction,
},
Remove {
x: u8,
y: u8,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CapPolicy {
Evict,
Reject,
}
impl CapPolicy {
pub fn token(self) -> &'static str {
match self {
CapPolicy::Evict => "evict",
CapPolicy::Reject => "reject",
}
}
pub fn from_token(token: &str) -> Option<CapPolicy> {
match token {
"evict" => Some(CapPolicy::Evict),
"reject" => Some(CapPolicy::Reject),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct Board {
width: u8,
height: u8,
seed: u64,
h_walls: Vec<bool>,
v_walls: Vec<bool>,
tiles: Vec<TileKind>,
signposts: Vec<Option<Signpost>>,
crabs: Vec<Crab>,
scores: [u32; MAX_PLAYERS],
rng: Pcg32,
tick: u64,
signpost_seq: u64,
next_crab_id: u32,
signpost_cap: u8,
cap_policy: CapPolicy,
gulls: Vec<Gull>,
next_gull_id: u32,
gull_period: u32,
round_length: Option<u32>,
lure: Option<(PlayerId, u32)>,
lure_cooldown: u32,
crabs_banked: u32,
golden_banked: u32,
events_enabled: bool,
mania: Option<(Mania, u32)>,
tempo: Option<(Tempo, u32)>,
last_event: Option<(TideEvent, u64)>,
wrap: bool,
event_queue: Vec<PlayerId>,
}
impl Board {
pub fn new(width: u8, height: u8, seed: u64) -> Board {
assert!(width > 0 && height > 0, "board must be at least 1×1");
let (w, h) = (width as usize, height as usize);
let mut board = Board {
width,
height,
seed,
h_walls: vec![false; (h + 1) * w],
v_walls: vec![false; h * (w + 1)],
tiles: vec![TileKind::Empty; w * h],
signposts: vec![None; w * h],
crabs: Vec::new(),
scores: [0; MAX_PLAYERS],
rng: Pcg32::new(seed, 0x0005_eaba_55ed),
tick: 0,
signpost_seq: 0,
next_crab_id: 0,
signpost_cap: MAX_SIGNPOSTS_PER_PLAYER as u8,
cap_policy: CapPolicy::Evict,
gulls: Vec::new(),
next_gull_id: 0,
gull_period: 0,
round_length: None,
lure: None,
lure_cooldown: 0,
crabs_banked: 0,
golden_banked: 0,
events_enabled: false,
mania: None,
tempo: None,
last_event: None,
wrap: false,
event_queue: Vec::new(),
};
board.set_wrap(false);
board
}
pub fn set_wall(&mut self, x: u8, y: u8, dir: Direction, present: bool) {
assert!(
self.in_bounds(i32::from(x), i32::from(y)),
"wall off the board"
);
self.set_edge(x as usize, y as usize, dir, present);
}
pub fn set_tile(&mut self, x: u8, y: u8, kind: TileKind) {
assert!(
self.in_bounds(i32::from(x), i32::from(y)),
"tile off the board"
);
if let TileKind::Castle(owner) = kind {
assert!(seat(owner).is_some(), "invalid castle owner");
}
if let TileKind::Spawner(s) = kind {
assert!(s.period > 0, "spawner period must be at least 1 tick");
}
let t = self.index(i32::from(x), i32::from(y));
self.tiles[t as usize] = kind;
}
pub fn set_signpost_rule(&mut self, cap: u8, policy: CapPolicy) {
self.signpost_cap = cap;
self.cap_policy = policy;
}
pub fn set_gull_period(&mut self, period: u32) {
self.gull_period = period;
}
pub fn set_round_length(&mut self, ticks: Option<u32>) {
self.round_length = ticks;
}
pub fn set_wrap(&mut self, wrap: bool) {
self.wrap = wrap;
let (w, h) = (self.width as usize, self.height as usize);
for x in 0..w {
self.h_walls[x] = !wrap;
self.h_walls[h * w + x] = !wrap;
}
for y in 0..h {
self.v_walls[y * (w + 1)] = !wrap;
self.v_walls[y * (w + 1) + w] = !wrap;
}
}
pub fn wrap(&self) -> bool {
self.wrap
}
pub fn events_enabled(&self) -> bool {
self.events_enabled
}
pub fn set_events_enabled(&mut self, enabled: bool) {
self.events_enabled = enabled;
}
pub fn last_event(&self) -> Option<(TideEvent, u64)> {
self.last_event
}
pub fn golden_banked(&self) -> u32 {
self.golden_banked
}
pub fn set_score(&mut self, player: PlayerId, score: u32) {
if let Some(seat) = seat(player) {
self.scores[seat] = score;
}
}
pub fn tick(&mut self, actions: &[PlayerAction; MAX_PLAYERS]) {
if self.round_over() {
return;
}
for player in self.action_order() {
self.apply_action(player, actions[player as usize]);
}
self.expire_signposts();
self.run_spawners();
self.run_gull_spawner();
self.move_crabs();
self.move_gulls();
self.gulls_eat();
if let Some((_, ticks)) = &mut self.lure {
*ticks -= 1;
if *ticks == 0 {
self.lure = None;
self.lure_cooldown = LURE_COOLDOWN;
}
} else {
self.lure_cooldown = self.lure_cooldown.saturating_sub(1);
}
if let Some((_, ticks)) = &mut self.mania {
*ticks -= 1;
if *ticks == 0 {
self.mania = None;
}
}
if let Some((_, ticks)) = &mut self.tempo {
*ticks -= 1;
if *ticks == 0 {
self.tempo = None;
}
}
self.tick += 1;
}
fn action_order(&self) -> [PlayerId; MAX_PLAYERS] {
let seats = u64::from(self.seats_in_play()).max(1);
let first = (self.tick ^ (self.tick >> 3)) % seats;
std::array::from_fn(|i| {
let i = i as u64;
if i < seats {
((first + i) % seats) as PlayerId
} else {
i as PlayerId }
})
}
pub fn seats_in_play(&self) -> u8 {
self.castle_owners().max().map_or(0, |owner| owner + 1)
}
pub fn round_over(&self) -> bool {
self.round_length
.is_some_and(|len| self.tick >= u64::from(len))
}
pub fn remaining_ticks(&self) -> Option<u64> {
self.round_length
.map(|len| u64::from(len).saturating_sub(self.tick))
}
pub fn in_surge(&self) -> bool {
self.remaining_ticks()
.is_some_and(|left| left <= u64::from(SURGE_TICKS))
}
pub fn tick_idle(&mut self) {
self.tick(&[PlayerAction::None; MAX_PLAYERS]);
}
fn apply_action(&mut self, player: PlayerId, action: PlayerAction) {
match action {
PlayerAction::None => {}
PlayerAction::Place { x, y, dir } => {
let _ = self.place_signpost(player, x, y, dir);
}
PlayerAction::Remove { x, y } => {
let _ = self.remove_signpost(player, x, y);
}
}
}
fn walk_step(&self, tile: u16, base: u16) -> u16 {
debug_assert!(
usize::from(tile) < self.tiles.len(),
"walking off the board: tile {tile} of {}",
self.tiles.len()
);
let step = self.tempo_speed(base);
if self.tiles[tile as usize] == TileKind::Pool {
return (step / 2).max(1);
}
step
}
fn tempo_speed(&self, base: u16) -> u16 {
match self.tempo {
Some((Tempo::Fast, _)) => base * 2,
Some((Tempo::Slow, _)) => (base / 2).max(1),
None => base,
}
}
pub fn width(&self) -> u8 {
self.width
}
pub fn height(&self) -> u8 {
self.height
}
pub fn ticks(&self) -> u64 {
self.tick
}
pub fn crabs(&self) -> &[Crab] {
&self.crabs
}
pub fn gulls(&self) -> &[Gull] {
&self.gulls
}
pub fn gull_period(&self) -> u32 {
self.gull_period
}
pub fn round_length(&self) -> Option<u32> {
self.round_length
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn remove_crabs_at(&mut self, x: u8, y: u8) {
let tile = self.index(i32::from(x), i32::from(y));
self.crabs.retain(|c| c.tile != tile);
}
pub fn remove_gulls_at(&mut self, x: u8, y: u8) {
let tile = self.index(i32::from(x), i32::from(y));
self.gulls.retain(|g| g.tile != tile);
}
pub fn lure(&self) -> Option<(PlayerId, u32)> {
self.lure
}
pub fn crabs_banked(&self) -> u32 {
self.crabs_banked
}
pub fn crabs_spawned(&self) -> u32 {
self.next_crab_id
}
pub fn scores(&self) -> &[u32; MAX_PLAYERS] {
&self.scores
}
pub fn tile_at(&self, x: u8, y: u8) -> TileKind {
assert!(self.in_bounds(i32::from(x), i32::from(y)));
self.tiles[self.index(i32::from(x), i32::from(y)) as usize]
}
pub fn tiles(&self) -> impl Iterator<Item = (u8, u8, TileKind)> + '_ {
let width = self.width;
self.tiles.iter().enumerate().map(move |(index, &kind)| {
(
(index % width as usize) as u8,
(index / width as usize) as u8,
kind,
)
})
}
pub fn castle_of(&self, player: PlayerId) -> Option<(u8, u8)> {
self.tiles()
.find(|&(_, _, kind)| kind == TileKind::Castle(player))
.map(|(x, y, _)| (x, y))
}
pub fn castle_owners(&self) -> impl Iterator<Item = PlayerId> + '_ {
self.tiles().filter_map(|(_, _, kind)| match kind {
TileKind::Castle(owner) => Some(owner),
TileKind::Empty
| TileKind::Rock
| TileKind::Spawner(_)
| TileKind::Turnstile { .. }
| TileKind::Kelp
| TileKind::Pool => None,
})
}
pub fn castle_seats(&self) -> u8 {
let mut seen = [false; MAX_PLAYERS];
for owner in self.castle_owners() {
if let Some(slot) = seen.get_mut(usize::from(owner)) {
*slot = true;
}
}
seen.iter().filter(|held| **held).count() as u8
}
pub fn first_signpost_of(&self, player: PlayerId) -> Option<(u8, u8)> {
self.signposts
.iter()
.position(|post| post.is_some_and(|post| post.owner == player))
.map(|index| self.coords_u8(index as u16))
}
pub fn coords_u8(&self, tile: u16) -> (u8, u8) {
let (x, y) = self.coords(tile);
(x as u8, y as u8)
}
pub fn wall_at(&self, x: u8, y: u8, dir: Direction) -> bool {
assert!(self.in_bounds(i32::from(x), i32::from(y)));
self.edge_blocked(x as usize, y as usize, dir)
}
}
mod crabs;
mod events;
mod geometry;
mod gulls;
mod hashing;
mod signposts;
mod snapshot;
#[cfg(test)]
mod tests;
pub use events::{Mania, Tempo, TideEvent};
pub(crate) use geometry::Walker;