hangman: edit board in place + fix ASCII art rendering; wyr: debug reaction logging
Lint / Shell (shellcheck) (push) Successful in 9s
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (ruff) (push) Successful in 5s
Lint / Python deps (pip-audit) (push) Successful in 44s
Lint / Secret scan (gitleaks) (push) Successful in 6s

- Add edit_html() to utils using m.replace so messages can be updated
- Hangman board now edits in place on every guess — shows progressing
  ASCII figure as wrong guesses accumulate instead of spamming new messages
- Extract _hangman_board_html() helper for consistent board rendering
- wyr: add INFO-level logging to reaction callback to diagnose vote tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 13:30:46 -04:00
parent 3405ab8b32
commit 126979f5cb
3 changed files with 90 additions and 87 deletions
+52 -84
View File
@@ -11,7 +11,7 @@ import aiohttp
from nio import AsyncClient
from utils import send_text, send_html, send_reaction, sanitize_input
from utils import send_text, send_html, send_reaction, edit_html, sanitize_input
from wordle import handle_wordle
from config import (
MAX_DICE_SIDES, MAX_DICE_COUNT, BOT_PREFIX, ADMIN_USERS,
@@ -1321,6 +1321,32 @@ def _hangman_display(game: dict) -> str:
return " ".join(c if c.lower() in guessed else "_" for c in word.upper())
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"]
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("```", "")
plain = (
f"🎯 Hangman!\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"\n{status_line}" if status_line else "")
)
html = (
f'<font color="#f59e0b"><strong>🎯 Hangman!</strong></font><br>'
f'<pre>{stage_art}</pre>'
f'<strong>Word:</strong> <code>{display}</code> ({len(word)} letters)<br>'
f'<strong>Hint:</strong> {game["hint"]}<br>'
f'Wrong ({wrong_count}/6): {", ".join(wrong_letters) or "none"}'
+ (f'<br><em>{status_line}</em>' if status_line else "")
)
return plain, html
async def _generate_hangman_word() -> dict | None:
system_msg = (
"You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. "
@@ -1393,25 +1419,14 @@ async def cmd_hangman(client: AsyncClient, room_id: str, sender: str, args: str)
"hint": hint,
"guessed_letters": set(),
"wrong_count": 0,
"board_event_id": None,
}
_HANGMAN_GAMES[room_id] = game
plain = (
f"🎯 Hangman!\n"
f"{_HANGMAN_STAGES[0]}\n"
f"Word: {display} ({len(word)} letters)\n"
f"Hint: {hint}\n"
f"Guess a letter or word with !guess <letter/word>\n"
f"Max 6 wrong guesses."
)
html = (
f'<font color="#f59e0b"><strong>🎯 Hangman!</strong></font><br>'
f'<pre>{_HANGMAN_STAGES[0].replace("```", "")}</pre>'
f'<strong>Word:</strong> <code>{display}</code> ({len(word)} letters)<br>'
f'<strong>Hint:</strong> {hint}<br>'
f'<em>Guess with <code>!guess &lt;letter/word&gt;</code> — max 6 wrong guesses</em>'
)
await send_html(client, room_id, plain, html)
plain, html = _hangman_board_html(game, "Guess with !guess <letter/word> — max 6 wrong guesses")
resp = await send_html(client, room_id, plain, html)
if hasattr(resp, "event_id"):
game["board_event_id"] = resp.event_id
@command("guess", "Guess a letter or word in hangman (!guess <letter/word>)")
@@ -1429,38 +1444,34 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str):
word = game["word"]
board_id = game.get("board_event_id")
async def _update_board(status: str):
"""Edit the board message in place, or send a new one if edit unavailable."""
p, h = _hangman_board_html(game, status)
if board_id:
await edit_html(client, room_id, board_id, p, h)
else:
await send_html(client, room_id, p, h)
# Full word guess
if len(guess) > 1:
winner = sender.split(":")[0].lstrip("@")
if guess == word:
del _HANGMAN_GAMES[room_id]
plain = f"🎉 {winner} got it! The word was: {word.upper()}"
html = (
f'<font color="#22c55e"><strong>🎉 {winner} got it! The word was: {word.upper()}</strong></font>'
await send_html(
client, room_id,
f"🎉 {winner} got it! The word was: {word.upper()}",
f'<font color="#22c55e"><strong>🎉 {winner} got it! The word was: {word.upper()}</strong></font>',
)
await send_html(client, room_id, plain, html)
else:
game["wrong_count"] += 1
display = _hangman_display(game)
if game["wrong_count"] >= 6:
del _HANGMAN_GAMES[room_id]
plain = f"'{guess.upper()}' is wrong! Game over — the word was: {word.upper()}"
html = f'<font color="#ef4444"><strong>❌ Wrong! Game over — the word was: {word.upper()}</strong></font>'
await send_html(client, room_id, plain, html)
await _update_board(f"💀 Wrong! Game over — the word was: {word.upper()}")
else:
remaining = 6 - game["wrong_count"]
plain = (
f"'{guess.upper()}' is wrong! {remaining} guesses remaining.\n"
f"{_HANGMAN_STAGES[game['wrong_count']]}\n"
f"Word: {display}\nHint: {game['hint']}"
)
html = (
f'<font color="#ef4444"><strong>❌ \'{guess.upper()}\' is wrong!</strong></font> '
f'{remaining} guesses remaining.<br>'
f'<strong>Word:</strong> <code>{display}</code><br>'
f'<strong>Hint:</strong> {game["hint"]}'
)
await send_html(client, room_id, plain, html)
await _update_board(f"'{guess.upper()}' is wrong! {remaining} guesses remaining.")
return
# Single letter guess
@@ -1473,64 +1484,21 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str):
if letter in word:
display = _hangman_display(game)
# Check if fully revealed
if "_" not in display:
del _HANGMAN_GAMES[room_id]
plain = f"🎉 Solved! The word was: {word.upper()}"
html = f'<font color="#22c55e"><strong>🎉 Solved! The word was: {word.upper()}</strong></font>'
await send_html(client, room_id, plain, html)
await _update_board(f"🎉 Solved! The word was: {word.upper()}")
return
wrong_letters = sorted(ch for ch in game["guessed_letters"] if ch not in word)
plain = (
f"'{letter.upper()}' is in the word!\n"
f"{_HANGMAN_STAGES[game['wrong_count']]}\n"
f"Word: {display}\n"
f"Hint: {game['hint']}\n"
f"Wrong guesses ({game['wrong_count']}/6): {', '.join(wrong_letters) or 'none'}"
)
html = (
f'<font color="#22c55e"><strong>✅ \'{letter.upper()}\' is in the word!</strong></font><br>'
f'<strong>Word:</strong> <code>{display}</code><br>'
f'<strong>Hint:</strong> {game["hint"]}<br>'
f'Wrong ({game["wrong_count"]}/6): {", ".join(wrong_letters) or "none"}'
)
await send_html(client, room_id, plain, html)
await _update_board(f"'{letter.upper()}' is in the word!")
else:
game["wrong_count"] += 1
wrong_count = game["wrong_count"]
display = _hangman_display(game)
wrong_letters = sorted(ch for ch in game["guessed_letters"] if ch not in word)
if wrong_count >= 6:
del _HANGMAN_GAMES[room_id]
plain = (
f"💀 '{letter.upper()}' is wrong! Game over!\n"
f"{_HANGMAN_STAGES[6]}\n"
f"The word was: {word.upper()}"
)
html = (
f'<font color="#ef4444"><strong>💀 Game Over!</strong></font><br>'
f'<strong>The word was: {word.upper()}</strong><br>'
f'Wrong letters: {", ".join(wrong_letters)}'
)
await send_html(client, room_id, plain, html)
await _update_board(f"💀 Game over! The word was: {word.upper()}")
else:
remaining = 6 - wrong_count
plain = (
f"'{letter.upper()}' is not in the word. {remaining} wrong guesses remaining.\n"
f"{_HANGMAN_STAGES[wrong_count]}\n"
f"Word: {display}\n"
f"Hint: {game['hint']}\n"
f"Wrong guesses ({wrong_count}/6): {', '.join(wrong_letters)}"
)
html = (
f'<font color="#ef4444"><strong>❌ \'{letter.upper()}\' is not in the word!</strong></font><br>'
f'<strong>Word:</strong> <code>{display}</code><br>'
f'<strong>Hint:</strong> {game["hint"]}<br>'
f'Wrong ({wrong_count}/6): {", ".join(wrong_letters)}'
)
await send_html(client, room_id, plain, html)
await _update_board(f"'{letter.upper()}' not in the word — {remaining} guesses left.")
# ---------------------------------------------------------------------------