Wordle

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

Краен срок:
24.11.2022 17:00
Точки:
20

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

use solution::*;
// За тестване че някакъв резултат пасва на някакъв pattern:
macro_rules! assert_match {
($expr:expr, $pat:pat) => {
if let $pat = $expr {
// all good
} else {
assert!(false, "Expression {:?} does not match the pattern {:?}", $expr, stringify!($pat));
}
}
}
#[test]
fn test_word_not_in_alphabet_on_construction() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
assert_match!(Game::new(english_letters, "oopsie-daisy"), Err(GameError::NotInAlphabet('-')));
}
#[test]
fn test_word_not_in_alphabet_on_construction_cyrrilic() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let bulgarian_letters = "абвгдежзийклмнопрстуфхцчшщъьюя";
assert_match!(Game::new(english_letters, "опа"), Err(GameError::NotInAlphabet('о')));
assert_match!(Game::new(bulgarian_letters, "oops"), Err(GameError::NotInAlphabet('o')));
assert_match!(Game::new(bulgarian_letters, "смайлифейс😄"), Err(GameError::NotInAlphabet('😄')));
}
#[test]
fn test_word_not_in_alphabet_on_guess() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "boop").unwrap();
assert_match!(game.guess_word("'oop"), Err(GameError::NotInAlphabet('\'')));
}
#[test]
fn test_word_not_in_alphabet_on_guess_cyrillic() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let bulgarian_letters = "абвгдежзийклмнопрстуфхцчшщъьюя";
let mut game = Game::new(english_letters, "boop").unwrap();
assert_match!(game.guess_word("хопа"), Err(GameError::NotInAlphabet('х')));
let mut game = Game::new(bulgarian_letters, "хелп").unwrap();
assert_match!(game.guess_word("хеlп"), Err(GameError::NotInAlphabet('l')));
}
#[test]
fn test_word_display() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "bop").unwrap();
assert_eq!(game.guess_word("ops").unwrap().to_string(), "(O)(P)>S<");
assert_eq!(game.guess_word("pob").unwrap().to_string(), "(P)[O](B)");
assert_eq!(game.guess_word("bop").unwrap().to_string(), "[B][O][P]");
}
#[test]
fn test_word_display_with_repetitions() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "boop").unwrap();
assert_eq!(game.guess_word("oops").unwrap().to_string(), "(O)[O](P)>S<");
assert_eq!(game.guess_word("poob").unwrap().to_string(), "(P)[O][O](B)");
assert_eq!(game.guess_word("boop").unwrap().to_string(), "[B][O][O][P]");
}
#[test]
fn test_word_display_bulgarian() {
let bulgarian_letters = "абвгдежзийклмнопрстуфхцчшщъьюя";
let mut game = Game::new(bulgarian_letters, "стол").unwrap();
assert_eq!(game.guess_word("лале").unwrap().to_string(), "(Л)>А<(Л)>Е<");
assert_eq!(game.guess_word("атол").unwrap().to_string(), ">А<[Т][О][Л]");
assert_eq!(game.guess_word("стол").unwrap().to_string(), "[С][Т][О][Л]");
}
#[test]
fn test_word_display_german() {
let german_letters = "abcdefghijklmnopqrstuvwxyzäöüß";
let mut game = Game::new(german_letters, "süß").unwrap();
assert_eq!(game.to_string(), "|_||_||_|");
assert_eq!(game.guess_word("füß").unwrap().to_string(), ">F<[Ü][SS]");
}
#[test]
fn test_wrong_length() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "boop").unwrap();
assert_match!(game.guess_word("boorish"), Err(GameError::WrongLength { expected: 4, actual: 7 }));
assert_match!(game.guess_word("bop"), Err(GameError::WrongLength { expected: 4, actual: 3 }));
assert_match!(game.guess_word(" bop "), Err(GameError::WrongLength { expected: 4, actual: 5 }));
assert_match!(game.guess_word(""), Err(GameError::WrongLength { expected: 4, actual: 0 }));
}
#[test]
fn test_game_display() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "cape").unwrap();
assert_eq!(game.to_string(), "|_||_||_||_|");
game.guess_word("peak").unwrap();
assert_eq!(game.to_string(), "|_||_||_||_|\n(P)(E)(A)>K<");
game.guess_word("cape").unwrap();
assert_eq!(game.to_string(), "|_||_||_||_|\n(P)(E)(A)>K<\n[C][A][P][E]");
}
#[test]
fn test_game_display_cyrillic() {
let bulgarian_letters = "абвгдежзийклмнопрстуфхцчшщъьюя";
let mut game = Game::new(bulgarian_letters, "аре").unwrap();
assert_eq!(game.to_string(), "|_||_||_|");
game.guess_word("опа").unwrap();
assert_eq!(game.to_string(), "|_||_||_|\n>О<>П<(А)");
game.guess_word("абе").unwrap();
assert_eq!(game.to_string(), "|_||_||_|\n>О<>П<(А)\n[А]>Б<[Е]");
game.guess_word("аре").unwrap();
assert_eq!(game.to_string(), "|_||_||_|\n>О<>П<(А)\n[А]>Б<[Е]\n[А][Р][Е]");
}
#[test]
fn test_game_display_german() {
let german_letters = "abcdefghijklmnopqrstuvwxyzäöüß";
let mut game = Game::new(german_letters, "süß").unwrap();
assert_eq!(game.to_string(), "|_||_||_|");
game.guess_word("füß").unwrap();
assert_eq!(game.to_string(), "|_||_||_|\n>F<[Ü][SS]");
}
#[test]
fn test_game_state_1() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "abc").unwrap();
assert_match!(game.status, GameStatus::InProgress);
game.guess_word("abc").unwrap();
assert_match!(game.status, GameStatus::Won);
assert_match!(game.guess_word("abc"), Err(GameError::GameIsOver(GameStatus::Won)));
}
#[test]
fn test_game_state_2() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "abc").unwrap();
assert_match!(game.status, GameStatus::InProgress);
for _ in 0..5 {
assert_match!(game.status, GameStatus::InProgress);
game.guess_word("bca").unwrap();
}
assert_match!(game.status, GameStatus::Lost);
assert_match!(game.guess_word("abc"), Err(GameError::GameIsOver(GameStatus::Lost)));
}
#[test]
fn test_game_state_3() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "abc").unwrap();
assert_eq!(game.attempts, 0);
for attempt_count in 1..=5 {
game.guess_word("bca").unwrap();
assert_eq!(game.attempts, attempt_count);
}
}

