Programming a Guessing Game

Let's jump into Oxide by working through a hands-on project together! This chapter introduces several common Oxide concepts by showing you how to use them in a real program. You'll learn about var, input/output, string interpolation, match expressions, and more!

The project is a classic beginner programming problem: we'll implement a guessing game. Here's how it works: the program generates a random integer between 1 and 100, then prompts you to enter a guess. After you enter a guess, the program indicates whether the guess is too low or too high. If your guess is correct, the game prints a congratulatory message and exits.

Setting Up a New Project

Let's set up a new Oxide project using Cargo with the --oxide flag:

$ cargo new --oxide guessing_game
$ cd guessing_game

This creates a new Oxide project with src/main.ox ready to go.

Development Status: Oxide v1.0 is currently in development. The --oxide flag will be available in Cargo once the Oxide toolchain is released. For early testing before the release, you can manually create a project with cargo new and rename src/main.rs to src/main.ox.

Now let's look at what Cargo generated. Open Cargo.toml:

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

[dependencies]

And here's the initial src/main.ox:

fn main() {
    println!("Hello, world!")
}

Let's test it with cargo run:

$ cargo run
   Compiling guessing_game v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/guessing_game`
Hello, world!

Great! The cargo run command compiles and runs the project in one step. Now let's make it into a guessing game.

Processing a Guess

The first part of the guessing game program will ask for user input, process that input, and check that the input is in the expected form. To start, we'll allow the player to input a guess.

Filename: src/main.ox

import std.io

fn main() {
    println!("Guess the number!")

    println!("Please input your guess.")

    var guess = String.new()

    io.stdin()
        .readLine(&mut guess)
        .expect("Failed to read line")

    println!("You guessed: \(guess)")
}

This code contains a lot of information, so let's go through it line by line.

Getting User Input

To obtain user input and then print the result, we need the io library from the standard library:

import std.io

In Oxide, we use import with dot notation. This replaces Rust's use std::io; syntax. Note that :: does not exist in Oxide - dot notation is the only path separator.

The main function is the entry point:

fn main() {

Next, we use println! to print a prompt:

println!("Guess the number!")
println!("Please input your guess.")

Storing Values with Variables

Now we'll create a variable to store the user input:

var guess = String.new()

In Oxide, we use var to create a mutable variable. This is equivalent to Rust's let mut. The variable guess is bound to a new, empty String. In Oxide, String is the same type as in Rust—a growable, UTF-8 encoded text type.

Receiving User Input

Next, we call readLine on the standard input handle:

io.stdin()
    .readLine(&mut guess)

The stdin function returns an instance of std.io.Stdin, a type representing a handle to the standard input. The .readLine(&mut guess) method reads user input into the string we pass to it.

We pass &mut guess as an argument. The & indicates this is a reference, which gives you a way to let multiple parts of your code access one piece of data without copying it into memory multiple times. References are immutable by default, so we need &mut to make it mutable.

Handling Potential Failure with Result

We're still working on this line:

    .expect("Failed to read line")

The readLine method returns a Result value. Result is an enum with variants Ok and Err. The Ok variant indicates the operation was successful, and inside Ok is the successfully generated value. The Err variant means the operation failed, and contains information about how or why the operation failed.

The expect method will cause the program to crash and display the message you passed to it if the Result is an Err value. If you don't call expect, the program will compile with a warning.

Printing Values with String Interpolation

Finally, we print the guess:

println!("You guessed: \(guess)")

Oxide supports string interpolation using \(expression) syntax. This is one of Oxide's ergonomic features that makes string formatting more concise. It's equivalent to Rust's format! macro.

Let's test this code:

$ cargo run
   Compiling guessing_game v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 2.34s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

Great! The first part is working.

Generating a Secret Number

Next, we need to generate a secret number that the user will try to guess. The secret number should be different every time so the game is fun to play more than once. Let's use a random number between 1 and 100.

Rust's standard library doesn't include random number functionality, so we'll use the rand crate. Add it to Cargo.toml:

Filename: Cargo.toml

[dependencies]
rand = "0.8.5"

Now update src/main.ox:

Filename: src/main.ox

import std.io
import rand.Rng

fn main() {
    println!("Guess the number!")

    let secretNumber = rand.thread_rng().gen_range(1..=100)

    println!("The secret number is: \(secretNumber)")

    println!("Please input your guess.")

    var guess = String.new()

    io.stdin()
        .readLine(&mut guess)
        .expect("Failed to read line")

    println!("You guessed: \(guess)")
}

We add import rand.Rng. The Rng trait defines methods that random number generators implement.

We call rand.thread_rng() to get a random number generator, then call gen_range with the range 1..=100 (inclusive on both ends).

Note that we use let (not var) for secretNumber because we won't be changing it.

Let's run it:

$ cargo run
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 3.45s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 42
Please input your guess.
25
You guessed: 25

You should see a different random number, and it should change each time you run the program.

Comparing the Guess to the Secret Number

Now that we have user input and a random number, we can compare them:

Filename: src/main.ox

import std.io
import std.cmp.Ordering
import rand.Rng

fn main() {
    println!("Guess the number!")

    let secretNumber = rand.thread_rng().gen_range(1..=100)

    println!("The secret number is: \(secretNumber)")

    println!("Please input your guess.")

    var guess = String.new()

    io.stdin()
        .readLine(&mut guess)
        .expect("Failed to read line")

    let guess: Int = guess.trim().parse().expect("Please type a number!")

    println!("You guessed: \(guess)")

    match guess.cmp(&secretNumber) {
        Ordering.Less -> println!("Too small!"),
        Ordering.Greater -> println!("Too big!"),
        Ordering.Equal -> println!("You win!"),
    }
}

We import Ordering, which is an enum with variants Less, Greater, and Equal.

We convert the guess string to a number:

let guess: Int = guess.trim().parse().expect("Please type a number!")

We create a new variable also named guess. Oxide, like Rust, allows us to shadow the previous value. The trim method removes whitespace, and parse converts the string to a number. We specify the type as Int (Oxide's alias for i32).

Then we use a match expression to compare the guess to the secret number:

match guess.cmp(&secretNumber) {
    Ordering.Less -> println!("Too small!"),
    Ordering.Greater -> println!("Too big!"),
    Ordering.Equal -> println!("You win!"),
}

In Oxide, we use -> for match arms (Rust's => is invalid in Oxide), and dot notation for enum variants (Ordering.Less). Rust's double colon syntax (Ordering::Less) does not exist in Oxide - it will cause a syntax error.

Let's try it:

$ cargo run
   Compiling guessing_game v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!

Nice! But we can only make one guess. Let's fix that with a loop.

Allowing Multiple Guesses with Looping

The loop keyword creates an infinite loop:

Filename: src/main.ox

import std.io
import std.cmp.Ordering
import rand.Rng

fn main() {
    println!("Guess the number!")

    let secretNumber = rand.thread_rng().gen_range(1..=100)

    loop {
        println!("Please input your guess.")

        var guess = String.new()

        io.stdin()
            .readLine(&mut guess)
            .expect("Failed to read line")

        let guess: Int = guess.trim().parse().expect("Please type a number!")

        println!("You guessed: \(guess)")

        match guess.cmp(&secretNumber) {
            Ordering.Less -> println!("Too small!"),
            Ordering.Greater -> println!("Too big!"),
            Ordering.Equal -> {
                println!("You win!")
                break
            },
        }
    }
}

We've moved everything after the secret number generation into a loop. When the guess equals the secret number, we print "You win!" and break to exit the loop.

$ cargo run
   Compiling guessing_game v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.45s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
50
You guessed: 50
Too small!
Please input your guess.
75
You guessed: 75
Too big!
Please input your guess.
62
You guessed: 62
You win!

Excellent! Now it loops until you guess correctly.

Handling Invalid Input

Let's make the game more robust by handling invalid input gracefully:

Filename: src/main.ox

import std.io
import std.cmp.Ordering
import rand.Rng

fn main() {
    println!("Guess the number!")

    let secretNumber = rand.thread_rng().gen_range(1..=100)

    loop {
        println!("Please input your guess.")

        var guess = String.new()

        io.stdin()
            .readLine(&mut guess)
            .expect("Failed to read line")

        let guess: Int = match guess.trim().parse() {
            Ok(num) -> num,
            Err(_) -> {
                println!("Please type a number!")
                continue
            },
        }

        println!("You guessed: \(guess)")

        match guess.cmp(&secretNumber) {
            Ordering.Less -> println!("Too small!"),
            Ordering.Greater -> println!("Too big!"),
            Ordering.Equal -> {
                println!("You win!")
                break
            },
        }
    }
}

Instead of crashing with expect, we now use a match expression to handle the Result from parse. If parse returns Ok, we extract the number. If it returns Err, we print a message and continue to the next iteration of the loop.

Let's test it:

$ cargo run
   Compiling guessing_game v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
abc
Please type a number!
Please input your guess.
50
You guessed: 50
Too small!
Please input your guess.
62
You guessed: 62
You win!

Perfect! Now let's remove the debug line that prints the secret number:

Filename: src/main.ox

import std.io
import std.cmp.Ordering
import rand.Rng

fn main() {
    println!("Guess the number!")

    let secretNumber = rand.thread_rng().gen_range(1..=100)

    loop {
        println!("Please input your guess.")

        var guess = String.new()

        io.stdin()
            .readLine(&mut guess)
            .expect("Failed to read line")

        let guess: Int = match guess.trim().parse() {
            Ok(num) -> num,
            Err(_) -> {
                println!("Please type a number!")
                continue
            },
        }

        println!("You guessed: \(guess)")

        match guess.cmp(&secretNumber) {
            Ordering.Less -> println!("Too small!"),
            Ordering.Greater -> println!("Too big!"),
            Ordering.Equal -> {
                println!("You win!")
                break
            },
        }
    }
}

Summary

This project was a hands-on way to introduce you to many new Oxide concepts: var, match, functions, external crates, and more. In the next chapters, you'll learn about these concepts in more detail.

This project demonstrated Oxide's practical syntax while building on Rust's excellent type system and error handling. The guessing game uses the same robust foundation as any Rust program—ownership, borrowing, and the type system—but with syntax that may feel more familiar if you're coming from languages like Swift, Kotlin, or TypeScript.

In Chapter 3, you'll learn about concepts that most programming languages have, such as variables, data types, and functions, and see how they work in Oxide.