Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Green and Yellow Game

In this assignment we will implement the game “Green and Yellow”. It’s like Wordle, but with numerical digits instead of letters. But for legal reasons it’s also entirely unlike Wordle, and entirely unlike the 1970’s board-game “Mastermind”.

  • The computer picks some random digits
  • You provide your guess for each digit
  • The computer replies with sequence of grey, green and yellow blocks - one for each digit you guess
    • If you guessed a digit in the right place, you get a green block for that guess
    • If you guessed a digit in the wrong place, you get a yellow block for that guess
    • Otherwise you get a grey block and you know the digit you guessed is not in the output

Here are some examples:

Example 1 - total failure

Secret1234
Guess5678
Output

Example 2 - one right

Secret1234
Guess1567
Output🟩

Example 3 - right digit, wrong place

Secret1234
Guess5617
Output🟨

Example 4 - duplicate digits

Of course, it gets tricky when you have repeated digits! You cannot match a any particular secret digit twice, so:

Secret1234
Guess5644
Output🟩

The third output is grey because although the guess of 4 is in the secret, you matched the only 4 in the secret with the fourth digit in the guess (and you got a green block for it).

After completing this exercise you will be able to

  • Write simple functions
  • Accept input from Standard In
  • Iterate through arrays with for loops
  • Generate random numbers

Prerequisites

To complete this exercise you need to have:

  • basic Rust programming skills
  • a computer you can execute Rust code on, interactively

Task

  1. Create a new binary crate called green-yellow
  2. Copy all the test cases into the main.rs
  3. Define a constant NUM_DIGITS: usize with the value 4
  4. Define a function fn calc_green_and_yellow(guess: &[u8; NUM_DIGITS], secret: &[u8; NUM_DIGITS]) -> [char; NUM_DIGITS] that implements the following rules:
    • For each digit, if guess[i] == secret[i], then the matching place in output should be a green block ('🟩')
    • Then, for each digit, if guess[i] matches any other digit in secret (and that secret digit hasn’t already been matched against something else) then the matching place in the output should be a yellow block ('🟨')
    • Any unmatched digit in guess should get a grey block ('⬜') as its output
    • Note: The output should only contain '⬜', '🟨', or '🟩' characters
  5. Ensure all the test cases pass! (see below)
  6. Write a main function that implements the following:
    • Generate 4 random digits - our ‘secret’
    • Go into a loop
    • Read a string from Standard In and trim the whitespace off it
    • Parse that string digit by digit into a [u8; NUM_DIGITS] (and give an error if the user makes a mistake)
    • Run the calculation routine above and print the coloured blocks
    • Exit if all the blocks are green (or if guess == secret)
  7. Play the game!

If you need it, we have provided a complete solution for this exercise.

Here are some test cases to check your algorithm (scroll past them to get to the hints section, if you need a hint):

