1use crate::impl_common_board_traits;
3use crate::types::*;
4
5use crate::types::{NeighborDeterminableGame, SnakeBodyGettableGame};
8use crate::wire_representation::Game;
9use itertools::Itertools;
10use rand::seq::SliceRandom;
11use rand::Rng;
12use std::borrow::Borrow;
13use std::collections::HashMap;
14use std::error::Error;
15use std::fmt::Display;
16
17use crate::{
18 types::{Action, Move, SimulableGame, SimulatorInstruments},
19 wire_representation::Position,
20};
21
22use super::core::{simulate_with_moves, EvaluateMode};
23use super::core::{CellBoard as CCB, CellIndex};
24use super::dimensions::{ArcadeMaze, Custom, Dimensions, Fixed, Square};
25use super::CellNum as CN;
26
27#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
30pub struct CellBoard<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize> {
31 embedded: CCB<T, D, BOARD_SIZE, MAX_SNAKES>,
32}
33
34impl_common_board_traits!(CellBoard);
35
36impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize>
37 CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
38{
39 pub fn assert_consistency(&self) -> bool {
41 self.embedded.assert_consistency()
42 }
43
44 pub fn convert_from_game(game: Game, snake_ids: &SnakeIDMap) -> Result<Self, Box<dyn Error>> {
46 if game.game.ruleset.name != "wrapped" {
47 return Err("only wrapped games are supported".into());
48 }
49 let embedded = CCB::convert_from_game(game, snake_ids)?;
50 Ok(CellBoard { embedded })
51 }
52
53 pub fn pack_as_hash(&self) -> HashMap<String, Vec<u32>> {
55 self.embedded.pack_as_hash()
56 }
57
58 pub fn from_packed_hash(hash: &HashMap<String, Vec<u32>>) -> Self {
60 Self {
61 embedded: CCB::from_packed_hash(hash),
62 }
63 }
64}
65
66pub type CellBoard4SnakesSquare7x7 = CellBoard<u8, Square, { 7 * 7 }, 4>;
68
69pub type CellBoard4SnakesSquare11x11 = CellBoard<u8, Square, { 11 * 11 }, 4>;
71
72pub type CellBoard8SnakesSquare15x15 = CellBoard<u8, Square, { 15 * 15 }, 8>;
75
76pub type CellBoard8SnakesSquare25x25 = CellBoard<u16, Custom, { 25 * 25 }, 8>;
78
79pub type CellBoard16SnakesSquare50x50 = CellBoard<u16, Custom, { 50 * 50 }, 16>;
81
82#[derive(Debug)]
84pub enum BestCellBoard {
85 Tiny(Box<CellBoard4SnakesSquare7x7>),
87 SmallExact(Box<CellBoard<u8, Fixed<7, 7>, { 7 * 7 }, 4>>),
89 Standard(Box<CellBoard4SnakesSquare11x11>),
91 MediumExact(Box<CellBoard<u8, Fixed<11, 11>, { 11 * 11 }, 4>>),
93 LargestU8(Box<CellBoard8SnakesSquare15x15>),
95 LargeExact(Box<CellBoard<u16, Fixed<19, 19>, { 19 * 19 }, 4>>),
97 ArcadeMaze(Box<CellBoard<u16, ArcadeMaze, { 19 * 21 }, 4>>),
99 ArcadeMaze8Snake(Box<CellBoard<u16, ArcadeMaze, { 19 * 21 }, 8>>),
101 Large(Box<CellBoard8SnakesSquare25x25>),
103 Silly(Box<CellBoard16SnakesSquare50x50>),
105}
106
107pub trait ToBestCellBoard {
112 #[allow(missing_docs)]
113 fn to_best_cell_board(self) -> Result<BestCellBoard, Box<dyn Error>>;
114}
115
116impl ToBestCellBoard for Game {
117 fn to_best_cell_board(self) -> Result<BestCellBoard, Box<dyn Error>> {
118 let width = self.board.width;
119 let height = self.board.height;
120 let num_snakes = self.board.snakes.len();
121 let id_map = build_snake_id_map(&self);
122
123 let best_board = if width == 7 && height == 7 && num_snakes <= 4 {
124 BestCellBoard::SmallExact(Box::new(CellBoard::convert_from_game(self, &id_map)?))
125 } else if width <= 7 && height <= 7 && num_snakes <= 4 {
126 BestCellBoard::Tiny(Box::new(CellBoard::convert_from_game(self, &id_map)?))
127 } else if width == 11 && height == 11 && num_snakes <= 4 {
128 BestCellBoard::MediumExact(Box::new(CellBoard::convert_from_game(self, &id_map)?))
129 } else if width <= 11 && height <= 11 && num_snakes <= 4 {
130 BestCellBoard::Standard(Box::new(CellBoard::convert_from_game(self, &id_map)?))
131 } else if width <= 15 && height <= 15 && num_snakes <= 8 {
132 BestCellBoard::LargestU8(Box::new(CellBoard::convert_from_game(self, &id_map)?))
133 } else if width == 19 && height == 19 && num_snakes <= 4 {
134 BestCellBoard::LargeExact(Box::new(CellBoard::convert_from_game(self, &id_map)?))
135 } else if width == 19 && height == 21 && num_snakes <= 4 {
136 BestCellBoard::ArcadeMaze(Box::new(CellBoard::convert_from_game(self, &id_map)?))
137 } else if width == 19 && height == 21 && num_snakes <= 8 {
138 BestCellBoard::ArcadeMaze8Snake(Box::new(CellBoard::convert_from_game(self, &id_map)?))
139 } else if width <= 25 && height < 25 && num_snakes <= 8 {
140 BestCellBoard::Large(Box::new(CellBoard::convert_from_game(self, &id_map)?))
141 } else if width <= 50 && height <= 50 && num_snakes <= 16 {
142 BestCellBoard::Silly(Box::new(CellBoard::convert_from_game(self, &id_map)?))
143 } else {
144 panic!("No board was big enough")
145 };
146
147 Ok(best_board)
148 }
149}
150
151impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize>
152 RandomReasonableMovesGame for CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
153{
154 fn random_reasonable_move_for_each_snake<'a>(
155 &'a self,
156 rng: &'a mut impl Rng,
157 ) -> Box<dyn std::iter::Iterator<Item = (SnakeId, Move)> + 'a> {
158 Box::new(
159 self.reasonable_moves_for_each_snake()
160 .map(move |(sid, mvs)| (sid, *mvs.choose(rng).unwrap())),
161 )
162 }
163}
164
165impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize> ReasonableMovesGame
166 for CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
167{
168 fn reasonable_moves_for_each_snake(
169 &self,
170 ) -> Box<dyn std::iter::Iterator<Item = (SnakeId, Vec<Move>)> + '_> {
171 let width = self.embedded.get_actual_width();
172 Box::new(
173 self.embedded
174 .iter_healths()
175 .enumerate()
176 .filter(|(_, health)| **health > 0)
177 .map(move |(idx, _)| {
178 let head_pos = self.get_head_as_position(&SnakeId(idx as u8));
179
180 let mvs = IntoIterator::into_iter(Move::all())
181 .filter(|mv| {
182 let mut new_head = head_pos.add_vec(mv.to_vector());
183 let wrapped_x = new_head.x.rem_euclid(self.get_width() as i32);
184 let wrapped_y = new_head.y.rem_euclid(self.get_height() as i32);
185
186 new_head = Position {
187 x: wrapped_x,
188 y: wrapped_y,
189 };
190
191 let ci = CellIndex::new(new_head, width);
192
193 if self.off_board(new_head) {
194 return false;
195 };
196
197 !self.off_board(new_head)
198 && ((!self.embedded.cell_is_body(ci)
199 && !self.embedded.cell_is_snake_head(ci))
200 || self.embedded.cell_is_single_tail(ci))
201 })
202 .collect_vec();
203 let mvs = if mvs.is_empty() { vec![Move::Up] } else { mvs };
204
205 (SnakeId(idx as u8), mvs)
206 }),
207 )
208 }
209}
210
211impl<
212 T: SimulatorInstruments,
213 N: CN,
214 D: Dimensions,
215 const BOARD_SIZE: usize,
216 const MAX_SNAKES: usize,
217 > SimulableGame<T, MAX_SNAKES> for CellBoard<N, D, BOARD_SIZE, MAX_SNAKES>
218{
219 #[allow(clippy::type_complexity)]
220 fn simulate_with_moves<S>(
221 &self,
222 instruments: &T,
223 snake_ids_and_moves: impl IntoIterator<Item = (Self::SnakeIDType, S)>,
224 ) -> Box<dyn Iterator<Item = (Action<MAX_SNAKES>, Self)> + '_>
225 where
226 S: Borrow<[Move]>,
227 {
228 Box::new(
229 simulate_with_moves(
230 &self.embedded,
231 instruments,
232 snake_ids_and_moves,
233 EvaluateMode::Wrapped,
234 )
235 .map(|v| {
236 let (action, board) = v;
237 (action, Self { embedded: board })
238 }),
239 )
240 }
241}
242
243impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize>
244 NeighborDeterminableGame for CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
245{
246 fn possible_moves<'a>(
247 &'a self,
248 pos: &Self::NativePositionType,
249 ) -> Box<(dyn std::iter::Iterator<Item = (Move, CellIndex<T>)> + 'a)> {
250 let width = self.embedded.get_actual_width();
251 let head_pos = pos.into_position(width);
252
253 Box::new(
254 Move::all_iter()
255 .map(move |mv| {
256 let new_head = head_pos.add_vec(mv.to_vector());
257 let ci = self.embedded.as_wrapped_cell_index(new_head);
258
259 debug_assert!(!self.embedded.off_board(ci.into_position(width)));
260
261 (mv, new_head, ci)
262 })
263 .map(|(mv, _, ci)| (mv, ci)),
264 )
265 }
266
267 fn neighbors<'a>(
268 &'a self,
269 pos: &Self::NativePositionType,
270 ) -> Box<(dyn Iterator<Item = CellIndex<T>> + 'a)> {
271 Box::new(self.possible_moves(pos).map(|(_, ci)| ci))
272 }
273}
274
275#[cfg(test)]
276mod test {
277 use std::collections::HashMap;
278
279 use itertools::Itertools;
280 use rand::{RngCore, SeedableRng};
281
282 use crate::{
283 compact_representation::core::Cell,
284 game_fixture,
285 types::{
286 build_snake_id_map, HeadGettableGame, HealthGettableGame, Move,
287 NeighborDeterminableGame, RandomReasonableMovesGame, ReasonableMovesGame,
288 SimulableGame, SimulatorInstruments, SnakeId,
289 },
290 wire_representation::Position,
291 };
292
293 use super::{CellBoard4SnakesSquare11x11, CellIndex};
294
295 #[derive(Debug)]
296 struct Instruments {}
297
298 impl SimulatorInstruments for Instruments {
299 fn observe_simulation(&self, _: std::time::Duration) {}
300 }
301
302 #[test]
303 fn test_to_hash_round_trips() {
304 let g = game_fixture(include_str!("../../../fixtures/wrapped_fixture.json"));
305 eprintln!("{}", g.board);
306 let snake_ids = build_snake_id_map(&g);
307 let orig_wrapped_cell: CellBoard4SnakesSquare11x11 =
308 g.as_wrapped_cell_board(&snake_ids).unwrap();
309 let hash = orig_wrapped_cell.pack_as_hash();
310 eprintln!("{}", serde_json::to_string(&hash).unwrap());
311 eprintln!(
312 "{}",
313 serde_json::to_string(
314 &CellBoard4SnakesSquare11x11::from_packed_hash(&hash).pack_as_hash()
315 )
316 .unwrap()
317 );
318 assert_eq!(
319 CellBoard4SnakesSquare11x11::from_packed_hash(&hash),
320 orig_wrapped_cell
321 );
322 }
323
324 #[test]
325 fn test_cell_round_trips() {
326 let mut c: Cell<u8> = Cell::empty();
327 c.set_body_piece(SnakeId(3), CellIndex::new(Position::new(1, 2), 11));
328 let as_u32 = c.pack_as_u32();
329 assert_eq!(c, Cell::from_u32(as_u32));
330 }
331
332 #[test]
333 fn test_wrapping_simulation_works() {
334 let g = game_fixture(include_str!("../../../fixtures/wrapped_fixture.json"));
335 eprintln!("{}", g.board);
336 let snake_ids = build_snake_id_map(&g);
337 let orig_wrapped_cell: CellBoard4SnakesSquare11x11 =
338 g.as_wrapped_cell_board(&snake_ids).unwrap();
339 let mut rng = rand::thread_rng();
340 run_move_test(
341 orig_wrapped_cell,
342 snake_ids.clone(),
343 11 * 2 + (rng.next_u32() % 20) as i32,
344 0,
345 1,
346 Move::Up,
347 );
348
349 let move_map = snake_ids
351 .values()
352 .cloned()
353 .map(|sid| (sid, [Move::Right].as_slice()))
354 .collect_vec();
355
356 let instruments = Instruments {};
357 let wrapped_for_down = orig_wrapped_cell
358 .clone()
359 .simulate_with_moves(&instruments, move_map)
360 .next()
361 .unwrap()
362 .1;
363 run_move_test(
364 wrapped_for_down,
365 snake_ids.clone(),
366 11 * 2 + (rng.next_u32() % 20) as i32,
367 0,
368 -1,
369 Move::Down,
370 );
371
372 run_move_test(
373 orig_wrapped_cell,
374 snake_ids.clone(),
375 11 * 2 + (rng.next_u32() % 20) as i32,
376 -1,
377 0,
378 Move::Left,
379 );
380 run_move_test(
381 orig_wrapped_cell,
382 snake_ids,
383 11 * 2 + (rng.next_u32() % 20) as i32,
384 1,
385 0,
386 Move::Right,
387 );
388
389 let mut wrapped = orig_wrapped_cell;
390 let mut rng = rand::rngs::SmallRng::from_entropy();
391 for _ in 0..15 {
392 let move_map = wrapped
393 .random_reasonable_move_for_each_snake(&mut rng)
394 .into_iter()
395 .map(|(sid, mv)| (sid, [mv]))
396 .collect_vec();
397 wrapped = wrapped
398 .simulate_with_moves(
399 &instruments,
400 move_map.iter().map(|(sid, mv)| (*sid, mv.as_slice())),
401 )
402 .collect_vec()[0]
403 .1;
404 }
405 assert!(wrapped.get_health(&SnakeId(0)) as i32 > 0);
406 assert!(wrapped.get_health(&SnakeId(1)) as i32 > 0);
407 }
408
409 fn run_move_test(
410 orig_wrapped_cell: super::CellBoard4SnakesSquare11x11,
411 snake_ids: HashMap<String, SnakeId>,
412 rollout: i32,
413 inc_x: i32,
414 inc_y: i32,
415 mv: Move,
416 ) {
417 let mut wrapped_cell = orig_wrapped_cell;
418 let instruments = Instruments {};
419 let start_health = wrapped_cell.get_health(&SnakeId(0));
420 let move_map = snake_ids.into_values().map(|sid| (sid, [mv])).collect_vec();
421 let start_y = wrapped_cell.get_head_as_position(&SnakeId(0)).y;
422 let start_x = wrapped_cell.get_head_as_position(&SnakeId(0)).x;
423 for _ in 0..rollout {
424 wrapped_cell = wrapped_cell
425 .simulate_with_moves(
426 &instruments,
427 move_map
428 .iter()
429 .map(|(sid, mv)| (*sid, mv.as_slice()))
430 .clone(),
431 )
432 .collect_vec()[0]
433 .1;
434 }
435 let end_y = wrapped_cell.get_head_as_position(&SnakeId(0)).y;
436 let end_x = wrapped_cell.get_head_as_position(&SnakeId(0)).x;
437 assert_eq!(
438 wrapped_cell.get_health(&SnakeId(0)) as i32,
439 start_health as i32 - rollout
440 );
441 assert_eq!(((start_y + (rollout * inc_y)).rem_euclid(11)), end_y);
442 assert_eq!(((start_x + (rollout * inc_x)).rem_euclid(11)), end_x);
443 }
444
445 #[test]
446 fn test_wrapped_panic() {
447 let orig_crash_game = game_fixture(include_str!("../../../fixtures/wrapped_panic.json"));
452 let snake_ids = build_snake_id_map(&orig_crash_game);
453 let compact_ids: Vec<SnakeId> = snake_ids.values().cloned().collect();
454
455 let instruments = Instruments {};
456 {
457 let json_hash = include_str!("../../../fixtures/crash_json_hash.json");
459 let hm = serde_json::from_str(json_hash).unwrap();
460 let game = super::CellBoard4SnakesSquare11x11::from_packed_hash(&hm);
461 eprintln!("{}", orig_crash_game.board);
462 dbg!(&compact_ids);
463 let snakes_and_moves = compact_ids.iter().map(|id| (*id, vec![Move::Up]));
464 let mut results = game
465 .simulate_with_moves(&instruments, snakes_and_moves)
466 .collect_vec();
467 assert!(results.len() == 1);
468 let (mvs, g) = results.pop().unwrap();
469 dbg!(mvs);
470 g.assert_consistency();
471 g.simulate(&instruments, compact_ids.clone()).for_each(drop);
472 }
473
474 {
475 let snakes_and_moves = vec![
476 (SnakeId(0), [Move::Up].as_slice()),
477 (SnakeId(1), [Move::Right].as_slice()),
478 (SnakeId(2), [Move::Up].as_slice()),
479 (SnakeId(3), [Move::Up].as_slice()),
480 ];
481 let json_hash = include_str!("../../../fixtures/another_wraped_panic_serialized.json");
482 let hm = serde_json::from_str(json_hash).unwrap();
483 let game = super::CellBoard4SnakesSquare11x11::from_packed_hash(&hm);
484 game.assert_consistency();
485 eprintln!(
486 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!1\n{}",
487 game
488 );
489 let mut results = game
490 .simulate_with_moves(&instruments, snakes_and_moves)
491 .collect_vec();
492 assert!(results.len() == 1);
493 let (mvs, g) = results.pop().unwrap();
494 dbg!(mvs);
495 eprintln!("{}", g);
496 g.assert_consistency();
497 g.simulate(&instruments, compact_ids.clone()).for_each(drop);
498 }
499 {
500 let snakes_and_moves = vec![
501 (SnakeId(0), [Move::Down].as_slice()),
502 (SnakeId(1), [Move::Left].as_slice()),
503 (SnakeId(2), [Move::Up].as_slice()),
504 ];
505 let json_hash = include_str!("../../../fixtures/another_wrapped_panic.json");
506 let hm = serde_json::from_str(json_hash).unwrap();
507 let game = super::CellBoard4SnakesSquare11x11::from_packed_hash(&hm);
508 game.assert_consistency();
509 eprintln!(
510 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!1\n{}",
511 game
512 );
513 let mut results = game
514 .simulate_with_moves(&instruments, snakes_and_moves)
515 .collect_vec();
516 assert!(results.len() == 1);
517 let (mvs, g) = results.pop().unwrap();
518 dbg!(mvs);
519 eprintln!("{}", g);
520 assert_eq!(g.get_health(&SnakeId(0)), 0);
522 assert_eq!(g.get_health(&SnakeId(1)), 0);
523 g.assert_consistency();
524 g.simulate(&instruments, compact_ids).for_each(drop);
525 }
526 }
527
528 #[test]
529 fn test_neighbors_and_possible_moves_cornered() {
530 let g = game_fixture(include_str!("../../../fixtures/cornered_wrapped.json"));
531 let snake_id_mapping = build_snake_id_map(&g);
532 let compact: CellBoard4SnakesSquare11x11 =
533 g.as_wrapped_cell_board(&snake_id_mapping).unwrap();
534
535 let head = compact.get_head_as_native_position(&SnakeId(0));
536 assert_eq!(head, CellIndex(10 * 11));
537
538 let expected_possible_moves = vec![
539 (Move::Up, CellIndex(0)),
540 (Move::Down, CellIndex(9 * 11)),
541 (Move::Left, CellIndex(10 * 11 + 10)),
542 (Move::Right, CellIndex(10 * 11 + 1)),
543 ];
544
545 assert_eq!(
546 compact.possible_moves(&head).collect::<Vec<_>>(),
547 expected_possible_moves
548 );
549
550 assert_eq!(
551 compact.neighbors(&head).collect::<Vec<_>>(),
552 expected_possible_moves
553 .into_iter()
554 .map(|(_, pos)| pos)
555 .collect::<Vec<_>>()
556 );
557 }
558
559 #[test]
560 fn reasonable_moves_for_each_snake_mojave_12_18_12_34() {
561 let g = game_fixture(include_str!("../../../fixtures/mojave_12_18_12_34.json"));
562 let snake_id_mapping = build_snake_id_map(&g);
563 let compact: CellBoard4SnakesSquare11x11 =
564 g.as_wrapped_cell_board(&snake_id_mapping).unwrap();
565
566 let moves = compact.reasonable_moves_for_each_snake().collect_vec();
567
568 assert_eq!(
569 moves,
570 vec![
571 (SnakeId(0), vec![Move::Up, Move::Down]),
572 (SnakeId(1), vec![Move::Down, Move::Left, Move::Right]),
573 ]
574 );
575 }
576}