480 lines
13 KiB
Rust
480 lines
13 KiB
Rust
use rand::prelude::*;
|
|
use std::str::FromStr;
|
|
|
|
/// Represents the `item` on the TTT board (X or O)
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
enum Item {
|
|
X,
|
|
O,
|
|
Empty,
|
|
}
|
|
|
|
impl Into<String> for Item {
|
|
fn into(self) -> String {
|
|
match self {
|
|
Self::X => "X".to_string(),
|
|
Self::O => "O".to_string(),
|
|
Self::Empty => "_".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for Item {
|
|
type Error = String;
|
|
fn try_from(item: String) -> Result<Self, Self::Error> {
|
|
match &item[..] {
|
|
"X" | "x" => Ok(Self::X),
|
|
"O" | "o" => Ok(Self::O),
|
|
_ => Err("unknown item, retry!".to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Item {
|
|
fn next(&self) -> Self {
|
|
match self {
|
|
Item::X => Item::O,
|
|
Item::O => Item::X,
|
|
Item::Empty => Item::Empty,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// In order to visualize the board game
|
|
trait Displayer {
|
|
fn show(&self);
|
|
}
|
|
|
|
/// Represents the **Tic-Tac-Toe** board game
|
|
struct Board {
|
|
// board array is reprensented as follow:
|
|
// [ 0, 1, 2 ]
|
|
// [ 3, 4, 5 ]
|
|
// [ 6, 7, 8 ]
|
|
items: [Item; 9],
|
|
}
|
|
|
|
impl Board {
|
|
fn new() -> Board {
|
|
Board {
|
|
items: [Item::Empty; 9],
|
|
}
|
|
}
|
|
|
|
fn base_display(group_items: Vec<String>) {
|
|
// output: X | _ | O
|
|
println!(
|
|
"{}",
|
|
format!(
|
|
"{:<5} {} {:<2}|{:<2} {} {:<2}|{:<2} {}",
|
|
"", group_items[0], "", "", group_items[1], "", "", group_items[2]
|
|
)
|
|
);
|
|
}
|
|
|
|
/// checks whether the board is item's full (quit the game)
|
|
fn is_full(&self) -> bool {
|
|
for item in self.items {
|
|
match item {
|
|
Item::Empty => return false,
|
|
_ => continue,
|
|
};
|
|
}
|
|
true
|
|
}
|
|
|
|
/// returns the available/free board indexes
|
|
fn get_available_indexes(&self) -> Vec<usize> {
|
|
let mut availables = vec![];
|
|
for (idx, item) in self.items.iter().enumerate() {
|
|
if item == &Item::Empty {
|
|
availables.push(idx);
|
|
}
|
|
}
|
|
availables
|
|
}
|
|
|
|
fn set_item(&mut self, item: Item, idx: usize) -> Result<(), &str> {
|
|
if idx == 0 || idx > 9 {
|
|
return Err("invalid input, must be between 1 <= x <= 9");
|
|
}
|
|
match self.items[idx - 1] {
|
|
Item::X => Err("already set, choose another one!"),
|
|
Item::O => Err("already set, choose another one!"),
|
|
Item::Empty => {
|
|
self.items[idx - 1] = item;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_first_line(&self, item: Item) -> bool {
|
|
self.items[0] == item && self.items[1] == item && self.items[2] == item
|
|
}
|
|
|
|
fn check_second_line(&self, item: Item) -> bool {
|
|
self.items[3] == item && self.items[4] == item && self.items[5] == item
|
|
}
|
|
|
|
fn check_last_line(&self, item: Item) -> bool {
|
|
self.items[6] == item && self.items[7] == item && self.items[8] == item
|
|
}
|
|
|
|
fn check_first_col(&self, item: Item) -> bool {
|
|
self.items[0] == item && self.items[3] == item && self.items[6] == item
|
|
}
|
|
|
|
fn check_second_col(&self, item: Item) -> bool {
|
|
self.items[1] == item && self.items[4] == item && self.items[7] == item
|
|
}
|
|
|
|
fn check_last_col(&self, item: Item) -> bool {
|
|
self.items[2] == item && self.items[5] == item && self.items[8] == item
|
|
}
|
|
|
|
fn check_first_diag(&self, item: Item) -> bool {
|
|
self.items[0] == item && self.items[4] == item && self.items[8] == item
|
|
}
|
|
|
|
fn check_last_diag(&self, item: Item) -> bool {
|
|
self.items[2] == item && self.items[4] == item && self.items[6] == item
|
|
}
|
|
|
|
/// instead of scanning all the board, checks input "win" combinaison
|
|
fn check_win(&self, item: Item, idx: usize) -> bool {
|
|
match idx {
|
|
0 => {
|
|
self.check_first_col(item)
|
|
|| self.check_first_line(item)
|
|
|| self.check_first_diag(item)
|
|
}
|
|
1 => self.check_first_line(item) || self.check_second_col(item),
|
|
2 => {
|
|
self.check_first_line(item)
|
|
|| self.check_last_col(item)
|
|
|| self.check_last_diag(item)
|
|
}
|
|
3 => self.check_first_col(item) || self.check_second_line(item),
|
|
4 => {
|
|
self.check_second_col(item)
|
|
|| self.check_second_line(item)
|
|
|| self.check_last_diag(item)
|
|
|| self.check_first_diag(item)
|
|
}
|
|
5 => self.check_last_col(item) || self.check_second_line(item),
|
|
6 => {
|
|
self.check_first_col(item)
|
|
|| self.check_last_line(item)
|
|
|| self.check_last_diag(item)
|
|
}
|
|
7 => self.check_last_line(item) || self.check_second_col(item),
|
|
8 => {
|
|
self.check_last_line(item)
|
|
|| self.check_last_col(item)
|
|
|| self.check_first_diag(item)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Displayer for Board {
|
|
/// loops over board items, groups items by 3 (multi dim array representation) and display it
|
|
/// also displays board index number (empty item) for easy use
|
|
fn show(&self) {
|
|
println!("\n");
|
|
println!("{:<3}{:-<23}", "", "");
|
|
|
|
let mut group_items: Vec<String> = vec![];
|
|
let items: Vec<String> = self
|
|
.items
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, x)| {
|
|
if x != &Item::Empty {
|
|
Into::<String>::into(*x)
|
|
} else {
|
|
format!("{}", idx + 1)
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
for item in items {
|
|
group_items.push(item);
|
|
if group_items.len() != 3 {
|
|
continue;
|
|
}
|
|
Board::base_display(group_items.clone());
|
|
group_items.clear();
|
|
println!("{:<3}{:-<23}", "", "");
|
|
}
|
|
|
|
println!("\n");
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct Player {
|
|
item: Item,
|
|
is_bot: bool,
|
|
}
|
|
|
|
impl Player {
|
|
fn new(item: Item, is_bot: bool) -> Self {
|
|
Player { item, is_bot }
|
|
}
|
|
}
|
|
|
|
impl Default for Player {
|
|
fn default() -> Self {
|
|
Player {
|
|
item: Item::Empty,
|
|
is_bot: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Player {
|
|
/// init players with the game type
|
|
fn from_game_type(game_type: GameType) -> Vec<Player> {
|
|
match game_type {
|
|
GameType::TwoPlayers => vec![
|
|
Player::new(Item::Empty, false),
|
|
Player::new(Item::Empty, false),
|
|
],
|
|
GameType::Bot => vec![
|
|
Player::new(Item::Empty, false),
|
|
Player::new(Item::Empty, true),
|
|
],
|
|
GameType::Unknown => vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Two `GameType` availables:
|
|
/// * TwoPlayers: no bot
|
|
/// * Bot: player against the bot
|
|
#[derive(Copy, Clone)]
|
|
enum GameType {
|
|
TwoPlayers,
|
|
Bot,
|
|
Unknown,
|
|
}
|
|
|
|
impl From<String> for GameType {
|
|
fn from(game_type: String) -> Self {
|
|
match &game_type[..] {
|
|
"0" => Self::TwoPlayers,
|
|
"1" => Self::Bot,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
struct TicTacToe {
|
|
board: Board,
|
|
players: Vec<Player>,
|
|
game_type: GameType,
|
|
turn: u8,
|
|
}
|
|
|
|
impl Default for TicTacToe {
|
|
fn default() -> Self {
|
|
TicTacToe {
|
|
board: Board::new(),
|
|
game_type: GameType::Unknown,
|
|
players: vec![],
|
|
turn: 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TicTacToe {
|
|
/// associated func in order to get user input
|
|
fn get_user_input(message: &str) -> String {
|
|
info(message.to_string());
|
|
let mut input = String::new();
|
|
std::io::stdin()
|
|
.read_line(&mut input)
|
|
.expect("IO error: unable to read the stdin input");
|
|
input.trim().to_string()
|
|
}
|
|
|
|
fn set_game_type(&mut self, game_type: GameType) {
|
|
self.game_type = game_type;
|
|
}
|
|
|
|
fn set_players_item(&mut self, first_item: Item) -> Result<(), String> {
|
|
if self.players.len() != 2 {
|
|
return Err(
|
|
"unable to initialize players item, 2 players must be initialized".to_string(),
|
|
);
|
|
}
|
|
|
|
self.players[0].item = first_item;
|
|
self.players[1].item = first_item.next();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// on each turn, set the next player who has to play (players vec swapped)
|
|
fn get_next_player(&mut self) -> Result<Player, String> {
|
|
if self.players.len() != 2 {
|
|
return Err("unable to get next player, 2 players must be initialized".to_string());
|
|
}
|
|
|
|
// on first turn, no need to swap
|
|
if self.turn > 1 {
|
|
self.players.swap(0, 1);
|
|
}
|
|
self.turn += 1;
|
|
|
|
Ok(self.players[0])
|
|
}
|
|
|
|
/// set the item for the first player (the one that starts)
|
|
fn select_first_player_item(&mut self) {
|
|
let mut attempt = 0;
|
|
loop {
|
|
if attempt == 2 {
|
|
error("come on... you can do better!".to_string());
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let input = TicTacToe::get_user_input("first player, choose your item: ('X' or 'O')");
|
|
let item_start = Item::try_from(input);
|
|
match item_start {
|
|
Ok(item) => {
|
|
match self.set_players_item(item) {
|
|
Ok(()) => break,
|
|
Err(e) => fatal(e),
|
|
}
|
|
break;
|
|
}
|
|
Err(e) => error(e.to_string()),
|
|
}
|
|
attempt += 1;
|
|
}
|
|
}
|
|
|
|
/// select game type and init players
|
|
fn select_game_type(&mut self) {
|
|
let mut attempt = 0;
|
|
loop {
|
|
if attempt == 2 {
|
|
error("come on... you can do better!".to_string());
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let input = TicTacToe::get_user_input("choose your game type: ('0': versus, '1': bot)");
|
|
let game_type = GameType::from(input);
|
|
match game_type {
|
|
GameType::Unknown => error("incorrect game type selected, retry!".to_string()),
|
|
_ => {
|
|
self.set_game_type(game_type);
|
|
break;
|
|
}
|
|
}
|
|
attempt += 1;
|
|
}
|
|
// init players
|
|
self.players = Player::from_game_type(self.game_type);
|
|
}
|
|
|
|
fn select_player_item(&self) -> Result<usize, String> {
|
|
if self.players.len() != 2 {
|
|
return Err(
|
|
"unable to get player item input, 2 players must be initialized".to_string(),
|
|
);
|
|
}
|
|
|
|
let player = self.players[0];
|
|
let item_icon: String = player.item.into();
|
|
|
|
if !player.is_bot {
|
|
let turn_message = format!("{} turn, which case do you want to fill ?", item_icon);
|
|
let idx = TicTacToe::get_user_input(&turn_message);
|
|
match usize::from_str(&idx) {
|
|
Ok(v) => return Ok(v),
|
|
Err(e) => return Err(e.to_string()),
|
|
}
|
|
}
|
|
|
|
// for now, use a dummy bot
|
|
let mut rng = rand::thread_rng();
|
|
let mut available_indexes = self.board.get_available_indexes();
|
|
available_indexes.shuffle(&mut rng);
|
|
|
|
Ok(available_indexes[0] + 1)
|
|
}
|
|
|
|
fn run(&mut self) {
|
|
self.select_game_type();
|
|
self.select_first_player_item();
|
|
|
|
loop {
|
|
if self.board.is_full() {
|
|
info("game over!".to_string());
|
|
self.board.show();
|
|
break;
|
|
}
|
|
|
|
let mut player = Player::default();
|
|
match self.get_next_player() {
|
|
Ok(p) => player = p,
|
|
Err(e) => fatal(e),
|
|
}
|
|
|
|
if !player.is_bot {
|
|
self.board.show();
|
|
}
|
|
|
|
let item_icon: String = player.item.into();
|
|
let mut item_position;
|
|
|
|
loop {
|
|
match self.select_player_item() {
|
|
Ok(pos) => {
|
|
item_position = pos;
|
|
match self.board.set_item(player.item, pos) {
|
|
Ok(_) => break,
|
|
Err(e) => {
|
|
error(e.to_string());
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error(e.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// check after the 5th round if there's a winner
|
|
if self.turn > 4 && self.board.check_win(player.item, item_position - 1) {
|
|
info(format!("{} wins!", item_icon));
|
|
self.board.show();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn info(msg: String) {
|
|
println!("{}", format!("[tic-tac-toe] [info] => {}", msg))
|
|
}
|
|
|
|
fn error(msg: String) {
|
|
eprintln!("{}", format!("[tic-tac-toe] [error] => {}", msg))
|
|
}
|
|
|
|
fn fatal(msg: String) {
|
|
eprintln!("{}", format!("[tic-tac-toe] [fatal] => {}", msg));
|
|
std::process::exit(1);
|
|
}
|
|
|
|
fn main() {
|
|
let mut game = TicTacToe::default();
|
|
game.run();
|
|
}
|