Refactor, breaking code into modules.
This commit is contained in:
1 parent
2f14532ef1
commit
0639113205
11 files changed
+312
-178
No files matched your search
+1
-1
@@ -3,7 +3,7 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import dictionary
|
||||
from wordle import LetterResult
|
||||
from game import LetterResult
|
||||
|
||||
|
||||
class Constraints:
|
||||
|
||||
+22
-11
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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
@@ -3,7 +3,7 @@
|
||||
import unittest
|
||||
|
||||
import constraints
|
||||
from wordle import LetterResult
|
||||
from game import LetterResult
|
||||
|
||||
|
||||
class TestConstraints(unittest.TestCase):
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
Reference in new issue
Block a user