sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
use std::iter::FusedIterator;

use super::GameBoard;
use crate::TileKind;
use crate::utilities::checked_index_1d;

/// Row major iterator over `((x, y), tile)` pairs of [`GameBoard`](crate::GameBoard).
#[derive(Debug, Clone, PartialEq)]
pub struct GameBoardIter<'a> {
    src:    &'a GameBoard,
    width:  usize,
    height: usize,
    x:      usize,
    y:      usize,
}

impl<'a> GameBoardIter<'a> {
    pub(super) fn new(src: &'a GameBoard) -> Self {
        Self {
            src,
            width: src.width(),
            height: src.height(),
            x: 0,
            y: 0,
        }
    }
}

impl Iterator for GameBoardIter<'_> {
    type Item = ((usize, usize), TileKind);

    fn next(&mut self) -> Option<Self::Item> {
        if self.x >= self.width || self.y >= self.height {
            return None;
        }

        let position = (self.x, self.y);
        let tile = self.src[position];

        self.x += 1;
        if self.x == self.width {
            self.x = 0;
            self.y += 1;
        }

        Some((position, tile))
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let total = self.width.saturating_mul(self.height);
        let consumed = checked_index_1d(self.x, self.y, self.width).unwrap_or(total);
        let remaining = total.saturating_sub(consumed);

        (remaining, Some(remaining))
    }
}

impl ExactSizeIterator for GameBoardIter<'_> {}

impl FusedIterator for GameBoardIter<'_> {}

impl<'a> IntoIterator for &'a GameBoard {
    type IntoIter = GameBoardIter<'a>;
    type Item = ((usize, usize), TileKind);

    fn into_iter(self) -> Self::IntoIter { self.iter() }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{BoardSyntaxError, Tessellation};

    fn board_text() -> String {
        [
            "    #####",
            "    #   #",
            "    #$  #",
            "  ###  $##",
            "  #  $ $ #",
            "### # ## #   ######",
            "#   # ## #####  ..#",
            "# $  $          ..#",
            "##### ### #@##  ..#",
            "    #     #########",
            "    #######",
        ]
        .join("\n")
    }

    fn board() -> Result<GameBoard, BoardSyntaxError> {
        GameBoard::parsed(&board_text(), Tessellation::Sokoban)
    }

    #[test]
    fn it_iterates_tiles_in_row_major_order() -> Result<(), BoardSyntaxError> {
        let small = GameBoard::new(3, 2, Tessellation::Sokoban);
        let positions: Vec<(usize, usize)> = small.iter().map(|(position, _)| position).collect();

        assert_eq!(positions, vec![(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]);

        let board = board()?;

        assert_eq!(board.iter().next().map(|(position, _)| position), Some((0, 0)));
        assert_eq!(
            board.iter().last().map(|(position, _)| position),
            Some((board.width() - 1, board.height() - 1))
        );

        Ok(())
    }

    #[test]
    fn it_iterates_over_all_tiles() -> Result<(), BoardSyntaxError> {
        let board = board()?;
        let total = board.width() * board.height();

        assert_eq!(board.iter().count(), total);
        assert_eq!(board.iter().len(), total);
        assert_eq!(board.iter().size_hint(), (total, Some(total)));

        let mut it = board.iter();
        it.next();
        assert_eq!(it.size_hint(), (total - 1, Some(total - 1)));
        assert_eq!(it.len(), total - 1);

        let mut visited: usize = 0;
        for (position, tile) in &board {
            assert_eq!(board[position], tile);
            visited += 1;
        }
        assert_eq!(visited, total);

        Ok(())
    }

    #[test]
    fn when_board_is_empty_it_iterates_nothing() -> Result<(), BoardSyntaxError> {
        let board = GameBoard::parsed("", Tessellation::Sokoban)?;

        assert_eq!(board.width(), 0);
        assert_eq!(board.height(), 0);
        assert!(board.iter().next().is_none());
        assert_eq!(board.iter().len(), 0);

        Ok(())
    }
}