Rust TicTacToe Game Example

Rust TicTactoe Game Example - well commented

This is a practical open source example of how to create the famous tictactoe game in Rust.

Tic-Tac-Toe is played on a 3x3 grid. You take turns placing pieces and attempt to get three in a row horizontally, vertically, or diagonally.

Here's what it looks like when it is running:

Step 1: Create project

Create an empty Rust project.

Step 2: Dependencies

No third party dependencies are needed.

Step 3: Create Game

Here is the full code with comments:

fame.rs

// This constant can be used to set the board size
// Since Rust's arrays are fat pointers, you won't see this constant referred to again after the
// we declare the type of Game. I mention this because if you were writing in a language like C,
// you would either need to pass the size to every function with the board or rely on this global
// constant. In Rust, that information is stored directly in the array so you always have the
// correct value.

const BOARD_SIZE: usize = 3;

// We want to use an enum for piece because we can either have one piece or the other on a tile,
// but never both at the same time
// derive automatically derives certain useful traits. These make this custom type that we've
// defined copyable, comparable for equality, and more without any additional work!
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Piece {
    // Access these variants using Piece::X or Piece::O
    X,
    O,
}

impl Piece {
    // This method is used to return the opposite piece and is used to quickly determine the next
    // piece after each move
    // By putting self as the first parameter, we are copying the piece that this method is
    // called on. This happens because this type derives Copy in its declaration. Without that,
    // using self alone would "move" the value into this function. Rust would ensure that no
    // other code could access it afterwards. Copy gives us complete control over which values we
    // want Rust to copy and which values we want Rust to move and only copy when we explicitly
    // ask for it.
    // For more information, see: https://doc.rust-lang.org/beta/std/marker/trait.Copy.html
    pub fn other(self) -> Piece {
        // The last expression in a function is returned from that function, so without writing
        // return anywhere, we can return the correct Piece from this function.
        // We could have also used multiple if statements, but this is a little simpler to read
        // once you understand the syntax.

        // Rust will tell us if Piece ever changes and this match doesn't cover every case
        match self {
            // match lets us conveniently express both cases without too much additional syntax
            Piece::X => Piece::O,
            Piece::O => Piece::X,
        }
    }
}

// By using an Option type, we can represent the possibility of having one of the valid piece
// types, or no piece at all. Notice that we chose not to just add an "Empty" piece type because
// this allows us to use Piece for other things like representing the choices for the current
// piece. The current piece can never be "empty", so it doesn't make sense to have an Empty variant
// in the Piece enum.
pub type Tile = Option<Piece>;
// We represent the tiles of the board using a 2D array
// Each element of the first array is a row of the board.
// tiles[1][2] accesses the second row and third column of the board.
pub type Tiles = [[Tile; BOARD_SIZE]; BOARD_SIZE];

// There are three possibilities for the winner at the end of the game. We represent them as an
// enum because only one of them can ever occur at a given time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Winner {
    X,
    O,
    Tie,
}

// This type represents the possible errors that can occur when making a move
#[derive(Debug, Clone)]
pub enum MoveError {
    // Putting /// instead of // means that Rust's documentation tool will automatically pickup
    // that comment and use it when generating beautiful documentation for this module.

    /// The game was already over when a move was attempted
    GameAlreadyOver,

    // Fields allow us to provide more information about what happened

    /// The position provided was invalid
    InvalidPosition { row: usize, col: usize },

    /// The tile already contained another piece
    TileNotEmpty { other_piece: Piece, row: usize, col: usize },
}

#[derive(Debug, Clone)]
pub struct Game {
    tiles: Tiles,
    // There is always a current piece, so we don't need to wrap it in an Option type.
    current_piece: Piece,
    // There is only a winner at the end of the game, and once there is, it never changes. If we
    // wanted to, we could use the Rust type system to enforce this invariant and make sure the
    // program can't even be written in a way that would violate that. I decided to keep it simple
    // and not do that, but it's a great exercise to try out!
    // Hint: Replace the Winner enum declaration with a struct Winner(...) and make the type of
    // this field Winner. If you make that type so that the winner can only be set to something
    // other than None once, it will no longer be possible to write a program that violates the
    // invariant stated above.
    winner: Option<Winner>,
}

impl Game {
    // Using Self inside of an impl allows us to refer to its type (i.e. Game) without using the
    // type name explicitly. This is useful for renaming!
    pub fn new() -> Self {
        // Here we construct and return a new instance of Game
        Self {
            // Here, we take advantage of the Default trait to make it so that this code doesn't
            // have to know the type we defined for tiles in order to initialize it. Rust has
            // already defined the trait for arrays and the Option type, so we don't need to
            // implement it ourself!
            // More info: https://doc.rust-lang.org/std/default/trait.Default.html
            tiles: Default::default(),
            // We want to start with X
            current_piece: Piece::X,
            // There is no winner at the start of the game. We cleanly represent this with None.
            // Rust will warn us before our program even tries to run if we forget that this value
            // might be None.
            winner: None,
        }
    }

    // &mut self reflects that we plan to modify this struct in this method. Rust will ensure
    // that no other thread can access this object while we are modifying it. Thus eliminating any
    // possible data races.
    // Both row and col must be values from 0 to (BOARD_SIZE-1)
    // In the return type, () indicates the "unit type". That means that on success, this function
    // returns nothing.
    pub fn make_move(&mut self, row: usize, col: usize) -> Result<(), MoveError> {
        if self.is_finished() {
            // Here, we use return to indicate that we want to leave this function early if this
            // case occurs. We could have written it without return by using else and indenting
            // the remaining function.
            return Err(MoveError::GameAlreadyOver);
        }
        // The usize type is "unsigned", meaning it is always positive. That means that this
        // potential error case is unrepresentable. We don't need to check for it if it can't
        // happen!
        // Notice that we use .len() instead of the BOARD_SIZE constant we defined because Rust
        // arrays provide their length.
        else if row >= self.tiles.len() || col >= self.tiles[0].len() {
            // Rust supports a "field shorthand" syntax which allows us to write {row, col} instead
            // of {row: row, col: col}
            return Err(MoveError::InvalidPosition {row, col});
        }
        // Rust allows us to conditionally test a pattern match without using match directly.
        // This makes it super convenient to check if the tile is empty or not
        else if let Some(other_piece) = self.tiles[row][col] {
            // The pattern match allows us to check if there is a potential value and extract it
            // in one quick sweep. This makes writing the next line very easy!
            return Err(MoveError::TileNotEmpty {other_piece, row, col});
        }

        // Now that we've done all of the error checking, we can proceed with making the move and
        // modifying the tiles and current piece

        // Here we store the current piece at the correct location in self.tiles
        self.tiles[row][col] = Some(self.current_piece);

        // Notice that since we don't publically expose a way to set the current piece, we can
        // always be sure that it will be updated correctly and according the rules we expect.
        self.current_piece = self.current_piece.other();

        // After making a move, it may be that someone won the game. We'll use another method for
        // that since this one is getting quite long.
        self.update_winner(row, col);

        // Now that everything is complete, we can go ahead and return our "nothing" value ()
        // called "unit" to indicate that this operation was a success. We construct a Result type
        // using its Ok variant as the constructor.
        Ok(())
    }

