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".
After completing this exercise you will be able to
- Work with rust slices and vectors
- Accept input from stdin
- Iterate through arrays and slices
- Generate random numbers
Prerequisites
To complete this exercise you need to have:
- basic Rust programming skills
- the Rust Syntax Cheat Sheet
Task
- Create a new binary crate called
green-yellow
- Copy all the test cases into into your
main.rs
- Define a function
fn calc_green_and_yellow(guess: &[u8; 4], secret: &[u8; 4]) -> String
that implements the following rules:- Return a string containing four Unicode characters
- For every item in guess, if
guess[i] == secret[i]
, then positioni
in the output String should be a green block (π©
) - Then, for every item in guess, if
guess[i]
is insecret
somewhere, and hasn't already been matched, then positioni
in the output String should be a yellow block (π¨
) - If any of the guesses do not appear in the secret, then that position in the output String should be a grey block (
β¬
)
- Ensure all the test cases pass!
- 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 into a guess, containing four digits (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
- Play the game
If you need it, we have provided a complete solution for this exercise.
Your test cases are:
#![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 two_in_secret_one_in_guess() { assert_eq!( &calc_green_and_yellow(&[1, 2, 3, 4], &[3, 3, 9, 9]), "β¬β¬π¨β¬" ); } }
Knowledge
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.8
.
You need a random number generator (call rand::thread_rng()
), and using that you can generate a number out of a given range with gen_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::Write
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!
Step-by-Step-Solution
We also recommend using the official Rust documentation to figure out unfamiliar concepts. 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 the build and see if 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 grey blocks. We put them in an Vec
first, as that's easier to index than a string.
Call the function from main()
to avoid the warning about it being unused.
Solution
fn calc_green_and_yellow(_guess: &[u8; 4], _secret: &[u8; 4]) -> String {
let result = ["β¬"; 4];
result.join("")
}
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 output square to be green.
Solution
fn calc_green_and_yellow(guess: &[u8; 4], secret: &[u8; 4]) -> String {
let mut result = ["β¬"; 4];
for i in 0..guess.len() {
if guess[i] == secret[i] {
result[i] = "π©";
}
}
result.join("")
}
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 do this by copying the input, so we can make it mutable, and mark off any values used in the green-square-loop by setting them to zero.
Solution
fn calc_green_and_yellow(guess: &[u8; 4], secret: &[u8; 4]) -> String {
let mut result = ["β¬"; 4];
let mut secret_handled = [false; 4];
for i in 0..guess.len() {
if guess[i] == secret[i] {
// that's a match
result[i] = "π©";
// don't match this secret digit again
secret_handled[i] = true;
}
}
'guess: for g_idx in 0..guess.len() {
// only process guess digits we haven't already dealt with
if result[g_idx] == "π©" {
continue;
}
for s_idx in 0..secret.len() {
// only process secret digits we haven't already dealt with
if secret_handled[s_idx] {
continue;
}
Step 5: Get some random numbers
Add rand = "0.8"
to your Cargo.toml, and make a random number generator with rand::thread_rng()
(Random Number Generator). You will also have to use rand::Rng;
to bring the trait into scope.
(A built-in random number generator is proposed for the Standard Library but is still nightly only as of October 2024).
Call your_rng.gen_range()
in a loop.
Solution
// put a yellow block in for this guess
result[g_idx] = "π¨";
// never match this secret digit again
secret_handled[s_idx] = true;
// stop comparing this guessed digit to any other secret digits
continue 'guess;
}
}
}
Step 6: Make the game loop
We a loop to handle each guess the user makes.
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,
continue
the loop. - If the digit parses but it out of range,
continue
the loop. - If you get the wrong number of digits,
continue
the loop. - If the guess matches the secret, then break out of the loop and congratulate the winner.
- Otherwise run the guess through our calculation function and print the squares.
Solution
}
fn main() {
let mut rng = rand::thread_rng();
let stdin = std::io::stdin();
let mut secret = [0u8; 4];
for digit in secret.iter_mut() {
*digit = rng.gen_range(1..=9);
}
println!("New game (secret is {:?}!", secret);
loop {
let mut line = String::new();
println!("Enter guess:");
stdin.read_line(&mut line).unwrap();
let mut guess = [0u8; 4];
let mut idx = 0;
for piece in line.trim().split(' ') {
let Ok(digit) = piece.parse::<u8>() else {
println!("{:?} wasn't a number", piece);
continue;
};
if digit < 1 || digit > 9 {
println!("{} is out of range", digit);
continue;
}
if idx >= guess.len() {
println!("Too many numbers, I only want {}", guess.len());
continue;
}
guess[idx] = digit;