battlesnake_game_types/compact_representation/wrapped/
mod.rs

1//! A compact board representation that is efficient for simulation
2use crate::impl_common_board_traits;
3use crate::types::*;
4
5/// you almost certainly want to use the `convert_from_game` method to
6/// cast from a json represention to a `CellBoard`
7use 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/// A compact board representation that is significantly faster for simulation than
28/// `battlesnake_game_types::wire_representation::Game`.
29#[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    /// Asserts that the board is consistent (e.g. no snake holes)
40    pub fn assert_consistency(&self) -> bool {
41        self.embedded.assert_consistency()
42    }
43
44    /// creates a wrapped board from a Wire Representation game
45    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    /// for debugging, packs this board into a custom json representation
54    pub fn pack_as_hash(&self) -> HashMap<String, Vec<u32>> {
55        self.embedded.pack_as_hash()
56    }
57
58    /// for debugging, unloads a board from a custom json representation
59    pub fn from_packed_hash(hash: &HashMap<String, Vec<u32>>) -> Self {
60        Self {
61            embedded: CCB::from_packed_hash(hash),
62        }
63    }
64}
65
66/// 7x7 board with 4 snakes
67pub type CellBoard4SnakesSquare7x7 = CellBoard<u8, Square, { 7 * 7 }, 4>;
68
69/// Used to represent the standard 11x11 game with up to 4 snakes.
70pub type CellBoard4SnakesSquare11x11 = CellBoard<u8, Square, { 11 * 11 }, 4>;
71
72/// Used to represent the a 15x15 board with up to 4 snakes. This is the biggest board size that
73/// can still use u8s
74pub type CellBoard8SnakesSquare15x15 = CellBoard<u8, Square, { 15 * 15 }, 8>;
75
76/// Used to represent the largest UI Selectable board with 8 snakes.
77pub type CellBoard8SnakesSquare25x25 = CellBoard<u16, Custom, { 25 * 25 }, 8>;
78
79/// Used to represent an absolutely silly game board
80pub type CellBoard16SnakesSquare50x50 = CellBoard<u16, Custom, { 50 * 50 }, 16>;
81
82/// Enum that holds a Cell Board sized right for the given game
83#[derive(Debug)]
84pub enum BestCellBoard {
85    /// A game that can have a max height and width of 7x7 and 4 snakes
86    Tiny(Box<CellBoard4SnakesSquare7x7>),
87    /// A exactly 7x7 board with 4 snakes
88    SmallExact(Box<CellBoard<u8, Fixed<7, 7>, { 7 * 7 }, 4>>),
89    /// A game that can have a max height and width of 11x11 and 4 snakes
90    Standard(Box<CellBoard4SnakesSquare11x11>),
91    /// A exactly 11x11 board with 4 snakes
92    MediumExact(Box<CellBoard<u8, Fixed<11, 11>, { 11 * 11 }, 4>>),
93    /// A game that can have a max height and width of 15x15 and 4 snakes
94    LargestU8(Box<CellBoard8SnakesSquare15x15>),
95    /// A exactly 19x19 board with 4 snakes
96    LargeExact(Box<CellBoard<u16, Fixed<19, 19>, { 19 * 19 }, 4>>),
97    /// A board that fits the Arcade Maze map
98    ArcadeMaze(Box<CellBoard<u16, ArcadeMaze, { 19 * 21 }, 4>>),
99    /// A board that fits the Arcade Maze map
100    ArcadeMaze8Snake(Box<CellBoard<u16, ArcadeMaze, { 19 * 21 }, 8>>),
101    /// A game that can have a max height and width of 25x25 and 8 snakes
102    Large(Box<CellBoard8SnakesSquare25x25>),
103    /// A game that can have a max height and width of 50x50 and 16 snakes
104    Silly(Box<CellBoard16SnakesSquare50x50>),
105}
106
107/// Trait to get the best sized cellboard for the given game. It returns the smallest Compact board
108/// that has enough room to fit the given Wire game. If the game can't fit in any of our Compact
109/// boards we panic. However the largest board available is MUCH larger than the biggest selectable
110/// board in the Battlesnake UI
111pub 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        // the input state isn't safe to move down in, but it is if we move one to the right
350        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        //        {"lengths":[9,0,19,0],"healths":[61,0,88,0],"hazard_damage":[0],"cells":[655361,5,5,5,5,5,5,5,5,5,589825,1,720897,786433,851969,5,5,5,5,5,1376257,917510,5,5,5,5,5,5,5,5,5,5,5,2818561,2163201,2228737,2294273,2359814,2425345,5,5,5,5,3539457,2949633,3670529,5,5,5,2490881,5,5,5,5,2884097,4260353,3604993,4,5,5,3211777,3932673,3998209,4063745,4129281,4194817,5,5,5,5,5,5,5,5,5,5,5,5,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,5,5,5,5,5,5,5],"heads":[21,0,37,0],"actual_width":[11]}
448        //
449        // this panic was because we were simulating a snake with zero health, which is always consistent because
450        // we essentially "break" the snake in the cell representation when we kill it.
451        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            // this json fixture is the frame at which we crashed, and it comes from a deep forward simulation of orig_crash_game
458            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            // head to head collision of 0 and 1 here
521            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}