use crate::compact_representation::core::CellNum as CN;
use crate::impl_common_board_traits;
use crate::types::*;
use crate::types::{NeighborDeterminableGame, SnakeBodyGettableGame};
use crate::wire_representation::Game;
use itertools::Itertools;
use rand::seq::SliceRandom;
use rand::Rng;
use std::borrow::Borrow;
use std::error::Error;
use std::fmt::Display;
use tracing::instrument;
use crate::{
types::{Move, SimulableGame, SimulatorInstruments},
wire_representation::Position,
};
use super::core::CellBoard as CCB;
use super::core::CellIndex;
use super::core::{simulate_with_moves, EvaluateMode};
use super::dimensions::{ArcadeMaze, Custom, Dimensions, Fixed, Square};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct CellBoard<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize> {
embedded: CCB<T, D, BOARD_SIZE, MAX_SNAKES>,
}
impl_common_board_traits!(CellBoard);
pub type CellBoard4Snakes7x7 = CellBoard<u8, Square, { 7 * 7 }, 4>;
pub type CellBoard4Snakes11x11 = CellBoard<u8, Square, { 11 * 11 }, 4>;
pub type CellBoard8Snakes15x15 = CellBoard<u8, Square, { 15 * 15 }, 8>;
pub type CellBoard8Snakes25x25 = CellBoard<u16, Custom, { 25 * 25 }, 8>;
pub type CellBoard16Snakes50x50 = CellBoard<u16, Custom, { 50 * 50 }, 16>;
impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize>
CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
{
pub fn convert_from_game(game: Game, snake_ids: &SnakeIDMap) -> Result<Self, Box<dyn Error>> {
if game.game.ruleset.name == "wrapped" {
return Err("Wrapped games are not supported".into());
}
let embedded = CCB::convert_from_game(game, snake_ids)?;
Ok(CellBoard { embedded })
}
fn off_board(&self, new_head: Position) -> bool {
new_head.x < 0
|| new_head.x >= self.embedded.get_actual_width() as i32
|| new_head.y < 0
|| new_head.y >= self.embedded.get_actual_height() as i32
}
pub fn get_all_empty(&self) -> impl Iterator<Item = CellIndex<T>> + '_ {
self.embedded.get_empty_cells()
}
}
impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize>
RandomReasonableMovesGame for CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
{
fn random_reasonable_move_for_each_snake<'a>(
&'a self,
rng: &'a mut impl Rng,
) -> Box<dyn std::iter::Iterator<Item = (SnakeId, Move)> + 'a> {
Box::new(
self.reasonable_moves_for_each_snake()
.map(move |(sid, mvs)| (sid, *mvs.choose(rng).unwrap())),
)
}
}
impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize> ReasonableMovesGame
for CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
{
fn reasonable_moves_for_each_snake(
&self,
) -> Box<dyn std::iter::Iterator<Item = (SnakeId, Vec<Move>)> + '_> {
let width = self.embedded.get_actual_width();
Box::new(
self.embedded
.iter_healths()
.enumerate()
.filter(|(_, health)| **health > 0)
.map(move |(idx, _)| {
let head_pos = self.get_head_as_position(&SnakeId(idx as u8));
let mvs = IntoIterator::into_iter(Move::all())
.filter(|mv| {
let new_head = head_pos.add_vec(mv.to_vector());
let ci = CellIndex::new(new_head, width);
!self.off_board(new_head)
&& (!self.embedded.cell_is_body(ci)
|| self.embedded.cell_is_single_tail(ci))
&& !self.embedded.cell_is_snake_head(ci)
})
.collect_vec();
let mvs = if mvs.is_empty() { vec![Move::Up] } else { mvs };
(SnakeId(idx as u8), mvs)
}),
)
}
}
impl<
T: SimulatorInstruments,
D: Dimensions,
N: CN,
const BOARD_SIZE: usize,
const MAX_SNAKES: usize,
> SimulableGame<T, MAX_SNAKES> for CellBoard<N, D, BOARD_SIZE, MAX_SNAKES>
{
#[allow(clippy::type_complexity)]
#[instrument(level = "trace", skip_all)]
fn simulate_with_moves<S>(
&self,
instruments: &T,
snake_ids_and_moves: impl IntoIterator<Item = (Self::SnakeIDType, S)>,
) -> Box<dyn Iterator<Item = (Action<MAX_SNAKES>, Self)> + '_>
where
S: Borrow<[Move]>,
{
Box::new(
simulate_with_moves(
&self.embedded,
instruments,
snake_ids_and_moves,
EvaluateMode::Standard,
)
.map(|v| {
let (action, board) = v;
(action, Self { embedded: board })
}),
)
}
}
impl<T: CN, D: Dimensions, const BOARD_SIZE: usize, const MAX_SNAKES: usize>
NeighborDeterminableGame for CellBoard<T, D, BOARD_SIZE, MAX_SNAKES>
{
fn possible_moves<'a>(
&'a self,
pos: &Self::NativePositionType,
) -> Box<(dyn std::iter::Iterator<Item = (Move, CellIndex<T>)> + 'a)> {
let width = self.embedded.get_actual_width();
let head_pos = pos.into_position(width);
Box::new(
Move::all_iter()
.map(move |mv| {
let new_head = head_pos.add_vec(mv.to_vector());
let ci = CellIndex::new(new_head, width);
(mv, new_head, ci)
})
.filter(move |(_mv, new_head, _)| !self.off_board(*new_head))
.map(|(mv, _, ci)| (mv, ci)),
)
}
fn neighbors<'a>(
&'a self,
pos: &Self::NativePositionType,
) -> Box<(dyn Iterator<Item = CellIndex<T>> + 'a)> {
let width = self.embedded.get_actual_width();
let head_pos = pos.into_position(width);
Box::new(
Move::all_iter()
.map(move |mv| {
let new_head = head_pos.add_vec(mv.to_vector());
let ci = CellIndex::new(new_head, width);
(new_head, ci)
})
.filter(move |(new_head, _)| !self.off_board(*new_head))
.map(|(_, ci)| ci),
)
}
}
#[derive(Debug)]
pub enum BestCellBoard {
Tiny(Box<CellBoard4Snakes7x7>),
SmallExact(Box<CellBoard<u8, Fixed<7, 7>, { 7 * 7 }, 4>>),
Standard(Box<CellBoard4Snakes11x11>),
MediumExact(Box<CellBoard<u8, Fixed<11, 11>, { 11 * 11 }, 4>>),
LargestU8(Box<CellBoard8Snakes15x15>),
LargeExact(Box<CellBoard<u16, Fixed<19, 19>, { 19 * 19 }, 4>>),
ArcadeMaze(Box<CellBoard<u16, ArcadeMaze, { 19 * 21 }, 4>>),
ArcadeMaze8Snake(Box<CellBoard<u16, ArcadeMaze, { 19 * 21 }, 8>>),
Large(Box<CellBoard8Snakes25x25>),
Silly(Box<CellBoard16Snakes50x50>),
}
pub trait ToBestCellBoard {
#[allow(missing_docs)]
fn to_best_cell_board(self) -> Result<BestCellBoard, Box<dyn Error>>;
}
impl ToBestCellBoard for Game {
fn to_best_cell_board(self) -> Result<BestCellBoard, Box<dyn Error>> {
let width = self.board.width;
let height = self.board.height;
let num_snakes = self.board.snakes.len();
let id_map = build_snake_id_map(&self);
let best_board = if width == 7 && height == 7 && num_snakes <= 4 {
BestCellBoard::SmallExact(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width <= 7 && height <= 7 && num_snakes <= 4 {
BestCellBoard::Tiny(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width == 11 && height == 11 && num_snakes <= 4 {
BestCellBoard::MediumExact(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width <= 11 && height <= 11 && num_snakes <= 4 {
BestCellBoard::Standard(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width <= 15 && height <= 15 && num_snakes <= 8 {
BestCellBoard::LargestU8(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width == 19 && height == 19 && num_snakes <= 4 {
BestCellBoard::LargeExact(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width == 19 && height == 21 && num_snakes <= 4 {
BestCellBoard::ArcadeMaze(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width == 19 && height == 21 && num_snakes <= 8 {
BestCellBoard::ArcadeMaze8Snake(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width <= 25 && height < 25 && num_snakes <= 8 {
BestCellBoard::Large(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else if width <= 50 && height <= 50 && num_snakes <= 16 {
BestCellBoard::Silly(Box::new(CellBoard::convert_from_game(self, &id_map)?))
} else {
panic!("No board was big enough")
};
Ok(best_board)
}
}
#[cfg(test)]
mod test {
use itertools::Itertools;
use super::*;
use crate::{
compact_representation::core::Cell, game_fixture, types::build_snake_id_map,
wire_representation::Game as DEGame,
};
#[derive(Debug)]
struct Instruments;
impl SimulatorInstruments for Instruments {
fn observe_simulation(&self, _: std::time::Duration) {}
}
#[test]
fn test_compact_board_conversion() {
let start_of_game_fixture =
game_fixture(include_str!("../../../fixtures/start_of_game.json"));
let converted = Game::to_best_cell_board(start_of_game_fixture);
assert!(converted.is_ok());
let u = converted.unwrap();
match u {
BestCellBoard::MediumExact(_) => {}
_ => panic!("expected standard board"),
}
let tiny_board = game_fixture(include_str!("../../../fixtures/7x7board.json"));
let converted = Game::to_best_cell_board(tiny_board);
assert!(converted.is_ok());
let u = converted.unwrap();
match u {
BestCellBoard::SmallExact(_) => {}
_ => panic!("expected standard board"),
}
let non_standard_small_board =
game_fixture(include_str!("../../../fixtures/8x8board.json"));
let converted = Game::to_best_cell_board(non_standard_small_board);
assert!(converted.is_ok());
let u = converted.unwrap();
match u {
BestCellBoard::Standard(_) => {}
_ => panic!("expected standard board"),
}
}
#[test]
fn test_head_gettable() {
let game_fixture = include_str!("../../../fixtures/late_stage.json");
let g: Result<DEGame, _> = serde_json::from_slice(game_fixture.as_bytes());
let g = g.expect("the json literal is valid");
let snake_id_mapping = build_snake_id_map(&g);
let compact: CellBoard4Snakes11x11 = g.as_cell_board(&snake_id_mapping).unwrap();
assert_eq!(
compact.get_head_as_position(&SnakeId(0)),
Position { x: 4, y: 6 }
);
assert_eq!(
compact.get_head_as_native_position(&SnakeId(0)),
CellIndex(6 * 11 + 4)
);
}
#[test]
fn test_tail_collision() {
let game_fixture = include_str!("../../../fixtures/start_of_game.json");
let g: Result<DEGame, _> = serde_json::from_slice(game_fixture.as_bytes());
let g = g.expect("the json literal is valid");
let snake_id_mapping = build_snake_id_map(&g);
let mut compact: CellBoard4Snakes11x11 = g.as_cell_board(&snake_id_mapping).unwrap();
let moves = [
Move::Left,
Move::Down,
Move::Right,
Move::Up,
Move::Left,
Move::Down,
];
let instruments = Instruments;
eprintln!("{}", compact);
for mv in moves {
let res = compact
.simulate_with_moves(&instruments, vec![(SnakeId(0), [mv].as_slice())])
.collect_vec();
compact = res[0].1;
eprintln!("{}", compact);
}
assert!(compact.get_health(&SnakeId(0)) > 0);
}
#[test]
fn test_set_hazard() {
let mut c: Cell<u8> = Cell::empty();
c.set_food();
assert!(c.is_food());
c.set_hazard();
assert!(c.is_food());
assert!(c.is_hazard());
assert!(!c.is_head());
assert!(!c.is_body());
}
#[test]
fn test_clear_hazard() {
let mut c: Cell<u8> = Cell::empty();
c.set_food();
assert!(c.is_food());
c.set_hazard();
c.clear_hazard();
assert!(c.is_food());
assert!(!c.is_hazard());
assert!(!c.is_head());
assert!(!c.is_body());
let mut c: Cell<u8> = Cell::make_double_stacked_piece(SnakeId(0), CellIndex(0));
c.set_hazard();
c.clear_hazard();
assert!(c.is_body());
assert!(!c.is_hazard());
}
#[test]
fn test_remove() {
let mut c: Cell<u8> = Cell::make_body_piece(SnakeId(3), CellIndex(17));
c.remove();
c.set_hazard();
assert!(c.is_empty());
assert!(c.is_hazard());
assert!(c.get_snake_id().is_none());
assert!(c.get_idx() == CellIndex(0));
}
#[test]
fn test_set_food() {
let mut c: Cell<u8> = Cell::empty();
c.set_food();
c.set_hazard();
assert!(c.is_food());
assert!(c.is_hazard());
assert!(c.get_snake_id().is_none());
assert!(c.get_idx() == CellIndex(0));
}
#[test]
fn test_set_head() {
let mut c: Cell<u8> = Cell::empty();
c.set_head(SnakeId(3), CellIndex(17));
c.set_hazard();
assert!(c.is_head());
assert!(c.is_hazard());
assert!(c.get_snake_id().unwrap() == SnakeId(3));
assert!(c.get_idx() == CellIndex(17));
}
#[test]
fn test_food_queryable() {
let game_fixture = include_str!("../../../fixtures/late_stage.json");
let g: Result<DEGame, _> = serde_json::from_slice(game_fixture.as_bytes());
let g = g.expect("the json literal is valid");
let snake_id_mapping = build_snake_id_map(&g);
let compact: CellBoard4Snakes11x11 = g.as_cell_board(&snake_id_mapping).unwrap();
assert!(!compact.is_food(&CellIndex(6 * 11 + 4)));
assert!(compact.is_food(&CellIndex(2 * 11)));
assert!(compact.is_food(&CellIndex(9 * 11)));
assert!(compact.is_food(&CellIndex(3 * 11 + 4)));
}
#[test]
fn test_neighbors_and_possible_moves_start_of_game() {
let game_fixture = include_str!("../../../fixtures/start_of_game.json");
let g: Result<DEGame, _> = serde_json::from_slice(game_fixture.as_bytes());
let g = g.expect("the json literal is valid");
let snake_id_mapping = build_snake_id_map(&g);
let compact: CellBoard4Snakes11x11 = g.as_cell_board(&snake_id_mapping).unwrap();
let head = compact.get_head_as_native_position(&SnakeId(0));
assert_eq!(head, CellIndex(8 * 11 + 5));
let expected_possible_moves = vec![
(Move::Up, CellIndex(9 * 11 + 5)),
(Move::Down, CellIndex(7 * 11 + 5)),
(Move::Left, CellIndex(8 * 11 + 4)),
(Move::Right, CellIndex(8 * 11 + 6)),
];
assert_eq!(
compact.possible_moves(&head).collect::<Vec<_>>(),
expected_possible_moves
);
assert_eq!(
compact.neighbors(&head).collect::<Vec<_>>(),
expected_possible_moves
.into_iter()
.map(|(_, pos)| pos)
.collect::<Vec<_>>()
);
}
#[test]
fn test_neighbors_and_possible_moves_cornered() {
let game_fixture = include_str!("../../../fixtures/cornered.json");
let g: Result<DEGame, _> = serde_json::from_slice(game_fixture.as_bytes());
let g = g.expect("the json literal is valid");
let snake_id_mapping = build_snake_id_map(&g);
let compact: CellBoard4Snakes11x11 = g.as_cell_board(&snake_id_mapping).unwrap();
let head = compact.get_head_as_native_position(&SnakeId(0));
assert_eq!(head, CellIndex(10 * 11));
let expected_possible_moves = vec![
(Move::Down, CellIndex(9 * 11)),
(Move::Right, CellIndex(10 * 11 + 1)),
];
assert_eq!(
compact.possible_moves(&head).collect::<Vec<_>>(),
expected_possible_moves
);
assert_eq!(
compact.neighbors(&head).collect::<Vec<_>>(),
expected_possible_moves
.into_iter()
.map(|(_, pos)| pos)
.collect::<Vec<_>>()
);
}
#[test]
fn test_tail_chase() {
let game_fixture = include_str!("../../../fixtures/tail_chase.json");
let g: Result<DEGame, _> = serde_json::from_slice(game_fixture.as_bytes());
let g = g.expect("the json literal is valid");
let snake_id_mapping = build_snake_id_map(&g);
let compact: CellBoard4Snakes11x11 = g.as_cell_board(&snake_id_mapping).unwrap();
let head = compact.get_head_as_native_position(&SnakeId(0));
assert_eq!(head, CellIndex(0));
let mut reasonable_moves = compact.reasonable_moves_for_each_snake();
let reasonable_moves_for_me = reasonable_moves.next().unwrap().1;
assert_eq!(reasonable_moves_for_me, vec![Move::Up]);
}
}