sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
use std::error::Error;
use std::fmt::{Debug, Display};

use super::{Direction, Tessellation};

/// Private, internal errors for `tessellation` module.
///
/// `tessellation` APIs that are exported from this module return `Option<T>` where internally
/// they'd produced `Result<T, TessellationError>`. This is by design which is motivated by:
///
/// - `Tessellation` methods usage in other places in this crate
/// - wish to simplify and declutter `Tessellation` API
///
/// For example, exported, public method [`Tessellation::neighbor()`] returns `Option<Point2D>`.
/// This method also documents all possible causes for returning `None`. All these documented causes
/// could arguably be `TessellationError` variants and method could return `Result<Point2D,
/// TessellationError>`, but it we deliberately chose to return `Option` instead of `Result`:
///
/// - on call sites of this method, it is often already known what directions are or are not
///   supported: having this method return error that describes conditions on requested `Direction`
///   is redundant information
/// - at the call sites it is also usually already known what positions are on or off board so
///   having this method return errors describing these conditions is also redundant information
///
/// API is "cleaner" without cluttering it with unimportant error variants.
///
/// For more precise testing, internally, we still want to have these error variants.
///
/// In conclusion: `TessellationError` exists for internal, testing purposes, and remains private by
/// design.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TessellationError {
    CoordinateOverflow(usize, usize, Direction),
    OffBoard(usize, usize, Direction),
    IllegalDirection(Tessellation, Direction),
    InvalidDirectionForTile(Tessellation, Direction, usize, usize),
}

impl Error for TessellationError {}

impl Display for TessellationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CoordinateOverflow(x, y, d) => {
                write!(f, "CoordinateOverflow x:{x} y:{y}, d:{d:?}")
            }
            Self::IllegalDirection(v, d) => {
                write!(f, "IllegalDirection {v:?} {d:?}")
            }
            Self::OffBoard(x, y, d) => {
                write!(f, "OffBoard x:{x} y:{y}, d:{d:?}")
            }
            Self::InvalidDirectionForTile(v, d, x, y) => {
                write!(f, "InvalidDirectionForTile {v:?} {d:?} {x} {y}")
            }
        }
    }
}

pub fn validate_and_apply_delta(
    x: usize, y: usize, to: Direction, dx: i8, dy: i8, width: usize, height: usize,
) -> Result<(usize, usize), TessellationError> {
    if x >= width || y >= height {
        return Err(TessellationError::OffBoard(x, y, to));
    }

    let mut new_x = x;
    let mut new_y = y;

    if dx == 1 {
        if x == usize::MAX {
            return Err(TessellationError::CoordinateOverflow(x, y, to));
        } else if x >= width {
            return Err(TessellationError::OffBoard(x, y, to));
        }
        new_x += 1;
    } else if dx == -1 {
        if x == 0 {
            return Err(TessellationError::OffBoard(x, y, to));
        }
        new_x -= 1;
    }

    if dy == 1 {
        if y == usize::MAX {
            return Err(TessellationError::CoordinateOverflow(x, y, to));
        } else if y >= height {
            return Err(TessellationError::OffBoard(x, y, to));
        }
        new_y += 1;
    } else if dy == -1 {
        if y == 0 {
            return Err(TessellationError::OffBoard(x, y, to));
        }
        new_y -= 1;
    }

    if new_x >= width || new_y >= height {
        Err(TessellationError::OffBoard(x, y, to))
    } else {
        Ok((new_x, new_y))
    }
}