use core::{cmp::Ordering, convert::Infallible};
use thiserror::Error;
const BOARD_WIDTH: usize = 8;
const BOARD_HEIGHT: usize = 8;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Game {
board: [[Option<Player>; BOARD_HEIGHT]; BOARD_WIDTH],
next_player: Player,
status: Status,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Player {
Player0,
Player1,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Status {
Ongoing,
Draw,
Win(Player),
}
#[derive(Debug, Error)]
pub enum Error {
#[error("position occupied")]
PositionOccupied,
#[error("invalid position")]
InvalidPosition,
#[error("game ended")]
GameEnded,
}
impl Game {
pub const fn new() -> Result<Self, Infallible> {
let mut board = [[None; BOARD_HEIGHT]; BOARD_WIDTH];
board[3][3] = Some(Player::Player0);
board[4][4] = Some(Player::Player0);
board[3][4] = Some(Player::Player1);
board[4][3] = Some(Player::Player1);
Ok(Self {
board,
next_player: Player::Player0,
status: Status::Ongoing,
})
}
pub const fn get(&self, row: usize, col: usize) -> Option<Player> {
self.board[row][col]
}
pub fn place(&mut self, row: usize, col: usize) -> Result<(), Error> {
if matches!(self.status, Status::Win(_) | Status::Draw) {
return Err(Error::GameEnded);
}
if self.board[row][col].is_some() {
return Err(Error::PositionOccupied);
}
let flipping_left_range = (0..col).rev();
let flipping_right_range = col + 1..BOARD_WIDTH;
let flipping_up_range = (0..row).rev();
let flipping_down_range = row + 1..BOARD_HEIGHT;
let mut any_flipped = false;
any_flipped |= self.try_flip_line(flipping_left_range.clone().map(|col| (row, col)));
any_flipped |= self.try_flip_line(flipping_right_range.clone().map(|col| (row, col)));
any_flipped |= self.try_flip_line(flipping_up_range.clone().map(|row| (row, col)));
any_flipped |= self.try_flip_line(flipping_down_range.clone().map(|row| (row, col)));
any_flipped |=
self.try_flip_line(flipping_up_range.clone().zip(flipping_left_range.clone()));
any_flipped |=
self.try_flip_line(flipping_up_range.clone().zip(flipping_right_range.clone()));
any_flipped |=
self.try_flip_line(flipping_down_range.clone().zip(flipping_left_range.clone()));
any_flipped |= self.try_flip_line(flipping_down_range.zip(flipping_right_range));
if !any_flipped {
return Err(Error::InvalidPosition);
}
self.board[row][col] = Some(self.next_player);
self.next_player = self.next_player.other();
if self.can_current_player_move() {
return Ok(());
}
self.next_player = self.next_player.other();
if self.can_current_player_move() {
return Ok(());
}
let mut player0_count = 0u8;
let mut player1_count = 0u8;
for row in 0..BOARD_HEIGHT {
for col in 0..BOARD_WIDTH {
match self.get(row, col) {
Some(Player::Player0) => player0_count += 1,
Some(Player::Player1) => player1_count += 1,
None => {}
}
}
}
match player0_count.cmp(&player1_count) {
Ordering::Greater => self.status = Status::Win(Player::Player0),
Ordering::Less => self.status = Status::Win(Player::Player1),
Ordering::Equal => self.status = Status::Draw,
}
Ok(())
}
pub fn can_place_at(&self, row: usize, col: usize) -> Result<(), Error> {
if matches!(self.status, Status::Win(_) | Status::Draw) {
return Err(Error::GameEnded);
}
if self.board[row][col].is_some() {
return Err(Error::PositionOccupied);
}
let checking_left_range = (0..col).rev();
let checking_right_range = col + 1..BOARD_WIDTH;
let checking_up_range = (0..row).rev();
let checking_down_range = row + 1..BOARD_HEIGHT;
if self.can_flip_line(checking_left_range.clone().map(|col| (row, col))) {
return Ok(());
}
if self.can_flip_line(checking_right_range.clone().map(|col| (row, col))) {
return Ok(());
}
if self.can_flip_line(checking_up_range.clone().map(|row| (row, col))) {
return Ok(());
}
if self.can_flip_line(checking_down_range.clone().map(|row| (row, col))) {
return Ok(());
}
if self.can_flip_line(checking_up_range.clone().zip(checking_left_range.clone())) {
return Ok(());
}
if self.can_flip_line(checking_up_range.clone().zip(checking_right_range.clone())) {
return Ok(());
}
if self.can_flip_line(checking_down_range.clone().zip(checking_left_range.clone())) {
return Ok(());
}
if self.can_flip_line(checking_down_range.zip(checking_right_range)) {
return Ok(());
}
Err(Error::InvalidPosition)
}
pub const fn next_player(&self) -> Player {
self.next_player
}
pub const fn status(&self) -> &Status {
&self.status
}
fn can_current_player_move(&self) -> bool {
for row in 0..BOARD_HEIGHT {
for col in 0..BOARD_WIDTH {
match self.can_place_at(row, col) {
Err(Error::PositionOccupied | Error::InvalidPosition) => continue,
Ok(()) => return true,
Err(Error::GameEnded) => unreachable!(),
}
}
}
false
}
fn try_flip_line(&mut self, line: impl Iterator<Item = (usize, usize)> + Clone) -> bool {
let mut skipped = 0;
let Some((row, col)) = line
.clone()
.skip_while(|(row, col)| {
let is_other_player = self.get(*row, *col) == Some(self.next_player().other());
skipped += is_other_player as usize;
is_other_player
})
.next()
else {
return false;
};
if skipped == 0 || self.get(row, col) != Some(self.next_player()) {
return false;
}
for (row, col) in line.take(skipped) {
self.board[row][col] = Some(self.next_player());
}
true
}
fn can_flip_line(&self, line: impl Iterator<Item = (usize, usize)>) -> bool {
let mut skipped = false;
let Some((row, col)) = line
.skip_while(|(row, col)| {
let is_other_player = self.get(*row, *col) == Some(self.next_player().other());
skipped |= is_other_player;
is_other_player
})
.next()
else {
return false;
};
skipped && self.get(row, col) == Some(self.next_player())
}
}
impl Player {
pub const fn other(self) -> Self {
match self {
Player::Player0 => Player::Player1,
Player::Player1 => Player::Player0,
}
}
}
#[cfg(test)]
mod tests {
use crate::reversi::*;
#[test]
fn test() {
let mut game = Game::new().unwrap();
game.can_place_at(2, 4).unwrap();
game.place(2, 4).unwrap();
game.place(2, 3).unwrap();
assert!(matches!(game.place(2, 3), Err(Error::PositionOccupied)));
assert!(matches!(game.place(2, 6), Err(Error::InvalidPosition)));
}
}