commit adfa99149e9ff84502092fff9b7f52866929161c Author: landrigun Date: Sat Nov 12 14:16:12 2022 +0000 first tic-tac-toe implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d10b003 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +tic-tac-toe diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..07cc37a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "tic-tac-toe" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f587244 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2cd4023 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Tic-Tac-Toe + +A simple Rust implementation of the famous game: **Tic-Tac-Toe** + +## Build +```bash +cargo build --release +``` + +## Run +```bash +./tic-tac-toe +``` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e73bb04 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,332 @@ +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()), + } + } +} + +/// Two `GameType` availables: +/// * TwoPlayers: no bot +/// * Bot: player against the bot +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, + } + } +} + +/// 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 + } + + fn set_item(&mut self, item: Item, pos: String) -> Result { + let idx = if let Ok(v) = usize::from_str(&pos) { + v + } else { + return Err("unable to parse the input, retry!"); + }; + if idx == 0 || idx > 9 { + return Err("can't be greater than 8"); + } + 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(idx - 1); + } + } + } + + 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.clone()); + if group_items.len() != 3 { + continue; + } + Board::base_display(group_items.clone()); + group_items.clear(); + println!("{:<3}{:-<23}", "", ""); + } + + println!("\n"); + } +} + +#[allow(dead_code)] +struct TicTacToe { + board: Board, + current_item: Item, + game_type: GameType, + turn: u8, +} + +impl Default for TicTacToe { + fn default() -> Self { + TicTacToe { + board: Board::new(), + game_type: GameType::Bot, + current_item: Item::Empty, + turn: 1, + } + } +} + +impl TicTacToe { + 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_current_item(&mut self, item: Item) { + self.current_item = item; + } + + /// tries to get the first item (the one that starts) + fn select_current_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 item_start = + TicTacToe::get_user_input("first player, choose your item: ('X' or 'O')"); + let item = Item::try_from(item_start); + match item { + Ok(i) => { + self.set_current_item(i); + break; + } + Err(e) => error(e.to_string()), + } + attempt += 1; + } + } + + /// on each turn, set the next item who has to play + fn next_item(&mut self) { + match self.current_item { + Item::X => self.set_current_item(Item::O), + Item::O => self.set_current_item(Item::X), + _ => return, + } + self.turn += 1; + } + + fn run(&mut self) { + self.select_current_item(); + self.board.show(); + + loop { + if self.board.is_full() { + info("game over!".to_string()); + self.board.show(); + break; + } + + let item_icon: String = self.current_item.into(); + let turn_message = format!( + "{} turn, which case do you wan to fill ?", + item_icon.clone() + ); + let idx = TicTacToe::get_user_input(&turn_message); + match self.board.set_item(self.current_item.clone(), idx) { + Ok(i) => { + // no need to check for a winner until the turn 5 + if self.turn > 4 && self.board.check_win(self.current_item.clone(), i) { + info(format!("{} wins!", item_icon.clone())); + self.board.show(); + break; + } + } + Err(e) => { + error(e.to_string()); + continue; + } + } + + self.next_item(); + self.board.show(); + } + } +} + +fn info(msg: String) { + println!("{}", format!("[TIC TAC TOE] [INFO] => {}", msg)) +} + +fn error(msg: String) { + eprintln!("{}", format!("[TIC TAC TOE] [ERROR] => {}", msg)) +} + +fn main() { + let mut game = TicTacToe::default(); + game.run(); +}