feat: initialize interactive live-stream game backend MVP
- Add FastAPI backend with WebSocket support - Implement ConnectionManager for client connections - Create GameEngine with async game loop (2s tick) - Add RuleBasedAgent for keyword-based responses - Define Pydantic schemas for GameEvent protocol - Create debug frontend dashboard for testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
6
backend/app/__init__.py
Normal file
6
backend/app/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""
|
||||||
|
The Island - Interactive Live-Stream Game Backend
|
||||||
|
A commercial-grade backend for real-time game interactions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
82
backend/app/agents.py
Normal file
82
backend/app/agents.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
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}"
|
||||||
145
backend/app/engine.py
Normal file
145
backend/app/engine.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
Core Game Engine - The "Heartbeat" of the game.
|
||||||
|
Runs the main game loop and coordinates between comments and agents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .schemas import GameEvent, EventType
|
||||||
|
from .agents import BaseAgent, RuleBasedAgent
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .server import ConnectionManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GameEngine:
|
||||||
|
"""
|
||||||
|
The core game engine that runs the main game loop.
|
||||||
|
|
||||||
|
Simulates live comments, processes them through agents,
|
||||||
|
and broadcasts responses to connected clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, connection_manager: "ConnectionManager") -> None:
|
||||||
|
"""
|
||||||
|
Initialize the game engine.
|
||||||
|
|
||||||
|
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!"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Check if the engine is currently running."""
|
||||||
|
return self._running
|
||||||
|
|
||||||
|
def _generate_mock_comment(self) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Generate a mock live comment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (username, comment_text)
|
||||||
|
"""
|
||||||
|
user = random.choice(self._mock_users)
|
||||||
|
action = random.choice(self._mock_actions)
|
||||||
|
return user, action
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
event = GameEvent(
|
||||||
|
event_type=event_type,
|
||||||
|
timestamp=time.time(),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
await self._manager.broadcast(event)
|
||||||
|
|
||||||
|
async def process_comment(self, user: str, message: str) -> None:
|
||||||
|
"""
|
||||||
|
Process a comment (real or mock) through the agent pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: Username of the commenter
|
||||||
|
message: The comment text
|
||||||
|
"""
|
||||||
|
# Broadcast the incoming comment
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
logger.info("Game loop started")
|
||||||
|
|
||||||
|
while self._running:
|
||||||
|
self._tick_count += 1
|
||||||
|
|
||||||
|
# Broadcast tick event
|
||||||
|
await self._broadcast_event(
|
||||||
|
EventType.TICK,
|
||||||
|
{"tick": self._tick_count}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate and process mock comment
|
||||||
|
user, message = self._generate_mock_comment()
|
||||||
|
logger.info(f"Tick {self._tick_count}: {user} says '{message}'")
|
||||||
|
|
||||||
|
await self.process_comment(user, message)
|
||||||
|
|
||||||
|
# Wait for next tick
|
||||||
|
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."""
|
||||||
|
if self._running:
|
||||||
|
logger.warning("Engine already running")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._running = True
|
||||||
|
asyncio.create_task(self._game_loop())
|
||||||
|
logger.info("Game engine started")
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""Stop the game engine loop."""
|
||||||
|
self._running = False
|
||||||
|
logger.info("Game engine stopping...")
|
||||||
102
backend/app/main.py
Normal file
102
backend/app/main.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
FastAPI entry point for the interactive live-stream game backend.
|
||||||
|
Configures the application, WebSocket routes, and lifecycle events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from .server import ConnectionManager
|
||||||
|
from .engine import GameEngine
|
||||||
|
from .schemas import GameEvent, ClientMessage, EventType
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Global instances
|
||||||
|
manager = ConnectionManager()
|
||||||
|
engine = GameEngine(manager)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""
|
||||||
|
Application lifespan manager.
|
||||||
|
Starts the game engine on startup and stops it on shutdown.
|
||||||
|
"""
|
||||||
|
logger.info("Starting application...")
|
||||||
|
await engine.start()
|
||||||
|
yield
|
||||||
|
logger.info("Shutting down application...")
|
||||||
|
await engine.stop()
|
||||||
|
|
||||||
|
|
||||||
|
# Create FastAPI application
|
||||||
|
app = FastAPI(
|
||||||
|
title="The Island - Live Stream Game Backend",
|
||||||
|
description="Commercial-grade interactive live-stream game backend",
|
||||||
|
version="0.1.0",
|
||||||
|
lifespan=lifespan
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure CORS for frontend access
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Health check endpoint."""
|
||||||
|
return {
|
||||||
|
"status": "running",
|
||||||
|
"service": "The Island Game Backend",
|
||||||
|
"connections": manager.connection_count,
|
||||||
|
"engine_running": engine.is_running
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
"""
|
||||||
|
WebSocket endpoint for real-time game communication.
|
||||||
|
|
||||||
|
Handles client connections and processes incoming messages.
|
||||||
|
"""
|
||||||
|
await manager.connect(websocket)
|
||||||
|
|
||||||
|
# Send welcome message
|
||||||
|
welcome = GameEvent(
|
||||||
|
event_type=EventType.SYSTEM,
|
||||||
|
data={"message": "Connected to The Island!"}
|
||||||
|
)
|
||||||
|
await manager.send_personal(websocket, welcome)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Receive and parse client message
|
||||||
|
data = await websocket.receive_json()
|
||||||
|
message = ClientMessage(**data)
|
||||||
|
|
||||||
|
# Handle mock comment action
|
||||||
|
if message.action == "send_comment":
|
||||||
|
user = message.payload.get("user", "Anonymous")
|
||||||
|
text = message.payload.get("message", "")
|
||||||
|
await engine.process_comment(user, text)
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"WebSocket error: {e}")
|
||||||
|
manager.disconnect(websocket)
|
||||||
53
backend/app/schemas.py
Normal file
53
backend/app/schemas.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
Pydantic models for the JSON protocol.
|
||||||
|
Defines standardized structures for game events and messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
class GameEvent(BaseModel):
|
||||||
|
"""
|
||||||
|
Standardized game event structure for WebSocket communication.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
event_type: The type of event (comment, agent_response, tick, etc.)
|
||||||
|
timestamp: Unix timestamp when the event was created
|
||||||
|
data: Arbitrary payload data for the event
|
||||||
|
"""
|
||||||
|
event_type: str = Field(..., description="Type of the game event")
|
||||||
|
timestamp: float = Field(default_factory=time.time, description="Unix timestamp")
|
||||||
|
data: dict[str, Any] = Field(default_factory=dict, description="Event payload")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"event_type": "comment",
|
||||||
|
"timestamp": 1704067200.0,
|
||||||
|
"data": {"user": "User123", "message": "Attack!"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ClientMessage(BaseModel):
|
||||||
|
"""
|
||||||
|
Message structure for client-to-server communication.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
action: The action the client wants to perform
|
||||||
|
payload: Data associated with the action
|
||||||
|
"""
|
||||||
|
action: str = Field(..., description="Action to perform")
|
||||||
|
payload: dict[str, Any] = Field(default_factory=dict, description="Action payload")
|
||||||
90
backend/app/server.py
Normal file
90
backend/app/server.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
WebSocket connection manager and server utilities.
|
||||||
|
Handles client connections, disconnections, and message broadcasting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
from .schemas import GameEvent
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
"""
|
||||||
|
Manages WebSocket connections for real-time communication.
|
||||||
|
|
||||||
|
Handles connection lifecycle and provides broadcast capabilities
|
||||||
|
to all connected clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the connection manager with an empty connection list."""
|
||||||
|
self._active_connections: list[WebSocket] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection_count(self) -> int:
|
||||||
|
"""Return the number of active connections."""
|
||||||
|
return len(self._active_connections)
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket) -> None:
|
||||||
|
"""
|
||||||
|
Accept and register a new WebSocket connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
websocket: The WebSocket connection to accept
|
||||||
|
"""
|
||||||
|
await websocket.accept()
|
||||||
|
self._active_connections.append(websocket)
|
||||||
|
logger.info(f"Client connected. Total connections: {self.connection_count}")
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket) -> None:
|
||||||
|
"""
|
||||||
|
Remove a WebSocket connection from the active list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
websocket: The WebSocket connection to remove
|
||||||
|
"""
|
||||||
|
if websocket in self._active_connections:
|
||||||
|
self._active_connections.remove(websocket)
|
||||||
|
logger.info(f"Client disconnected. Total connections: {self.connection_count}")
|
||||||
|
|
||||||
|
async def broadcast(self, event: GameEvent) -> None:
|
||||||
|
"""
|
||||||
|
Send a GameEvent to all connected clients.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: The GameEvent to broadcast
|
||||||
|
"""
|
||||||
|
if not self._active_connections:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = event.model_dump_json()
|
||||||
|
disconnected: list[WebSocket] = []
|
||||||
|
|
||||||
|
for connection in self._active_connections:
|
||||||
|
try:
|
||||||
|
await connection.send_text(message)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to send to client: {e}")
|
||||||
|
disconnected.append(connection)
|
||||||
|
|
||||||
|
# Clean up failed connections
|
||||||
|
for conn in disconnected:
|
||||||
|
self.disconnect(conn)
|
||||||
|
|
||||||
|
async def send_personal(self, websocket: WebSocket, event: GameEvent) -> None:
|
||||||
|
"""
|
||||||
|
Send a GameEvent to a specific client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
websocket: The target WebSocket connection
|
||||||
|
event: The GameEvent to send
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await websocket.send_text(event.model_dump_json())
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to send personal message: {e}")
|
||||||
|
self.disconnect(websocket)
|
||||||
4
backend/requirements.txt
Normal file
4
backend/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
fastapi>=0.109.0
|
||||||
|
uvicorn[standard]>=0.27.0
|
||||||
|
websockets>=12.0
|
||||||
|
pydantic>=2.5.0
|
||||||
157
frontend/app.js
Normal file
157
frontend/app.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* The Island - Debug Client JavaScript
|
||||||
|
* Handles WebSocket connection and UI interactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
const WS_URL = 'ws://localhost:8000/ws';
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const statusDot = document.getElementById('statusDot');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const connectBtn = document.getElementById('connectBtn');
|
||||||
|
const eventLog = document.getElementById('eventLog');
|
||||||
|
const usernameInput = document.getElementById('username');
|
||||||
|
const messageInput = document.getElementById('message');
|
||||||
|
const autoScrollCheckbox = document.getElementById('autoScroll');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle WebSocket connection
|
||||||
|
*/
|
||||||
|
function toggleConnection() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
} else {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establish WebSocket connection
|
||||||
|
*/
|
||||||
|
function connect() {
|
||||||
|
statusText.textContent = 'Connecting...';
|
||||||
|
connectBtn.disabled = true;
|
||||||
|
|
||||||
|
ws = new WebSocket(WS_URL);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
statusDot.classList.add('connected');
|
||||||
|
statusText.textContent = 'Connected';
|
||||||
|
connectBtn.textContent = 'Disconnect';
|
||||||
|
connectBtn.disabled = false;
|
||||||
|
logEvent({ event_type: 'system', data: { message: 'WebSocket connected' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
statusDot.classList.remove('connected');
|
||||||
|
statusText.textContent = 'Disconnected';
|
||||||
|
connectBtn.textContent = 'Connect';
|
||||||
|
connectBtn.disabled = false;
|
||||||
|
logEvent({ event_type: 'system', data: { message: 'WebSocket disconnected' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
logEvent({ event_type: 'error', data: { message: 'Connection error' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
logEvent(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse message:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a mock comment to the server
|
||||||
|
*/
|
||||||
|
function sendComment() {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
alert('Not connected to server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = usernameInput.value.trim() || 'Anonymous';
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
alert('Please enter a message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
action: 'send_comment',
|
||||||
|
payload: { user, message }
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
messageInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format timestamp for display
|
||||||
|
*/
|
||||||
|
function formatTime(timestamp) {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
return date.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format event data for display
|
||||||
|
*/
|
||||||
|
function formatEventData(eventType, data) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'comment':
|
||||||
|
return `${data.user}: ${data.message}`;
|
||||||
|
case 'agent_response':
|
||||||
|
return data.response;
|
||||||
|
case 'tick':
|
||||||
|
return `Tick #${data.tick}`;
|
||||||
|
case 'system':
|
||||||
|
case 'error':
|
||||||
|
return data.message;
|
||||||
|
default:
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an event to the display
|
||||||
|
*/
|
||||||
|
function logEvent(event) {
|
||||||
|
const eventType = event.event_type || 'unknown';
|
||||||
|
const timestamp = event.timestamp || Date.now() / 1000;
|
||||||
|
const data = event.data || {};
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `event ${eventType}`;
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="event-time">${formatTime(timestamp)}</span>
|
||||||
|
<span class="event-type">${eventType}</span>
|
||||||
|
<div class="event-data">${formatEventData(eventType, data)}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
eventLog.appendChild(div);
|
||||||
|
|
||||||
|
if (autoScrollCheckbox.checked) {
|
||||||
|
eventLog.scrollTop = eventLog.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the event log
|
||||||
|
*/
|
||||||
|
function clearLog() {
|
||||||
|
eventLog.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to send comment
|
||||||
|
messageInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
sendComment();
|
||||||
|
}
|
||||||
|
});
|
||||||
179
frontend/debug_client.html
Normal file
179
frontend/debug_client.html
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>The Island - Debug Client</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
color: #eee;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #00d4ff;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
}
|
||||||
|
.status-dot.connected {
|
||||||
|
background: #44ff44;
|
||||||
|
box-shadow: 0 0 10px rgba(68, 255, 68, 0.5);
|
||||||
|
}
|
||||||
|
.panels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.panel h2 {
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #00d4ff;
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #00b8e6;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.controls label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.event-log {
|
||||||
|
height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.event {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid;
|
||||||
|
}
|
||||||
|
.event.comment { border-color: #ffaa00; background: rgba(255, 170, 0, 0.1); }
|
||||||
|
.event.agent_response { border-color: #00ff88; background: rgba(0, 255, 136, 0.1); }
|
||||||
|
.event.tick { border-color: #888; background: rgba(136, 136, 136, 0.1); }
|
||||||
|
.event.system { border-color: #00d4ff; background: rgba(0, 212, 255, 0.1); }
|
||||||
|
.event.error { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
|
||||||
|
.event-time { color: #888; font-size: 11px; }
|
||||||
|
.event-type { font-weight: bold; text-transform: uppercase; font-size: 11px; }
|
||||||
|
.event-data { margin-top: 5px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>The Island - Debug Client</h1>
|
||||||
|
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot" id="statusDot"></div>
|
||||||
|
<span id="statusText">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
<button id="connectBtn" onclick="toggleConnection()">Connect</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panels">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Send Mock Comment</h2>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="username" placeholder="Username" value="TestUser">
|
||||||
|
<input type="text" id="message" placeholder="Message (try: Attack!, Heal, Help...)">
|
||||||
|
<button onclick="sendComment()">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Event Log</h2>
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="clearLog()">Clear Log</button>
|
||||||
|
<label><input type="checkbox" id="autoScroll" checked> Auto-scroll</label>
|
||||||
|
</div>
|
||||||
|
<div class="event-log" id="eventLog"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
run.py
Normal file
16
run.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Startup script for The Island game backend.
|
||||||
|
Runs the FastAPI server with uvicorn and hot-reloading enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"backend.app.main:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8000,
|
||||||
|
reload=True,
|
||||||
|
log_level="info"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user