|
use std::error::Error; |
|
use std::fmt::Display; |
|
use std::io::{stdin, stdout, Write}; |
|
|
|
type Board = [Pos; 9]; |
|
type Role = (Player, Mark); |
|
type Roles = (Role, Role); |
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
|
enum Pos { |
|
Mark(Mark), |
|
None, |
|
} |
|
|
|
impl Display for Pos { |
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
match self { |
|
Self::Mark(m) => Ok(write!(f, "{m:?}")?), |
|
Self::None => Ok(write!(f, " ")?), |
|
} |
|
} |
|
} |
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
|
enum Mark { |
|
X, |
|
O, |
|
} |
|
impl Mark { |
|
fn opposite(&self) -> Self { |
|
match self { |
|
Self::X => Self::O, |
|
Self::O => Self::X, |
|
} |
|
} |
|
} |
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
|
enum Player { |
|
One, |
|
Two, |
|
} |
|
|
|
#[derive(Debug, PartialEq, Eq)] |
|
enum Status { |
|
InProgress, |
|
Winner(Player), |
|
Draw, |
|
} |
|
|
|
struct Turn(Role); |
|
|
|
impl Turn { |
|
fn next(&mut self) { |
|
self.0 = match &self.0 { |
|
(Player::One, m) => (Player::Two, m.opposite()), |
|
(Player::Two, m) => (Player::One, m.opposite()), |
|
} |
|
} |
|
} |
|
|
|
struct Game { |
|
status: Status, |
|
roles: Roles, |
|
turn: Turn, |
|
board: Board, |
|
} |
|
|
|
impl Game { |
|
fn new() -> Self { |
|
let status = Status::InProgress; |
|
let roles = Self::rand_roles(); |
|
let turn = Self::rand_turn(&roles); |
|
let board: Board = [Pos::None; 9]; |
|
Self { |
|
status, |
|
roles, |
|
turn, |
|
board, |
|
} |
|
} |
|
|
|
fn handle_input(&mut self, index: usize) -> Result<(), Box<dyn Error>> { |
|
let index = index - 1; |
|
let mut pos = self.board[index]; |
|
if pos != Pos::None { |
|
return Err( |
|
format!("Position {index} is already occupied, choose an empty position",).into(), |
|
); |
|
} |
|
pos = match self.turn.0 { |
|
(_, Mark::X) => Pos::Mark(Mark::X), |
|
(_, Mark::O) => Pos::Mark(Mark::O), |
|
}; |
|
self.board[index] = pos; |
|
|
|
Ok(()) |
|
} |
|
|
|
fn check_win(&mut self) { |
|
let board = &self.board; |
|
let (player, _) = self.turn.0; |
|
// Diagonal win |
|
if (board[0] != Pos::None && (board[0] == board[4] && board[0] == board[8])) |
|
|| (board[2] != Pos::None && (board[2] == board[4] && board[2] == board[6])) |
|
{ |
|
return self.status = Status::Winner(player); |
|
} |
|
|
|
for pos in 0..=2 { |
|
if board[pos] == Pos::None { |
|
continue; |
|
} |
|
// Column win |
|
if board[pos] == board[pos + 3] && board[pos] == board[pos + 6] { |
|
return self.status = Status::Winner(player); |
|
} |
|
let pos = pos * 3; |
|
if board[pos] == Pos::None { |
|
continue; |
|
} |
|
// Row win |
|
if board[pos] == board[pos + 1] && board[pos] == board[pos + 2] { |
|
return self.status = Status::Winner(player); |
|
} |
|
} |
|
|
|
// Draw |
|
if board.iter().all(|p| *p != Pos::None) { |
|
self.status = Status::Draw; |
|
} |
|
} |
|
|
|
fn switch_player(&mut self) { |
|
self.turn.next(); |
|
} |
|
|
|
fn rand_roles() -> Roles { |
|
match rand::random::<bool>() { |
|
true => ((Player::One, Mark::X), (Player::Two, Mark::O)), |
|
false => ((Player::One, Mark::O), (Player::Two, Mark::X)), |
|
} |
|
} |
|
|
|
fn rand_turn(roles: &Roles) -> Turn { |
|
match rand::random::<bool>() { |
|
true => Turn(roles.0), |
|
false => Turn(roles.1), |
|
} |
|
} |
|
} |
|
|
|
impl Display for Game { |
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
// Display the grid |
|
for pos in 0..=2 { |
|
let index = pos * 3; |
|
write!( |
|
f, |
|
"\ |
|
-------------\n\ |
|
| {} | {} | {} |\n\ |
|
-------------\n", |
|
self.board[index], |
|
self.board[index + 1], |
|
self.board[index + 2] |
|
)?; |
|
} |
|
writeln!(f, "-------------\n")?; |
|
|
|
writeln!(f, "{:?} is Player {:?}", self.roles.0 .1, self.roles.0 .0)?; |
|
writeln!(f, "{:?} is Player {:?}", self.roles.1 .1, self.roles.1 .0)?; |
|
Ok(()) |
|
} |
|
} |
|
|
|
fn strip_trailing_newline(input: &str) -> &str { |
|
input |
|
.strip_suffix("\r\n") |
|
.or(input.strip_suffix('\n')) |
|
.unwrap_or(input) |
|
} |
|
|
|
fn main() -> Result<(), Box<dyn Error>> { |
|
let mut game = Game::new(); |
|
|
|
while game.status == Status::InProgress { |
|
// Clear screen |
|
print!("\x1B[2J\x1B[1;1H"); |
|
|
|
print!("{game}"); |
|
|
|
let mut try_input = || -> Result<(), Box<dyn Error>> { |
|
println!("Enter a position 1...9"); |
|
print!( |
|
"Player {:?}'s Turn ( {:?} ):> ", |
|
game.turn.0 .0, game.turn.0 .1 |
|
); |
|
stdout().flush().unwrap(); |
|
let mut input = String::new(); |
|
stdin() |
|
.read_line(&mut input) |
|
.map_err(|_| "Oops! Couldn't read that input. Try again...")?; |
|
let input: &str = strip_trailing_newline(input.as_str()); |
|
let pos = input |
|
.parse::<usize>() |
|
.map_err(|_| "Enter a valid position between 1..9")?; |
|
if !(1..=9).contains(&pos) { |
|
return Err("Enter a valid position between 1..9".into()); |
|
} |
|
game.handle_input(pos)?; |
|
|
|
Ok(()) |
|
}; |
|
loop { |
|
match try_input() { |
|
// Valid input |
|
Ok(()) => break, |
|
// Try input again |
|
Err(err) => { |
|
println!("{err}"); |
|
continue; |
|
} |
|
} |
|
} |
|
|
|
game.check_win(); |
|
|
|
game.switch_player(); |
|
} |
|
print!("{game}"); |
|
println!("Game Over => {:?}", game.status); |
|
Ok(()) |
|
} |