#![allow(unused)]
fn main() {
#[test]
fn all_wrong() {
    assert_eq!(
        calc_green_and_yellow(&[5, 6, 7, 8], &[1, 2, 3, 4]),
        ['⬜', '⬜', '⬜', '⬜']
    );
}

#[test]
fn all_green() {
    assert_eq!(
        calc_green_and_yellow(&[1, 2, 3, 4], &[1, 2, 3, 4]),
        ['🟩', '🟩', '🟩', '🟩']
    );
}

#[test]
fn one_wrong() {
    assert_eq!(
        calc_green_and_yellow(&[1, 2, 3, 5], &[1, 2, 3, 4]),
        ['🟩', '🟩', '🟩', '⬜']
    );
}

#[test]
fn all_yellow() {
    assert_eq!(
        calc_green_and_yellow(&[4, 3, 2, 1], &[1, 2, 3, 4]),
        ['🟨', '🟨', '🟨', '🟨']
    );
}

#[test]
fn one_wrong_but_duplicate() {
    assert_eq!(
        calc_green_and_yellow(&[1, 2, 3, 1], &[1, 2, 3, 4]),
        ['🟩', '🟩', '🟩', '⬜']
    );
}

#[test]
fn one_right_others_duplicate() {
    assert_eq!(
        calc_green_and_yellow(&[1, 1, 1, 1], &[1, 2, 3, 4]),
        ['🟩', '⬜', '⬜', '⬜']
    );
}

#[test]
fn two_right_two_swapped() {
    assert_eq!(
        calc_green_and_yellow(&[1, 2, 2, 2], &[2, 2, 2, 1]),
        ['🟨', '🟩', '🟩', '🟨']
    );
}

#[test]
fn two_wrong_two_swapped() {
    assert_eq!(
        calc_green_and_yellow(&[1, 3, 3, 2], &[2, 2, 2, 1]),
        ['🟨', '⬜', '⬜', '🟨']
    );
}

#[test]
fn a_bit_of_everything() {
    assert_eq!(
        calc_green_and_yellow(&[1, 9, 4, 3], &[1, 2, 3, 4]),
        ['🟩', '⬜', '🟨', '🟨']
    );
}

#[test]
fn two_in_guess_one_in_secret() {
    assert_eq!(
        calc_green_and_yellow(&[1, 2, 3, 3], &[3, 9, 9, 9]),
        ['⬜', '⬜', '🟨', '⬜']
    );
}

#[test]
fn four_in_guess_one_in_secret() {
    assert_eq!(
        calc_green_and_yellow(&[1, 1, 1, 1], &[4, 3, 1, 2]),
        ['⬜', '⬜', '🟩', '⬜']
    );
}

#[test]
fn one_in_guess_two_in_secret() {
    assert_eq!(
        calc_green_and_yellow(&[1, 2, 3, 4], &[3, 3, 9, 9]),
        ['⬜', '⬜', '🟨', '⬜']
    );
}
}

Hints

Generating Random Numbers

There are no random number generators in the standard library - you have to use the rand crate.

You will need to change Cargo.toml to depend on the rand crate - we suggest version 0.10.

You need to pick a random number from a range (try rand::random_range()). See https://docs.rs/rand for more details.

Reading from the Console

You need to grab a standard input handle with std::io::stdin(). This implements the std::io::Read trait, so you can call read_to_string(&mut some_string) and get a line of text into your some_string: String variable.

Parsing Strings into Integers

Strings have a parse() method, which returns a Result, because of course the user may not have typed in a proper digit. The parse() function works out what you are trying to create based on context - so if you want a u8, try let x: u8 = my_str.parse().unwrap(). Or you can say let x = my_str.parse::<u8>().unwrap(). Of course, try to do something better than unwrap because it seems rude to crash the game if the player has a mistake with their input.

Step-by-Step-Solution

If you aren’t sure how to proceed, try this step by step guide.

If you ever feel completely stuck, or if you haven’t understood something specific, please hail the trainers quickly.

Step 1: New Project

Create a new binary Cargo project, check it runs.

Solution
cargo new green-yellow
cd green-yellow
cargo run

Step 2: Generate some squares

Get calc_green_and_yellow to just generate an array of four grey blocks.

Call the function from main() to avoid the warning about it being unused.

Solution
const NUM_DIGITS: usize = 4;

fn calc_green_and_yellow(
    _guess: &[u8; NUM_DIGITS],
    _secret: &[u8; NUM_DIGITS],
) -> [char; NUM_DIGITS] {
    let result = ['⬜'; NUM_DIGITS];

    result
}

Step 3: Check for green squares

You need to go through every pair of items in the input arrays and check if they are the same. If so, set the matching output square to be green.

Solution
const NUM_DIGITS: usize = 4;

fn calc_green_and_yellow(
    guess: &[u8; NUM_DIGITS],
    secret: &[u8; NUM_DIGITS],
) -> [char; NUM_DIGITS] {
    let mut result = ['⬜'; NUM_DIGITS];

    for i in 0..NUM_DIGITS {
        if guess[i] == secret[i] {
            // that's a match
            result[i] = '🟩';
        }
    }

    result
}

