Last active
October 25, 2024 09:32
-
-
Save hkurokawa/72d89f8edb83f90a662588d527e044b0 to your computer and use it in GitHub Desktop.
Wordle solver
This file contains hidden or 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 random | |
def is_alphabet(s): | |
return all('a' <= ch <= 'z' for ch in s.strip().lower()) | |
class Problem: | |
def __init__(self, answer=None): | |
if answer is not None: | |
self.answer = answer | |
return | |
with open("/usr/share/dict/words") as file: | |
self.answer = random.choice([s.strip().lower() for s in file if len(s) == 6 and is_alphabet(s)]) | |
def guess(self, word): | |
if len(word) != 5 or not is_alphabet(word): | |
raise ValueError(f"Unexpected guess: {word}") | |
return "".join( | |
['C' if word[i] == self.answer[i] else 'E' if word[i] in self.answer else '.' for i in range(len(word))]) | |
class Guesser: | |
def __init__(self): | |
with open("/usr/share/dict/words") as file: | |
self.possible_answers = [s.strip().lower() for s in file if len(s) == 6 and is_alphabet(s)] | |
self.possible_guesses = self.possible_answers.copy() | |
self.round = 0 | |
def think(self, guess, result): | |
"""Update possible answers based on the guess and the result. | |
""" | |
if result == "CCCCC": | |
return | |
self.possible_guesses.remove(guess) | |
for i in range(len(result)): | |
ch = guess[i] | |
res = result[i] | |
if res == ".": | |
self.possible_answers = list(filter(lambda word: ch not in word, self.possible_answers)) | |
elif res == "E": | |
self.possible_answers = list(filter(lambda word: ch in word and word[i] != ch, self.possible_answers)) | |
else: | |
self.possible_answers = list(filter(lambda word: word[i] == ch, self.possible_answers)) | |
def guess(self): | |
self.round += 1 | |
if self.round == 1: | |
return "oates" # Most efficient guess for the 1st time | |
if len(self.possible_answers) == 1: | |
return self.possible_answers[0] | |
def bonus(word): | |
return 1 if word in self.possible_answers else 0 | |
return max(self.possible_guesses, key=lambda guess: self.score(guess) + bonus(guess)) | |
def score(self, guess): | |
"""Score a guess from a point of view how much variety of hints (e.g., CCC..EE) it would produce. | |
""" | |
def hint(word, answer): | |
return "".join( | |
['C' if word[i] == answer[i] else 'E' if word[i] in answer else '.' for i in range(len(word))]) | |
# Compute frequency of hints a guess would produce | |
counter = collections.Counter() | |
for a in self.possible_answers: | |
counter[hint(guess, a)] += 1 | |
# Lower the score if many possible answers would produce the same hint. | |
# In other words, the score is how much likely the guess results in various hints. | |
# If we want to be more accurate here, we are able to compute how much this guess could reduce the entropy. | |
return len(self.possible_answers) - counter.most_common(1)[0][1] | |
class Round: | |
def __init__(self, answer=None): | |
self.problem = Problem(answer) | |
self.guesser = Guesser() | |
self.round = 0 | |
self.history = [] | |
def play_round(self): | |
self.round += 1 | |
word = self.guesser.guess() | |
result = self.problem.guess(word) | |
self.history.append((word, result)) | |
# print(f"Round {self.round}: Guess \"{word}\" -> {result}") | |
self.guesser.think(word, result) | |
if result == "CCCCC": | |
return True | |
else: | |
return False | |
def play(self): | |
while self.round < 6: | |
result = self.play_round() | |
if result: | |
return True, self.round | |
return False, self.round | |
def random_mode(): | |
N = 50 | |
num_solved = 0 | |
for _ in range(N): | |
r = Round() | |
(res, num_round) = r.play() | |
if res: | |
num_solved += 1 | |
print(f"Congratulations! You solved the problem in {num_round} round(s).") | |
else: | |
print(f"Failed to solve the problem. The answer was \"{r.problem.answer}\".") | |
print() | |
print(f"Number of correct answer: {num_solved} / {N}") | |
print(f"Correct answer rate: {num_solved * 100.0 / N}%") | |
def exhaustive_mode(file=None): | |
if file is None: | |
file = "all.txt" | |
with open(file) as f: | |
problems = [s.strip().lower() for s in f if len(s) == 6 and is_alphabet(s)] | |
num_solved = 0 | |
for p in problems: | |
r = Round(p) | |
(res, num_round) = r.play() | |
if res: | |
num_solved += 1 | |
else: | |
for h in r.history: | |
print(f"\"{h[0]}\" -> {h[1]}") | |
print(f"Answer was \"{p}\"") | |
print() | |
print(f"Number of correct answer: {num_solved} / {len(problems)}") | |
print(f"Correct answer rate: {num_solved * 100.0 / len(problems)}%") | |
def interactive_mode(): | |
guesser = Guesser() | |
for _ in range(6): | |
guess = guesser.guess() | |
print(f"Guess: {guess}") | |
result = input("Input the result in C/E/.: ").strip() | |
if result == "CCCCC": | |
break | |
guesser.think(guess, result) | |
if __name__ == '__main__': | |
# random_mode() | |
exhaustive_mode() | |
# interactive_mode() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment