Hangman

Предадени решения

Краен срок:
08.12.2017 17:00
Точки:
15

Срокът за предаване на решения е отминал

extern crate solution;
use self::solution::*;
macro_rules! assert_substring {
($expected:expr, $actual:expr) => {
assert!($actual.contains($expected), "Expected {:?} to contain {:?}", $actual, $expected);
}
}
macro_rules! assert_not_substring {
($expected:expr, $actual:expr) => {
assert!(!$actual.contains($expected), "Expected {:?} to NOT contain {:?}", $actual, $expected);
}
}
macro_rules! assert_match {
($pattern:pat, $actual:expr) => {
if let $pattern = $actual {
assert!(true);
} else {
assert!(false, "Expected {} to match {:?}", stringify!($pattern), $actual);
}
}
}
#[test]
fn test_game_basic() {
let g = Game::new("funyarinpa", 10).unwrap();
assert_eq!(10, g.attempts_remaining);
assert_eq!(0, g.attempted_letters.len());
assert_eq!(0, g.attempted_words.len());
}
#[test]
fn test_game_error() {
assert_match!(Some(GameError::InvalidSolution(_)), Game::new("foo bar", 10).err());
assert_match!(Some(GameError::InvalidSolution(_)), Game::new("foo_bar", 10).err());
assert_match!(Some(GameError::InvalidSolution(_)), Game::new("", 10).err());
assert_match!(Some(GameError::InvalidSolution(_)), Game::new("_ _ _", 10).err());
}
#[test]
fn test_game_cyrillic() {
let mut g = Game::new("клаксон", 10).unwrap();
let _ = g.guess_letter('к');
assert_eq!(1, g.attempted_letters.len());
assert_substring!("к _ _ к _ _ _", format!("{}", g));
let _ = g.guess_word("класкон");
assert_eq!(1, g.attempted_letters.len());
assert_eq!(1, g.attempted_words.len());
assert_substring!("к _ _ к _ _ _", format!("{}", g));
let _ = g.guess_word("клаксон");
assert_substring!("клаксон", format!("{}", g));
}
#[test]
fn test_game_display() {
let g = Game::new("foo", 10).unwrap();
assert_substring!("_ _ _", format!("{}", g));
assert_not_substring!("_ _ _ _", format!("{}", g));
let g = Game::new("foobar", 10).unwrap();
assert_substring!("_ _ _ _ _ _", format!("{}", g));
assert_not_substring!("_ _ _ _ _ _ _", format!("{}", g));
}
#[test]
fn test_game_guess_basic() {
let mut g = Game::new("foo", 10).unwrap();
assert_substring!("_ _ _", format!("{}", g));
assert_eq!(10, g.attempts_remaining);
assert_eq!(true, g.guess_letter('o').unwrap());
assert_substring!("_ o o", format!("{}", g));
assert_eq!(10, g.attempts_remaining);
assert_match!(GameError::BadGuess(_), g.guess_letter('o').unwrap_err());
assert_substring!("_ o o", format!("{}", g));
assert_eq!(10, g.attempts_remaining);
assert_eq!(false, g.guess_letter('z').unwrap());
assert_substring!("_ o o", format!("{}", g));
assert_eq!(9, g.attempts_remaining);
}
#[test]
fn test_game_guess_word() {
let mut g = Game::new("foobar", 2).unwrap();
assert_substring!("_ _ _ _ _ _", format!("{}", g));
assert_eq!(2, g.attempts_remaining);
assert_eq!(false, g.guess_word("barfoo").unwrap());
assert_substring!("_ _ _ _ _ _", format!("{}", g));
assert_eq!(1, g.attempts_remaining);
assert_match!(GameError::BadGuess(_), g.guess_word("barfoo").unwrap_err());
assert_substring!("_ _ _ _ _ _", format!("{}", g));
assert_eq!(1, g.attempts_remaining);
assert_eq!(true, g.guess_word("foobar").unwrap());
assert_substring!("foobar", format!("{}", g));
assert_eq!(1, g.attempts_remaining);
}
#[test]
fn test_game_guess_state_lose() {
let mut g = Game::new("xyzzy", 2).unwrap();
let _ = g.guess_letter('a');
assert_eq!(1, g.attempts_remaining);
assert_eq!(false, g.is_over());
let _ = g.guess_letter('b');
assert_eq!(0, g.attempts_remaining);
assert_eq!(true, g.is_over());
assert_substring!("lost", g.to_string());
assert_not_substring!("won", g.to_string());
assert_substring!("xyzzy", g.to_string());
}
#[test]
fn test_game_guess_state_won() {
let mut g = Game::new("zzzz", 2).unwrap();
let _ = g.guess_letter('a');
assert_eq!(1, g.attempts_remaining);
assert_eq!(false, g.is_over());
let _ = g.guess_letter('z');
assert_eq!(1, g.attempts_remaining);
assert_eq!(true, g.is_over());
assert_substring!("won", g.to_string());
assert_not_substring!("lost", g.to_string());
assert_substring!("zzzz", g.to_string());
}
#[test]
fn test_game_over_guesses() {
let mut g = Game::new("foo", 10).unwrap();
let _ = g.guess_word("foo");
assert!(g.is_over());
assert_match!(Err(GameError::GameOver), g.guess_letter('f'));
assert_match!(Err(GameError::GameOver), g.guess_letter('b'));
assert_match!(Err(GameError::GameOver), g.guess_word("foo"));
assert_match!(Err(GameError::GameOver), g.guess_word("bar"));
}
#[test]
fn test_command_parsing_full_words() {
assert_match!(Ok(Command::Quit), "quit".parse::<Command>());
assert_match!(Ok(Command::Quit), "Quit".parse::<Command>());
assert_match!(Ok(Command::Quit), "QUIT".parse::<Command>());
assert_match!(Ok(Command::Quit), "quit quit".parse::<Command>());
assert_match!(Ok(Command::Quit), "quit anything else".parse::<Command>());
assert_match!(Ok(Command::Quit), "quit with the rage of an angry god".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "flip".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "uqit".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "RAGE QUIT".parse::<Command>());
assert_match!(Ok(Command::Info), "info".parse::<Command>());
assert_match!(Ok(Command::Info), "Info".parse::<Command>());
assert_match!(Ok(Command::Info), "INFO".parse::<Command>());
assert_match!(Ok(Command::TryLetter('x')), "try letter x".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "try letter xy".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "try letter".parse::<Command>());
assert_match!(Ok(Command::TryLetter('z')), "Try Letter z".parse::<Command>());
assert_match!(Ok(Command::TryWord(_)), "try word x".parse::<Command>());
assert_match!(Ok(Command::TryWord(_)), "try word xyzzy".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "tryword xyzzy".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "try word".parse::<Command>());
}
#[test]
fn test_command_parsing_special() {
assert_match!(Ok(Command::TryLetter('я')), "try letter я".parse::<Command>());
assert_match!(Ok(Command::TryWord(_)), "Try Word Язовец".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), " ".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "___".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "word".parse::<Command>());
}
#[test]
fn test_command_parsing_partial_words() {
assert_match!(Ok(Command::Quit), "q".parse::<Command>());
assert_match!(Ok(Command::Quit), "Q".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "1q".parse::<Command>());
assert_match!(Ok(Command::Help), "help".parse::<Command>());
assert_match!(Ok(Command::Help), "H".parse::<Command>());
assert_match!(Ok(Command::Help), "h".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "~h".parse::<Command>());
assert_match!(Ok(Command::Info), "i".parse::<Command>());
assert_match!(Ok(Command::Info), "I".parse::<Command>());
assert_match!(Ok(Command::Info), " i ".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "_i".parse::<Command>());
assert_match!(Ok(Command::TryLetter('c')), "t l c".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "t t t".parse::<Command>());
assert_match!(Ok(Command::TryWord(_)), "t w c".parse::<Command>());
assert_match!(Err(GameError::ParseError(_)), "w t w".parse::<Command>());
}
#[test]
fn test_command_parsing_spacing() {
assert_match!(Ok(Command::Quit), " quit".parse::<Command>());
assert_match!(Ok(Command::Quit), "q ".parse::<Command>());
}
#[test]
fn test_command_parsing_cyrillic() {
assert_match!(Ok(Command::TryLetter('я')), "try letter я".parse::<Command>());
assert_match!(Ok(Command::TryWord(_)), "try word бабаяга".parse::<Command>());
}
#[test]
fn test_command_parsing_extra_stuff() {
assert_match!(Ok(Command::TryLetter('a')), "try letter a b c".parse::<Command>());
match "try word foo bar baz".parse::<Command>() {
Ok(Command::TryWord(word)) => assert_eq!(word, "foo"),
Ok(command) => assert!(false, "Failed to parse: {:?}", command),
Err(e) => assert!(false, "Failed to parse: {:?}", e),
}
assert_match!(Ok(Command::TryLetter('a')), "tr le a".parse::<Command>());
}

