podch 0.1.0

Game engine for the podch abstract board game
Documentation
use std::fmt;
use crate::{Board, EditedBoard, HashUsedSituations, Stone, UsedSituations};

/// Type of invalid move
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum MoveError {
    /// The selected square is outside the board
    SquareOutOfBounds,

    /// The selected square is occupied by another player's stone
    SquareOccupied,

    /// The resulting situation is similar to one used before
    SituationUsedBefore,
}

impl fmt::Display for MoveError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MoveError::SquareOutOfBounds => { f.write_str("The square is out of the board") }
            MoveError::SquareOccupied => { f.write_str("The square is occupied by a foreign stone") }
            MoveError::SituationUsedBefore => { f.write_str("The situation was used before") }
        }
    }
}

impl std::error::Error for MoveError {}

/// Result of a move attempt
pub type MoveResult = Result<(), MoveError>;

/// Podch game
pub trait Game {
    type Board: Board;

    /// Construct new game
    ///
    /// # Examples
    /// ```
    /// use podch::Game;
    /// let game = podch::SimpleGame::<podch::VecBoard>::new(2, 3);
    /// assert_eq!(game.size(), (2, 3));
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "000\n000");
    /// assert_eq!(game.current_player(), podch::Stone::Dark);
    /// ```
    fn new(height: usize, width: usize) -> Self;

    /// Current situation on the board
    ///
    /// # Examples
    /// ```
    /// use podch::{Game, Board};
    /// let game = podch::SimpleGame::<podch::BinBoard>::new(2, 3);
    /// let board = game.board();
    /// assert_eq!(board.size(), game.size());
    /// assert_eq!(podch::BoardDisplay::from(board).to_string(), "000\n000");
    /// ```
    fn board(&self) -> &Self::Board;

    /// Height and width of the board
    ///
    /// # Examples
    /// ```
    /// use podch::Game;
    /// let game = podch::SimpleGame::<podch::VecBoard>::new(2, 3);
    /// let (height, width) = game.size();
    /// assert_eq!(height, 2);
    /// assert_eq!(width, 3);
    /// ```
    fn size(&self) -> (usize, usize) {
        self.board().size()
    }

    /// Color of the player whose turn
    ///
    /// # Examples
    /// ```
    /// use podch::{Game, MoveResult};
    /// let mut game = podch::SimpleGame::<podch::BinBoard>::new(2, 3);
    /// let player = game.current_player();
    /// assert_eq!(player, podch::Stone::Dark);
    /// game.make_move(0, 0).unwrap();
    /// assert_eq!(game.current_player(), podch::Stone::Light);
    /// ```
    fn current_player(&self) -> Stone;

    type UsedSituations: UsedSituations<Self::Board>;

    /// Situations already used
    ///
    /// # Examples
    /// ```
    /// use podch::{Game, UsedSituations, Board};
    /// let mut game = podch::SimpleGame::<podch::VecBoard>::new(2, 3);
    /// let used = game.used_situations();
    /// let mut board = podch::VecBoard::from_vec(vec![podch::Stone::None; 6], 3);
    /// assert!(used.is(&board));
    /// board.set(0, 0, podch::Stone::Dark);
    /// assert!(!used.is(&board));
    /// game.make_move(1, 2).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "000\n001");
    /// assert!(game.used_situations().is(&board));
    /// ```
    fn used_situations(&self) -> &Self::UsedSituations;

    /// Check whether a move to square in `row` and `col`umn is valid
    ///
    /// # Examples
    /// ```
    /// use podch::Game;
    /// let mut game = podch::SimpleGame::<podch::BinBoard>::new(2, 3);
    /// assert!(game.check_move(0, 0).is_ok());
    /// assert_eq!(game.check_move(2, 0), Err(podch::MoveError::SquareOutOfBounds));
    /// game.make_move(0, 0).unwrap();
    /// assert_eq!(game.check_move(0, 0), Err(podch::MoveError::SquareOccupied));
    /// game.make_move(1, 0).unwrap();
    /// assert_eq!(game.check_move(0, 0), Err(podch::MoveError::SituationUsedBefore));
    /// assert_eq!(game.check_move(1, 0), Err(podch::MoveError::SquareOccupied));
    /// ```
    fn check_move(&self, row: usize, col: usize) -> MoveResult;

    /// Make a move to square in `row` and `col`umn (set or remove a stone)
    ///
    /// # Examples
    /// ```
    /// use podch::{Game, UsedSituations};
    /// let mut game = podch::SimpleGame::<podch::BinBoard>::new(2, 3);
    /// assert!(game.make_move(0, 0).is_ok());
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "100\n000");
    /// assert_eq!(game.current_player(), podch::Stone::Light);
    /// assert!(game.used_situations().is(game.board()));
    /// assert_eq!(game.make_move(0, 0), Err(podch::MoveError::SquareOccupied));
    ///
    /// assert!(game.make_move(1, 0).is_ok());
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "100\n200");
    /// assert_eq!(game.current_player(), podch::Stone::Dark);
    /// assert!(game.used_situations().is(game.board()));
    /// assert_eq!(game.make_move(0, 0), Err(podch::MoveError::SituationUsedBefore));
    /// ```
    fn make_move(&mut self, row: usize, col: usize) -> MoveResult;

    /// Check whether the game is over
    ///
    /// # Examples
    /// ```
    /// use podch::Game;
    /// let mut game = podch::SimpleGame::<podch::BinBoard>::new(1, 2);
    /// assert!(!game.is_over());
    /// game.make_move(0, 0).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "10");
    /// assert!(!game.is_over());
    /// game.make_move(0, 1).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "12");
    /// assert!(game.is_over());
    /// ```
    fn is_over(&self) -> bool {
        (0..self.size().0).map(
            |i| (0..self.size().1).all(
                |j| self.check_move(i, j).is_err()
            )
        ).all(|x| x)
    }

    /// Get the winner of a finished game; `None` if the game is not over
    ///
    /// # Examples
    /// ```
    /// use podch::Game;
    /// let mut game = podch::SimpleGame::<podch::BinBoard>::new(1, 3);
    /// game.make_move(0, 0).unwrap();
    /// game.make_move(0, 2).unwrap();
    /// game.make_move(0, 1).unwrap();
    /// game.make_move(0, 2).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "110");
    /// assert!(game.winner().is_none());
    /// let mut game2 = game.clone();
    /// game.make_move(0, 2).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game.board()).to_string(), "111");
    /// assert_eq!(game.winner(), Some(podch::Stone::Dark));
    ///
    /// game2.make_move(0, 0).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game2.board()).to_string(), "010");
    /// assert!(game2.winner().is_none());
    /// game2.make_move(0, 0).unwrap();
    /// assert_eq!(podch::BoardDisplay::from(game2.board()).to_string(), "210");
    /// assert_eq!(game2.winner(), Some(podch::Stone::Light));
    /// ```
    fn winner(&self) -> Option<Stone> {
        if self.is_over() {
            Some(-self.current_player())
        } else {
            None
        }
    }
}

