sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
//! [`Sokoban`](crate::Tessellation::Sokoban) board text: parsing, printing and resizing.
//!
//! A stub by design, and it should stay one. Every method body comes from the `BoardParser` /
//! `BoardPrinter` / `BoardResizer` defaults in `parser.rs`; all this module declares is which
//! tessellation's padding rule those defaults apply. Shared behavior therefore belongs in a trait
//! default rather than here.

use super::parser::{BoardParser, BoardPrinter, BoardResizer};
use crate::Tessellation;

pub struct SokobanBoardParser {}

impl BoardParser for SokobanBoardParser {}

impl BoardPrinter for SokobanBoardParser {}

impl BoardResizer for SokobanBoardParser {
    const TESSELLATION: Tessellation = Tessellation::Sokoban;
}

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

    use super::super::*;

    /// A 90 degree turn, on the smallest board where the *sense* of the turn is unmistakable.
    ///
    /// Nothing else in the suite pins it. Injectivity, adjacency preservation, tile multisets and
    /// round trips are all satisfied by a 180 degree map or by a rotation composed with a
    /// translation, and `content_signature()` in `game_board.rs` normalizes translation away by
    /// construction. Here the whole board is asserted, position by position.
    ///
    /// Hand-derived, and worth reproducing rather than trusting: the raw map is `(x, y) -> (-y, x)`
    /// (`doc/dev-notes/board-geometry-and-transformations.md` section 4). Over `x` in `0..5` and
    /// `y` in `0..3` that spans `x'` in `-2..1` and `y'` in `0..5`, and Sokoban admits every
    /// translation, so the smallest anchor is `dx = 2, dy = 0` - giving `(x, y) -> (2 - y, x)` and
    /// a 3x5 result. The pusher starts leftmost, so it must end up nearest the *top*; that is the
    /// direction check of section 4, and it is what a flipped sense cannot survive.
    mod test_rotation {
        use super::*;

        #[test]
        fn it_rotates_board_clockwise() -> Result<(), BoardSyntaxError> {
            let src = indoc! {"#####
                               #@$.#
                               #####"};

            let expected = indoc! {"###
                                    #@#
                                    #$#
                                    #.#
                                    ###"};

            let mut board = GameBoard::parsed(src, Tessellation::Sokoban)?;

            board.rotate(Rotation::Clockwise);

            assert_eq!(board.width(), 3);
            assert_eq!(board.height(), 5);
            assert_eq!(board.printed(true), expected);

            Ok(())
        }

        #[test]
        fn it_rotates_board_counter_clockwise() -> Result<(), BoardSyntaxError> {
            let src = indoc! {"#####
                               #@$.#
                               #####"};

            // The same column as clockwise, read the other way: `@` was leftmost, so counter
            // clockwise sends it to the bottom.
            let expected = indoc! {"###
                                    #.#
                                    #$#
                                    #@#
                                    ###"};

            let mut board = GameBoard::parsed(src, Tessellation::Sokoban)?;

            board.rotate(Rotation::CounterClockwise);

            assert_eq!(board.width(), 3);
            assert_eq!(board.height(), 5);
            assert_eq!(board.printed(true), expected);

            Ok(())
        }
    }

    /// Mirroring does not dispatch to a `BoardParser` implementor - it goes through
    /// `transform::transformed()` - but the expected board text is Sokoban geometry, so it
    /// belongs beside Sokoban.
    mod test_mirroring {
        use super::*;

        #[test]
        fn it_mirrors_board_up_down() -> Result<(), BoardSyntaxError> {
            let src = indoc! {"    #####
                               #   #
                               #$  #
                             ###  $##
                             #  $ $ #
                           ### # ## #   ######
                           #   # ## #####  ..#
                           # $  $          ..#
                           ##### ### #@##  ..#
                               #     #########
                               #######"};

            let result = indoc! {"----#######--------
                              ----#-----#########
                              #####-###-#@##--..#
                              #-$--$----------..#
                              #---#-##-#####--..#
                              ###-#-##-#---######
                              --#--$-$-#---------
                              --###--$##---------
                              ----#$--#----------
                              ----#---#----------
                              ----#####----------"};

            let mut board = GameBoard::parsed(src, Tessellation::Sokoban)?;

            board.mirror(MirrorAxis::Horizontal);

            assert_eq!(board.width(), 19);
            assert_eq!(board.height(), 11);
            assert_eq!(board.printed(true), result);

            Ok(())
        }

        #[test]
        fn it_mirrors_board_left_right() -> Result<(), BoardSyntaxError> {
            let src = indoc! {"    #####
                               #   #
                               #$  #
                             ###  $##
                             #  $ $ #
                           ### # ## #   ######
                           #   # ## #####  ..#
                           # $  $          ..#
                           ##### ### #@##  ..#
                               #     #########
                               #######"};

            let result = indoc! {"----------#####----
                              ----------#---#----
                              ----------#--$#----
                              ---------##$--###--
                              ---------#-$-$--#--
                              ######---#-##-#-###
                              #..--#####-##-#---#
                              #..----------$--$-#
                              #..--##@#-###-#####
                              #########-----#----
                              --------#######----"};

            let mut board = GameBoard::parsed(src, Tessellation::Sokoban)?;

            board.mirror(MirrorAxis::Vertical);

            assert_eq!(board.width(), 19);
            assert_eq!(board.height(), 11);
            assert_eq!(board.printed(true), result);

            Ok(())
        }
    }

    /// Sokoban needs no perpendicular padding on any edge, so a resize is a plain crop or
    /// extend. The parity-sensitive tessellations are covered in `parser.rs`.
    mod test_resizing {
        use super::*;

        #[test]
        fn test_it_can_shrink_game_board() -> Result<(), BoardSyntaxError> {
            let src = indoc! {"------------------------
                           ------------------------
                           ------#####-------------
                           ------#---#-------------
                           ------#$--#-------------
                           ----###--$##------------
                           ----#--$-$-#------------
                           --###-#-##-#---######---
                           --#---#-##-#####--..#---
                           --#-$--$----------..#---
                           --#####-###-#@##--..#---
                           ------#-----#########---
                           ------#######-----------
                           ------------------------
                           ------------------------
                           ------------------------"};

            let result = indoc! {"    #####
                                  #   #
                                  #$  #
                                ###  $##
                                #  $ $ #
                              ### # ## #   ######
                              #   # ## #####  ..#
                              # $  $          ..#
                              ##### ### #@##  ..#
                                  #     #########
                                  #######"};

            let mut board = GameBoard::parsed(src, Tessellation::Sokoban)?;

            board.remove_columns(VerticalEdge::Left, 2);
            board.remove_columns(VerticalEdge::Right, 3);
            board.remove_rows(HorizontalEdge::Top, 2);
            board.remove_rows(HorizontalEdge::Bottom, 3);

            assert_eq!(board.width(), 19);
            assert_eq!(board.height(), 11);
            assert_eq!(board.printed(false), result);

            Ok(())
        }

        #[test]
        fn test_it_can_enlarge_game_board() -> Result<(), BoardSyntaxError> {
            let src = indoc! {"    #####
                               #   #
                               #$  #
                             ###  $##
                             #  $ $ #
                           ### # ## #   ######
                           #   # ## #####  ..#
                           # $  $          ..#
                           ##### ### #@##  ..#
                               #     #########
                               #######"};

            let result = indoc! {"------------------------
                              ------------------------
                              ------#####-------------
                              ------#---#-------------
                              ------#$--#-------------
                              ----###--$##------------
                              ----#--$-$-#------------
                              --###-#-##-#---######---
                              --#---#-##-#####--..#---
                              --#-$--$----------..#---
                              --#####-###-#@##--..#---
                              ------#-----#########---
                              ------#######-----------
                              ------------------------
                              ------------------------
                              ------------------------"};

            let mut board = GameBoard::parsed(src, Tessellation::Sokoban)?;

            board.add_columns(VerticalEdge::Left, 2);
            board.add_columns(VerticalEdge::Right, 3);
            board.add_rows(HorizontalEdge::Top, 2);
            board.add_rows(HorizontalEdge::Bottom, 3);

            assert_eq!(board.width(), 24);
            assert_eq!(board.height(), 16);
            assert_eq!(board.printed(true), result);

            Ok(())
        }
    }
}