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