use crate::{MnkBoard, PlaceError, Player};
use std::error::Error;
use std::fmt;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum PlayError {
GameOver(GameStatus),
PlaceError(PlaceError),
RuleError {
message: String,
},
}
impl fmt::Display for PlayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::GameOver(status) => write!(f, "game already over: {status}"),
Self::PlaceError(place_error) => write!(f, "impossible move: {place_error}"),
Self::RuleError { message } => write!(f, "illegal move: {message}"),
}
}
}
impl Error for PlayError {}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum GameStatus {
Drawn,
Ongoing {
next: Player,
},
Won(Player),
}
impl fmt::Display for GameStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Drawn => write!(f, "Draw"),
Self::Ongoing { next } => write!(f, "Next: {next}"),
Self::Won(player) => write!(f, "{player} won!"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct MnkGame<const R: usize, const C: usize, const K: usize> {
board: MnkBoard<R, C, K>,
status: GameStatus,
}
impl<const R: usize, const C: usize, const K: usize> MnkGame<R, C, K> {
#[must_use]
pub const fn new() -> Self {
Self {
board: MnkBoard::<R, C, K>::new(),
status: GameStatus::Ongoing { next: Player::X },
}
}
#[must_use]
pub const fn board(&self) -> &MnkBoard<R, C, K> {
&self.board
}
#[must_use]
pub const fn status(&self) -> GameStatus {
self.status
}
pub fn play_at(&mut self, row: usize, column: usize) -> Result<(), PlayError> {
match self.status {
GameStatus::Drawn | GameStatus::Won(_) => Err(PlayError::GameOver(self.status)),
GameStatus::Ongoing { next } => self.board.place(next, row, column).map_or_else(
|err| Err(PlayError::PlaceError(err)),
|()| {
self.status = GameStatus::Ongoing { next: !next };
self.update_status();
Ok(())
},
),
}
}
fn update_status(&mut self) {
self.status = self.board.winner().map_or_else(
|| {
if self.board.full() {
GameStatus::Drawn
} else {
self.status }
},
GameStatus::Won,
);
}
}
impl<const R: usize, const C: usize, const K: usize> Default for MnkGame<R, C, K> {
fn default() -> Self {
Self::new()
}
}
impl<const R: usize, const C: usize, const K: usize> From<MnkGame<R, C, K>> for MnkBoard<R, C, K> {
fn from(game: MnkGame<R, C, K>) -> Self {
game.board
}
}
impl<const R: usize, const C: usize, const K: usize> fmt::Display for MnkGame<R, C, K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}\n{}", self.board, self.status)
}
}
#[cfg(test)]
mod test_play_at {
use super::*;
use crate::Space;
#[test]
fn rejects_finished_games() {
let mut drawn = MnkGame::<1, 1, 1>::new();
drawn.status = GameStatus::Drawn;
assert_eq!(
drawn.play_at(0, 0),
Err(PlayError::GameOver(GameStatus::Drawn))
);
assert_eq!(drawn.board, MnkBoard::<1, 1, 1>::new());
let mut x_won = MnkGame::<1, 1, 1>::new();
x_won.status = GameStatus::Won(Player::X);
assert_eq!(
x_won.play_at(0, 0),
Err(PlayError::GameOver(GameStatus::Won(Player::X)))
);
assert_eq!(x_won.board, MnkBoard::<1, 1, 1>::new());
let mut o_won = MnkGame::<1, 1, 1>::new();
o_won.status = GameStatus::Won(Player::O);
assert_eq!(
o_won.play_at(0, 0),
Err(PlayError::GameOver(GameStatus::Won(Player::O)))
);
assert_eq!(o_won.board, MnkBoard::<1, 1, 1>::new());
}
#[test]
fn rejects_place_errors() {
let mut empty = MnkGame::<1, 1, 1>::new();
assert_eq!(
empty.play_at(1, 0),
Err(PlayError::PlaceError(PlaceError::OutOfBounds {
row: 1,
column: 0
}))
);
}
#[test]
fn depends_on_next_player() {
let mut x_plays = MnkGame::<1, 1, 1>::new();
assert_eq!(x_plays.play_at(0, 0), Ok(()));
assert_eq!(x_plays.board.get(0, 0), Some(&Space::Stone(Player::X)));
let mut o_plays = MnkGame::<1, 1, 1>::new();
o_plays.status = GameStatus::Ongoing { next: Player::O };
assert_eq!(o_plays.play_at(0, 0), Ok(()));
assert_eq!(o_plays.board.get(0, 0), Some(&Space::Stone(Player::O)));
}
#[test]
fn swaps_next_player() {
let mut x_plays = MnkGame::<2, 2, 2>::new();
assert_eq!(x_plays.play_at(0, 0), Ok(()));
assert_eq!(x_plays.status, GameStatus::Ongoing { next: Player::O });
let mut o_plays = MnkGame::<2, 2, 2>::new();
o_plays.status = GameStatus::Ongoing { next: Player::O };
assert_eq!(o_plays.play_at(0, 0), Ok(()));
assert_eq!(o_plays.status, GameStatus::Ongoing { next: Player::X });
}
#[test]
fn updates_status() {
let mut x_wins = MnkGame::<1, 1, 1>::new();
assert_eq!(x_wins.play_at(0, 0), Ok(()));
assert_eq!(x_wins.status, GameStatus::Won(Player::X));
}
}
#[cfg(test)]
mod test_update_status {
use super::*;
use crate::Space;
#[test]
fn detects_wins() {
let mut x_wins = MnkGame::<1, 1, 1>::new();
x_wins.board = MnkBoard::from([[Space::Stone(Player::X)]]);
x_wins.update_status();
assert_eq!(x_wins.status, GameStatus::Won(Player::X));
let mut o_wins = MnkGame::<1, 1, 1>::new();
o_wins.board = MnkBoard::from([[Space::Stone(Player::O)]]);
o_wins.update_status();
assert_eq!(o_wins.status, GameStatus::Won(Player::O));
}
#[test]
fn detects_draws() {
let mut drawn = MnkGame::<1, 1, 2>::new();
drawn.board = MnkBoard::from([[Space::Stone(Player::X)]]);
drawn.update_status();
assert_eq!(drawn.status, GameStatus::Drawn);
}
#[test]
fn detects_ongoing() {
let mut ongoing = MnkGame::<1, 1, 1>::new();
ongoing.update_status();
assert_eq!(ongoing.status, GameStatus::Ongoing { next: Player::X });
}
}
#[cfg(test)]
mod test_mnk_game_display {
use crate::{GameStatus, MnkGame, Player};
#[test]
fn draw() {
let mut draw = MnkGame::<1, 1, 1>::new();
draw.status = GameStatus::Drawn;
assert_eq!(
draw.to_string(),
"+-+\n\
| |\n\
+-+\n\
Draw"
);
}
#[test]
fn ongoing() {
let x_next = MnkGame::<1, 1, 1>::new();
assert_eq!(
x_next.to_string(),
"+-+\n\
| |\n\
+-+\n\
Next: X"
);
let mut o_next = MnkGame::<1, 1, 1>::new();
o_next.status = GameStatus::Ongoing { next: Player::O };
assert_eq!(
o_next.to_string(),
"+-+\n\
| |\n\
+-+\n\
Next: O"
);
}
#[test]
fn won() {
let mut x_won = MnkGame::<1, 1, 1>::new();
x_won.status = GameStatus::Won(Player::X);
assert_eq!(
x_won.to_string(),
"+-+\n\
| |\n\
+-+\n\
X won!"
);
let mut o_won = MnkGame::<1, 1, 1>::new();
o_won.status = GameStatus::Won(Player::O);
assert_eq!(
o_won.to_string(),
"+-+\n\
| |\n\
+-+\n\
O won!"
);
}
}