I'm starting out with Rust and decided to implement a game of Hangman. Can you please provide general feedback on the code?
I've already identified some lines (see comments [A], [B], [C]) where I cannot find a better way; feedback regarding these would be much appreciated as well.
use rand::seq::SliceRandom;
use std::io::stdin;
const MAX_ALLOWED_ERRORS: i32 = 7;
fn main() {
println!("\nWelcome to Hangman!\n");
let to_guess = generate_word();
let mut display = str::repeat("_ ", to_guess.len());
println!("{}", display);
let mut nb_errors = 0;
loop {
let mut user_input = String::new();
stdin().read_line(&mut user_input).expect("Could not read user input");
match extract_and_capitalize_first_letter(&user_input) {
Ok(user_guess) => {
println!("You guessed: {}", user_guess);
let (is_error, is_full, new_display) = compute_displayed_word(
&to_guess, &Some(display), &user_guess,
);
// [A] Is there a way to avoid this? Ideally, I would like
// to use the same variable for storing the result as
// the one passed as the argument
display = new_display;
if is_error {
nb_errors += 1;
}
if is_full {
println!("You win!");
break;
}
}
Err(_) => {
println!("Could not process your input");
nb_errors += 1;
}
}
println!("Error counter: {}", nb_errors); // maybe later print a real hangman
if nb_errors > MAX_ALLOWED_ERRORS {
println!("You lose! The word was: {}", to_guess);
break;
}
println!("{}", display);
}
}
// return the string to display along with 2 booleans:
// 1. indicating if an error should be counted
// 2. indicating if the word is fully guessed
fn compute_displayed_word(
word: &String,
current_display: &Option<String>,
guess: &char,
) -> (bool, bool, String) {
let mut is_error = true;
let mut is_full = true;
// [B] Should I work with a Vec<char>, or is a mutable String ok?
let mut new_display = "".to_string();
for (i, letter) in word.chars().enumerate() {
if letter.to_ascii_uppercase().eq(guess) {
is_error = false;
new_display.push(*guess);
} else {
let letter_to_display = match current_display {
// [C] I couldn't find a way to not do the .chars().collect()
// at each iteration, apparently this is due to the
// absence of Copy implementation in Vec<char>
Some(d) => d.chars().collect::<Vec<char>>()[i * 2],
_ => '_'
};
if letter_to_display.eq(&'_') {
is_full = false;
}
new_display.push(letter_to_display);
}
new_display.push(' ');
}
(is_error, is_full, new_display)
}
fn generate_word() -> String {
let all_words = vec!["test", "cat", "dog", "controller", "operation", "jazz"];
let chosen_word = all_words.choose(&mut rand::thread_rng());
match chosen_word {
Some(&s) => {
return s.to_string();
}
None => panic!("Could not choose a word!")
}
}
fn extract_and_capitalize_first_letter(s: &String) -> Result<char, String> {
return match s.chars().nth(0) {
Some(c) => {
if !c.is_ascii_alphabetic() {
return Err("Only alphabetic characters allowed".to_string());
}
Ok(c.to_ascii_uppercase())
}
None => Err("Empty string".to_string()),
};
}