shortestpath 0.10.0

Shortest Path is an experimental library finding the shortest path from A to B.
Documentation
// Copyright (C) 2025 Christian Mauduit <ufoot@ufoot.org>

//! Text-based 2D source implementation.
//!
//! This module provides the ability to parse pathfinding grids from ASCII text.
//! Characters are interpreted as walls or free cells based on configurable rules.

use super::cell_type::*;
use super::source_2d::*;
use crate::errors::*;
use crate::mesh_2d::Shape2D;
use std::str::FromStr;

/// A 2D grid source that reads from text.
///
/// By default:
/// - Free characters: ` ` (space), `.`, `o`
/// - Wall characters: Everything else except separators
/// - Separator characters: `-`, `_`, `=` (used for 3D layer separation)
///
/// # Example
///
/// ```
/// use shortestpath::mesh_source::{Source2DFromText, Source2D, CellType};
///
/// let map = "\
/// .....
/// .###.
/// .#.#.
/// .....";
///
/// let source = Source2DFromText::from_text(map);
/// assert_eq!(source.width(), 5);
/// assert_eq!(source.height(), 4);
/// assert_eq!(source.get(0, 0).unwrap(), CellType::FLOOR);  // '.' is free
/// assert_eq!(source.get(1, 1).unwrap(), CellType::WALL);  // '#' is wall
/// ```
#[derive(Debug, Clone)]
pub struct Source2DFromText {
    width: usize,
    data: Vec<Vec<char>>,
}

impl Source2DFromText {
    /// Default wall character.
    pub const DEFAULT_WALL_CHAR: char = '#';

    /// Characters that are considered free (walkable).
    pub const FREE_CHARS: &'static str = " .o";

    /// Default free character.
    pub const DEFAULT_FREE_CHAR: char = ' ';

    /// Separator characters (used for 3D layer boundaries, not walls).
    pub const SEP_CHARS: &'static str = "-_=";

    /// Default separator character.
    pub const DEFAULT_SEP_CHAR: char = '-';

    fn width_from_data(data: &[Vec<char>]) -> usize {
        data.iter().map(|l| l.len()).max().unwrap_or(0)
    }

    /// Gets the character at the given coordinates.
    ///
    /// Returns the default free character if the position is beyond the line length.
    fn get_char(&self, x: usize, y: usize) -> Result<char> {
        if y >= self.data.len() {
            return Err(Error::invalid_xy(x, y));
        }
        if x >= self.width {
            return Err(Error::invalid_xy(x, y));
        }
        let line = &self.data[y];
        if x >= line.len() {
            // If undefined, it's not a wall, one can go there
            return Ok(Self::DEFAULT_FREE_CHAR);
        }
        Ok(line[x])
    }

    /// Checks if a character represents a wall.
    ///
    /// A character is a wall if it's not a free character and not a separator.
    pub fn is_wall_char(chr: char) -> bool {
        Self::FREE_CHARS.find(chr).is_none() && !Self::is_sep_char(chr)
    }

    /// Checks if a character is a separator (used for 3D layers).
    pub fn is_sep_char(chr: char) -> bool {
        Self::SEP_CHARS.find(chr).is_some()
    }

    /// Creates a Source2DFromText from a string slice.
    ///
    /// This is equivalent to using the `FromStr` trait's `parse()` method.
    ///
    /// # Example
    ///
    /// ```
    /// use shortestpath::mesh_source::{Source2DFromText, Source2D};
    ///
    /// let source = Source2DFromText::from_text("...\n.#.\n...");
    /// assert_eq!(source.width(), 3);
    /// assert_eq!(source.height(), 3);
    /// ```
    pub fn from_text(input: &str) -> Source2DFromText {
        Self::from_lines(input.lines())
    }

    /// Creates a Source2DFromText from a slice of string slices.
    pub fn from_slice(input: &[&str]) -> Source2DFromText {
        Self::from_lines(input.iter().copied())
    }

    /// Creates a Source2DFromText from a slice of Strings.
    pub fn from_strings(input: &[String]) -> Source2DFromText {
        Self::from_lines(input.iter().map(|s| s.as_str()))
    }

    /// Creates a Source2DFromText from an iterator of string slices.
    pub fn from_lines<'a, I>(input: I) -> Source2DFromText
    where
        I: Iterator<Item = &'a str>,
    {
        let data: Vec<Vec<char>> = input
            .map(|line| String::from(line).chars().collect())
            .collect();
        let width = Self::width_from_data(&data);
        Source2DFromText { width, data }
    }
}

impl FromStr for Source2DFromText {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        Ok(Self::from_text(s))
    }
}

impl Source2D for Source2DFromText {
    fn get(&self, x: usize, y: usize) -> Result<CellType> {
        let chr = self.get_char(x, y)?;
        Ok(if Self::is_wall_char(chr) {
            CellType::WALL
        } else {
            CellType::FLOOR
        })
    }

    fn width(&self) -> usize {
        self.width
    }

    fn height(&self) -> usize {
        self.data.len()
    }
}

impl Shape2D for Source2DFromText {
    fn shape(&self) -> (usize, usize) {
        (self.width, self.data.len())
    }
}

impl std::fmt::Display for Source2DFromText {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Source2DFromText ({}x{}):", self.width(), self.height())?;
        write!(f, "{}", crate::mesh_source::repr::repr_source_2d(self))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_wall_char() {
        assert!(Source2DFromText::is_wall_char('#'));
        assert!(Source2DFromText::is_wall_char('a'));
        assert!(Source2DFromText::is_wall_char('*'));
        assert!(Source2DFromText::is_wall_char('/'));
        assert!(!Source2DFromText::is_wall_char('-'));
        assert!(!Source2DFromText::is_wall_char('_'));
        assert!(!Source2DFromText::is_wall_char('='));
        assert!(!Source2DFromText::is_wall_char(' '));
        assert!(!Source2DFromText::is_wall_char('.'));
        assert!(!Source2DFromText::is_wall_char('o'));
    }

    #[test]
    fn test_is_sep_char() {
        assert!(!Source2DFromText::is_sep_char('#'));
        assert!(!Source2DFromText::is_sep_char('a'));
        assert!(!Source2DFromText::is_sep_char('*'));
        assert!(!Source2DFromText::is_sep_char('/'));
        assert!(Source2DFromText::is_sep_char('-'));
        assert!(Source2DFromText::is_sep_char('_'));
        assert!(Source2DFromText::is_sep_char('='));
        assert!(!Source2DFromText::is_sep_char(' '));
        assert!(!Source2DFromText::is_sep_char('.'));
        assert!(!Source2DFromText::is_sep_char('o'));
    }

    #[test]
    fn test_from_text_basic() {
        let source = Source2DFromText::from_text("aaaaaa\nb b b \n      \n123456");
        assert_eq!(source.width(), 6);
        assert_eq!(source.height(), 4);
        assert_eq!(source.get_char(0, 0).unwrap(), 'a');
        assert_eq!(source.get(0, 0).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(2, 1).unwrap(), 'b');
        assert_eq!(source.get(2, 1).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(3, 1).unwrap(), ' ');
        assert_eq!(source.get(3, 1).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(4, 2).unwrap(), ' ');
        assert_eq!(source.get(4, 2).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(5, 3).unwrap(), '6');
        assert_eq!(source.get(5, 3).unwrap(), CellType::WALL);
        assert!(source.get_char(6, 4).is_err());
        assert!(source.get(6, 4).is_err());
    }

    #[test]
    fn test_from_text_last_line() {
        let source = Source2DFromText::from_text("aaaaaa\nb b b \n      \n123456\n");
        assert_eq!(source.width(), 6);
        assert_eq!(source.height(), 4);
    }

    #[test]
    fn test_from_text_various_len() {
        let source = Source2DFromText::from_text("a\nb b b \n\n123456\n");
        assert_eq!(source.width(), 6);
        assert_eq!(source.height(), 4);
        assert_eq!(source.get_char(0, 0).unwrap(), 'a');
        assert_eq!(source.get(0, 0).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(2, 1).unwrap(), 'b');
        assert_eq!(source.get(2, 1).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(3, 1).unwrap(), ' ');
        assert_eq!(source.get(3, 1).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(4, 2).unwrap(), ' ');
        assert_eq!(source.get(4, 2).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(5, 3).unwrap(), '6');
        assert_eq!(source.get(5, 3).unwrap(), CellType::WALL);
        assert!(source.get_char(6, 4).is_err());
        assert!(source.get(6, 4).is_err());
    }
}