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 <[email protected]>"]
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))
}
```