Решение на Wordle от Давид Петров

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

Към профила на Давид Петров

Резултати

  • 19 точки от тестове
  • 0 бонус точки
  • 19 точки общо
  • 14 успешни тест(а)
  • 1 неуспешни тест(а)

Код

use std::fmt;
//just a tip I found on stack overflow in that regard
//although that is regarded as obsolete in most cases
#[cfg(windows)]
const EOL: &'static str = "\r\n";
#[cfg(not(windows))]
const EOL: &'static str = "\n";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameStatus {
InProgress,
Won,
Lost,
}
#[derive(Debug, Clone, Copy)]
pub enum GameError {
NotInAlphabet(char),
WrongLength { expected: usize, actual: usize },
GameIsOver(GameStatus),
}
#[derive(Debug)]
pub struct Game {
pub status: GameStatus,
pub attempts: u8,
pub alphabet: String,
pub word: String,
pub word_char_count: usize,
pub history: Vec<Word>,
}
#[derive(Debug, Clone)]
pub struct Word {
letters: Vec<Letter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LetterState {
NoMatch,
PartialMatch,
FullMatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Letter {
character: char,
state: LetterState,
}
impl Word {
fn new(letters: Vec<Letter>) -> Self {
Word { letters }
}
/// Returns true if all letters in the word are a full match
fn is_win(&self) -> bool {
self.letters.iter()
.all(|letter| letter.state == LetterState::FullMatch)
}
}
impl fmt::Display for Letter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let uppercased = self.character.to_uppercase();
match &self.state {
LetterState::NoMatch => write!(f, ">{}<", uppercased),
LetterState::PartialMatch => write!(f, "({})", uppercased),
LetterState::FullMatch => write!(f, "[{}]", uppercased),
}
}
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.letters.iter()
.map(Letter::to_string)
.collect::<String>()
.fmt(f)
}
}
impl fmt::Display for Game {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::iter::once("|_|".repeat(self.word_char_count))
.chain(self.history.iter().map(Word::to_string))
.collect::<Vec<_>>()
.join(EOL)
.fmt(f)
}
}
impl Game {
pub const MAX_ATTEMPTS: usize = 5;
/// Checks whether all letters of the passed word are contained withing the given alphabet
/// Simply returns a continuation of control flow (Ok of unit) if that condition is matched
/// Otherwise returns an Err(GameError::NotInAlphabet)
fn check_letters_in_alphabet(word: &str, alphabet: &str) -> Result<(), GameError> {
word.chars()
.find(|&c| !alphabet.contains(c))
.map_or(Ok(()), |c| Err(GameError::NotInAlphabet(c)))
}
/// Конструира нова игра с думи/букви от дадената в `alphabet` азбука. Alphabet е просто низ,
/// в който всеки символ е отделна буква, който вероятно искате да си запазите някак за после.
///
/// Подадената дума с `word` трябва да има само букви от тази азбука. Иначе очакваме да върнете
/// `GameError::NotInAlphabet` грешка с първия символ в `word`, който не е от азбуката.
///
/// Началното състояние на играта е `InProgress` а началния брой опити `attempts` е 0.
///
pub fn new(alphabet: &str, word: &str) -> Result<Self, GameError> {
Game::check_letters_in_alphabet(word, alphabet)?;
let word = String::from(word);
let word_char_count = word.chars().count();
Ok(Game {
status: GameStatus::InProgress,
attempts: 0,
alphabet: String::from(alphabet),
word_char_count,
word,
history: Vec::with_capacity(Game::MAX_ATTEMPTS)
})
}
/// Опитва се да познае търсената дума. Опита е в `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> {
if self.status != GameStatus::InProgress {
return Err(GameError::GameIsOver(self.status));
}
let guess_char_count = guess.chars().count();
if guess_char_count != self.word_char_count {
return Err(GameError::WrongLength { expected: self.word_char_count, actual: guess_char_count });
}
Game::check_letters_in_alphabet(guess, &self.alphabet)?;
let match_char_at_position = |(index, ch) : (usize, char)| {
let state =
if let Some(i) = self.word.chars().position(|c| c == ch) {
if i == index {
LetterState::FullMatch
} else {
LetterState::PartialMatch
}
} else {
LetterState::NoMatch
};
Letter {character: ch, state}
};
let letters: Vec<Letter> = guess
.chars()
.enumerate()
.map(match_char_at_position)
.collect();
let word = Word::new(letters);
if word.is_win() {
self.status = GameStatus::Won;
}
self.history.push(word.clone());
self.attempts += 1;
if self.status != GameStatus::Won
&& self.attempts as usize >= Game::MAX_ATTEMPTS {
self.status = GameStatus::Lost;
}
Ok(word)
}
}
#[cfg(test)]
mod test {
use crate::*;
#[test]
fn 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]");
}
#[cfg(test)]
mod letter {
use crate::*;
#[test]
fn display_fullmatch() {
let letter = Letter { character: 'a', state: LetterState::FullMatch };
assert_eq!("[A]", letter.to_string(), "Expected a different display format for full match state.");
}
#[test]
fn display_no_match() {
let letter = Letter { character: 'b', state: LetterState::NoMatch };
assert_eq!(">B<", letter.to_string(), "Expected a different display format for no match state.");
}
#[test]
fn display_partial_match() {
let letter = Letter { character: 'c', state: LetterState::PartialMatch };
assert_eq!("(C)", letter.to_string(), "Expected a different display format for partial match state.");
}
#[test]
fn display_uppercase_remains_unchanged() {
let letter = Letter { character: 'C', state: LetterState::PartialMatch };
assert_eq!("(C)", letter.to_string(), "Uppercase letters should remain unchanged.");
}
#[test]
fn display_unicode() {
let letter = Letter { character: 'ü', state: LetterState::FullMatch };
assert_eq!("[Ü]", letter.to_string(), "Expected a an uppercase umlaut Ü.");
let letter = Letter { character: 'ß', state: LetterState::FullMatch };
assert_eq!("[SS]", letter.to_string(), "Expected a an uppercase ß as SS.");
let letter = Letter { character: 'á', state: LetterState::FullMatch };
assert_eq!("[Á]", letter.to_string(), "Expected a an uppercase á as Á.");
}
}
#[cfg(test)]
mod word {
use crate::*;
#[test]
fn display_ascii() {
let word = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::NoMatch},
Letter {character: 'c', state: LetterState::PartialMatch},
Letter {character: 'd', state: LetterState::FullMatch},
]);
assert_eq!("[A]>B<(C)[D]", word.to_string(), "Display of word should be concatenation of uppercased letters.");
}
#[test]
fn is_win() {
let word = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::FullMatch},
Letter {character: 'c', state: LetterState::FullMatch},
Letter {character: 'd', state: LetterState::FullMatch},
]);
assert!(word.is_win(), "A word with all letters having a full match must be a win.");
}
#[test]
fn is_not_win() {
let partial = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::NoMatch},
]);
assert!(!partial.is_win(), "A word with only some of the caracters (not all) fully matched must not be a win!");
}
}
#[cfg(test)]
mod game {
use crate::*;
const ENGLISH_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
const BULGARIAN_ALPHABET: &str = "абвгдежзийклмнопрстуфхцчшщъьюя";
#[test]
fn initialization() {
assert!(matches!(Game::new(ENGLISH_ALPHABET, "süß"), Err(GameError::NotInAlphabet(_))),
"Initializing a game with a word containing characters outside of the given alphabet must result in an error.");
let game_init = Game::new(ENGLISH_ALPHABET, "search");
assert!(game_init.is_ok(), "Initializing a game with a word containing only valid characters must be ok.");
let game = game_init.unwrap();
assert_eq!(GameStatus::InProgress, game.status, "Freshly initialized game must have an in-progress status.");
assert_eq!(0, game.attempts, "Freshly initialized game must have a zero attempt count.");
assert_eq!(0, game.history.len(), "Freshly initialized game must have an empty history.");
}
#[test]
fn single_guess_state() {
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
let guess = game.guess_word("some");
assert!(matches!(guess, Err(GameError::WrongLength { expected: 6, actual: 4 })),
"A guess with a mismatched length must return an error with information about actual and expected length.");
let guess = game.guess_word("branch");
assert_eq!(1, game.attempts, "Valid guesses must increment the attempt counter.");
assert_eq!(GameStatus::InProgress, game.status, "A single guess must not exhaust the game.");
assert_eq!(">B<(R)[A]>N<[C][H]", guess.unwrap().to_string(), "Mismatch in expected string representation of a guess.");
}
#[test]
fn guess_cyrillic() {
let mut game = Game::new(BULGARIAN_ALPHABET, "ъьюящй").unwrap();
let guess = game.guess_word("юьаяшй");
assert!(guess.is_ok(), "Expected guess with cyrillic letters to be valid.");
assert_eq!("(Ю)[Ь]>А<[Я]>Ш<[Й]", guess.unwrap().to_string(),
"Mismatch of correct cyrillic letters in a guess.");
}
#[test]
fn guess_ascii_and_cyrillic_mixed() {
let mut game = Game::new((BULGARIAN_ALPHABET.to_string() + ENGLISH_ALPHABET).as_str(), "аbбcв").unwrap();
let guess = game.guess_word("bbbbbbb");
assert!(matches!(guess, Err(GameError::WrongLength { expected: 5, actual: 7 })),
"UTF-8 characters must be counted properly despite differences in byte length.");
let guess = game.guess_word("abcбв");
assert!(guess.is_ok(),
"UTF-8 characters must be counted properly despite differences in byte length.");
assert_eq!(">A<[B](C)(Б)[В]", guess.unwrap().to_string(),
"UTF-8 characters must be matched possitionally properly despite differences in byte length.");
}
#[test]
fn guess_diacritics() {
let mut game = Game::new("sy̆e", "y̆es").unwrap();
let guess = game.guess_word("y̆es");
assert!(guess.is_ok(), "Expected guess with diacritic characters to be valid.");
assert_eq!("[Y][\u{306}][E][S]", guess.unwrap().to_string(),
"Diacritics are expected as separate characters (for now at least...)");
}
#[test]
fn guess_with_same_count_repetitions(){
let mut game = Game::new(ENGLISH_ALPHABET, "foobar").unwrap();
assert_eq!("(O)[O]>P<>S<>I<>E<", game.guess_word("oopsie").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (when the number of occurences matches in the sought word and the guess).");
}
#[test]
fn guess_with_different_count_repetitions() {
let mut game = Game::new(ENGLISH_ALPHABET, "ab").unwrap();
assert_eq!("(B)[B]", game.guess_word("bb").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (even with less occurences in the guess than in the sought word).");
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
assert_eq!("[S](C)[A][R][C](E)", game.guess_word("scarce").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (even with less occurences in the guess than in the sought word).");
}
#[test]
fn lost() {
let mut game = Game::new(ENGLISH_ALPHABET, "a").unwrap();
for _ in 1..=Game::MAX_ATTEMPTS {
assert!(game.guess_word("b").is_ok(), "Valid attempt expected.");
}
assert_eq!(Game::MAX_ATTEMPTS, game.attempts as usize,
"Maximum number of attempts ({}) expected to be reached.", Game::MAX_ATTEMPTS);
assert!(matches!(game.guess_word("a"), Err(GameError::GameIsOver(GameStatus::Lost))),
"Game status must be lost after {} wrong (but valid) guesses.", Game::MAX_ATTEMPTS);
}
#[test]
fn won() {
let mut game = Game::new(ENGLISH_ALPHABET, "a").unwrap();
assert!(game.guess_word("b").is_ok(), "Valid attempt expected.");
assert!(game.guess_word("a").is_ok(), "Valid attempt expected.");
assert_eq!(2, game.attempts, "Attempt counter must be incremented after valid guesses.");
assert!(matches!(game.guess_word("a"), Err(GameError::GameIsOver(GameStatus::Won))),
"Game status must be won after the right second guess.");
}
#[test]
fn display_ascii() {
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
let _ = game.guess_word("branch");
let _ = game.guess_word("scarce");
let _ = game.guess_word("search");
assert_eq!(
game.to_string(),
vec![
"|_||_||_||_||_||_|",
">B<(R)[A]>N<[C][H]",
"[S](C)[A][R][C](E)",
"[S][E][A][R][C][H]",
].join(EOL),
"A problem arose with the game's history string representation.");
}
#[test]
fn display_ascii_and_cyrillic_mixed() {
let mut game = Game::new("abcабв", "aбcв").unwrap();
let _ = game.guess_word("бвab");
let _ = game.guess_word("aбвc");
let _ = game.guess_word("aбcв");
assert_eq!(
game.to_string(),
vec![
"|_||_||_||_|",
"(Б)(В)(A)>B<",
"[A][Б](В)(C)",
"[A][Б][C][В]",
].join(EOL),
"A problem arose with the game's history string representation.");
}
}
}

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

Compiling solution v0.1.0 (/tmp/d20230111-3772066-55xywx/solution)
    Finished test [unoptimized + debuginfo] target(s) in 0.86s
     Running tests/solution_test.rs (target/debug/deps/solution_test-0edbea2040daef01)

running 15 tests
test solution_test::test_game_display ... ok
test solution_test::test_game_display_german ... ok
test solution_test::test_game_display_cyrillic ... ok
test solution_test::test_game_state_1 ... ok
test solution_test::test_game_state_2 ... ok
test solution_test::test_game_state_3 ... ok
test solution_test::test_word_display ... ok
test solution_test::test_word_display_bulgarian ... ok
test solution_test::test_word_display_german ... ok
test solution_test::test_word_not_in_alphabet_on_construction ... ok
test solution_test::test_word_display_with_repetitions ... FAILED
test solution_test::test_word_not_in_alphabet_on_construction_cyrrilic ... ok
test solution_test::test_word_not_in_alphabet_on_guess ... ok
test solution_test::test_word_not_in_alphabet_on_guess_cyrillic ... ok
test solution_test::test_wrong_length ... ok

failures:

---- solution_test::test_word_display_with_repetitions stdout ----
thread 'solution_test::test_word_display_with_repetitions' panicked at 'assertion failed: `(left == right)`
  left: `"(P)[O](O)(B)"`,
 right: `"(P)[O][O](B)"`', tests/solution_test.rs:68:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    solution_test::test_word_display_with_repetitions

test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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

История (3 версии и 4 коментара)

Давид качи първо решение на 20.11.2022 00:21 (преди почти 3 години)

Давид качи решение на 20.11.2022 00:23 (преди почти 3 години)

use std::fmt;
-//just a tip I found on stack overflow in that regard
-//although that is regarded as obsolete in most cases
#[cfg(windows)]
const EOL: &'static str = "\r\n";
#[cfg(not(windows))]
const EOL: &'static str = "\n";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameStatus {
InProgress,
Won,
Lost,
}
#[derive(Debug, Clone, Copy)]
pub enum GameError {
NotInAlphabet(char),
WrongLength { expected: usize, actual: usize },
GameIsOver(GameStatus),
}
#[derive(Debug)]
pub struct Game {
pub status: GameStatus,
pub attempts: u8,
pub alphabet: String,
pub word: String,
pub history: Vec<Word>,
}
#[derive(Debug, Clone)]
pub struct Word {
letters: Vec<Letter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LetterState {
NoMatch,
PartialMatch,
FullMatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Letter {
character: char,
state: LetterState,
}
impl Word {
fn new(letters: Vec<Letter>) -> Self {
Word { letters }
}
fn is_win(&self) -> bool {
self.letters.iter()
.all(|letter| letter.state == LetterState::FullMatch)
}
}
impl fmt::Display for Letter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let uppercased = self.character.to_uppercase();
match &self.state {
LetterState::NoMatch => write!(f, ">{}<", uppercased),
LetterState::PartialMatch => write!(f, "({})", uppercased),
LetterState::FullMatch => write!(f, "[{}]", uppercased),
}
}
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.letters.iter()
.map(Letter::to_string)
.collect::<String>()
.fmt(f)
}
}
impl fmt::Display for Game {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::iter::once("|_|".repeat(self.word.len()))
.chain(self.history.iter().map(Word::to_string))
.collect::<Vec<_>>()
.join(EOL)
.fmt(f)
}
}
impl Game {
pub const MAX_ATTEMPTS: usize = 5;
fn check_letters_in_alphabet(word: &str, alphabet: &str) -> Result<(), GameError> {
word.chars()
.find(|&c| !alphabet.contains(c))
.map_or(Ok(()), |c| Err(GameError::NotInAlphabet(c)))
}
/// Конструира нова игра с думи/букви от дадената в `alphabet` азбука. Alphabet е просто низ,
/// в който всеки символ е отделна буква, който вероятно искате да си запазите някак за после.
///
/// Подадената дума с `word` трябва да има само букви от тази азбука. Иначе очакваме да върнете
/// `GameError::NotInAlphabet` грешка с първия символ в `word`, който не е от азбуката.
///
/// Началното състояние на играта е `InProgress` а началния брой опити `attempts` е 0.
///
pub fn new(alphabet: &str, word: &str) -> Result<Self, GameError> {
Game::check_letters_in_alphabet(word, alphabet)?;
Ok(Game {
status: GameStatus::InProgress,
attempts: 0,
alphabet: String::from(alphabet),
word: String::from(word),
history: Vec::with_capacity(Game::MAX_ATTEMPTS)
})
}
/// Опитва се да познае търсената дума. Опита е в `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> {
if self.status != GameStatus::InProgress {
return Err(GameError::GameIsOver(self.status));
}
if guess.len() != self.word.len() {
return Err(GameError::WrongLength { expected: self.word.len(), actual: guess.len() });
}
Game::check_letters_in_alphabet(guess, &self.alphabet)?;
let match_char_at_position = |(index, ch) : (usize, char)| {
let state =
if let Some(i) = self.word.chars().position(|c| c == ch) {
if i == index {
LetterState::FullMatch
} else {
LetterState::PartialMatch
}
} else {
LetterState::NoMatch
};
Letter {character: ch, state}
};
let letters: Vec<Letter> = guess
.char_indices()
.map(match_char_at_position)
.collect();
let word = Word::new(letters);
if word.is_win() {
self.status = GameStatus::Won;
}
self.history.push(word.clone());
self.attempts += 1;
if self.status != GameStatus::Won
&& self.attempts as usize >= Game::MAX_ATTEMPTS {
self.status = GameStatus::Lost;
}
Ok(word)
}
}
-
-
#[cfg(test)]
mod test {
use crate::*;
#[test]
fn 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]");
}
#[cfg(test)]
mod letter {
use crate::*;
#[test]
fn display_fullmatch() {
let letter = Letter { character: 'a', state: LetterState::FullMatch };
assert_eq!("[A]", letter.to_string(), "Expected a different display format for full match state.");
}
#[test]
fn display_no_match() {
let letter = Letter { character: 'b', state: LetterState::NoMatch };
assert_eq!(">B<", letter.to_string(), "Expected a different display format for no match state.");
}
#[test]
fn display_partial_match() {
let letter = Letter { character: 'c', state: LetterState::PartialMatch };
assert_eq!("(C)", letter.to_string(), "Expected a different display format for partial match state.");
}
#[test]
fn display_uppercase_remains_unchanged() {
let letter = Letter { character: 'C', state: LetterState::PartialMatch };
assert_eq!("(C)", letter.to_string(), "Uppercase letters should remain unchanged.");
}
#[test]
fn display_unicode() {
let letter = Letter { character: 'ü', state: LetterState::FullMatch };
assert_eq!("[Ü]", letter.to_string(), "Expected a an uppercase umlaut Ü.");
let letter = Letter { character: 'ß', state: LetterState::FullMatch };
assert_eq!("[SS]", letter.to_string(), "Expected a an uppercase ß as SS.");
let letter = Letter { character: 'á', state: LetterState::FullMatch };
assert_eq!("[Á]", letter.to_string(), "Expected a an uppercase á as Á.");
}
}
#[cfg(test)]
mod word {
use crate::*;
#[test]
fn display_ascii() {
let word = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::NoMatch},
Letter {character: 'c', state: LetterState::PartialMatch},
Letter {character: 'd', state: LetterState::FullMatch},
]);
assert_eq!("[A]>B<(C)[D]", word.to_string(), "Display of word should be concatenation of uppercased letters.");
}
#[test]
fn is_win() {
let word = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::FullMatch},
Letter {character: 'c', state: LetterState::FullMatch},
Letter {character: 'd', state: LetterState::FullMatch},
]);
assert!(word.is_win(), "A word with all letters having a full match must be a win.");
}
}
#[cfg(test)]
mod game {
use crate::*;
const ENGLISH_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
#[test]
fn initialization() {
assert!(matches!(Game::new(ENGLISH_ALPHABET, "süß"), Err(GameError::NotInAlphabet(_))),
"Initializing a game with a word containing characters outside of the given alphabet must result in an error.");
let game_init = Game::new(ENGLISH_ALPHABET, "search");
assert!(game_init.is_ok(), "Initializing a game with a word containing only valid characters must be ok.");
let game = game_init.unwrap();
assert_eq!(GameStatus::InProgress, game.status, "Freshly initialized game must have an in-progress status.");
assert_eq!(0, game.attempts, "Freshly initialized game must have a zero attempt count.");
assert_eq!(0, game.history.len(), "Freshly initialized game must have an empty history.");
}
#[test]
fn single_guess() {
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
let guess = game.guess_word("some");
assert!(matches!(guess, Err(GameError::WrongLength { expected: 6, actual: 4 })),
"A guess with a mismatched length must return an error with information about actual and expected length.");
let guess = game.guess_word("branch");
assert_eq!(1, game.attempts, "Valid guesses must increment the attempt counter.");
assert_eq!(GameStatus::InProgress, game.status, "A single guess must not exhaust the game.");
assert_eq!(">B<(R)[A]>N<[C][H]", guess.unwrap().to_string(), "Mismatch in expected string representation of a guess.");
}
#[test]
fn guess_with_same_count_repetitions(){
let mut game = Game::new(ENGLISH_ALPHABET, "foobar").unwrap();
assert_eq!("(O)[O]>P<>S<>I<>E<", game.guess_word("oopsie").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (when the number of occurences matches in the sought word and the guess).");
}
#[test]
fn guess_with_different_count_repetitions() {
let mut game = Game::new(ENGLISH_ALPHABET, "ab").unwrap();
assert_eq!("(B)[B]", game.guess_word("bb").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (even with less occurences in the guess than in the sought word).");
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
assert_eq!("[S](C)[A][R][C](E)", game.guess_word("scarce").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (even with less occurences in the guess than in the sought word).");
}
#[test]
fn lost() {
let mut game = Game::new(ENGLISH_ALPHABET, "a").unwrap();
for _ in 1..=Game::MAX_ATTEMPTS {
assert!(game.guess_word("b").is_ok(), "Valid attempt expected.");
}
assert_eq!(Game::MAX_ATTEMPTS, game.attempts as usize,
"Maximum number of attempts ({}) expected to be reached.", Game::MAX_ATTEMPTS);
assert!(matches!(game.guess_word("a"), Err(GameError::GameIsOver(GameStatus::Lost))),
"Game status must be lost after {} wrong (but valid) guesses.", Game::MAX_ATTEMPTS);
}
#[test]
fn won() {
let mut game = Game::new(ENGLISH_ALPHABET, "a").unwrap();
assert!(game.guess_word("b").is_ok(), "Valid attempt expected.");
assert!(game.guess_word("a").is_ok(), "Valid attempt expected.");
assert_eq!(2, game.attempts, "Attempt counter must be incremented after valid guesses.");
assert!(matches!(game.guess_word("a"), Err(GameError::GameIsOver(GameStatus::Won))),
"Game status must be won after the right second guess.");
}
#[test]
fn display() {
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
let _ = game.guess_word("branch");
let _ = game.guess_word("scarce");
let _ = game.guess_word("search");
assert_eq!(
game.to_string(),
vec![
"|_||_||_||_||_||_|",
">B<(R)[A]>N<[C][H]",
"[S](C)[A][R][C](E)",
"[S][E][A][R][C][H]"
].join(EOL),
"A problem arose with the game's history string representation.");
}
}
}

Давид качи решение на 20.11.2022 22:49 (преди почти 3 години)

use std::fmt;
+//just a tip I found on stack overflow in that regard
+//although that is regarded as obsolete in most cases
#[cfg(windows)]
const EOL: &'static str = "\r\n";
#[cfg(not(windows))]
const EOL: &'static str = "\n";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameStatus {
InProgress,
Won,
Lost,
}
#[derive(Debug, Clone, Copy)]
pub enum GameError {
NotInAlphabet(char),
WrongLength { expected: usize, actual: usize },
GameIsOver(GameStatus),
}
#[derive(Debug)]
pub struct Game {
pub status: GameStatus,
pub attempts: u8,
pub alphabet: String,
pub word: String,
+ pub word_char_count: usize,
pub history: Vec<Word>,
}
#[derive(Debug, Clone)]
pub struct Word {
letters: Vec<Letter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LetterState {
NoMatch,
PartialMatch,
FullMatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Letter {
character: char,
state: LetterState,
}
impl Word {
fn new(letters: Vec<Letter>) -> Self {
Word { letters }
}
+ /// Returns true if all letters in the word are a full match
fn is_win(&self) -> bool {
self.letters.iter()
.all(|letter| letter.state == LetterState::FullMatch)
}
}
impl fmt::Display for Letter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let uppercased = self.character.to_uppercase();
match &self.state {
LetterState::NoMatch => write!(f, ">{}<", uppercased),
LetterState::PartialMatch => write!(f, "({})", uppercased),
LetterState::FullMatch => write!(f, "[{}]", uppercased),
}
}
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.letters.iter()
.map(Letter::to_string)
.collect::<String>()
.fmt(f)
}
}
impl fmt::Display for Game {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- std::iter::once("|_|".repeat(self.word.len()))
+ std::iter::once("|_|".repeat(self.word_char_count))
.chain(self.history.iter().map(Word::to_string))
.collect::<Vec<_>>()
.join(EOL)
.fmt(f)
}
}
impl Game {
pub const MAX_ATTEMPTS: usize = 5;
+ /// Checks whether all letters of the passed word are contained withing the given alphabet
+ /// Simply returns a continuation of control flow (Ok of unit) if that condition is matched
+ /// Otherwise returns an Err(GameError::NotInAlphabet)
fn check_letters_in_alphabet(word: &str, alphabet: &str) -> Result<(), GameError> {
word.chars()
.find(|&c| !alphabet.contains(c))
.map_or(Ok(()), |c| Err(GameError::NotInAlphabet(c)))
}
/// Конструира нова игра с думи/букви от дадената в `alphabet` азбука. Alphabet е просто низ,
/// в който всеки символ е отделна буква, който вероятно искате да си запазите някак за после.
///
/// Подадената дума с `word` трябва да има само букви от тази азбука. Иначе очакваме да върнете
/// `GameError::NotInAlphabet` грешка с първия символ в `word`, който не е от азбуката.
///
/// Началното състояние на играта е `InProgress` а началния брой опити `attempts` е 0.
///
pub fn new(alphabet: &str, word: &str) -> Result<Self, GameError> {
Game::check_letters_in_alphabet(word, alphabet)?;
+ let word = String::from(word);
+ let word_char_count = word.chars().count();
+
Ok(Game {
status: GameStatus::InProgress,
attempts: 0,
alphabet: String::from(alphabet),
- word: String::from(word),
+ word_char_count,
+ word,
history: Vec::with_capacity(Game::MAX_ATTEMPTS)
})
}
/// Опитва се да познае търсената дума. Опита е в `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> {
if self.status != GameStatus::InProgress {
return Err(GameError::GameIsOver(self.status));
}
- if guess.len() != self.word.len() {
- return Err(GameError::WrongLength { expected: self.word.len(), actual: guess.len() });
+ let guess_char_count = guess.chars().count();
+
+ if guess_char_count != self.word_char_count {
+ return Err(GameError::WrongLength { expected: self.word_char_count, actual: guess_char_count });
}
Game::check_letters_in_alphabet(guess, &self.alphabet)?;
let match_char_at_position = |(index, ch) : (usize, char)| {
let state =
if let Some(i) = self.word.chars().position(|c| c == ch) {
if i == index {
LetterState::FullMatch
} else {
LetterState::PartialMatch
}
} else {
LetterState::NoMatch
};
Letter {character: ch, state}
};
let letters: Vec<Letter> = guess
- .char_indices()
+ .chars()
+ .enumerate()
.map(match_char_at_position)
.collect();
let word = Word::new(letters);
if word.is_win() {
self.status = GameStatus::Won;
}
self.history.push(word.clone());
self.attempts += 1;
if self.status != GameStatus::Won
&& self.attempts as usize >= Game::MAX_ATTEMPTS {
self.status = GameStatus::Lost;
}
Ok(word)
}
}
#[cfg(test)]
mod test {
use crate::*;
#[test]
fn 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]");
}
#[cfg(test)]
mod letter {
use crate::*;
#[test]
fn display_fullmatch() {
let letter = Letter { character: 'a', state: LetterState::FullMatch };
assert_eq!("[A]", letter.to_string(), "Expected a different display format for full match state.");
}
#[test]
fn display_no_match() {
let letter = Letter { character: 'b', state: LetterState::NoMatch };
assert_eq!(">B<", letter.to_string(), "Expected a different display format for no match state.");
}
#[test]
fn display_partial_match() {
let letter = Letter { character: 'c', state: LetterState::PartialMatch };
assert_eq!("(C)", letter.to_string(), "Expected a different display format for partial match state.");
}
#[test]
fn display_uppercase_remains_unchanged() {
let letter = Letter { character: 'C', state: LetterState::PartialMatch };
assert_eq!("(C)", letter.to_string(), "Uppercase letters should remain unchanged.");
}
#[test]
fn display_unicode() {
let letter = Letter { character: 'ü', state: LetterState::FullMatch };
assert_eq!("[Ü]", letter.to_string(), "Expected a an uppercase umlaut Ü.");
let letter = Letter { character: 'ß', state: LetterState::FullMatch };
assert_eq!("[SS]", letter.to_string(), "Expected a an uppercase ß as SS.");
let letter = Letter { character: 'á', state: LetterState::FullMatch };
assert_eq!("[Á]", letter.to_string(), "Expected a an uppercase á as Á.");
}
}
#[cfg(test)]
mod word {
use crate::*;
#[test]
fn display_ascii() {
let word = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::NoMatch},
Letter {character: 'c', state: LetterState::PartialMatch},
Letter {character: 'd', state: LetterState::FullMatch},
]);
assert_eq!("[A]>B<(C)[D]", word.to_string(), "Display of word should be concatenation of uppercased letters.");
}
#[test]
fn is_win() {
let word = Word::new(vec![
Letter {character: 'a', state: LetterState::FullMatch},
Letter {character: 'b', state: LetterState::FullMatch},
Letter {character: 'c', state: LetterState::FullMatch},
Letter {character: 'd', state: LetterState::FullMatch},
]);
assert!(word.is_win(), "A word with all letters having a full match must be a win.");
}
+
+ #[test]
+ fn is_not_win() {
+ let partial = Word::new(vec![
+ Letter {character: 'a', state: LetterState::FullMatch},
+ Letter {character: 'b', state: LetterState::NoMatch},
+ ]);
+
+ assert!(!partial.is_win(), "A word with only some of the caracters (not all) fully matched must not be a win!");
+ }
}
#[cfg(test)]
mod game {
use crate::*;
const ENGLISH_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
+ const BULGARIAN_ALPHABET: &str = "абвгдежзийклмнопрстуфхцчшщъьюя";
#[test]
fn initialization() {
assert!(matches!(Game::new(ENGLISH_ALPHABET, "süß"), Err(GameError::NotInAlphabet(_))),
"Initializing a game with a word containing characters outside of the given alphabet must result in an error.");
let game_init = Game::new(ENGLISH_ALPHABET, "search");
assert!(game_init.is_ok(), "Initializing a game with a word containing only valid characters must be ok.");
let game = game_init.unwrap();
assert_eq!(GameStatus::InProgress, game.status, "Freshly initialized game must have an in-progress status.");
assert_eq!(0, game.attempts, "Freshly initialized game must have a zero attempt count.");
assert_eq!(0, game.history.len(), "Freshly initialized game must have an empty history.");
}
#[test]
- fn single_guess() {
+ fn single_guess_state() {
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
let guess = game.guess_word("some");
assert!(matches!(guess, Err(GameError::WrongLength { expected: 6, actual: 4 })),
"A guess with a mismatched length must return an error with information about actual and expected length.");
let guess = game.guess_word("branch");
assert_eq!(1, game.attempts, "Valid guesses must increment the attempt counter.");
assert_eq!(GameStatus::InProgress, game.status, "A single guess must not exhaust the game.");
assert_eq!(">B<(R)[A]>N<[C][H]", guess.unwrap().to_string(), "Mismatch in expected string representation of a guess.");
}
#[test]
+ fn guess_cyrillic() {
+ let mut game = Game::new(BULGARIAN_ALPHABET, "ъьюящй").unwrap();
+ let guess = game.guess_word("юьаяшй");
+ assert!(guess.is_ok(), "Expected guess with cyrillic letters to be valid.");
+ assert_eq!("(Ю)[Ь]>А<[Я]>Ш<[Й]", guess.unwrap().to_string(),
+ "Mismatch of correct cyrillic letters in a guess.");
+ }
+
+ #[test]
+ fn guess_ascii_and_cyrillic_mixed() {
+ let mut game = Game::new((BULGARIAN_ALPHABET.to_string() + ENGLISH_ALPHABET).as_str(), "аbбcв").unwrap();
+ let guess = game.guess_word("bbbbbbb");
+ assert!(matches!(guess, Err(GameError::WrongLength { expected: 5, actual: 7 })),
+ "UTF-8 characters must be counted properly despite differences in byte length.");
+
+ let guess = game.guess_word("abcбв");
+ assert!(guess.is_ok(),
+ "UTF-8 characters must be counted properly despite differences in byte length.");
+
+ assert_eq!(">A<[B](C)(Б)[В]", guess.unwrap().to_string(),
+ "UTF-8 characters must be matched possitionally properly despite differences in byte length.");
+ }
+
+ #[test]
+ fn guess_diacritics() {
+ let mut game = Game::new("sy̆e", "y̆es").unwrap();
+ let guess = game.guess_word("y̆es");
+ assert!(guess.is_ok(), "Expected guess with diacritic characters to be valid.");
+ assert_eq!("[Y][\u{306}][E][S]", guess.unwrap().to_string(),
+ "Diacritics are expected as separate characters (for now at least...)");
+ }
+
+ #[test]
fn guess_with_same_count_repetitions(){
let mut game = Game::new(ENGLISH_ALPHABET, "foobar").unwrap();
assert_eq!("(O)[O]>P<>S<>I<>E<", game.guess_word("oopsie").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (when the number of occurences matches in the sought word and the guess).");
}
#[test]
fn guess_with_different_count_repetitions() {
let mut game = Game::new(ENGLISH_ALPHABET, "ab").unwrap();
assert_eq!("(B)[B]", game.guess_word("bb").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (even with less occurences in the guess than in the sought word).");
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
assert_eq!("[S](C)[A][R][C](E)", game.guess_word("scarce").unwrap().to_string(),
"Repeated letters must be flagged correctly if present (even with less occurences in the guess than in the sought word).");
}
#[test]
fn lost() {
let mut game = Game::new(ENGLISH_ALPHABET, "a").unwrap();
for _ in 1..=Game::MAX_ATTEMPTS {
assert!(game.guess_word("b").is_ok(), "Valid attempt expected.");
}
assert_eq!(Game::MAX_ATTEMPTS, game.attempts as usize,
"Maximum number of attempts ({}) expected to be reached.", Game::MAX_ATTEMPTS);
assert!(matches!(game.guess_word("a"), Err(GameError::GameIsOver(GameStatus::Lost))),
"Game status must be lost after {} wrong (but valid) guesses.", Game::MAX_ATTEMPTS);
}
#[test]
fn won() {
let mut game = Game::new(ENGLISH_ALPHABET, "a").unwrap();
assert!(game.guess_word("b").is_ok(), "Valid attempt expected.");
assert!(game.guess_word("a").is_ok(), "Valid attempt expected.");
assert_eq!(2, game.attempts, "Attempt counter must be incremented after valid guesses.");
assert!(matches!(game.guess_word("a"), Err(GameError::GameIsOver(GameStatus::Won))),
"Game status must be won after the right second guess.");
}
#[test]
- fn display() {
+ fn display_ascii() {
let mut game = Game::new(ENGLISH_ALPHABET, "search").unwrap();
let _ = game.guess_word("branch");
let _ = game.guess_word("scarce");
let _ = game.guess_word("search");
assert_eq!(
game.to_string(),
vec![
"|_||_||_||_||_||_|",
">B<(R)[A]>N<[C][H]",
"[S](C)[A][R][C](E)",
- "[S][E][A][R][C][H]"
+ "[S][E][A][R][C][H]",
+ ].join(EOL),
+ "A problem arose with the game's history string representation.");
+ }
+
+ #[test]
+ fn display_ascii_and_cyrillic_mixed() {
+ let mut game = Game::new("abcабв", "aбcв").unwrap();
+ let _ = game.guess_word("бвab");
+ let _ = game.guess_word("aбвc");
+ let _ = game.guess_word("aбcв");
+
+ assert_eq!(
+ game.to_string(),
+ vec![
+ "|_||_||_||_|",
+ "(Б)(В)(A)>B<",
+ "[A][Б](В)(C)",
+ "[A][Б][C][В]",
].join(EOL),
"A problem arose with the game's history string representation.");
}
}
}

От всичките игри с corner case-ове, накрая някак фундаментално грешната логика за търсене на символ така и не ми е направила впечатление след всичките тестови примери, които ми хрумнаха... щото кой да се замисли малко повече... :D Да спестя гледане за обратна връзка: като цяло винаги спирам търсенето до първо срещане на символа и дори и да има на търсения индекс някое следващо срещане, сравнявам с първия и го пиша PartialMatch... Голяма глупост :D

Реално искаме по-скоро нещо такова:

        let match_char_at_position = |(index, character) : (usize, char)| {
            let occurrences: Vec<usize> = 
                self.word.chars()
                .enumerate()
                .filter(|(_index, c)| *c == character)
                .map(|p| p.0)
                .collect();

            let state =
                if occurrences.contains(&index) {
                    LetterState::FullMatch
                } else if !occurrences.is_empty() {
                    LetterState::PartialMatch
                } else {
                    LetterState::NoMatch
                };

            Letter {character, state}
        };