1use crate::sim::board::{Board, PlayerAction, PlayerId, TileKind};
22use crate::sim::crab::{CrabKind, Handedness};
23use crate::sim::direction::Direction;
24use crate::sim::gull::GullState;
25
26#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
30pub enum BotLevel {
31 Easy,
32 #[default]
33 Normal,
34 Hard,
35}
36
37impl BotLevel {
38 fn cadence(self) -> u64 {
40 match self {
41 BotLevel::Easy => 40,
42 BotLevel::Normal => 20,
43 BotLevel::Hard => 12,
44 }
45 }
46
47 fn defend_radius(self) -> i32 {
48 match self {
49 BotLevel::Easy => 2,
50 BotLevel::Normal => 4,
51 BotLevel::Hard => 6,
52 }
53 }
54
55 fn reach(self) -> i32 {
59 match self {
60 BotLevel::Easy | BotLevel::Normal => 0,
61 BotLevel::Hard => 5,
62 }
63 }
64
65 fn jackpot_reach(self) -> i32 {
70 match self {
71 BotLevel::Easy | BotLevel::Normal => 0,
72 BotLevel::Hard => 14,
73 }
74 }
75
76 fn blunder_every(self) -> u64 {
83 match self {
84 BotLevel::Easy => 4,
85 BotLevel::Normal => 8,
86 BotLevel::Hard => 0,
87 }
88 }
89
90 fn values_the_catch(self) -> bool {
94 !matches!(self, BotLevel::Easy)
95 }
96
97 fn cursor_ticks_per_tile(self) -> u64 {
102 match self {
103 BotLevel::Easy => 4,
104 BotLevel::Normal => 3,
105 BotLevel::Hard => 2,
106 }
107 }
108
109 fn recruit_radius(self) -> i32 {
110 match self {
111 BotLevel::Easy => 4,
112 BotLevel::Normal => 7,
113 BotLevel::Hard => 10,
114 }
115 }
116
117 fn reads_terrain(self) -> bool {
120 matches!(self, BotLevel::Hard)
121 }
122
123 fn acts_on(self, player: PlayerId, ticks: u64) -> bool {
134 let cadence = self.cadence();
135 let window = ticks / cadence;
136 let mut z = window.wrapping_mul(0x9E37_79B9_7F4A_7C15)
137 ^ u64::from(player).wrapping_mul(0xBF58_476D_1CE4_E5B9);
138 z ^= z >> 31;
139 z = z.wrapping_mul(0x94D0_49BB_1331_11EB);
140 z ^= z >> 29;
141 ticks % cadence == z % cadence
142 }
143}
144
145const ATTACK_RADIUS: i32 = 6;
147
148const CURSOR_LIFT: u64 = 8;
150
151#[derive(Clone, Copy, PartialEq, Eq, Debug)]
155enum Intent {
156 Defend,
157 Recruit,
158 Attack,
159}
160
161pub fn bot_action(board: &Board, player: PlayerId, level: BotLevel) -> PlayerAction {
168 let (wanted, intent) = decide(board, player, level);
169 let wanted = fumble(wanted, player, level, board.ticks());
170 if let PlayerAction::Place { x, y, .. } = wanted
173 && (!hand_arrived(board, player, level, x, y)
174 || !worth_the_walk(board, player, level, x, y, intent))
175 {
176 return PlayerAction::None;
177 }
178 wanted
179}
180
181fn fumble(action: PlayerAction, player: PlayerId, level: BotLevel, ticks: u64) -> PlayerAction {
185 let every = level.blunder_every();
186 if every == 0 {
187 return action;
188 }
189 if let PlayerAction::Place { x, y, dir } = action
191 && (ticks ^ u64::from(player)).is_multiple_of(every)
192 {
193 return PlayerAction::Place {
194 x,
195 y,
196 dir: dir.right(),
197 };
198 }
199 action
200}
201
202fn worth_the_walk(
212 board: &Board,
213 player: PlayerId,
214 level: BotLevel,
215 x: u8,
216 y: u8,
217 intent: Intent,
218) -> bool {
219 if intent != Intent::Attack {
220 return true;
221 }
222 let Some((from_x, from_y, _)) = board.newest_signpost_of(player) else {
223 return true;
224 };
225 let steps = i32::from(x.abs_diff(from_x)) + i32::from(y.abs_diff(from_y));
226 steps <= level.reach()
227}
228
229fn hand_arrived(board: &Board, player: PlayerId, level: BotLevel, x: u8, y: u8) -> bool {
233 let Some((from_x, from_y, since)) = board.newest_signpost_of(player) else {
234 return true;
235 };
236 let steps = u64::from(x.abs_diff(from_x)) + u64::from(y.abs_diff(from_y));
237 let walk = match steps {
242 0 | 1 => 0,
243 far => CURSOR_LIFT + (far - 1) * level.cursor_ticks_per_tile(),
244 };
245 board.ticks().saturating_sub(since) >= walk
246}
247
248fn decide(board: &Board, player: PlayerId, level: BotLevel) -> (PlayerAction, Intent) {
255 let nothing = (PlayerAction::None, Intent::Recruit);
256 if !level.acts_on(player, board.ticks()) {
257 return nothing;
258 }
259 let Some(castle) = castle_of(board, player) else {
260 return nothing;
261 };
262 defend(board, player, level, castle)
263 .or_else(|| chase_jackpot(board, player, level, castle))
264 .or_else(|| recruit(board, player, level, castle))
265 .or_else(|| attack(board, player, level))
266 .unwrap_or(nothing)
267}
268
269fn defend(
271 board: &Board,
272 player: PlayerId,
273 level: BotLevel,
274 castle: u16,
275) -> Option<(PlayerAction, Intent)> {
276 let mut best: Option<(i32, u16, Direction)> = None;
277 for gull in board.gulls() {
278 if gull.state != GullState::Walking {
279 continue;
280 }
281 let d = manhattan(board, gull.tile, castle);
282 if d > level.defend_radius() {
283 continue;
284 }
285 let closing = board
290 .step(gull.tile, gull.dir)
291 .is_some_and(|next| manhattan(board, next, castle) < d);
292 if closing && best.is_none_or(|(bd, ..)| d < bd) {
293 best = Some((d, gull.tile, gull.dir));
294 }
295 }
296 let (_, tile, dir) = best?;
297 let out = board
303 .step(tile, dir)
304 .filter(|_| level.reads_terrain())
305 .and_then(|target| safe_kelp_shove(board, target, tile, dir, castle))
306 .unwrap_or_else(|| dir.reverse());
307 let action = place_ahead(board, player, tile, dir, out, level)?;
308 Some((action, Intent::Defend))
309}
310
311fn chase_jackpot(
315 board: &Board,
316 player: PlayerId,
317 level: BotLevel,
318 castle: u16,
319) -> Option<(PlayerAction, Intent)> {
320 if level.jackpot_reach() == 0 {
321 return None;
322 }
323 let mut best: Option<(u32, i32, u16, Direction)> = None;
324 for crab in board.crabs() {
325 let worth = match crab.kind {
326 CrabKind::Golden => 50,
327 CrabKind::Molting => 30, CrabKind::Giant => 10,
329 CrabKind::Common | CrabKind::Juvenile | CrabKind::Sparkling => continue,
330 };
331 let d = manhattan(board, crab.tile, castle);
332 if d == 0 || d > level.jackpot_reach() || crab.dir == toward(board, crab.tile, castle) {
333 continue;
334 }
335 if best.is_none_or(|(bw, bd, ..)| worth > bw || (worth == bw && d < bd)) {
336 best = Some((worth, d, crab.tile, crab.dir));
337 }
338 }
339 let (_, _, tile, dir) = best?;
340 let ahead = board.step(tile, dir).unwrap_or(tile);
341 let home = homeward(board, ahead, castle, level, TileKind::Pool);
342 let action = place_ahead(board, player, tile, dir, home, level)?;
343 Some((action, Intent::Recruit))
344}
345
346fn recruit(
348 board: &Board,
349 player: PlayerId,
350 level: BotLevel,
351 castle: u16,
352) -> Option<(PlayerAction, Intent)> {
353 let mut best: Option<(u32, i32, u16, Direction)> = None;
354 for crab in board.crabs() {
355 let d = manhattan(board, crab.tile, castle);
356 if d == 0 || d > level.recruit_radius() {
357 continue;
358 }
359 if crab.dir == toward(board, crab.tile, castle) {
360 continue; }
362 let value = if level.values_the_catch() {
365 crab.kind.value()
366 } else {
367 1
368 };
369 if best.is_none_or(|(bv, bd, ..)| value > bv || (value == bv && d < bd)) {
370 best = Some((value, d, crab.tile, crab.dir));
371 }
372 }
373 let (_, _, tile, dir) = best?;
374 let ahead = board.step(tile, dir).unwrap_or(tile);
375 let home = homeward(board, ahead, castle, level, TileKind::Pool);
376 let action = place_ahead(board, player, tile, dir, home, level)?;
377 Some((action, Intent::Recruit))
378}
379
380fn attack(board: &Board, player: PlayerId, level: BotLevel) -> Option<(PlayerAction, Intent)> {
383 if level != BotLevel::Hard {
384 return None;
385 }
386 let target = leading_rival_castle(board, player)?;
387 for gull in board.gulls() {
388 if gull.state != GullState::Walking {
389 continue;
390 }
391 let d = manhattan(board, gull.tile, target);
392 if d == 0 || d > ATTACK_RADIUS || gull.dir == toward(board, gull.tile, target) {
393 continue;
394 }
395 let ahead = board.step(gull.tile, gull.dir).unwrap_or(gull.tile);
396 let aim = homeward(board, ahead, target, level, TileKind::Kelp);
397 if let Some(action) = place_ahead(board, player, gull.tile, gull.dir, aim, level) {
398 return Some((action, Intent::Attack));
399 }
400 }
401 None
402}
403
404fn leading_rival_castle(board: &Board, player: PlayerId) -> Option<u16> {
406 let scores = board.scores();
407 let mut best: Option<(u32, PlayerId)> = None;
408 for seat in 0..crate::sim::MAX_PLAYERS as PlayerId {
409 if seat == player {
410 continue;
411 }
412 let Some(_) = castle_of(board, seat) else {
413 continue;
414 };
415 if best.is_none_or(|(s, _)| scores[seat as usize] > s) {
416 best = Some((scores[seat as usize], seat));
417 }
418 }
419 best.and_then(|(_, seat)| castle_of(board, seat))
420}
421
422fn castle_of(board: &Board, player: PlayerId) -> Option<u16> {
425 board.castle_of(player).map(|(x, y)| board.index_of(x, y))
426}
427
428fn manhattan(board: &Board, a: u16, b: u16) -> i32 {
429 let (ax, ay) = board.coords(a);
430 let (bx, by) = board.coords(b);
431 (ax - bx).abs() + (ay - by).abs()
432}
433
434fn toward(board: &Board, from: u16, to: u16) -> Direction {
436 let (fx, fy) = board.coords(from);
437 let (tx, ty) = board.coords(to);
438 Direction::toward(tx - fx, ty - fy)
439}
440
441fn cross_toward(board: &Board, from: u16, to: u16) -> Option<Direction> {
444 let (fx, fy) = board.coords(from);
445 let (tx, ty) = board.coords(to);
446 let (dx, dy) = (tx - fx, ty - fy);
447 match toward(board, from, to) {
448 Direction::Left | Direction::Right => (dy != 0).then(|| Direction::toward(0, dy)),
449 Direction::Up | Direction::Down => (dx != 0).then(|| Direction::toward(dx, 0)),
450 }
451}
452
453fn kind_ahead(board: &Board, tile: u16, dir: Direction) -> Option<TileKind> {
455 let target = board.step(tile, dir)?;
456 let (x, y) = board.coords(target);
457 Some(board.tile_at(x as u8, y as u8))
458}
459
460fn gull_passable(board: &Board, tile: u16, dir: Direction) -> bool {
465 let (x, y) = board.coords_u8(tile);
466 !board.wall_at(x, y, dir)
467 && !matches!(
468 kind_ahead(board, tile, dir),
469 None | Some(TileKind::Rock | TileKind::Kelp)
470 )
471}
472
473fn kelp_turn(board: &Board, tile: u16, into_kelp: Direction, handed: Handedness) -> Direction {
477 let (first, second) = match handed {
478 Handedness::Left => (into_kelp.left(), into_kelp.right()),
479 Handedness::Right => (into_kelp.right(), into_kelp.left()),
480 };
481 if gull_passable(board, tile, first) {
482 first
483 } else if gull_passable(board, tile, second) {
484 second
485 } else {
486 into_kelp.reverse()
487 }
488}
489
490fn safe_kelp_shove(
499 board: &Board,
500 target: u16,
501 tile: u16,
502 travel: Direction,
503 castle: u16,
504) -> Option<Direction> {
505 let reversed = manhattan(board, tile, castle);
506 Direction::ALL.into_iter().find(|&dir| {
507 dir != travel
508 && kind_ahead(board, target, dir) == Some(TileKind::Kelp)
509 && [Handedness::Left, Handedness::Right]
510 .into_iter()
511 .all(|handed| {
512 let turned = kelp_turn(board, target, dir, handed);
513 board
514 .step(target, turned)
515 .is_none_or(|next| manhattan(board, next, castle) >= reversed)
516 })
517 })
518}
519
520fn homeward(board: &Board, from: u16, to: u16, level: BotLevel, hazard: TileKind) -> Direction {
525 let greedy = toward(board, from, to);
526 if !level.reads_terrain() || kind_ahead(board, from, greedy) != Some(hazard) {
527 return greedy;
528 }
529 match cross_toward(board, from, to) {
530 Some(other) if kind_ahead(board, from, other) != Some(hazard) => other,
531 _ => greedy,
532 }
533}
534
535fn place_ahead(
538 board: &Board,
539 player: PlayerId,
540 creature_tile: u16,
541 creature_dir: Direction,
542 dir_out: Direction,
543 level: BotLevel,
544) -> Option<PlayerAction> {
545 let target = board.step(creature_tile, creature_dir)?;
546 let (x, y) = board.coords(target);
547 let (x, y) = (x as u8, y as u8);
548 if board.tile_at(x, y) != TileKind::Empty {
549 return None;
550 }
551 if level.reads_terrain()
555 && matches!(
556 kind_ahead(board, target, dir_out),
557 Some(TileKind::Turnstile { .. })
558 )
559 {
560 return None;
561 }
562 match board.signpost_at(x, y) {
563 Some(sp) if sp.owner != player => None,
564 Some(sp) if sp.dir == dir_out => None, _ => Some(PlayerAction::Place { x, y, dir: dir_out }),
566 }
567}
568
569#[cfg(test)]
570mod tests;