Миналата година Wordle изригна, но го изпуснахме за домашнo -- а е чудесна задачка, не е особено сложна за имплементация. So here we are, ще си напишем компонентите на някаква wordle-подобна игра.

Играта

Започваме с основните структури, които съхраняват състоянието на играта и малко error handling:

#[derive(Debug)]
pub enum GameStatus {
    InProgress,
    Won,
    Lost,
}

#[derive(Debug)]
pub enum GameError {
    NotInAlphabet(char),
    WrongLength { expected: usize, actual: usize },
    GameIsOver(GameStatus),
}

#[derive(Debug)]
pub struct Game {
    pub status: GameStatus,
    pub attempts: u8,
    // Каквито други полета ви трябват
}

#[derive(Debug)]
pub struct Word {
    // Каквито полета ви трябват
}

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

impl Game {
    /// Конструира нова игра с думи/букви от дадената в `alphabet` азбука. Alphabet е просто низ,
    /// в който всеки символ е отделна буква, който вероятно искате да си запазите някак за после.
    ///
    /// Подадената дума с `word` трябва да има само букви от тази азбука. Иначе очакваме да върнете
    /// `GameError::NotInAlphabet` грешка с първия символ в `word`, който не е от азбуката.
    ///
    /// Началното състояние на играта е `InProgress` а началния брой опити `attempts` е 0.
    ///
    pub fn new(alphabet: &str, word: &str) -> Result<Self, GameError> {
        todo!()
    }

