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 = (
"Wordle
"
"Guess the 5-letter word in 6 tries!
"
"Commands:"
""
f"{p}wordle — Start today's daily puzzle "
f"{p}wordle <word> — Submit a guess "
f"{p}wordle stats — View your statistics "
f"{p}wordle hard — Toggle hard mode "
f"{p}wordle share — Share your last result "
f"{p}wordle give up — Forfeit current game "
"
"
"How to play:"
""
'- G '
"Green = correct letter, correct position
"
'- Y '
"Yellow = correct letter, wrong position
"
'- X '
"Gray = letter not in the word
"
"
"
"Hard mode: You must use all revealed hints in subsequent guesses.
"
"Everyone gets the same daily word!
"
)
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"Wordle {game.daily_number}{mode} — "
f"Guess {len(game.guesses) + 1}/6
"
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 to guess."
)
html = (
f''
f"Wordle #{puzzle_number}{mode_str}
"
f"Guess a 5-letter word! You have 6 attempts.
"
f"Type {BOT_PREFIX}wordle <word> to guess."
f"
{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
_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"
f"{grid_plain}\n\n"
f"Use {BOT_PREFIX}wordle stats for your statistics "
f"or {BOT_PREFIX}wordle share to share!"
)
grid_html = render_grid_html(game)
mode = "*" if game.hard_mode else ""
html = (
f''
f"{congrats} "
f"Wordle {game.daily_number} {num}/6{mode}
"
f"{grid_html}
"
f"Use {BOT_PREFIX}wordle stats for statistics "
f"or {BOT_PREFIX}wordle share to share!"
)
await send_html(client, room_id, plain, html)
return
# Check loss (6 guesses used)
if len(game.guesses) >= 6:
game.finished = True
game.won = False
_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"Wordle {game.daily_number} X/6{mode}
"
f"{grid_html}
"
f'The word was: '
f"{game.target}
"
f"Better luck tomorrow!"
)
await send_html(client, room_id, plain, html)
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"Wordle {game.daily_number}{mode} — "
f"Guess {len(game.guesses) + 1}/6
"
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
_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"Game Over
"
f"{grid_html}
"
f'The word was: '
f"{game.target}
"
f"Better luck tomorrow!"
)
await send_html(client, room_id, plain, html)
# ---------------------------------------------------------------------------
# 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 ""
# 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)
# Notify the public room if we're redirecting to a DM
if dm_room != room_id:
await send_text(client, room_id, "\U0001f4ec Check your DMs to play Wordle!")
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()