Step 4: Check for yellow squares

This gets a little more tricky.

We need to loop through every item in the guess array and compare it to every item in the secret array. But! We must make sure we ignore any values we already ‘used up’ when we produced the green squares.

Let’s make an array to record which secret digits have been used.

If you wanted, you could instead create a mutable copy of the secret array and set its digits to something invalid (like 0 or 255) once they’ve been used.

Solution
const NUM_DIGITS: usize = 4;

fn calc_green_and_yellow(
    guess: &[u8; NUM_DIGITS],
    secret: &[u8; NUM_DIGITS],
) -> [char; NUM_DIGITS] {
    let mut result = ['⬜'; NUM_DIGITS];
    let mut secret_used = [false; NUM_DIGITS];

    for i in 0..NUM_DIGITS {
        if guess[i] == secret[i] {
            // that's a match
            result[i] = '🟩';
            // don't match this secret digit again
            secret_used[i] = true;
        }
    }

    for index_g in 0..NUM_DIGITS {
        // only process guess digits that weren't a perfect match
        if result[index_g] != '🟩' {
            for index_s in 0..NUM_DIGITS {
                // does the guess digit match that secret digit (and is that secret digit unused so far?)
                if (guess[index_g] == secret[index_s]) && !secret_used[index_s] {
                    // this is a correct digit but in the wrong place
                    result[index_g] = '🟨';
                    // don't match this secret digit again
                    secret_used[index_s] = true;
                    // move to next guess digit now
                    break;
                }
            }
        }
    }

    result
}

Step 5: Get some random numbers

Add rand = "0.10" to your Cargo.toml, and use the rand::random_range() function to create each digit, passing in a range (like 0..5). What is the right range for each digit?

You’ll want to create a secret array of u8 values and then fill in each digit one by one.

🔎 A built-in random number generator is proposed for the Standard Library but is still nightly-only as of October 2024.

Solution
    let mut secret = [0u8; NUM_DIGITS];
    for digit in secret.iter_mut() {
        *digit = rand::random_range(1..=9);
    }

Step 6: Make the game loop

We need a loop to handle each guess the user makes and report the outcome of the guess.

For each guess we need to read from Standard Input (using std::io::stdin() and its read_line()) method.

You will need to trim and then split the input, then parse each piece into a digit.

  • If the digit doesn’t parse, print an error and continue the game loop.
  • If the digit parses but it is out of range, print an error and continue the game loop.
  • If you get the wrong number of digits, print an error and continue the game loop.
  • Run the guess through our calculation function and print the squares.
  • If the guess matches the secret, then break out of the loop and congratulate the winner.
Solution

    println!("New game!");

    let mut secret = [0u8; NUM_DIGITS];
    for digit in secret.iter_mut() {
        *digit = rand::random_range(1..=9);
    }

    'guess_loop: loop {
        let mut line = String::new();
        println!("Enter guess:");
        stdin.read_line(&mut line).unwrap();
        let mut guess = [0u8; NUM_DIGITS];
        let mut idx = 0;
        for piece in line.trim().split(' ') {
            let Ok(digit) = piece.parse::<u8>() else {
                println!("{:?} wasn't a number", piece);
                continue 'guess_loop;
            };
            if digit < 1 || digit > 9 {
                println!("{} is out of range", digit);
                continue 'guess_loop;
            }
            if let Some(slot) = guess.get_mut(idx) {
                *slot = digit;
            } else {
                println!("Too many numbers, I only want 4!");
                continue 'guess_loop;
            }
            idx += 1;
        }
        if idx < guess.len() {
            println!("Not enough numbers, I want {}", guess.len());
            continue 'guess_loop;
        }

        println!("Your guess is {:?}", guess);

        let score = calc_green_and_yellow(&guess, &secret);

        let nice_string: String = score.iter().collect();
        println!("That gives: {}", nice_string);

        if guess == secret {
            println!("Well done!!");
            break;
        }
    }