-
-
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 π
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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