hangman: edit board in place + fix ASCII art rendering; wyr: debug reaction logging
- 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:
+13
-3
@@ -81,19 +81,29 @@ class Callbacks:
|
|||||||
|
|
||||||
# m.reaction events come as UnknownEvent with type "m.reaction"
|
# m.reaction events come as UnknownEvent with type "m.reaction"
|
||||||
if not hasattr(event, "source"):
|
if not hasattr(event, "source"):
|
||||||
|
logger.info("reaction: event has no source attr, type=%s sender=%s", type(event).__name__, event.sender)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
event_type = event.source.get("type", "")
|
||||||
content = event.source.get("content", {})
|
content = event.source.get("content", {})
|
||||||
relates_to = content.get("m.relates_to", {})
|
relates_to = content.get("m.relates_to", {})
|
||||||
if relates_to.get("rel_type") != "m.annotation":
|
rel_type = relates_to.get("rel_type", "")
|
||||||
return
|
|
||||||
|
|
||||||
reacted_event_id = relates_to.get("event_id", "")
|
reacted_event_id = relates_to.get("event_id", "")
|
||||||
key = relates_to.get("key", "")
|
key = relates_to.get("key", "")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"reaction: type=%s rel_type=%s key=%r target=%s sender=%s",
|
||||||
|
event_type, rel_type, key, reacted_event_id[:16] if reacted_event_id else "", event.sender,
|
||||||
|
)
|
||||||
|
|
||||||
|
if rel_type != "m.annotation":
|
||||||
|
return
|
||||||
|
|
||||||
await handle_welcome_reaction(
|
await handle_welcome_reaction(
|
||||||
self.client, room.room_id, event.sender, reacted_event_id, key
|
self.client, room.room_id, event.sender, reacted_event_id, key
|
||||||
)
|
)
|
||||||
|
from commands import _WYR_POLLS
|
||||||
|
logger.info("reaction: wyr polls active=%s matched=%s", list(_WYR_POLLS.keys()), reacted_event_id in _WYR_POLLS)
|
||||||
record_wyr_vote(reacted_event_id, event.sender, key)
|
record_wyr_vote(reacted_event_id, event.sender, key)
|
||||||
|
|
||||||
async def member(self, room, event):
|
async def member(self, room, event):
|
||||||
|
|||||||
+52
-84
@@ -11,7 +11,7 @@ import aiohttp
|
|||||||
|
|
||||||
from nio import AsyncClient
|
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 wordle import handle_wordle
|
||||||
from config import (
|
from config import (
|
||||||
MAX_DICE_SIDES, MAX_DICE_COUNT, BOT_PREFIX, ADMIN_USERS,
|
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())
|
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:
|
async def _generate_hangman_word() -> dict | None:
|
||||||
system_msg = (
|
system_msg = (
|
||||||
"You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. "
|
"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,
|
"hint": hint,
|
||||||
"guessed_letters": set(),
|
"guessed_letters": set(),
|
||||||
"wrong_count": 0,
|
"wrong_count": 0,
|
||||||
|
"board_event_id": None,
|
||||||
}
|
}
|
||||||
_HANGMAN_GAMES[room_id] = game
|
_HANGMAN_GAMES[room_id] = game
|
||||||
|
|
||||||
plain = (
|
plain, html = _hangman_board_html(game, "Guess with !guess <letter/word> — max 6 wrong guesses")
|
||||||
f"🎯 Hangman!\n"
|
resp = await send_html(client, room_id, plain, html)
|
||||||
f"{_HANGMAN_STAGES[0]}\n"
|
if hasattr(resp, "event_id"):
|
||||||
f"Word: {display} ({len(word)} letters)\n"
|
game["board_event_id"] = resp.event_id
|
||||||
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 <letter/word></code> — max 6 wrong guesses</em>'
|
|
||||||
)
|
|
||||||
await send_html(client, room_id, plain, html)
|
|
||||||
|
|
||||||
|
|
||||||
@command("guess", "Guess a letter or word in hangman (!guess <letter/word>)")
|
@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"]
|
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
|
# Full word guess
|
||||||
if len(guess) > 1:
|
if len(guess) > 1:
|
||||||
winner = sender.split(":")[0].lstrip("@")
|
winner = sender.split(":")[0].lstrip("@")
|
||||||
if guess == word:
|
if guess == word:
|
||||||
del _HANGMAN_GAMES[room_id]
|
del _HANGMAN_GAMES[room_id]
|
||||||
plain = f"🎉 {winner} got it! The word was: {word.upper()}"
|
await send_html(
|
||||||
html = (
|
client, room_id,
|
||||||
f'<font color="#22c55e"><strong>🎉 {winner} got it! The word was: {word.upper()}</strong></font>'
|
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:
|
else:
|
||||||
game["wrong_count"] += 1
|
game["wrong_count"] += 1
|
||||||
display = _hangman_display(game)
|
|
||||||
if game["wrong_count"] >= 6:
|
if game["wrong_count"] >= 6:
|
||||||
del _HANGMAN_GAMES[room_id]
|
del _HANGMAN_GAMES[room_id]
|
||||||
plain = f"❌ '{guess.upper()}' is wrong! Game over — the word was: {word.upper()}"
|
await _update_board(f"💀 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)
|
|
||||||
else:
|
else:
|
||||||
remaining = 6 - game["wrong_count"]
|
remaining = 6 - game["wrong_count"]
|
||||||
plain = (
|
await _update_board(f"❌ '{guess.upper()}' is wrong! {remaining} guesses remaining.")
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Single letter guess
|
# Single letter guess
|
||||||
@@ -1473,64 +1484,21 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str):
|
|||||||
|
|
||||||
if letter in word:
|
if letter in word:
|
||||||
display = _hangman_display(game)
|
display = _hangman_display(game)
|
||||||
# Check if fully revealed
|
|
||||||
if "_" not in display:
|
if "_" not in display:
|
||||||
del _HANGMAN_GAMES[room_id]
|
del _HANGMAN_GAMES[room_id]
|
||||||
plain = f"🎉 Solved! The word was: {word.upper()}"
|
await _update_board(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)
|
|
||||||
return
|
return
|
||||||
|
await _update_board(f"✅ '{letter.upper()}' is in the word!")
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
game["wrong_count"] += 1
|
game["wrong_count"] += 1
|
||||||
wrong_count = game["wrong_count"]
|
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:
|
if wrong_count >= 6:
|
||||||
del _HANGMAN_GAMES[room_id]
|
del _HANGMAN_GAMES[room_id]
|
||||||
plain = (
|
await _update_board(f"💀 Game over! The word was: {word.upper()}")
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
remaining = 6 - wrong_count
|
remaining = 6 - wrong_count
|
||||||
plain = (
|
await _update_board(f"❌ '{letter.upper()}' not in the word — {remaining} guesses left.")
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -87,6 +87,31 @@ async def send_html(client: AsyncClient, room_id: str, plain: str, html: str):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_html(client: AsyncClient, room_id: str, event_id: str, plain: str, html: str):
|
||||||
|
"""Edit an existing message in place using m.replace."""
|
||||||
|
logger = logging.getLogger("matrixbot")
|
||||||
|
content = {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": f"* {plain}",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": html,
|
||||||
|
"m.new_content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": plain,
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": html,
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.replace",
|
||||||
|
"event_id": event_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp = await _room_send_trusted(client, room_id, message_type="m.room.message", content=content)
|
||||||
|
if not isinstance(resp, RoomSendResponse):
|
||||||
|
logger.error("edit_html failed: %s", resp)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
async def send_reaction(client: AsyncClient, room_id: str, event_id: str, emoji: str):
|
async def send_reaction(client: AsyncClient, room_id: str, event_id: str, emoji: str):
|
||||||
return await _room_send_trusted(
|
return await _room_send_trusted(
|
||||||
client, room_id,
|
client, room_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user