Skip to content

Commit 12f910c

Browse files
danielrfraserDan Fraser
andauthored
#52 Refactored the way we handle non participating characters (#54)
Co-authored-by: Dan Fraser <dfraser@atlassian.com>
1 parent 53d0bca commit 12f910c

File tree

1 file changed

+115
-23
lines changed

1 file changed

+115
-23
lines changed

native/rustsolver/src/solver.rs

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use rand::{prelude::SliceRandom, thread_rng};
2+
use std::collections::HashMap;
23
use std::iter::zip;
34
use std::ops::{Add, AddAssign};
45

@@ -7,14 +8,14 @@ type PosVec = Vec<PosTuple>;
78
type PosSlice<'a> = &'a [PosTuple];
89

910
struct GuessResult {
10-
non_partipating: Vec<char>,
11+
max_participating: HashMap<char, usize>,
1112
misplaced: PosVec,
1213
correct: PosVec,
1314
}
1415

1516
impl AddAssign for GuessResult {
1617
fn add_assign(&mut self, other: Self) {
17-
self.non_partipating.extend(other.non_partipating);
18+
self.max_participating.extend(other.max_participating);
1819
self.misplaced.extend(other.misplaced);
1920
self.correct.extend(other.correct);
2021
}
@@ -28,10 +29,6 @@ impl Add for GuessResult {
2829
}
2930
}
3031

31-
fn contains_any(word: &str, chars: &[char]) -> bool {
32-
chars.iter().any(|c| word.contains(*c))
33-
}
34-
3532
fn contains_all(word: &str, chars: &[char]) -> bool {
3633
chars.iter().all(|c| word.contains(*c))
3734
}
@@ -49,6 +46,32 @@ fn contains_at_all(word: &str, pos: PosSlice) -> bool {
4946
.all(|b| b)
5047
}
5148

49+
fn get_letter_counts(word: &str) -> HashMap<char, usize> {
50+
let mut counts: HashMap<char, usize> = HashMap::new();
51+
52+
for c in word.chars() {
53+
*counts.entry(c).or_insert(0) += 1;
54+
}
55+
counts
56+
}
57+
58+
fn contains_no_more_than(word: &str, max_counts: &HashMap<char, usize>) -> bool {
59+
let word_counts = get_letter_counts(word);
60+
61+
!max_counts.iter().any(|(c, max_count)| {
62+
// Check to see if the word has more than the max number of each letter
63+
let word_count_opt = word_counts.get(c);
64+
65+
if let Some(word_count) = word_count_opt {
66+
// Check if the word contains more than allowed max of the char
67+
word_count > max_count
68+
} else {
69+
// If the word doesn't contain the letter at all, we're okay.
70+
false
71+
}
72+
})
73+
}
74+
5275
fn contains_at_any(word: &str, pos: PosSlice) -> bool {
5376
pos.iter()
5477
.map(|(val, pos)| contains_at(word, *val, *pos))
@@ -60,18 +83,36 @@ fn remaining_wordles_words(word_list: &[String], t: &GuessResult) -> Vec<String>
6083

6184
word_list
6285
.iter()
63-
.filter(|word| !contains_any(word, &t.non_partipating))
86+
.filter(|word| contains_no_more_than(word, &t.max_participating))
6487
.filter(|word| contains_all(word, &misplaced_positions_chars))
6588
.filter(|word| !contains_at_any(word, &t.misplaced))
6689
.filter(|word| contains_at_all(word, &t.correct))
6790
.cloned()
6891
.collect()
6992
}
7093

71-
fn get_non_participating_chars(solution: &str, guess: &str) -> Vec<char> {
72-
// For the provided solution and guess: Returns all the characters contained
73-
// in guess which do not occur in solution.
74-
guess.chars().filter(|c| !solution.contains(*c)).collect()
94+
fn get_max_char_counts(solution: &str, guess: &str) -> HashMap<char, usize> {
95+
// If the guess has more instances of a character than the solution has, we
96+
// can prune any word which has more instances of the character than the
97+
// solution. This function produces a map which contains this list.
98+
99+
let solution_counts = get_letter_counts(solution);
100+
let guess_counts = get_letter_counts(guess);
101+
102+
let mut char_counts: HashMap<char, usize> = HashMap::new();
103+
104+
for (c, guess_count) in guess_counts.iter() {
105+
let solution_count_opt = solution_counts.get(c);
106+
107+
if let Some(solution_count) = solution_count_opt {
108+
if *guess_count > *solution_count {
109+
char_counts.insert(*c, *solution_count);
110+
}
111+
} else {
112+
char_counts.insert(*c, 0);
113+
}
114+
}
115+
char_counts
75116
}
76117

77118
fn get_correct_chars(solution: &str, guess: &str) -> PosVec {
@@ -106,7 +147,7 @@ fn get_misplaced_chars(solution: &str, guess: &str) -> PosVec {
106147

107148
fn get_all(solution: &str, guess: &str) -> GuessResult {
108149
GuessResult {
109-
non_partipating: get_non_participating_chars(solution, guess),
150+
max_participating: get_max_char_counts(solution, guess),
110151
misplaced: get_misplaced_chars(solution, guess),
111152
correct: get_correct_chars(solution, guess),
112153
}
@@ -124,13 +165,12 @@ pub fn last_words_mr_bond(
124165
.map(|guess| get_all(&solution.to_lowercase(), &guess.to_lowercase()))
125166
.fold(
126167
GuessResult {
127-
non_partipating: vec![],
168+
max_participating: HashMap::new(),
128169
misplaced: vec![],
129170
correct: vec![],
130171
},
131172
|a, b| a + b,
132173
);
133-
134174
remaining_wordles_words(word_list, &guess_result)
135175
.choose_multiple(&mut thread_rng(), sample_size)
136176
.cloned()
@@ -214,9 +254,47 @@ mod tests {
214254
}
215255

216256
#[test]
217-
fn test_contains_any() {
218-
assert!(!contains_any("eeuib", &str2vec("asdf")));
219-
assert!(contains_any("eeaib", &str2vec("asdf")));
257+
fn test_contains_no_more_than() {
258+
// eerie contains no more than 3 e
259+
assert!(contains_no_more_than("eerie", &HashMap::from([('e', 3)])));
260+
// eerie contains no more than 2 r
261+
assert!(contains_no_more_than("eerie", &HashMap::from([('r', 2)])));
262+
// eerie contains no more than 1 q
263+
assert!(contains_no_more_than("eerie", &HashMap::from([('q', 1)])));
264+
// eerie contains more than 2 e
265+
assert!(!contains_no_more_than("eerie", &HashMap::from([('e', 2)])));
266+
// eerie contains more than 1 e
267+
assert!(!contains_no_more_than("eerie", &HashMap::from([('e', 1)])));
268+
// eerie contains more than 0 r
269+
assert!(!contains_no_more_than("eerie", &HashMap::from([('r', 0)])));
270+
}
271+
272+
#[test]
273+
fn test_get_max_char_counts() {
274+
// The guess doesn't reveal more e's than the solution, so we can't
275+
// infer a max count
276+
assert_eq!(get_max_char_counts("eerie", "e"), HashMap::from([]));
277+
// The guess still doesn't reveal more e's than the solution, so we
278+
// can't infer a max count
279+
assert_eq!(get_max_char_counts("eerie", "ee"), HashMap::from([]));
280+
281+
// The guess still doesn't reveal more e's than the solution, so we
282+
// can't infer a max count
283+
assert_eq!(get_max_char_counts("eerie", "eee"), HashMap::from([]));
284+
285+
// The guess reveals more e's than the solution, so we can infer that
286+
// there is at most 3 e's
287+
assert_eq!(
288+
get_max_char_counts("eerie", "eeee"),
289+
HashMap::from([('e', 3)])
290+
);
291+
292+
// The guess reveals more e's than the solution, so we can infer that
293+
// there is at most 3 e's.
294+
assert_eq!(
295+
get_max_char_counts("poops", "pppooos"),
296+
HashMap::from([('p', 2), ('o', 2)])
297+
);
220298
}
221299

222300
#[test]
@@ -264,7 +342,7 @@ mod tests {
264342
remaining_wordles_words(
265343
&WORDS,
266344
&GuessResult {
267-
non_partipating: str2vec("tread"),
345+
max_participating: non_participating_count("tread"),
268346
misplaced: vec!(),
269347
correct: vec!()
270348
}
@@ -276,7 +354,7 @@ mod tests {
276354
remaining_wordles_words(
277355
&WORDS,
278356
&GuessResult {
279-
non_partipating: str2vec("treadbo"),
357+
max_participating: non_participating_count("treadbo"),
280358
misplaced: vec!(('s', 4)),
281359
correct: vec!(('i', 2), ('l', 3))
282360
}
@@ -288,19 +366,24 @@ mod tests {
288366
remaining_wordles_words(
289367
&WORDS,
290368
&GuessResult {
291-
non_partipating: str2vec("treadbok"),
369+
max_participating: non_participating_count("treadbok"),
292370
misplaced: vec!(('s', 4)),
293371
correct: vec!(('i', 2), ('l', 3), ('s', 0))
294372
}
295373
)
296374
.len(),
297375
5
298-
);
376+
)
299377
}
300378

301379
#[test]
302-
fn test_get_non_participating_chars() {
303-
assert_eq!(get_non_participating_chars("asdf", "abcd"), vec!('b', 'c'));
380+
fn test_last_words_mr_bond() {
381+
let v: Vec<String> = vec!["purge".to_string(), "puree".to_string()];
382+
383+
assert_eq!(
384+
last_words_mr_bond(&v, "purge", vec!("pence".to_string()), 5),
385+
vec!("purge")
386+
);
304387
}
305388

306389
#[test]
@@ -400,4 +483,13 @@ mod tests {
400483
fn str2vec(str: &str) -> Vec<char> {
401484
Vec::from_iter(str.chars())
402485
}
486+
487+
fn non_participating_count(str: &str) -> HashMap<char, usize> {
488+
let mut counts: HashMap<char, usize> = HashMap::new();
489+
490+
for c in str.chars() {
491+
counts.insert(c, 0);
492+
}
493+
counts
494+
}
403495
}

0 commit comments

Comments
 (0)