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

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

118
Cargo.lock generated Normal file
View file

@ -0,0 +1,118 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "labirust"
version = "0.1.0"
dependencies = [
"rand",
"termion",
]
[[package]]
name = "libc"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_termios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall",
]
[[package]]
name = "termion"
version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [
"libc",
"numtoa",
"redox_syscall",
"redox_termios",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "labirust"
version = "0.1.0"
edition = "2021"
license = "MIT"
authors = ["JOLIMAITRE Matthieu <matthieu@imagevo.fr>"]
description = "Naive rust crate for implementing and testing maze solving Algorithms."
[dependencies]
rand = "0.8.5"
termion = "1.5.6"

27
README.md Normal file
View file

@ -0,0 +1,27 @@
# Labirust
Labirynths, in Rust.
---
## Description
Naive rust crate for implementing and testing maze solving Algorithms.
## Context
Hi, I am a junior rust dev (yes, how original, I know) and this is one of the small projects I do to learn the languages and feel its system or learn its good practices.
## Usage
If you want to see what the library is capable of, you can clone this repo and run the tests to see mazes solving themselves on the terminal.
for you to see anything during the execution of tests, don't forget to add these arguments:
```sh
-- --nocapture
# example:
cargo test -- implementations::breath_first --nocapture
```
If you want to implement more resolution algorithm yourself and for some reason you think this is the right crate to provide frameworks, you can explore the crate documentation and add this crate to your project dependencies.

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)
}
}