use std::{error::Error, fmt, ops::Index};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use arrayvec::ArrayVec;
pub type Coords2D = (usize, usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
enum MapType {
Octile,
FourConnected,
}
impl MapType {
fn from_string(s: &str) -> Self {
match s.to_lowercase().as_str() {
"octile" => MapType::Octile,
_ => MapType::FourConnected,
}
}
}
pub trait Map2D<T> {
fn height(&self) -> usize;
fn width(&self) -> usize;
fn get(&self, coords: Coords2D) -> &T;
fn is_out_of_bound(&self, coords: Coords2D) -> bool;
fn is_traversable(&self, tile: Coords2D) -> bool;
fn is_traversable_from(&self, from: Coords2D, to: Coords2D) -> bool;
fn coords(&self) -> CoordsIter;
fn free_states(&self) -> usize;
fn neighbors(&self, tile: Coords2D) -> ArrayVec<Coords2D, 8>;
}
#[derive(Debug)]
pub enum ParseError {
InvalidMapSize,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseError::InvalidMapSize => {
write!(f, "Map size does not match the provided height * width")
}
}
}
}
impl Error for ParseError {}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MovingAiMap {
map_type: MapType,
height: usize,
width: usize,
map: Box<[char]>,
}
impl MovingAiMap {
pub fn new(
map_type: String,
height: usize,
width: usize,
map: Vec<char>,
) -> Result<MovingAiMap, ParseError> {
if map.len() != height * width {
return Err(ParseError::InvalidMapSize);
}
MovingAiMap::new_from_slice(map_type, height, width, map.into_boxed_slice())
}
pub fn new_from_slice(
map_type: String,
height: usize,
width: usize,
map: Box<[char]>,
) -> Result<MovingAiMap, ParseError> {
if map.len() != height * width {
return Err(ParseError::InvalidMapSize);
}
Ok(MovingAiMap {
map_type: MapType::from_string(&map_type),
height,
width,
map,
})
}
fn coordinates_connect(&self, coords_a: Coords2D, coords_b: Coords2D) -> bool {
let (x1, y1) = (coords_a.0 as isize, coords_a.1 as isize);
let (x2, y2) = (coords_b.0 as isize, coords_b.1 as isize);
match self.map_type {
MapType::Octile => (x1 - x2).abs() <= 1 && (y1 - y2).abs() <= 1,
MapType::FourConnected => {
(y2 == y1 && (x2 == x1 + 1 || x2 == x1 - 1))
|| (x2 == x1 && (y2 == y1 + 1 || y2 == y1 - 1))
}
}
}
}
pub struct CoordsIter {
pub width: usize,
pub height: usize,
pub curr_x: usize,
pub curr_y: usize,
}
impl Iterator for CoordsIter {
type Item = Coords2D;
fn next(&mut self) -> Option<Self::Item> {
let x = self.curr_x;
let y = self.curr_y;
if self.curr_y >= self.height {
return None;
}
self.curr_x += 1;
if self.curr_x >= self.width {
self.curr_x = 0;
self.curr_y += 1;
}
Some((x, y))
}
}
impl Map2D<char> for MovingAiMap {
fn height(&self) -> usize {
self.height
}
fn width(&self) -> usize {
self.width
}
fn get(&self, coords: Coords2D) -> &char {
&self.map[coords.1 * self.width() + coords.0]
}
fn is_out_of_bound(&self, coords: Coords2D) -> bool {
coords.0 >= self.width || coords.1 >= self.height
}
fn is_traversable(&self, tile: Coords2D) -> bool {
if self.is_out_of_bound(tile) {
return false;
}
let tile_char = self.get(tile);
match *tile_char {
'.' | 'G' | 'S' | 'W' => true,
'@' | 'O' | 'T' => false,
_ => false, }
}
fn is_traversable_from(&self, from: Coords2D, to: Coords2D) -> bool {
if self.is_out_of_bound(to) {
return false;
}
if self.is_out_of_bound(from) {
return false;
}
if !self.coordinates_connect(to, from) {
return false;
}
let diagonal = from.0 != to.0 && from.1 != to.1;
let tile_char = *(self.get(to));
let from_char = *(self.get(from));
match (self.map_type, diagonal) {
(MapType::FourConnected, _) | (MapType::Octile, false) => {
match (tile_char, from_char) {
('.', _) => true,
('G', _) => true,
('@', _) => false,
('O', _) => false,
('T', _) => false,
('S', '.') => true,
('S', 'S') => true,
('W', 'W') => true,
_ => false,
}
}
(MapType::Octile, true) => {
let (x, y) = from;
let (p, q) = to;
let intermediate_a = (x, q);
let intermediate_b = (p, y);
self.is_traversable_from(from, intermediate_a)
&& self.is_traversable_from(intermediate_a, to)
&& self.is_traversable_from(from, intermediate_b)
&& self.is_traversable_from(intermediate_b, to)
}
}
}
fn coords(&self) -> CoordsIter {
CoordsIter {
width: self.width,
height: self.height,
curr_x: 0,
curr_y: 0,
}
}
fn free_states(&self) -> usize {
self.coords().filter(|c| self.is_traversable(*c)).count()
}
fn neighbors(&self, tile: Coords2D) -> ArrayVec<Coords2D, 8> {
const OFFSETS: [(isize, isize); 8] = [
(1, 0),
(-1, 0),
(0, 1),
(0, -1),
(1, 1),
(1, -1),
(-1, 1),
(-1, -1),
];
let (x, y) = tile;
OFFSETS
.iter()
.filter_map(|&(dx, dy)| {
let nx = x.checked_add_signed(dx)?;
let ny = y.checked_add_signed(dy)?;
Some((nx, ny))
})
.filter(|&neighbor| self.is_traversable_from(tile, neighbor))
.collect()
}
}
impl Index<Coords2D> for MovingAiMap {
type Output = char;
fn index(&self, coords: Coords2D) -> &char {
self.get(coords)
}
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SceneRecord {
pub bucket: u32,
pub map_file: String,
pub map_width: usize,
pub map_height: usize,
pub start_pos: Coords2D,
pub goal_pos: Coords2D,
pub optimal_length: f64,
}