File size: 6,058 Bytes
ff59e62 b3e30db ff59e62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
import json, math, torch, gradio as gr
from collections import Counter
import numpy as np
from huggingface_hub import hf_hub_download
import torch.nn as nn
REPO_ID = "sato2ru/wordle-solver" # β update this
# ββ Load assets from HF Hub ββββββββββββββββββββββββββββββββββββββ
config = json.load(open(hf_hub_download(REPO_ID, "config.json")))
ANSWERS = json.load(open(hf_hub_download(REPO_ID, "answers.json")))
ALLOWED = json.load(open(hf_hub_download(REPO_ID, "allowed.json")))
WORD2IDX = {w: i for i, w in enumerate(ALLOWED)}
LETTERS = "abcdefghijklmnopqrstuvwxyz"
L2I = {c: i for i, c in enumerate(LETTERS)}
INPUT_DIM = config["input_dim"]
OUTPUT_DIM = config["output_dim"]
OPENING = config["opening_guess"]
WIN_PATTERN = (2,2,2,2,2)
# ββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class WordleNet(nn.Module):
def __init__(self):
super().__init__()
h = config["hidden"]
self.net = nn.Sequential(
nn.Linear(INPUT_DIM, h), nn.BatchNorm1d(h), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(h, h), nn.BatchNorm1d(h), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(h, 256), nn.BatchNorm1d(256), nn.ReLU(),
nn.Linear(256, OUTPUT_DIM)
)
def forward(self, x): return self.net(x)
model = WordleNet()
model.load_state_dict(torch.load(hf_hub_download(REPO_ID, "model_weights.pt"), map_location="cpu"))
model.eval()
# ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_pattern(guess, answer):
pattern = [0]*5
counts = Counter(answer)
for i in range(5):
if guess[i] == answer[i]: pattern[i] = 2; counts[guess[i]] -= 1
for i in range(5):
if pattern[i] == 0 and counts.get(guess[i],0) > 0:
pattern[i] = 1; counts[guess[i]] -= 1
return tuple(pattern)
def filter_words(words, guess, pattern):
return [w for w in words if get_pattern(guess, w) == pattern]
def entropy_score(guess, possible):
buckets = Counter(get_pattern(guess, w) for w in possible)
n = len(possible)
return sum(-(c/n)*math.log2(c/n) for c in buckets.values())
def encode_board(history):
vec = np.zeros(INPUT_DIM, dtype=np.float32)
for word, pattern in history:
for pos, (letter, state) in enumerate(zip(word, pattern)):
vec[L2I[letter]*15 + pos*3 + state] = 1.0
return vec
def model_suggest(history, possible):
if len(possible) == 1: return possible[0]
if not history: return OPENING
state = torch.tensor(encode_board(history)).unsqueeze(0)
with torch.no_grad():
logits = model(state)[0]
top5 = [ALLOWED[i] for i in logits.topk(5).indices.tolist()]
return max(top5, key=lambda w: entropy_score(w, possible))
# ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def init_state():
return {"possible": list(ANSWERS), "history": [], "done": False}
def render_board(history):
colours = {0: "β¬", 1: "π¨", 2: "π©"}
rows = []
for word, pattern in history:
tiles = " ".join(f"{colours[s]}{c.upper()}" for c, s in zip(word, pattern))
rows.append(tiles)
return "
".join(rows) if rows else "(no guesses yet)"
def process_guess(guess_input, pattern_input, state):
if state["done"]:
return render_board(state["history"]), "Game over β press Reset", state
guess = guess_input.strip().lower()
if len(guess) != 5:
return render_board(state["history"]), "β οΈ Guess must be 5 letters", state
if len(pattern_input) != 5 or not all(c in "012" for c in pattern_input):
return render_board(state["history"]), "β οΈ Pattern must be 5 digits (0/1/2)", state
pattern = tuple(int(c) for c in pattern_input)
state["history"].append((guess, pattern))
if pattern == WIN_PATTERN:
state["done"] = True
msg = f"π Solved in {len(state["history"])} turns!"
return render_board(state["history"]), msg, state
state["possible"] = filter_words(state["possible"], guess, pattern)
if not state["possible"]:
state["done"] = True
return render_board(state["history"]), "β No words left. Check your input.", state
suggestion = model_suggest(state["history"], state["possible"])
msg = f"Try: **{suggestion.upper()}** | {len(state["possible"])} words left"
return render_board(state["history"]), msg, state
def reset(_state):
s = init_state()
return render_board([]), f"Try: **{OPENING.upper()}** to start", s
# ββ Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Blocks(title="Wordle Solver", theme=gr.themes.Monochrome()) as demo:
gr.Markdown("# π© Wordle Solver
Entropy-trained neural network. Enter each guess + the colour pattern.")
gr.Markdown("**Pattern key:** `0` = β¬ grey Β· `1` = π¨ yellow Β· `2` = π© green")
state = gr.State(init_state())
board_out = gr.Textbox(label="Board", lines=7, interactive=False)
msg_out = gr.Markdown(f"Try: **{OPENING.upper()}** to start")
with gr.Row():
guess_in = gr.Textbox(label="Your guess", placeholder="crane", max_lines=1)
pattern_in = gr.Textbox(label="Pattern (5 digits)", placeholder="02100", max_lines=1)
with gr.Row():
submit_btn = gr.Button("Submit", variant="primary")
reset_btn = gr.Button("Reset")
submit_btn.click(process_guess, [guess_in, pattern_in, state], [board_out, msg_out, state])
reset_btn.click(reset, [state], [board_out, msg_out, state])
demo.launch()
|