Building Tic-Tac-Toe in Rust: Part 1 - A Simple Text Game

Tic-Tac-Toe is a classic game that’s perfect for practicing programming. In this advanced tutorial series, we’ll build a modular, testable, and extensible version of Tic-Tac-Toe in Rust. In this first part, we’ll create a simple text-based version of the game that:

  1. Alternates between two players.
  2. Accepts input to choose squares.
  3. Displays the game board and lists empty squares after each turn.

Step 1: Plan the Game

Before jumping into the code, let’s break the game into modular components:

  • Game State: Tracks the board and whose turn it is.
  • Player Input: Handles player input and validates it.
  • Game Logic: Checks for a win or draw and updates the board.
  • Display: Prints the game board and available squares.

Step 2: Set Up the Rust Project

  1. Create a new Rust project:

    cargo new tic_tac_toe
    cd tic_tac_toe
    
  2. Add a src/main.rs file to start coding.


Step 3: Define the Game State

We’ll represent the board as a 3x3 grid using a vector of Option<char>. Each square will be None (empty), or contain 'X' or 'O'.

#[derive(Debug, Clone)]
struct GameState {
    board: Vec<Option<char>>,
    current_player: char,
}

impl GameState {
    fn new() -> Self {
        GameState {
            board: vec![None; 9],
            current_player: 'X',
        }
    }

    fn display(&self) {
        println!("\nCurrent Board:");
        for row in self.board.chunks(3) {
            println!(" {} | {} | {} ",
                Self::symbol(row[0]),
                Self::symbol(row[1]),
                Self::symbol(row[2])
            );
            println!("---+---+---");
        }
    }

    fn symbol(square: Option<char>) -> char {
        match square {
            Some(c) => c,
            None => ' ',
        }
    }
}

Step 4: Handle Player Input

We’ll write a function to:

  1. Ask the current player for a move.
  2. Validate the input.
  3. Update the game board.
impl GameState {
    fn play_turn(&mut self) {
        loop {
            println!("Player {}, enter a position (1-9):", self.current_player);
            let mut input = String::new();
            std::io::stdin().read_line(&mut input).unwrap();

            if let Ok(position) = input.trim().parse::<usize>() {
                if position >= 1 && position <= 9 && self.board[position - 1].is_none() {
                    self.board[position - 1] = Some(self.current_player);
                    break;
                }
            }
            println!("Invalid input. Please try again.");
        }

        self.current_player = if self.current_player == 'X' { 'O' } else { 'X' };
    }
}

Step 5: Check for a Winner

Add logic to determine if the game has been won:

impl GameState {
    fn check_winner(&self) -> Option<char> {
        let winning_combinations = [
            [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
            [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
            [0, 4, 8], [2, 4, 6],           // Diagonals
        ];

        for combo in &winning_combinations {
            if let (Some(a), Some(b), Some(c)) = (
                self.board[combo[0]],
                self.board[combo[1]],
                self.board[combo[2]],
            ) {
                if a == b && b == c {
                    return Some(a);
                }
            }
        }
        None
    }
}

Step 6: Main Game Loop

Finally, put everything together in a loop that alternates turns, displays the board, and checks for a winner or draw.

fn main() {
    let mut game = GameState::new();

    loop {
        game.display();
        game.play_turn();

        if let Some(winner) = game.check_winner() {
            game.display();
            println!("Player {} wins!", winner);
            break;
        }

        if game.board.iter().all(|&square| square.is_some()) {
            game.display();
            println!("It's a draw!");
            break;
        }
    }
}

Next Steps

In the next post, we’ll:

  • Add unit tests for the game logic.
  • Refactor for better modularity and testability.
  • Introduce error handling and improve user experience.

Try coding along and let me know if you create any variations or discover interesting challenges!

You can find the code for this post on GitHub: Tic-Tac-Toe Repository.