DEV Community

Cover image for Build your first game using rust
Bekka
Bekka

Posted on • Edited on

Build your first game using rust

When you get familiar with rust's syntax and concepts, It's great to strengthen your mental model by building games because not only will you have fun building a game, It'll also give you another perspective when you're writing code.

In this post, we are going to build a simple hangman game using rust! But first, I'm going to list the concepts you are going to use in this program.

Prerequisites

  • Basic knowledge of command-line and how to traverse between directories

  • Basic knowledge of Rust

Concepts used in this project

  • Variables

  • Input/Output operations from the command-line

  • Structs and also implementing methods

  • Vectors

  • Match statements and control flow

  • Functions

  • Loops

Install Rust

If you don't have rust already installed in your machine, let's install rust. If you're using MacOs or a linux machine you can download it via install rust using rustup or if you're using windows.

It's best to run this project in your local machine and not on an online editor because some of the packages we will be using might not work as expected on an online editor.

The idea behind Hangman game

If you don't know about the famous hangman game, It's a famous game that used to be in the old Desktops (mostly windows). I used to play this game with my siblings when I was a kid.

You are given a word without letters πŸ˜„. You have to guess each alphabet that makes up that word, while you're guessing, there's a man waiting, his life is dependent on your guesses, so for each wrong guess you make, you pave the path for the poor guy to be hung, for each correct guess you make nothing happens, if you guess all the alphabets in that word correctly, he is released from his shackles!

If you wanna see a better representation of what I'm writing about, check it out here

Hangman game

Once you're done installing rust and checking out the online game, let's move into the purpose of this post.

Pseudocode/logic in human terms

  • Player starts the game.

  • Player is given a word with empty letters.

  • Player types in a letter in attempt to guess the word.

  • Player is given ten attempts to guess the word.

  • For every guess either wrong or right, the remaining guesses are reduced by one.

  • To make things harder, If the player guesses an already guessed letter, the remaining guesses are reduced by one.

  • If players guesses all the letters correctly before the guesses are exhausted, the player wins else the player loses.

  • Program ends, start again from the beginning.

In our case, the amount of guesses we have can be likened to the hangman dependent on our guesses instead of drawing the hangman in our terminal, this is too much work for that, trust me you don't want to start messing with the GUI for that(Especially for beginners), but if wanna do it, be my guest!

Creating the hangman project

Open your terminal and create a new project using cargo

cargo new hangman
Enter fullscreen mode Exit fullscreen mode

This will create a new rust project and install cargo, we will be able to run this project using cargo. So cd into the newly created project and type cargo run this should print "Hello, world!" in the terminal.

Next, we add the bare bone logic for this project which is to be able to compare letters with words.

So clear all the code in the fn main () function which is in the src folder inside main.rs. Note, this is where you will be writing all the logic.

Implementing the base logic (user can guess letter and compare letters)

Firstly, we are going to initialise a struct, this will be used to keep all our "assets" something likened to the state of the program, it will also be used for observation, comparisons and more. Copy the code below.

In Rust, memory management is quite different from Javascript, in the sense that all variables are kept in memory and are freed in certain circumstances. This is great, rust has a mechanism for checking all these types using the borrow checker. This is why when you create a struct and initialise it's values won't change until you do!

Please note, this is my first project in rust, so the code style might not suit you, it's just for learning purposes, so excuse me :).


struct PlayerRoot {
        word: String,
        no_of_guesses: i8,
        available_alphabets: Vec<char>,
        list_of_words_to_guess_from: Vec<String>,
        output_string: Vec<char>,
        max_tries: i8,
        guess: String,
    }
Enter fullscreen mode Exit fullscreen mode

The PlayerRoot struct already gives you an idea of how the project is going to be structured like.

Next is to implement the following;

  • Import rand to choose random indexes in the list of available words, std::io for input/output operations.

  • Create the struct handling the player's asset, which are the guesses and tries.

  • Implement methods into the struct to populate the struct key/values when initialised.

  • The generate_random_word() method is created to generate new words using rand, this is an easier way to do this because vectors are always stored on the heap, rand package is a great package for handling situations like that. If you don't really know much about the heap and stack in rust, you can read about here

  • The new() method to initialise the struct with the necessary values, it is also considered an idiomatic way of writing rust.

  • The application takes in an input from the user and compares the user's input against the random word chosen for the game round, it checks if the letter is in the random word and prints out to the console, this is done in a short loop.

  • Make sure the user input is a character which is of char type.

  • Create a list of words to choose randomly from.

