From d095c34276af4e1e9bdbdcc9f3b8487ea1df275a Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 26 Apr 2026 18:56:30 -0400 Subject: [PATCH] fix: ttt/triviaduel crash, blackjack per-player, improve hottake/nhie prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- matrixbot/commands.py | 91 ++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/matrixbot/commands.py b/matrixbot/commands.py index 67ab6c1..bd14303 100644 --- a/matrixbot/commands.py +++ b/matrixbot/commands.py @@ -18,7 +18,7 @@ from config import ( MAX_DICE_SIDES, MAX_DICE_COUNT, BOT_PREFIX, ADMIN_USERS, OLLAMA_URL, OLLAMA_MODEL, CREATIVE_MODEL, ASK_MODEL, COOLDOWN_SECONDS, 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") @@ -2925,13 +2925,23 @@ def record_nhie_reaction(event_id: str, sender: str, key: str) -> None: 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: + topic = random.choice(_NHIE_TOPICS) system_msg = ( - "Generate a fun, surprising, or relatable 'Never Have I Ever' statement. " - "Do NOT include 'Never have I ever' in your response — just the action part. " - "Keep it funny, PG-13 at most, and something that creates an interesting mix of have/have-not. " - "One sentence only, no quotes." + "You write Never Have I Ever statements for a party game. Rules: " + "1) Return ONLY the action — do NOT write 'Never have I ever'. " + "2) Keep it simple and realistic — something an average person might actually have done. " + "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: timeout = aiohttp.ClientTimeout(total=30) async with aiohttp.ClientSession(timeout=timeout) as session: @@ -2939,7 +2949,7 @@ async def _generate_nhie_prompt() -> str | None: f"{OLLAMA_URL}/api/chat", json={"model": CREATIVE_MODEL, "stream": False, "messages": [{"role": "system", "content": system_msg}, - {"role": "user", "content": "Generate a Never Have I Ever statement."}]}, + {"role": "user", "content": user_msg}]}, ) as response: data = await response.json() 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) +_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: + topic = random.choice(_HOTTAKE_TOPICS) system_msg = ( - "Generate a spicy, controversial hot take opinion. It should be something where " - "reasonable people strongly disagree — not hateful, but genuinely polarising. " - "Keep it fun and debate-worthy. One sentence only, stated as a confident opinion. No quotes." + "You generate short, spicy hot take opinions. Rules: " + "1) ONE sentence only — no more. " + "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: timeout = aiohttp.ClientTimeout(total=30) async with aiohttp.ClientSession(timeout=timeout) as session: @@ -3028,7 +3054,7 @@ async def _generate_hot_take() -> str | None: f"{OLLAMA_URL}/api/chat", json={"model": CREATIVE_MODEL, "stream": False, "messages": [{"role": "system", "content": system_msg}, - {"role": "user", "content": "Give me a hot take."}]}, + {"role": "user", "content": user_msg}]}, ) as response: data = await response.json() 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_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: @@ -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") async def cmd_blackjack(client: AsyncClient, room_id: str, sender: str, args: str): - if room_id in _BLACKJACK_GAMES: - g = _BLACKJACK_GAMES[room_id] - plain, html = _bj_board(g) + room_games = _BLACKJACK_GAMES.get(room_id, {}) + if sender in room_games: + plain, html = _bj_board(room_games[sender]) await send_html(client, room_id, plain, html) return @@ -3309,13 +3336,12 @@ async def cmd_blackjack(client: AsyncClient, room_id: str, sender: str, args: st "player_id": sender, "board_event_id": None, } - _BLACKJACK_GAMES[room_id] = game + _BLACKJACK_GAMES.setdefault(room_id, {})[sender] = game # Check for instant blackjack if _bj_total(player_hand) == 21: - del _BLACKJACK_GAMES[room_id] - plain, html = _bj_board(game, reveal_dealer=True, - status="🎉 BLACKJACK! You win!") + del _BLACKJACK_GAMES[room_id][sender] + plain, html = _bj_board(game, reveal_dealer=True, status="🎉 BLACKJACK! You win!") await send_html(client, room_id, plain, html) return @@ -3337,12 +3363,9 @@ async def _bj_update(client: AsyncClient, room_id: str, game: dict, @command("hit", "Draw another card in Blackjack") async def cmd_hit(client: AsyncClient, room_id: str, sender: str, args: str): - if room_id not in _BLACKJACK_GAMES: - await send_text(client, room_id, "No Blackjack game active. Start one with !blackjack") - return - game = _BLACKJACK_GAMES[room_id] - if sender != game["player_id"]: - await send_text(client, room_id, "It's not your game!") + game = _BLACKJACK_GAMES.get(room_id, {}).get(sender) + if not game: + await send_text(client, room_id, "You don't have an active Blackjack game. Start one with !blackjack") return 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"]) if total > 21: - del _BLACKJACK_GAMES[room_id] + del _BLACKJACK_GAMES[room_id][sender] await _bj_update(client, room_id, game, reveal=True, status=f"💀 Bust! You went over 21 with {total}. Dealer wins.") elif total == 21: - # Auto-stand on 21 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: 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.""" - _BLACKJACK_GAMES.pop(room_id, None) + _BLACKJACK_GAMES.get(room_id, {}).pop(player_id, None) dealer_hand = game["dealer_hand"] deck = game["deck"] 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") async def cmd_stand(client: AsyncClient, room_id: str, sender: str, args: str): - if room_id not in _BLACKJACK_GAMES: - await send_text(client, room_id, "No Blackjack game active. Start one with !blackjack") + game = _BLACKJACK_GAMES.get(room_id, {}).get(sender) + if not game: + await send_text(client, room_id, "You don't have an active Blackjack game. Start one with !blackjack") return - game = _BLACKJACK_GAMES[room_id] - if sender != game["player_id"]: - await send_text(client, room_id, "It's not your game!") - return - await _auto_stand(client, room_id, game) + await _auto_stand(client, room_id, sender, game) # ===========================================================================