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))}```