feat: pivot to island survival simulation with SQLite persistence
Phase 3 - "The Island" transformation: - Add SQLAlchemy + SQLite for data persistence (database.py) - Rewrite models.py with User, Agent, WorldState ORM models - Refactor engine.py for survival mechanics (energy decay, starvation) - Implement feed command (10 gold -> +20 energy) - Auto-seed 3 NPCs on startup (Jack/Luna/Bob) - Update frontend with agent card view and Chinese UI - Remove old Boss/Player RPG mechanics - Add .gitignore for database and cache files - Fix SQLAlchemy session detachment issue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
"""
|
||||
Agent classes for processing game inputs and generating responses.
|
||||
Provides base abstraction and rule-based implementation.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAgent(ABC):
|
||||
"""
|
||||
Abstract base class for all game agents.
|
||||
|
||||
Agents process input text and generate appropriate responses.
|
||||
Subclasses must implement the process_input method.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "BaseAgent") -> None:
|
||||
"""
|
||||
Initialize the agent.
|
||||
|
||||
Args:
|
||||
name: Display name for the agent
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
@abstractmethod
|
||||
def process_input(self, text: str) -> str:
|
||||
"""
|
||||
Process input text and return a response.
|
||||
|
||||
Args:
|
||||
text: The input text to process
|
||||
|
||||
Returns:
|
||||
The agent's response string
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RuleBasedAgent(BaseAgent):
|
||||
"""
|
||||
A simple rule-based agent that responds based on keyword matching.
|
||||
|
||||
This agent does not use LLMs - it returns static strings based on
|
||||
detected keywords in the input text.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "RuleBot") -> None:
|
||||
"""Initialize the rule-based agent."""
|
||||
super().__init__(name)
|
||||
self._rules: dict[str, str] = {
|
||||
"attack": "Defending! Shield activated!",
|
||||
"heal": "Healing spell cast! +50 HP",
|
||||
"run": "Too slow! You can't escape!",
|
||||
"help": "Allies are on the way!",
|
||||
"fire": "Water shield deployed!",
|
||||
"magic": "Counter-spell activated!",
|
||||
}
|
||||
self._default_response = "I heard you! Processing command..."
|
||||
|
||||
def process_input(self, text: str) -> str:
|
||||
"""
|
||||
Process input and return response based on keyword rules.
|
||||
|
||||
Args:
|
||||
text: The input text to analyze
|
||||
|
||||
Returns:
|
||||
Response string based on matched keywords
|
||||
"""
|
||||
text_lower = text.lower()
|
||||
|
||||
for keyword, response in self._rules.items():
|
||||
if keyword in text_lower:
|
||||
logger.info(f"Agent matched keyword: {keyword}")
|
||||
return f"{self.name}: {response}"
|
||||
|
||||
logger.info("Agent using default response")
|
||||
return f"{self.name}: {self._default_response}"
|
||||
61
backend/app/database.py
Normal file
61
backend/app/database.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Database configuration for The Island.
|
||||
SQLite + SQLAlchemy ORM setup.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
|
||||
# Database file path (in project root)
|
||||
DB_PATH = Path(__file__).parent.parent.parent / "island.db"
|
||||
DATABASE_URL = f"sqlite:///{DB_PATH}"
|
||||
|
||||
# Create engine with SQLite optimizations
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
connect_args={"check_same_thread": False}, # Required for SQLite with FastAPI
|
||||
echo=False # Set True for SQL debugging
|
||||
)
|
||||
|
||||
# Session factory
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Base class for ORM models
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize database tables."""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
Dependency for getting database session.
|
||||
Use with FastAPI's Depends() or as context manager.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_session():
|
||||
"""
|
||||
Context manager for database session.
|
||||
Usage: with get_db_session() as db: ...
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Core Game Engine - The "Heartbeat" of the game.
|
||||
Manages game state, processes commands, and coordinates broadcasts.
|
||||
Core Game Engine - The Island Survival Simulation.
|
||||
Manages survival mechanics, agent states, and user interactions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -10,35 +10,43 @@ import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .schemas import GameEvent, EventType
|
||||
from .models import Player, Boss
|
||||
from .database import init_db, get_db_session
|
||||
from .models import User, Agent, WorldState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import ConnectionManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Command patterns for string matching
|
||||
ATTACK_PATTERN = re.compile(r"(attack|攻击|打|砍|杀)", re.IGNORECASE)
|
||||
HEAL_PATTERN = re.compile(r"(heal|治疗|回血|加血|恢复)", re.IGNORECASE)
|
||||
STATUS_PATTERN = re.compile(r"(status|查询|状态|信息)", re.IGNORECASE)
|
||||
# Command patterns
|
||||
FEED_PATTERN = re.compile(r"feed\s+(\w+)", re.IGNORECASE)
|
||||
CHECK_PATTERN = re.compile(r"(check|查询|状态)", re.IGNORECASE)
|
||||
|
||||
# Game constants
|
||||
ATTACK_DAMAGE = 10
|
||||
ATTACK_GOLD_REWARD = 10
|
||||
HEAL_AMOUNT = 10
|
||||
BOSS_COUNTER_DAMAGE = 15 # Boss反击伤害
|
||||
TICK_INTERVAL = 5.0 # Seconds between ticks
|
||||
ENERGY_DECAY_PER_TICK = 2 # Energy lost per tick
|
||||
HP_DECAY_WHEN_STARVING = 5 # HP lost when energy is 0
|
||||
FEED_COST = 10 # Gold cost to feed an agent
|
||||
FEED_ENERGY_RESTORE = 20 # Energy restored when fed
|
||||
INITIAL_USER_GOLD = 100 # Starting gold for new users
|
||||
|
||||
# Initial NPC data
|
||||
INITIAL_AGENTS = [
|
||||
{"name": "Jack", "personality": "勇敢"},
|
||||
{"name": "Luna", "personality": "狡猾"},
|
||||
{"name": "Bob", "personality": "老实"},
|
||||
]
|
||||
|
||||
|
||||
class GameEngine:
|
||||
"""
|
||||
The core game engine that manages RPG state and game loop.
|
||||
|
||||
Manages players, boss, and processes commands through string matching.
|
||||
The core game engine for island survival simulation.
|
||||
Manages agents, users, and survival mechanics with database persistence.
|
||||
"""
|
||||
|
||||
def __init__(self, connection_manager: "ConnectionManager") -> None:
|
||||
"""
|
||||
Initialize the game engine with state storage.
|
||||
Initialize the game engine.
|
||||
|
||||
Args:
|
||||
connection_manager: The WebSocket connection manager for broadcasting
|
||||
@@ -46,40 +54,50 @@ class GameEngine:
|
||||
self._manager = connection_manager
|
||||
self._running = False
|
||||
self._tick_count = 0
|
||||
self._tick_interval = 2.0 # seconds
|
||||
|
||||
# Game state
|
||||
self.players: dict[str, Player] = {}
|
||||
self.boss = Boss(name="Dragon", hp=1000, max_hp=1000)
|
||||
self._tick_interval = TICK_INTERVAL
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if the engine is currently running."""
|
||||
return self._running
|
||||
|
||||
def _get_or_create_player(self, username: str) -> Player:
|
||||
"""
|
||||
Get existing player or create new one.
|
||||
def _seed_initial_data(self) -> None:
|
||||
"""Seed initial agents and world state if database is empty."""
|
||||
with get_db_session() as db:
|
||||
# Check if agents exist
|
||||
agent_count = db.query(Agent).count()
|
||||
if agent_count == 0:
|
||||
logger.info("Seeding initial agents...")
|
||||
for agent_data in INITIAL_AGENTS:
|
||||
agent = Agent(
|
||||
name=agent_data["name"],
|
||||
personality=agent_data["personality"],
|
||||
status="Alive",
|
||||
hp=100,
|
||||
energy=100
|
||||
)
|
||||
db.add(agent)
|
||||
logger.info(f"Created {len(INITIAL_AGENTS)} initial agents")
|
||||
|
||||
Args:
|
||||
username: The player's username
|
||||
# Check if world state exists
|
||||
world = db.query(WorldState).first()
|
||||
if world is None:
|
||||
logger.info("Seeding initial world state...")
|
||||
world = WorldState(day_count=1, weather="Sunny", resource_level=100)
|
||||
db.add(world)
|
||||
|
||||
Returns:
|
||||
Player instance
|
||||
"""
|
||||
if username not in self.players:
|
||||
self.players[username] = Player(name=username)
|
||||
logger.info(f"New player registered: {username}")
|
||||
return self.players[username]
|
||||
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() # Get the ID without committing
|
||||
logger.info(f"New user registered: {username}")
|
||||
return user
|
||||
|
||||
async def _broadcast_event(self, event_type: str, data: dict) -> None:
|
||||
"""
|
||||
Create and broadcast a game event.
|
||||
|
||||
Args:
|
||||
event_type: Type of the event
|
||||
data: Event payload data
|
||||
"""
|
||||
"""Create and broadcast a game event."""
|
||||
event = GameEvent(
|
||||
event_type=event_type,
|
||||
timestamp=time.time(),
|
||||
@@ -87,216 +105,245 @@ class GameEngine:
|
||||
)
|
||||
await self._manager.broadcast(event)
|
||||
|
||||
async def _broadcast_boss_update(self) -> None:
|
||||
"""Broadcast current boss status to all clients."""
|
||||
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.BOSS_UPDATE,
|
||||
{
|
||||
"boss_name": self.boss.name,
|
||||
"boss_hp": self.boss.hp,
|
||||
"boss_max_hp": self.boss.max_hp,
|
||||
"boss_hp_percentage": self.boss.hp_percentage
|
||||
}
|
||||
EventType.AGENTS_UPDATE,
|
||||
{"agents": agents_data}
|
||||
)
|
||||
|
||||
async def _handle_attack(self, player: Player) -> None:
|
||||
async def _process_survival_tick(self) -> None:
|
||||
"""
|
||||
Handle attack command. Boss will counter-attack!
|
||||
Process survival mechanics for all alive agents.
|
||||
- Decrease energy
|
||||
- If energy <= 0, decrease HP
|
||||
- If HP <= 0, mark as Dead
|
||||
"""
|
||||
deaths = []
|
||||
|
||||
Args:
|
||||
player: The attacking player
|
||||
"""
|
||||
if not self.boss.is_alive:
|
||||
with get_db_session() as db:
|
||||
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
|
||||
for agent in alive_agents:
|
||||
# Energy decay
|
||||
agent.energy = max(0, agent.energy - ENERGY_DECAY_PER_TICK)
|
||||
|
||||
# Starvation damage
|
||||
if agent.energy <= 0:
|
||||
agent.hp = max(0, agent.hp - HP_DECAY_WHEN_STARVING)
|
||||
|
||||
# Check for death
|
||||
if agent.hp <= 0:
|
||||
agent.status = "Dead"
|
||||
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.SYSTEM,
|
||||
{"message": f"Boss is already defeated! Waiting for respawn..."}
|
||||
EventType.AGENT_DIED,
|
||||
{
|
||||
"agent_name": death["name"],
|
||||
"message": f"💀 {death['name']}({death['personality']})因饥饿而死亡..."
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
# Player attacks boss
|
||||
damage = self.boss.take_damage(ATTACK_DAMAGE)
|
||||
player.add_gold(ATTACK_GOLD_REWARD)
|
||||
|
||||
# Boss counter-attacks player
|
||||
counter_damage = player.take_damage(BOSS_COUNTER_DAMAGE)
|
||||
|
||||
await self._broadcast_event(
|
||||
EventType.ATTACK,
|
||||
{
|
||||
"user": player.name,
|
||||
"damage": damage,
|
||||
"counter_damage": counter_damage,
|
||||
"gold_earned": ATTACK_GOLD_REWARD,
|
||||
"boss_hp": self.boss.hp,
|
||||
"boss_max_hp": self.boss.max_hp,
|
||||
"player_hp": player.hp,
|
||||
"player_max_hp": player.max_hp,
|
||||
"player_gold": player.gold,
|
||||
"message": f"⚔️ {player.name} attacked {self.boss.name}! "
|
||||
f"Dealt {damage} dmg, received {counter_damage} counter-attack. "
|
||||
f"HP: {player.hp}/{player.max_hp} | Boss: {self.boss.hp}/{self.boss.max_hp}"
|
||||
}
|
||||
)
|
||||
|
||||
await self._broadcast_boss_update()
|
||||
|
||||
# Check if player died
|
||||
if not player.is_alive:
|
||||
await self._handle_player_death(player)
|
||||
return
|
||||
|
||||
# Check if boss defeated
|
||||
if not self.boss.is_alive:
|
||||
await self._handle_boss_defeated()
|
||||
|
||||
async def _handle_player_death(self, player: Player) -> None:
|
||||
"""Handle player death - respawn with full HP but lose half gold."""
|
||||
lost_gold = player.gold // 2
|
||||
player.gold -= lost_gold
|
||||
player.hp = player.max_hp # Respawn with full HP
|
||||
|
||||
await self._broadcast_event(
|
||||
EventType.SYSTEM,
|
||||
{
|
||||
"user": player.name,
|
||||
"player_hp": player.hp,
|
||||
"player_max_hp": player.max_hp,
|
||||
"player_gold": player.gold,
|
||||
"message": f"💀 {player.name} was slain by {self.boss.name}! "
|
||||
f"Lost {lost_gold} gold. Respawned with full HP."
|
||||
}
|
||||
)
|
||||
|
||||
async def _handle_boss_defeated(self) -> None:
|
||||
"""Handle boss defeat and reset."""
|
||||
await self._broadcast_event(
|
||||
EventType.BOSS_DEFEATED,
|
||||
{
|
||||
"boss_name": self.boss.name,
|
||||
"message": f"🎉 {self.boss.name} has been defeated! A new boss will spawn soon..."
|
||||
}
|
||||
)
|
||||
|
||||
# Reset boss after short delay
|
||||
await asyncio.sleep(3.0)
|
||||
self.boss.reset()
|
||||
await self._broadcast_event(
|
||||
EventType.SYSTEM,
|
||||
{"message": f"⚔️ {self.boss.name} has respawned with full HP!"}
|
||||
)
|
||||
await self._broadcast_boss_update()
|
||||
|
||||
async def _handle_heal(self, player: Player) -> None:
|
||||
async def _handle_feed(self, username: str, agent_name: str) -> None:
|
||||
"""
|
||||
Handle heal command.
|
||||
Handle feed command.
|
||||
|
||||
Args:
|
||||
player: The healing player
|
||||
username: The user feeding the agent
|
||||
agent_name: Name of the agent to feed
|
||||
"""
|
||||
healed = player.heal(HEAL_AMOUNT)
|
||||
# Variables to store data for broadcasting (outside session)
|
||||
feed_result = None
|
||||
|
||||
await self._broadcast_event(
|
||||
EventType.HEAL,
|
||||
{
|
||||
"user": player.name,
|
||||
"healed": healed,
|
||||
"player_hp": player.hp,
|
||||
"player_max_hp": player.max_hp,
|
||||
"message": f"{player.name} healed themselves. "
|
||||
f"Restored {healed} HP. HP: {player.hp}/{player.max_hp}"
|
||||
with get_db_session() as db:
|
||||
user = self._get_or_create_user(db, username)
|
||||
|
||||
# Find the agent
|
||||
agent = db.query(Agent).filter(
|
||||
Agent.name.ilike(agent_name)
|
||||
).first()
|
||||
|
||||
if agent is None:
|
||||
await self._broadcast_event(
|
||||
EventType.ERROR,
|
||||
{"message": f"找不到名为 {agent_name} 的角色"}
|
||||
)
|
||||
return
|
||||
|
||||
if agent.status != "Alive":
|
||||
await self._broadcast_event(
|
||||
EventType.ERROR,
|
||||
{"message": f"{agent.name} 已经死亡,无法投喂"}
|
||||
)
|
||||
return
|
||||
|
||||
if user.gold < FEED_COST:
|
||||
await self._broadcast_event(
|
||||
EventType.ERROR,
|
||||
{
|
||||
"user": username,
|
||||
"message": f"金币不足!需要 {FEED_COST} 金币,当前只有 {user.gold} 金币"
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
# Perform feed
|
||||
user.gold -= FEED_COST
|
||||
old_energy = agent.energy
|
||||
agent.energy = min(100, agent.energy + FEED_ENERGY_RESTORE)
|
||||
actual_restore = agent.energy - old_energy
|
||||
|
||||
# Store data for broadcasting before session closes
|
||||
feed_result = {
|
||||
"agent_name": agent.name,
|
||||
"actual_restore": actual_restore,
|
||||
"agent_energy": agent.energy,
|
||||
"user_gold": user.gold
|
||||
}
|
||||
)
|
||||
|
||||
async def _handle_status(self, player: Player) -> None:
|
||||
"""
|
||||
Handle status query command.
|
||||
# Broadcast outside of session
|
||||
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} 投喂了 {feed_result['agent_name']}!"
|
||||
f"恢复 {feed_result['actual_restore']} 点体力(当前: {feed_result['agent_energy']}/100)"
|
||||
}
|
||||
)
|
||||
|
||||
await self._broadcast_event(
|
||||
EventType.USER_UPDATE,
|
||||
{
|
||||
"user": username,
|
||||
"gold": feed_result["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()
|
||||
|
||||
# Extract data before session closes
|
||||
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 {}
|
||||
message = f"📊 {username} 的状态 - 金币: {user_data['gold']}"
|
||||
|
||||
Args:
|
||||
player: The querying player
|
||||
"""
|
||||
await self._broadcast_event(
|
||||
EventType.STATUS,
|
||||
EventType.CHECK,
|
||||
{
|
||||
"user": player.name,
|
||||
"player_hp": player.hp,
|
||||
"player_max_hp": player.max_hp,
|
||||
"player_gold": player.gold,
|
||||
"boss_name": self.boss.name,
|
||||
"boss_hp": self.boss.hp,
|
||||
"boss_max_hp": self.boss.max_hp,
|
||||
"message": f"📊 {player.name}'s Status - HP: {player.hp}/{player.max_hp}, "
|
||||
f"Gold: {player.gold} | Boss {self.boss.name}: {self.boss.hp}/{self.boss.max_hp}"
|
||||
"user": user_data,
|
||||
"agents": agents_data,
|
||||
"world": world_data,
|
||||
"message": message
|
||||
}
|
||||
)
|
||||
|
||||
async def process_comment(self, user: str, message: str) -> None:
|
||||
"""
|
||||
Process a comment through string matching command system.
|
||||
Process a comment through command matching.
|
||||
|
||||
Args:
|
||||
user: Username of the commenter
|
||||
message: The comment text
|
||||
"""
|
||||
# Get or create player
|
||||
player = self._get_or_create_player(user)
|
||||
|
||||
# Broadcast the incoming comment first
|
||||
# Broadcast the incoming comment
|
||||
await self._broadcast_event(
|
||||
EventType.COMMENT,
|
||||
{"user": user, "message": message}
|
||||
)
|
||||
|
||||
# Process command through string matching
|
||||
if ATTACK_PATTERN.search(message):
|
||||
await self._handle_attack(player)
|
||||
elif HEAL_PATTERN.search(message):
|
||||
await self._handle_heal(player)
|
||||
elif STATUS_PATTERN.search(message):
|
||||
await self._handle_status(player)
|
||||
# If no command matched, treat as regular chat (no action needed)
|
||||
# Match commands
|
||||
feed_match = FEED_PATTERN.search(message)
|
||||
if feed_match:
|
||||
agent_name = feed_match.group(1)
|
||||
await self._handle_feed(user, agent_name)
|
||||
return
|
||||
|
||||
if CHECK_PATTERN.search(message):
|
||||
await self._handle_check(user)
|
||||
return
|
||||
|
||||
# No command matched - treat as regular chat
|
||||
|
||||
async def _game_loop(self) -> None:
|
||||
"""
|
||||
The main game loop - runs continuously while engine is active.
|
||||
The main game loop - survival simulation.
|
||||
|
||||
Every tick broadcasts current game state.
|
||||
Every tick:
|
||||
1. Process survival mechanics (energy/HP decay)
|
||||
2. Broadcast agent states
|
||||
"""
|
||||
logger.info("Game loop started")
|
||||
logger.info("Game loop started - Island survival simulation")
|
||||
|
||||
# Broadcast initial boss state
|
||||
await self._broadcast_boss_update()
|
||||
# Initial broadcast
|
||||
await self._broadcast_agents_status()
|
||||
|
||||
while self._running:
|
||||
self._tick_count += 1
|
||||
|
||||
# Broadcast tick event with game state
|
||||
# Process survival mechanics
|
||||
await self._process_survival_tick()
|
||||
|
||||
# Broadcast current state
|
||||
await self._broadcast_agents_status()
|
||||
|
||||
# Broadcast tick event
|
||||
with get_db_session() as db:
|
||||
alive_count = db.query(Agent).filter(Agent.status == "Alive").count()
|
||||
world = db.query(WorldState).first()
|
||||
day = world.day_count if world else 1
|
||||
|
||||
await self._broadcast_event(
|
||||
EventType.TICK,
|
||||
{
|
||||
"tick": self._tick_count,
|
||||
"boss_hp": self.boss.hp,
|
||||
"boss_max_hp": self.boss.max_hp,
|
||||
"player_count": len(self.players)
|
||||
"day": day,
|
||||
"alive_agents": alive_count
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(f"Tick {self._tick_count}: Boss HP {self.boss.hp}/{self.boss.max_hp}")
|
||||
logger.debug(f"Tick {self._tick_count}: {alive_count} agents alive")
|
||||
|
||||
await asyncio.sleep(self._tick_interval)
|
||||
|
||||
logger.info("Game loop stopped")
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the game engine loop as a background task."""
|
||||
"""Start the game engine."""
|
||||
if self._running:
|
||||
logger.warning("Engine already running")
|
||||
return
|
||||
|
||||
# Initialize database and seed data
|
||||
logger.info("Initializing database...")
|
||||
init_db()
|
||||
self._seed_initial_data()
|
||||
|
||||
self._running = True
|
||||
asyncio.create_task(self._game_loop())
|
||||
logger.info("Game engine started")
|
||||
logger.info("Game engine started - The Island awaits...")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the game engine loop."""
|
||||
"""Stop the game engine."""
|
||||
self._running = False
|
||||
logger.info("Game engine stopping...")
|
||||
|
||||
@@ -1,66 +1,81 @@
|
||||
"""
|
||||
Game entity models for The Island.
|
||||
Defines Player and Boss data structures.
|
||||
SQLAlchemy ORM models for The Island.
|
||||
Defines User (viewers), Agent (NPCs), and WorldState entities.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, func
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class Player(BaseModel):
|
||||
class User(Base):
|
||||
"""
|
||||
Represents a player in the game world.
|
||||
Represents a viewer/donor from the live stream.
|
||||
They can spend gold to influence the island.
|
||||
"""
|
||||
name: str
|
||||
hp: int = Field(default=100, ge=0)
|
||||
max_hp: int = Field(default=100, gt=0)
|
||||
gold: int = Field(default=0, ge=0)
|
||||
__tablename__ = "users"
|
||||
|
||||
def take_damage(self, amount: int) -> int:
|
||||
"""Apply damage to player, returns actual damage dealt."""
|
||||
actual = min(amount, self.hp)
|
||||
self.hp -= actual
|
||||
return actual
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(100), unique=True, index=True, nullable=False)
|
||||
gold = Column(Integer, default=100) # Starting gold for new users
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
|
||||
def heal(self, amount: int) -> int:
|
||||
"""Heal player, returns actual HP restored."""
|
||||
actual = min(amount, self.max_hp - self.hp)
|
||||
self.hp += actual
|
||||
return actual
|
||||
|
||||
def add_gold(self, amount: int) -> None:
|
||||
"""Add gold to player."""
|
||||
self.gold += amount
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if player is alive."""
|
||||
return self.hp > 0
|
||||
def __repr__(self):
|
||||
return f"<User {self.username} gold={self.gold}>"
|
||||
|
||||
|
||||
class Boss(BaseModel):
|
||||
class Agent(Base):
|
||||
"""
|
||||
Represents a boss enemy in the game.
|
||||
Represents an NPC survivor on the island.
|
||||
Has personality, health, energy, and inventory.
|
||||
"""
|
||||
name: str
|
||||
hp: int = Field(ge=0)
|
||||
max_hp: int = Field(gt=0)
|
||||
__tablename__ = "agents"
|
||||
|
||||
def take_damage(self, amount: int) -> int:
|
||||
"""Apply damage to boss, returns actual damage dealt."""
|
||||
actual = min(amount, self.hp)
|
||||
self.hp -= actual
|
||||
return actual
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(50), unique=True, nullable=False)
|
||||
personality = Column(String(50), nullable=False)
|
||||
status = Column(String(20), default="Alive") # Alive, Exiled, Dead
|
||||
hp = Column(Integer, default=100)
|
||||
energy = Column(Integer, default=100)
|
||||
inventory = Column(String(500), default="{}") # JSON string
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset boss to full HP."""
|
||||
self.hp = self.max_hp
|
||||
def __repr__(self):
|
||||
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Status={self.status}>"
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if boss is alive."""
|
||||
return self.hp > 0
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"personality": self.personality,
|
||||
"status": self.status,
|
||||
"hp": self.hp,
|
||||
"energy": self.energy,
|
||||
"inventory": self.inventory
|
||||
}
|
||||
|
||||
@property
|
||||
def hp_percentage(self) -> float:
|
||||
"""Get HP as percentage."""
|
||||
return (self.hp / self.max_hp) * 100 if self.max_hp > 0 else 0
|
||||
|
||||
class WorldState(Base):
|
||||
"""
|
||||
Global state of the island environment.
|
||||
Tracks day count, weather, and shared resources.
|
||||
"""
|
||||
__tablename__ = "world_state"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
day_count = Column(Integer, default=1)
|
||||
weather = Column(String(20), default="Sunny")
|
||||
resource_level = Column(Integer, default=100)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorldState Day={self.day_count} Weather={self.weather} Resources={self.resource_level}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
return {
|
||||
"day_count": self.day_count,
|
||||
"weather": self.weather,
|
||||
"resource_level": self.resource_level
|
||||
}
|
||||
|
||||
@@ -12,17 +12,16 @@ import time
|
||||
class EventType(str, Enum):
|
||||
"""Enumeration of all possible game event types."""
|
||||
COMMENT = "comment"
|
||||
AGENT_RESPONSE = "agent_response"
|
||||
TICK = "tick"
|
||||
SYSTEM = "system"
|
||||
ERROR = "error"
|
||||
# RPG-specific events
|
||||
ATTACK = "attack"
|
||||
HEAL = "heal"
|
||||
STATUS = "status"
|
||||
BOSS_UPDATE = "boss_update"
|
||||
BOSS_DEFEATED = "boss_defeated"
|
||||
PLAYER_UPDATE = "player_update"
|
||||
# Island survival events
|
||||
AGENTS_UPDATE = "agents_update" # All agents status broadcast
|
||||
AGENT_DIED = "agent_died" # An agent has died
|
||||
FEED = "feed" # User fed an agent
|
||||
USER_UPDATE = "user_update" # User gold/status update
|
||||
WORLD_UPDATE = "world_update" # World state update
|
||||
CHECK = "check" # Status check response
|
||||
|
||||
|
||||
class GameEvent(BaseModel):
|
||||
|
||||
@@ -2,3 +2,5 @@ fastapi>=0.109.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
websockets>=12.0
|
||||
pydantic>=2.5.0
|
||||
sqlalchemy>=2.0.0
|
||||
aiosqlite>=0.19.0
|
||||
|
||||
Reference in New Issue
Block a user