Quantcast
Channel: Command-line Tic-Tac-Toc in Rust - Code Review Stack Exchange
Viewing all articles
Browse latest Browse all 3

Command-line Tic-Tac-Toc in Rust

$
0
0

I implemented Tic-Tac-Toe in Rust to learn the language.

I split the implementation into two files: game.rs contains the actual game logic, and main.rs contains the main function and the command-line UI.

I'm interested in any feedback. One particular question I have: I'm interested in trying to implement a GUI interface for the game – have I split the logic correctly between main.rs and game.rs so that game.rs could be reused in a GUI version?

Cargo.toml

[package]name = "tic-tac-toe"version = "0.1.0"authors = ["Stephen Wade <stephen@stephenwade.me>"]edition = "2018"[dependencies]rustyline = "7.1.0"

game.rs

use std::fmt;use std::ops::{Deref, DerefMut};#[derive(Clone, Copy, PartialEq)]pub enum BoardValue {    Filled(Player),    Empty,}impl fmt::Display for BoardValue {    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        match self {            Self::Filled(player) => write!(f, "{}", player),            Self::Empty => write!(f, " "),        }    }}impl Default for BoardValue {    fn default() -> Self {        Self::Empty    }}impl BoardValue {    fn player(self) -> Player {        if let Self::Filled(player) = self {            player        } else {            panic!("not filled")        }    }}#[derive(Clone, Copy, PartialEq)]pub enum Player {    X,    O,}impl fmt::Display for Player {    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        write!(            f,"{}",            match self {                Self::X => "X",                Self::O => "O",            }        )    }}impl Default for Player {    fn default() -> Self {        Player::X    }}type BoardType = [[BoardValue; 3]; 3];#[derive(Default)]pub struct Board(BoardType);impl Deref for Board {    type Target = BoardType;    fn deref(&self) -> &BoardType {&self.0    }}impl DerefMut for Board {    fn deref_mut(&mut self) -> &mut BoardType {&mut self.0    }}impl fmt::Display for Board {    #[rustfmt::skip]    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        writeln!(f, "┌───┬───┬───┐")?;        writeln!(f, "│ {} │ {} │ {} │", self[0][0], self[0][1], self[0][2])?;        writeln!(f, "├───┼───┼───┤")?;        writeln!(f, "│ {} │ {} │ {} │", self[1][0], self[1][1], self[1][2])?;        writeln!(f, "├───┼───┼───┤")?;        writeln!(f, "│ {} │ {} │ {} │", self[2][0], self[2][1], self[2][2])?;        write!  (f, "└───┴───┴───┘")    }}impl Board {    fn get_winnable_slices(&self) -> Vec<[&BoardValue; 3]> {        vec![            // Horizontal            [&self[0][0], &self[0][1], &self[0][2]],            [&self[1][0], &self[1][1], &self[1][2]],            [&self[2][0], &self[2][1], &self[2][2]],            // Vertical            [&self[0][0], &self[1][0], &self[2][0]],            [&self[0][1], &self[1][1], &self[2][1]],            [&self[0][2], &self[1][2], &self[2][2]],            // Diagonal            [&self[0][0], &self[1][1], &self[2][2]],            [&self[0][2], &self[1][1], &self[2][0]],        ]    }    fn get_all_cells(&self) -> Vec<&BoardValue> {        self.iter().flatten().collect::<Vec<&BoardValue>>()    }}pub struct Game {    pub board: Board,    pub current_player: Player,}pub enum GameStatus {    Continue,    PlayerWins(Player),    Draw,}pub enum PlayError {    InvalidMove,}impl Game {    pub fn new() -> Self {        Game {            board: Board::default(),            current_player: Player::default(),        }    }    pub fn play(&mut self, row: usize, column: usize) -> Result<GameStatus, PlayError> {        if matches!(self.board[row][column], BoardValue::Filled(_)) {            return Err(PlayError::InvalidMove);        }        self.board[row][column] = BoardValue::Filled(self.current_player);        let game_status = self.get_game_status();        if matches!(game_status, GameStatus::Continue) {            self.current_player = match self.current_player {                Player::X => Player::O,                Player::O => Player::X,            };        }        Ok(game_status)    }    fn get_game_status(&self) -> GameStatus {        for slice in self.board.get_winnable_slices() {            if matches!(*slice[0], BoardValue::Filled(_))&& slice[0] == slice[1]&& slice[1] == slice[2]            {                return GameStatus::PlayerWins(slice[0].player());            }        }        if self            .board            .get_all_cells()            .into_iter()            .all(|cell| matches!(*cell, BoardValue::Filled(_)))        {            return GameStatus::Draw;        }        GameStatus::Continue    }}

main.rs

mod game;use game::*;use std::str;use rustyline::Editor;fn main() {    let mut game = Game::new();    println!("Welcome to tic tac toe!");    loop {        println!("{}", game.board);        println!("It's {}'s turn.", game.current_player);        let inputs = match get_row_and_column() {            Ok(inputs) => inputs,            Err(_) => return,        };        match game.play(inputs.0, inputs.1) {            Ok(GameStatus::Continue) => continue,            Ok(GameStatus::Draw) => {                println!("{}", game.board);                println!("It's a draw.");                return;            }            Ok(GameStatus::PlayerWins(player)) => {                println!("{}", game.board);                println!("{} wins!", player);                return;            }            Err(PlayError::InvalidMove) => {                println!("Invalid move. Please try again.");            }        };    }}fn get_row_and_column() -> Result<(usize, usize), ()> {    let strings = match read_row_and_column_strings() {        Ok(strings) => strings,        Err(_) => return Err(()),    };    let numbers = match parse_row_and_column_strings(strings) {        Ok(numbers) => numbers,        Err(_) => return Err(()),    };    Ok((numbers.0 - 1, numbers.1 - 1))}fn parse_row_and_column_strings(    input: (String, String),) -> Result<(usize, usize), <usize as str::FromStr>::Err> {    let row: usize = input.0.parse()?;    let column: usize = input.1.parse()?;    Ok((row, column))}fn read_row_and_column_strings() -> Result<(String, String), rustyline::error::ReadlineError> {    let mut rl = Editor::<()>::new();    let valid_inputs: Vec<&str> = vec!["1", "2", "3"];    let mut row_line: String;    let mut column_line: String;    loop {        row_line = rl.readline("Enter a row (1, 2, 3): ")?;        if valid_inputs.iter().any(|&s| row_line == s) {            break;        };    }    loop {        column_line = rl.readline("Enter a column (1, 2, 3): ")?;        if valid_inputs.iter().any(|&s| column_line == s) {            break;        };    }    Ok((row_line, column_line))}```

Viewing all articles
Browse latest Browse all 3

Latest Images

Trending Articles





Latest Images