174 lines
5.6 KiB
Python
174 lines
5.6 KiB
Python
|
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())
|