|
""" This script recommends word choices for the word-game wordle, which can be |
|
played at this URL: https://www.nytimes.com/games/wordle/index.html . |
|
|
|
Edit the arguments to the make_wordle_guess function with the outputs from your |
|
recent guesses on Wordle, to see your recommended guesses (NOTE: the |
|
character-positions use zero-indexing). |
|
|
|
If you don't like the suggested guesses, add them to BAD_WORDS_LIST, and they |
|
will no longer be recommended. """ |
|
|
|
from word_list import ( |
|
WORD_LIST, |
|
BAD_WORDS_LIST, |
|
WORDLE_POSSIBILITES, |
|
WORDLE_ALLOWED_GUESSES, |
|
) |
|
|
|
def main(): |
|
""" Main function for the script. Call make_wordle_guess with the specified |
|
arguments. """ |
|
make_wordle_guess_any( |
|
good_char_good_pos_set=str_to_letter_set("t0a1y4"), |
|
good_char_bad_pos_set=str_to_letter_set("n3"), |
|
bad_char_str="riseclouw", |
|
) |
|
|
|
def str_to_letter_set(s): |
|
""" Convert an appropriate string to a set of Letter objects. The string |
|
should alternate alphabetic characters and numeric characters. The number |
|
following each character refers to the position that number is in. Note |
|
that the position should use zero-indexing. |
|
|
|
For example, str_to_letter_set("p0e1r2") == {Letter("p", 0), Letter("e", |
|
1), Letter("r", 2), } """ |
|
char_str = s[0::2] |
|
pos_str = s[1::2] |
|
letter_set = { |
|
Letter(char, int(pos)) |
|
for char, pos in zip(char_str, pos_str) |
|
} |
|
return letter_set |
|
|
|
class Letter: |
|
def __init__(self, char, pos): |
|
self.char = char |
|
self.pos = pos |
|
|
|
def __repr__(self): |
|
return "Letter(\"%s\", %i)" % (self.char, self.pos) |
|
|
|
def __hash__(self): |
|
return hash((self.char, self.pos)) |
|
|
|
def __eq__(self, other): |
|
return (self.char == other.char) and (self.pos == other.pos) |
|
|
|
def assess_guess(true_word, guess_word_list): |
|
""" Given the true word that is trying to be guessed, and a list of guesses |
|
for that word, return the set of correct characters in the correct |
|
positions (as Letter objects), the set of correct characters in the |
|
incorrect positions (as Letter objects), and a string of incorrect |
|
characters """ |
|
good_char_good_pos_set = set( |
|
Letter(char, i) |
|
for guess_word in guess_word_list |
|
for i, char in enumerate(guess_word) |
|
if true_word[i] == char |
|
) |
|
good_char_bad_pos_set = set( |
|
Letter(char, i) |
|
for guess_word in guess_word_list |
|
for i, char in enumerate(guess_word) |
|
if (char in true_word) |
|
and (true_word[i] != char) |
|
) |
|
bad_char_str = "".join( |
|
char |
|
for guess_word in guess_word_list |
|
for char in guess_word |
|
if char not in true_word |
|
) |
|
|
|
return good_char_good_pos_set, good_char_bad_pos_set, bad_char_str |
|
|
|
def make_wordle_guess_restricted( |
|
good_char_good_pos_set=None, |
|
good_char_bad_pos_set=None, |
|
bad_char_str=None, |
|
word_length=5, |
|
word_list=WORDLE_POSSIBILITES, |
|
): |
|
""" Calculate the approximately best guess to make in a game of Wordle, |
|
given the information in the input arguments (see docstring to assess_guess |
|
function). |
|
|
|
This function only allows guessing words which are possible answers. """ |
|
|
|
# Find the sets of valid words and characters |
|
n_length_word_set = set( |
|
word |
|
for word in word_list |
|
if len(word) == word_length |
|
and word not in BAD_WORDS_LIST |
|
) |
|
valid_word_set = get_valid_word_set( |
|
n_length_word_set, |
|
word_length, |
|
good_char_good_pos_set, |
|
good_char_bad_pos_set, |
|
bad_char_str, |
|
) |
|
if len(valid_word_set) == 1: |
|
return valid_word_set.pop() |
|
elif len(valid_word_set) == 0: |
|
print("No valid words, returning empty string") |
|
return "" |
|
|
|
char_set = set(char for word in valid_word_set for char in word) |
|
|
|
# Find the dictionaries mapping characters and words to scores |
|
char_score_dict = { |
|
char: sum((1 if char in word else 0) for word in valid_word_set) |
|
for char in char_set |
|
} |
|
|
|
word_score_dict = { |
|
word: sum(char_score_dict[char] for char in set(word)) |
|
for word in valid_word_set |
|
} |
|
|
|
# Find the words with the best score |
|
best_score = max(word_score_dict.values()) |
|
best_word_list = [ |
|
word for word in valid_word_set |
|
if word_score_dict[word] == best_score |
|
] |
|
print("Best scoring words: %s\n" % ", ".join(sorted(best_word_list))) |
|
|
|
return sorted(best_word_list)[0] |
|
|
|
def make_wordle_guess_any( |
|
good_char_good_pos_set=None, |
|
good_char_bad_pos_set=None, |
|
bad_char_str=None, |
|
word_length=5, |
|
known_char_score=0, |
|
valid_word_bonus=10, |
|
word_list=WORDLE_POSSIBILITES, |
|
): |
|
""" Calculate the approximately best guess to make in a game of Wordle, |
|
given the information in the input arguments (see docstring to assess_guess |
|
function). |
|
|
|
This function allows guesses from any real word of the right length, even |
|
if it is not a possible answer. """ |
|
|
|
# Find the sets of valid words and known and scoring characters |
|
n_length_word_set = set( |
|
word |
|
for word in word_list |
|
if len(word) == word_length |
|
and word not in BAD_WORDS_LIST |
|
) |
|
valid_word_set = get_valid_word_set( |
|
n_length_word_set, |
|
word_length, |
|
good_char_good_pos_set, |
|
good_char_bad_pos_set, |
|
bad_char_str, |
|
) |
|
if len(valid_word_set) == 1: |
|
return valid_word_set.pop() |
|
elif len(valid_word_set) == 0: |
|
print("No valid words, returning empty string") |
|
return "" |
|
|
|
f = lambda x: x if x is not None else [] |
|
known_char_set = set( |
|
letter.char |
|
for s in [f(good_char_good_pos_set), f(good_char_bad_pos_set)] |
|
for letter in s |
|
) |
|
scoring_char_set = set( |
|
char |
|
for word in valid_word_set |
|
for char in word |
|
) |
|
|
|
# Find the dictionaries mapping characters and words to scores |
|
char_score_dict = { |
|
char: ( |
|
sum((1 if char in word else 0) for word in valid_word_set) |
|
if char not in known_char_set |
|
else known_char_score |
|
) |
|
for char in scoring_char_set |
|
} |
|
|
|
if valid_word_bonus is None: |
|
valid_word_bonus = word_length |
|
|
|
word_score_dict = { |
|
word: ( |
|
sum(char_score_dict.get(char, 0) for char in set(word)) |
|
+ (valid_word_bonus if word in valid_word_set else 0) |
|
) |
|
for word in n_length_word_set |
|
} |
|
|
|
# Find the words with the best score |
|
best_score = max(word_score_dict.values()) |
|
best_word_list = [ |
|
word for word in n_length_word_set |
|
if word_score_dict[word] == best_score |
|
] |
|
print("Best scoring words: %s\n" % ", ".join(sorted(best_word_list))) |
|
|
|
return sorted(best_word_list)[0] |
|
|
|
def get_valid_word_set( |
|
word_set, |
|
word_length, |
|
good_char_good_pos_set=None, |
|
good_char_bad_pos_set=None, |
|
bad_char_str=None, |
|
): |
|
""" Given a set of words, filter it such that it only contains words with |
|
correct characters in the correct positions and not in incorrect positions, |
|
and no words with incorrect characters """ |
|
valid_word_set = word_set |
|
|
|
if good_char_good_pos_set is not None: |
|
valid_word_set = set( |
|
word for word in valid_word_set |
|
if all( |
|
word[letter.pos] == letter.char |
|
for letter in good_char_good_pos_set |
|
) |
|
) |
|
|
|
if good_char_bad_pos_set is not None: |
|
valid_word_set = set( |
|
word for word in valid_word_set |
|
if all( |
|
letter.char in word |
|
and word[letter.pos] != letter.char |
|
for letter in good_char_bad_pos_set |
|
) |
|
) |
|
|
|
if bad_char_str is not None: |
|
valid_word_set = set( |
|
word for word in valid_word_set |
|
if all( |
|
char not in word |
|
for char in bad_char_str |
|
) |
|
) |
|
|
|
print( |
|
"Number of valid %i-letter words = %i" |
|
% (word_length, len(valid_word_set)) |
|
) |
|
if len(valid_word_set) < 40: |
|
print("Valid word list: %s" % ", ".join(sorted(valid_word_set))) |
|
|
|
return valid_word_set |
|
|
|
def simulate_game( |
|
true_word, |
|
initial_guess_list=None, |
|
word_guesser=make_wordle_guess_any, |
|
max_guesses=6, |
|
): |
|
""" Given a true word to guess, and a function which makes guesses (EG |
|
make_wordle_guess_any or make_wordle_guess_restricted), simulate a game of |
|
wordle, which ends when the correct word is guessed, or the same word is |
|
guessed twice """ |
|
if initial_guess_list is not None: |
|
guess_list = initial_guess_list |
|
else: |
|
guess_list = [] |
|
|
|
while len(guess_list) < max_guesses: |
|
new_guess = word_guesser(*assess_guess(true_word, guess_list)) |
|
if new_guess in guess_list: |
|
print_with_emphasis( |
|
"Failure: a word has been guessed multiple times: %s" |
|
% " -> ".join(guess_list) |
|
) |
|
return |
|
guess_list.append(new_guess) |
|
if true_word in guess_list: |
|
print_with_emphasis( |
|
"Success! True word %r guessed in %i guesses: %s" |
|
% (true_word, len(guess_list), " -> ".join(guess_list)) |
|
) |
|
return |
|
|
|
print_with_emphasis( |
|
"Failure: the word %r has not been guessed after %i attempts: %s" |
|
% (true_word, len(guess_list), " -> ".join(guess_list)) |
|
) |
|
|
|
|
|
def print_with_emphasis(s): |
|
""" Print a string with extra emphasis (newlines, asterisks, etc) """ |
|
line_break = "*" * len(s) |
|
print("\n%s\n%s\n%s\n" % (line_break, s, line_break)) |
|
|
|
|
|
if __name__ == "__main__": |
|
# make_wordle_guess_any(*assess_guess("tangy", [])) |
|
# make_wordle_guess_any(*assess_guess("tangy", ["raise"])) |
|
# make_wordle_guess_any(*assess_guess("tangy", ["raise", "monty"])) |
|
# print(*assess_guess("tangy", ["raise", "monty"]), sep="\n") |
|
# simulate_game("drink") |
|
# simulate_game("abbey") |
|
# simulate_game("tangy") |
|
# simulate_game("favor") |
|
# simulate_game("query") |
|
# simulate_game("panic") |
|
# simulate_game("break") |
|
# simulate_game("chair") |
|
# simulate_game("panic", ["raise"]) |
|
# simulate_game("favor", ["raise"]) |
|
# simulate_game("favor", ["raise", "clout"]) |
|
# simulate_game("favor", word_guesser=make_wordle_guess_restricted) |
|
main() |