use crate::sim::board::{Board, PlayerAction, PlayerId, TileKind};
use crate::sim::crab::{CrabKind, Handedness};
use crate::sim::direction::Direction;
use crate::sim::gull::GullState;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum BotLevel {
Easy,
#[default]
Normal,
Hard,
}
impl BotLevel {
fn cadence(self) -> u64 {
match self {
BotLevel::Easy => 40,
BotLevel::Normal => 20,
BotLevel::Hard => 12,
}
}
fn defend_radius(self) -> i32 {
match self {
BotLevel::Easy => 2,
BotLevel::Normal => 4,
BotLevel::Hard => 6,
}
}
fn reach(self) -> i32 {
match self {
BotLevel::Easy | BotLevel::Normal => 0,
BotLevel::Hard => 5,
}
}
fn jackpot_reach(self) -> i32 {
match self {
BotLevel::Easy | BotLevel::Normal => 0,
BotLevel::Hard => 14,
}
}
fn blunder_every(self) -> u64 {
match self {
BotLevel::Easy => 4,
BotLevel::Normal => 8,
BotLevel::Hard => 0,
}
}
fn values_the_catch(self) -> bool {
!matches!(self, BotLevel::Easy)
}
fn cursor_ticks_per_tile(self) -> u64 {
match self {
BotLevel::Easy => 4,
BotLevel::Normal => 3,
BotLevel::Hard => 2,
}
}
fn recruit_radius(self) -> i32 {
match self {
BotLevel::Easy => 4,
BotLevel::Normal => 7,
BotLevel::Hard => 10,
}
}
fn reads_terrain(self) -> bool {
matches!(self, BotLevel::Hard)
}
fn acts_on(self, player: PlayerId, ticks: u64) -> bool {
let cadence = self.cadence();
let window = ticks / cadence;
let mut z = window.wrapping_mul(0x9E37_79B9_7F4A_7C15)
^ u64::from(player).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^= z >> 31;
z = z.wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 29;
ticks % cadence == z % cadence
}
}
const ATTACK_RADIUS: i32 = 6;
const CURSOR_LIFT: u64 = 8;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Intent {
Defend,
Recruit,
Attack,
}
pub fn bot_action(board: &Board, player: PlayerId, level: BotLevel) -> PlayerAction {
let (wanted, intent) = decide(board, player, level);
let wanted = fumble(wanted, player, level, board.ticks());
if let PlayerAction::Place { x, y, .. } = wanted
&& (!hand_arrived(board, player, level, x, y)
|| !worth_the_walk(board, player, level, x, y, intent))
{
return PlayerAction::None;
}
wanted
}
fn fumble(action: PlayerAction, player: PlayerId, level: BotLevel, ticks: u64) -> PlayerAction {
let every = level.blunder_every();
if every == 0 {
return action;
}
if let PlayerAction::Place { x, y, dir } = action
&& (ticks ^ u64::from(player)).is_multiple_of(every)
{
return PlayerAction::Place {
x,
y,
dir: dir.right(),
};
}
action
}
fn worth_the_walk(
board: &Board,
player: PlayerId,
level: BotLevel,
x: u8,
y: u8,
intent: Intent,
) -> bool {
if intent != Intent::Attack {
return true;
}
let Some((from_x, from_y, _)) = board.newest_signpost_of(player) else {
return true;
};
let steps = i32::from(x.abs_diff(from_x)) + i32::from(y.abs_diff(from_y));
steps <= level.reach()
}
fn hand_arrived(board: &Board, player: PlayerId, level: BotLevel, x: u8, y: u8) -> bool {
let Some((from_x, from_y, since)) = board.newest_signpost_of(player) else {
return true;
};
let steps = u64::from(x.abs_diff(from_x)) + u64::from(y.abs_diff(from_y));
let walk = match steps {
0 | 1 => 0,
far => CURSOR_LIFT + (far - 1) * level.cursor_ticks_per_tile(),
};
board.ticks().saturating_sub(since) >= walk
}
fn decide(board: &Board, player: PlayerId, level: BotLevel) -> (PlayerAction, Intent) {
let nothing = (PlayerAction::None, Intent::Recruit);
if !level.acts_on(player, board.ticks()) {
return nothing;
}
let Some(castle) = castle_of(board, player) else {
return nothing;
};
defend(board, player, level, castle)
.or_else(|| chase_jackpot(board, player, level, castle))
.or_else(|| recruit(board, player, level, castle))
.or_else(|| attack(board, player, level))
.unwrap_or(nothing)
}
fn defend(
board: &Board,
player: PlayerId,
level: BotLevel,
castle: u16,
) -> Option<(PlayerAction, Intent)> {
let mut best: Option<(i32, u16, Direction)> = None;
for gull in board.gulls() {
if gull.state != GullState::Walking {
continue;
}
let d = manhattan(board, gull.tile, castle);
if d > level.defend_radius() {
continue;
}
let closing = board
.step(gull.tile, gull.dir)
.is_some_and(|next| manhattan(board, next, castle) < d);
if closing && best.is_none_or(|(bd, ..)| d < bd) {
best = Some((d, gull.tile, gull.dir));
}
}
let (_, tile, dir) = best?;
let out = board
.step(tile, dir)
.filter(|_| level.reads_terrain())
.and_then(|target| safe_kelp_shove(board, target, tile, dir, castle))
.unwrap_or_else(|| dir.reverse());
let action = place_ahead(board, player, tile, dir, out, level)?;
Some((action, Intent::Defend))
}
fn chase_jackpot(
board: &Board,
player: PlayerId,
level: BotLevel,
castle: u16,
) -> Option<(PlayerAction, Intent)> {
if level.jackpot_reach() == 0 {
return None;
}
let mut best: Option<(u32, i32, u16, Direction)> = None;
for crab in board.crabs() {
let worth = match crab.kind {
CrabKind::Golden => 50,
CrabKind::Molting => 30, CrabKind::Giant => 10,
CrabKind::Common | CrabKind::Juvenile | CrabKind::Sparkling => continue,
};
let d = manhattan(board, crab.tile, castle);
if d == 0 || d > level.jackpot_reach() || crab.dir == toward(board, crab.tile, castle) {
continue;
}
if best.is_none_or(|(bw, bd, ..)| worth > bw || (worth == bw && d < bd)) {
best = Some((worth, d, crab.tile, crab.dir));
}
}
let (_, _, tile, dir) = best?;
let ahead = board.step(tile, dir).unwrap_or(tile);
let home = homeward(board, ahead, castle, level, TileKind::Pool);
let action = place_ahead(board, player, tile, dir, home, level)?;
Some((action, Intent::Recruit))
}
fn recruit(
board: &Board,
player: PlayerId,
level: BotLevel,
castle: u16,
) -> Option<(PlayerAction, Intent)> {
let mut best: Option<(u32, i32, u16, Direction)> = None;
for crab in board.crabs() {
let d = manhattan(board, crab.tile, castle);
if d == 0 || d > level.recruit_radius() {
continue;
}
if crab.dir == toward(board, crab.tile, castle) {
continue; }
let value = if level.values_the_catch() {
crab.kind.value()
} else {
1
};
if best.is_none_or(|(bv, bd, ..)| value > bv || (value == bv && d < bd)) {
best = Some((value, d, crab.tile, crab.dir));
}
}
let (_, _, tile, dir) = best?;
let ahead = board.step(tile, dir).unwrap_or(tile);
let home = homeward(board, ahead, castle, level, TileKind::Pool);
let action = place_ahead(board, player, tile, dir, home, level)?;
Some((action, Intent::Recruit))
}
fn attack(board: &Board, player: PlayerId, level: BotLevel) -> Option<(PlayerAction, Intent)> {
if level != BotLevel::Hard {
return None;
}
let target = leading_rival_castle(board, player)?;
for gull in board.gulls() {
if gull.state != GullState::Walking {
continue;
}
let d = manhattan(board, gull.tile, target);
if d == 0 || d > ATTACK_RADIUS || gull.dir == toward(board, gull.tile, target) {
continue;
}
let ahead = board.step(gull.tile, gull.dir).unwrap_or(gull.tile);
let aim = homeward(board, ahead, target, level, TileKind::Kelp);
if let Some(action) = place_ahead(board, player, gull.tile, gull.dir, aim, level) {
return Some((action, Intent::Attack));
}
}
None
}
fn leading_rival_castle(board: &Board, player: PlayerId) -> Option<u16> {
let scores = board.scores();
let mut best: Option<(u32, PlayerId)> = None;
for seat in 0..crate::sim::MAX_PLAYERS as PlayerId {
if seat == player {
continue;
}
let Some(_) = castle_of(board, seat) else {
continue;
};
if best.is_none_or(|(s, _)| scores[seat as usize] > s) {
best = Some((scores[seat as usize], seat));
}
}
best.and_then(|(_, seat)| castle_of(board, seat))
}
fn castle_of(board: &Board, player: PlayerId) -> Option<u16> {
board.castle_of(player).map(|(x, y)| board.index_of(x, y))
}
fn manhattan(board: &Board, a: u16, b: u16) -> i32 {
let (ax, ay) = board.coords(a);
let (bx, by) = board.coords(b);
(ax - bx).abs() + (ay - by).abs()
}
fn toward(board: &Board, from: u16, to: u16) -> Direction {
let (fx, fy) = board.coords(from);
let (tx, ty) = board.coords(to);
Direction::toward(tx - fx, ty - fy)
}
fn cross_toward(board: &Board, from: u16, to: u16) -> Option<Direction> {
let (fx, fy) = board.coords(from);
let (tx, ty) = board.coords(to);
let (dx, dy) = (tx - fx, ty - fy);
match toward(board, from, to) {
Direction::Left | Direction::Right => (dy != 0).then(|| Direction::toward(0, dy)),
Direction::Up | Direction::Down => (dx != 0).then(|| Direction::toward(dx, 0)),
}
}
fn kind_ahead(board: &Board, tile: u16, dir: Direction) -> Option<TileKind> {
let target = board.step(tile, dir)?;
let (x, y) = board.coords(target);
Some(board.tile_at(x as u8, y as u8))
}
fn gull_passable(board: &Board, tile: u16, dir: Direction) -> bool {
let (x, y) = board.coords_u8(tile);
!board.wall_at(x, y, dir)
&& !matches!(
kind_ahead(board, tile, dir),
None | Some(TileKind::Rock | TileKind::Kelp)
)
}
fn kelp_turn(board: &Board, tile: u16, into_kelp: Direction, handed: Handedness) -> Direction {
let (first, second) = match handed {
Handedness::Left => (into_kelp.left(), into_kelp.right()),
Handedness::Right => (into_kelp.right(), into_kelp.left()),
};
if gull_passable(board, tile, first) {
first
} else if gull_passable(board, tile, second) {
second
} else {
into_kelp.reverse()
}
}
fn safe_kelp_shove(
board: &Board,
target: u16,
tile: u16,
travel: Direction,
castle: u16,
) -> Option<Direction> {
let reversed = manhattan(board, tile, castle);
Direction::ALL.into_iter().find(|&dir| {
dir != travel
&& kind_ahead(board, target, dir) == Some(TileKind::Kelp)
&& [Handedness::Left, Handedness::Right]
.into_iter()
.all(|handed| {
let turned = kelp_turn(board, target, dir, handed);
board
.step(target, turned)
.is_none_or(|next| manhattan(board, next, castle) >= reversed)
})
})
}
fn homeward(board: &Board, from: u16, to: u16, level: BotLevel, hazard: TileKind) -> Direction {
let greedy = toward(board, from, to);
if !level.reads_terrain() || kind_ahead(board, from, greedy) != Some(hazard) {
return greedy;
}
match cross_toward(board, from, to) {
Some(other) if kind_ahead(board, from, other) != Some(hazard) => other,
_ => greedy,
}
}
fn place_ahead(
board: &Board,
player: PlayerId,
creature_tile: u16,
creature_dir: Direction,
dir_out: Direction,
level: BotLevel,
) -> Option<PlayerAction> {
let target = board.step(creature_tile, creature_dir)?;
let (x, y) = board.coords(target);
let (x, y) = (x as u8, y as u8);
if board.tile_at(x, y) != TileKind::Empty {
return None;
}
if level.reads_terrain()
&& matches!(
kind_ahead(board, target, dir_out),
Some(TileKind::Turnstile { .. })
)
{
return None;
}
match board.signpost_at(x, y) {
Some(sp) if sp.owner != player => None,
Some(sp) if sp.dir == dir_out => None, _ => Some(PlayerAction::Place { x, y, dir: dir_out }),
}
}
#[cfg(test)]
mod tests;