Copy the following code to replace everything in main.rs.


use rand::seq::SliceRandom;
use std::io;
#[allow(dead_code)]

fn main() {
    struct PlayerRoot {
        word: String,
        no_of_guesses: i8,
        available_alphabets: Vec<char>,
        list_of_words_to_guess_from: Vec<String>,
        output_string: Vec<char>,
        max_tries: i8,
        guess: String,
    }

    struct PlayerGuesser {
        guess: char,
        tries: i8,
    }

    impl PlayerRoot {
        fn new(
            word: &str,
            no_of_guesses: i8,
            available_alphabets: Vec<char>,
            list_of_words_to_guess_from: Vec<String>,
            output_string: Vec<char>,
            max: i8,
            guess: String,
        ) -> PlayerRoot {
            PlayerRoot {
                word: String::from(word),
                no_of_guesses,
                available_alphabets,
                list_of_words_to_guess_from,
                output_string,
                max_tries: max,
                guess,
            }
        }

        fn generate_random_word(list: &Vec<String>) -> String {
            let word = list.choose(&mut rand::thread_rng()).unwrap();
            println!("word {:?}", word);
            word.to_string()
        }
    }


    //list of words for the game
    let list_of_words = vec![
        "hunting".to_string(),
        "dizzy".to_string(),
        "while".to_string(),
        "string".to_string(),
        "something".to_string(),
        "notified".to_string(),
    ];

    let random_word = PlayerRoot::generate_random_word(&list_of_words);

    let letters = vec![
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
        'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
    ];


    let guess_chars = vec![];

    // for our simple UI 
    let guess_vec: Vec<char> = random_word.clone().chars().collect();
    let output_string_vec = vec!['_'; guess_vec.len()];

    let mut player_one = PlayerRoot::new(
        &random_word,
        0,
        letters,
        list_of_words,
        output_string_vec,
        10,
        "".to_string(),
        guess_chars,
    );


    loop {
        //Takes in an input
        let mut guess = String::from("");
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        println!("You guessed: {}", guess);
        let altered_guess = guess.chars().next().unwrap();
        for n in random_word.char_indices() {
            println!("{:?}", n.1);
            if n.1 == altered_guess {
                println!("your guess{:?}, the rest {:?}", altered_guess, random_word)
            }
        }
        break;
    }
}
Enter fullscreen mode Exit fullscreen mode

Add rand into your cargo.toml like this:

rand = "0.8.5"
Enter fullscreen mode Exit fullscreen mode

At this time of writing, this is the current version, you can change to any version at whatever time you are :)

You can run this code and see what it does so far.

Implementing the logic to keep the character the user has guessed and reduce the number of guesses left

For you to keep up with the guesses left and the letters guessed, you need to do a check for every guess made by the user. You're going to do the following in code.

  • Add a new field in the struct to keep correct guesses named correct_guesses and initialise it along with the default fields.

  • Add an if block to check. If a guess is made increase the guess tries by 1, if the guess is correct, push the letter into correct_guesses:vec!.

Let's write some code. Add the following to PlayerRoot struct.

     correct_guesses: Vec<char>,
Enter fullscreen mode Exit fullscreen mode

Also add it to the new() method in the implement block, It should look like so;


     fn new(
            word: &str,
            no_of_guesses: i8,
            available_alphabets: Vec<char>,
            list_of_words_to_guess_from: Vec<String>,
            output_string: Vec<char>,
            max: i8,
            guess: String,
            correct_guesses: Vec<char>,
        ) -> PlayerRoot {
            PlayerRoot {
                word: String::from(word),
                no_of_guesses,
                available_alphabets,
                list_of_words_to_guess_from,
                output_string,
                max_tries: max,
                guess,
                correct_guesses
            }
        }

Enter fullscreen mode Exit fullscreen mode

Look for where we initialise player_one make it mutable, create a new variable;

let guess_chars = vec![];
Enter fullscreen mode Exit fullscreen mode

Add it in the player_one fields. It should look like this.

 let guess_chars = vec![];
   let guess_vec: Vec<char> = random_word.clone().chars().collect();
let output_string_vec = vec!['_'; guess_vec.len()];
    let mut player_one = PlayerRoot::new(
        &random_word,
        0,
        letters,
        list_of_words,
        output_string_vec,
        10,
        "".to_string(),
        guess_chars,
    );
Enter fullscreen mode Exit fullscreen mode

You're going to comment out the current loop block we have and add a new one. Copy the following code block;

println!("Welcome to the hangman game built with rust!, please enter a letter");
loop {

        //Takes in an input
        let mut guess = String::from("");
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        println!("You guessed: {}", guess);

        // makes sure the guess is a char
        let altered_guess = guess.trim().chars().next().unwrap();
player_one.no_of_guesses += 1;
        // creates an outer loop
        'outer: for n in random_word.char_indices() {
            // Make the check for correct guesses and does something after
            if !player_one.correct_guesses.contains(&n.1) {
                if n.1 == altered_guess {
                    player_one.correct_guesses.push(n.1);
                }
            } else {
                continue 'outer;
            }
            println!(
                "correct guess list{:?}, no of guesses {:?}",
                player_one.correct_guesses, player_one.no_of_guesses
            )
        }

    }

Enter fullscreen mode Exit fullscreen mode

You can run this code and see what it does so far. Right now it prints the random word, creates a vector for correct guesses, increases the total number of guesses by one for each guess, If the guess is correct, It adds it into the vector of correct guesses, If the total guess tries is equal to the max guesses, then it's "GAME OVER!!!". All these are printed in the console for you to see.

Already, We've done a major part of the work, although there are some bugs, don't worry we'll fix them. So far, the player can guess the letters in the word and if they don't get it right within the amount of guesses given, then the game ends, the program ends.

Add a Simple UI to the Hangman game and complete the game.

Just like the most hangman games, there are blank spaces, these blank spaces are what make up the word.

In this section, you are going to build a simple UI, which is the blank spaces to depict the blank letters to fill in a word and complete the remaining part of this project.

Add the code block below, replace or comment out the loop block.


loop {

        //Takes in an input
        //Todo Check if input is more than one char
        let mut guess = String::from("");
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        let altered_guess: char = match guess.trim().chars().next() {
            Some(val) => val,
            _ => {
                println!("No letter inputted,type a letter!");
                //It complains it needs a char :D
                '0'
            }
        };


// Checks if guess is valid
        if !player_one.output_string.contains(&altered_guess) {
            player_one.no_of_guesses += 1;

            if !player_one.word.contains(altered_guess) {
                let guess_score = player_one.max_tries - player_one.no_of_guesses;
                println!(
                    "Wrong guess\nFill in the blank spaces{:?} no of guesses remaining {:?}",
                    player_one.output_string, guess_score
                );
            }

            // loops through the word, check if guess is correct, reduces number of guess by one
            for n in player_one.word.char_indices() {
                if n.1 == altered_guess {
                    let guess_score = player_one.max_tries - player_one.no_of_guesses;

                    player_one.correct_guesses.push(n.1);
                    player_one.output_string[n.0] = n.1;

                    println!(
                        "Fill in the blank spaces{:?} no of guesses remaining {:?}",
                        player_one.output_string, guess_score
                    );
                }
            }
        } else {
            println!("That letter is taken!!! guess again")
        }
        // If the player wins
        if player_one.correct_guesses.len() == guess_vec.len() {
            println!("YOU WIN!!");
            break;
        }

        // If the player loses
        if player_one.max_tries == player_one.no_of_guesses {
            println!("GAME OVER!!! \n THE WORD IS {:?}", player_one.word);
            break;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Here, you did the following;

  • The game takes in the player's guess and makes sure it is a char, so if the user types in two letters, it takes in the first. This isn't a bug, it's a feature :)

  • Checks if the guess is valid

  • Loop through the random word given, check if guess is correct, reduces number of guess by one.

  • Write the condition for the player to win.

  • Make another condition for the player lose.

The main.rs file should look like this;


use rand::seq::SliceRandom;
use std::io;
#[allow(dead_code)]

fn main() {
    struct PlayerRoot {
        word: String,
        no_of_guesses: i8,
        available_alphabets: Vec<char>,
        list_of_words_to_guess_from: Vec<String>,
        output_string: Vec<char>,
        max_tries: i8,
        guess: String,
        correct_guesses: Vec<char>,
    }

    struct PlayerGuesser {
        guess: char,
        tries: i8,
    }

    impl PlayerRoot {
        fn new(
            word: &str,
            no_of_guesses: i8,
            available_alphabets: Vec<char>,
            list_of_words_to_guess_from: Vec<String>,
            output_string: Vec<char>,
            max: i8,
            guess: String,
            correct_guesses: Vec<char>,
        ) -> PlayerRoot {
            PlayerRoot {
                word: String::from(word),
                no_of_guesses,
                available_alphabets,
                list_of_words_to_guess_from,
                output_string,
                max_tries: max,
                guess,
                correct_guesses,
            }
        }

        fn generate_random_word(list: &Vec<String>) -> String {
            let word = list.choose(&mut rand::thread_rng()).unwrap();
            // println!("word {:?}", word);
            word.to_string()
        }
    }

    //list of words for the game
    let list_of_words = vec![
        "hunting".to_string(),
        "dizzy".to_string(),
        "while".to_string(),
        "string".to_string(),
        "something".to_string(),
        "notified".to_string(),
    ];

    let random_word = PlayerRoot::generate_random_word(&list_of_words);

    let letters = vec![
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
        'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
    ];

    let guess_chars = vec![];

    // for our simple UI
    let guess_vec: Vec<char> = random_word.clone().chars().collect();
    let output_string_vec = vec!['_'; guess_vec.len()];

    let mut player_one = PlayerRoot::new(
        &random_word,
        0,
        letters,
        list_of_words,
        output_string_vec,
        10,
        "".to_string(),
        guess_chars,
    );

    println!("Welcome to the hangman game built with rust!, please enter a letter");
    println!(
        "Fill in the blank spaces{:?}, no of guesses made {:?}, no of max tries {:?}",
        player_one.output_string, player_one.no_of_guesses, player_one.max_tries
    );

    loop {

        //Takes in an input
        //Todo Check if input is more than one char
        let mut guess = String::from("");
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        let altered_guess: char = match guess.trim().chars().next() {
            Some(val) => val,
            _ => {
                println!("No letter inputted,type a letter!");
                //It complains it needs a char :D
                '0'
            }
        };


// Checks if guess is valid
        if !player_one.output_string.contains(&altered_guess) {
            player_one.no_of_guesses += 1;

            if !player_one.word.contains(altered_guess) {
                let guess_score = player_one.max_tries - player_one.no_of_guesses;
                println!(
                    "Wrong guess\nFill in the blank spaces{:?} no of guesses remaining {:?}",
                    player_one.output_string, guess_score
                );
            }

            // loops through the word, check if guess is correct, reduces number of guess by one
            for n in player_one.word.char_indices() {
                if n.1 == altered_guess {
                    let guess_score = player_one.max_tries - player_one.no_of_guesses;

                    player_one.correct_guesses.push(n.1);
                    player_one.output_string[n.0] = n.1;

                    println!(
                        "Fill in the blank spaces{:?} no of guesses remaining {:?}",
                        player_one.output_string, guess_score
                    );
                }
            }
        } else {
            println!("That letter is taken!!! guess again")
        }
        // If the player wins
        if player_one.correct_guesses.len() == guess_vec.len() {
            println!("YOU WIN!!");
            break;
        }

        // If the player loses
        if player_one.max_tries == player_one.no_of_guesses {
            println!("GAME OVER!!! \n THE WORD IS {:?}", player_one.word);
            break;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

When you run the program the output should look like this below, of course it might be another word that is longer. This is the link to the source code

Welcome to the hangman game built with rust!, please enter a letter
Fill in the blank spaces['_', '_', '_', '_', '_'], no of guesses made 0, no of max tries 10

Enter fullscreen mode Exit fullscreen mode

Conclusion

You just built your first game in rust! Woot! Congratulations on making it this far!. You can play around this project, add modifications, add GUI, add a score keeper, e.t.c.

This is a great step in learning how to code, remember it is cool to have fun at what you do. This is not really a project is to push to production, but It's great for learning. If you're learning rust check out the book, It's a great resource. What are you going to build for fun this week?πŸ˜ƒ

Top comments (0)