use std::fmt::Write;
use anyhow::{anyhow, Result};
use petgraph::algo::is_isomorphic;
use petgraph::graphmap::GraphMap;
use petgraph::stable_graph::DefaultIx;
use petgraph::Undirected;
use crate::prelude::*;
pub(crate) type MazeGraph = GraphMap<Coordinates, (), Undirected>;
#[derive(Clone)]
pub struct Maze {
pub(crate) graph: MazeGraph,
pub start: Coordinates,
pub goal: Coordinates,
pub size: (i32, i32),
}
impl Maze {
pub(crate) fn new(width: i32, height: i32, start: Coordinates, goal: Coordinates) -> Self {
debug_assert!(width > 0, "maze width should be >0");
debug_assert!(height > 0, "maze height should be >0");
Maze {
graph: GraphMap::with_capacity((width * height) as usize, 0),
size: (width, height),
start,
goal,
}
}
pub fn get_field(&self, coordinates: &Coordinates) -> Option<Field> {
if self.are_coordinates_inside(coordinates) {
let passages: Vec<_> = Direction::all()
.iter()
.filter(|dir| {
self.graph
.contains_edge(*coordinates, coordinates.next(dir))
})
.copied()
.collect();
let field_type = if &self.start == coordinates {
FieldType::Start
} else if &self.goal == coordinates {
FieldType::Goal
} else {
FieldType::Normal
};
Some(Field::new(field_type, *coordinates, passages))
} else {
None
}
}
pub(crate) fn are_coordinates_inside(&self, coordinates: &Coordinates) -> bool {
coordinates.x >= 0
&& coordinates.x < self.size.0
&& coordinates.y >= 0
&& coordinates.y < self.size.1
}
}
impl std::fmt::Debug for Maze {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
for iy in 0..self.size.1 {
for ix in 0..self.size.0 {
f.write_str("ยท")?;
if self
.get_field(&(ix, iy).into())
.ok_or(std::fmt::Error {})?
.has_passage(&Direction::North)
{
f.write_str(" ")?;
} else {
f.write_str("-")?;
}
}
f.write_str("ยท\n")?;
for ix in 0..self.size.0 {
let field = self.get_field(&(ix, iy).into()).ok_or(std::fmt::Error {})?;
if field.has_passage(&Direction::West) {
f.write_str(" ")?;
} else {
f.write_str("|")?;
}
f.write_str(match field.field_type {
FieldType::Start => "S",
FieldType::Goal => "G",
_ => " ",
})?;
}
f.write_str("|\n")?;
if iy == self.size.1 - 1 {
for _ix in 0..self.size.0 {
f.write_str("ยท-")?;
}
f.write_str("ยท\n")?;
}
}
Ok(())
}
}
impl Maze {
pub fn to_svg(&self, svgoptions: SvgOptions) -> Result<String> {
let padding = svgoptions.padding; let markersize = svgoptions.markersize; let mut height = match svgoptions.height {
None => (2 + self.size.1) * padding,
Some(h) => h,
};
let mut width = height * self.size.0 / self.size.1;
let scx = width / self.size.0;
let scx2 = scx / 2;
let scy = height / self.size.1;
let scy2 = scy / 2;
width = scx * self.size.0;
height = scy * self.size.1;
let mut x1;
let mut x2;
let mut y1;
let mut y2;
let mut svg = String::new();
writeln!(svg, "<?xml version=\"1.0\" encoding=\"utf-8\"?>")?;
writeln!(svg, "<svg xmlns=\"http://www.w3.org/2000/svg\"")?;
writeln!(svg, " xmlns:xlink=\"http://www.w3.org/1999/xlink\"")?;
writeln!(
svg,
" width=\"{}\" height=\"{}\" viewBox=\"{} {} {} {}\">",
width + 2 * padding,
height + 2 * padding,
-padding,
-padding,
width + 2 * padding,
height + 2 * padding
)?;
writeln!(svg, "<defs>\n<style type=\"text/css\"><![CDATA[")?;
writeln!(svg, "line {{")?;
writeln!(
svg,
" stroke: {};\n stroke-linecap: square;",
svgoptions.strokecol
)?;
writeln!(svg, " stroke-width: {};\n}}", svgoptions.strokewidth)?;
writeln!(svg, "]]></style>\n</defs>")?;
for iy in 0..self.size.1 {
for ix in 0..self.size.0 {
if self
.get_field(&(ix, iy).into())
.ok_or_else(|| {
anyhow!("Could not get maze field at coordinates {},{}", ix, iy)
})?
.has_passage(&Direction::North)
{
} else {
x1 = ix * scx;
y1 = iy * scy;
x2 = (ix + 1) * scx;
y2 = iy * scy;
writeln!(
svg,
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\"/>",
x1, y1, x2, y2
)?;
}
}
for ix in 0..self.size.0 {
let field = self.get_field(&(ix, iy).into()).ok_or_else(|| {
anyhow!("Could not get maze field at coordinates {},{}", ix, iy)
})?;
if field.has_passage(&Direction::West) {
} else {
x1 = ix * scx;
y1 = iy * scy;
x2 = ix * scx;
y2 = (iy + 1) * scy;
writeln!(
svg,
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\"/>",
x1, y1, x2, y2
)?;
}
match field.field_type {
FieldType::Start => {
x1 = ix * scx + scx2;
y1 = iy * scy + scy2;
writeln!(svg, "<circle cx=\"{}\" cy=\"{}\" r=\"{}\" stroke=\"{}\" stroke-width=\"{}\" fill=\"{}\" />", x1, y1, markersize, svgoptions.startcol, markersize + 1, svgoptions.startcol)?;
}
FieldType::Goal => {
x1 = ix * scx + scx2;
y1 = iy * scy + scy2;
writeln!(svg, "<circle cx=\"{}\" cy=\"{}\" r=\"{}\" stroke=\"{}\" stroke-width=\"{}\" fill=\"{}\" />", x1, y1, markersize, svgoptions.goalcol, markersize + 1, svgoptions.goalcol)?;
}
_ => continue,
};
}
x1 = 0;
y1 = (self.size.1) * scy;
x2 = (self.size.0) * scx;
y2 = (self.size.1) * scy;
writeln!(
svg,
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\"/>",
x1, y1, x2, y2
)?;
x1 = (self.size.0) * scx;
y1 = 0;
x2 = (self.size.0) * scx;
y2 = (self.size.1) * scy;
writeln!(
svg,
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\"/>",
x1, y1, x2, y2
)?;
}
writeln!(svg, "</svg>")?;
Ok(svg)
}
}
impl From<Maze> for MazeGraph {
fn from(m: Maze) -> Self {
m.graph
}
}
impl PartialEq for Maze {
fn eq(&self, other: &Self) -> bool {
self.start == other.start
&& self.goal == other.goal
&& self.size == other.size
&& is_isomorphic(
&self.graph.clone().into_graph::<DefaultIx>(),
&other.graph.clone().into_graph::<DefaultIx>(),
)
}
}
impl Eq for Maze {}