Files
the-island/backend/app/engine.py
empty 1f29010de6 docs: 更新 README 和游戏引擎
- 更新 README 中的 Twitch 配置说明
- 更新游戏引擎相关代码
2026-01-01 19:23:21 +08:00

974 lines
39 KiB
Python

"""
Core Game Engine - The Island Survival Simulation.
Manages survival mechanics, agent states, weather, time, social interactions, and user commands.
"""
import asyncio
import logging
import random
import re
import time
from typing import TYPE_CHECKING, Optional
from .schemas import GameEvent, EventType
from .database import init_db, get_db_session
from .models import User, Agent, WorldState, GameConfig, AgentRelationship
from .llm import llm_service
if TYPE_CHECKING:
from .server import ConnectionManager
logger = logging.getLogger(__name__)
# =============================================================================
# Command patterns
# =============================================================================
FEED_PATTERN = re.compile(r"feed\s+(\w+)", re.IGNORECASE)
CHECK_PATTERN = re.compile(r"(check|查询|状态)", re.IGNORECASE)
RESET_PATTERN = re.compile(r"(reset|重新开始|重置)", re.IGNORECASE)
HEAL_PATTERN = re.compile(r"heal\s+(\w+)", re.IGNORECASE)
TALK_PATTERN = re.compile(r"talk\s+(\w+)\s*(.*)?", re.IGNORECASE)
ENCOURAGE_PATTERN = re.compile(r"encourage\s+(\w+)", re.IGNORECASE)
REVIVE_PATTERN = re.compile(r"revive\s+(\w+)", re.IGNORECASE)
# =============================================================================
# Game constants
# =============================================================================
TICK_INTERVAL = 5.0 # Seconds between ticks
# Survival (base values, modified by difficulty)
BASE_ENERGY_DECAY_PER_TICK = 2
BASE_HP_DECAY_WHEN_STARVING = 5
# Command costs and effects
FEED_COST = 10
FEED_ENERGY_RESTORE = 20
HEAL_COST = 15
HEAL_HP_RESTORE = 30
ENCOURAGE_COST = 5
ENCOURAGE_MOOD_BOOST = 15
REVIVE_COST = 10 # Casual mode cost
INITIAL_USER_GOLD = 100
IDLE_CHAT_PROBABILITY = 0.15
# =============================================================================
# Day/Night cycle
# =============================================================================
TICKS_PER_DAY = 120 # 10 minutes per day at 5s/tick
DAY_PHASES = {
"dawn": (0, 15), # Ticks 0-15
"day": (16, 75), # Ticks 16-75
"dusk": (76, 90), # Ticks 76-90
"night": (91, 119) # Ticks 91-119
}
PHASE_MODIFIERS = {
"dawn": {"energy_decay": 0.8, "hp_recovery": 1, "mood_change": 3},
"day": {"energy_decay": 1.0, "hp_recovery": 2, "mood_change": 2},
"dusk": {"energy_decay": 1.2, "hp_recovery": 0, "mood_change": -2},
"night": {"energy_decay": 1.3, "hp_recovery": 0, "mood_change": -3}
}
# =============================================================================
# Weather system
# =============================================================================
WEATHER_TYPES = {
"Sunny": {"energy_modifier": 1.0, "mood_change": 5},
"Cloudy": {"energy_modifier": 1.0, "mood_change": 0},
"Rainy": {"energy_modifier": 1.2, "mood_change": -8},
"Stormy": {"energy_modifier": 1.4, "mood_change": -15},
"Hot": {"energy_modifier": 1.3, "mood_change": -5},
"Foggy": {"energy_modifier": 1.1, "mood_change": -3}
}
WEATHER_TRANSITIONS = {
"Sunny": {"Sunny": 0.5, "Cloudy": 0.3, "Hot": 0.15, "Rainy": 0.05},
"Cloudy": {"Cloudy": 0.3, "Sunny": 0.3, "Rainy": 0.25, "Foggy": 0.1, "Stormy": 0.05},
"Rainy": {"Rainy": 0.3, "Cloudy": 0.35, "Stormy": 0.2, "Foggy": 0.15},
"Stormy": {"Stormy": 0.2, "Rainy": 0.5, "Cloudy": 0.3},
"Hot": {"Hot": 0.3, "Sunny": 0.5, "Cloudy": 0.15, "Stormy": 0.05},
"Foggy": {"Foggy": 0.2, "Cloudy": 0.4, "Rainy": 0.25, "Sunny": 0.15}
}
WEATHER_MIN_DURATION = 15
WEATHER_MAX_DURATION = 35
# =============================================================================
# Social interactions
# =============================================================================
SOCIAL_INTERACTIONS = {
"chat": {"affection": (1, 4), "trust": (0, 2), "weight": 0.4},
"share_food": {"affection": (3, 7), "trust": (2, 4), "weight": 0.15, "min_energy": 40},
"help": {"affection": (4, 8), "trust": (3, 6), "weight": 0.15, "min_energy": 30},
"comfort": {"affection": (3, 6), "trust": (2, 4), "weight": 0.2, "target_max_mood": 40},
"argue": {"affection": (-8, -3), "trust": (-5, -2), "weight": 0.1, "max_mood": 35}
}
# Initial NPC data
INITIAL_AGENTS = [
{"name": "Jack", "personality": "Brave", "social_tendency": "extrovert"},
{"name": "Luna", "personality": "Cunning", "social_tendency": "neutral"},
{"name": "Bob", "personality": "Honest", "social_tendency": "introvert"},
]
class GameEngine:
"""
The core game engine for island survival simulation.
Manages agents, users, time, weather, social interactions, and survival mechanics.
"""
def __init__(self, connection_manager: "ConnectionManager") -> None:
self._manager = connection_manager
self._running = False
self._tick_count = 0
self._tick_interval = TICK_INTERVAL
self._config: Optional[GameConfig] = None
@property
def is_running(self) -> bool:
return self._running
def _get_config(self) -> GameConfig:
"""Get or load game configuration."""
if self._config is None:
with get_db_session() as db:
config = db.query(GameConfig).first()
if config is None:
config = GameConfig()
# Expunge to detach from session, then make it usable outside
db.expunge(config)
# Access all attributes while still valid to load them
_ = (config.difficulty, config.energy_decay_multiplier,
config.hp_decay_multiplier, config.auto_revive_enabled,
config.auto_revive_delay_ticks, config.revive_hp,
config.revive_energy, config.social_interaction_probability)
self._config = config
return self._config
def _seed_initial_data(self) -> None:
"""Seed initial agents, world state, and config if database is empty."""
with get_db_session() as db:
# Seed agents
if db.query(Agent).count() == 0:
logger.info("Seeding initial agents...")
for agent_data in INITIAL_AGENTS:
agent = Agent(
name=agent_data["name"],
personality=agent_data["personality"],
social_tendency=agent_data.get("social_tendency", "neutral"),
status="Alive",
hp=100,
energy=100,
mood=70
)
db.add(agent)
logger.info(f"Created {len(INITIAL_AGENTS)} initial agents")
# Seed world state
if db.query(WorldState).first() is None:
logger.info("Seeding initial world state...")
world = WorldState(day_count=1, weather="Sunny", resource_level=100)
db.add(world)
# Seed game config (casual mode by default)
if db.query(GameConfig).first() is None:
logger.info("Seeding game config (casual mode)...")
config = GameConfig(difficulty="casual")
db.add(config)
def _get_or_create_user(self, db, username: str) -> User:
"""Get existing user or create new one."""
user = db.query(User).filter(User.username == username).first()
if user is None:
user = User(username=username, gold=INITIAL_USER_GOLD)
db.add(user)
db.flush()
logger.info(f"New user registered: {username}")
return user
# =========================================================================
# Event broadcasting
# =========================================================================
async def _broadcast_event(self, event_type: str, data: dict) -> None:
"""Create and broadcast a game event."""
event = GameEvent(event_type=event_type, timestamp=time.time(), data=data)
await self._manager.broadcast(event)
async def _broadcast_agents_status(self) -> None:
"""Broadcast all agents' current status."""
with get_db_session() as db:
agents = db.query(Agent).all()
agents_data = [agent.to_dict() for agent in agents]
await self._broadcast_event(EventType.AGENTS_UPDATE, {"agents": agents_data})
async def _broadcast_world_status(self) -> None:
"""Broadcast world state."""
with get_db_session() as db:
world = db.query(WorldState).first()
if world:
await self._broadcast_event(EventType.WORLD_UPDATE, world.to_dict())
# =========================================================================
# Day/Night cycle (Phase 2)
# =========================================================================
async def _advance_time(self) -> Optional[dict]:
"""Advance time and return phase change info if applicable."""
with get_db_session() as db:
world = db.query(WorldState).first()
if not world:
return None
old_phase = world.time_of_day
world.current_tick_in_day += 1
# New day
if world.current_tick_in_day >= TICKS_PER_DAY:
world.current_tick_in_day = 0
world.day_count += 1
await self._broadcast_event(EventType.DAY_CHANGE, {
"day": world.day_count,
"message": f"Day {world.day_count} begins!"
})
# Determine current phase
tick = world.current_tick_in_day
for phase, (start, end) in DAY_PHASES.items():
if start <= tick <= end:
if world.time_of_day != phase:
world.time_of_day = phase
return {"old_phase": old_phase, "new_phase": phase, "day": world.day_count}
break
return None
# =========================================================================
# Weather system (Phase 3)
# =========================================================================
async def _update_weather(self) -> Optional[dict]:
"""Update weather based on transition probabilities."""
with get_db_session() as db:
world = db.query(WorldState).first()
if not world:
return None
world.weather_duration += 1
# Check if weather should change
target_duration = random.randint(WEATHER_MIN_DURATION, WEATHER_MAX_DURATION)
if world.weather_duration >= target_duration:
old_weather = world.weather
transitions = WEATHER_TRANSITIONS.get(old_weather, {"Sunny": 1.0})
new_weather = random.choices(
list(transitions.keys()),
weights=list(transitions.values())
)[0]
if new_weather != old_weather:
world.weather = new_weather
world.weather_duration = 0
return {"old_weather": old_weather, "new_weather": new_weather}
return None
async def _update_moods(self) -> None:
"""Update agent moods based on weather and time."""
with get_db_session() as db:
world = db.query(WorldState).first()
if not world:
return
weather_effect = WEATHER_TYPES.get(world.weather, {}).get("mood_change", 0)
phase_effect = PHASE_MODIFIERS.get(world.time_of_day, {}).get("mood_change", 0)
agents = db.query(Agent).filter(Agent.status == "Alive").all()
for agent in agents:
# Apply mood changes (scaled down for per-tick application)
mood_delta = (weather_effect + phase_effect) * 0.1
agent.mood = max(0, min(100, agent.mood + mood_delta))
# Update mood state
if agent.mood >= 70:
agent.mood_state = "happy"
elif agent.mood >= 40:
agent.mood_state = "neutral"
elif agent.mood >= 20:
agent.mood_state = "sad"
else:
agent.mood_state = "anxious"
# =========================================================================
# Survival mechanics
# =========================================================================
async def _process_survival_tick(self) -> None:
"""Process survival mechanics with difficulty modifiers."""
config = self._get_config()
deaths = []
with get_db_session() as db:
world = db.query(WorldState).first()
phase_mod = PHASE_MODIFIERS.get(world.time_of_day if world else "day", {})
weather_mod = WEATHER_TYPES.get(world.weather if world else "Sunny", {})
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
for agent in alive_agents:
# Calculate energy decay with all modifiers
base_decay = BASE_ENERGY_DECAY_PER_TICK
decay = base_decay * config.energy_decay_multiplier
decay *= phase_mod.get("energy_decay", 1.0)
decay *= weather_mod.get("energy_modifier", 1.0)
agent.energy = max(0, agent.energy - int(decay))
# HP recovery during day phases
hp_recovery = phase_mod.get("hp_recovery", 0)
if hp_recovery > 0 and agent.energy > 20:
agent.hp = min(100, agent.hp + hp_recovery)
# Starvation damage
if agent.energy <= 0:
hp_decay = BASE_HP_DECAY_WHEN_STARVING * config.hp_decay_multiplier
agent.hp = max(0, agent.hp - int(hp_decay))
# Death check
if agent.hp <= 0:
agent.status = "Dead"
agent.death_tick = self._tick_count
deaths.append({"name": agent.name, "personality": agent.personality})
logger.info(f"Agent {agent.name} has died!")
# Broadcast death events
for death in deaths:
await self._broadcast_event(EventType.AGENT_DIED, {
"agent_name": death["name"],
"message": f"{death['name']} ({death['personality']}) has died..."
})
async def _process_auto_revive(self) -> None:
"""Auto-revive dead agents in casual mode."""
config = self._get_config()
if not config.auto_revive_enabled:
return
with get_db_session() as db:
dead_agents = db.query(Agent).filter(Agent.status == "Dead").all()
for agent in dead_agents:
if agent.death_tick is None:
continue
ticks_dead = self._tick_count - agent.death_tick
if ticks_dead >= config.auto_revive_delay_ticks:
agent.status = "Alive"
agent.hp = config.revive_hp
agent.energy = config.revive_energy
agent.mood = 50
agent.mood_state = "neutral"
agent.death_tick = None
await self._broadcast_event(EventType.AUTO_REVIVE, {
"agent_name": agent.name,
"message": f"{agent.name} has been revived!"
})
logger.info(f"Agent {agent.name} auto-revived")
# =========================================================================
# Social system (Phase 5)
# =========================================================================
async def _process_social_tick(self) -> None:
"""Process autonomous social interactions between agents."""
config = self._get_config()
if random.random() > config.social_interaction_probability:
return
with get_db_session() as db:
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
if len(alive_agents) < 2:
return
# Select initiator (extroverts more likely)
weights = []
for agent in alive_agents:
if agent.social_tendency == "extrovert":
weights.append(2.0)
elif agent.social_tendency == "introvert":
weights.append(0.5)
else:
weights.append(1.0)
initiator = random.choices(alive_agents, weights=weights)[0]
# Select target (not self)
targets = [a for a in alive_agents if a.id != initiator.id]
if not targets:
return
target = random.choice(targets)
# Get or create relationship
relationship = db.query(AgentRelationship).filter(
AgentRelationship.agent_from_id == initiator.id,
AgentRelationship.agent_to_id == target.id
).first()
if not relationship:
relationship = AgentRelationship(
agent_from_id=initiator.id,
agent_to_id=target.id
)
db.add(relationship)
db.flush()
# Determine interaction type
interaction_type = self._select_interaction(initiator, target, relationship)
if not interaction_type:
return
# Apply interaction effects
effects = SOCIAL_INTERACTIONS[interaction_type]
affection_change = random.randint(*effects["affection"])
trust_change = random.randint(*effects.get("trust", (0, 0)))
relationship.affection = max(-100, min(100, relationship.affection + affection_change))
relationship.trust = max(-100, min(100, relationship.trust + trust_change))
relationship.interaction_count += 1
relationship.last_interaction_tick = self._tick_count
relationship.update_relationship_type()
# Store data for broadcasting
interaction_data = {
"initiator_id": initiator.id,
"initiator_name": initiator.name,
"target_id": target.id,
"target_name": target.name,
"interaction_type": interaction_type,
"relationship_type": relationship.relationship_type
}
world = db.query(WorldState).first()
weather = world.weather if world else "Sunny"
time_of_day = world.time_of_day if world else "day"
# Generate LLM dialogue for interaction
asyncio.create_task(self._trigger_social_dialogue(
interaction_data, weather, time_of_day
))
def _select_interaction(self, initiator: Agent, target: Agent, relationship: AgentRelationship) -> Optional[str]:
"""Select appropriate interaction type based on conditions."""
valid_interactions = []
weights = []
for itype, config in SOCIAL_INTERACTIONS.items():
# Check conditions
if "min_energy" in config and initiator.energy < config["min_energy"]:
continue
if "max_mood" in config and initiator.mood > config["max_mood"]:
continue
if "target_max_mood" in config and target.mood > config["target_max_mood"]:
continue
valid_interactions.append(itype)
weights.append(config["weight"])
if not valid_interactions:
return None
return random.choices(valid_interactions, weights=weights)[0]
async def _trigger_social_dialogue(self, interaction_data: dict, weather: str, time_of_day: str) -> None:
"""Generate and broadcast social interaction dialogue."""
try:
dialogue = await llm_service.generate_social_interaction(
initiator_name=interaction_data["initiator_name"],
target_name=interaction_data["target_name"],
interaction_type=interaction_data["interaction_type"],
relationship_type=interaction_data["relationship_type"],
weather=weather,
time_of_day=time_of_day
)
await self._broadcast_event(EventType.SOCIAL_INTERACTION, {
**interaction_data,
"dialogue": dialogue
})
except Exception as e:
logger.error(f"Error in social dialogue: {e}")
# =========================================================================
# LLM-powered agent speech
# =========================================================================
async def _trigger_agent_speak(
self, agent_id: int, agent_name: str, agent_personality: str,
agent_hp: int, agent_energy: int, agent_mood: int,
event_description: str, event_type: str = "feed"
) -> None:
"""Fire-and-forget LLM call to generate agent speech."""
try:
class AgentSnapshot:
def __init__(self, name, personality, hp, energy, mood):
self.name = name
self.personality = personality
self.hp = hp
self.energy = energy
self.mood = mood
agent_snapshot = AgentSnapshot(
agent_name, agent_personality, agent_hp, agent_energy, agent_mood
)
text = await llm_service.generate_reaction(agent_snapshot, event_description, event_type)
await self._broadcast_event(EventType.AGENT_SPEAK, {
"agent_id": agent_id,
"agent_name": agent_name,
"text": text
})
except Exception as e:
logger.error(f"Error in agent speak: {e}")
async def _trigger_idle_chat(self) -> None:
"""Randomly select an alive agent to say something."""
with get_db_session() as db:
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
world = db.query(WorldState).first()
weather = world.weather if world else "Sunny"
time_of_day = world.time_of_day if world else "day"
if not alive_agents:
return
agent = random.choice(alive_agents)
agent_data = {
"id": agent.id, "name": agent.name, "personality": agent.personality,
"hp": agent.hp, "energy": agent.energy, "mood": agent.mood,
"mood_state": agent.mood_state
}
try:
class AgentSnapshot:
def __init__(self, name, personality, hp, energy, mood):
self.name = name
self.personality = personality
self.hp = hp
self.energy = energy
self.mood = mood
agent_snapshot = AgentSnapshot(
agent_data["name"], agent_data["personality"],
agent_data["hp"], agent_data["energy"], agent_data["mood"]
)
text = await llm_service.generate_idle_chat(agent_snapshot, weather, time_of_day)
await self._broadcast_event(EventType.AGENT_SPEAK, {
"agent_id": agent_data["id"],
"agent_name": agent_data["name"],
"text": text
})
except Exception as e:
logger.error(f"Error in idle chat: {e}")
# =========================================================================
# Command handlers
# =========================================================================
async def _handle_feed(self, username: str, agent_name: str) -> None:
"""Handle feed command."""
feed_result = None
with get_db_session() as db:
user = self._get_or_create_user(db, username)
agent = db.query(Agent).filter(Agent.name.ilike(agent_name)).first()
if agent is None:
await self._broadcast_event(EventType.ERROR, {"message": f"Agent '{agent_name}' not found"})
return
if agent.status != "Alive":
await self._broadcast_event(EventType.ERROR, {"message": f"{agent.name} is dead"})
return
if user.gold < FEED_COST:
await self._broadcast_event(EventType.ERROR, {
"user": username, "message": f"Not enough gold! Need {FEED_COST}, have {user.gold}"
})
return
user.gold -= FEED_COST
old_energy = agent.energy
agent.energy = min(100, agent.energy + FEED_ENERGY_RESTORE)
agent.mood = min(100, agent.mood + 5)
feed_result = {
"agent_id": agent.id, "agent_name": agent.name,
"agent_personality": agent.personality, "agent_hp": agent.hp,
"agent_energy": agent.energy, "agent_mood": agent.mood,
"actual_restore": agent.energy - old_energy, "user_gold": user.gold
}
if feed_result:
await self._broadcast_event(EventType.FEED, {
"user": username, "agent_name": feed_result["agent_name"],
"energy_restored": feed_result["actual_restore"],
"agent_energy": feed_result["agent_energy"], "user_gold": feed_result["user_gold"],
"message": f"{username} fed {feed_result['agent_name']}!"
})
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": feed_result["user_gold"]})
asyncio.create_task(self._trigger_agent_speak(
feed_result["agent_id"], feed_result["agent_name"],
feed_result["agent_personality"], feed_result["agent_hp"],
feed_result["agent_energy"], feed_result["agent_mood"],
f"User {username} gave you food!", "feed"
))
async def _handle_heal(self, username: str, agent_name: str) -> None:
"""Handle heal command."""
with get_db_session() as db:
user = self._get_or_create_user(db, username)
agent = db.query(Agent).filter(Agent.name.ilike(agent_name)).first()
if agent is None:
await self._broadcast_event(EventType.ERROR, {"message": f"Agent '{agent_name}' not found"})
return
if agent.status != "Alive":
await self._broadcast_event(EventType.ERROR, {"message": f"{agent.name} is dead"})
return
if user.gold < HEAL_COST:
await self._broadcast_event(EventType.ERROR, {
"user": username, "message": f"Not enough gold! Need {HEAL_COST}, have {user.gold}"
})
return
user.gold -= HEAL_COST
old_hp = agent.hp
agent.hp = min(100, agent.hp + HEAL_HP_RESTORE)
await self._broadcast_event(EventType.HEAL, {
"user": username, "agent_name": agent.name,
"hp_restored": agent.hp - old_hp, "agent_hp": agent.hp,
"user_gold": user.gold, "message": f"{username} healed {agent.name}!"
})
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": user.gold})
asyncio.create_task(self._trigger_agent_speak(
agent.id, agent.name, agent.personality, agent.hp, agent.energy, agent.mood,
f"User {username} healed you!", "heal"
))
async def _handle_encourage(self, username: str, agent_name: str) -> None:
"""Handle encourage command."""
with get_db_session() as db:
user = self._get_or_create_user(db, username)
agent = db.query(Agent).filter(Agent.name.ilike(agent_name)).first()
if agent is None:
await self._broadcast_event(EventType.ERROR, {"message": f"Agent '{agent_name}' not found"})
return
if agent.status != "Alive":
await self._broadcast_event(EventType.ERROR, {"message": f"{agent.name} is dead"})
return
if user.gold < ENCOURAGE_COST:
await self._broadcast_event(EventType.ERROR, {
"user": username, "message": f"Not enough gold! Need {ENCOURAGE_COST}, have {user.gold}"
})
return
user.gold -= ENCOURAGE_COST
old_mood = agent.mood
agent.mood = min(100, agent.mood + ENCOURAGE_MOOD_BOOST)
await self._broadcast_event(EventType.ENCOURAGE, {
"user": username, "agent_name": agent.name,
"mood_boost": agent.mood - old_mood, "agent_mood": agent.mood,
"user_gold": user.gold, "message": f"{username} encouraged {agent.name}!"
})
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": user.gold})
asyncio.create_task(self._trigger_agent_speak(
agent.id, agent.name, agent.personality, agent.hp, agent.energy, agent.mood,
f"User {username} encouraged you!", "encourage"
))
async def _handle_talk(self, username: str, agent_name: str, topic: str = "") -> None:
"""Handle talk command - free conversation with agent."""
with get_db_session() as db:
agent = db.query(Agent).filter(Agent.name.ilike(agent_name)).first()
if agent is None:
await self._broadcast_event(EventType.ERROR, {"message": f"Agent '{agent_name}' not found"})
return
if agent.status != "Alive":
await self._broadcast_event(EventType.ERROR, {"message": f"{agent.name} is dead"})
return
agent_data = {
"id": agent.id, "name": agent.name, "personality": agent.personality,
"hp": agent.hp, "energy": agent.energy, "mood": agent.mood
}
# Generate conversation response
try:
response = await llm_service.generate_conversation_response(
agent_name=agent_data["name"],
agent_personality=agent_data["personality"],
agent_mood=agent_data["mood"],
username=username,
topic=topic or "just chatting"
)
await self._broadcast_event(EventType.TALK, {
"user": username, "agent_name": agent_data["name"],
"topic": topic, "response": response
})
except Exception as e:
logger.error(f"Error in talk: {e}")
async def _handle_revive(self, username: str, agent_name: str) -> None:
"""Handle revive command (casual mode)."""
config = self._get_config()
with get_db_session() as db:
user = self._get_or_create_user(db, username)
agent = db.query(Agent).filter(Agent.name.ilike(agent_name)).first()
if agent is None:
await self._broadcast_event(EventType.ERROR, {"message": f"Agent '{agent_name}' not found"})
return
if agent.status == "Alive":
await self._broadcast_event(EventType.ERROR, {"message": f"{agent.name} is already alive!"})
return
if user.gold < REVIVE_COST:
await self._broadcast_event(EventType.ERROR, {
"user": username, "message": f"Not enough gold! Need {REVIVE_COST}, have {user.gold}"
})
return
user.gold -= REVIVE_COST
agent.status = "Alive"
agent.hp = config.revive_hp
agent.energy = config.revive_energy
agent.mood = 50
agent.mood_state = "neutral"
agent.death_tick = None
await self._broadcast_event(EventType.REVIVE, {
"user": username, "agent_name": agent.name,
"user_gold": user.gold, "message": f"{username} revived {agent.name}!"
})
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": user.gold})
async def _handle_check(self, username: str) -> None:
"""Handle check/status command."""
with get_db_session() as db:
user = self._get_or_create_user(db, username)
agents = db.query(Agent).all()
world = db.query(WorldState).first()
config = db.query(GameConfig).first()
user_data = {"username": user.username, "gold": user.gold}
agents_data = [agent.to_dict() for agent in agents]
world_data = world.to_dict() if world else {}
config_data = config.to_dict() if config else {}
await self._broadcast_event(EventType.CHECK, {
"user": user_data, "agents": agents_data,
"world": world_data, "config": config_data,
"message": f"{username}'s status - Gold: {user_data['gold']}"
})
async def _handle_reset(self, username: str) -> None:
"""Handle reset command - reset all agents."""
with get_db_session() as db:
agents = db.query(Agent).all()
for agent in agents:
agent.hp = 100
agent.energy = 100
agent.mood = 70
agent.mood_state = "neutral"
agent.status = "Alive"
agent.death_tick = None
world = db.query(WorldState).first()
if world:
world.day_count = 1
world.current_tick_in_day = 0
world.time_of_day = "day"
world.weather = "Sunny"
world.weather_duration = 0
await self._broadcast_event(EventType.SYSTEM, {
"message": f"{username} triggered a restart! All survivors have been revived."
})
await self._broadcast_agents_status()
async def process_comment(self, user: str, message: str) -> None:
"""Process a comment through command matching."""
await self._broadcast_event(EventType.COMMENT, {"user": user, "message": message})
# Match commands in priority order
if match := FEED_PATTERN.search(message):
await self._handle_feed(user, match.group(1))
return
if match := HEAL_PATTERN.search(message):
await self._handle_heal(user, match.group(1))
return
if match := TALK_PATTERN.search(message):
topic = match.group(2) or ""
await self._handle_talk(user, match.group(1), topic.strip())
return
if match := ENCOURAGE_PATTERN.search(message):
await self._handle_encourage(user, match.group(1))
return
if match := REVIVE_PATTERN.search(message):
await self._handle_revive(user, match.group(1))
return
if CHECK_PATTERN.search(message):
await self._handle_check(user)
return
if RESET_PATTERN.search(message):
await self._handle_reset(user)
return
# =========================================================================
# Game loop
# =========================================================================
async def _game_loop(self) -> None:
"""The main game loop with all systems."""
logger.info("Game loop started - The Island awaits...")
await self._broadcast_agents_status()
await self._broadcast_world_status()
while self._running:
self._tick_count += 1
# 1. Advance time (Phase 2)
phase_change = await self._advance_time()
if phase_change:
await self._broadcast_event(EventType.PHASE_CHANGE, {
"old_phase": phase_change["old_phase"],
"new_phase": phase_change["new_phase"],
"day": phase_change["day"],
"message": f"The {phase_change['new_phase']} begins..."
})
# 2. Update weather (Phase 3)
weather_change = await self._update_weather()
if weather_change:
await self._broadcast_event(EventType.WEATHER_CHANGE, {
"old_weather": weather_change["old_weather"],
"new_weather": weather_change["new_weather"],
"message": f"Weather changed to {weather_change['new_weather']}"
})
# 3. Process survival (with difficulty modifiers)
await self._process_survival_tick()
# 4. Auto-revive check (casual mode)
await self._process_auto_revive()
# 5. Update moods (Phase 3)
await self._update_moods()
# 6. Social interactions (Phase 5)
await self._process_social_tick()
# 7. Idle chat
with get_db_session() as db:
alive_count = db.query(Agent).filter(Agent.status == "Alive").count()
if alive_count > 0 and random.random() < IDLE_CHAT_PROBABILITY:
asyncio.create_task(self._trigger_idle_chat())
# 8. Broadcast states
await self._broadcast_agents_status()
# Tick event
with get_db_session() as db:
world = db.query(WorldState).first()
day = world.day_count if world else 1
time_of_day = world.time_of_day if world else "day"
weather = world.weather if world else "Sunny"
await self._broadcast_event(EventType.TICK, {
"tick": self._tick_count,
"day": day,
"time_of_day": time_of_day,
"weather": weather,
"alive_agents": alive_count
})
await asyncio.sleep(self._tick_interval)
logger.info("Game loop stopped")
async def start(self) -> None:
"""Start the game engine."""
if self._running:
logger.warning("Engine already running")
return
logger.info("Initializing database...")
init_db()
self._seed_initial_data()
# Reload config
self._config = None
self._get_config()
self._running = True
asyncio.create_task(self._game_loop())
logger.info("Game engine started - The Island awaits...")
async def stop(self) -> None:
"""Stop the game engine."""
self._running = False
logger.info("Game engine stopping...")
async def process_command(self, user: str, text: str) -> None:
"""Process a command from Twitch chat."""
# Use the existing process_comment method to handle commands
await self.process_comment(user, text)
async def process_bits(self, user: str, amount: int) -> None:
"""Process Twitch bits and convert to game gold."""
# 1 Bit = 1 Gold conversion rate
gold_added = amount
with get_db_session() as db:
user_obj = self._get_or_create_user(db, user)
user_obj.gold += gold_added
await self._broadcast_event(EventType.USER_UPDATE, {
"user": user,
"gold": user_obj.gold,
"message": f"{user} received {gold_added} gold from {amount} bits!"
})
# Also broadcast a special bits event for UI effects
await self._broadcast_event("bits_received", {
"user": user,
"bits": amount,
"gold": gold_added
})
logger.info(f"Processed bits: {user} -> {amount} bits -> {gold_added} gold")