use std::ops::{Index, IndexMut};
use super::GameBoard;
use crate::utilities::{index_x, index_y};
use crate::{Point2D, TileKind};
impl Index<(usize, usize)> for GameBoard {
type Output = TileKind;
#[expect(clippy::panic)]
fn index(&self, index: (usize, usize)) -> &Self::Output {
self.data
.get(index.1)
.and_then(|row| row.get(index.0))
.unwrap_or_else(|| panic!("Invalid GameBoard index {index:?}!"))
}
}
impl IndexMut<(usize, usize)> for GameBoard {
#[expect(clippy::panic)]
fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output {
self.data
.get_mut(index.1)
.and_then(|row| row.get_mut(index.0))
.unwrap_or_else(|| panic!("Invalid GameBoard index {index:?}!"))
}
}
impl Index<usize> for GameBoard {
type Output = TileKind;
#[expect(clippy::panic)]
fn index(&self, index: usize) -> &Self::Output {
let width = self.width();
assert!(width != 0, "Invalid GameBoard index {index}!");
self.data
.get(index_y(index, width))
.and_then(|row| row.get(index_x(index, width)))
.unwrap_or_else(|| panic!("Invalid GameBoard index {index}!"))
}
}
impl IndexMut<usize> for GameBoard {
#[expect(clippy::panic)]
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
let width = self.width();
assert!(width != 0, "Invalid GameBoard index {index}!");
self.data
.get_mut(index_y(index, width))
.and_then(|row| row.get_mut(index_x(index, width)))
.unwrap_or_else(|| panic!("Invalid GameBoard index {index}!"))
}
}
impl Index<Point2D> for GameBoard {
type Output = TileKind;
#[expect(clippy::panic)]
fn index(&self, index: Point2D) -> &Self::Output {
self.data
.get(index.y)
.and_then(|row| row.get(index.x))
.unwrap_or_else(|| panic!("Invalid GameBoard index {index}!"))
}
}
impl IndexMut<Point2D> for GameBoard {
#[expect(clippy::panic)]
fn index_mut(&mut self, index: Point2D) -> &mut Self::Output {
self.data
.get_mut(index.y)
.and_then(|row| row.get_mut(index.x))
.unwrap_or_else(|| panic!("Invalid GameBoard index {index}!"))
}
}
#[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_reads_tiles_by_x_and_y() -> Result<(), BoardSyntaxError> {
let board = board()?;
assert_eq!(board[(0, 0)], TileKind::Floor);
assert_eq!(board[(4, 0)], TileKind::Wall);
assert_eq!(board[(5, 2)], TileKind::Box);
assert_eq!(board[(16, 6)], TileKind::Goal);
assert_eq!(board[(11, 8)], TileKind::Pusher);
Ok(())
}
#[test]
fn when_indexed_by_position_it_matches_indexing_by_x_and_y() -> Result<(), BoardSyntaxError> {
let board = board()?;
for y in 0..board.height() {
for x in 0..board.width() {
assert_eq!(board[(x, y)], board[y * board.width() + x]);
}
}
Ok(())
}
#[test]
fn it_writes_tiles_by_x_and_y() -> Result<(), BoardSyntaxError> {
let mut board = board()?;
let boxes_count = board.boxes_count();
assert_eq!(board[(0, 0)], TileKind::Floor);
board[(0, 0)] = TileKind::Box;
assert_eq!(board[(0, 0)], TileKind::Box);
assert_eq!(board.boxes_count(), boxes_count + 1);
Ok(())
}
#[test]
fn it_writes_tiles_by_linear_position() -> Result<(), BoardSyntaxError> {
let mut board = board()?;
let position: usize = 4;
assert_eq!(board[position], TileKind::Wall);
board[position] = TileKind::Goal;
assert_eq!(board[position], TileKind::Goal);
assert_eq!(board[(4, 0)], TileKind::Goal);
Ok(())
}
#[test]
fn it_reads_tiles_by_point() -> Result<(), BoardSyntaxError> {
let board = board()?;
assert_eq!(board[Point2D::new(0, 0)], TileKind::Floor);
assert_eq!(board[Point2D::new(4, 0)], TileKind::Wall);
assert_eq!(board[Point2D::new(5, 2)], TileKind::Box);
assert_eq!(board[Point2D::new(16, 6)], TileKind::Goal);
assert_eq!(board[Point2D::new(11, 8)], TileKind::Pusher);
Ok(())
}
#[test]
fn when_indexed_by_point_it_matches_indexing_by_x_and_y() -> Result<(), BoardSyntaxError> {
let board = board()?;
for y in 0..board.height() {
for x in 0..board.width() {
assert_eq!(board[Point2D::new(x, y)], board[(x, y)]);
assert_eq!(board[Point2D::new(x, y)], board[y * board.width() + x]);
}
}
Ok(())
}
#[test]
fn it_writes_tiles_by_point() -> Result<(), BoardSyntaxError> {
let mut board = board()?;
let boxes_count = board.boxes_count();
let position = Point2D::new(5, 2);
assert_eq!(board[position], TileKind::Box);
assert_eq!(board[(2, 5)], TileKind::Wall);
board[position] = TileKind::Goal;
assert_eq!(board[position], TileKind::Goal);
assert_eq!(board[(5, 2)], TileKind::Goal);
assert_eq!(board[(2, 5)], TileKind::Wall, "x and y must not be swapped");
assert_eq!(board.boxes_count(), boxes_count - 1);
Ok(())
}
#[test]
#[should_panic(expected = "Invalid GameBoard index")]
fn it_panics_when_x_and_y_are_out_of_bounds() {
let board = GameBoard::new(3, 3, Tessellation::Sokoban);
let _ = board[(0, board.height())];
}
#[test]
#[should_panic(expected = "Invalid GameBoard index")]
fn it_panics_when_position_is_out_of_bounds() {
let board = GameBoard::new(3, 3, Tessellation::Sokoban);
let _ = board[board.width() * board.height()];
}
#[test]
#[should_panic(expected = "Invalid GameBoard index")]
fn when_board_is_empty_indexing_by_position_panics() {
let board = GameBoard::new(0, 0, Tessellation::Sokoban);
let position: usize = 0;
let _ = board[position];
}
#[test]
#[should_panic(expected = "Invalid GameBoard index")]
fn it_panics_when_point_x_is_out_of_bounds() {
let board = GameBoard::new(3, 3, Tessellation::Sokoban);
let _ = board[Point2D::new(board.width(), 0)];
}
#[test]
#[should_panic(expected = "Invalid GameBoard index")]
fn it_panics_when_point_y_is_out_of_bounds() {
let board = GameBoard::new(3, 3, Tessellation::Sokoban);
let _ = board[Point2D::new(0, board.height())];
}
#[test]
#[should_panic(expected = "Invalid GameBoard index")]
fn when_board_is_empty_indexing_by_point_panics() {
let board = GameBoard::new(0, 0, Tessellation::Sokoban);
let _ = board[Point2D::origin()];
}
}