Hangman

First things first: вижте guide-а за предаване на домашни! Дори да сте го гледали преди, може да има нови неща в него. Бъдете сигурни, че решението ви поне се компилира с базовия тест, иначе ще получите 0 точки.

Тази задача е игра на бесеница. Ще имплементирате няколко компонента от играта:

  • Инициализиране на игра от дума и брой опити
  • Конвертиране на текущото състояние на играта до низ, показваем на потребителя.
  • Парсене на команда от потребителя.
  • Краен резултат, победа или загуба.

Грешки

Като за начало, очакваме да дефинирате следния тип, с всички възможни грешки, които може да връщате в играта:

#[derive(Debug)]
pub enum GameError {
    ParseError(String),
    BadGuess(String),
    InvalidSolution(String),
    GameOver,
}

impl Display for GameError {
    /// Имплементацията на този метод може да върне какъвто низ искате, но типа `GameError` трябва
    /// да имплементира trait-а. Чувствайте се свободни да бъдете креативни със съобщенията, или не.
    ///
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // ...
    }
}

По-долу ще ви уточним кои методи какви типове грешки се очаква да връщат. Съветваме си да имплементирате From trait-а, за да конвертирате лесно от стандартни грешки към тези типове грешки. Не е нужно да го направите, и може би не си заслужава за всички видове грешки.

