use core::convert::Infallible;
use thiserror::Error;
const BOARD_WIDTH: usize = 15;
const BOARD_HEIGHT: usize = 15;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Game {
board: [[Option<Player>; BOARD_HEIGHT]; BOARD_WIDTH],
move_count: usize,
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("game ended")]
GameEnded,
}
struct LastMove {
player: Player,
row: usize,
col: usize,
}
impl Game {
pub const fn new() -> Result<Self, Infallible> {
Ok(Self {
board: [[None; BOARD_HEIGHT]; BOARD_WIDTH],
move_count: 0,
next_player: Player::Player0,
status: Status::Ongoing,
})
}
pub const fn get(&self, row: usize, col: usize) -> Option<Player> {
self.board[row][col]
}
pub fn put(&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);
}
self.board[row][col] = Some(self.next_player);
let last_move = LastMove {
player: self.next_player,
row,
col,
};
self.move_count += 1;
self.next_player = self.next_player.other();
self.update_status(last_move);
Ok(())
}
pub const fn next_player(&self) -> Player {
self.next_player
}
pub const fn status(&self) -> &Status {
&self.status
}
fn update_status(&mut self, last_move: LastMove) {
const DIRECTIONS: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
for (row_delta, col_delta) in DIRECTIONS {
let connected = 1
+ self.count_direction(&last_move, row_delta, col_delta)
+ self.count_direction(&last_move, -row_delta, -col_delta);
if connected >= 5 {
self.status = Status::Win(last_move.player);
return;
}
}
if self.move_count == BOARD_HEIGHT * BOARD_WIDTH {
self.status = Status::Draw;
}
}
fn count_direction(&self, last_move: &LastMove, row_delta: isize, col_delta: isize) -> usize {
let mut row = last_move.row as isize + row_delta;
let mut col = last_move.col as isize + col_delta;
let mut count = 0;
while row >= 0 && row < BOARD_HEIGHT as isize && col >= 0 && col < BOARD_WIDTH as isize {
if self.get(row as usize, col as usize) != Some(last_move.player) {
break;
}
count += 1;
row += row_delta;
col += col_delta;
}
count
}
}
impl Player {
pub const fn other(self) -> Self {
match self {
Player::Player0 => Player::Player1,
Player::Player1 => Player::Player0,
}
}
}
#[cfg(test)]
mod tests {
use crate::gomoku::*;
#[test]
fn separated_pieces_do_not_win() {
let mut game = Game::new().unwrap();
game.put(7, 0).unwrap();
game.put(0, 0).unwrap();
game.put(7, 2).unwrap();
game.put(0, 2).unwrap();
game.put(7, 6).unwrap();
game.put(0, 4).unwrap();
game.put(7, 8).unwrap();
game.put(0, 6).unwrap();
game.put(7, 4).unwrap();
assert_eq!(game.status(), &Status::Ongoing);
}
#[test]
fn detects_diagonal_win_near_board_edge() {
let mut game = Game::new().unwrap();
game.put(0, 4).unwrap();
game.put(14, 0).unwrap();
game.put(2, 6).unwrap();
game.put(14, 2).unwrap();
game.put(3, 7).unwrap();
game.put(14, 4).unwrap();
game.put(4, 8).unwrap();
game.put(14, 6).unwrap();
game.put(1, 5).unwrap();
assert_eq!(game.status(), &Status::Win(Player::Player0));
}
#[test]
fn detects_anti_diagonal_win_near_board_edge() {
let mut game = Game::new().unwrap();
game.put(0, 8).unwrap();
game.put(14, 0).unwrap();
game.put(2, 6).unwrap();
game.put(14, 2).unwrap();
game.put(3, 5).unwrap();
game.put(14, 4).unwrap();
game.put(4, 4).unwrap();
game.put(14, 6).unwrap();
game.put(1, 7).unwrap();
assert_eq!(game.status(), &Status::Win(Player::Player0));
}
#[test]
fn six_in_a_row_wins() {
let mut game = Game::new().unwrap();
game.put(7, 0).unwrap();
game.put(0, 0).unwrap();
game.put(7, 1).unwrap();
game.put(0, 2).unwrap();
game.put(7, 2).unwrap();
game.put(0, 4).unwrap();
game.put(7, 4).unwrap();
game.put(0, 6).unwrap();
game.put(7, 5).unwrap();
game.put(0, 8).unwrap();
game.put(7, 3).unwrap();
assert_eq!(game.status(), &Status::Win(Player::Player0));
}
}