use crate::board::{Action, Board, Color};
use crate::error::GameError;
#[derive(Debug, Clone)]
pub struct Game {
board: Board,
prev_state: Option<(Board, Action)>,
current_player: Color,
forbidden_action: Option<Action>,
legal_actions: Vec<Action>,
}
impl std::default::Default for Game {
fn default() -> Self {
Self::new()
}
}
impl Game {
pub fn new() -> Self {
let board = Board::default();
let first_player = Color::B;
Self {
board,
prev_state: None,
current_player: first_player,
forbidden_action: None,
legal_actions: Self::collect_legal_actions(board, first_player, None),
}
}
pub fn board(&self) -> &Board {
&self.board
}
pub fn current_player(&self) -> Color {
self.current_player
}
pub fn is_legal_action(&self, action: Action) -> bool {
self.legal_actions.contains(&action)
}
pub fn legal_actions(&self) -> &[Action] {
&self.legal_actions
}
pub fn apply_action(&mut self, action: Action) -> Result<(), GameError> {
if matches!(self.result(), GameResult::Finished { .. }) {
return Err(GameError::GameAlreadyOver);
}
if !self.is_legal_action(action) {
return Err(GameError::IllegalAction(action));
}
let prev_board = self.board;
self.board.apply_unchecked(action);
self.prev_state = match self.forbidden_action {
None => Some((prev_board, action)), Some(_) => None, };
let next_player = self.current_player.opposite();
self.current_player = next_player;
self.forbidden_action = None;
self.legal_actions = Self::collect_legal_actions(self.board, next_player, None);
Ok(())
}
pub fn can_declare_second_best(&self) -> bool {
self.prev_state.is_some()
}
pub fn declare_second_best(&mut self) -> Result<(), GameError> {
let Some((prev_board, prev_action)) = self.prev_state.take() else {
return Err(GameError::CannotDeclareSecondBest);
};
self.board = prev_board;
let prev_player = self.current_player.opposite();
self.current_player = prev_player;
self.forbidden_action = Some(prev_action);
self.legal_actions =
Self::collect_legal_actions(self.board, prev_player, Some(prev_action));
Ok(())
}
fn collect_legal_actions(
board: Board,
player: Color,
forbidden_action: Option<Action>,
) -> Vec<Action> {
if board.lines_up(player) || board.lines_up(player.opposite()) {
return Vec::new();
}
board
.legal_action_iter(player)
.filter(|&a| Some(a) != forbidden_action)
.collect()
}
pub fn result(&self) -> GameResult {
if self.can_declare_second_best() {
return GameResult::InProgress;
}
let prev_player = self.current_player.opposite();
if self.board.lines_up(prev_player) {
GameResult::with_winner(prev_player)
} else if self.board.lines_up(self.current_player) {
GameResult::with_winner(self.current_player)
} else if self.legal_actions.is_empty() {
GameResult::with_winner(prev_player)
} else {
GameResult::InProgress
}
}
pub fn is_finished(&self) -> bool {
self.result().is_finished()
}
pub fn is_in_progress(&self) -> bool {
self.result().is_in_progress()
}
}
impl std::fmt::Display for Game {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Board state:")?;
writeln!(f, "{}", self.board)?;
writeln!(f, "Current player: {:?}", self.current_player)?;
writeln!(f, "{}", self.result())?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GameResult {
Finished { winner: Color },
InProgress,
}
impl GameResult {
fn with_winner(winner: Color) -> Self {
GameResult::Finished { winner }
}
pub fn winner(&self) -> Option<Color> {
use GameResult::*;
match self {
Finished { winner } => Some(*winner),
InProgress => None,
}
}
pub fn is_finished(&self) -> bool {
matches!(self, GameResult::Finished { .. })
}
pub fn is_in_progress(&self) -> bool {
matches!(self, GameResult::InProgress)
}
}
impl std::fmt::Display for GameResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GameResult::Finished { winner } => write!(f, "Game over - Winner: {}", winner),
GameResult::InProgress => write!(f, "Game in progress"),
}
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[derive(Debug, Clone)]
struct RandomGameGenerator {
game: Game,
num: usize,
}
impl RandomGameGenerator {
pub fn new(num: usize) -> Self {
Self {
game: Game::new(),
num,
}
}
pub fn num(&self) -> usize {
self.num
}
}
impl Iterator for RandomGameGenerator {
type Item = (Game, Option<Action>);
fn next(&mut self) -> Option<Self::Item> {
fn next_rand(state: usize) -> usize {
state.wrapping_mul(6364136223846793005).wrapping_add(1)
}
if self.game.is_finished() {
return None;
}
if self.game.can_declare_second_best() {
self.game.declare_second_best().unwrap();
return Some((self.game.clone(), None));
}
let legal_actions = self.game.legal_actions();
if legal_actions.is_empty() {
return None;
}
let action = legal_actions[self.num % legal_actions.len()];
self.num = next_rand(self.num);
self.game.apply_action(action).unwrap();
Some((self.game.clone(), Some(action)))
}
}
fn test_with_random_games<F>(test_fn: F) -> usize
where
F: Fn(&Game, Option<&Action>),
{
const NUM_GAMES: usize = 5;
let mut seed = 12345;
for _ in 0..NUM_GAMES {
let mut generator = RandomGameGenerator::new(seed);
for (game, action) in &mut generator {
test_fn(&game, action.as_ref());
}
seed = generator.num();
}
seed
}
#[test]
fn test_new() {
let game = Game::new();
assert_eq!(*game.board(), Board::default());
assert_eq!(game.current_player(), Color::B);
}
#[test]
fn test_board() {
const NUM_GAMES: usize = 5;
let mut seed = 12345;
for _ in 0..NUM_GAMES {
let mut generator = RandomGameGenerator::new(seed);
let mut board = Board::default();
for (game, action) in &mut generator {
if let Some(a) = action {
assert!(board.apply(a).is_ok());
assert_eq!(*game.board(), board);
} else {
board = *game.board();
}
}
seed = generator.num();
}
}
#[test]
fn test_current_player() {
test_with_random_games(|game, _| {
if game.is_finished() {
return;
}
let player = game.current_player();
for &action in game.legal_actions() {
let mut game_clone = game.clone();
assert!(game_clone.apply_action(action).is_ok());
let next_player = game_clone.current_player();
assert_eq!(next_player, player.opposite());
}
});
}
#[test]
fn test_is_legal_action() {
test_with_random_games(|game, _| {
if game.is_finished() {
return;
}
test_is_legal_action_for_put(game);
test_is_legal_action_for_move(game);
});
}
fn test_is_legal_action_for_put(game: &Game) {
let lines_up = game.board().lines_up(Color::B) || game.board().lines_up(Color::W);
let player = game.current_player();
let num_pieces = game.board().count_pieces(player);
for pos in Position::iter() {
for color in [Color::B, Color::W] {
let action = Action::Put(pos, color);
if color != player {
assert!(!game.is_legal_action(action));
continue;
}
if num_pieces >= 8 {
assert!(!game.is_legal_action(action));
continue;
}
if lines_up {
assert!(!game.is_legal_action(action));
continue;
}
if game.forbidden_action == Some(action) {
assert!(!game.is_legal_action(action));
continue;
}
assert_eq!(
game.board().get_pieces_at(pos).len() < 3,
game.is_legal_action(action)
);
}
}
}
fn test_is_legal_action_for_move(game: &Game) {
let lines_up = game.board().lines_up(Color::B) || game.board().lines_up(Color::W);
let player = game.current_player();
let num_pieces = game.board().count_pieces(player);
for from in Position::iter() {
use Position::*;
let legal_dsts = match from {
N => [NE, NW, S],
NE => [N, E, SW],
E => [NE, SE, W],
SE => [E, S, NW],
S => [SE, SW, N],
SW => [S, W, NE],
W => [SW, NW, E],
NW => [W, N, SE],
};
for to in Position::iter() {
let action = Action::Move(from, to);
if num_pieces < 8 {
assert!(!game.is_legal_action(action));
continue;
}
if lines_up {
assert!(!game.is_legal_action(action));
continue;
}
if !legal_dsts.contains(&to) {
assert!(!game.is_legal_action(action));
continue;
}
if game.forbidden_action == Some(action) {
assert!(!game.is_legal_action(action));
continue;
}
let pieces_src = game.board().get_pieces_at(from);
let pieces_dst = game.board().get_pieces_at(to);
assert_eq!(
(pieces_src.last() == Some(&player)) && (pieces_dst.len() < 3),
game.is_legal_action(action)
);
}
}
}
#[test]
fn test_legal_actions() {
test_with_random_games(|game, _| {
for pos in Position::iter() {
for color in [Color::B, Color::W] {
let action = Action::Put(pos, color);
assert_eq!(
game.legal_actions().contains(&action),
game.is_legal_action(action)
);
}
}
for from in Position::iter() {
for to in Position::iter() {
let action = Action::Move(from, to);
assert_eq!(
game.legal_actions().contains(&action),
game.is_legal_action(action)
);
}
}
});
}
#[test]
fn test_apply_action() {
test_with_random_games(|game, _| {
for pos in Position::iter() {
for color in [Color::B, Color::W] {
let action = Action::Put(pos, color);
let mut game_clone = game.clone();
assert_eq!(
game.is_legal_action(action),
game_clone.apply_action(action).is_ok()
);
}
}
for from in Position::iter() {
for to in Position::iter() {
let action = Action::Move(from, to);
let mut game_clone = game.clone();
assert_eq!(
game.is_legal_action(action),
game_clone.apply_action(action).is_ok()
);
}
}
});
}
#[test]
fn test_can_declare_second_best() {
test_with_random_games(|game, prev_action| {
for &action in game.legal_actions() {
let mut game_clone = game.clone();
assert!(game_clone.apply_action(action).is_ok());
assert_eq!(game_clone.can_declare_second_best(), prev_action.is_some());
}
});
}
#[test]
fn test_declare_second_best() {
test_with_random_games(|game, _| {
let prev_board = game.board();
for &action in game.legal_actions() {
let mut game_clone = game.clone();
assert!(game_clone.apply_action(action).is_ok());
let can_declare = game_clone.can_declare_second_best();
assert_eq!(game_clone.declare_second_best().is_ok(), can_declare);
if can_declare {
assert_eq!(game_clone.board(), prev_board);
}
}
});
}
#[test]
fn test_result() {
test_with_random_games(|game, _| {
let result = game.result();
if game.can_declare_second_best() {
assert!(matches!(result, GameResult::InProgress));
return;
}
let player = game.current_player();
let opponent = player.opposite();
let board = game.board();
match (board.lines_up(player), board.lines_up(player.opposite())) {
(true, false) => assert_eq!(result, GameResult::Finished { winner: player }),
(false, true) => assert_eq!(result, GameResult::Finished { winner: opponent }),
(true, true) => assert_eq!(result, GameResult::Finished { winner: opponent }),
(false, false) => {
if game.legal_actions().is_empty() {
assert_eq!(result, GameResult::Finished { winner: opponent })
} else {
assert_eq!(result, GameResult::InProgress)
}
}
}
});
}
#[test]
fn test_is_finished() {
test_with_random_games(|game, _| {
assert_eq!(game.result().is_finished(), game.is_finished())
});
}
#[test]
fn test_is_in_progress() {
test_with_random_games(|game, _| {
assert_eq!(game.result().is_in_progress(), game.is_in_progress())
});
}
}