    // We use a private method to separate code that shouldn't be accessed publically
    fn update_winner(&mut self, row: usize, col: usize) {
        // To find a potential winner, we only need to check the row, column and (maybe) diagonal
        // that the last move was made in.

        // Let's make some convenience variables for the number of rows and columns
        let rows = self.tiles.len();
        let cols = self.tiles[0].len();

        // We can extract the row pretty easily because of how we stored tiles
        let tiles_row = self.tiles[row];

        // To get the correct column, we could do something very fancy that would work for every
        // size of board, but in this case we'll just do the simplest thing and get the column
        // directly using indexing.
        let tiles_col = [self.tiles[0][col], self.tiles[1][col], self.tiles[2][col]];

        // This relies on the assumption that the board has size 3, so let's assert that so that if
        // someone ever changes this code there are no weird bugs
        // This will produce an error at runtime if this assumption is broken.
        assert!(rows == 3 && cols == 3,
            "This code was written with the assumption that there are three rows and columns");

        // There are two diagonals on the board. Their positions are as follows:
        // 1. (0, 0), (1, 1), (2, 2)
        // 2. (0, 2), (1, 1), (2, 0)
        // Due to the possibility of being on (1, 1), we might be on both diagonals. We will check
        // both diagonals separately.
        // Notice that on a 3x3 board, if row == col, we are on the first diagonal
        // and if (rows - row - 1) == col, we are on the second diagonal.
        // If we are on neither diagonal, we can just use an array of None's so that it definitely
        // won't find a match.

        // Here, we see that if statements can be used as expressions just like match statements.
        // That means that we can assign this variable to the result of the if statement.
        let tiles_diagonal_1 = if row == col {
            // Once again, we'll do the simplest thing and just use an array.

            // Diagonal 1
            [self.tiles[0][0], self.tiles[1][1], self.tiles[2][2]]
        }
        else {
            // This will never produce a winner, so it is suitable to use for the case where the
            // last move isn't on diagonal 1 anyway.
            [None, None, None]
        };

        let tiles_diagonal_2 = if (rows - row - 1) == col {
            // Diagonal 2
            [self.tiles[0][2], self.tiles[1][1], self.tiles[2][0]]
        }
        else {
            // Our last move isn't on diagonal 2.
            [None, None, None]
        };

        // Now that we have the row, column and diagonal of the last move, let's check if we have
        // a winner. To do that, we'll use a check_winner function that either returns a new
        // Winner or None. This is useful because we can chain together the methods of the Option
        // type to produce a result. This is an alternative to multiple if statements that works
        // just as well.
        fn check_winner(row: &[Tile]) -> Option<Winner> {
            // This is an "inner function". It is only visible to this update_winner method. We
            // could have defined this as a method or defined it as a function separate from this
            // impl too.
            // The type &[Tile] is known as a slice. This is how we pass an array by reference.
            // We don't have to pass the size with the array because the array pointer also stores
            // its length.
            // By returning an option type, we signal that this function may return some value or
            // no value (i.e. None).

            // Here, we once again do the simplest thing possible and just use indexes to check
            // if the entire row is the same. We could potentially do something more general using
            // iterators, but why do that if this simpler way works?
            if row[0] == row[1] && row[1] == row[2] {
                // We use a match to retrieve the correct winner based on the piece that has filled
                // this row.
                match row[0] {
                    Some(Piece::X) => Some(Winner::X),
                    Some(Piece::O) => Some(Winner::O),
                    None => None,
                }
            }
            else {
                // All the tiles are not the same, there is no winner yet, so let's signal that
                // with None
                None
            }
        }
        // Now that we can determine if there is a winner or not, we can use the option type's
        // methods to chain together the results. See the Option type documentation for more info:
        // https://doc.rust-lang.org/std/option/enum.Option.html
        self.winner = self.winner
            // The || syntax is actually defining a special function called a "closure" (or
            // "lambda" in some languages). That allows us to delay calling the check_winner
            // function until we actually need it.
            // By using or_else over and over again, we never overwrite a previously found winner
            // and the code is only run in case a previous winner was *not* found.
            .or_else(|| check_winner(&tiles_row))
            .or_else(|| check_winner(&tiles_col))
            .or_else(|| check_winner(&tiles_diagonal_1))
            .or_else(|| check_winner(&tiles_diagonal_2));

        // The final case is when the board has filled up. Here, for the first time, we'll be a
        // bit fancy and use the Iterator trait. For more info, see the book:
        // https://doc.rust-lang.org/book/second-edition/ch13-02-iterators.html
        // This is also the first time we see a multiline closure using curly braces. Just like
        // any other function, this returns the final (and only) value between the curly braces.
        self.winner = self.winner.or_else(|| {
            // You can read this code as follows:
            // if in each of the rows, all tiles have *something* in them,
            //     return that the winner is a tie.
            // otherwise, return that there is no winner yet
            // For more information on all, see:
            // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.all
            if self.tiles.iter().all(|row| row.iter().all(|tile| tile.is_some())) {
                Some(Winner::Tie)
            }
            else {
                None
            }
        });
    }

