use core::{
convert::Infallible,
fmt::{Debug, Error as FmtError, Formatter},
};
use thiserror::Error;
const BOARD_WIDTH: usize = 7;
const BOARD_HEIGHT: usize = 6;
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Game {
columns: [Column; 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("column filled")]
ColumnFilled,
#[error("game ended")]
GameEnded,
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct Column {
cells: [Player; BOARD_HEIGHT],
filled: usize,
}
struct LastMove {
player: Player,
row: usize,
col: usize,
}
impl Game {
pub const fn new() -> Result<Self, Infallible> {
Ok(Self {
columns: [Column {
cells: [Player::Player0; BOARD_HEIGHT],
filled: 0,
}; BOARD_WIDTH],
move_count: 0,
next_player: Player::Player0,
status: Status::Ongoing,
})
}
pub const fn get(&self, row: usize, col: usize) -> Option<Player> {
let column = &self.columns[col];
if row >= BOARD_HEIGHT - column.filled {
Some(column.cells[row])
} else {
None
}
}
pub fn put(&mut self, col: usize) -> Result<(), Error> {
if matches!(self.status, Status::Win(_) | Status::Draw) {
return Err(Error::GameEnded);
}
if self.columns[col].filled == BOARD_HEIGHT {
return Err(Error::ColumnFilled);
}
let column = &mut self.columns[col];
let row = BOARD_HEIGHT - 1 - column.filled;
column.cells[row] = self.next_player;
column.filled += 1;
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 >= 4 {
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,
}
}
}
impl Debug for Game {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
let mut board = [[None; BOARD_HEIGHT]; BOARD_WIDTH];
for (col, board_column) in board.iter_mut().enumerate() {
let column = &self.columns[col];
for (row, cell) in board_column
.iter_mut()
.enumerate()
.skip(BOARD_HEIGHT - column.filled)
{
*cell = Some(column.cells[row]);
}
}
f.debug_struct("Game")
.field("board", &board)
.field("move_count", &self.move_count)
.field("next_player", &self.next_player)
.field("status", &self.status)
.finish()
}
}
#[cfg(test)]
mod tests {
use crate::connect_four::*;
use core::fmt::Write as _;
#[test]
fn test() {
let mut game = Game::new().unwrap();
game.put(3).unwrap();
game.put(2).unwrap();
game.put(2).unwrap();
game.put(1).unwrap();
game.put(1).unwrap();
game.put(0).unwrap();
game.put(3).unwrap();
game.put(0).unwrap();
game.put(1).unwrap();
game.put(6).unwrap();
game.put(2).unwrap();
game.put(6).unwrap();
game.put(3).unwrap();
game.put(5).unwrap();
game.put(0).unwrap();
assert_eq!(game.status(), &Status::Win(Player::Player0));
}
#[test]
fn detects_diagonal_win_near_board_edge() {
let mut game = Game::new().unwrap();
for col in [
6, 6, 6, 6, 6, 6, 5, 5, 5, 4, 5, 5, 5, 4, 3, 4, 4, 0, 4, 4, 3, 3, 3, 3, 3,
] {
game.put(col).unwrap();
}
assert_eq!(game.get(0, 3), Some(Player::Player0));
assert_eq!(game.get(1, 4), Some(Player::Player0));
assert_eq!(game.get(2, 5), Some(Player::Player0));
assert_eq!(game.get(3, 6), Some(Player::Player0));
assert_eq!(game.status(), &Status::Win(Player::Player0));
}
#[test]
fn debug_board_uses_actual_row_positions() {
let mut game = Game::new().unwrap();
game.put(3).unwrap();
game.put(2).unwrap();
extern crate alloc;
let mut debug = alloc::string::String::new();
write!(debug, "{game:?}").unwrap();
assert!(debug.contains("[None, None, None, None, None, Some(Player1)]"));
assert!(debug.contains("[None, None, None, None, None, Some(Player0)]"));
assert!(!debug.contains("[Some(Player0), None, None, None, None, None]"));
}
}