    /// Опитва се да познае търсената дума. Опита е в `guess`.
    ///
    /// Ако играта е приключила, тоест статуса ѝ е `Won` или `Lost`, очакваме да върнете
    /// `GameIsOver` със статуса, с който е приключила.
    ///
    /// Ако `guess` има различен брой букви от търсената дума, очакваме да върнете
    /// `GameError::WrongLength`. Полето `expected` на грешката трябва да съдържа броя букви на
    /// търсената дума, а `actual` да е броя букви на опита `guess`.
    ///
    /// Ако `guess` има правилния брой букви, но има буква, която не е от азбуката на играта,
    /// очакваме `GameError::NotInAlphabet` както по-горе, с първия символ от `guess`, който не е
    /// от азбуката.
    ///
    /// Метода приема `&mut self`, защото всеки валиден опит (такъв, който не връща грешка) се
    /// запазва в играта за по-нататък. Метода връща `Word`, което описва освен самите символи на
    /// `guess`, и как тези символи са се напаснали на търсената дума. Също така инкрементира
    /// `attempts` с 1.
    ///
    /// След опита за напасване на думата, ако всички букви са уцелени на правилните места,
    /// очакваме `state` полето да се промени на `Won`. Иначе, ако `attempts` са станали 5,
    /// състоянието трябва да е `Lost`.
    ///
    pub fn guess_word(&mut self, guess: &str) -> Result<Word, GameError> {
        todo!()
    }
}

Какво значи "как тези символи са се напаснали"? Не изискваме конструктор за думи (макар че си направете, ако искате). Структурата Word се очаква да съдържа не само буквички, но и как те са паснали при опита, съответно се конструира, когато опитаме да познаем думата. Очакваме да имплементирате Display trait-а за Word и за Game:

use std::fmt;

impl fmt::Display for Word {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        todo!()
    }
}

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

За всяка една буква в Word, очакваме да се печата в определен вид базирано на това дали се съдържа в търсената дума:

  • Буквата е uppercase-ната, тоест ако сме вкарали щ, печатаме Щ. Ако вече е голяма буква или някакъв символ, просто си я оставяме (което char::to_uppercase() вече прави)
  • Ако буквата присъства в думата и е на правилната позиция, форматираме я като [Щ].
  • Ако буквата присъства в думата някъде другаде, форматираме я като (Щ).
  • Ако буквата не присъства никъде в думата, форматираме я като >Щ<.

Примерно:

let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "rebus").unwrap();

println!("{}", game.guess_word("route").unwrap());
// [R]>O<(U)>T<(E)
println!("{}", game.guess_word("rules").unwrap());
// [R](U)>L<(E)[S]
println!("{}", game.guess_word("rebus").unwrap());
// [R][E][B][U][S]

(Печатаме думата с {}, което използва Display. Имайте предвид за тестове, че .to_string() също използва Display имплементацията за да върне директно низ)

В примера буквата r пасва, така че винаги е [R]. Буквата o не присъства, така че е >O<. Буквата u в първия и втория опит се намира на друго място.

Няма проблеми да има дубликация, примерно:

let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "foobar").unwrap();

println!("{}", game.guess_word("oopsie").unwrap());
// (O)[O]>P<>S<>I<>E<

Първото o не пасва, но присъства, затова е (O). Второто o пасва на правилната позиция -- [O]. Просто карайте символ по символ.

История на опитите

Как печатаме пълната игра? Със всички опити, които сме направили досега:

let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "rebus").unwrap();

println!("{}", game);
//|_||_||_||_||_|

game.guess_word("route").unwrap();
println!("{}", game);
//|_||_||_||_||_|
//[R]>O<(U)>T<(E)

game.guess_word("rebus").unwrap();
println!("{}", game);
//|_||_||_||_||_|
//[R]>O<(U)>T<(E)
//[R][E][B][U][S]

