Решение на Hangman от Георги Божинов

Обратно към всички решения

Към профила на Георги Божинов

Резултати

  • 11 точки от тестове
  • 0 бонус точки
  • 11 точки общо
  • 11 успешни тест(а)
  • 4 неуспешни тест(а)

Код

//! An implementation of a hangman game.
use std::fmt;
use std::str::FromStr;
use std::collections::HashSet;
/// The different types of errors our playing can cause.
#[derive(Debug)]
pub enum GameError {
/// Error when an invalid command is entered
ParseError(String),
/// Error when a non-alphabetic character is submitted as a guess
BadGuess(String),
/// Error when an empty solution is provided, or one with non-alphabetic characters
InvalidSolution(String),
/// Error when the game is over
GameOver,
}
impl fmt::Display for GameError {
/// Display a visualization for each of the error types
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GameError::ParseError(ref command) => {
write!(f, "Cannot parse {}! No such command!", command)
}
GameError::BadGuess(ref letter) => write!(f, "{} is not correct!! Git gud.", letter),
GameError::InvalidSolution(ref string) => {
write!(
f,
"{} is not a valid word! It contains invalid symbols or is empty!",
string
)
}
GameError::GameOver => write!(f, "You got gud! Or you lost."),
}
}
}
/// Commands that the player's input is transformed into
#[derive(Debug)]
pub enum Command {
TryLetter(char),
TryWord(String),
Info,
Help,
Quit,
}
fn parse(s: &str) -> Option<Command> {
match s.to_lowercase().trim().chars().next() {
Some('h') => return Some(Command::Help),
Some('i') => return Some(Command::Info),
Some('q') => return Some(Command::Quit),
_ => {
let split = s.split_whitespace().collect::<Vec<&str>>();
if split.iter().count() != 3 {
return None;
}
let second = split.get(1).unwrap();
let third = split.get(2).unwrap();
if second.chars().next() == Some('l') && third.len() != 1 {
return None;
}
let mut letters = split.iter().map(|s| s.chars().next().unwrap());
let first = letters.next().unwrap();
if first != 't' {
return None;
}
let second = letters.next().unwrap();
if second != 'l' && second != 'w' {
return None;
}
let last = letters.next().unwrap();
if second == 'l' {
return Some(Command::TryLetter(last));
} else {
return Some(Command::TryWord(last.to_string()));
}

Решението започва добре, но бързичко се усложнява и събира бъгове :). Като за начало, метода len не брои char-ове, а байтове: https://doc.rust-lang.org/std/primitive.str.html#method.len. Това чупи тестовете с кирилица.

После, имаш и проблем с разликата между try letter и try word -- променливата last e просто един символ и в двата случая, което значи, че try word foo се превежда до TryWord("f"), което съвсем не е правилно.

Още, проверката за != 3 не е съвсем точна -- казахме, че допълнителни неща в командите просто се игнорират.

Трудничко ми е да проследя логиката и да намеря останалите проблеми, донякъде защото имаш доста unwrap-ове. Така не е лесно да видиш какви са ти потенциалните грешки, които трябва да handle-ваш. За да ти е по-лесно, съветвам те да пробваш да се отървеш от unwrap-ове напълно, и да работиш повече с "безопасните" методи на Rust. Огледай останалите решения за някакво вдъхновение. (Примерно, моето: https://fmi.rust-lang.bg/tasks/4/solutions/43).

}
}
}
impl FromStr for Command {
type Err = GameError;
/// Transforms the string as input into a `Command` instance
/// Returns a ParseError in case of an invalid command
fn from_str(s: &str) -> Result<Self, Self::Err> {
match parse(s) {
Some(cmd) => Ok(cmd),
None => Err(GameError::ParseError(String::from("Invalid command!"))),
}
}
}
/// Our main Game state
#[derive(Debug)]
pub struct Game {
/// Letters that have been guessed
pub attempted_letters: HashSet<char>,
/// Words that have been guessed
pub attempted_words: HashSet<String>,
/// Tries left
pub attempts_remaining: usize,
/// The solution to the game
solution: String,
/// Number of successful letter guesses
number_of_letters_guessed: usize,
/// Guessed letters
correct_letters: HashSet<char>,
}
impl Game {
/// The first argument is the solution of the game.
/// Its constraints are: must be a non-empty string, and it must contain alphabetic letters only.
/// The second argument is the amount of tries allowed before the game ends.
/// When it reaches 0, the game ends with a loss. A corresponding struct variable exists.
pub fn new(solution: &str, attempts_remaining: usize) -> Result<Self, GameError> {
if solution.len() == 0 || solution.chars().filter(|c| !c.is_alphabetic()).count() > 0 {
return Err(GameError::InvalidSolution(String::from(solution)));
}
Ok(Self {
attempts_remaining,
solution: String::from(solution),
attempted_letters: HashSet::new(),
attempted_words: HashSet::new(),
number_of_letters_guessed: 0,
correct_letters: HashSet::new(),
})
}
/// Returns true if the game is either a win (all letters have been guessed)
/// or a loss (no more attempts remaining)
pub fn is_over(&self) -> bool {
self.attempts_remaining == 0 || self.number_of_letters_guessed == self.solution.len()
}
/// Accepts a symbol and checks if this symbol is contained in the solution.
/// If yes, returns `Ok(true)`.
/// If no, returns `Ok(false)` and decrements `self.attempts_remaining`.
///
/// 1. If the method is called on a game that's over, returns `Err(GameError::GameOver)`.
/// 2. If the symbol has been tried already, returns `Err(GameError::BadGuess)`.
///
/// If `self.attempts_remaining` is 0, the game is lost.
///
/// If the word is guessed ( all of its symbols have been guessed ), the game is won.
pub fn guess_letter(&mut self, guess: char) -> Result<bool, GameError> {
if self.is_over() {
return Err(GameError::GameOver);
}
if !guess.is_alphabetic() {
return Err(GameError::ParseError(String::from("Not a letter!")));
}
if self.attempted_letters.contains(&guess) {
return Err(GameError::BadGuess(String::from("Letter already tried!")));
}
self.attempted_letters.insert(guess);
if self.solution.contains(guess) {
self.number_of_letters_guessed = self.number_of_letters_guessed +
self.solution.matches(guess).count();
self.correct_letters.insert(guess);
return Ok(true);
} else {
self.attempts_remaining = self.attempts_remaining - 1;
return Ok(false);
}
}
/// Accepts a word and checks if this symbol is contained in the solution.
/// If yes, returns `Ok(true)`.
/// If no, returns `Ok(false)` and decrements `self.attempts_remaining`.
///
/// 1. If the method is called on a game that's over, returns `Err(GameError::GameOver)`.
/// 2. If the symbol has been tried already, returns `Err(GameError::BadGuess)`.
///
/// If `self.attempts_remaining` is 0, the game is lost.
///
/// If the word is guessed ( all of its symbols have been guessed ), the game is won.
pub fn guess_word(&mut self, guess: &str) -> Result<bool, GameError> {
if self.is_over() {
return Err(GameError::GameOver);
}
if guess.chars().filter(|&c| !c.is_alphabetic()).count() > 0 {
return Err(GameError::ParseError(
String::from("Word does not contain only letters!"),
));
}
if self.attempted_words.contains(guess) {
return Err(GameError::BadGuess(String::from("Word already tried!")));
}
self.attempted_words.insert(guess.to_string());
if self.solution == guess {
self.number_of_letters_guessed = self.solution.len();
return Ok(true);
} else {
self.attempts_remaining = self.attempts_remaining - 1;
return Ok(false);
}
}
}
/// Display the solution of the game depending on how many symbols have been guessed so far.
/// ## For example:
/// 1. A word `hangman` and no letters guessed will appear as `_ _ _ _ _ _ _`
/// 2. A word with a couple of letters guessed will appear as `h _ n _ m _ n`
/// If the game is lost (`self.attempts_remaining` is 0), a `git gud` message is displayed.
/// Analogous with `self.number_of_letters_guessed` is as long as the solution.
impl fmt::Display for Game {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !self.is_over() {
for c in self.solution.chars() {
if self.correct_letters.contains(&c) {
write!(f, "{} ", c)?;
} else {
write!(f, "_ ")?;
};
}
write!(f, "\nAttempts remaining: {}", self.attempts_remaining)
} else {
if self.attempts_remaining == 0 {
write!(
f,
"You lost!\n You better git gud! Your word was: {}",
self.solution
)?;
} else if self.number_of_letters_guessed == self.solution.len() {
write!(f, "You won and got gud!\n Your word was: {}", self.solution)?;
}
write!(f, "\n")
}
}
}

