feat: implement basic RPG mechanics with boss counter-attack
- Add Player and Boss data models (models.py) - Implement command processing via regex matching (attack/heal/status) - Add boss counter-attack mechanism (15 dmg per player attack) - Add player death/respawn system (lose half gold, respawn full HP) - Update frontend with boss HP bar, player stats panel, quick action buttons - Add colored event log (red for attack, green for heal) - Change port from 8000 to 8080 🤖 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,63 +1,76 @@
|
||||
"""
|
||||
Core Game Engine - The "Heartbeat" of the game.
|
||||
Runs the main game loop and coordinates between comments and agents.
|
||||
Manages game state, processes commands, and coordinates broadcasts.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .schemas import GameEvent, EventType
|
||||
from .agents import BaseAgent, RuleBasedAgent
|
||||
from .models import Player, Boss
|
||||
|
||||
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)
|
||||
|
||||
# Game constants
|
||||
ATTACK_DAMAGE = 10
|
||||
ATTACK_GOLD_REWARD = 10
|
||||
HEAL_AMOUNT = 10
|
||||
BOSS_COUNTER_DAMAGE = 15 # Boss反击伤害
|
||||
|
||||
|
||||
class GameEngine:
|
||||
"""
|
||||
The core game engine that runs the main game loop.
|
||||
The core game engine that manages RPG state and game loop.
|
||||
|
||||
Simulates live comments, processes them through agents,
|
||||
and broadcasts responses to connected clients.
|
||||
Manages players, boss, and processes commands through string matching.
|
||||
"""
|
||||
|
||||
def __init__(self, connection_manager: "ConnectionManager") -> None:
|
||||
"""
|
||||
Initialize the game engine.
|
||||
Initialize the game engine with state storage.
|
||||
|
||||
Args:
|
||||
connection_manager: The WebSocket connection manager for broadcasting
|
||||
"""
|
||||
self._manager = connection_manager
|
||||
self._agent: BaseAgent = RuleBasedAgent(name="Guardian")
|
||||
self._running = False
|
||||
self._tick_count = 0
|
||||
self._tick_interval = 2.0 # seconds
|
||||
|
||||
# Mock comment templates
|
||||
self._mock_users = ["User123", "GamerPro", "DragonSlayer", "NightOwl", "StarGazer"]
|
||||
self._mock_actions = ["Attack!", "Heal me!", "Run away!", "Help!", "Fire spell!", "Magic blast!"]
|
||||
# Game state
|
||||
self.players: dict[str, Player] = {}
|
||||
self.boss = Boss(name="Dragon", hp=1000, max_hp=1000)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if the engine is currently running."""
|
||||
return self._running
|
||||
|
||||
def _generate_mock_comment(self) -> tuple[str, str]:
|
||||
def _get_or_create_player(self, username: str) -> Player:
|
||||
"""
|
||||
Generate a mock live comment.
|
||||
Get existing player or create new one.
|
||||
|
||||
Args:
|
||||
username: The player's username
|
||||
|
||||
Returns:
|
||||
Tuple of (username, comment_text)
|
||||
Player instance
|
||||
"""
|
||||
user = random.choice(self._mock_users)
|
||||
action = random.choice(self._mock_actions)
|
||||
return user, action
|
||||
if username not in self.players:
|
||||
self.players[username] = Player(name=username)
|
||||
logger.info(f"New player registered: {username}")
|
||||
return self.players[username]
|
||||
|
||||
async def _broadcast_event(self, event_type: str, data: dict) -> None:
|
||||
"""
|
||||
@@ -74,57 +87,201 @@ class GameEngine:
|
||||
)
|
||||
await self._manager.broadcast(event)
|
||||
|
||||
async def _broadcast_boss_update(self) -> None:
|
||||
"""Broadcast current boss status to all clients."""
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
async def _handle_attack(self, player: Player) -> None:
|
||||
"""
|
||||
Handle attack command. Boss will counter-attack!
|
||||
|
||||
Args:
|
||||
player: The attacking player
|
||||
"""
|
||||
if not self.boss.is_alive:
|
||||
await self._broadcast_event(
|
||||
EventType.SYSTEM,
|
||||
{"message": f"Boss is already defeated! Waiting for respawn..."}
|
||||
)
|
||||
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:
|
||||
"""
|
||||
Handle heal command.
|
||||
|
||||
Args:
|
||||
player: The healing player
|
||||
"""
|
||||
healed = player.heal(HEAL_AMOUNT)
|
||||
|
||||
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}"
|
||||
}
|
||||
)
|
||||
|
||||
async def _handle_status(self, player: Player) -> None:
|
||||
"""
|
||||
Handle status query command.
|
||||
|
||||
Args:
|
||||
player: The querying player
|
||||
"""
|
||||
await self._broadcast_event(
|
||||
EventType.STATUS,
|
||||
{
|
||||
"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}"
|
||||
}
|
||||
)
|
||||
|
||||
async def process_comment(self, user: str, message: str) -> None:
|
||||
"""
|
||||
Process a comment (real or mock) through the agent pipeline.
|
||||
Process a comment through string matching command system.
|
||||
|
||||
Args:
|
||||
user: Username of the commenter
|
||||
message: The comment text
|
||||
"""
|
||||
# Broadcast the incoming comment
|
||||
# Get or create player
|
||||
player = self._get_or_create_player(user)
|
||||
|
||||
# Broadcast the incoming comment first
|
||||
await self._broadcast_event(
|
||||
EventType.COMMENT,
|
||||
{"user": user, "message": message}
|
||||
)
|
||||
|
||||
# Process through agent
|
||||
full_comment = f"{user}: {message}"
|
||||
response = self._agent.process_input(full_comment)
|
||||
|
||||
# Broadcast agent response
|
||||
await self._broadcast_event(
|
||||
EventType.AGENT_RESPONSE,
|
||||
{"agent": self._agent.name, "response": response}
|
||||
)
|
||||
# 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)
|
||||
|
||||
async def _game_loop(self) -> None:
|
||||
"""
|
||||
The main game loop - runs continuously while engine is active.
|
||||
|
||||
Every tick:
|
||||
1. Simulates a mock live comment
|
||||
2. Passes it to the agent
|
||||
3. Broadcasts the response
|
||||
Every tick broadcasts current game state.
|
||||
"""
|
||||
logger.info("Game loop started")
|
||||
|
||||
# Broadcast initial boss state
|
||||
await self._broadcast_boss_update()
|
||||
|
||||
while self._running:
|
||||
self._tick_count += 1
|
||||
|
||||
# Broadcast tick event
|
||||
# Broadcast tick event with game state
|
||||
await self._broadcast_event(
|
||||
EventType.TICK,
|
||||
{"tick": self._tick_count}
|
||||
{
|
||||
"tick": self._tick_count,
|
||||
"boss_hp": self.boss.hp,
|
||||
"boss_max_hp": self.boss.max_hp,
|
||||
"player_count": len(self.players)
|
||||
}
|
||||
)
|
||||
|
||||
# Generate and process mock comment
|
||||
user, message = self._generate_mock_comment()
|
||||
logger.info(f"Tick {self._tick_count}: {user} says '{message}'")
|
||||
logger.debug(f"Tick {self._tick_count}: Boss HP {self.boss.hp}/{self.boss.max_hp}")
|
||||
|
||||
await self.process_comment(user, message)
|
||||
|
||||
# Wait for next tick
|
||||
await asyncio.sleep(self._tick_interval)
|
||||
|
||||
logger.info("Game loop stopped")
|
||||
|
||||
@@ -5,9 +5,12 @@ Configures the application, WebSocket routes, and lifecycle events.
|
||||
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from .server import ConnectionManager
|
||||
from .engine import GameEngine
|
||||
@@ -24,6 +27,9 @@ logger = logging.getLogger(__name__)
|
||||
manager = ConnectionManager()
|
||||
engine = GameEngine(manager)
|
||||
|
||||
# Frontend path
|
||||
FRONTEND_DIR = Path(__file__).parent.parent.parent / "frontend"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -55,9 +61,14 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Serve the debug client page."""
|
||||
return FileResponse(FRONTEND_DIR / "debug_client.html")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint."""
|
||||
return {
|
||||
"status": "running",
|
||||
@@ -100,3 +111,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error: {e}")
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
# Mount static files (must be after all routes)
|
||||
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
|
||||
|
||||
66
backend/app/models.py
Normal file
66
backend/app/models.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Game entity models for The Island.
|
||||
Defines Player and Boss data structures.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Player(BaseModel):
|
||||
"""
|
||||
Represents a player in the game world.
|
||||
"""
|
||||
name: str
|
||||
hp: int = Field(default=100, ge=0)
|
||||
max_hp: int = Field(default=100, gt=0)
|
||||
gold: int = Field(default=0, ge=0)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Boss(BaseModel):
|
||||
"""
|
||||
Represents a boss enemy in the game.
|
||||
"""
|
||||
name: str
|
||||
hp: int = Field(ge=0)
|
||||
max_hp: int = Field(gt=0)
|
||||
|
||||
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
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset boss to full HP."""
|
||||
self.hp = self.max_hp
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if boss is alive."""
|
||||
return self.hp > 0
|
||||
|
||||
@property
|
||||
def hp_percentage(self) -> float:
|
||||
"""Get HP as percentage."""
|
||||
return (self.hp / self.max_hp) * 100 if self.max_hp > 0 else 0
|
||||
@@ -16,6 +16,13 @@ class EventType(str, Enum):
|
||||
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"
|
||||
|
||||
|
||||
class GameEvent(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user