import logging import random import re from collections import deque from pathlib import Path import discord from discord.ext import commands logger = logging.getLogger("bcs.wordle") _PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts" _prompt_cache: dict[str, str] = {} def _load_prompt(filename: str) -> str: if filename not in _prompt_cache: _prompt_cache[filename] = (_PROMPTS_DIR / filename).read_text(encoding="utf-8") return _prompt_cache[filename] def _parse_wordle_embeds(message: discord.Message) -> dict | None: """Extract useful info from a Wordle bot message. Returns a dict with keys like 'type', 'summary', 'scores', 'streak', 'wordle_number' or None if this isn't a recognizable Wordle result message. """ if not message.embeds: return None full_text = "" wordle_number = None for embed in message.embeds: if embed.description: full_text += embed.description + "\n" if embed.title: full_text += embed.title + "\n" m = re.search(r"Wordle No\.\s*(\d+)", embed.title) if m: wordle_number = int(m.group(1)) if not full_text.strip(): return None # Detect result messages (contain score patterns like "3/6:") score_pattern = re.findall(r"(\d/6):\s*@?(.+?)(?:\n|$)", full_text) streak_match = re.search(r"(\d+)\s*day streak", full_text) if score_pattern: scores = [{"score": s[0], "player": s[1].strip()} for s in score_pattern] return { "type": "results", "wordle_number": wordle_number, "streak": int(streak_match.group(1)) if streak_match else None, "scores": scores, "summary": full_text.strip(), } # Detect "was playing" messages if "was playing" in full_text: return { "type": "playing", "wordle_number": wordle_number, "summary": full_text.strip(), } return None class WordleCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self._chat_history: dict[int, deque] = {} def _get_active_prompt(self) -> str: mode_config = self.bot.get_mode_config() prompt_file = mode_config.get("prompt_file", "chat_personality.txt") return _load_prompt(prompt_file) def _get_wordle_config(self) -> dict: return self.bot.config.get("wordle", {}) @commands.Cog.listener() async def on_message(self, message: discord.Message): if not message.author.bot: return if not message.guild: return config = self._get_wordle_config() if not config.get("enabled", False): return # Match the Wordle bot by name bot_name = config.get("bot_name", "Wordle") if message.author.name != bot_name: return parsed = _parse_wordle_embeds(message) if not parsed: return # Only comment on results, not "playing" notifications if parsed["type"] == "playing": reply_chance = config.get("playing_reply_chance", 0.0) if reply_chance <= 0 or random.random() > reply_chance: return else: reply_chance = config.get("reply_chance", 0.5) if random.random() > reply_chance: return # Build context for the LLM context_parts = [ f"[Wordle bot posted in #{message.channel.name}]", "[Wordle scoring: players guess a 5-letter word in up to 6 tries. " "LOWER is BETTER — 1/6 is a genius guess, 2/6 is incredible, 3/6 is great, " "4/6 is mediocre, 5/6 is rough, 6/6 barely scraped by, X/6 means they failed]", ] if parsed["type"] == "results": context_parts.append("[This is a Wordle results summary]") if parsed.get("streak"): context_parts.append(f"[Group streak: {parsed['streak']} days]") if parsed.get("wordle_number"): context_parts.append(f"[Wordle #{parsed['wordle_number']}]") for s in parsed.get("scores", []): context_parts.append(f"[{s['player']} scored {s['score']}]") # Identify the winner (lowest score = best) scores = parsed.get("scores", []) if scores: best = min(scores, key=lambda s: int(s["score"][0])) worst = max(scores, key=lambda s: int(s["score"][0])) if best != worst: context_parts.append( f"[{best['player']} won with {best['score']}, " f"{worst['player']} came last with {worst['score']}]" ) elif parsed["type"] == "playing": context_parts.append(f"[Someone is currently playing Wordle]") context_parts.append(f"[{parsed['summary']}]") prompt_context = "\n".join(context_parts) user_msg = ( f"{prompt_context}\n" f"React to this Wordle update with a short, fun comment. " f"Keep it to 1-2 sentences." ) ch_id = message.channel.id if ch_id not in self._chat_history: self._chat_history[ch_id] = deque(maxlen=6) self._chat_history[ch_id].append({"role": "user", "content": user_msg}) active_prompt = self._get_active_prompt() recent_bot_replies = [ m["content"][:150] for m in self._chat_history[ch_id] if m["role"] == "assistant" ][-3:] typing_ctx = None async def start_typing(): nonlocal typing_ctx typing_ctx = message.channel.typing() await typing_ctx.__aenter__() response = await self.bot.llm_chat.chat( list(self._chat_history[ch_id]), active_prompt, on_first_token=start_typing, recent_bot_replies=recent_bot_replies, ) if typing_ctx: await typing_ctx.__aexit__(None, None, None) # Strip leaked metadata brackets (same as chat.py) if response: segments = re.split(r"^\s*\[[^\]]*\]\s*$", response, flags=re.MULTILINE) segments = [s.strip() for s in segments if s.strip()] response = segments[-1] if segments else "" if not response: logger.warning("LLM returned no response for Wordle comment in #%s", message.channel.name) return self._chat_history[ch_id].append({"role": "assistant", "content": response}) await message.reply(response, mention_author=False) logger.info( "Wordle %s reply in #%s: %s", parsed["type"], message.channel.name, response[:100], ) async def setup(bot: commands.Bot): await bot.add_cog(WordleCog(bot))