Created
May 31, 2025 15:13
-
-
Save agrif/03f761767c1f1b722f7438058a08aa32 to your computer and use it in GitHub Desktop.
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
#!/usr/bin/env python3 | |
# usage: ./wordlyzer.py [guess ...] answer | |
import enum | |
import math | |
import pathlib | |
import sys | |
import urllib.request | |
import numpy | |
# don't try to do expected average with a word list above this size | |
EXPECTED_AVERAGE_THRESHOLD = 500 | |
# shhhhh | |
WORD_LIST_URL = 'https://gist.github.com/cfreshman/a03ef2cba789d8cf00c08f767e0fad7b/raw/c46f451920d5cf6326d550fb2d6abb1642717852/wordle-answers-alphabetical.txt' | |
def load_words(): | |
local = pathlib.Path(__file__).parent / 'wordle-words.txt' | |
if local.is_file(): | |
with open(local) as f: | |
return f.readlines() | |
else: | |
with urllib.request.urlopen(WORD_LIST_URL) as f: | |
return [line.decode('utf-8') for line in f.readlines()] | |
def bits(collection): | |
try: | |
return math.log2(max(len(collection), 1)) | |
except TypeError: | |
return math.log2(max(collection, 1)) | |
RESET = '\x1b[0m' | |
class Color(enum.Enum): | |
GREY = enum.auto() | |
YELLOW = enum.auto() | |
GREEN = enum.auto() | |
def ansi(self): | |
if self == self.GREY: | |
return '' | |
elif self == self.YELLOW: | |
return '\x1b[33;1m' | |
elif self == self.GREEN: | |
return '\x1b[32;1m' | |
def erased(self): | |
if self == self.GREY: | |
return 'β¬' | |
elif self == self.YELLOW: | |
return 'π¨' | |
elif self == self.GREEN: | |
return 'π©' | |
class Result: | |
def __init__(self, old_words, words, guess, colors, expected_bits, average_bits, expected_average_bits): | |
self.old_words = old_words | |
self.words = words | |
self.guess = guess | |
self.colors = colors | |
self.expected_bits = expected_bits | |
self.average_bits = average_bits | |
self.expected_average_bits = expected_average_bits | |
@property | |
def bits(self): | |
return self.bits_before - self.bits_after | |
@property | |
def bits_before(self): | |
return bits(self.old_words) | |
@property | |
def bits_after(self): | |
return bits(self.words) | |
@property | |
def best_average_bits(self): | |
if self.expected_average_bits is None: | |
return self.average_bits | |
return self.expected_average_bits | |
@property | |
def erased(self): | |
return ''.join(c.erased() for c in self.colors) | |
def __repr__(self): | |
return f'Result(words[:{len(self.words)}], {self})' | |
def __str__(self): | |
s = '' | |
for a, c in zip(self.guess, self.colors): | |
s += c.ansi() + a + RESET | |
return s | |
def check_and_apply(words, guess, answer): | |
colors = [] | |
for (a, b) in zip(guess, answer): | |
if a == b: | |
colors.append(Color.GREEN) | |
elif a in answer: | |
colors.append(Color.YELLOW) | |
else: | |
colors.append(Color.GREY) | |
for i, (b, c) in enumerate(zip(guess, colors)): | |
if c == Color.GREY: | |
words = words[numpy.all(words != b, axis=1)] | |
elif c == Color.YELLOW: | |
words = words[words[:, i] != b] | |
words = words[numpy.any(words == b, axis=1)] | |
if c == Color.GREEN: | |
words = words[words[:, i] == b] | |
return colors, words | |
def analyze(words, guess, answer): | |
colors, new_words = check_and_apply(words, guess, answer) | |
expected_bits = [] | |
average_bits = [] | |
expected_average_bits = [] | |
do_exp_avg = len(words) < EXPECTED_AVERAGE_THRESHOLD | |
start_bits = bits(words) | |
for i, word in enumerate(words): | |
expected_bits.append(bits(check_and_apply(words, guess, word)[1])) | |
average_bits.append(bits(check_and_apply(words, word, answer)[1])) | |
if do_exp_avg: | |
for j, word2 in enumerate(words): | |
expected_average_bits.append(bits(check_and_apply(words, word, word2)[1])) | |
# guard against answers not in known word list | |
if not len(words): | |
expected_bits.append(start_bits) | |
average_bits.append(start_bits) | |
if do_exp_avg: | |
expected_average_bits.append(start_bits) | |
return Result( | |
words, | |
new_words, | |
guess, | |
colors, | |
start_bits - numpy.mean(expected_bits), | |
start_bits - numpy.mean(average_bits), | |
start_bits - numpy.mean(expected_average_bits) if do_exp_avg else None, | |
) | |
def main(guesses): | |
words = (word.strip().upper() for word in load_words()) | |
words = [word for word in words if word] | |
words = numpy.array(words).view('U1').reshape((-1, 5)) | |
if not guesses: | |
guesses = ['trace', 'sound', 'quasi', 'quash'] | |
guesses = [guess.upper() for guess in guesses] | |
answer = guesses[-1] | |
results = [] | |
print(' Words Bits Guess Change Exp Avg ExpAvg', file=sys.stderr) | |
for i, guess in enumerate(guesses): | |
r = analyze(words, guess, answer) | |
words = r.words | |
lucky = r.bits > r.expected_bits | |
smart = r.expected_bits > r.best_average_bits | |
judgement = ' '.join([ | |
'π' if lucky else 'β', | |
'π§ ' if smart else 'β', | |
]) | |
color = '\x1b[33m' | |
if r.bits < min(r.expected_bits, r.best_average_bits): | |
color = '\x1b[31m' | |
if r.bits > max(r.expected_bits, r.best_average_bits): | |
color = '\x1b[32m' | |
colorex = '\x1b[32m' if r.expected_bits >= r.best_average_bits else '\x1b[33m' | |
results.append((r, judgement)) | |
if r.expected_average_bits is not None: | |
exp_avg = f'{r.expected_average_bits: 5.1f}' | |
else: | |
exp_avg = ' ---' | |
print(f'[{i}] {len(r.old_words): 6} {r.bits_before: 5.1f} bits {r} {judgement} {color}{r.bits:+6.1f} bits{RESET} {colorex}{r.expected_bits: 5.1f}{RESET} {r.average_bits: 5.1f} {exp_avg}', file=sys.stderr) | |
print('', file=sys.stderr) | |
if len(guesses) > 6: | |
print('Wordle') | |
else: | |
print(f'Wordle {len(guesses)}/6') | |
for r, judgement in results[:6]: | |
def fmt_10(v): | |
x = 10 * v / r.bits_before | |
s = f'{x:.1f}' | |
if len(s) > 3: | |
s = f'{x:.0f}.' | |
return s | |
note = '--' | |
if r.bits_before: | |
change = fmt_10(r.bits) | |
expected = fmt_10(r.expected_bits) | |
avg = fmt_10(r.best_average_bits) | |
note = f'{change} / {expected} / {avg}' | |
print(f'{r.erased} {judgement} {note}') | |
if __name__ == '__main__': | |
main(sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment