Basic Dice Roll Simulator in Rust

Basic Dice Roll Simulator in Rust

ยท

5 min read

As a newcomer to Rust, I've embarked on a learning journey into this powerful language. In this post, I share my experience of learning Rust by walking you through the creation of a dice roll simulator. By gradually introducing new concepts and building upon the previous ones. I welcome your feedback and suggestions.

The Basic Dice Roll Simulator

In our first step, we implement a basic dice roll simulator. The program uses the rand crate to generate a random number between 1 and 6, simulating the roll of a six-sided die.

extern crate rand;
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let roll = rng.gen_range(1..=6);

    println!("You rolled a {}", roll);
}

The rand::thread_rng() function gives us a random number generator that we can use to generate our dice roll. The gen_range function generates a number in the range specified (in this case, 1 to 6).

Adding a Condition

Next, we want our program to keep rolling until it rolls a 2 or a 6. We can accomplish this by adding a loop and an if statement to check the result of each roll.

extern crate rand;
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let mut counter = 0;

    loop {
        let roll = rng.gen_range(1..=6);
        counter += 1;

        if roll == 2 || roll == 6 {
            println!("Rolled a {} after {} tries!", roll, counter);
            break;
        }
    }
}

Here, we've introduced a counter to keep track of how many rolls we've made. The loop construct creates an infinite loop that only breaks when we roll a 2 or a 6.

Utilising Match Patterns

Rust's match expression is a powerful tool that allows for clean, readable code. We can use a match statement instead of an if statement to check the result of each roll.

extern crate rand;
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let mut counter = 0;

    loop {
        let roll = rng.gen_range(1..=6);
        counter += 1;

        match roll {
            2 | 6 => {
                println!("Rolled a {} after {} tries!", roll, counter);
                break;
            },
            _ => continue,
        }
    }
}

In this version, the match pattern 2 | 6 matches if the roll is either 2 or 6, which makes the code more concise and easier to read.

Separating Concerns

To make our code more maintainable, we'll separate the logic for rolling the dice into its own function.

extern crate rand;
use rand::Rng;

fn main() {
    let mut counter = 0;

    reroll(&mut counter);
}

fn reroll(counter: &mut i32) {
    let mut rng = rand::thread_rng();

    loop {
        let roll = rng.gen_range(1..=6);
        *counter += 1;

        match roll {
            2 | 6 => {
                println!("Rolled a {} after {} tries!", roll, *counter);
                break;
            },
            _ => continue,
        }
    }
}

The main function now simply initializes the counter and calls reroll, making it easier to see at a glance what the program does. Meanwhile, the reroll function focuses solely on rolling the dice and checking the result, making it easier to understand and modify this logic separately.

Making the Target Numbers Dynamic

We can increase the versatility of our dice roll simulator by modifying the reroll function to accept a slice of integers representing the numbers we are trying to roll.

extern crate rand;
use rand::Rng;

fn main() {
    let mut counter = 0;
    let target_numbers = [2, 6];

    reroll(&mut counter, &target_numbers);
}

fn reroll(counter: &mut i32, target_numbers: &[i32]) {
    let mut rng = rand::thread_rng();

    loop {
        let roll = rng.gen_range(1..=6);
        *counter += 1;

        if target_numbers.contains(&roll) {
            println!("Rolled a {} after {} tries!", roll, *counter);
            break;
        }
    }
}

With this enhancement, we define an array of target numbers in the main function and pass it to the reroll function. The reroll function uses the contains method to check if the roll is in the slice of target numbers, making our program more dynamic.

Reading Target Numbers from Command Line Arguments

Finally, we modify our program to accept target numbers from command line arguments. This way, the user can specify their own target numbers when they run the program.

extern crate rand;
use std::env;
use rand::Rng;

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut counter = 0;

    if args.len() < 2 {
        eprintln!("Please provide at least one target number.");
        std::process::exit(1);
    }

    let target_numbers: Vec<i32> = args[1..]
        .iter()
        .map(|x| x.parse::<i32>().unwrap_or_else(|_| {
            eprintln!("Failed to parse '{}'. Please provide integers.", x);
            std::process::exit(1);
        }))
        .collect();

    reroll(&mut counter, &target_numbers);
}

fn reroll(counter: &mut i32, target_numbers: &[i32]) {
    let mut rng = rand::thread_rng();

    loop {
        let roll = rng.gen_range(1..=6);
        *counter += 1;

        if target_numbers.contains(&roll) {
            println!("Rolled a {} after {} tries!", roll, *counter);
            break;
        }
    }
}

In this final version, we use the std::env::args function to collect the command line arguments into a Vec<String>. We then parse these strings into integers and pass them as the target numbers to the reroll function. If any of the arguments can't be parsed as integers, the program prints an error message and exits.

How to Run the Program

To compile and run the program, you can use the cargo run command followed by your target numbers. The cargo run command first compiles your Rust code and then runs the resulting executable. For example:

cargo run 2 6

This will run the program and it will keep rolling the dice until it rolls a 2 or a 6.

Conclusion

This post outlines my journey learning Rust via a dice roll simulator project. I progressively incorporated features, starting from a simple roll to user-defined targets. Each step was a lesson in Rust's rich capabilities, from match patterns to error handling. Please note, this approach, while instrumental to my learning, may not be the most efficient way to write Rust code. I share this as a testament to my learning experience, hoping it might inform and inspire fellow Rust learners.

ย