initial commit

This commit is contained in:
JOLIMAITRE Matthieu 2022-05-30 00:33:36 +03:00
commit 08592261b3
12 changed files with 822 additions and 0 deletions

170
src/algorithm.rs Normal file
View file

@ -0,0 +1,170 @@
//! ## Algorithm
//!
//! This module contains the definition of the [`Algorithm`] trait, implemented by [`Maze`] resolution strategies.
//! Already existing implementations of that trait can be found in the [`crate::implementations`] module.
//!
use std::{
collections::{HashMap, HashSet},
thread,
time::Duration,
};
use crate::{Maze, Pos};
/// A guess to pass to the current [`Executor`] at the end of every `progress` call.
pub struct Guess(Vec<Pos>);
/// An insight given to the [`Algorithm`] on every `progress` call.
/// On the first time about the starting point and every consecutive call about the tail of the previous guess.
pub struct Insight<'p> {
position: Pos,
paths: &'p [Pos],
}
impl<'p> Insight<'p> {
fn new(position: Pos, paths: &'p [Pos]) -> Self {
Self { paths, position }
}
fn from_position(position: Pos, maze: &'p Maze) -> Self {
let paths = maze.paths_from(position);
Self::new(position, paths)
}
/// The position of the insight.
pub fn position(&self) -> Pos {
self.position
}
/// the paths from that position.
pub fn paths(&self) -> &[Pos] {
self.paths
}
}
/// A context given to the [`Algorithm`] on every `progress` call, provide informations about the maze and method to create a [`Guess`].
pub struct Context<'m> {
maze: &'m Maze,
}
impl<'m> Context<'m> {
fn new(maze: &'m Maze) -> Self {
Self { maze }
}
/// Constructor for [`Guess`].
/// Takes a path, that is a vector of positions from the starting point to the position to discover on the next call to `progress`.
pub fn guess(&self, pos: Vec<Pos>) -> Guess {
Guess(pos)
}
/// Returns the position of the `start` of the [`Maze`].
pub fn start(&self) -> Pos {
self.maze.start()
}
/// Returns the position of the `end` of the [`Maze`].
pub fn end(&self) -> Pos {
self.maze.end()
}
/// Returns the `width` of the [`Maze`].
pub fn width(&self) -> isize {
self.maze.width()
}
/// Returns the `height` of the [`Maze`].
pub fn height(&self) -> isize {
self.maze.width()
}
/// Returns a tuple containing both the `width` and `height` of the [`Maze`].
pub fn size(&self) -> (isize, isize) {
self.maze.size()
}
}
/// Trait encapsulating the behavior of an algorithm solving mazes.
/// Implementing this trait is done by providing a `progress` method which gets called iteratively on each steps of a [`Maze`] resolution.
pub trait Algorithm {
/// will be called on each step of the traversal of the [`Maze`].
/// `insight` is a view on the position discovered on the previous movement.
/// `ctx` is a view on the [`Maze`], useful for accessing properties of the maze.
fn progress(&mut self, insight: &Insight, ctx: &mut Context) -> Guess;
}
/// A structure holding a [`Maze`] and iteratively solving it with a provided [`Algorithm`].
pub struct Executor<Algo>
where
Algo: Algorithm,
{
maze: Maze,
algorithm: Algo,
}
impl<A> Executor<A>
where
A: Algorithm,
{
/// Constructor.
pub fn new(maze: Maze, algorithm: A) -> Self {
Self { maze, algorithm }
}
/// Submit the maze to the [`Algorithm`] and iteratively progress through the maze driven by said algorithm.
pub fn run(&mut self) {
let Self { maze, algorithm } = self;
let mut insight = Insight::from_position(maze.start(), &maze);
let mut tick = 0;
let mut tried = HashSet::new();
loop {
let mut context = Context::new(maze);
let Guess(guess) = algorithm.progress(&insight, &mut context);
// TODO:
// - extract metrics from the context
// - check if path is actually a path
guess.iter().for_each(|&p| {
tried.insert(p);
});
let tail = *guess.last().expect("returned an empty path");
// draw
Self::draw(maze, &tried, tick, &guess);
thread::sleep(Duration::from_millis(100));
tick += 1;
// check for next iteration
if maze.is_end(tail) {
break;
} else {
insight = Insight::from_position(tail, maze)
}
}
}
fn draw(maze: &Maze, tried: &HashSet<Pos>, tick: usize, path: &Vec<Pos>) {
let mut overlay = HashMap::new();
for position in tried {
overlay.insert(*position, 'T');
}
for position in path {
overlay.insert(*position, '#');
}
overlay.insert(*path.last().unwrap(), 'G');
let text = maze.display(Some(overlay));
// DIRTY!
// print the frame on top of the previous one
if tick > 0 {
let count = text.lines().count() + 1;
let up = termion::cursor::Up(count as u16);
print!("{up}")
}
print!("tick {tick}:\n{text}\n");
}
}

