From 49cc0b3d75b7e8758f1ea27eaa953eb810079d20 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 26 Apr 2026 15:40:51 -0400 Subject: [PATCH] hangman: add --hard and --extended flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --hard (-h): words 9-15 letters instead of 5-8 --extended (-e): 10 wrong guesses with full body (feet + ears stages) Flags are combinable: !hangman --hard --extended Board header shows mode emoji (🔥 hard, 💀 extended, 💀🔥 both). Wrong counter shows X/10 in extended mode. All guess logic reads max_wrong from game dict instead of hardcoded 6. Co-Authored-By: Claude Sonnet 4.6 --- matrixbot/commands.py | 96 +++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/matrixbot/commands.py b/matrixbot/commands.py index 7e5d7bc..066babe 100644 --- a/matrixbot/commands.py +++ b/matrixbot/commands.py @@ -1351,6 +1351,31 @@ _HANGMAN_STAGES = [ "```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n |\n=========```", ] +_HANGMAN_STAGES_EXTENDED = [ + # 0 wrong + "```\n +---+\n | |\n |\n |\n |\n |\n=========```", + # 1 wrong - head + "```\n +---+\n | |\n O |\n |\n |\n |\n=========```", + # 2 wrong - body + "```\n +---+\n | |\n O |\n | |\n |\n |\n=========```", + # 3 wrong - left arm + "```\n +---+\n | |\n O |\n /| |\n |\n |\n=========```", + # 4 wrong - right arm + "```\n +---+\n | |\n O |\n /|\\ |\n |\n |\n=========```", + # 5 wrong - left leg + "```\n +---+\n | |\n O |\n /|\\ |\n / |\n |\n=========```", + # 6 wrong - right leg + "```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n |\n=========```", + # 7 wrong - left foot + "```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n/ |\n=========```", + # 8 wrong - right foot + "```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n/ \\ |\n=========```", + # 9 wrong - left ear + "```\n +---+\n | |\n \\O |\n /|\\ |\n / \\ |\n/ \\ |\n=========```", + # 10 wrong (dead) - both ears + "```\n +---+\n | |\n \\O/ |\n /|\\ |\n / \\ |\n/ \\ |\n=========```", +] + def _hangman_display(game: dict) -> str: word = game["word"] @@ -1362,23 +1387,32 @@ def _hangman_board_html(game: dict, status_line: str = "") -> tuple[str, str]: """Return (plain, html) for the current hangman board state.""" word = game["word"] wrong_count = game["wrong_count"] + max_wrong = game.get("max_wrong", 6) + stages = _HANGMAN_STAGES_EXTENDED if game.get("extended") else _HANGMAN_STAGES display = _hangman_display(game) wrong_letters = sorted(ch for ch in game["guessed_letters"] if ch not in word) - stage_art = _HANGMAN_STAGES[wrong_count].replace("```", "") + stage_art = stages[min(wrong_count, len(stages) - 1)].replace("```", "") + mode_tag = "" + if game.get("hard") and game.get("extended"): + mode_tag = " 💀🔥" + elif game.get("hard"): + mode_tag = " 🔥" + elif game.get("extended"): + mode_tag = " 💀" plain = ( - f"🎯 Hangman!\n{stage_art}\n" + f"🎯 Hangman{mode_tag}!\n{stage_art}\n" f"Word: {display} ({len(word)} letters)\n" f"Hint: {game['hint']}\n" - f"Wrong ({wrong_count}/6): {', '.join(wrong_letters) or 'none'}" + f"Wrong ({wrong_count}/{max_wrong}): {', '.join(wrong_letters) or 'none'}" + (f"\n{status_line}" if status_line else "") ) html = ( - f'🎯 Hangman!
' + f'🎯 Hangman{mode_tag}!
' f'
{stage_art}
' f'Word: {display} ({len(word)} letters)
' f'Hint: {game["hint"]}
' - f'Wrong ({wrong_count}/6): {", ".join(wrong_letters) or "none"}' + f'Wrong ({wrong_count}/{max_wrong}): {", ".join(wrong_letters) or "none"}' + (f'
{status_line}' if status_line else "") ) return plain, html @@ -1406,7 +1440,7 @@ def _save_hangman_cache(words: list[str]) -> None: _hangman_recent: list[str] = _load_hangman_cache() -async def _generate_hangman_word() -> dict | None: +async def _generate_hangman_word(min_len: int = 5, max_len: int = 8) -> dict | None: avoid_clause = ( " Do NOT use any of these recently used words: " + ", ".join(f'"{w}"' for w in _hangman_recent[-20:]) @@ -1416,7 +1450,10 @@ async def _generate_hangman_word() -> dict | None: "You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. " 'Format: {"word": "example", "hint": "short category or hint"}' ) - user_msg = f"Pick a common English word between 5 and 8 letters (lowercase letters only, no hyphens or spaces) and give a short hint.{avoid_clause}" + user_msg = ( + f"Pick a common English word between {min_len} and {max_len} letters " + f"(lowercase letters only, no hyphens or spaces) and give a short hint.{avoid_clause}" + ) for attempt in range(2): try: timeout = aiohttp.ClientTimeout(total=60) @@ -1445,7 +1482,7 @@ async def _generate_hangman_word() -> dict | None: parsed = {} word = parsed.get("word", "").lower().strip() hint = parsed.get("hint", "").strip() - if word.isalpha() and 5 <= len(word) <= 8 and hint: + if word.isalpha() and min_len <= len(word) <= max_len and hint: _hangman_recent.append(word) if len(_hangman_recent) > _HANGMAN_RECENT_MAX: _hangman_recent.pop(0) @@ -1457,46 +1494,43 @@ async def _generate_hangman_word() -> dict | None: return None -@command("hangman", "Play hangman! AI picks a word, guess letters with !guess") +@command("hangman", "Play hangman — flags: --hard (long words), --extended (10 guesses + more body parts)") async def cmd_hangman(client: AsyncClient, room_id: str, sender: str, args: str): if room_id in _HANGMAN_GAMES: - game = _HANGMAN_GAMES[room_id] - display = _hangman_display(game) - wrong = game["wrong_count"] - guessed = sorted(game["guessed_letters"]) - wrong_letters = [ch for ch in guessed if ch not in game["word"]] - plain = ( - f"Hangman already in progress!\n" - f"{_HANGMAN_STAGES[wrong]}\n" - f"Word: {display}\n" - f"Hint: {game['hint']}\n" - f"Wrong guesses ({wrong}/6): {', '.join(wrong_letters) or 'none'}\n" - f"Use !guess or !guess " - ) - await send_text(client, room_id, plain) + plain, html = _hangman_board_html(_HANGMAN_GAMES[room_id], "Use !guess or !guess ") + await send_html(client, room_id, plain, html) return + # Parse flags + flags = args.lower().split() + hard = "--hard" in flags or "-h" in flags + extended = "--extended" in flags or "-e" in flags + max_wrong = 10 if extended else 6 + min_len, max_len = (9, 15) if hard else (5, 8) + await send_text(client, room_id, "🎯 Picking a word...") - word_data = await _generate_hangman_word() + word_data = await _generate_hangman_word(min_len=min_len, max_len=max_len) if word_data is None: await send_text(client, room_id, "Failed to generate a word. Try again later.") return word = word_data["word"] hint = word_data["hint"] - display = " ".join("_" for _ in word) game = { "word": word, "hint": hint, "guessed_letters": set(), "wrong_count": 0, + "max_wrong": max_wrong, + "hard": hard, + "extended": extended, "board_event_id": None, } _HANGMAN_GAMES[room_id] = game - plain, html = _hangman_board_html(game, "Guess with !guess — max 6 wrong guesses") + plain, html = _hangman_board_html(game, f"Guess with !guess — max {max_wrong} wrong guesses") resp = await send_html(client, room_id, plain, html) if hasattr(resp, "event_id"): game["board_event_id"] = resp.event_id @@ -1527,6 +1561,8 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str): else: await send_html(client, room_id, p, h) + max_wrong = game.get("max_wrong", 6) + # Full word guess if len(guess) > 1: winner = sender.split(":")[0].lstrip("@") @@ -1539,11 +1575,11 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str): ) else: game["wrong_count"] += 1 - if game["wrong_count"] >= 6: + if game["wrong_count"] >= max_wrong: del _HANGMAN_GAMES[room_id] await _update_board(f"💀 Wrong! Game over — the word was: {word.upper()}") else: - remaining = 6 - game["wrong_count"] + remaining = max_wrong - game["wrong_count"] await _update_board(f"❌ '{guess.upper()}' is wrong! {remaining} guesses remaining.") return @@ -1566,11 +1602,11 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str): game["wrong_count"] += 1 wrong_count = game["wrong_count"] - if wrong_count >= 6: + if wrong_count >= max_wrong: del _HANGMAN_GAMES[room_id] await _update_board(f"💀 Game over! The word was: {word.upper()}") else: - remaining = 6 - wrong_count + remaining = max_wrong - wrong_count await _update_board(f"❌ '{letter.upper()}' not in the word — {remaining} guesses left.")