From cce785e9754719df0060eb481dad9943493517a2 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 28 Jun 2025 08:26:32 +0200 Subject: [PATCH] Wordle bot. --- bot_snek_wordle.py | 173 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 bot_snek_wordle.py diff --git a/bot_snek_wordle.py b/bot_snek_wordle.py new file mode 100644 index 0000000..ddfcb14 --- /dev/null +++ b/bot_snek_wordle.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import asyncio +import random +import re +from pathlib import Path +from typing import Dict, List + +from snekbot.bot import Bot + +from ai import AI + + +WORD_LIST_PATH = Path(__file__).with_suffix(".wordlist") +if WORD_LIST_PATH.exists(): + with WORD_LIST_PATH.open() as _f: + DICTIONARY: List[str] = [w.strip().lower() for w in _f if len(w.strip()) == 5] +else: + DICTIONARY: List[str] = [] + + +def squares(secret: str, guess: str) -> str: + """Return 🟩🟨⬜ pattern for *guess* vs *secret*.""" + res = ["⬜"] * 5 + s_left = list(secret) + g_left = list(guess) + + for i, (g, s) in enumerate(zip(g_left, s_left)): + if g == s: + res[i] = "🟩" + s_left[i] = None + g_left[i] = None + + for i, g in enumerate(g_left): + if g and g in s_left: + res[i] = "🟨" + s_left[s_left.index(g)] = None + return "".join(res) + + +def _try_ai(system_prompt: str, **kwargs): + try: + kwargs["use_cache"] = False + return AI(timeout=4).prompt(system_prompt, **kwargs) + except Exception: + return None + + +def determine_action(msg: str) -> Dict[str, str]: + sys = ( + "You are WordleBot's intent detector.\n" + "Reply ONLY with JSON: {'action':'new_game'} | {'action':'guess','guess':'xxxxx'} | {'action':'unknown'}.\n" + "If the user clearly explictely wants a new game choose new_game, else extract the first 5‑letter word as guess. Chose unknown for anything else.\n" + f"Message: {msg}" + ) + out = _try_ai(sys, json=True) + if isinstance(out, dict) and out.get("action") != "unknown": + print(msg, out) + return out + + if re.search(r"\b(new game|reset|restart|start)\b", msg.lower()): + return {"action": "new_game"} + m = re.search(r"\b[a-z]{5}\b", msg.lower()) + return {"action": "guess", "guess": m.group(0) if m else ""} + + +def say(ctx: str) -> str: + sys = ( + "You are WordleBot, an upbeat, game‑show‑style host! Use emojis and exclamation marks, max 140 chars.\n" + "Respond ONLY with JSON: {'text':'...'}\n" + f"Context: {ctx}" + ) + out = _try_ai(sys, json=True) + if isinstance(out, dict) and "text" in out: + return out["text"] + return ctx + + +def imagine_word() -> str: + with open("wordle.wordlist", "r") as f: + words = [word.strip() for word in f.read().split("\n") if word.strip()] + return random.choice(words) + + +class WordleBot(Bot): + MAX_TRIES = 6 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.games: Dict[str, Dict] = {} + + def _start_game(self, chan: str): + self.games[chan] = {"word": imagine_word(), "guesses": {}, "lost": set()} + + def _add_guess(self, chan: str, user: str, word: str): + self.games[chan]["guesses"].setdefault(user, []).append(word) + + def _log_state(self, chan: str): + g = self.games.get(chan) + if not g: + print(f"[CHAN {chan}] no active game") + return + parts = ( + [f"word={g['word']}"] + + [f"{u}:{v}" for u, v in g["guesses"].items()] + + [f"lost:{g['lost']}"] + ) + print(f"[CHAN {chan}] " + " | ".join(parts)) + + async def on_mention( + self, username: str, user_nick: str | None, channel_uid: str, message: str + ): + intent = determine_action(message) + + if intent["action"] == "new_game": + self._start_game(channel_uid) + self._log_state(channel_uid) + return await self.send_message( + channel_uid, + say( + "🎉 New round! A fresh 5‑letter mystery awaits – fire your first shot!" + ), + ) + + guess = intent.get("guess", "").lower() + if len(guess) != 5 or not guess.isalpha(): + self._log_state(channel_uid) + return await self.send_message( + channel_uid, say("⛔ Oops! I need a real 5‑letter word – try again!") + ) + + if channel_uid not in self.games: + self._start_game(channel_uid) + game = self.games[channel_uid] + + self._add_guess(channel_uid, username, guess) + patt = squares(game["word"], guess) + used = len(game["guesses"][username]) + left = self.MAX_TRIES - used + + if guess == game["word"]: + msg = say( + f"🏆 {guess.upper()} {patt} – spotlight on {user_nick or username}! Fancy another round?" + ) + del self.games[channel_uid] + self._log_state(channel_uid) + return await self.send_message(channel_uid, msg) + + if left == 0: + game["lost"].add(username) + everyone = set(game["guesses"].keys()) + if game["lost"] == everyone and everyone: + ans = game["word"].upper() + msg = say( + f"🛑 All challengers down! The word was {ans}. Shall we spin up a new puzzle?" + ) + del self.games[channel_uid] + self._log_state(channel_uid) + return await self.send_message(channel_uid, msg) + bust_msg = say( + f"💀 {guess.upper()} {patt}. You're out of turns – cheer on the rest!" + ) + self._log_state(channel_uid) + return await self.send_message(channel_uid, bust_msg) + + feedback = f"{patt} ({left} left)" + self._log_state(channel_uid) + return await self.send_message(channel_uid, feedback) + + +if __name__ == "__main__": + bot = WordleBot(username="wordlebot", password="thetopsecretpassword") + asyncio.run(bot.run())