    // We can define helpful accessor functions for common questions that will be asked about this
    // type. This makes it so that people using this type won't have to rely on how the type is
    // represented.
    // &self tells the Rust compiler that we won't be modifying this type
    pub fn is_finished(&self) -> bool {
        // The last line of a function is its return value, so we don't need to write return for
        // simple one line functions.

        // The game is finished if there is a winner.
        // Since we used an Option type, we can use the convenient method it provides for checking
        // if it is Some or None instead of having to match on the type itself.
        self.winner.is_some()
    }

    // This method returns the winner of the game (if any). Since Winner derives the Copy trait, we
    // can return it directly from this function without moving its value. Rust will copy the value
    // (including the Option type that wraps it). For small types, this can make writing the code
    // much easier without introducing any additional performance penalty.
    pub fn winner(&self) -> Option<Winner> {
        self.winner
    }

    // This method is similar to the winner method above. It returns a copy of the current piece.
    // Just like Winner, Piece also implements the Copy trait.
    pub fn current_piece(&self) -> Piece {
        self.current_piece
    }

    // This function gives public, read-only access to the tiles of the board. Rust will enforce
    // at compile-time that no outside entity is able to modify the tiles from this reference.
    pub fn tiles(&self) -> &Tiles {
        // The & at the front creates a read-only reference. self.tiles accesses the tiles
        // field of this struct.
        &self.tiles
    }
}

// These are tests! Rust has testing built-in so you get a streamlined experience that encourages
// you to write tests more often.
// To run these tests, run cargo test
// More information: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html

// #[cfg(test)] tells the compiler to only include the rest of this code if we are running
// cargo test. This speeds up compile times since Rust doesn't need to process a bunch of code
// which won't be run otherwise.
#[cfg(test)]
mod tests {
    // This imports everything above so we get access to our Game, Winner, Piece, etc. types
    use super::*;

    //TODO: HELP WANTED. Please help us by writing more tests. These are not even close to
    // exhaustive, but they are a good start!

    // This is a test! Just add #[test] to a regular function and Rust will find that function
    // and run it during cargo test
    #[test]
    fn col_3_o_wins() {
        // To test the game, we just have to create a game, play on it a bit, and then check
        // what happened
        let mut game = Game::new();
        // In tests, it is okay to "unwrap" and ignore errors. If something goes wrong, the test
        // will fail because unwrap will exit with an error
        // It can be helpful to use "expect" instead since with that you can provide a message
        // for more context.
        game.make_move(0, 0).unwrap();
        game.make_move(2, 2).unwrap();
        game.make_move(2, 1).unwrap();
        game.make_move(1, 2).unwrap();
        game.make_move(0, 1).unwrap();
        game.make_move(0, 2).unwrap();
        // assert_eq! is a special macro (like println!) which checks if two things are equal
        // and then exits with an error if they are not. We use this to make sure game behaves
        // as we expect
        assert_eq!(game.winner().unwrap(), Winner::O);
    }

    #[test]
    fn diag_1_x_wins() {
        let mut game = Game::new();
        game.make_move(0, 0).unwrap();
        game.make_move(0, 1).unwrap();
        game.make_move(2, 2).unwrap();
        game.make_move(2, 1).unwrap();
        game.make_move(1, 1).unwrap();
        assert_eq!(game.winner().unwrap(), Winner::X);
    }

    #[test]
    fn diag_2_x_wins() {
        let mut game = Game::new();
        game.make_move(0, 2).unwrap();
        game.make_move(0, 1).unwrap();
        game.make_move(2, 0).unwrap();
        game.make_move(2, 1).unwrap();
        game.make_move(1, 1).unwrap();
        assert_eq!(game.winner().unwrap(), Winner::X);
    }

    #[test]
    fn row_2_o_wins() {
        let mut game = Game::new();
        game.make_move(0, 0).unwrap();
        game.make_move(1, 0).unwrap();
        game.make_move(2, 1).unwrap();
        game.make_move(1, 1).unwrap();
        game.make_move(0, 2).unwrap();
        game.make_move(1, 2).unwrap();
        assert_eq!(game.winner().unwrap(), Winner::O);
    }

    #[test]
    fn tie() {
        let mut game = Game::new();
        game.make_move(0, 0).unwrap();
        game.make_move(0, 1).unwrap();
        game.make_move(0, 2).unwrap();
        game.make_move(2, 0).unwrap();
        game.make_move(2, 1).unwrap();
        game.make_move(2, 2).unwrap();
        game.make_move(1, 0).unwrap();
        game.make_move(1, 2).unwrap();
        game.make_move(1, 1).unwrap();
        assert_eq!(game.winner().unwrap(), Winner::Tie);
    }
}

Step 4: Create main file

Here is the full code with comments:

main.rs

// This tells the Rust compiler that there is a module called "game" in a file called "game.rs"
// Conventions like this make it really easy to write code fast. If you want to customize that
// behaviour, Rust gives you the power to do that too.
mod game;

// This is how we "import" a module from the standard library. A module is a group of functions and
// types. "std" stands for "standard library" and "io" stands for "input/output". We will use this
// module to read input from the user of our application.
// The import "self" imports the name "io" itself, and "Write" imports the "Write trait" which we
// need to flush stdout below.
use std::io::{self, Write};
// We use the process::exit function to quit the program when we need to.
use std::process;

// This is how we import names from our own module. Notice that there is no "std::" prefix.
// For more information on modules, see:
// https://doc.rust-lang.org/book/second-edition/ch07-00-modules.html
use game::{Game, Piece, Winner, Tiles, MoveError};

// This type is used to provide an error when the user provides an invalid move string. If we
// wanted to avoid copying the invalid string, we could use &str instead and Rust would enforce at
// compile time that the reference remained valid until any instance of InvalidPiece containing it
// goes out of scope. String is used for the same of simplicity. By marking the type stored in this
// struct as pub, its value can be freely accessed even in patterns (for example, match
// statements).
#[derive(Debug, Clone)]
pub struct InvalidMove(pub String);

