Manish Shivanandhan Machine learning, cybersecurity, & AWS professional manishshiva.com

Building a simple guessing game with Rust Rhai

7 min read 2008

Building a Game with Rust Rhai

Rhai is a simple embedded scripting language for Rust. It provides a safe and efficient way of adding scripts to your Rust applications. The syntax of Rhai is closely related to JavaScript, making it easier in terms of picking it up relatively quickly.

In this article, we are going to be taking a look at Rhai and build a very simple game using Rust and Rhai together.

If you are new to Rust, you can take a look at our archives here to find a wealth of information on all of the basics of Rust development that might be of interest to you as a starting place.

That being said, let’s proceed with the article and take a quick look at Rust before learning how Rhai works with it.

Jump ahead:

What is Rust?

Rust is an extremely popular language today. It has been consistently voted by developers as the most loved programming language for several years now. In spite of its steep learning curve, once you get to grips with it, Rust offers a lot of benefits over other languages such as security, maintainability, and scalability.

Many companies have been adopting Rust as part of their technology stack. Rust also has a strong community backing it, making it a great choice if you’re looking for a scalable and stable programming language for the long-term.

Now that you know what Rust is, let’s look at Rhai and what it can do for you.

Rhai

Rhai, also called RhaiScript, is a fast and embeddable scripting language. Its core purpose is to enable you to write scripts within your Rust applications.

The name “Rhai” comes from Chai, a scripting language for the C++ language. Similar to Rhai, Chai can be used to write scripts with C++. There are also other scripting languages like Lua, which can work with multiple languages, like C and Python.

A major advantage of using Rhai is its simple syntax. Unlike Rust, which has a relatively steep learning curve as I mentioned, Rhai is syntactically similar to JavaScript.

This makes Rhai very useful when building large-scale Rust applications. You can even write complicated algorithms in Rust using Rhai, making it an ideal choice when it comes to writing scripts for Rust.

Additionally, Rhai is memory safe compared to other scripting languages, like JavaScript. One of the core principles of Rust is memory safety, so Rhai is always run in a controlled environment — Rhai scripts cannot change any of the values in its environment and always runs in a sandbox.
Some of the other features that make Rhai very attractive to developers are:

  • Dynamic typing
  • Tight integration with native Rust functions and types
  • Supports passing Rust values into a script as variables or constants through Scope
  • Call a script-defined function from Rust
  • Minimal need for additional dependencies
  • Plugins and libraries are available to extend Rhai’s functionalities
  • Function and operator overloading
  • Availability of closures that can capture shared values
  • Debugging interface for fixing errors

Writing Scripts with Rhai

Now that you know what Rhai can do, let’s look at a few examples of Rhai scripts.



If you want to follow along, you can use the Online Playground to try some scripts yourself. To install and run Rhai on your system, you can follow the installation instructions here.

First, let’s write a hello world function using Rhai.

To use Rust with Rhai, you have to create an instance of the scripting engine. You can then write your Rhai script and execute it by calling the run() function.

use rhai::{Engine, EvalAltResult};

pub fn main() -> Result<(), Box<EvalAltResult>>
{
  let engine = Engine::new(); // create an instance of "Engine"
  let script = "print('Hello World');"; // one line Rhai script stored in a variable
  engine.run(script)?; // Run the script
  Ok(())
}

If you look at the above example, we invoke an instance of “Engine” to run the hello world script. But the best way to run Rhai scripts is to have your code in files and then call them from your Rust function.

Here is another example of the hello world code executed from a Rhai script file. First, write your Rhai code in a .rhai file.

// fiename: helloworld.rhai
print("Hello world");

Then, you can call your Rhai file from your Rust function using the run_file or eval_file methods.

// filename hello.rs
use rhai::{Engine, EvalAltResult};

pub fn main() -> Result<(), Box<EvalAltResult>>
{
  let engine = Engine::new(); // create an instance of "Engine"
  engine.run_file("helloworld.rhai".into())?; // run the rhai script
  Ok(())
}

Writing a Game with Rhai

Now let’s try a simple game using Rust and Rhai. We will write a guessing game that generates a random number between 1 and 100, and the user will keep guessing till they get it right.

We will use cargo to initiate a project. Cargo is a package manager for Rust, similar to npm for Node.js. It helps us to create new projects and manage dependencies in our project in addition to a few other useful functions.

(Note: Here is the complete source code of the project if you want to follow along)

$ cargo new guessing_game
     Created binary (application) `guessing_game` package
$ cd guessing_game
$ ls
Cargo.toml src

The Cargo.toml file contains the dependencies for your project. In this project, we will use two dependencies — Rand and Rhai.


More great articles from LogRocket:


We need the Rand module to generate a random number. And the Rhai module is used for invoking the Rhai engine to run our script.

Now that we have setup our project, we can start writing the code. Here is how our game logic will work:

  • Generate a number between 1 and 100
  • Get the guess as input from the user.=
  • If the number is greater than the guess, let the user know that and get a guess again
  • If the number is lesser than the guess, let the user know that and get a guess again
  • If the user guesses the correct number, tell them that they won and exit the function

