Wordle
- Краен срок:
- 24.11.2022 17:00
- Точки:
- 20
Срокът за предаване на решения е отминал
Миналата година 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), така че каквото измислите за този случай ще е валидно.
Задължително прочетете (или си припомнете): Указания за предаване на домашни
Погрижете се решението ви да се компилира с базовия тест: