Wordle bot.
This commit is contained in:
parent
29d64f93a4
commit
cce785e975
173
bot_snek_wordle.py
Normal file
173
bot_snek_wordle.py
Normal file
@ -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())
|
Loadingβ¦
Reference in New Issue
Block a user