Важно е да изпипаме детайлите, така че ето ви един пример с assert_eq! за да видите ясно къде има интервали и къде има нови редове:

let english_letters = "abcdefghijklmnopqrstuvwxyz";
let mut game = Game::new(english_letters, "rebus").unwrap();

game.guess_word("route").unwrap();
game.guess_word("rebus").unwrap();

assert_eq!(game.to_string(), "|_||_||_||_||_|\n[R]>O<(U)>T<(E)\n[R][E][B][U][S]");

Преди да познаем каквато и да е дума, "шаблона" се печата, като за всяка една непозната буква поставяме |_| -- няма начални интервали, няма крайни такива. След всеки нов опит има символ за нов ред, \n, освен последния. Важно е да спазвате детайлите при форматиране, иначе няма как да минат тестовете, които ги сравняват.

Üppercase-ването на немски е sehr досадно

Ако прочетете документацията на char::to_uppercase(), ще разберете защо не връща char а структура-итератор. Би трябвало този линк да ви даде достатъчно информация, за да използвате функцията, или може да използвате други методи за uppercase-ване, кой знае. Но нещо важно е, че печатането на uppercase-ната буква може да бъде повече от няколко букви, примерно:

let german_letters = "abcdefghijklmnopqrstuvwxyzäöüß";
let mut game = Game::new(german_letters, "süß").unwrap();

println!("{}", game.guess_word("füß").unwrap());
// >F<[Ü][SS]

Ако пускате домашно в последния момент (недейте) и нямате време да схванете как се работи с ToUppercase итератор, винаги има .to_uppercase().next().unwrap(), но нали, ще изгубите една-две точки.

Uppercase-ването е важно само за печатане. Приемете, че ще вкарваме като вход само малки букви и символи.

Hint: Може би Letter?

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

#[derive(Debug)]
enum Letter {
    Unknown(char),
    FullMatch(char),
    PartialMatch(char),
    NoMatch(char),
}

Или може вместо това да пазите char-а и състоянието му отделно, с нещо като това, може би пакетирано в Option:

#[derive(Debug)]
enum LetterState {
    FullMatch,
    PartialMatch,
    NoMatch,
}

И двете структури са просто неща, които можете да ползвате, които не са маркирани като "pub". В тестовете ще достъпваме само и единствено публични структури и функции -- свободни сте да си организирате кода както решите и да дефинирате каквито структури си искате, или никакви структури и просто да карате на char-ове и низове.

Ако все още ви е трудно да си измислите някакви такива неща, пробвайте примерно да копирате Letter и да му имплементирате Display.

Още съвети

  • Не ни е грижа за performance кой знае колко, така че не се притеснявайте ако примерно loop-вате два пъти през един и същ низ или копирате неща. Ако видим нещо, което изглежда ненужно, може да ви дадем съвет как да го избегнете, но със сигурност няма да ви смъкнем точки.
  • Сложили сме #[derive(Debug)] на всичко горе. Запазете го, за да не ни прави проблеми на тестовете. Ако някой accidentally го махне, можем да пренапишем тестовете и без него, но ще ни е досадно :/. Също така смело може да добавяте още derive-ове ако са ви полезни, или да си имплементирате други trait-ове
  • Приемете, че няма да тестваме с вход празен низ (не се сетихме, oops), така че каквото измислите за този случай ще е валидно.

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

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

use solution::*;
#[test]
fn test_basic() {
let english_letters = "abcdefghijklmnopqrstuvwxyz";
// Конструираме по два различни начина, just in case -- няма причина да не работи и с двата.
assert!(Game::new(english_letters, "!!!").is_err());
let mut game = Game::new(&String::from(english_letters), "abc").unwrap();
assert!(matches!(game.status, GameStatus::InProgress));
assert_eq!(game.attempts, 0);
assert_eq!(game.to_string(), "|_||_||_|");
assert_eq!(game.guess_word("abc").unwrap().to_string(), "[A][B][C]");
}