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, nor remotely similar to the 1970βs board-game "Mastermind".
After completing this exercise you are able to
- Work with rust slices and vectors
- Accept input from stdin
- Iterate through arrays and slices
- Generate random numbers
Prerequisites
For completing 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]), "π©β¬π¨π¨" ); } }
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 and do something better than unwrap!
Step-by-Step-Solution
In general, we also recommend to use the Rust documentation to figure out things you are missing to familiarize yourself with it. If you ever feel completely stuck or that you havenβt understood something, 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 fizzbuzz
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 guess = *guess;
let mut secret = *secret;
for i in 0..guess.len() {
if guess[i] == secret[i] {
result[i] = "π©";
secret[i] = 0;
guess[i] = 0;
}
}
for i in 0..guess.len() {
for j in 0..secret.len() {
if guess[i] == secret[j] && secret[j] != 0 && guess[i] != 0 {
result[i] = "π¨";
}
}
}
result.join("")
}
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.
Call your_rng.gen_range()
in a loop.
Solution
fn main() {
let mut rng = rand::thread_rng();
let mut secret = [0u8; 4];
for digit in secret.iter_mut() {
*digit = rng.gen_range(1..=9);
}
println!("{:?}", secret);
println!("{}", calc_green_and_yellow(&[1, 2, 3, 4], &secret));
}
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
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;
idx += 1;
}
if idx < guess.len() {
println!("Not enough numbers, I want {}", guess.len());
continue;
}
println!("Your guess is {:?}", guess);
if guess == secret {
println!("Well done!!");
break;
}
}