Забележете, че някои от грешките приемат низове, които могат да се използват за вариране на начина, по който се показват грешките. В идеалния случай, вие сами бихте могли да си изберете структурата и типовете данни, които да съхранявате във вашите собствени грешки, но понеже трябва да тестваме тия неща, този път просто ги имплементирайте както сме ги описали.

Превръщане на низ в команда

Очакваме да дефинирате следния тип:

use std::str::FromStr;

#[derive(Debug)]
pub enum Command {
    TryLetter(char),
    TryWord(String),
    Info,
    Help,
    Quit,
}

impl FromStr for Command {
    type Err = GameError;

    /// Този метод ще приеме string slice, и ще върне нова команда, която му съответства. Правилата
    /// за това кои низове се превръщат в какви команди са по-долу.
    ///
    /// В случай на грешка, винаги ще се върне `GameError::ParseError`. С какъвто низ искате -- било то
    /// само входния низ, или пълно съобщение за грешка.
    ///
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // ...
    }
}

Командите, както виждате, са 5. Най-просто казано, командите се конвертират по следния начин:

  • От входа "help" получаваме командата Command::Help
  • От входа "info" получаваме командата Command::Info
  • От входа "quit" получаваме командата Command::Quit
  • От входа "try letter x" получаваме командата Command::TryLetter('x')
  • От входа "try word abc" получаваме командата Command::TryWord(String::from("abc"))

Ще се наложи да обработите входа и да прецените коя команда с какви параметри ще извадите. Ще дадем доста свобода на входа, за доброто на потребителя:

  • Малки или големи букви нямат значение. Командите help, Help и HELP са еквивалентни
  • Начален и краен whitespace няма значение. Низа try letter x е еквивалентен на try letter x
  • Може командите да бъдат съкратени, примерно t l x и tr le x са еквивалентни на try letter x. (Hint: може да проверявате първите буквички, няма да тестваме с неща като "hlep" или "ifno")
  • Всичко след правилната команда се игнорира. Тоест, help i need somebody се парси просто до Command::Help, a try word one two three се парси до Command::TryWord(String::from("one"))
  • Няма да тестваме с повече от един интервал между компонентите. Чувствайте се свободни да разбивате входа по само един интервал. (Макар че hint: типа &str си има вече метод за тая цел)
  • Няма да тестваме за валидност на символа или думата. По-долу, когато инициализирате играта, ще проверявате дали решението е валидно, но тук няма нужда (макар че ако искате, няма да попречи).

Ето и някои ограничения, обаче:

  • В командата try letter x, въвеждането на повече от един символ за x е грешка. Тоест, try letter xy е грешка.
  • Каквото и да е освен тези команди, е грешка. foo try letter x е невалидна команда.
  • Празен низ е грешка.

Инициализиране на нова игра

Състоянието на играта ще се съхранява като структура от тип Game. Ето публичните атрибути, които очакваме:

use std::collections::HashSet;

pub struct Game {
    /// Букви, които вече са били пробвани
    pub attempted_letters: HashSet<char>,

    /// Думи, които вече са били пробвани
    pub attempted_words: HashSet<String>,

    /// Брой на оставащите опити.
    pub attempts_remaining: u32,

    // ...
}

Чувствайте се свободни да вкарате каквито още атрибути решите. Вероятно ще ви трябва поне едно поле за правилната дума, в каквато форма изберете. Може би и още няколко полета ще ви бъдат полезни.

Ето как създаваме нова игра:

impl Game {
    /// Първия аргумент е думата, която ще е правилния отговор на играта. Вижте по-долу за
    /// ограниченията, които трябва да спазва.
    ///
    /// Втория аргумент е броя опити, които има играча да познае думата. Когато ударят 0, играта
    /// минава в състояние на "загуба". Запишете тази стойност в `self.attempts_remaining`.
    ///
    pub fn new(solution: &str, attempts: u32) -> Result<Self, GameError> {
        // ...
    }
}

