Skip to content

Instantly share code, notes, and snippets.

@trptcolin
Last active January 14, 2022 19:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trptcolin/f208c19d10144c19045dd9541b693e4c to your computer and use it in GitHub Desktop.
Save trptcolin/f208c19d10144c19045dd9541b693e4c to your computer and use it in GitHub Desktop.
Just a bit of fun, not really the way I code at work πŸ˜‰
import collections
import math
DEFAULT_WORDLIST_PATH = "/usr/share/dict/words"
DEFAULT_WORD_LENGTH = 5
GREEN = "g"
YELLOW = "y"
BLANK = "_"
WORDLISTS = {}
class WordleSolver:
def __init__(self, path=DEFAULT_WORDLIST_PATH, length=DEFAULT_WORD_LENGTH):
self.word_length = length
self.wordlist_path = path
self.setup()
def setup(self):
self.known = [BLANK] * self.word_length
self.words = set()
# Only read the file once, not again over and over
if self.wordlist_path not in WORDLISTS:
wordlist = set()
with open(self.wordlist_path) as file:
for line in file:
word = line.strip().lower()
wordlist.add(word)
WORDLISTS[self.wordlist_path] = frozenset(wordlist)
for word in WORDLISTS[self.wordlist_path]:
if len(word) == self.word_length:
self.words.add(word)
def rule_out_if(self, f):
to_remove = set()
for word in self.words:
if f(word):
to_remove.add(word)
self.words.difference_update(to_remove)
# There might be a way to optimize this.
#
# It's currently at *least* O(len(guess) * len(self.words))
#
# However, since the length of guess is a constant (and a small one!), it's
# plenty fast for our purposes - no need to burn time on it.
def update_evaluation(self, guess, evaluation):
correct_letters = set()
# fill in greens first
for i, pair in enumerate(zip(guess, evaluation)):
guessed_letter, result = pair
if result == GREEN:
# rule out any word w/o this letter *in this slot*
self.rule_out_if(lambda word: word[i] != guessed_letter)
self.known[i] = guessed_letter
for i, pair in enumerate(zip(guess, evaluation)):
guessed_letter, result = pair
if result == GREEN:
# already done
pass
elif result == YELLOW:
# rule out any word w/o this letter (or with this letter, *at
# this spot*, since that'd be green) we *could* probably be a
# bit more aggressive here in pruning the search space by
# looking at counts
self.rule_out_if(
lambda word: guessed_letter not in set(word)
or word[i] == guessed_letter
)
else:
# rule out any word w/ this letter IF it's not already known to
# be in the word since we are scoring earlier letters first,
# this edge case works here, but real-Wordle may evaluate
# differently edge case: guess "truss", and there's only one S
# in the solution - the second S in the guess will both be
# marked blank, but we should *not* rule out the solution word
# which contains S
self.rule_out_if(
lambda word: word.count(guessed_letter)
>= guess.count(guessed_letter)
)
def make_guess(self):
if len(self.words) == 0:
raise RuntimeError("Could not find word")
elif len(self.words) == 1:
return self.words.pop()
guess_letters = []
remaining_words = self.words.copy()
for i in range(self.word_length):
if self.known[i] != BLANK:
# hard mode: always guess what we already know
guess_letters.append(self.known[i])
continue
counter = collections.Counter([word[i] for word in remaining_words])
_, best_count = counter.most_common(1)[0]
common_letters = [
letter for letter in counter if counter[letter] == best_count
]
chosen_letter = None
for letter in common_letters:
if letter not in guess_letters:
chosen_letter = letter
break
if chosen_letter is None:
chosen_letter = common_letters[0]
guess_letters.append(chosen_letter)
this_prefix = "".join(guess_letters)
remaining_words = {
word for word in remaining_words if word.startswith(this_prefix)
}
return "".join(guess_letters)
# Note that if a guess contains more of a given letter than the solution, the
# first one(s) gets priority to be marked yellow, and future letters are marked
# as misses (even though they're actually in the word)
def evaluate_guess(guess, solution):
solution_counts = collections.Counter(solution)
result = [BLANK for _ in range(len(guess))]
# fill in the green ones first
for i, guessed_letter in enumerate(guess):
if solution[i] == guessed_letter:
result[i] = GREEN
solution_counts[guessed_letter] -= 1
for i, guessed_letter in enumerate(guess):
if solution[i] == guessed_letter:
pass
elif solution_counts[guessed_letter] > 0:
result[i] = YELLOW
solution_counts[guessed_letter] -= 1
return "".join(result)
def solve(
solution,
wordlist_path=DEFAULT_WORDLIST_PATH,
length=DEFAULT_WORD_LENGTH,
max_guesses=10,
):
solver = WordleSolver(wordlist_path, length)
guesses = []
for i in range(max_guesses):
try:
guess = solver.make_guess()
except:
print(f"No dice!\nWas looking for '{solution}'")
print("Guesses:", guesses)
return (False, len(guesses))
guesses.append(guess)
evaluation = evaluate_guess(guess, solution)
if evaluation == GREEN * length:
print(f"Got '{guess}' after {len(guesses)} guesses: {guesses}")
return (True, len(guesses))
else:
solver.update_evaluation(guess, evaluation)
print(f"No dice!\nWas looking for '{solution}'")
print("Guesses:", guesses)
return (False, len(guesses))
if __name__ == "__main__":
solver = WordleSolver("/usr/share/dict/words", 5)
print(f"There are {len(solver.words)} possible words")
import unittest
import wordle
class TestWordle(unittest.TestCase):
def test_evaluate_guess(self):
self.assertEqual(wordle.evaluate_guess("zzzzz", "favor"), "_____")
self.assertEqual(wordle.evaluate_guess("stare", "favor"), "__yy_")
self.assertEqual(wordle.evaluate_guess("mound", "favor"), "_y___")
self.assertEqual(wordle.evaluate_guess("cargo", "favor"), "_gy_y")
self.assertEqual(wordle.evaluate_guess("razor", "favor"), "_g_gg")
self.assertEqual(wordle.evaluate_guess("vapor", "favor"), "yg_gg")
self.assertEqual(wordle.evaluate_guess("favor", "favor"), "ggggg")
def test_evaluate_guess_duplicate_letters_in_solution(self):
self.assertEqual(wordle.evaluate_guess("stare", "tenet"), "_y__y")
self.assertEqual(wordle.evaluate_guess("state", "tenet"), "_y_yy")
self.assertEqual(wordle.evaluate_guess("nonet", "tenet"), "__ggg")
self.assertEqual(wordle.evaluate_guess("nylon", "tenet"), "y____")
def test_update_evaluation(self):
solver = wordle.WordleSolver()
solver.update_evaluation("stare", "__yy_")
self.assertEqual("favor" in solver.words, True)
def test_update_evaluation_when_guessing_more_of_a_character_than_in_solution(self):
solver = wordle.WordleSolver()
solver.update_evaluation("nylon", "y____")
self.assertEqual("tenet" in solver.words, True)
def test_lots_of_words(self):
file_path = "./wordle_possible_solutions.txt"
# file_path = "./wordle_possible_guesses.txt"
# file_path = "/usr/share/dict/words"
with open(file_path) as file:
for line in file:
word = line.strip().lower()
if len(word) != 5:
continue
result, guess_count = wordle.solve(word, file_path, 5, 10)
self.assertTrue(result)
if __name__ == "__main__":
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment