use crate::{MnkBoard, PlaceError, Player, Space};
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!"),
}
}
}
#[must_use]
fn winner_in_run<'a>(
run: impl IntoIterator<Item = &'a Space>,
win_length: usize,
) -> Option<Player> {
let mut consecutive = 0;
let mut previous = &Space::Empty;
for space in run {
match *space {
Space::Empty => {
consecutive = 0;
}
Space::Stone(player) => {
if space == previous {
consecutive += 1;
} else {
consecutive = 1;
}
if consecutive == win_length {
return Some(player);
}
}
}
previous = space;
}
None
}
#[must_use]
fn winner_in_runs<'a>(
runs: impl IntoIterator<Item = impl IntoIterator<Item = &'a Space>>,
win_length: usize,
) -> Option<Player> {
let mut winners = runs.into_iter().map(|run| winner_in_run(run, win_length));
winners.find(Option::is_some).flatten()
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct MnkGame<const R: usize, const C: usize, const K: usize> {
board: MnkBoard<R, C>,
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>::new(),
status: GameStatus::Ongoing { next: Player::X },
}
}
#[must_use]
pub const fn board(&self) -> &MnkBoard<R, C> {
&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.winner().map_or_else(
|| {
if self.board.full() {
GameStatus::Drawn
} else {
self.status }
},
GameStatus::Won,
);
}
#[must_use]
fn winner(&self) -> Option<Player> {
if C >= K {
let winner = winner_in_runs(self.board.rows(), K);
if winner.is_some() {
return winner;
}
}
if R >= K {
let winner = winner_in_runs(self.board.columns(), K);
if winner.is_some() {
return winner;
}
}
if R >= K && C >= K {
let mut winner = winner_in_runs(self.board.top_right_diagonals(K), K);
if winner.is_some() {
return winner;
}
winner = winner_in_runs(self.board.left_down_diagonals(K), K);
if winner.is_some() {
return winner;
}
winner = winner_in_runs(self.board.top_left_diagonals(K), K);
if winner.is_some() {
return winner;
}
winner_in_runs(self.board.right_down_diagonals(K), K)
} else {
None
}
}
}
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> {
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_winner_in_run {
use super::*;
#[test]
fn trivial() {
let empty: [&Space; 0] = [];
assert!(winner_in_run(empty, 1).is_none());
assert!(winner_in_run(empty, 2).is_none());
assert!(winner_in_run(empty, 3).is_none());
let one_empty = [&Space::Empty];
assert!(winner_in_run(one_empty, 1).is_none());
assert!(winner_in_run(one_empty, 2).is_none());
let one_x = [&Space::Stone(Player::X)];
assert_eq!(winner_in_run(one_x, 1), Some(Player::X));
assert!(winner_in_run(one_x, 2).is_none());
let one_o = [&Space::Stone(Player::O)];
assert_eq!(winner_in_run(one_o, 1), Some(Player::O));
assert!(winner_in_run(one_o, 2).is_none());
}
#[test]
fn single_player() {
let right_run = [
&Space::Empty,
&Space::Empty,
&Space::Stone(Player::X),
&Space::Stone(Player::X),
&Space::Stone(Player::X),
];
assert_eq!(winner_in_run(right_run, 3), Some(Player::X));
assert!(winner_in_run(right_run, 4).is_none());
let interrupted = [
&Space::Stone(Player::X),
&Space::Stone(Player::X),
&Space::Empty,
&Space::Stone(Player::X),
&Space::Stone(Player::X),
];
assert_eq!(winner_in_run(interrupted, 2), Some(Player::X));
assert!(winner_in_run(interrupted, 3).is_none());
let bookend = [
&Space::Empty,
&Space::Stone(Player::X),
&Space::Stone(Player::X),
&Space::Stone(Player::X),
&Space::Empty,
];
assert_eq!(winner_in_run(bookend, 3), Some(Player::X));
assert!(winner_in_run(bookend, 4).is_none());
}
#[test]
fn two_player() {
let left_heavy = [
&Space::Stone(Player::X),
&Space::Stone(Player::X),
&Space::Stone(Player::O),
];
assert_eq!(winner_in_run(left_heavy, 2), Some(Player::X));
assert!(winner_in_run(left_heavy, 3).is_none());
let right_heavy = [
&Space::Stone(Player::O),
&Space::Stone(Player::X),
&Space::Stone(Player::X),
];
assert_eq!(winner_in_run(right_heavy, 2), Some(Player::X));
assert!(winner_in_run(right_heavy, 3).is_none());
let interrupted = [
&Space::Stone(Player::X),
&Space::Stone(Player::O),
&Space::Stone(Player::X),
];
assert!(winner_in_run(interrupted, 2).is_none());
assert!(winner_in_run(interrupted, 3).is_none());
}
}
#[cfg(test)]
mod test_winner_in_runs {
use super::*;
use std::iter;
#[test]
fn trivial() {
let empty: iter::Empty<iter::Empty<&Space>> = iter::empty();
assert!(winner_in_runs(empty, 1).is_none());
let single = iter::once(iter::once(&Space::Stone(Player::X)));
assert_eq!(winner_in_runs(single, 1), Some(Player::X));
}
#[test]
fn several_runs() {
let delayed = [
iter::once(&Space::Empty),
iter::once(&Space::Stone(Player::X)),
];
assert_eq!(winner_in_runs(delayed, 1), Some(Player::X));
let all_empty = [
iter::once(&Space::Empty),
iter::once(&Space::Empty),
iter::once(&Space::Empty),
];
assert!(winner_in_runs(all_empty, 1).is_none());
}
}
#[cfg(test)]
mod test_play_at {
use super::*;
use crate::Space;
#[test]
fn rejects_finished_games() {
let mut drawn: MnkGame<1, 1, 1> = MnkGame::new();
drawn.status = GameStatus::Drawn;
assert_eq!(
drawn.play_at(0, 0),
Err(PlayError::GameOver(GameStatus::Drawn))
);
assert_eq!(drawn.board, MnkBoard::<1, 1>::new());
let mut x_won: MnkGame<1, 1, 1> = MnkGame::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>::new());
let mut o_won: MnkGame<1, 1, 1> = MnkGame::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>::new());
}
#[test]
fn rejects_place_errors() {
let mut empty: MnkGame<1, 1, 1> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::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> = MnkGame::new();
ongoing.update_status();
assert_eq!(ongoing.status, GameStatus::Ongoing { next: Player::X });
}
}
#[cfg(test)]
mod test_winner {
use super::*;
fn ongoing_game<const R: usize, const C: usize, const K: usize>(
board: MnkBoard<R, C>,
) -> MnkGame<R, C, K> {
MnkGame {
board,
status: GameStatus::Ongoing { next: Player::X },
}
}
#[test]
fn draws() {
let empty_0x0: MnkGame<0, 0, 1> = ongoing_game(MnkBoard::new());
assert!(empty_0x0.winner().is_none());
let empty_3x3: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::new());
assert!(empty_3x3.winner().is_none());
let drawn_3x3: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::from([
[
Space::Stone(Player::X),
Space::Stone(Player::O),
Space::Stone(Player::X),
],
[
Space::Stone(Player::X),
Space::Stone(Player::O),
Space::Stone(Player::O),
],
[
Space::Stone(Player::O),
Space::Stone(Player::X),
Space::Stone(Player::X),
],
]));
assert!(drawn_3x3.winner().is_none());
}
#[test]
fn row_win() {
let row_win: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::from([
[
Space::Stone(Player::X),
Space::Stone(Player::X),
Space::Stone(Player::X),
],
[Space::Empty, Space::Empty, Space::Empty],
[Space::Empty, Space::Empty, Space::Empty],
]));
assert_eq!(row_win.winner(), Some(Player::X));
}
#[test]
fn column_win() {
let column_win: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::from([
[Space::Stone(Player::X), Space::Empty, Space::Empty],
[Space::Stone(Player::X), Space::Empty, Space::Empty],
[Space::Stone(Player::X), Space::Empty, Space::Empty],
]));
assert_eq!(column_win.winner(), Some(Player::X));
}
#[test]
fn top_right_win() {
let top_right_win: MnkGame<3, 3, 2> = ongoing_game(MnkBoard::from([
[Space::Stone(Player::X), Space::Empty, Space::Empty],
[Space::Empty, Space::Stone(Player::X), Space::Empty],
[Space::Empty, Space::Empty, Space::Empty],
]));
assert_eq!(top_right_win.winner(), Some(Player::X));
}
#[test]
fn left_down_win() {
let left_down_win: MnkGame<4, 3, 3> = ongoing_game(MnkBoard::from([
[Space::Empty, Space::Empty, Space::Empty],
[Space::Stone(Player::X), Space::Empty, Space::Empty],
[Space::Empty, Space::Stone(Player::X), Space::Empty],
[Space::Empty, Space::Empty, Space::Stone(Player::X)],
]));
assert_eq!(left_down_win.winner(), Some(Player::X));
}
#[test]
fn top_left_win() {
let top_left_win: MnkGame<3, 3, 2> = ongoing_game(MnkBoard::from([
[Space::Empty, Space::Empty, Space::Stone(Player::X)],
[Space::Empty, Space::Stone(Player::X), Space::Empty],
[Space::Empty, Space::Empty, Space::Empty],
]));
assert_eq!(top_left_win.winner(), Some(Player::X));
}
#[test]
fn right_down_win() {
let right_down_win: MnkGame<4, 3, 3> = ongoing_game(MnkBoard::from([
[Space::Empty, Space::Empty, Space::Empty],
[Space::Empty, Space::Empty, Space::Stone(Player::X)],
[Space::Empty, Space::Stone(Player::X), Space::Empty],
[Space::Stone(Player::X), Space::Empty, Space::Empty],
]));
assert_eq!(right_down_win.winner(), Some(Player::X));
}
}
#[cfg(test)]
mod test_mnk_game_display {
use crate::{GameStatus, MnkGame, Player};
#[test]
fn draw() {
let mut draw: MnkGame<1, 1, 1> = MnkGame::new();
draw.status = GameStatus::Drawn;
assert_eq!(
draw.to_string(),
"+-+\n\
| |\n\
+-+\n\
Draw"
);
}
#[test]
fn ongoing() {
let x_next: MnkGame<1, 1, 1> = MnkGame::new();
assert_eq!(
x_next.to_string(),
"+-+\n\
| |\n\
+-+\n\
Next: X"
);
let mut o_next: MnkGame<1, 1, 1> = MnkGame::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> = MnkGame::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> = MnkGame::new();
o_won.status = GameStatus::Won(Player::O);
assert_eq!(
o_won.to_string(),
"+-+\n\
| |\n\
+-+\n\
O won!"
);
}
}