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
import dictionary
from wordle import LetterResult
from game import LetterResult
class Constraints:
+22 -11
View File
@@ -1,5 +1,7 @@
#!/usr/bin/python
import h5py
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():
if not _wordle_dictionary:
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
return WordleWords(filename="dictionary.hdf5")
+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
import wordle
import dictionary
import game
def display_instructions():
@@ -13,9 +15,9 @@ def display_instructions():
make up to six guesses to find out what it is.
After each guess, you get feedback on each letter of your guess.
* {wordle.display_result(wordle.LetterResult.CORRECT, "Correct!")}
* {wordle.display_result(wordle.LetterResult.MOVE, "Correct, in a different location.")}
* {wordle.display_result(wordle.LetterResult.INCORRECT, "Not in the word.")}
* {game.display_result(game.LetterResult.CORRECT, "Correct!")}
* {game.display_result(game.LetterResult.MOVE, "Correct, in a different location.")}
* {game.display_result(game.LetterResult.INCORRECT, "Not in the word.")}
"""
)
@@ -24,7 +26,7 @@ def read_valid_guess(guess_number):
while True:
print(f"Guess {guess_number}: ", end="")
guess = input()
if guess in wordle.dictionary():
if guess in dictionary.words():
return guess
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):
print(" ", end="")
for i in range(wordle.WORDLE_LENGTH):
print(wordle.display_result(results[i], guess[i]), end="")
for i in range(dictionary.WORDLE_LENGTH):
print(game.display_result(results[i], guess[i]), end="")
print("")
def play_game():
display_instructions()
target = wordle.select_word()
target = game.select_word()
# To help with debugging
# print(f"The target is: {target}")
for guess_number in range(1, 6):
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.")
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 constraints
from wordle import LetterResult
from game import LetterResult
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])