// The main function is where Rust starts running our program from. No code is allowed outside of
// functions so that you can rely on the code in main() running first.
fn main() {
    // The constructor for Game creates a new, empty Tic-Tac-Toe board. mut signals that we plan
    // to modify the value of the game variable. Rust will tell us if we forget to use this and
    // warn us if we use it but it isn't needed.
    let mut game = Game::new();

    // Let's continuously prompt the user for input using a loop until the game is finished
    while !game.is_finished() {
        // First, print out the current board
        print_tiles(game.tiles());

        // Inform the user of who's turn it currently is
        // match will enforce that we do not forget any case and the string that it produces will
        // replace {} in the printed string.
        println!("Current piece: {}", match game.current_piece() {
            Piece::X => "x",
            Piece::O => "o",
        });

        // prompt_move continuously prompts for a valid move from the user, determines exactly
        // which position on the board that move is referring to, and then returns that move
        let (row, col) = prompt_move();

        // Now that we have a move, let's attempt to make it
        // We use match to account for every case of the result
        match game.make_move(row, col) {
            // If the move is made successfully, we can just move on. You can think of empty
            // curly braces as an "empty expression". We could have also used the unit value ().
            Ok(()) => {},
            // Match allows us to conveniently match even nested types like Result and pull out the
            // fields as variables

            // Since we are using is_finished(), it should never be possible for this error to
            // occur. If it does, that means that we (the programmer) did something wrong, not the
            // user. unreachable!() works a lot like println!(); except it exits the program
            // with an error using the message that we provided it. Use unreachable!() whenever
            // you encounter a case that you think should never be reached.
            Err(MoveError::GameAlreadyOver) => unreachable!("Game was already over when it should not have been"),
            // Since prompt_move limits the range of what can be returned, it should never allow
            // the user to enter a move that is out of range. Thus, this case is unreachable as
            // well.
            Err(MoveError::InvalidPosition {row, col}) => {
                unreachable!("Should not be able to enter an invalid move, but still got ({}, {})", row, col)
            },

            // Notice that we have already eliminated two possible errors just by structuring our
            // code in a certain way!

            // This is the only case that prompt_move does not account for, so if this happens, we
            // print an error message.
            // The eprintln! macro is exactly the same as println! except it prints to stderr
            // instead of stdout.
            Err(MoveError::TileNotEmpty {other_piece, row, col}) => eprintln!(
                // Each {} will be replaced with one of the arguments following this string
                "The tile at position {}{} already has piece {} in it!",
                // The row number that is displayed starts at 1, not zero, so we add 1 to get the
                // correct value
                row + 1,
                // b'A' produces the ASCII character code for the letter A (i.e. 65)
                // Adding col to it will produce either 65 (A), 66 (B), or 67 (C).
                // as u8 is necessary because b'A' has type u8 and we can't add u8 to usize
                // without performing a conversion first.
                // Converting it to char using as char will get Rust to format this as a
                // character rather than printing the number out
                (b'A' + col as u8) as char,
                // match allows us to print something for each case and will tell us if something
                // ever changes such that this is no longer complete
                match other_piece {
                    Piece::X => "x",
                    Piece::O => "o",
                },
            ),
        }
    }

    // Once the loop is over, the game is finished. Let's output the results

    // First, we'll print the board again
    print_tiles(game.tiles());

    // Then print out which piece won the game
    // We use expect() to express that there should definitely be a winner now and if the winner
    // method returns None, the program should exit with this error
    match game.winner().expect("finished game should have winner") {
        Winner::X => println!("x wins!"),
        Winner::O => println!("o wins!"),
        Winner::Tie => println!("Tie!"),
    }
}

// Functions do not need to be ordered in any particular way in the file. That means that Rust
// doesn't suffer from any forward declaration issues where those declarations can get out of sync
// with the actual function implementation.

// This function returns a "tuple" of two values, the row and column of the selected move. Tuples
// are very useful for when you have a function that needs to return two values because it saves
// you from having to define a custom struct just for that purpose.
fn prompt_move() -> (usize, usize) {
    // We'll use loop to continuously prompt for input until the user provides what we want. When
    // we get the answer we want, the loop will return the value and it will be used as the return
    // value of this function
    loop {
        // Rust supports convenient print! and println! macros which support easy and
        // customizable formatting of values from your program. Here we are just using them to
        // prompt for some values that we want the user of our program to provide.
        print!("Enter move (e.g. 1A): ");

        // Line-buffering is when something waits until it sees a new line character before
        // actually writing to its designated destination. Rust's stdout is line-buffered by
        // default, so print! does not produce any output unless we "flush" the contents of
        // stdout's buffer in the line below.
        // expect() is how we "ignore" any error that could occur during this process. If an error
        // does occur, the program will exit with the message we provided.
        io::stdout().flush().expect("Failed to flush stdout");

        // The read_line() function is something we defined below to make reading input quick and
        // easy.
        let line = read_line();

        // We delegate reading the line as a move to the parse_move function. That function takes a
        // string and converts it to a "tuple" of two values (row, col). The read_line function
        // returns the type String, but parse_move expects a &str. We use & here to convert
        // String to &String. Rust then automatically converts &String to &str. This isn't a
        // special case for just strings, Rust supports a feature called "deref conversions" and
        // this is just a consequence of that. For more information, see:
        // http://hermanradtke.com/2015/05/03/string-vs-str-in-rust-functions.html
        match parse_move(&line) {
            // The benefit of parse_move returning a Result is that we can't forget to handle the
            // case where the input might be invalid. match gives us a convenient syntax for
            // handling each case.

            // Rust allows us to "return" a value from a loop by providing it to break. When
            // the loop exits, this will be the return value of the function too because the loop
            // is the last statement in this function.
            Ok((row, col)) => break (row, col),
            // Instead of defining methods to extract the value from InvalidMove, we can use
            // pattern matching to extract its value and print a helpful error message. The
            // eprintln! macro is exactly the same as println! except it prints to stderr
            // instead of stdout.
            Err(InvalidMove(invalid_str)) => eprintln!(
                // The {} is replaced with the next argument passed to eprintln. We can pass an
                // arbitrary amount of arguments and Rust can even tell us at compile time if there
                // is a mismatch between the number of {} and the number of additional arguments
                // passed.
                "Invalid move: '{}'. Please try again.",
                invalid_str,
            ),
        }
    }
}

