Files
the-island/backend/app/twitch_service.py
empty 8915a4b074 feat: implement AI Director & Narrative Voting System (Phase 9)
Add complete AI Director system that transforms the survival simulation
into a user-driven interactive story with audience voting.

Backend:
- Add DirectorService for LLM-powered plot generation with fallback templates
- Add VoteManager for dual-channel voting (Twitch + Unity)
- Integrate 4-phase game loop: Simulation → Narrative → Voting → Resolution
- Add vote command parsing (!1, !2, !A, !B) in Twitch service
- Add type-safe LLM output handling with _coerce_int() helper
- Normalize voter IDs for case-insensitive duplicate prevention

Unity Client:
- Add NarrativeUI for cinematic event cards and voting progress bars
- Add 7 new event types and data models for director/voting events
- Add delayed subscription coroutine for NetworkManager timing
- Sync client timer with server's remaining_seconds to prevent drift

Documentation:
- Update README.md with AI Director features, voting commands, and event types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 03:37:41 +08:00

126 lines
4.2 KiB
Python

"""
Twitch service for connecting to Twitch chat and handling events.
Integrates with the game engine to process chat commands and bits.
Compatible with twitchio 2.x (IRC-based)
"""
import os
import logging
import random
from typing import TYPE_CHECKING
from twitchio.ext import commands
if TYPE_CHECKING:
from .engine import GameEngine
logger = logging.getLogger(__name__)
class TwitchBot(commands.Bot):
"""
Twitch bot that listens to chat messages and bits events.
Forwards them to the game engine for processing.
Compatible with twitchio 2.x API (IRC-based).
"""
def __init__(self, game_engine: "GameEngine"):
# Initialize bot with environment variables
self._token = os.getenv("TWITCH_TOKEN")
self._channel = os.getenv("TWITCH_CHANNEL_NAME")
prefix = os.getenv("TWITCH_COMMAND_PREFIX", "!")
if not self._token:
raise ValueError("TWITCH_TOKEN environment variable is required")
if not self._channel:
raise ValueError("TWITCH_CHANNEL_NAME environment variable is required")
# Store game engine reference
self._game_engine = game_engine
# Initialize the bot (twitchio 2.x API - IRC based)
super().__init__(
token=self._token,
prefix=prefix,
initial_channels=[self._channel]
)
logger.info(f"TwitchBot initialized for channel: {self._channel}")
async def event_ready(self):
"""Called when the bot successfully connects to Twitch."""
logger.info(f"Twitch Bot logged in as: {self.nick}")
logger.info(f"Connected to channels: {[c.name for c in self.connected_channels]}")
async def event_message(self, message):
"""Called when a message is received in chat."""
# Ignore messages from the bot itself
if message.echo:
return
# Handle commands first
await self.handle_commands(message)
# Extract user and message content
author = message.author
if author is None:
return
username = author.name
content = message.content.strip()
# Log the message for debugging
logger.info(f"Twitch chat [{username}]: {content}")
# Phase 9: Check for vote commands first (!1, !2, !A, !B)
vote_index = self._game_engine.parse_vote_command(content)
if vote_index is not None:
try:
voted = await self._game_engine.process_vote(username, vote_index, "twitch")
if voted:
logger.info(f"Vote recorded: {username} -> option {vote_index + 1}")
return # Don't process as regular command
except Exception as e:
logger.error(f"Error processing vote: {e}")
return
# Forward to game engine for command processing
try:
await self._game_engine.process_command(username, content)
except Exception as e:
logger.error(f"Error processing command: {e}")
# Check for bits in the message tags
if hasattr(message, 'tags') and message.tags:
bits = message.tags.get('bits')
if bits:
await self._handle_bits(username, int(bits))
async def _handle_bits(self, username: str, bits_amount: int):
"""
Handle bits donation.
Delegates to game engine's unified gift handling.
"""
try:
logger.info(f"Received {bits_amount} bits from {username}")
await self._game_engine.handle_gift(username, bits_amount, "bits")
except Exception as e:
logger.error(f"Error handling bits: {e}")
async def event_command_error(self, context, error):
"""Called when a command error occurs."""
# Ignore command not found errors (most chat messages aren't commands)
if isinstance(error, commands.CommandNotFound):
return
logger.error(f"Command error: {error}")
async def event_error(self, error: Exception, data: str = None):
"""Called when an error occurs."""
logger.error(f"Twitch bot error: {error}")
if data:
logger.debug(f"Error data: {data}")