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
|
from collections import defaultdict
|
||||||
|
|
||||||
import dictionary
|
import dictionary
|
||||||
from wordle import LetterResult
|
from game import LetterResult
|
||||||
|
|
||||||
|
|
||||||
class Constraints:
|
class Constraints:
|
||||||
|
|||||||
+22
-11
@@ -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
|
|
||||||
@@ -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
|
#!/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
|
||||||
|
|
||||||
|
|||||||
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 unittest
|
||||||
|
|
||||||
import constraints
|
import constraints
|
||||||
from wordle import LetterResult
|
from game import LetterResult
|
||||||
|
|
||||||
|
|
||||||
class TestConstraints(unittest.TestCase):
|
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