Забележете, че метода не връща игра, а връща резултат! Възможно е една игра да бъде конструирана невалидно. Може да се спори, че в такава ситуация е ок да се panic-не, но за целите на упражнението, ще върнем грешка от тип GameError::InvalidSolution. Това се случва ако:

  • solution е празен низ
  • solution съдържа не-азбучни символи -- тези, за които char::is_alphabetic() връща false.

Иначе, инициализирате играта с началното ѝ състояние и я връщате като резултат.

Познаване на букви или думи

Предоставяме следните методи, с които може да се опитаме да познаем буква или дума, променяйки състоянието на играта:

impl Game {
    /// Приема символ, проверява дали този символ присъства в решението:
    ///
    ///   - Ако да, връща `Ok(true)`
    ///   - Ако не, връща `Ok(false)` и намалява `self.attempts_remaining` с 1.
    ///
    /// Сами решете дали да различавате малки и големи букви -- няма да тестваме за това.
    ///
    /// 1. Ако метода е извикан на игра, която вече е приключила, връща резултат `Err(GameOver)`.
    /// 2. Ако символа вече е бил пробван, връща `Err(GameError::BadGuess)` с каквото съобщение пожелаете.
    ///
    /// Ако `self.attempts_remaining` е 0, играта е приключила със загуба. По-долу ще видите как
    /// това става ясно за потребителя.
    ///
    /// Ако думата е позната (всичките ѝ символи са "разкрити"), играта е приключила с победа.
    /// По-долу ще видите как това става ясно за потребителя.
    ///
    pub fn guess_letter(&mut self, guess: char) -> Result<bool, GameError> {
        // ...
    }

    /// Приема дума, проверява дали тази дума е решението:
    ///
    ///   - Ако да, връща `Ok(true)`
    ///   - Ако не, връща `Ok(false)` и намалява `self.attempts_remaining` с 1.
    ///
    /// Сами решете дали да различавате малки и големи букви -- няма да тестваме за това.
    ///
    /// 1. Ако метода е извикан на игра, която вече е приключила, връща резултат `Err(GameOver)`.
    /// 2. Ако думата вече е бил пробвана, връща `Err(GameError::BadGuess)` с каквото съобщение пожелаете.
    ///
    /// Ако `self.attempts_remaining` е 0, играта е приключила със загуба. По-долу ще видите как
    /// това става ясно за потребителя.
    ///
    /// Ако думата е позната, играта е приключила с победа. По-долу ще видите как това става ясно
    /// за потребителя.
    ///
    pub fn guess_word(&mut self, guess: &str) -> Result<bool, GameError> {
        // ...
    }

    /// Връща `true`, ако играта е приключила, по един или друг начин. Иначе връща `false`.
    ///
    pub fn is_over(&self) -> bool {
        // ...
    }
}

Показване на текстова репрезентация на играта

Очакваме да имплементирате trait-а Display за играта, който да я конвертира до текст. Това би изглеждало така:

use std::fmt::{self, Display, Write};

impl Display for Game {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // ...
    }
}

Играта се рендерира различно в зависимост от това дали е спечелена, загубена, или в момента продължава.

В случай на победа, очакваме да върнете низ, който включва думата "won" и думата-решение. Примерно, ако solution е било "баба", може да върнете низа:

You won! ^_^
The word was: баба

В кода, това би изглеждало така: "You won! ^_^\nThe word was: баба". Напълно е ок да напишете нещо друго, стига то да включва низа "won" и (в случая) низа "баба".

В случай на загуба, очакваме да върнете низ, който включва думата "lost" и думата-решение. Примерно, ако solution е било "дядо", може да върнете низа:

You lost! :/
The word was: дядо

Отново, свободни сте да импровизирате с истинското съдържание, стига да включва думата "lost" и решението.

В случай, че играта в момента продължава, очакваме да извадите низ, който показва буквите, които вече са "разкрити" (познати правилно), и показва останалите като "_". Като остава по един интервал между всеки един от тези символи.

Тоест, ако решението е "крокодил", и сме пробвали буквата "к" и буквата "о", може да извадите това:

Attempts remaining: 10
Guess: к _ о к о _ _ _

Единственото, за което ще проверяваме, ще е форматираната дума с правилния брой подчертавки и познати букви. Чувствайте се свободни да импровизирате за останалия текст, включително, ако искате, да покажете някакво бесило, което да се рисува на процент опити :).

Не забравяйте!

Вижте guide-а за предаване на домашни! Дори да сте го гледали преди, може да има нови неща в него. Бъдете сигурни, че решението ви поне се компилира с базовия тест, иначе ще получите 0 точки.

Задължително прочетете (или си припомнете): Указания за предаване на домашни