// This function gets the row and column of the move the user entered. If the string doesn't
// represent a valid move, we return Result::Err to indicate failure.
// We pretty much always want to use &str instead of String in function arguments.
// For learn why, see:
// http://hermanradtke.com/2015/05/03/string-vs-str-in-rust-functions.html
// NOTE: There are various ways that we could make this more "idiomatic" using some of the advanced
// features of Rust. However, notice though that we don't really lose anything or make anything
// worse for ourselves by keeping it simple. Rust lets you write nice code even if you haven't
// mastered all of its features just yet.
fn parse_move(input: &str) -> Result<(usize, usize), InvalidMove> {
    // The move will be in the format 1A, 2C, 3B, etc.
    // Let's start by rejecting any input that isn't of size 2
    if input.len() != 2 {
        // We use return to exit early from this function in case the size of the input is
        // incorrect.
        return Err(InvalidMove(input.to_string()));
    }

    // Let's start by getting the row number
    // Using match allows us to easily accept the cases we want to support and reject everything
    // else. If none of the cases match, an error will be returned.
    let row = match &input[0..1] {
        "1" => 0,
        "2" => 1,
        "3" => 2,
        _ => return Err(InvalidMove(input.to_string())),
    };

    let col = match &input[1..2] {
        // Rust lets us match against multiple patterns using | to separate them. This
        // lets us accept either lowercase or uppercase versions of the letters.
        "A" | "a" => 0,
        "B" | "b" => 1,
        "C" | "c" => 2,

        // We didn't find a match so far, so the string must be invalid. We use the Err
        // variant of Result to express that.
        // We can convert a &str to a String using to_string(). InvalidMove expects a String,
        // so we need to do this for this code to work.
        invalid => return Err(InvalidMove(invalid.to_string())),
    };

    // The last line of the function is the return value, so we construct the tuple that we want
    // to return with the move that the user selected
    Ok((row, col))
}

