fix: ttt/triviaduel crash, blackjack per-player, improve hottake/nhie prompts
Lint / Shell (shellcheck) (push) Successful in 8s
Lint / JS (eslint) (push) Successful in 6s
Lint / Python (ruff) (push) Failing after 5s
Lint / Python deps (pip-audit) (push) Successful in 43s
Lint / Secret scan (gitleaks) (push) Successful in 5s

- Import MATRIX_USER_ID in commands.py (was missing — caused !ttt and
  !triviaduel to crash with NameError on every invocation)
- Blackjack is now per-player per-room: multiple players can each run
  their own game simultaneously; !hit and !stand operate on the caller's
  own game only
- !hottake: pick a random topic from 20 categories and pass it to the
  model so takes aren't all nostalgia-flavoured
- !nhie: tighter prompt with topic rotation and a word-count cap so
  generated scenarios are simpler and more relatable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:56:30 -04:00
parent 54c73535b8
commit d095c34276
+55 -36
View File
@@ -18,7 +18,7 @@ from config import (
MAX_DICE_SIDES, MAX_DICE_COUNT, BOT_PREFIX, ADMIN_USERS, MAX_DICE_SIDES, MAX_DICE_COUNT, BOT_PREFIX, ADMIN_USERS,
OLLAMA_URL, OLLAMA_MODEL, CREATIVE_MODEL, ASK_MODEL, COOLDOWN_SECONDS, OLLAMA_URL, OLLAMA_MODEL, CREATIVE_MODEL, ASK_MODEL, COOLDOWN_SECONDS,
MINECRAFT_RCON_HOST, MINECRAFT_RCON_PORT, MINECRAFT_RCON_PASSWORD, MINECRAFT_RCON_HOST, MINECRAFT_RCON_PORT, MINECRAFT_RCON_PASSWORD,
RCON_TIMEOUT, MIN_USERNAME_LENGTH, MAX_USERNAME_LENGTH, RCON_TIMEOUT, MIN_USERNAME_LENGTH, MAX_USERNAME_LENGTH, MATRIX_USER_ID,
) )
logger = logging.getLogger("matrixbot") logger = logging.getLogger("matrixbot")
@@ -2925,13 +2925,23 @@ def record_nhie_reaction(event_id: str, sender: str, key: str) -> None:
poll["have"].discard(sender) poll["have"].discard(sender)
_NHIE_TOPICS = [
"travel", "food", "social situations", "school or work", "technology",
"outdoor adventures", "relationships", "embarrassing moments",
"sleep habits", "gaming or movies", "sports", "shopping",
]
async def _generate_nhie_prompt() -> str | None: async def _generate_nhie_prompt() -> str | None:
topic = random.choice(_NHIE_TOPICS)
system_msg = ( system_msg = (
"Generate a fun, surprising, or relatable 'Never Have I Ever' statement. " "You write Never Have I Ever statements for a party game. Rules: "
"Do NOT include 'Never have I ever' in your response — just the action part. " "1) Return ONLY the action — do NOT write 'Never have I ever'. "
"Keep it funny, PG-13 at most, and something that creates an interesting mix of have/have-not. " "2) Keep it simple and realistic — something an average person might actually have done. "
"One sentence only, no quotes." "3) Short: under 12 words. "
"4) PG-13 at most. No quotes. No explanation."
) )
user_msg = f"Write a Never Have I Ever statement about: {topic}"
try: try:
timeout = aiohttp.ClientTimeout(total=30) timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
@@ -2939,7 +2949,7 @@ async def _generate_nhie_prompt() -> str | None:
f"{OLLAMA_URL}/api/chat", f"{OLLAMA_URL}/api/chat",
json={"model": CREATIVE_MODEL, "stream": False, json={"model": CREATIVE_MODEL, "stream": False,
"messages": [{"role": "system", "content": system_msg}, "messages": [{"role": "system", "content": system_msg},
{"role": "user", "content": "Generate a Never Have I Ever statement."}]}, {"role": "user", "content": user_msg}]},
) as response: ) as response:
data = await response.json() data = await response.json()
text = data.get("message", {}).get("content", "").strip().strip('"') text = data.get("message", {}).get("content", "").strip().strip('"')
@@ -3015,12 +3025,28 @@ def record_hottake_reaction(event_id: str, sender: str, key: str) -> None:
poll["agree"].discard(sender) poll["agree"].discard(sender)
_HOTTAKE_TOPICS = [
"food and cooking", "music genres", "social media and technology",
"sports and fitness", "video games", "movies and TV shows",
"work and career culture", "fashion and style", "travel and tourism",
"pets and animals", "relationships and dating", "education and school",
"sleep and daily habits", "outdoor activities", "city vs rural living",
"coffee and caffeine", "cars and driving", "reading and books",
"money and spending habits", "home and interior design",
]
async def _generate_hot_take() -> str | None: async def _generate_hot_take() -> str | None:
topic = random.choice(_HOTTAKE_TOPICS)
system_msg = ( system_msg = (
"Generate a spicy, controversial hot take opinion. It should be something where " "You generate short, spicy hot take opinions. Rules: "
"reasonable people strongly disagree — not hateful, but genuinely polarising. " "1) ONE sentence only — no more. "
"Keep it fun and debate-worthy. One sentence only, stated as a confident opinion. No quotes." "2) State it as a confident, direct opinion. "
"3) It must be genuinely controversial — people should strongly disagree. "
"4) Do NOT mention nostalgia, pop culture legacy, or historical impact. "
"5) No quotes around your response. No preamble. Just the hot take."
) )
user_msg = f"Give me a hot take about: {topic}"
try: try:
timeout = aiohttp.ClientTimeout(total=30) timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
@@ -3028,7 +3054,7 @@ async def _generate_hot_take() -> str | None:
f"{OLLAMA_URL}/api/chat", f"{OLLAMA_URL}/api/chat",
json={"model": CREATIVE_MODEL, "stream": False, json={"model": CREATIVE_MODEL, "stream": False,
"messages": [{"role": "system", "content": system_msg}, "messages": [{"role": "system", "content": system_msg},
{"role": "user", "content": "Give me a hot take."}]}, {"role": "user", "content": user_msg}]},
) as response: ) as response:
data = await response.json() data = await response.json()
text = data.get("message", {}).get("content", "").strip().strip('"') text = data.get("message", {}).get("content", "").strip().strip('"')
@@ -3231,7 +3257,8 @@ async def cmd_move(client: AsyncClient, room_id: str, sender: str, args: str):
# Blackjack # Blackjack
# =========================================================================== # ===========================================================================
_BLACKJACK_GAMES: dict[str, dict] = {} # {room_id: {player_id: game}} — multiple players per room each get their own game
_BLACKJACK_GAMES: dict[str, dict[str, dict]] = {}
def _bj_new_deck() -> list: def _bj_new_deck() -> list:
@@ -3293,9 +3320,9 @@ def _bj_board(game: dict, reveal_dealer: bool = False, status: str = "") -> tupl
@command("blackjack", "Play Blackjack! Beat the dealer — !hit to draw, !stand to stay") @command("blackjack", "Play Blackjack! Beat the dealer — !hit to draw, !stand to stay")
async def cmd_blackjack(client: AsyncClient, room_id: str, sender: str, args: str): async def cmd_blackjack(client: AsyncClient, room_id: str, sender: str, args: str):
if room_id in _BLACKJACK_GAMES: room_games = _BLACKJACK_GAMES.get(room_id, {})
g = _BLACKJACK_GAMES[room_id] if sender in room_games:
plain, html = _bj_board(g) plain, html = _bj_board(room_games[sender])
await send_html(client, room_id, plain, html) await send_html(client, room_id, plain, html)
return return
@@ -3309,13 +3336,12 @@ async def cmd_blackjack(client: AsyncClient, room_id: str, sender: str, args: st
"player_id": sender, "player_id": sender,
"board_event_id": None, "board_event_id": None,
} }
_BLACKJACK_GAMES[room_id] = game _BLACKJACK_GAMES.setdefault(room_id, {})[sender] = game
# Check for instant blackjack # Check for instant blackjack
if _bj_total(player_hand) == 21: if _bj_total(player_hand) == 21:
del _BLACKJACK_GAMES[room_id] del _BLACKJACK_GAMES[room_id][sender]
plain, html = _bj_board(game, reveal_dealer=True, plain, html = _bj_board(game, reveal_dealer=True, status="🎉 BLACKJACK! You win!")
status="🎉 BLACKJACK! You win!")
await send_html(client, room_id, plain, html) await send_html(client, room_id, plain, html)
return return
@@ -3337,12 +3363,9 @@ async def _bj_update(client: AsyncClient, room_id: str, game: dict,
@command("hit", "Draw another card in Blackjack") @command("hit", "Draw another card in Blackjack")
async def cmd_hit(client: AsyncClient, room_id: str, sender: str, args: str): async def cmd_hit(client: AsyncClient, room_id: str, sender: str, args: str):
if room_id not in _BLACKJACK_GAMES: game = _BLACKJACK_GAMES.get(room_id, {}).get(sender)
await send_text(client, room_id, "No Blackjack game active. Start one with !blackjack") if not game:
return await send_text(client, room_id, "You don't have an active Blackjack game. Start one with !blackjack")
game = _BLACKJACK_GAMES[room_id]
if sender != game["player_id"]:
await send_text(client, room_id, "It's not your game!")
return return
card = game["deck"].pop() if game["deck"] else _bj_new_deck().pop() card = game["deck"].pop() if game["deck"] else _bj_new_deck().pop()
@@ -3350,20 +3373,19 @@ async def cmd_hit(client: AsyncClient, room_id: str, sender: str, args: str):
total = _bj_total(game["player_hand"]) total = _bj_total(game["player_hand"])
if total > 21: if total > 21:
del _BLACKJACK_GAMES[room_id] del _BLACKJACK_GAMES[room_id][sender]
await _bj_update(client, room_id, game, reveal=True, await _bj_update(client, room_id, game, reveal=True,
status=f"💀 Bust! You went over 21 with {total}. Dealer wins.") status=f"💀 Bust! You went over 21 with {total}. Dealer wins.")
elif total == 21: elif total == 21:
# Auto-stand on 21
await _bj_update(client, room_id, game, status="Hit 21! Standing automatically...") await _bj_update(client, room_id, game, status="Hit 21! Standing automatically...")
await _auto_stand(client, room_id, game) await _auto_stand(client, room_id, sender, game)
else: else:
await _bj_update(client, room_id, game) await _bj_update(client, room_id, game)
async def _auto_stand(client: AsyncClient, room_id: str, game: dict): async def _auto_stand(client: AsyncClient, room_id: str, player_id: str, game: dict):
"""Dealer plays out and resolve the game.""" """Dealer plays out and resolve the game."""
_BLACKJACK_GAMES.pop(room_id, None) _BLACKJACK_GAMES.get(room_id, {}).pop(player_id, None)
dealer_hand = game["dealer_hand"] dealer_hand = game["dealer_hand"]
deck = game["deck"] deck = game["deck"]
while _bj_total(dealer_hand) < 17: while _bj_total(dealer_hand) < 17:
@@ -3386,14 +3408,11 @@ async def _auto_stand(client: AsyncClient, room_id: str, game: dict):
@command("stand", "Stand in Blackjack — dealer plays out") @command("stand", "Stand in Blackjack — dealer plays out")
async def cmd_stand(client: AsyncClient, room_id: str, sender: str, args: str): async def cmd_stand(client: AsyncClient, room_id: str, sender: str, args: str):
if room_id not in _BLACKJACK_GAMES: game = _BLACKJACK_GAMES.get(room_id, {}).get(sender)
await send_text(client, room_id, "No Blackjack game active. Start one with !blackjack") if not game:
await send_text(client, room_id, "You don't have an active Blackjack game. Start one with !blackjack")
return return
game = _BLACKJACK_GAMES[room_id] await _auto_stand(client, room_id, sender, game)
if sender != game["player_id"]:
await send_text(client, room_id, "It's not your game!")
return
await _auto_stand(client, room_id, game)
# =========================================================================== # ===========================================================================