Лог от изпълнението

Compiling solution v0.1.0 (file:///tmp/d20171210-6053-1kbbomw/solution)
    Finished dev [unoptimized + debuginfo] target(s) in 6.52 secs
     Running target/debug/deps/solution-3f98bfa5c86a5dd9

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/solution_test-3d9e4ea2eafbbc82

running 15 tests
test solution_test::test_command_parsing_cyrillic ... FAILED
test solution_test::test_command_parsing_extra_stuff ... FAILED
test solution_test::test_command_parsing_full_words ... FAILED
test solution_test::test_command_parsing_partial_words ... ok
test solution_test::test_command_parsing_spacing ... ok
test solution_test::test_command_parsing_special ... FAILED
test solution_test::test_game_basic ... ok
test solution_test::test_game_cyrillic ... ok
test solution_test::test_game_display ... ok
test solution_test::test_game_error ... ok
test solution_test::test_game_guess_basic ... ok
test solution_test::test_game_guess_state_lose ... ok
test solution_test::test_game_guess_state_won ... ok
test solution_test::test_game_guess_word ... ok
test solution_test::test_game_over_guesses ... ok

failures:

---- solution_test::test_command_parsing_cyrillic stdout ----
	thread 'solution_test::test_command_parsing_cyrillic' panicked at 'Expected Ok(Command::TryLetter('\u{44f}')) to match Err(ParseError("Invalid command!"))', tests/solution_test.rs:235:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.

---- solution_test::test_command_parsing_extra_stuff stdout ----
	thread 'solution_test::test_command_parsing_extra_stuff' panicked at 'Expected Ok(Command::TryLetter('a')) to match Err(ParseError("Invalid command!"))', tests/solution_test.rs:241:4

---- solution_test::test_command_parsing_full_words stdout ----
	thread 'solution_test::test_command_parsing_full_words' panicked at 'Expected Ok(Command::TryLetter('z')) to match Err(ParseError("Invalid command!"))', tests/solution_test.rs:183:4

---- solution_test::test_command_parsing_special stdout ----
	thread 'solution_test::test_command_parsing_special' panicked at 'Expected Ok(Command::TryLetter('\u{44f}')) to match Err(ParseError("Invalid command!"))', tests/solution_test.rs:194:4


failures:
    solution_test::test_command_parsing_cyrillic
    solution_test::test_command_parsing_extra_stuff
    solution_test::test_command_parsing_full_words
    solution_test::test_command_parsing_special

test result: FAILED. 11 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--test solution_test'

История (1 версия и 2 коментара)

Георги качи първо решение на 05.12.2017 18:04 (преди почти 8 години)

Стабилно решение като цяло, и е чудесно, че си и документирал частите -- добро упражнение е, за когато решиш да си правиш собствени библиотеки, или за проекта :).

That said, парсенето на команди не си го тествал много добре :). Може би си го оставил за накрая? Either way, другия път мисли внимателно за edge case-ове и тествай.