29
src/implementations.rs Normal file
View file

@ -0,0 +1,29 @@
//! ## Implementations
//!
//! This module contains concrete types implementing the [`crate::Algorithm`] trait.
//! They drive the resolution of a [`crate::Maze`] through different means.
//!
mod breath_first;
mod depth_first;
pub use breath_first::BreathFirst;
pub use depth_first::DepthFirst;
#[test]
fn depth_first() {
use crate::{algorithm::Executor, generate};
let algorithm = DepthFirst::new();
let maze = generate(20, 20);
let mut executor = Executor::new(maze, algorithm);
executor.run();
}
#[test]
fn breath_first() {
use crate::{algorithm::Executor, generate};
let algorithm = BreathFirst::new();
let maze = generate(20, 20);
let mut executor = Executor::new(maze, algorithm);
executor.run();
}

View file

@ -0,0 +1,45 @@
use std::collections::{HashSet, VecDeque};
use crate::{
algorithm::{Context, Guess, Insight},
Algorithm, Pos,
};
/// [`Algorithm`] traversing the [`crate::Maze`] as a common graph.
/// Storing each possible paths form shortest to longest and extending the shortest ones first.
/// Most effective when the resolution is among the shortest possible paths.
pub struct BreathFirst {
paths: VecDeque<Vec<Pos>>,
visited: HashSet<Pos>,
last_path: Vec<Pos>,
}
impl BreathFirst {
/// Constructor.
pub fn new() -> Self {
Self {
paths: VecDeque::new(),
visited: HashSet::new(),
last_path: Vec::new(),
}
}
}
impl Algorithm for BreathFirst {
fn progress(&mut self, insight: &Insight, ctx: &mut Context) -> Guess {
self.visited.insert(insight.position());
let path = self.last_path.clone();
for &branch in insight.paths() {
if self.visited.contains(&branch) {
continue;
}
let mut new_path = path.clone();
new_path.push(branch);
self.paths.push_back(new_path);
}
let path = self.paths.pop_front().expect("no more options");
self.last_path = path.clone();
ctx.guess(path)
}
}

View file

@ -0,0 +1,55 @@
use std::collections::HashSet;
use crate::{
algorithm::{Context, Guess, Insight},
Algorithm, Pos,
};
/// Frame of the stack used by a [`DepthFirst`] to retain its path and possible branches.
pub struct Frame {
position: Pos,
remaining_branches: Vec<Pos>,
}
/// [`Algorithm`] driving the resolution of a [`crate::Maze`] traversing it as a common graph in a depth-first fashion.
/// Stores the current path and possible branches in a stack.
pub struct DepthFirst {
visited: HashSet<Pos>,
stack: Vec<Frame>,
}
impl DepthFirst {
/// constructor.
pub fn new() -> Self {
Self {
visited: HashSet::new(),
stack: Vec::new(),
}
}
}
impl Algorithm for DepthFirst {
fn progress(&mut self, insight: &Insight, ctx: &mut Context) -> Guess {
let position = insight.position();
let branches = insight.paths().iter().cloned().collect();
self.visited.insert(position);
self.stack.push(Frame {
position,
remaining_branches: branches,
});
loop {
let last = self.stack.last_mut().expect("no more options");
if let Some(branch) = last.remaining_branches.pop() {
if !self.visited.contains(&branch) {
let mut path: Vec<_> = self.stack.iter().map(|f| f.position).collect();
path.push(branch);
return ctx.guess(path);
}
} else {
self.stack.pop();
}
}
}
}

218
src/labyrinth.rs Normal file
View file

@ -0,0 +1,218 @@
//! ## Labyrinth
//!
//! This module contains the data structure representing a maze for the rest of the library.
use std::collections::HashMap;
use crate::Pos;
/// Data structure representing a maze on a grid.
/// stores each possible paths as a [`HashMap`] mapping each positions to the accessible adjascent ones.
#[derive(Debug, Clone)]
pub struct Maze {
width: isize,
height: isize,
start: Pos,
end: Pos,
paths: HashMap<Pos, Vec<Pos>>,
}
impl Maze {
/// Constructor.
pub fn new(
width: isize,
height: isize,
start: Pos,
end: Pos,
paths_: Vec<(Pos, Vec<Pos>)>,
) -> Self {
let mut paths = HashMap::new();
for y in 0..height {
for x in 0..width {
paths.insert((x, y).into(), Vec::new());
}
}
let mut result = Self {
width,
height,
start,
end,
paths,
};
for (position, accessibles) in paths_ {
for accessible in accessibles {
result.create_path(position, accessible);
}
}
result
}
fn create_path(&mut self, position_a: Pos, position_b: Pos) {
self.paths
.get_mut(&position_a)
.expect("position out of bounds")
.push(position_b); // warning: mutation before all preconditions are checked
self.paths
.get_mut(&position_b)
.expect("position out of bounds")
.push(position_a);
}
/// Width of the [`Maze`].
pub fn width(&self) -> isize {
self.width
}
/// Height of the [`Maze`].
pub fn height(&self) -> isize {
self.height
}
/// Tuple containing both the width and height of the [`Maze`].
pub fn size(&self) -> (isize, isize) {
(self.width(), self.height())
}
/// The starting position of the [`Maze`].
pub fn start(&self) -> Pos {
self.start
}
/// The ending position of the [`Maze`].
pub fn end(&self) -> Pos {
self.end
}
/// Check if the provided position is the start of the [`Maze`].
pub fn is_start(&self, position: Pos) -> bool {
self.start() == position
}
/// Check if the provided position is the end of the [`Maze`].
pub fn is_end(&self, position: Pos) -> bool {
self.end() == position
}
/// Returns an array of all positions directly accessible from a position in the [`Maze`].
pub fn paths_from(&self, position: Pos) -> &[Pos] {
let accessibles = self.paths.get(&position).expect("position out of bounds");
accessibles
}
/// Check if a position is included within the [`Maze`].
pub fn is_inside(&self, position: Pos) -> bool {
let (x, y) = position.decompose();
x >= 0 && x < self.width() && y >= 0 && y < self.height()
}
/// Returns adjascent positions of `position` that are included in the [`Maze`].
pub fn adjascent(&self, position: Pos) -> Vec<Pos> {
let (x, y) = position.decompose();
[(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]
.into_iter()
.map(|t| t.into())
.filter(|&p| self.is_inside(p))
.collect()
}
/// Check if there is a wall between two adjascent positions in the [`Maze`].
pub fn is_walled(&self, position_a: Pos, position_b: Pos) -> bool {
self.paths_from(position_a)
.iter()
.find(|&&p| p == position_b)
.is_none()
}
/// return a string representing the [`Maze`].
pub fn display(&self, overlay: Option<HashMap<Pos, char>>) -> String {
// output
let mut out: Vec<Vec<_>> = (0..(self.height() * 2 + 1))
.map(|_| (0..(self.width() * 2 + 1)).map(|_| ' ').collect())
.collect();
// outer walls
for x in 0..self.width() {
let mapped_x = (x * 2 + 1) as usize;
out.first_mut().unwrap()[mapped_x] = '─';
out.last_mut().unwrap()[mapped_x] = '─';
}
for y in 0..self.height() {
let mapped_y = (y * 2 + 1) as usize;
*out[mapped_y].first_mut().unwrap() = '│';
*out[mapped_y].last_mut().unwrap() = '│';
}
// vertical walls
for y in 0..self.height() {
for x in 1..self.width() {
let current_cell = (x, y).into();
let left_cell = current_cell - (1, 0).into();
if self.is_walled(left_cell, current_cell) {
let mapped_y = (2 * y + 1) as usize;
let mapped_x = (2 * x) as usize;
out[mapped_y][mapped_x] = '│'
}
}
}
// horizontal walls
for y in 1..self.height() {
for x in 0..self.width() {
let current_cell = (x, y).into();
let upper_cell = current_cell - (0, 1).into();
if self.is_walled(upper_cell, current_cell) {
let mapped_x = (2 * x + 1) as usize;
let mapped_y = (2 * y) as usize;
out[mapped_y][mapped_x] = '─';
}
}
}
// corners
for y in 0..(self.height() + 1) {
for x in 0..(self.width() + 1) {
let mapped_x = (2 * x) as usize;
let mapped_y = (2 * y) as usize;
out[mapped_y][mapped_x] = '•';
}
}
// overlay
if let Some(overlay) = overlay {
for (position, character) in overlay {
let (x, y) = position.decompose();
let mapped_x = (x * 2 + 1) as usize;
let mapped_y = (y * 2 + 1) as usize;
out[mapped_y][mapped_x] = character;
}
}
out.into_iter()
.map(|line| line.into_iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
}
#[test]
fn display() {
let maze = Maze::new(
3,
3,
(0, 0).into(),
(2, 2).into(),
vec![
((0, 0).into(), vec![(1, 0).into(), (0, 1).into()]),
((1, 0).into(), vec![(1, 1).into()]),
((1, 1).into(), vec![(1, 2).into()]),
((1, 2).into(), vec![(2, 2).into()]),
],
);
let text = maze.display(None);
println!("{text}");
}
pub mod generator;

View file

@ -0,0 +1,46 @@
//! ## Generator
//!
//! This module contains raw functions generating mazes.
use std::collections::HashSet;
use rand::{prelude::SliceRandom, thread_rng};
use crate::{Maze, Pos};
/// Most common maze generation technique, recursively creating paths to unvisited cells, each time choosing next direction at random.
pub fn generate(width: isize, height: isize) -> Maze {
let mut result = Maze::new(
width,
height,
Pos::zero(),
(width - 1, height - 1).into(),
Vec::new(),
);
fn recursive(current: Pos, result: &mut Maze, visited: &mut HashSet<Pos>) {
visited.insert(current);
let mut adjascent_positions = result.adjascent(current);
adjascent_positions.shuffle(&mut thread_rng());
for neighbor in adjascent_positions {
if visited.contains(&neighbor) {
continue;
}
result.create_path(current, neighbor);
recursive(neighbor, result, visited);
}
}
let mut visited = HashSet::new();
let current = Pos::zero();
recursive(current, &mut result, &mut visited);
result
}
#[test]
fn generation() {
let maze = generate(20, 20);
let text = maze.display(None);
println!("{text}");
}

16
src/lib.rs Normal file
View file

@ -0,0 +1,16 @@
//! # Labirust
//!
//! This crate is a small and naive implementation of several [`Algorithm`]s resolving [`Maze`]s.
//!
//! * It exposes the [`Algorithm`] trait encapsulating the behavior of such an algorithm.
//! * It also provides structures to generate [`Maze`] ([`generate`]) and execute said algorithms on them ([`Executor`]).
//!
mod algorithm;
pub mod implementations;
mod labyrinth;
mod position;
pub use algorithm::{Algorithm, Executor};
pub use labyrinth::{generator::generate, Maze};
pub use position::Pos;

86
src/position.rs Normal file
View file

@ -0,0 +1,86 @@
//! ## Position
//!
//! This module contains the definition of the [`Pos`] type, used to represent positions in the rest of the library;
use std::ops::{Add, Mul, Sub};
/// A discrete position on a 2D-grid.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Pos(isize, isize);
impl Pos {
/// Constructor.
pub fn new(x: isize, y: isize) -> Self {
Self(x, y)
}
/// Origin of the 2D-grid.
pub fn zero() -> Self {
Self::new(0, 0)
}
/// Unit length on both axis of the 2D-grid.
pub fn one() -> Self {
Self::new(1, 1)
}
/// Constructor for positions of diagonal vectors.
pub fn sized(scale: isize) -> Self {
Self::one().scale(scale)
}
/// Scale the vector by an integer value.
pub fn scale(self, factor: isize) -> Self {
self * Self::sized(factor)
}
/// Accessor of the x-coordinate.
pub fn x(self) -> isize {
self.0
}
/// Accessor of the y-coordinate.
pub fn y(self) -> isize {
self.1
}
/// Decompose the position in both its x-part and y-part as a tuple.
pub fn decompose(self) -> (isize, isize) {
(self.x(), self.y())
}
}
impl From<(isize, isize)> for Pos {
fn from((x, y): (isize, isize)) -> Self {
Self::new(x, y)
}
}
impl Add for Pos {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let x = self.x() + rhs.x();
let y = self.y() + rhs.y();
Self::new(x, y)
}
}
impl Sub for Pos {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let x = self.x() - rhs.x();
let y = self.y() - rhs.y();
Self::new(x, y)
}
}
impl Mul for Pos {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
let x = self.x() * rhs.x();
let y = self.y() * rhs.y();
Self::new(x, y)
}
}