// This function is something we've defined to make reading a line of input convenient. Rust gives
// us a lot of control over our program so we could do many fancy things like buffer the input as
// we read it or properly handle error conditions. However, since this is a simple application, we
// have chosen to just exit the program when an error occurs and do no extra buffering of the
// input. Since we're just reading a line at a time and we expect the lines to be short, this
// should not cause problems in the majority of cases. Rust gives us the power to make that choice
// explicitly and know that we are making it in the code.
fn read_line() -> String {
    // This creates a new growable/heap-allocated string. The mut after let declares that we
    // plan to modify the string. Saying this explicitly lets the compiler automatically check that
    // we don't modify any variables that we don't intend to. Many languages encourage you to use
    // const or final on pretty much everything until you don't need to. In Rust, that
    // behaviour is by default.
    let mut input = String::new();
    // Here, we read a line of input from the standard input stream stdin. &mut input passes a
    // mutable reference to the String in the input variable. This allows the function to modify
    // input without taking ownership of its value. That way we can return it from this function
    // afterwards.
    // expect() is a function that takes a Result value and exits the program with an error message
    // if the Result value is anything other than Ok(...). This in a way is "ignoring" any error
    // that can occur while reading input. However, instead of ignoring it implicitly, we explciitly
    // call out that we intend to just exit the program with an error if this operation fails. This
    // is one of the ways that Rust gives you control. Don't want to deal with a potential failure?
    // You don't have to! But it's really nice to know where the error came from if something ever
    // does go wrong and you want to figure out why.
    io::stdin().read_line(&mut input).expect("Failed to read input");

    // An empty string will only be returned if we reach the end of input (otherwise we always
    // receive at least a newline character).
    if input.is_empty() {
        // We print a final newline because otherwise the cursor may still be at the end of one
        // of our print! calls earlier.
        println!();

        // process::exit(0) indicates that the program exited successfully. This will end the
        // program right here, and none of the rest of our code will run.
        process::exit(0);
    }

    // read_line leaves the trailing newline on the string, so we remove it using truncate. By
    // modifying the string in place, we avoid copying its contents after it was just allocated.
    let len_without_newline = input.trim_right().len();
    input.truncate(len_without_newline);

    // The last expression in a function is returned from that function. We want to return the
    // line that was read, so we put that variable on its own at the end of the function in order
    // to provide it as the result of this function.
    input
}

// This function is used to print out the board in a human readable way
fn print_tiles(tiles: &Tiles) {
    // The result of this function will be something like the following:
    //   A B C
    // 1 x ▢ ▢
    // 2 ▢ ▢ o
    // 3 ▢ ▢ ▢
    //
    // The boxes represent empty tiles, and x and o are placed wherever a tile is filled.

    // First we print the space before the column letters
    print!("  ");
    // Then we look from the numbers 0 to 2.
    // a..b creates a "range" of numbers from a to one less than b.
    // tiles[0].len() gets the number of columns (i.e. 2)
    // as u8 converts the length from the type usize to the type u8 so that it works in the
    // body of the loop
    for j in 0..tiles[0].len() as u8 {
        // b'A' produces the ASCII character code for the letter A (i.e. 65)
        // By adding j to it, we get 'A', then 'B', and then 'C'.
        // We don't just want to print the ASCII character code, so we convert that number into
        // a character using as char. That way Rust will print it correctly.
        print!(" {}", (b'A' + j) as char);
    }
    // This prints the final newline after the row of column letters
    println!();

    // Now we print each row preceeded by its row number
    // .iter().enumerate() goes through each row and provides a row number with each element using
    // a tuple.
    for (i, row) in tiles.iter().enumerate() {
        // We print the row number with a space in front of it
        print!(" {}", i + 1);
        // Now we go through each tile in the row and print it out
        for tile in row {
            // Here, we match on the value of the tile. We use * to "dereference" the tile and
            // match on its value of type Option<Piece>. This is just for convenience and is
            // actually something that future versions of Rust might not even require in order to
            // match on something as simple as this.
            print!(" {}", match *tile {
                // The string produced by this match will be printed in print!. This match works
                // because we return the same type, &str, in each branch. Rust still requires that
                // if a match statement produces a value, it produces a value of the same type in
                // every branch.
                // Notice that we don't need to create another match for the piece produced in
                // Some(...). Rust allows us to match arbitrarily nested structures with no
                // additional syntax.
                Some(Piece::X) => "x",
                Some(Piece::O) => "o",
                None => "\u{25A2}",
            });
        }
        // We finish each row by printing a final new line
        println!();
    }

    // Add an extra line at the end of the board to space it out from the prompts that follow
    println!();
}

Run

Lastly run the project.

Reference

Download the code below:

Number Link
1. Download Example
2. Follow code author

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *