use std::collections::HashSet;
use std::error;
use std::fmt;
use crate::board;
pub use crate::board::Position;
const BOARD_SIZE: board::Size = board::Size {
rows: 3,
columns: 3,
};
#[derive(Clone)]
pub struct Game {
board: board::Board,
state: State,
next_game_starting_state: State,
}
impl Game {
pub fn new() -> Self {
let board = board::Board::new(BOARD_SIZE);
let state = State::PlayerXMove;
let next_game_starting_state = Self::next_players_turn(&state);
Game {
board,
state,
next_game_starting_state,
}
}
pub fn board(&self) -> &board::Board {
&self.board
}
pub fn state(&self) -> State {
self.state.clone()
}
pub fn free_positions(&self) -> FreePositions {
FreePositions {
board_iter: self.board.iter(),
is_game_over: self.state.is_game_over(),
}
}
pub fn can_move(&self, position: board::Position) -> bool {
if self.state.is_game_over() || !self.board.contains(position) {
false
} else {
self.board().get(position).unwrap() == board::Owner::None
}
}
pub fn do_move(&mut self, position: board::Position) -> Result<State, Error> {
let new_owner = match self.state {
State::PlayerXMove => board::Owner::PlayerX,
State::PlayerOMove => board::Owner::PlayerO,
_ => return Err(Error::GameOver),
};
let existing_owner = match self.board.get_mut(position) {
Some(owner) => owner,
None => return Err(Error::InvalidPosition(position)),
};
if *existing_owner != board::Owner::None {
return Err(Error::PositionAlreadyOwned(position, *existing_owner));
}
*existing_owner = new_owner;
self.state = self.calculate_next_state();
Ok(self.state())
}
pub fn start_next_game(&mut self) -> State {
self.board = board::Board::new(BOARD_SIZE);
self.state = self.next_game_starting_state.clone();
self.next_game_starting_state = Self::next_players_turn(&self.state);
self.state()
}
fn calculate_next_state(&self) -> State {
let winning_positions = self.find_winning_positions();
if !winning_positions.is_empty() {
self.get_winning_player(winning_positions)
} else if self
.board
.iter()
.find(|(_position, owner)| *owner == board::Owner::None)
.is_none()
{
State::CatsGame
} else {
Self::next_players_turn(&self.state)
}
}
fn find_winning_positions(&self) -> HashSet<board::Position> {
const MAX_WINNING_POSITIONS: usize = 5;
debug_assert_eq!(
self.board.size(),
board::Size {
rows: 3,
columns: 3
}
);
let mut winning_positions = HashSet::with_capacity(MAX_WINNING_POSITIONS);
self.check_rows(&mut winning_positions);
self.check_columns(&mut winning_positions);
self.check_top_left_to_bottom_right(&mut winning_positions);
self.check_top_right_to_bottom_left(&mut winning_positions);
winning_positions
}
fn check_rows(&self, mut winning_positions: &mut HashSet<Position>) {
for row in 0..self.board.size().rows {
let starting_position = board::Position { row, column: 0 };
let next_position_fn = |x: board::Position| board::Position {
row: x.row,
column: x.column + 1,
};
self.check_sequence(&mut winning_positions, starting_position, next_position_fn);
}
}
fn check_columns(&self, mut winning_positions: &mut HashSet<Position>) {
for column in 0..self.board.size().columns {
let starting_position = board::Position { row: 0, column };
let next_position_fn = |x: board::Position| board::Position {
row: x.row + 1,
column: x.column,
};
self.check_sequence(&mut winning_positions, starting_position, next_position_fn);
}
}
fn check_top_left_to_bottom_right(&self, mut winning_positions: &mut HashSet<Position>) {
let starting_position = board::Position { row: 0, column: 0 };
let next_position_fn = |x: board::Position| board::Position {
row: x.row + 1,
column: x.column + 1,
};
self.check_sequence(&mut winning_positions, starting_position, next_position_fn);
}
fn check_top_right_to_bottom_left(&self, mut winning_positions: &mut HashSet<Position>) {
let starting_position = board::Position { row: 0, column: 2 };
let next_position_fn = |x: board::Position| board::Position {
row: x.row + 1,
column: x.column - 1,
};
self.check_sequence(&mut winning_positions, starting_position, next_position_fn);
}
fn check_sequence(
&self,
winning_positions: &mut HashSet<board::Position>,
starting_position: board::Position,
next_position_fn: fn(board::Position) -> board::Position,
) {
let initial_owner = self
.board
.get(starting_position)
.unwrap_or(board::Owner::None);
if initial_owner == board::Owner::None {
return;
}
const POSITIONS_SIZE: usize = 3;
let mut positions: [board::Position; POSITIONS_SIZE] = [starting_position; POSITIONS_SIZE];
let mut positions_index = 0;
let mut position = next_position_fn(starting_position);
while let Some(owner) = self.board.get(position) {
if owner != initial_owner {
return;
}
positions_index += 1;
positions[positions_index] = position;
position = next_position_fn(position);
}
for p in &positions {
winning_positions.insert(*p);
}
}
fn get_winning_player(&self, winning_positions: HashSet<board::Position>) -> State {
assert!(!winning_positions.is_empty());
let winning_owner = self
.board
.get(*winning_positions.iter().next().unwrap())
.unwrap();
debug_assert!(
winning_positions
.iter()
.find(|&&x| self.board.get(x).unwrap() != winning_owner)
.is_none(),
"Multiple owners found for positions in the set of winning positions. \
This can be caused by not updating the state of the game after every move."
);
match winning_owner {
board::Owner::PlayerX => State::PlayerXWin(winning_positions),
board::Owner::PlayerO => State::PlayerOWin(winning_positions),
board::Owner::None => panic!(
"The game thinks there should be a winner \
but it cannot determine who won the game. This condition is \
the result of a bug in the open_ttt_lib used by this application."
),
}
}
fn next_players_turn(current_state: &State) -> State {
match current_state {
State::PlayerXMove => State::PlayerOMove,
State::PlayerOMove => State::PlayerXMove,
_ => panic!(
"Attempting to get the next player's turn but the game \
is over ({:?}). This condition is the result of a bug in the \
open_ttt_lib used by this application.",
current_state
),
}
}
}
impl Default for Game {
fn default() -> Self {
Self::new()
}
}
pub struct FreePositions<'a> {
board_iter: board::Iter<'a>,
is_game_over: bool,
}
impl Iterator for FreePositions<'_> {
type Item = board::Position;
fn next(&mut self) -> Option<Self::Item> {
if self.is_game_over {
return None;
}
while let Some((position, owner)) = self.board_iter.next() {
if owner == board::Owner::None {
return Some(position);
}
}
None
}
}
#[derive(Debug)]
pub enum Error {
GameOver,
PositionAlreadyOwned(board::Position, board::Owner),
InvalidPosition(board::Position),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::GameOver => write!(
f,
"The game is over so no more moves can \
be performed. Use start_next_game() to start the next game."
),
Self::PositionAlreadyOwned(position, owner) => write!(
f,
"The square at {:?} is already owned by {:?}. Once a square is \
owned by a player it cannot be used by a different player. Use \
free_positions() to get available positions that can be used.",
position, owner
),
Self::InvalidPosition(position) => write!(
f,
"The position {:?} is outside the area of the board. Please use \
a valid position contained by the board.",
position
),
}
}
}
impl error::Error for Error {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum State {
PlayerXMove,
PlayerOMove,
PlayerXWin(HashSet<board::Position>),
PlayerOWin(HashSet<board::Position>),
CatsGame,
}
impl State {
pub fn is_game_over(&self) -> bool {
match self {
Self::PlayerXMove | Self::PlayerOMove => false,
Self::PlayerXWin(_) | Self::PlayerOWin(_) | Self::CatsGame => true,
}
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
fn set_positions(game: &mut Game, owner: board::Owner, positions: &[board::Position]) {
for position in positions {
*game.board.get_mut(*position).unwrap() = owner;
}
}
#[test]
fn game_new_should_create_3x3_board() {
let expected_size = board::Size {
rows: 3,
columns: 3,
};
let game = Game::new();
let actual_size = game.board().size();
assert_eq!(expected_size, actual_size);
}
#[test]
fn game_new_should_not_be_game_over_state() {
let expected_is_game_over = false;
let game = Game::new();
let actual_is_game_over = game.state().is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
#[test]
fn game_new_should_all_positions_should_be_free() {
let game = Game::new();
let board_size = game.board().size();
let expected_free_squares = board_size.rows * board_size.columns;
let actual_free_squares = game.free_positions().count();
assert_eq!(expected_free_squares as usize, actual_free_squares);
}
#[test]
fn game_default_should_create_3x3_board() {
let expected_size = board::Size {
rows: 3,
columns: 3,
};
let game = Game::default();
let actual_size = game.board().size();
assert_eq!(expected_size, actual_size);
}
#[test]
fn game_free_positions_should_not_contain_any_owned_positions() {
let mut game = Game::new();
*game
.board
.get_mut(board::Position { row: 0, column: 0 })
.unwrap() = board::Owner::PlayerX;
*game
.board
.get_mut(board::Position { row: 0, column: 1 })
.unwrap() = board::Owner::PlayerO;
let expected_num_owned_positions = 0;
let actual_num_owned_positions = game
.free_positions()
.filter(|x| game.board().get(*x).unwrap() != board::Owner::None)
.count();
assert_eq!(expected_num_owned_positions, actual_num_owned_positions);
}
#[test]
fn game_free_positions_when_game_over_should_be_none() {
let mut game = Game::new();
game.state = State::CatsGame;
let expected_num_free_positions = 0;
let actual_num_free_positions = game.free_positions().count();
assert_eq!(expected_num_free_positions, actual_num_free_positions);
}
#[test]
fn game_can_move_when_unowned_positions_should_be_true() {
let position = board::Position { row: 0, column: 0 };
let game = Game::new();
let expected_can_move = true;
let actual_can_move = game.can_move(position);
assert_eq!(expected_can_move, actual_can_move);
}
#[test]
fn game_can_move_when_owned_positions_should_be_false() {
let position = board::Position { row: 0, column: 0 };
let mut game = Game::new();
*game.board.get_mut(position).unwrap() = board::Owner::PlayerX;
let expected_can_move = false;
let actual_can_move = game.can_move(position);
assert_eq!(expected_can_move, actual_can_move);
}
#[test]
fn game_can_move_when_game_over_should_be_false() {
let position = board::Position { row: 0, column: 0 };
let mut game = Game::new();
game.state = State::CatsGame;
let expected_can_move = false;
let actual_can_move = game.can_move(position);
assert_eq!(expected_can_move, actual_can_move);
}
#[test]
fn game_can_move_when_outside_game_board_should_be_false() {
let position_outside_game_board = board::Position {
row: -1,
column: -1,
};
let game = Game::new();
let expected_can_move = false;
let actual_can_move = game.can_move(position_outside_game_board);
assert_eq!(expected_can_move, actual_can_move);
}
#[test]
fn game_do_move_returned_state_should_match_game_state() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let returned_state = game.do_move(board::Position { row: 0, column: 0 }).unwrap();
let game_state = game.state();
assert_eq!(returned_state, game_state);
}
#[test]
fn game_do_move_when_player_X_move_should_return_player_O_move_state() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let expected_state = State::PlayerOMove;
let actual_state = game.do_move(board::Position { row: 0, column: 0 }).unwrap();
assert_eq!(expected_state, actual_state);
}
#[test]
fn game_do_move_when_player_O_move_should_return_player_X_move_state() {
let mut game = Game::new();
game.state = State::PlayerOMove;
let expected_state = State::PlayerXMove;
let actual_state = game.do_move(board::Position { row: 0, column: 0 }).unwrap();
assert_eq!(expected_state, actual_state);
}
#[test]
fn game_do_move_when_owned_position_should_return_error() {
let position = board::Position { row: 0, column: 0 };
let mut game = Game::new();
game.do_move(position).unwrap();
let move_result = game.do_move(position);
assert!(move_result.is_err());
}
#[test]
fn game_do_move_when_game_over_should_return_error() {
let position = board::Position { row: 0, column: 0 };
let mut game = Game::new();
game.state = State::CatsGame;
let move_result = game.do_move(position);
assert!(move_result.is_err());
}
#[test]
fn game_do_move_when_position_outside_board_should_return_error() {
let position_outside_board = board::Position {
row: 100,
column: 100,
};
let mut game = Game::new();
let move_result = game.do_move(position_outside_board);
assert!(move_result.is_err());
}
#[test]
fn game_do_move_when_three_X_in_row_should_return_player_X_win() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let existing_positions = [
board::Position { row: 0, column: 0 },
board::Position { row: 0, column: 1 },
];
set_positions(&mut game, board::Owner::PlayerX, &existing_positions);
let winning_position = board::Position { row: 0, column: 2 };
let mut winning_positions: HashSet<board::Position> =
existing_positions.iter().cloned().collect();
winning_positions.insert(winning_position);
let expected_state = State::PlayerXWin(winning_positions);
let actual_state = game.do_move(winning_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_do_move_when_three_X_in_column_should_return_player_X_win() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let existing_positions = [
board::Position { row: 0, column: 0 },
board::Position { row: 1, column: 0 },
];
set_positions(&mut game, board::Owner::PlayerX, &existing_positions);
let winning_position = board::Position { row: 2, column: 0 };
let mut winning_positions: HashSet<board::Position> =
existing_positions.iter().cloned().collect();
winning_positions.insert(winning_position);
let expected_state = State::PlayerXWin(winning_positions);
let actual_state = game.do_move(winning_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_do_move_when_three_X_in_top_left_to_bottom_right_diagonal_should_return_player_X_win() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let existing_positions = [
board::Position { row: 0, column: 0 },
board::Position { row: 1, column: 1 },
];
set_positions(&mut game, board::Owner::PlayerX, &existing_positions);
let winning_position = board::Position { row: 2, column: 2 };
let mut winning_positions: HashSet<board::Position> =
existing_positions.iter().cloned().collect();
winning_positions.insert(winning_position);
let expected_state = State::PlayerXWin(winning_positions);
let actual_state = game.do_move(winning_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_do_move_when_three_X_in_top_right_to_bottom_left_diagonal_should_return_player_X_win() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let existing_positions = [
board::Position { row: 0, column: 2 },
board::Position { row: 1, column: 1 },
];
set_positions(&mut game, board::Owner::PlayerX, &existing_positions);
let winning_position = board::Position { row: 2, column: 0 };
let mut winning_positions: HashSet<board::Position> =
existing_positions.iter().cloned().collect();
winning_positions.insert(winning_position);
let expected_state = State::PlayerXWin(winning_positions);
let actual_state = game.do_move(winning_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_do_move_when_both_winning_row_and_diagonal_should_contain_all_winning_positions() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let existing_positions = [
board::Position { row: 0, column: 1 },
board::Position { row: 0, column: 2 },
board::Position { row: 1, column: 1 },
board::Position { row: 2, column: 2 },
];
set_positions(&mut game, board::Owner::PlayerX, &existing_positions);
let winning_position = board::Position { row: 0, column: 0 };
let mut winning_positions: HashSet<board::Position> =
existing_positions.iter().cloned().collect();
winning_positions.insert(winning_position);
let expected_state = State::PlayerXWin(winning_positions);
let actual_state = game.do_move(winning_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_do_move_when_three_O_in_row_should_return_player_O_win() {
let mut game = Game::new();
game.state = State::PlayerOMove;
let existing_positions = [
board::Position { row: 1, column: 0 },
board::Position { row: 1, column: 1 },
];
set_positions(&mut game, board::Owner::PlayerO, &existing_positions);
let winning_position = board::Position { row: 1, column: 2 };
let mut winning_positions: HashSet<board::Position> =
existing_positions.iter().cloned().collect();
winning_positions.insert(winning_position);
let expected_state = State::PlayerOWin(winning_positions);
let actual_state = game.do_move(winning_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_do_move_when_last_position_filled_should_return_cats_game() {
let mut game = Game::new();
game.state = State::PlayerXMove;
let existing_X_positions = [
board::Position { row: 0, column: 0 },
board::Position { row: 0, column: 2 },
board::Position { row: 1, column: 0 },
board::Position { row: 2, column: 1 },
];
set_positions(&mut game, board::Owner::PlayerX, &existing_X_positions);
let existing_O_positions = [
board::Position { row: 0, column: 1 },
board::Position { row: 1, column: 1 },
board::Position { row: 1, column: 2 },
board::Position { row: 2, column: 0 },
];
set_positions(&mut game, board::Owner::PlayerO, &existing_O_positions);
let last_position = board::Position { row: 2, column: 2 };
let expected_state = State::CatsGame;
let actual_state = game.do_move(last_position).unwrap();
assert_eq!(
expected_state,
actual_state,
"\nGame board used for this test: \n{}",
game.board()
);
}
#[test]
fn game_start_next_game_should_ensure_player_who_went_went_second_goes_first_next_game() {
let mut game = Game::new();
let first_player_last_game = game.state();
let first_player_next_game = game.start_next_game();
assert_ne!(first_player_last_game, first_player_next_game);
}
#[test]
fn game_start_next_game_should_alternate_between_players_who_go_first() {
let mut game = Game::new();
let player_1 = game.start_next_game();
let player_2 = game.start_next_game();
assert_ne!(player_1, player_2);
}
#[test]
fn game_start_next_game_when_game_not_over_should_start_next_game() {
let mut game = Game::new();
let expected_is_game_over = false;
game.start_next_game();
let actual_is_game_over = game.state().is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
#[test]
fn error_display_when_game_over_should_be_non_empty() {
let error = Error::GameOver;
let error_message = error.to_string();
assert_ne!(0, error_message.len());
}
#[test]
fn error_display_when_position_already_owned_should_contain_position_text() {
let position = board::Position { row: 0, column: 0 };
let owner = board::Owner::PlayerX;
let position_text = format!("{:?}", position);
let error = Error::PositionAlreadyOwned(position, owner);
let error_message = error.to_string();
assert!(error_message.contains(&position_text));
}
#[test]
fn error_display_when_position_already_owned_should_contain_owner_text() {
let position = board::Position { row: 0, column: 0 };
let owner = board::Owner::PlayerX;
let owner_text = format!("{:?}", owner);
let error = Error::PositionAlreadyOwned(position, owner);
let error_message = error.to_string();
assert!(error_message.contains(&owner_text));
}
#[test]
fn error_display_when_invalid_position_should_contain_position_text() {
let position = board::Position { row: 0, column: 0 };
let position_text = format!("{:?}", position);
let error = Error::InvalidPosition(position);
let error_message = error.to_string();
assert!(error_message.contains(&position_text));
}
#[test]
fn state_is_game_over_when_player_X_move_should_be_false() {
let state = State::PlayerXMove;
let expected_is_game_over = false;
let actual_is_game_over = state.is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
#[test]
fn state_is_game_over_when_player_O_move_should_be_false() {
let state = State::PlayerOMove;
let expected_is_game_over = false;
let actual_is_game_over = state.is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
#[test]
fn state_is_game_over_when_player_X_win_should_be_true() {
let state = State::PlayerXWin(Default::default());
let expected_is_game_over = true;
let actual_is_game_over = state.is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
#[test]
fn state_is_game_over_when_player_O_win_should_be_true() {
let state = State::PlayerOWin(Default::default());
let expected_is_game_over = true;
let actual_is_game_over = state.is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
#[test]
fn state_is_game_over_when_cats_game_should_be_true() {
let state = State::CatsGame;
let expected_is_game_over = true;
let actual_is_game_over = state.is_game_over();
assert_eq!(expected_is_game_over, actual_is_game_over);
}
}