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
| Secret | 1 | 2 | 3 | 4 |
| Guess | 5 | 6 | 7 | 8 |
| Output | ⬜ | ⬜ | ⬜ | ⬜ |
Example 2 - one right
| Secret | 1 | 2 | 3 | 4 |
| Guess | 1 | 5 | 6 | 7 |
| Output | 🟩 | ⬜ | ⬜ | ⬜ |
Example 3 - right digit, wrong place
| Secret | 1 | 2 | 3 | 4 |
| Guess | 5 | 6 | 1 | 7 |
| 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:
| Secret | 1 | 2 | 3 | 4 |
| Guess | 5 | 6 | 4 | 4 |
| 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
- Create a new binary crate called
green-yellow - Copy all the test cases into the
main.rs - Define a constant
NUM_DIGITS: usizewith the value4 - 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 insecret(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
guessshould get a grey block ('⬜') as its output - Note: The output should only contain
'⬜','🟨', or'🟩'characters
- For each digit, if
- Ensure all the test cases pass! (see below)
- Write a
mainfunction 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)
- 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
continuethe game loop. - If the digit parses but it is out of range, print an error and
continuethe game loop. - If you get the wrong number of digits, print an error and
continuethe 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;
}
}