Refactor, breaking code into modules.

This commit is contained in:
dongar committed 2023-07-08 10:07:36 -05:00
1 parent 2f14532ef1
commit 0639113205
11 files changed
+312 -178

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
from collections import defaultdict from collections import defaultdict
import dictionary import dictionary
from wordle import LetterResult from game import LetterResult
class Constraints: class Constraints:
+22 -11
View File
@@ -1,5 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
import h5py
WORDLE_LENGTH = 5 WORDLE_LENGTH = 5
@@ -63,17 +65,26 @@ _invalid_words = set(
) )
_wordle_dictionary = set() class WordleWords:
_words = set()
def __init__(self, filename):
if not self._words:
with open("words_alpha.txt") as word_file:
all_words = word_file.read().split()
self._words.update(
filter(
lambda w: len(w) == WORDLE_LENGTH and w not in _invalid_words,
all_words,
)
)
def _initialize_db(self):
pass
def __iter__(self):
return iter(self._words)
def words(): def words():
if not _wordle_dictionary: return WordleWords(filename="dictionary.hdf5")
with open("words_alpha.txt") as word_file:
all_words = word_file.read().split()
_wordle_dictionary.update(
filter(
lambda w: len(w) == WORDLE_LENGTH and w not in _invalid_words, all_words
)
)
return _wordle_dictionary
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/python
import random
from enum import Enum
import dictionary
class LetterResult(Enum):
"""Is an individual letter in a word in the result?"""
CORRECT = "C"
MOVE = "M"
INCORRECT = "I"
UNKNOWN = "U"
class InvalidWord(Exception):
"""Thrown when an input word is not a valid Wordle."""
pass
def select_word(words=dictionary.words()):
return random.choice(list(words))
def grade_word(target, guess, words=dictionary.words()):
# We don't check word length, since all valid words are 5 letters.
if guess not in words:
raise InvalidWord(f"{guess} is not a valid Wordle.")
result = [LetterResult.UNKNOWN] * dictionary.WORDLE_LENGTH
# We copy target, and remove letters if we find them. This handles duplicate letters correctly.
# For example, if the target is "pulls" and we guess "apple", only the first "p" should be a
# MOVE. The second should be INCORRECT.
remaining = list(target)
# Find all correct results first.
for i in range(dictionary.WORDLE_LENGTH):
if target[i] == guess[i]:
result[i] = LetterResult.CORRECT
remaining.remove(guess[i])
for i in range(dictionary.WORDLE_LENGTH):
if result[i] == LetterResult.UNKNOWN:
if guess[i] in remaining:
# Letter is in the wrong place.
result[i] = LetterResult.MOVE
remaining.remove(guess[i])
else:
# Letter is not in target.
result[i] = LetterResult.INCORRECT
return result
def display_result(result, text):
opening = {
LetterResult.CORRECT: "\033[92m",
LetterResult.MOVE: "\033[33m",
LetterResult.INCORRECT: "\033[36m",
LetterResult.UNKNOWN: "",
}
closing = "\033[0m"
return f"{opening[result]}{text}{closing}"
def success_result(result):
for r in result:
if r != LetterResult.CORRECT:
return False
return True
+12 -10
View File
@@ -1,6 +1,8 @@
#!/usr/bin/python #!/usr/bin/python
import wordle
import dictionary
import game
def display_instructions(): def display_instructions():
@@ -13,9 +15,9 @@ def display_instructions():
make up to six guesses to find out what it is. make up to six guesses to find out what it is.
After each guess, you get feedback on each letter of your guess. After each guess, you get feedback on each letter of your guess.
* {wordle.display_result(wordle.LetterResult.CORRECT, "Correct!")} * {game.display_result(game.LetterResult.CORRECT, "Correct!")}
* {wordle.display_result(wordle.LetterResult.MOVE, "Correct, in a different location.")} * {game.display_result(game.LetterResult.MOVE, "Correct, in a different location.")}
* {wordle.display_result(wordle.LetterResult.INCORRECT, "Not in the word.")} * {game.display_result(game.LetterResult.INCORRECT, "Not in the word.")}
""" """
) )
@@ -24,7 +26,7 @@ def read_valid_guess(guess_number):
while True: while True:
print(f"Guess {guess_number}: ", end="") print(f"Guess {guess_number}: ", end="")
guess = input() guess = input()
if guess in wordle.dictionary(): if guess in dictionary.words():
return guess return guess
print(f"{guess} is not a valid Wordle word. Please try again.") print(f"{guess} is not a valid Wordle word. Please try again.")
@@ -32,24 +34,24 @@ def read_valid_guess(guess_number):
def display_results(guess, results): def display_results(guess, results):
print(" ", end="") print(" ", end="")
for i in range(wordle.WORDLE_LENGTH): for i in range(dictionary.WORDLE_LENGTH):
print(wordle.display_result(results[i], guess[i]), end="") print(game.display_result(results[i], guess[i]), end="")
print("") print("")
def play_game(): def play_game():
display_instructions() display_instructions()
target = wordle.select_word() target = game.select_word()
# To help with debugging # To help with debugging
# print(f"The target is: {target}") # print(f"The target is: {target}")
for guess_number in range(1, 6): for guess_number in range(1, 6):
guess = read_valid_guess(guess_number) guess = read_valid_guess(guess_number)
results = wordle.grade_word(target, guess) results = game.grade_word(target, guess)
if wordle.success_result(results): if game.success_result(results):
print(f"You won! It took {guess_number} guesses.") print(f"You won! It took {guess_number} guesses.")
return return
BIN
View File
Binary file not shown.
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/python
import string
from collections import defaultdict
import dictionary
def analyze_dictionary(wordle_dictionary):
# Word Position, Letter: Count
position_matches = [
defaultdict(int),
defaultdict(int),
defaultdict(int),
defaultdict(int),
defaultdict(int),
]
# Letter: Count
word_matches = defaultdict(int)
for word in wordle_dictionary:
unique_letters = set(word)
for letter in unique_letters:
word_matches[letter] += 1
for index in range(len(word)):
letter = word[index]
assert letter in string.ascii_lowercase
position_matches[index][letter] += 1
return position_matches, word_matches
def score_word(position_matches, word_matches, word):
result = 0
# Green Score
for index in range(len(word)):
letter = word[index]
green_score = position_matches[index][letter]
result += 10 * green_score
# Yellow Score
for letter in set(word):
yellow_score = word_matches[letter]
result += yellow_score
return result
def scored_words(wordle_dictionary=dictionary.words()):
# word: score
scored_words = dict()
position_matches, word_matches = analyze_dictionary(wordle_dictionary)
for word in wordle_dictionary:
scored_words[word] = score_word(position_matches, word_matches, word)
result = list(wordle_dictionary)
result.sort(key=lambda word: scored_words[word], reverse=True)
return [(word, scored_words[word]) for word in result]
if __name__ == "__main__":
consts = constraints.Constraints()
consts.update_graded(
"cares",
[
solver.LetterResult.INCORRECT,
solver.LetterResult.INCORRECT,
solver.LetterResult.INCORRECT,
solver.LetterResult.INCORRECT,
solver.LetterResult.INCORRECT,
],
)
valid_words = consts.filter_words(dictionary.words())
print("Top: ", solver.scored_words(valid_words)[:5])
+1 -1
View File
@@ -3,7 +3,7 @@
import unittest import unittest
import constraints import constraints
from wordle import LetterResult from game import LetterResult
class TestConstraints(unittest.TestCase): class TestConstraints(unittest.TestCase):
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/python
import unittest
import dictionary
import game
class TestGame(unittest.TestCase):
test_words = (
"aaaaa",
"aabbb",
"aaccc",
"bbbaa",
"bbbbb",
"ccaaa",
"cccaa",
"ccccc",
"abcde",
)
def test_select_word(self):
word = game.select_word(self.test_words)
self.assertIn(word, self.test_words)
word = game.select_word()
self.assertIn(word, dictionary.words())
def test_grade_word_correct(self):
target = "aaaaa"
guess = "aaaaa"
result = game.grade_word(target, guess, self.test_words)
expected = [
game.LetterResult.CORRECT,
game.LetterResult.CORRECT,
game.LetterResult.CORRECT,
game.LetterResult.CORRECT,
game.LetterResult.CORRECT,
]
self.assertEqual(expected, result)
self.assertTrue(game.success_result(result))
def test_grade_word_mixed(self):
C = game.LetterResult.CORRECT
M = game.LetterResult.MOVE
I = game.LetterResult.INCORRECT
test_cases = (
("aabbb", "aabbb", [C, C, C, C, C]),
("aabbb", "aaccc", [C, C, I, I, I]),
("bbbaa", "cccaa", [I, I, I, C, C]),
("aabbb", "cccaa", [I, I, I, M, M]),
("aabbb", "bbbaa", [M, M, C, M, M]),
("aabbb", "ccaaa", [I, I, M, M, I]),
("aabbb", "abcde", [C, M, I, I, I]),
("aabbb", "ccccc", [I, I, I, I, I]),
)
for target, guess, expected in test_cases:
result = game.grade_word(target, guess, self.test_words)
self.assertEqual(expected, result)
def test_grade_word_invalid(self):
target = "aaaaa"
guess = "inval"
with self.assertRaises(Exception):
game.grade_word(target, guess, self.test_words)
if __name__ == "__main__":
unittest.main()
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/python
import unittest
import play
class TestPlay(unittest.TestCase):
def test_noop(self):
"""Currently no tests for this code."""
if __name__ == "__main__":
unittest.main()
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/python
import unittest
import solver
class TestSolver(unittest.TestCase):
def test_scored_words(self):
"""Minimal end to end test."""
result = solver.scored_words()
expected = [
("cares", 139631),
("sanes", 139129),
("tares", 138755),
("bares", 138375),
("sales", 137491),
]
self.assertListEqual(result[:5], expected)
if __name__ == "__main__":
unittest.main()
-155
View File
@@ -1,155 +0,0 @@
#!/usr/bin/python
import random
import string
from collections import defaultdict
from enum import Enum
import constraints
import dictionary
class LetterResult(Enum):
"""Is an individual letter in a word in the result?"""
CORRECT = "C"
MOVE = "M"
INCORRECT = "I"
UNKNOWN = "U"
class InvalidWord(Exception):
"""Thrown when an input word is not a valid Wordle."""
pass
def select_word():
return random.choice(list(dictionary.words()))
def grade_word(target, guess):
# We don't check word length, since all valid words are 5 letters.
if guess not in dictionary.words():
raise InvalidWord(f"{guess} is not a valid Wordle.")
result = [LetterResult.UNKNOWN] * dictionary.WORDLE_LENGTH
# We copy target, and remove letters if we find them. This handles duplicate letters correctly.
# For example, if the target is "pulls" and we guess "apple", only the first "p" should be a
# MOVE. The second should be INCORRECT.
remaining = list(target)
# Find all correct results first.
for i in range(dictionary.WORDLE_LENGTH):
if target[i] == guess[i]:
result[i] = LetterResult.CORRECT
remaining.remove(guess[i])
for i in range(dictionary.WORDLE_LENGTH):
if result[i] == LetterResult.UNKNOWN:
if guess[i] in remaining:
# Letter is in the wrong place.
result[i] = LetterResult.MOVE
remaining.remove(guess[i])
else:
# Letter is not in target.
result[i] = LetterResult.INCORRECT
return result
def display_result(result, text):
opening = {
LetterResult.CORRECT: "\033[92m",
LetterResult.MOVE: "\033[33m",
LetterResult.INCORRECT: "\033[36m",
LetterResult.UNKNOWN: "",
}
closing = "\033[0m"
return f"{opening[result]}{text}{closing}"
def success_result(result):
for r in result:
if r != LetterResult.CORRECT:
return False
return True
def analyze_dictionary(wordle_dictionary):
# Word Position, Letter: Count
position_matches = [
defaultdict(int),
defaultdict(int),
defaultdict(int),
defaultdict(int),
defaultdict(int),
]
# Letter: Count
word_matches = defaultdict(int)
for word in wordle_dictionary:
unique_letters = set(word)
for letter in unique_letters:
word_matches[letter] += 1
for index in range(len(word)):
letter = word[index]
assert letter in string.ascii_lowercase
position_matches[index][letter] += 1
return position_matches, word_matches
def score_word(position_matches, word_matches, word):
result = 0
# Green Score
for index in range(len(word)):
letter = word[index]
green_score = position_matches[index][letter]
result += 10 * green_score
# Yellow Score
for letter in set(word):
yellow_score = word_matches[letter]
result += yellow_score
return result
def scored_words(wordle_dictionary=dictionary.words()):
# word: score
scored_words = dict()
position_matches, word_matches = analyze_dictionary(wordle_dictionary)
for word in wordle_dictionary:
scored_words[word] = score_word(position_matches, word_matches, word)
result = list(wordle_dictionary)
result.sort(key=lambda word: scored_words[word], reverse=True)
return [(word, scored_words[word]) for word in result]
if __name__ == "__main__":
consts = constraints.Constraints()
consts.update_graded(
"cares",
[
LetterResult.INCORRECT,
LetterResult.INCORRECT,
LetterResult.INCORRECT,
LetterResult.INCORRECT,
LetterResult.INCORRECT,
],
)
valid_words = consts.filter_words(dictionary.words())
print("Top: ", scored_words(valid_words)[:5])