Let’s play Tic-Tac-Toe but in a rusty way.

Saakshi Raut
7 min readMar 24, 2024

--

No one can beat me in this game😉

Featured Image

Hello everyone, in this blog I explain how you can create a Tic Tac Toe game using Rust. I've also previously created a Guessing Game, so be sure to check it out because I've explained most of the syntax there.

We've all played Tic-Tac-Toe multiple times, but have you developed it in Rust yet? If not, let's get started.

So basically, we are creating a game that can be played in the terminal. Specifically, I am developing this project to learn more about the syntax of Rust and get hands-on experience with it.

Let’s begin

Create a directory for your “tic-tac-toe” game and set it up to run our main.rs file. Within the main file, we’ll start by initializing the board and the first player. In our game, the empty grid is indicated by the symbol and X is the first player. The following piece of code does the same.

fn main() {
println!("Tic Tac Toe Game");
let mut board = [['-'; 3]; 3];
let mut player = 'X';
}

Print the board

In this project, we will need to print the board every time one of the users makes a move. Therefore, it would be better if we create a separate print function for this purpose. The following is the user-defined print function to which we will pass the board, and it will print it. Don’t forget to call it in your main function.

// Print the board
fn print_board(board: &[[char; 3]; 3]) {
// For printing three characters:
// 0..3 indicates < 3 whereas you can also use 0..=2 which indicates <= 2
for i in 0..3 {
for j in 0..3 {
print!("'{}' ", board[i][j]);
}
println!("\n")
}
}

fn main() {
println!("Tic Tac Toe Game");
let mut board = [['-'; 3]; 3];
let mut player = 'X';
}

User Input

This game is a command line interface (CLI) game, where we take the user input as the row number and column number of where they want to place the sign. To take user input, we first need to import the std::io file. Since the user will type the input in one line, we'll need to split the input and convert it into an integer because the read_line function returns a string by default. Additionally, since our array starts from 0, we'll need to subtract 1 from the user input. The figure below will clarify what I'm trying to explain.

The POV of Coordinates.

Add the below code to take the user input:

let mut coordinates = input.split_whitespace();
let mut row: usize = coordinates.next().unwrap().parse().expect("Enter Integer.");
row = row - 1;
let mut col: usize = coordinates
.next()
.unwrap()
.parse()
.expect("Enter valid col");
col = col - 1;

Now obviously we don’t want to overwrite the grid. So before adding the symbol, X or O we’ll check whether that grid is empty or not. The following code does the same:

use std::io;

// Print the board
fn print_board(board: &[[char; 3]; 3]) {
for i in 0..3 {
for j in 0..3 {
print!("'{}' ", board[i][j]);
}
println!("\n")
}
}

fn main() {
println!("Tic Tac Toe Game");
let mut board = [['-'; 3]; 3];
let mut player = 'X';
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read the input.");
let mut coordinates = input.split_whitespace();
let mut row: usize = coordinates.next().unwrap().parse().expect("Enter Integer.");
row = row - 1;
let mut col: usize = coordinates
.next()
.unwrap()
.parse()
.expect("Enter Integer.");
col = col - 1;
// Check whether the given row or col is empty:
if board[row][col] == '-' {
board[row][col] = player;
} else {
println!("Overwriting the grid is not allowed.");

}
}

Is the player Winning?

Now we’ll write a function to check whether the player is winning or not. Now there are 4 cases shown in the following figure:

Winning Chances

The function given below checks whether the player wins after every move or not.

fn is_win(board: &[[char; 3]; 3], player: char) -> bool {
// Check rows
for i in 0..3 {
let mut win = true;
for j in 0..3 {
if board[i][j] != player {
win = false;
break;
}
}
if win {
return true;
}
}
// Check columns
for j in 0..3 {
let mut win = true;
for i in 0..3 {
if board[i][j] != player {
win = false;
break;
}
}
if win {
return true;
}
}
// Check diagonals
if (board[0][0] == player && board[1][1] == player && board[2][2] == player)
|| (board[0][2] == player && board[1][1] == player && board[2][0] == player)
{
return true;
}
false
}

Now let’s call this function after the player makes a move:

use std::io;

// Print the board
fn print_board(board: &[[char; 3]; 3]) {
for i in 0..3 {
for j in 0..3 {
print!("'{}' ", board[i][j]);
}
println!("\n")
}
}

// Check whether the player is winning
fn is_win(board: &[[char; 3]; 3], player: char) -> bool {
// Check rows
for i in 0..=2 {
let mut win = true;
for j in 0..3 {
if board[i][j] != player {
win = false;
break;
}
}
if win {
return true;
}
}
// Check columns
for j in 0..3 {
let mut win = true;
for i in 0..3 {
if board[i][j] != player {
win = false;
break;
}
}
if win {
return true;
}
}
// Check diagonals
if (board[0][0] == player && board[1][1] == player && board[2][2] == player)
|| (board[0][2] == player && board[1][1] == player && board[2][0] == player){
return true;
}
false
}

fn main() {
println!("Tic Tac Toe Game");
let mut board = [['-'; 3]; 3];
let mut player = 'X';
println!("{player}'s turn: ");
println!("Enter the row and col: ");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read the input.");
let mut coordinates = input.split_whitespace();
let mut row: usize = coordinates.next().unwrap().parse().expect("Enter Integer.");
row = row - 1;
let mut col: usize = coordinates
.next()
.unwrap()
.parse()
.expect("Enter Integer.");
col = col - 1;
// Check whether the given row or col is empty:
if board[row][col] == '-' {
board[row][col] = player;
} else {
println!("Overwriting the grid is not allowed.");

}
// Check wheter the player wins;
if is_win(&board, player) {
println!("{player}'x wins.");
}
}

There's one more case: if all the fields are filled and no one wins, the game will still continue. To avoid this, we'll include the is_board_filled()function in the code and call it in the main function.

// Check if the board is filled
fn is_board_filled(board: &[[char; 3]; 3]) -> bool {
for i in 0..3 {
for j in 0..3 {
if board[i][j] == '-' {
return false;
}
}
}
true
}
// Call the function in main
if is_board_filled(&board) {
println!("It's a draw");
}

Change player

Now, it's time to change the player, so we'll include another function that will take care of that.

fn next_player(player: char) -> char {
if player == 'X' {
return '0';
} else {
return 'X';
}
}

In the main function, change the player using the following line, player = next_player(player);

Now the only thing left is to add a loop which will enable the players to make moves until the match draws or someone wins.

The final piece of code:

use std::io;

fn print_board(board: &[[char; 3]; 3]) {
for i in 0..=2 {
for j in 0..3 {
print!("'{}' ", board[i][j]);
}
println!("\n")
}
}

fn next_player(player: char) -> char {
if player == 'X' {
return '0';
} else {
return 'X';
}
}

fn is_win(board: &[[char; 3]; 3], player: char) -> bool {
// Check rows
for i in 0..=2 {
let mut win = true;
for j in 0..3 {
if board[i][j] != player {
win = false;
break;
}
}
if win {
return true;
}
}
// Check columns
for j in 0..3 {
let mut win = true;
for i in 0..3 {
if board[i][j] != player {
win = false;
break;
}
}
if win {
return true;
}
}

// Check diagonals
if (board[0][0] == player && board[1][1] == player && board[2][2] == player)
|| (board[0][2] == player && board[1][1] == player && board[2][0] == player)
{
return true;
}

false
}

fn is_board_filled(board: &[[char; 3]; 3]) -> bool {
for i in 0..3 {
for j in 0..3 {
if board[i][j] == '-' {
return false;
}
}
}
true
}

fn main() {
println!("Tic Tac Toe Game");
let mut board = [['-'; 3]; 3];
let mut player = 'X';
print_board(&board);
loop {
println!("{player}'s turn: ");
println!("Enter the row and col: ");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read the input.");
let mut coordinates = input.split_whitespace();
let mut row: usize = coordinates.next().unwrap().parse().expect("Enter Integer.");
row = row - 1;
let mut col: usize = coordinates
.next()
.unwrap()
.parse()
.expect("Enter valid col");
col = col - 1;
// Check whether the given row or col is empty:
if board[row][col] == '-' {
board[row][col] = player;
} else {
println!("Overwriting the grid is not allowed.");
break;
}
// Check wheter the player wins;
if is_win(&board, player) {
println!("{player}'x wins.");
break;
}
if is_board_filled(&board) {
println!("It's a draw");
break;
}
print_board(&board);
player = next_player(player);
}
}

Now you can enjoy playing this game with your friends :))

Conclusion

That’s it for the day. If you get any error or have a way to optimize the code let me know in the comment section. Plus, I want to make a web application of this game so if you have any resources regarding that please let me know in the comment section below.

Stay tuned for some more rusty blogs lol. 😂😂

--

--

Saakshi Raut

Hello, I'm Saakshi Raut, a web developer and open source contributor. 👽