diff --git a/.vscode/launch.json b/.vscode/launch.json index c612c97..25fc024 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,17 @@ ], "jinja": true, "justMyCode": true + }, + { + "name": "Python: Debug Tests", + "type": "python", + "request": "launch", + "program": "${file}", + "purpose": [ + "debug-test" + ], + "console": "integratedTerminal", + "justMyCode": false } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e065b89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "test*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/test_wordle.py b/test_wordle.py new file mode 100644 index 0000000..b58703b --- /dev/null +++ b/test_wordle.py @@ -0,0 +1,47 @@ +#!/usr/bin/python + + +import wordle + +from wordle import LetterResult + +import unittest + + +class TestConstraints(unittest.TestCase): + + def test_empty(self): + constraints = wordle.Constraints() + self.assertTrue(constraints.matches('abcde')) + self.assertTrue(constraints.matches('bcdef')) + + def test_correct_single(self): + constraints = wordle.Constraints() + constraints.update_graded('abcde', [LetterResult.CORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT]) + + self.assertTrue(constraints.matches('amnop')) + self.assertTrue(constraints.matches('aaaaa')) + + self.assertFalse(constraints.matches('abcde')) + self.assertFalse(constraints.matches('lmnop')) + self.assertFalse(constraints.matches('aedcb')) + self.assertFalse(constraints.matches('amnob')) + + def test_correct_duplicate_letter(self): + constraints = wordle.Constraints() + constraints.update_graded('aazzz', [LetterResult.CORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT]) + + self.assertTrue(constraints.matches('abcde')) + self.assertTrue(constraints.matches('abbbb')) + + self.assertFalse(constraints.matches('aabbb')) + self.assertFalse(constraints.matches('abbba')) + self.assertFalse(constraints.matches('bbbbb')) + self.assertFalse(constraints.matches('bbbba')) + self.assertFalse(constraints.matches('babbb')) + + + + +if __name__ == '__main__': + unittest.main() diff --git a/wordle.py b/wordle.py index 97dc84d..6cf316d 100644 --- a/wordle.py +++ b/wordle.py @@ -27,7 +27,9 @@ _wordle_dictionary = set() _invalid_words = set(['staia', 'erept', 'boily', 'bolty', 'pindy', 'hyrse', 'hirse', 'retan', 'sisel', 'skeel', 'telae', 'genae', 'beata', 'beala', 'macco', 'beele', 'divet', 'vedet', - 'stail', 'roter', 'fikie', 'muang', 'nurry']) + 'stail', 'roter', 'fikie', 'muang', 'nurry', 'arara', 'araru', 'araua', 'arawa', + 'mitra', 'gotra', 'whaly', 'whalm', 'doray', 'korah', 'caroa', 'carlo', 'carot', + 'carum', 'theet', 'tebet', 'ghole', 'odell']) def dictionary(): @@ -152,36 +154,39 @@ def scored_words(wordle_dictionary=dictionary()): class Constraints(): - correct = [None] * WORDLE_LENGTH # Letter is None, or known. - # Sets of letters that must be elsewhere - move = defaultdict(set) - min = defaultdict(int) - exact = set() + + def __init__(self): + # Array of known letters in result with None for unknown. + self.correct = [None] * WORDLE_LENGTH + # Array with per-letter sets of known bad letters. + self.incorrect = [set() for _ in range(WORDLE_LENGTH)] + # Dict of letter -> int with minimum required count of letter in word. + self.count = defaultdict(int) + # If letter is present in set, value in min is both min and max. + self.exact = set() # List of LetterResult values. def update_graded(self, guess, grade): guess = guess.lower() - new_min = defaultdict(int) - new_exact = set() + new_count = defaultdict(int) for i, letter in enumerate(guess): match grade[i]: case LetterResult.CORRECT: self.correct[i] = letter - new_min[letter] += 1 + new_count[letter] += 1 case LetterResult.MOVE: - self.move[letter].add(i) - new_min[letter] += 1 + self.incorrect[i].add(letter) + new_count[letter] += 1 case LetterResult.INCORRECT: - new_exact.add(letter) - new_min[letter] += 0 # Ensure letter is in min. + self.incorrect[i].add(letter) + self.exact.add(letter) + new_count[letter] += 0 # Ensure letter is in dict - for letter, m in new_min.items(): - self.min[letter] = max(m, self.min[letter]) - - self.exact |= new_exact + for letter, c in new_count.items(): + self.count[letter] = max(c, self.count[letter]) def matches(self, guess): @@ -189,22 +194,22 @@ class Constraints(): # All known correct letters in place. for i, letter in enumerate(guess): + # If we have a known letter, and don't match... if self.correct[i] and self.correct[i] != letter: - return False # If it's incorrect, the word is incorrect. + return False - # Any MOVE letters in known wrong locations. - if letter in self.move: - if i in self.move[letter]: - return False + # If we have a letter that's known to be incorrect in the current location... + if letter in self.incorrect[i]: + return False counts[letter] += 1 - for letter, min in self.min.items(): + for letter, c in self.count.items(): if letter in self.exact: - if counts[letter] != min: + if counts[letter] != c: return False else: - if counts[letter] < min: + if counts[letter] < c: return False return True @@ -217,6 +222,8 @@ def filter_dictionary(constraints, wordle_dictionary=dictionary()): if __name__ == '__main__': constraints = Constraints() constraints.update_graded('cares', [LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT]) + constraints.update_graded('monty', [LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT]) + constraints.update_graded('filii', [LetterResult.MOVE, LetterResult.MOVE, LetterResult.INCORRECT, LetterResult.INCORRECT, LetterResult.INCORRECT]) valid_words = filter_dictionary(constraints)