In this guessing game, we will delegate the conditional flow to Rhai. We will get the input from the user and send the input along with the target number to Rhai to tell us if the guess is greater than, lesser than, or correct.

Before we start writing our script, let’s understand the concept of scopes. In Rust, you can create a scope and add values to it. This scope can then be sent to our Rhai script, which will have access to the values in the scope. This is important to understand since Rhai cannot directly change any values in our Rust program due to memory-safety constraints.

So, let’s write a Rhai script with a function that takes in one parameter, the guess. We will create a scope in Rust and add the original target number and send it to Rhai during runtime.

// filename: guess.rhai

fn guess(input) {
    if input == target{
        print(`Congrats! You Won!!!`);
        return true;
    }
    else if input > target{
        print(`INPUT TOO BIG`);
        return false;
    }
    else{
        print(`INPUT TOO SMALL`);
        return false;
    }
}

Now, let’s call this script using Rust. We will place the user input prompt inside a loop and use Rhai to check if they have guessed the correct number. We will send the user feedbacks until they guess the correct number.

In the previous example, we saw the engine.run_file function to run a script. A better approach is to compile your script first and then call the functions within your script. For Rhai, we use the AST compiler. AST compilers are out of scope for this article, so if you want to learn more, here is a great resource for you to take a look at.

// src/main.rs
use rhai::{Engine, Scope, EvalAltResult};
use rand::Rng;
use std::io;
pub fn main() -> Result<(), Box<EvalAltResult>>{

    let engine = Engine::new(); //invoke the RHAI engine
    let mut scope = Scope::new(); // create a new scope

    let target = rand::thread_rng().gen_range(1..=100); // generate a random number between 1 and 100
    scope.push("target", target); // adding the target number to scope for Rhai

    // use ast compiler to compile the script
    let ast = engine.compile_file("guess.rhai".into())?;

    println!("{}","Guess the number!");

    // loop till the user guesses the correct number
    loop{
        let mut input = String::new(); // variable to get user input

        // get the input from user
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");

        // convert the input from string to number
        let guess: i32 = input
            .trim()
            .parse()
            .expect("Wanted a number");

        // call the guess function from the compiled script and pass scope along with the input number. Returns a boolean value. 
        let result = engine.call_fn::<bool>(&mut scope, &ast, "guess", ( guess,) )?;

        // if "true" is returned, break out of the loop
        if result {
            break
        };
    }

    Ok(())
}

You can see that we have invoked an instance of the Rhai engine, followed by creating a scope. This is followed by generating a random number between 1 and 100 and pushing it to the scope.

We then compile our Rhai script to be used for calling the “guess” function. Now, we can create a loop, get the input from the user and run the logic using the Rhai script.

The script returns true if the guess is correct. This is used to break out of the loop. We use the engine.call_fn method to pass the scope, function name, and the function parameters in a tuple.

Now, lets run our script. We can use “cargo run” command to build and run our Rust program.

$ cargo run

The cargo run command display

Congrats! You have written your first game using Rust and Rhai. You can also use Rhai for implementing more complex scripts and algorithms that may be harder to write in Rust.

Limitations of Rhai

So far we have seen a lot about Rhai and writing scripts using Rhai. Now, let’s look at some of the downsides of Rhai.

  • Limited scripting capabilities: Rhai does not support classes or other complex data structures, so Rhai is best for writing utility scripts and not full-scale applications
  • No garbage collection: Like Rust, Rhai doesn’t have garbage collection
  • No formal language grammar: Unlike scripting languages like JavaScript or Lua, Rhai lacks formal language grammar. This limits Rhai’s capabilities in terms of object-oriented syntax like using inheritance, interfaces, or generics

Alternatives to Rhai

Rhai is not the only scripting language for Rust. Let’s look at a few other options you may wish to consider.

GameLisp

Gamelisp is a great alternative to Rhai if you are looking for a scripting engine just for writing games. Gamelisp integrates with Rust easily and has more features than Rhai for writing games. This includes garbage collection, memory safety, and seamless integration with the Rust api.

Throne

Throne is another useful game scripting language for Rust. It offers rapid prototyping and story logic features, making it a great choice when writing games in Rust. Throne is relatively unpopular compared to Rhai, but is steadily gaining a developer community.

Dyon

Dyon is another dynamically typed scripting language for Rust. Dyon is packed with features including 4D vectors, macros, and dynamic modules. It is a great choice if you are looking to write simple games and interactive programming environments.

Conclusion

Rhai is a fantastic scripting language for Rust. It offers a simple scripting experience for developers when working with an intricate language like Rust. From writing complex algorithms to simple games, Rhai is a great tool to work with.

Rhai also has it challenges like any scripting language. It is designed to work with Rust, so it does not offer the full capabilities you may have come to expect from working with languages like JavaScript. That being said, Rhai is an excellent choice of scripting language when building applications with Rust.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Manish Shivanandhan Machine learning, cybersecurity, & AWS professional manishshiva.com

Leave a Reply