Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
"""Wordle game for Matrix bot.
|
|
|
|
|
|
|
|
|
|
Full implementation with daily puzzles, statistics tracking,
|
|
|
|
|
hard mode, shareable results, and rich HTML rendering.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import time
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from datetime import date
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from nio import AsyncClient
|
|
|
|
|
|
|
|
|
|
from utils import send_text, send_html, get_or_create_dm
|
|
|
|
|
from config import BOT_PREFIX
|
|
|
|
|
|
|
|
|
|
from wordlist_answers import ANSWERS
|
|
|
|
|
from wordlist_valid import VALID_GUESSES
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("matrixbot")
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Constants
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_WORDLE_EPOCH = date(2021, 6, 19)
|
|
|
|
|
|
|
|
|
|
# Build lookup sets at import time
|
|
|
|
|
_ANSWER_LIST = [w.upper() for w in ANSWERS]
|
|
|
|
|
_VALID_SET = frozenset(w.upper() for w in VALID_GUESSES) | frozenset(_ANSWER_LIST)
|
|
|
|
|
|
|
|
|
|
# Tile colors (Wordle official palette)
|
|
|
|
|
_TILE = {
|
|
|
|
|
2: {"bg": "#538d4e", "label": "correct"}, # Green
|
|
|
|
|
1: {"bg": "#b59f3b", "label": "present"}, # Yellow
|
|
|
|
|
0: {"bg": "#3a3a3c", "label": "absent"}, # Gray
|
|
|
|
|
}
|
|
|
|
|
_EMPTY_BG = "#121213"
|
|
|
|
|
_EMPTY_BORDER = "#3a3a3c"
|
|
|
|
|
|
|
|
|
|
# Emoji squares for plain-text fallback & share
|
|
|
|
|
_EMOJI = {2: "\U0001f7e9", 1: "\U0001f7e8", 0: "\u2b1b"}
|
|
|
|
|
|
|
|
|
|
# Keyboard layout
|
|
|
|
|
_KB_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
|
|
|
|
|
|
|
|
|
|
# Stats file
|
|
|
|
|
STATS_FILE = Path("wordle_stats.json")
|
|
|
|
|
|
|
|
|
|
# Congratulations messages by guess number
|
|
|
|
|
_CONGRATS = {
|
|
|
|
|
1: "Genius!",
|
|
|
|
|
2: "Magnificent!",
|
|
|
|
|
3: "Impressive!",
|
|
|
|
|
4: "Splendid!",
|
|
|
|
|
5: "Great!",
|
|
|
|
|
6: "Phew!",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Data structures
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class WordleGame:
|
|
|
|
|
player_id: str
|
|
|
|
|
room_id: str
|
|
|
|
|
target: str
|
|
|
|
|
guesses: list = field(default_factory=list)
|
|
|
|
|
results: list = field(default_factory=list)
|
|
|
|
|
hard_mode: bool = False
|
|
|
|
|
daily_number: int = 0
|
|
|
|
|
started_at: float = field(default_factory=time.time)
|
|
|
|
|
finished: bool = False
|
|
|
|
|
won: bool = False
|
|
|
|
|
origin_room_id: str = "" # Public room where game was started (for share)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Module-level state
|
|
|
|
|
_active_games: dict[str, WordleGame] = {}
|
|
|
|
|
_all_stats: dict[str, dict] = {}
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Stats persistence
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _load_stats():
|
|
|
|
|
global _all_stats
|
|
|
|
|
if STATS_FILE.exists():
|
|
|
|
|
try:
|
|
|
|
|
_all_stats = json.loads(STATS_FILE.read_text())
|
|
|
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
|
|
|
logger.error("Failed to load wordle stats: %s", e)
|
|
|
|
|
_all_stats = {}
|
|
|
|
|
else:
|
|
|
|
|
_all_stats = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save_stats():
|
|
|
|
|
try:
|
|
|
|
|
tmp = STATS_FILE.with_suffix(".tmp")
|
|
|
|
|
tmp.write_text(json.dumps(_all_stats, indent=2))
|
|
|
|
|
tmp.rename(STATS_FILE)
|
|
|
|
|
except OSError as e:
|
|
|
|
|
logger.error("Failed to save wordle stats: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_player_stats(player_id: str) -> dict:
|
|
|
|
|
if player_id not in _all_stats:
|
|
|
|
|
_all_stats[player_id] = {
|
|
|
|
|
"games_played": 0,
|
|
|
|
|
"games_won": 0,
|
|
|
|
|
"current_streak": 0,
|
|
|
|
|
"max_streak": 0,
|
|
|
|
|
"guess_distribution": {str(i): 0 for i in range(1, 7)},
|
|
|
|
|
"last_daily": -1,
|
|
|
|
|
"hard_mode": False,
|
|
|
|
|
"last_daily_result": None,
|
|
|
|
|
"last_daily_guesses": None,
|
|
|
|
|
}
|
|
|
|
|
return _all_stats[player_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _record_game_result(player_id: str, game: WordleGame):
|
|
|
|
|
stats = _get_player_stats(player_id)
|
|
|
|
|
stats["games_played"] += 1
|
|
|
|
|
|
|
|
|
|
if game.won:
|
|
|
|
|
stats["games_won"] += 1
|
|
|
|
|
stats["current_streak"] += 1
|
|
|
|
|
stats["max_streak"] = max(stats["max_streak"], stats["current_streak"])
|
|
|
|
|
num_guesses = str(len(game.guesses))
|
|
|
|
|
stats["guess_distribution"][num_guesses] = (
|
|
|
|
|
stats["guess_distribution"].get(num_guesses, 0) + 1
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
stats["current_streak"] = 0
|
|
|
|
|
|
|
|
|
|
stats["last_daily"] = game.daily_number
|
|
|
|
|
stats["last_daily_result"] = game.results
|
|
|
|
|
stats["last_daily_guesses"] = game.guesses
|
|
|
|
|
stats["last_daily_won"] = game.won
|
|
|
|
|
stats["last_daily_hard"] = game.hard_mode
|
|
|
|
|
stats["last_origin_room"] = game.origin_room_id
|
|
|
|
|
|
|
|
|
|
_save_stats()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Core algorithms
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def get_daily_word() -> tuple[str, int]:
|
|
|
|
|
"""Return (word, puzzle_number) for today's daily puzzle."""
|
|
|
|
|
today = date.today()
|
|
|
|
|
puzzle_number = (today - _WORDLE_EPOCH).days
|
|
|
|
|
word = _ANSWER_LIST[puzzle_number % len(_ANSWER_LIST)]
|
|
|
|
|
return word, puzzle_number
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def evaluate_guess(guess: str, target: str) -> list[int]:
|
|
|
|
|
"""Evaluate a guess against the target. Returns list of 5 scores:
|
|
|
|
|
2 = correct position (green), 1 = wrong position (yellow), 0 = absent (gray).
|
|
|
|
|
Handles duplicate letters correctly with a two-pass approach.
|
|
|
|
|
"""
|
|
|
|
|
result = [0] * 5
|
|
|
|
|
target_remaining = list(target)
|
|
|
|
|
|
|
|
|
|
# Pass 1: mark exact matches (green)
|
|
|
|
|
for i in range(5):
|
|
|
|
|
if guess[i] == target[i]:
|
|
|
|
|
result[i] = 2
|
|
|
|
|
target_remaining[i] = None
|
|
|
|
|
|
|
|
|
|
# Pass 2: mark present-but-wrong-position (yellow)
|
|
|
|
|
for i in range(5):
|
|
|
|
|
if result[i] == 2:
|
|
|
|
|
continue
|
|
|
|
|
if guess[i] in target_remaining:
|
|
|
|
|
result[i] = 1
|
|
|
|
|
target_remaining[target_remaining.index(guess[i])] = None
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_hard_mode(
|
|
|
|
|
guess: str,
|
|
|
|
|
previous_guesses: list[str],
|
|
|
|
|
previous_results: list[list[int]],
|
|
|
|
|
) -> str | None:
|
|
|
|
|
"""Return None if valid, or an error message if hard mode violated."""
|
|
|
|
|
for prev_guess, prev_result in zip(previous_guesses, previous_results):
|
|
|
|
|
for i, (letter, score) in enumerate(zip(prev_guess, prev_result)):
|
|
|
|
|
if score == 2 and guess[i] != letter:
|
|
|
|
|
return (
|
|
|
|
|
f"Hard mode: position {i + 1} must be "
|
|
|
|
|
f"'{letter}' (green from previous guess)"
|
|
|
|
|
)
|
|
|
|
|
if score == 1 and letter not in guess:
|
|
|
|
|
return (
|
|
|
|
|
f"Hard mode: guess must contain "
|
|
|
|
|
f"'{letter}' (yellow from previous guess)"
|
|
|
|
|
)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# HTML rendering
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _tile_span(letter: str, bg: str) -> str:
|
|
|
|
|
"""Render a single letter tile using Matrix-compatible attributes."""
|
|
|
|
|
return (
|
|
|
|
|
f'<font data-mx-bg-color="{bg}" data-mx-color="#ffffff">'
|
|
|
|
|
f"<b>\u00a0{letter}\u00a0</b></font>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_grid_html(game: WordleGame) -> str:
|
|
|
|
|
"""Render the Wordle grid as inline spans (compatible with Cinny)."""
|
|
|
|
|
rows = []
|
|
|
|
|
for row_idx in range(6):
|
|
|
|
|
tiles = []
|
|
|
|
|
if row_idx < len(game.guesses):
|
|
|
|
|
guess = game.guesses[row_idx]
|
|
|
|
|
result = game.results[row_idx]
|
|
|
|
|
for letter, score in zip(guess, result):
|
|
|
|
|
bg = _TILE[score]["bg"]
|
|
|
|
|
tiles.append(_tile_span(letter, bg))
|
|
|
|
|
else:
|
|
|
|
|
for _ in range(5):
|
|
|
|
|
tiles.append(_tile_span("\u00a0", _EMPTY_BG))
|
|
|
|
|
rows.append("".join(tiles))
|
|
|
|
|
|
|
|
|
|
return "<br>".join(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_keyboard_html(game: WordleGame) -> str:
|
|
|
|
|
"""Render a virtual keyboard showing letter states."""
|
|
|
|
|
letter_states: dict[str, int] = {}
|
|
|
|
|
for guess, result in zip(game.guesses, game.results):
|
|
|
|
|
for letter, score in zip(guess, result):
|
|
|
|
|
letter_states[letter] = max(letter_states.get(letter, -1), score)
|
|
|
|
|
|
|
|
|
|
kb_rows = []
|
|
|
|
|
for row in _KB_ROWS:
|
|
|
|
|
keys = []
|
|
|
|
|
for letter in row:
|
|
|
|
|
state = letter_states.get(letter, -1)
|
|
|
|
|
if state == -1:
|
|
|
|
|
bg, color = "#818384", "#ffffff"
|
|
|
|
|
elif state == 0:
|
|
|
|
|
bg, color = "#3a3a3c", "#555555"
|
|
|
|
|
elif state == 1:
|
|
|
|
|
bg, color = "#b59f3b", "#ffffff"
|
|
|
|
|
else:
|
|
|
|
|
bg, color = "#538d4e", "#ffffff"
|
|
|
|
|
keys.append(
|
|
|
|
|
f'<font data-mx-bg-color="{bg}" data-mx-color="{color}">'
|
|
|
|
|
f"{letter}</font>"
|
|
|
|
|
)
|
|
|
|
|
kb_rows.append(" ".join(keys))
|
|
|
|
|
|
|
|
|
|
return "<br>" + "<br>".join(kb_rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_grid_plain(game: WordleGame) -> str:
|
|
|
|
|
"""Plain text grid with emoji squares and letter markers."""
|
|
|
|
|
_marker = {2: "!", 1: "?", 0: "."} # ! = correct, ? = wrong spot, . = absent
|
|
|
|
|
lines = []
|
|
|
|
|
for guess, result in zip(game.guesses, game.results):
|
|
|
|
|
emoji_row = "".join(_EMOJI[s] for s in result)
|
|
|
|
|
# Show each letter with a marker: [C!] = correct, [R?] = wrong spot, [A.] = absent
|
|
|
|
|
marked = " ".join(f"{letter}{_marker[score]}" for letter, score in zip(guess, result))
|
|
|
|
|
lines.append(f"{emoji_row} {marked}")
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_keyboard_plain(game: WordleGame) -> str:
|
|
|
|
|
"""Plain text keyboard status."""
|
|
|
|
|
letter_states: dict[str, int] = {}
|
|
|
|
|
for guess, result in zip(game.guesses, game.results):
|
|
|
|
|
for letter, score in zip(guess, result):
|
|
|
|
|
letter_states[letter] = max(letter_states.get(letter, -1), score)
|
|
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
|
symbols = {-1: " ", 0: "\u2717", 1: "?", 2: "\u2713"}
|
|
|
|
|
for row in _KB_ROWS:
|
|
|
|
|
chars = []
|
|
|
|
|
for letter in row:
|
|
|
|
|
state = letter_states.get(letter, -1)
|
|
|
|
|
if state == 0:
|
|
|
|
|
chars.append("\u00b7") # dimmed
|
|
|
|
|
elif state >= 1:
|
|
|
|
|
chars.append(letter)
|
|
|
|
|
else:
|
|
|
|
|
chars.append(letter.lower())
|
|
|
|
|
lines.append(" ".join(chars))
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_stats_html(stats: dict) -> str:
|
|
|
|
|
"""Render player statistics as HTML (Matrix-compatible)."""
|
|
|
|
|
played = stats["games_played"]
|
|
|
|
|
won = stats["games_won"]
|
|
|
|
|
win_pct = (won / max(played, 1)) * 100
|
|
|
|
|
streak = stats["current_streak"]
|
|
|
|
|
max_streak = stats["max_streak"]
|
|
|
|
|
dist = stats["guess_distribution"]
|
|
|
|
|
max_count = max((int(v) for v in dist.values()), default=1) or 1
|
|
|
|
|
|
|
|
|
|
html = "<strong>Wordle Statistics</strong><br><br>"
|
|
|
|
|
html += (
|
|
|
|
|
f"<b>{played}</b> Played | "
|
|
|
|
|
f"<b>{win_pct:.0f}%</b> Win | "
|
|
|
|
|
f"<b>{streak}</b> Streak | "
|
|
|
|
|
f"<b>{max_streak}</b> Best<br><br>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
html += "<strong>Guess Distribution</strong><br>"
|
|
|
|
|
for i in range(1, 7):
|
|
|
|
|
count = int(dist.get(str(i), 0))
|
|
|
|
|
is_max = count == max_count and count > 0
|
|
|
|
|
bar_len = max(int((count / max_count) * 10), 1) if max_count > 0 else 1
|
|
|
|
|
bar = "\u2588" * bar_len # Block character for bar
|
|
|
|
|
if is_max and count > 0:
|
|
|
|
|
html += f'{i} <font data-mx-bg-color="#538d4e" data-mx-color="#ffffff"> {bar} {count} </font><br>'
|
|
|
|
|
else:
|
|
|
|
|
html += f'{i} <font data-mx-bg-color="#3a3a3c" data-mx-color="#ffffff"> {bar} {count} </font><br>'
|
|
|
|
|
return html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_stats_plain(stats: dict) -> str:
|
|
|
|
|
"""Plain text stats."""
|
|
|
|
|
played = stats["games_played"]
|
|
|
|
|
won = stats["games_won"]
|
|
|
|
|
win_pct = (won / max(played, 1)) * 100
|
|
|
|
|
streak = stats["current_streak"]
|
|
|
|
|
max_streak = stats["max_streak"]
|
|
|
|
|
dist = stats["guess_distribution"]
|
|
|
|
|
max_count = max((int(v) for v in dist.values()), default=1) or 1
|
|
|
|
|
|
|
|
|
|
lines = [
|
|
|
|
|
"Wordle Statistics",
|
|
|
|
|
f"Played: {played} | Win: {win_pct:.0f}% | Streak: {streak} | Max: {max_streak}",
|
|
|
|
|
"",
|
|
|
|
|
"Guess Distribution:",
|
|
|
|
|
]
|
|
|
|
|
for i in range(1, 7):
|
|
|
|
|
count = int(dist.get(str(i), 0))
|
|
|
|
|
bar_len = max(round((count / max_count) * 16), 1) if count > 0 else 0
|
|
|
|
|
bar = "\u2588" * bar_len
|
|
|
|
|
lines.append(f" {i}: {bar} {count}")
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_share(stats: dict) -> str:
|
|
|
|
|
"""Generate the shareable emoji grid from last completed daily."""
|
|
|
|
|
results = stats.get("last_daily_result")
|
|
|
|
|
if not results:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
won = stats.get("last_daily_won", False)
|
|
|
|
|
hard = stats.get("last_daily_hard", False)
|
|
|
|
|
daily_num = stats.get("last_daily", 0)
|
|
|
|
|
score = str(len(results)) if won else "X"
|
|
|
|
|
mode = "*" if hard else ""
|
|
|
|
|
|
|
|
|
|
header = f"Wordle {daily_num} {score}/6{mode}\n\n"
|
|
|
|
|
rows = []
|
|
|
|
|
for result in results:
|
|
|
|
|
rows.append("".join(_EMOJI[s] for s in result))
|
|
|
|
|
return header + "\n".join(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Subcommand handlers
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
async def wordle_help(client: AsyncClient, room_id: str):
|
|
|
|
|
"""Show help text with rules and commands."""
|
|
|
|
|
p = BOT_PREFIX
|
|
|
|
|
plain = (
|
|
|
|
|
f"Wordle - Guess the 5-letter word in 6 tries!\n\n"
|
|
|
|
|
f"Commands:\n"
|
|
|
|
|
f" {p}wordle Start today's daily puzzle (or show current game)\n"
|
|
|
|
|
f" {p}wordle <word> Submit a 5-letter guess\n"
|
|
|
|
|
f" {p}wordle stats View your statistics\n"
|
|
|
|
|
f" {p}wordle hard Toggle hard mode\n"
|
|
|
|
|
f" {p}wordle share Share your last daily result\n"
|
|
|
|
|
f" {p}wordle give up Forfeit current game\n"
|
|
|
|
|
f" {p}wordle help Show this help\n\n"
|
|
|
|
|
f"Rules:\n"
|
|
|
|
|
f" - Each guess must be a valid 5-letter English word\n"
|
|
|
|
|
f" - Green = correct letter, correct position\n"
|
|
|
|
|
f" - Yellow = correct letter, wrong position\n"
|
|
|
|
|
f" - Gray = letter not in the word\n"
|
|
|
|
|
f" - Hard mode: must use all revealed hints in subsequent guesses\n"
|
|
|
|
|
f" - Everyone gets the same daily word!"
|
|
|
|
|
)
|
|
|
|
|
html = (
|
|
|
|
|
"<h4>Wordle</h4>"
|
|
|
|
|
"<p>Guess the 5-letter word in 6 tries!</p>"
|
|
|
|
|
"<strong>Commands:</strong>"
|
|
|
|
|
"<ul>"
|
|
|
|
|
f"<li><code>{p}wordle</code> — Start today's daily puzzle</li>"
|
|
|
|
|
f"<li><code>{p}wordle <word></code> — Submit a guess</li>"
|
|
|
|
|
f"<li><code>{p}wordle stats</code> — View your statistics</li>"
|
|
|
|
|
f"<li><code>{p}wordle hard</code> — Toggle hard mode</li>"
|
|
|
|
|
f"<li><code>{p}wordle share</code> — Share your last result</li>"
|
|
|
|
|
f"<li><code>{p}wordle give up</code> — Forfeit current game</li>"
|
|
|
|
|
"</ul>"
|
|
|
|
|
"<strong>How to play:</strong>"
|
|
|
|
|
"<ul>"
|
|
|
|
|
'<li><font data-mx-bg-color="#538d4e" data-mx-color="#ffffff"><b> G </b></font> '
|
|
|
|
|
"Green = correct letter, correct position</li>"
|
|
|
|
|
'<li><font data-mx-bg-color="#b59f3b" data-mx-color="#ffffff"><b> Y </b></font> '
|
|
|
|
|
"Yellow = correct letter, wrong position</li>"
|
|
|
|
|
'<li><font data-mx-bg-color="#3a3a3c" data-mx-color="#ffffff"><b> X </b></font> '
|
|
|
|
|
"Gray = letter not in the word</li>"
|
|
|
|
|
"</ul>"
|
|
|
|
|
"<p><em>Hard mode:</em> You must use all revealed hints in subsequent guesses.</p>"
|
|
|
|
|
"<p>Everyone gets the same daily word!</p>"
|
|
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def wordle_start_or_status(client: AsyncClient, room_id: str, sender: str, origin_room_id: str = ""):
|
|
|
|
|
"""Start a new daily game or show current game status."""
|
|
|
|
|
# Check for active game
|
|
|
|
|
if sender in _active_games:
|
|
|
|
|
game = _active_games[sender]
|
|
|
|
|
if not game.finished:
|
|
|
|
|
guesses_left = 6 - len(game.guesses)
|
|
|
|
|
grid_plain = render_grid_plain(game)
|
|
|
|
|
kb_plain = render_keyboard_plain(game)
|
|
|
|
|
plain = (
|
|
|
|
|
f"Wordle {game.daily_number} — "
|
|
|
|
|
f"Guess {len(game.guesses) + 1}/6\n\n"
|
|
|
|
|
f"{grid_plain}\n\n{kb_plain}"
|
|
|
|
|
)
|
|
|
|
|
grid_html = render_grid_html(game)
|
|
|
|
|
kb_html = render_keyboard_html(game)
|
|
|
|
|
mode = " (Hard Mode)" if game.hard_mode else ""
|
|
|
|
|
html = (
|
|
|
|
|
f''
|
|
|
|
|
f"<strong>Wordle {game.daily_number}{mode}</strong> — "
|
|
|
|
|
f"Guess {len(game.guesses) + 1}/6<br><br>"
|
|
|
|
|
f"{grid_html}{kb_html}"
|
|
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Check if already completed today's puzzle
|
|
|
|
|
word, puzzle_number = get_daily_word()
|
|
|
|
|
stats = _get_player_stats(sender)
|
|
|
|
|
|
|
|
|
|
if stats["last_daily"] == puzzle_number:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"You already completed today's Wordle (#{puzzle_number})! "
|
|
|
|
|
f"Use {BOT_PREFIX}wordle stats to see your results "
|
|
|
|
|
f"or {BOT_PREFIX}wordle share to share them."
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Start new game
|
|
|
|
|
hard_mode = stats.get("hard_mode", False)
|
|
|
|
|
game = WordleGame(
|
|
|
|
|
player_id=sender,
|
|
|
|
|
room_id=room_id,
|
|
|
|
|
target=word,
|
|
|
|
|
hard_mode=hard_mode,
|
|
|
|
|
daily_number=puzzle_number,
|
|
|
|
|
origin_room_id=origin_room_id or room_id,
|
|
|
|
|
)
|
|
|
|
|
_active_games[sender] = game
|
|
|
|
|
|
|
|
|
|
mode_str = " (Hard Mode)" if hard_mode else ""
|
|
|
|
|
grid_html = render_grid_html(game)
|
|
|
|
|
kb_html = render_keyboard_html(game)
|
|
|
|
|
plain = (
|
|
|
|
|
f"Wordle #{puzzle_number}{mode_str}\n"
|
|
|
|
|
f"Guess a 5-letter word! You have 6 attempts.\n"
|
|
|
|
|
f"Type {BOT_PREFIX}wordle <word> to guess."
|
|
|
|
|
)
|
|
|
|
|
html = (
|
|
|
|
|
f''
|
|
|
|
|
f"<strong>Wordle #{puzzle_number}{mode_str}</strong><br>"
|
|
|
|
|
f"Guess a 5-letter word! You have 6 attempts.<br>"
|
|
|
|
|
f"Type <code>{BOT_PREFIX}wordle <word></code> to guess."
|
|
|
|
|
f"<br><br>{grid_html}{kb_html}"
|
|
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def wordle_guess(
|
|
|
|
|
client: AsyncClient, room_id: str, sender: str, guess: str
|
|
|
|
|
):
|
|
|
|
|
"""Process a guess."""
|
|
|
|
|
if sender not in _active_games:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"No active game. Start one with {BOT_PREFIX}wordle"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
game = _active_games[sender]
|
|
|
|
|
if game.finished:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"Your game is already finished! "
|
|
|
|
|
f"Use {BOT_PREFIX}wordle to start a new daily puzzle."
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Validate word
|
|
|
|
|
if guess not in _VALID_SET:
|
|
|
|
|
await send_text(client, room_id, f"'{guess.lower()}' is not in the word list. Try again.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Hard mode validation
|
|
|
|
|
if game.hard_mode and game.guesses:
|
|
|
|
|
violation = validate_hard_mode(guess, game.guesses, game.results)
|
|
|
|
|
if violation:
|
|
|
|
|
await send_text(client, room_id, violation)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Evaluate
|
|
|
|
|
result = evaluate_guess(guess, game.target)
|
|
|
|
|
game.guesses.append(guess)
|
|
|
|
|
game.results.append(result)
|
|
|
|
|
|
|
|
|
|
# Check win
|
|
|
|
|
if all(s == 2 for s in result):
|
|
|
|
|
game.finished = True
|
|
|
|
|
game.won = True
|
2026-02-21 14:23:57 -05:00
|
|
|
origin = game.origin_room_id
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
_record_game_result(sender, game)
|
|
|
|
|
del _active_games[sender]
|
|
|
|
|
|
|
|
|
|
num = len(game.guesses)
|
|
|
|
|
congrats = _CONGRATS.get(num, "Nice!")
|
|
|
|
|
grid_plain = render_grid_plain(game)
|
|
|
|
|
plain = (
|
|
|
|
|
f"{congrats} Wordle {game.daily_number} {num}/6"
|
|
|
|
|
f"{'*' if game.hard_mode else ''}\n\n"
|
2026-02-21 14:23:57 -05:00
|
|
|
f"{grid_plain}"
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
)
|
|
|
|
|
grid_html = render_grid_html(game)
|
|
|
|
|
mode = "*" if game.hard_mode else ""
|
|
|
|
|
html = (
|
|
|
|
|
f''
|
|
|
|
|
f"<strong>{congrats}</strong> "
|
|
|
|
|
f"Wordle {game.daily_number} {num}/6{mode}<br><br>"
|
2026-02-21 14:23:57 -05:00
|
|
|
f"{grid_html}"
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
2026-02-21 14:23:57 -05:00
|
|
|
if origin and origin != room_id:
|
|
|
|
|
await wordle_share(client, origin, sender)
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Check loss (6 guesses used)
|
|
|
|
|
if len(game.guesses) >= 6:
|
|
|
|
|
game.finished = True
|
|
|
|
|
game.won = False
|
2026-02-21 14:23:57 -05:00
|
|
|
origin = game.origin_room_id
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
_record_game_result(sender, game)
|
|
|
|
|
del _active_games[sender]
|
|
|
|
|
|
|
|
|
|
grid_plain = render_grid_plain(game)
|
|
|
|
|
plain = (
|
|
|
|
|
f"Wordle {game.daily_number} X/6"
|
|
|
|
|
f"{'*' if game.hard_mode else ''}\n\n"
|
|
|
|
|
f"{grid_plain}\n\n"
|
|
|
|
|
f"The word was: {game.target}\n"
|
|
|
|
|
f"Better luck tomorrow!"
|
|
|
|
|
)
|
|
|
|
|
grid_html = render_grid_html(game)
|
|
|
|
|
mode = "*" if game.hard_mode else ""
|
|
|
|
|
html = (
|
|
|
|
|
f''
|
|
|
|
|
f"<strong>Wordle {game.daily_number} X/6{mode}</strong><br><br>"
|
|
|
|
|
f"{grid_html}<br>"
|
|
|
|
|
f'The word was: <font data-mx-color="#538d4e"><strong>'
|
|
|
|
|
f"{game.target}</strong></font><br>"
|
|
|
|
|
f"<em>Better luck tomorrow!</em>"
|
|
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
2026-02-21 14:23:57 -05:00
|
|
|
if origin and origin != room_id:
|
|
|
|
|
await wordle_share(client, origin, sender)
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Still playing — show grid + keyboard
|
|
|
|
|
guesses_left = 6 - len(game.guesses)
|
|
|
|
|
grid_plain = render_grid_plain(game)
|
|
|
|
|
kb_plain = render_keyboard_plain(game)
|
|
|
|
|
plain = (
|
|
|
|
|
f"Wordle {game.daily_number} — "
|
|
|
|
|
f"Guess {len(game.guesses) + 1}/6\n\n"
|
|
|
|
|
f"{grid_plain}\n\n{kb_plain}"
|
|
|
|
|
)
|
|
|
|
|
grid_html = render_grid_html(game)
|
|
|
|
|
kb_html = render_keyboard_html(game)
|
|
|
|
|
mode = " (Hard Mode)" if game.hard_mode else ""
|
|
|
|
|
html = (
|
|
|
|
|
f''
|
|
|
|
|
f"<strong>Wordle {game.daily_number}{mode}</strong> — "
|
|
|
|
|
f"Guess {len(game.guesses) + 1}/6<br><br>"
|
|
|
|
|
f"{grid_html}{kb_html}"
|
|
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def wordle_stats(client: AsyncClient, room_id: str, sender: str):
|
|
|
|
|
"""Show player statistics."""
|
|
|
|
|
stats = _get_player_stats(sender)
|
|
|
|
|
|
|
|
|
|
if stats["games_played"] == 0:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"No Wordle stats yet! Start a game with {BOT_PREFIX}wordle"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
plain = render_stats_plain(stats)
|
|
|
|
|
html = render_stats_html(stats)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def wordle_toggle_hard(client: AsyncClient, room_id: str, sender: str):
|
|
|
|
|
"""Toggle hard mode for the player."""
|
|
|
|
|
stats = _get_player_stats(sender)
|
|
|
|
|
new_mode = not stats.get("hard_mode", False)
|
|
|
|
|
stats["hard_mode"] = new_mode
|
|
|
|
|
_save_stats()
|
|
|
|
|
|
|
|
|
|
# Also update active game if one exists
|
|
|
|
|
if sender in _active_games:
|
|
|
|
|
game = _active_games[sender]
|
|
|
|
|
if not game.guesses:
|
|
|
|
|
# Only allow toggling before first guess
|
|
|
|
|
game.hard_mode = new_mode
|
|
|
|
|
elif new_mode:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
"Hard mode enabled for future games. "
|
|
|
|
|
"Cannot enable mid-game after guessing."
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
game.hard_mode = False
|
|
|
|
|
|
|
|
|
|
status = "enabled" if new_mode else "disabled"
|
|
|
|
|
plain = (
|
|
|
|
|
f"Hard mode {status}. "
|
|
|
|
|
+ ("You must use all revealed hints in subsequent guesses."
|
|
|
|
|
if new_mode else "Standard rules apply.")
|
|
|
|
|
)
|
|
|
|
|
await send_text(client, room_id, plain)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def wordle_share(client: AsyncClient, room_id: str, sender: str):
|
|
|
|
|
"""Share the last completed daily result."""
|
|
|
|
|
stats = _get_player_stats(sender)
|
|
|
|
|
share_text = generate_share(stats)
|
|
|
|
|
|
|
|
|
|
if not share_text:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"No completed daily puzzle to share. Play one with {BOT_PREFIX}wordle"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
await send_text(client, room_id, share_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def wordle_give_up(client: AsyncClient, room_id: str, sender: str):
|
|
|
|
|
"""Forfeit the current game."""
|
|
|
|
|
if sender not in _active_games:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"No active game to give up. Start one with {BOT_PREFIX}wordle"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
game = _active_games[sender]
|
|
|
|
|
if game.finished:
|
|
|
|
|
del _active_games[sender]
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
game.finished = True
|
|
|
|
|
game.won = False
|
2026-02-21 14:23:57 -05:00
|
|
|
origin = game.origin_room_id
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
_record_game_result(sender, game)
|
|
|
|
|
del _active_games[sender]
|
|
|
|
|
|
|
|
|
|
grid_plain = render_grid_plain(game)
|
|
|
|
|
plain = (
|
|
|
|
|
f"Game over! The word was: {game.target}\n\n"
|
|
|
|
|
f"{grid_plain}\n\n"
|
|
|
|
|
f"Better luck tomorrow!"
|
|
|
|
|
)
|
|
|
|
|
grid_html = render_grid_html(game)
|
|
|
|
|
html = (
|
|
|
|
|
f''
|
|
|
|
|
f"<strong>Game Over</strong><br><br>"
|
|
|
|
|
f"{grid_html}<br>"
|
|
|
|
|
f'The word was: <font data-mx-color="#538d4e"><strong>'
|
|
|
|
|
f"{game.target}</strong></font><br>"
|
|
|
|
|
f"<em>Better luck tomorrow!</em>"
|
|
|
|
|
)
|
|
|
|
|
await send_html(client, room_id, plain, html)
|
2026-02-21 14:23:57 -05:00
|
|
|
if origin and origin != room_id:
|
|
|
|
|
await wordle_share(client, origin, sender)
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Main router
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
async def _get_dm_room(client: AsyncClient, room_id: str, sender: str) -> tuple[str, str]:
|
|
|
|
|
"""Get or create DM room for the sender. Returns (dm_room_id, origin_room_id).
|
|
|
|
|
|
|
|
|
|
If already in a DM, returns (room_id, stored_origin or room_id).
|
|
|
|
|
If in a public room, creates/finds DM and returns (dm_room_id, room_id).
|
|
|
|
|
"""
|
|
|
|
|
# Check if this is already a DM (2 members)
|
|
|
|
|
room = client.rooms.get(room_id)
|
|
|
|
|
if room and room.member_count == 2:
|
|
|
|
|
# Already in DM — use origin from active game if available
|
|
|
|
|
game = _active_games.get(sender)
|
|
|
|
|
origin = game.origin_room_id if game and game.origin_room_id else room_id
|
|
|
|
|
return room_id, origin
|
|
|
|
|
|
|
|
|
|
# Public room — find/create DM
|
|
|
|
|
dm_room = await get_or_create_dm(client, sender)
|
|
|
|
|
if dm_room:
|
|
|
|
|
return dm_room, room_id
|
|
|
|
|
|
|
|
|
|
# Fallback to public room if DM creation fails
|
|
|
|
|
logger.warning("Could not create DM with %s, falling back to public room", sender)
|
|
|
|
|
return room_id, room_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def handle_wordle(
|
|
|
|
|
client: AsyncClient, room_id: str, sender: str, args: str
|
|
|
|
|
):
|
|
|
|
|
"""Main entry point — dispatches to subcommands."""
|
|
|
|
|
parts = args.strip().split(None, 1)
|
|
|
|
|
subcmd = parts[0].lower() if parts else ""
|
|
|
|
|
sub_args = parts[1] if len(parts) > 1 else ""
|
|
|
|
|
|
2026-02-20 10:39:11 -05:00
|
|
|
# Share always goes to the public room
|
|
|
|
|
if subcmd == "share":
|
|
|
|
|
await wordle_share(client, room_id, sender)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# All other commands route through DM
|
|
|
|
|
dm_room, origin = await _get_dm_room(client, room_id, sender)
|
|
|
|
|
|
2026-02-21 14:23:57 -05:00
|
|
|
# Silently redirect to DM — origin room will get an auto-share when the game ends
|
2026-02-20 10:39:11 -05:00
|
|
|
|
Add Wordle, welcome system, integrations, and update roadmap
- Add Wordle game engine with daily puzzles, hard mode, stats, and share
- Add welcome module (react-to-join onboarding, Space join DMs)
- Add Ollama LLM integration (!ask), Minecraft RCON whitelist (!minecraft)
- Add !trivia, !champion, !agent, !health commands
- Add DM routing for Wordle (games in DMs, share to public room)
- Update README: reflect Phase 4 completion, hookshot webhook setup,
infrastructure migration (LXC 151/109 to large1), Spam and Stuff room,
all 12 webhook connections with UUIDs and transform notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:29:36 -05:00
|
|
|
if subcmd == "help":
|
|
|
|
|
await wordle_help(client, dm_room)
|
|
|
|
|
elif subcmd == "stats":
|
|
|
|
|
await wordle_stats(client, dm_room, sender)
|
|
|
|
|
elif subcmd == "hard":
|
|
|
|
|
await wordle_toggle_hard(client, dm_room, sender)
|
|
|
|
|
elif subcmd == "give" and sub_args.lower().startswith("up"):
|
|
|
|
|
await wordle_give_up(client, dm_room, sender)
|
|
|
|
|
elif subcmd == "":
|
|
|
|
|
await wordle_start_or_status(client, dm_room, sender, origin)
|
|
|
|
|
elif len(subcmd) == 5 and subcmd.isalpha():
|
|
|
|
|
await wordle_guess(client, dm_room, sender, subcmd.upper())
|
|
|
|
|
else:
|
|
|
|
|
await send_text(
|
|
|
|
|
client, room_id,
|
|
|
|
|
f"Invalid wordle command or guess. "
|
|
|
|
|
f"Guesses must be exactly 5 letters. "
|
|
|
|
|
f"Try {BOT_PREFIX}wordle help"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Load stats on module import
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
_load_stats()
|