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::pose::Pose;
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 = 600;
pub const EVENT_COOLDOWN: u32 = EVENT_TICKS;
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 GOLDEN_CALLED_VALUE: u32 = 30;
pub const GOLDEN_NOTICE: u32 = 5 * TICKS_PER_SECOND;
pub const SWEEP_HOLD: u64 = 66;
pub const SWEEP_STAGGER: u64 = 4;
pub const SWEEP_FLIGHT: u64 = 30;
pub const VERSUS_GOLDEN_EVERY: u32 = 3 * 60 * TICKS_PER_SECOND;
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,
},
CallEvent(TideEvent),
}
#[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, Copy, PartialEq, Eq, Debug)]
pub struct Rules {
pub signpost_cap: u8,
pub cap_policy: CapPolicy,
pub gull_period: u32,
pub gull_cap: u8,
pub gull_turnover: bool,
pub golden_every: u32,
pub timed_lock: bool,
pub sweep_flight: bool,
pub round_length: Option<u32>,
pub castle_raids: bool,
}
#[derive(Clone, Debug)]
pub struct Board {
grid: Grid,
seed: u64,
signposts: Vec<Option<Signpost>>,
rules: Rules,
crabs: Vec<Crab>,
scores: [u32; MAX_PLAYERS],
rng: Pcg32,
tick: u64,
signpost_seq: u64,
next_crab_id: u32,
gulls: Vec<Gull>,
next_gull_id: u32,
lure: Option<(PlayerId, u32)>,
lure_cooldown: u32,
crabs_banked: u32,
golden_banked: u32,
golden_call: Option<(u64, u16)>,
wrap: bool,
pub(crate) tide: events::Tide,
swept_home: Vec<Swept>,
sweeping: Vec<Sweeping>,
}
#[derive(Clone, Copy, Debug)]
pub struct Sweeping {
pub crab: Crab,
pub owner: PlayerId,
pub launch: u64,
pub lands: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Swept {
crab: u32,
owner: PlayerId,
at: u64,
}
const SWEEP_MEMORY: u64 = 8;
fn refill<T: Copy>(dst: &mut Vec<T>, src: &[T]) {
dst.clear();
dst.extend_from_slice(src);
}
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 {
grid: Grid {
width,
height,
h_walls: vec![false; (h + 1) * w],
v_walls: vec![false; h * (w + 1)],
tiles: vec![TileKind::Empty; w * h],
},
seed,
signposts: vec![None; w * h],
rules: Rules {
signpost_cap: MAX_SIGNPOSTS_PER_PLAYER as u8,
cap_policy: CapPolicy::Evict,
gull_period: 0,
gull_cap: GULL_CAP as u8,
gull_turnover: false,
golden_every: 0,
timed_lock: false,
sweep_flight: false,
round_length: None,
castle_raids: true,
},
crabs: Vec::new(),
scores: [0; MAX_PLAYERS],
rng: Pcg32::new(seed, 0x0005_eaba_55ed),
tick: 0,
signpost_seq: 0,
next_crab_id: 0,
gulls: Vec::new(),
next_gull_id: 0,
lure: None,
lure_cooldown: 0,
crabs_banked: 0,
golden_banked: 0,
golden_call: None,
wrap: false,
tide: events::Tide::default(),
swept_home: Vec::new(),
sweeping: Vec::new(),
};
board.set_wrap(false);
board
}
pub(crate) fn copy_from(&mut self, other: &Self) {
let Self {
grid,
seed,
signposts,
rules,
crabs,
scores,
rng,
tick,
signpost_seq,
next_crab_id,
gulls,
next_gull_id,
lure,
lure_cooldown,
crabs_banked,
golden_banked,
golden_call,
wrap,
tide,
swept_home,
sweeping,
} = other;
self.grid.copy_from(grid);
self.seed = *seed;
refill(&mut self.signposts, signposts);
self.rules = *rules;
refill(&mut self.crabs, crabs);
self.scores = *scores;
self.rng.clone_from(rng);
self.tick = *tick;
self.signpost_seq = *signpost_seq;
self.next_crab_id = *next_crab_id;
refill(&mut self.gulls, gulls);
self.next_gull_id = *next_gull_id;
self.lure = *lure;
self.lure_cooldown = *lure_cooldown;
self.crabs_banked = *crabs_banked;
self.golden_banked = *golden_banked;
self.golden_call = *golden_call;
self.wrap = *wrap;
self.tide.copy_from(tide);
refill(&mut self.swept_home, swept_home);
refill(&mut self.sweeping, sweeping);
}
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"
);
if self.on_open_rim(x, y, dir) {
return;
}
self.set_edge(x as usize, y as usize, dir, present);
}
pub fn on_open_rim(&self, x: u8, y: u8, dir: Direction) -> bool {
let (dx, dy) = dir.offset();
self.wrap && !self.in_bounds(i32::from(x) + dx, i32::from(y) + dy)
}
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.grid.tiles[t as usize] = kind;
}
pub fn set_castle_raids(&mut self, raids: bool) {
self.rules.castle_raids = raids;
}
pub fn castle_raids(&self) -> bool {
self.rules.castle_raids
}
pub fn set_signpost_rule(&mut self, cap: u8, policy: CapPolicy) {
self.rules.signpost_cap = cap;
self.rules.cap_policy = policy;
}
pub fn set_gull_period(&mut self, period: u32) {
self.rules.gull_period = period;
}
pub fn set_gull_cap(&mut self, cap: u8) {
self.rules.gull_cap = cap;
}
pub fn set_gull_turnover(&mut self, on: bool) {
self.rules.gull_turnover = on;
}
pub fn set_sweep_flight(&mut self, on: bool) {
self.rules.sweep_flight = on;
}
pub fn set_timed_lock(&mut self, on: bool) {
self.rules.timed_lock = on;
}
pub fn set_golden_every(&mut self, ticks: u32) {
self.rules.golden_every = ticks;
}
pub fn set_round_length(&mut self, ticks: Option<u32>) {
self.rules.round_length = ticks;
}
pub fn set_wrap(&mut self, wrap: bool) {
self.wrap = wrap;
self.grid.set_rim(!wrap);
}
pub fn wrap(&self) -> bool {
self.wrap
}
pub fn events_enabled(&self) -> bool {
self.tide.enabled
}
pub fn set_events_enabled(&mut self, enabled: bool) {
self.tide.enabled = enabled;
}
pub fn last_event(&self) -> Option<(TideEvent, u64)> {
self.tide.last
}
pub fn event_spun_by(&self) -> Option<PlayerId> {
self.tide.spun_by
}
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;
}
self.tide.cooldown = self.tide.cooldown.saturating_sub(1);
let tick = self.tick;
self.swept_home
.retain(|swept| swept.at + SWEEP_MEMORY > tick);
for player in self.action_order() {
self.apply_action(player, actions[player as usize]);
}
self.expire_signposts();
self.run_golden_call();
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.tide.mania {
*ticks -= 1;
if *ticks == 0 {
self.tide.mania = None;
}
}
self.tide.claw_call = self.tide.claw_call.saturating_sub(1);
if let Some((_, ticks)) = &mut self.tide.tempo {
*ticks -= 1;
if *ticks == 0 {
self.tide.tempo = None;
}
}
self.land_sweeps();
self.tick += 1;
#[cfg(debug_assertions)]
self.check_invariants();
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
for (what, left) in [
("lure", self.lure.map(|(_, t)| t)),
("mania", self.tide.mania.map(|(_, t)| t)),
("tempo", self.tide.tempo.map(|(_, t)| t)),
] {
debug_assert!(left != Some(0), "a {what} left running with no ticks");
}
let tiles = self.grid.tiles.len();
let mut held = [0usize; MAX_PLAYERS];
for (t, post) in self.signposts.iter().enumerate() {
let Some(post) = post else {
continue;
};
debug_assert_eq!(
self.grid.tiles[t],
TileKind::Empty,
"a signpost stands on a {:?} at tile {t}",
self.grid.tiles[t]
);
debug_assert!(post.seq < self.signpost_seq, "a signpost from the future");
if let Some(slot) = held.get_mut(usize::from(post.owner)) {
*slot += 1;
}
}
debug_assert!(
held.iter()
.all(|&n| n <= usize::from(self.rules.signpost_cap)),
"posts over the cap of {}: {held:?}",
self.rules.signpost_cap
);
debug_assert!(
self.crabs.iter().all(|crab| usize::from(crab.tile) < tiles),
"a crab off the board"
);
debug_assert!(
self.gulls.iter().all(|gull| usize::from(gull.tile) < tiles),
"a gull off the board"
);
debug_assert!(self.golden_banked <= self.crabs_banked);
debug_assert!(
!(self.wrap && self.grid.rim_walled()),
"a wall on the rim of an open beach"
);
}
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.rules
.round_length
.is_some_and(|len| self.tick >= u64::from(len))
}
pub fn remaining_ticks(&self) -> Option<u64> {
self.rules
.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);
}
PlayerAction::CallEvent(event) => {
self.force_tide_event(self.surge_safe(event), player);
}
}
}
fn land_sweeps(&mut self) {
if self.sweeping.is_empty() {
return;
}
let tick = self.tick;
let last = self
.rules
.round_length
.is_some_and(|len| tick + 1 >= u64::from(len));
let mut i = 0;
while i < self.sweeping.len() {
if !last && self.sweeping[i].lands > tick {
i += 1;
continue;
}
let Sweeping { crab, owner, .. } = self.sweeping.remove(i);
self.credit_bank(owner, &crab);
self.crabs_banked += 1;
if crab.kind == CrabKind::Golden {
self.golden_banked += 1;
}
self.swept_home.push(Swept {
crab: crab.id,
owner,
at: tick,
});
}
}
pub(super) fn credit_bank(&mut self, owner: PlayerId, crab: &Crab) {
let points = crab.bank_points(self.crab_value(crab.kind), self.in_claw_call());
let score = &mut self.scores[owner as usize];
*score = score.saturating_add_signed(points);
}
fn walk_step(&self, tile: u16, base: u16) -> u16 {
debug_assert!(
usize::from(tile) < self.grid.tiles.len(),
"walking off the board: tile {tile} of {}",
self.grid.tiles.len()
);
let step = self.tempo_speed(base);
if self.grid.tiles[tile as usize] == TileKind::Pool {
return (step / 2).max(1);
}
step
}
fn tempo_speed(&self, base: u16) -> u16 {
match self.tide.tempo {
Some((Tempo::Fast, _)) => base * 2,
Some((Tempo::Slow, _)) => (base / 2).max(1),
None => base,
}
}
pub fn width(&self) -> u8 {
self.grid.width
}
pub fn height(&self) -> u8 {
self.grid.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.rules.gull_period
}
pub fn gull_cap(&self) -> u8 {
self.rules.gull_cap
}
pub fn gull_turnover(&self) -> bool {
self.rules.gull_turnover
}
pub fn golden_every(&self) -> u32 {
self.rules.golden_every
}
pub fn timed_lock(&self) -> bool {
self.rules.timed_lock
}
pub fn sweep_flight(&self) -> bool {
self.rules.sweep_flight
}
pub fn sweeping(&self) -> &[Sweeping] {
&self.sweeping
}
pub fn golden_call(&self) -> Option<(u64, u8, u8)> {
self.golden_call
.filter(|&(at, _)| at.saturating_sub(self.tick) <= u64::from(GOLDEN_NOTICE))
.map(|(at, tile)| {
let (x, y) = self.coords_u8(tile);
(at, x, y)
})
}
pub fn crab_value(&self, kind: CrabKind) -> u32 {
match (kind, self.rules.golden_every) {
(CrabKind::Golden, 1..) => GOLDEN_CALLED_VALUE,
_ => kind.value(),
}
}
pub fn round_length(&self) -> Option<u32> {
self.rules.round_length
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn remove_crabs_at(&mut self, x: u8, y: u8) {
assert!(
self.in_bounds(i32::from(x), i32::from(y)),
"crab off the board"
);
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) {
assert!(
self.in_bounds(i32::from(x), i32::from(y)),
"gull off the board"
);
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 lure_cooldown(&self) -> u32 {
self.lure_cooldown
}
pub fn in_claw_call(&self) -> bool {
self.tide.claw_call > 0
}
pub fn bank_worth(&self, crab: &Crab) -> Option<u32> {
u32::try_from(crab.bank_points(self.crab_value(crab.kind), self.in_claw_call())).ok()
}
pub fn crabs_banked(&self) -> u32 {
self.crabs_banked
}
pub fn swept_home(&self, crab: u32) -> Option<PlayerId> {
self.swept_home
.iter()
.find(|swept| swept.crab == crab)
.map(|swept| swept.owner)
}
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.grid.tiles[self.index(i32::from(x), i32::from(y)) as usize]
}
pub fn tiles(&self) -> impl Iterator<Item = (u8, u8, TileKind)> + '_ {
let width = self.grid.width;
self.grid
.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.grid.tiles.iter().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 lookahead;
mod signposts;
mod snapshot;
#[cfg(test)]
mod tests;
pub use events::{Mania, Tempo, TideEvent};
pub use signposts::Refusal;
pub use geometry::Grid;
pub(crate) use geometry::Walker;