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 for Item { fn into(self) -> String { match self { Self::X => "X".to_string(), Self::O => "O".to_string(), Self::Empty => "_".to_string(), } } } impl TryFrom for Item { type Error = String; fn try_from(item: String) -> Result { 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) { // 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 { 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 = vec![]; let items: Vec = self .items .iter() .enumerate() .map(|(idx, x)| { if x != &Item::Empty { Into::::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 { 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 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, 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 { 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 { 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(); }