Hi, I'm reading the rust book and decided I need some practice. I'm pretty sure this is just horrible, so I'd very much appreciate any pointers to what can be improved. Thanks!
// TODO: Handle multiple identical letters correctly.
use colored::Colorize;
use rand::seq::IteratorRandom;
use std::{
collections::HashSet,
fs::File,
io::{self, BufRead, BufReader, Write},
};
const WORD_LENGTH: usize = 5;
const MAX_ATTEMPTS: u32 = 5;
// All ~2300 possible solutions, one word per line
const SOLUTION_FILE: &str = "./word-dict.txt";
// All ~13k possible guesses, one word per line
const GUESS_FILE: &str = "./guess-dict.txt";
#[derive(Debug)]
enum CharGuessResult {
Correct(char),
Occuring(char),
NotOccuring,
}
#[derive(Debug)]
enum GuessResult {
Correct,
Incorrect(Vec<CharGuessResult>),
}
fn main() {
let word = &pick_word();
// Having 13k * 5 chars in memory sounds better than combing through a file for each guess...
// right?
let allowed_guess_words = read_guess_dict();
println!();
println!("Welcome to Garble!");
println!("==================");
println!();
game_loop(word, &allowed_guess_words)
}
fn game_loop(word: &str, allow_list: &HashSet<String>) {
let mut attempt = 1;
while attempt <= MAX_ATTEMPTS {
let guess = read_user_guess(attempt, MAX_ATTEMPTS);
if let Err(msg) = validate_guess(WORD_LENGTH, &guess, allow_list) {
println!("{}", msg);
continue;
}
let result = evaluate_guess(&guess, word);
println!(
"{}",
stringify_guess_result(&result, word, attempt, MAX_ATTEMPTS)
);
if matches!(result, GuessResult::Correct) {
return;
}
attempt += 1;
}
}
fn pick_word() -> String {
let f = File::open(SOLUTION_FILE)
.unwrap_or_else(|e| panic!("(;_;) file not found: {}: {}", SOLUTION_FILE, e));
let f = BufReader::new(f);
let lines = f.lines().map(|l| l.expect("Couldn't read line"));
lines
.choose(&mut rand::thread_rng())
.expect("File has no lines?!")
.to_uppercase()
}
fn read_guess_dict() -> HashSet<String> {
std::fs::read_to_string(GUESS_FILE)
.unwrap_or_else(|_| panic!("file not found: {}", GUESS_FILE))
.lines()
.map(|x| x.to_uppercase())
.collect::<HashSet<String>>()
}
fn validate_guess<'a>(
word_length: usize,
guess: &'a str,
allow_list: &HashSet<String>,
) -> Result<&'a str, String> {
// println!(
// "guess: '{}', word_length: {}, guess.len: {}",
// guess,
// word_length,
// guess.len()
// );
let is_correct_length = guess.len() == word_length;
let is_in_list = allow_list.contains(guess);
if !is_correct_length {
return Result::Err(format!("Your guess must be {} letters long.", word_length));
}
if !is_in_list {
return Result::Err("Your guess must be an English word.".to_string());
}
Result::Ok(guess)
}
fn read_user_guess(attempt: u32, max_attempts: u32) -> String {
let mut guess = String::new();
print!(
"{}{}{} {} ",
attempt.to_string().blue(),
"/".blue(),
max_attempts.to_string().blue(),
">?".blue()
);
io::stdout()
.flush()
.expect("When flushing fails, you're in deep shit.");
io::stdin()
.read_line(&mut guess)
.expect("I don't know what you did, but I can't handle it.");
guess.trim().to_uppercase()
}
// Cannot change case and get a char back so we get a string instead.
fn stringify_char_result(result: &CharGuessResult) -> String {
match result {
CharGuessResult::Correct(c) => c.to_string().to_uppercase().green().to_string(),
CharGuessResult::Occuring(c) => c.to_string().to_lowercase().yellow().to_string(),
CharGuessResult::NotOccuring => String::from('_'),
}
}
fn stringify_guess_result(
result: &GuessResult,
word: &str,
attempts: u32,
max_attempts: u32,
) -> String {
match result {
GuessResult::Correct => format!("Correct! You did it in {attempts} attempts."),
GuessResult::Incorrect(chars) if attempts < max_attempts => chars
.iter()
.map(stringify_char_result)
.collect::<Vec<String>>()
.join(" ")
.to_string(),
_ => format!("The correct word was {word}, better luck next time."),
}
}
fn evaluate_guess(guess: &str, word: &str) -> GuessResult {
let zipped = guess.chars().zip(word.chars());
// apparently iter.all below consumes the iter and that's a mutation.
// So I create a Vec instead and then an iter from that in the next line.
let char_results: Vec<CharGuessResult> =
zipped.map(|(a, b)| evaluate_char(a, b, word)).collect();
let is_correct = char_results
.iter()
.all(|result| matches!(result, CharGuessResult::Correct(_)));
if is_correct {
GuessResult::Correct
} else {
GuessResult::Incorrect(char_results)
}
}
fn evaluate_char(guess_char: char, word_char: char, word: &str) -> CharGuessResult {
if guess_char == word_char {
CharGuessResult::Correct(guess_char)
} else if word.contains(guess_char) {
CharGuessResult::Occuring(guess_char)
} else {
CharGuessResult::NotOccuring
}
}
Output: