Skip to content

Instantly share code, notes, and snippets.

@agrif
Created May 31, 2025 15:13
Show Gist options
  • Save agrif/03f761767c1f1b722f7438058a08aa32 to your computer and use it in GitHub Desktop.
Save agrif/03f761767c1f1b722f7438058a08aa32 to your computer and use it in GitHub Desktop.
#!/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