/// Simple game using HashUsedSituations and EditedBoard
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SimpleGame<B: Board> {
    board: B,
    player: Stone,
    used: HashUsedSituations,
}

impl<B: Board> Game for SimpleGame<B> {
    type Board = B;

    fn new(height: usize, width: usize) -> Self {
        let board = B::new(height, width);
        let mut used = HashUsedSituations::new();
        used.add(&board);
        Self {
            board,
            player: Stone::Dark,
            used,
        }
    }

    fn board(&self) -> &Self::Board {
        &self.board
    }

    fn current_player(&self) -> Stone {
        self.player
    }

    type UsedSituations = HashUsedSituations;

    fn used_situations(&self) -> &Self::UsedSituations {
        &self.used
    }

    fn check_move(&self, row: usize, col: usize) -> MoveResult {
        let val;
        if row >= self.size().0 || col >= self.size().1 {
            Err(MoveError::SquareOutOfBounds)
        } else if {
            val = self.board.get(row, col);
            val == -self.player
        } {
            Err(MoveError::SquareOccupied)
        } else if {
            let edited = EditedBoard::new(self.board(), row, col, if val == Stone::None { self.player } else { Stone::None });
            self.used.is(&edited)
        } {
            Err(MoveError::SituationUsedBefore)
        } else {
            Ok(())
        }
    }

    fn make_move(&mut self, row: usize, col: usize) -> MoveResult {
        self.check_move(row, col)?;
        self.board.set(row, col, if self.board.get(row, col) == Stone::None { self.player } else { Stone::None });
        self.player = -self.player;
        self.used.add(&self.board);
        Ok(())
    }
}