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:
empty
2025-12-30 16:50:07 +08:00
parent 714b5824ba
commit 4c08b00832
7 changed files with 615 additions and 51 deletions

View File

@@ -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")

View File

@@ -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
View 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

View File

@@ -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):

View File

@@ -1,10 +1,17 @@
/**
* The Island - Debug Client JavaScript
* Handles WebSocket connection and UI interactions
* Handles WebSocket connection, UI interactions, and game state display
*/
let ws = null;
const WS_URL = 'ws://localhost:8000/ws';
const WS_URL = 'ws://localhost:8080/ws';
// Player state (tracked from server events)
let playerState = {
hp: 100,
maxHp: 100,
gold: 0
};
// DOM Elements
const statusDot = document.getElementById('statusDot');
@@ -14,6 +21,17 @@ const eventLog = document.getElementById('eventLog');
const usernameInput = document.getElementById('username');
const messageInput = document.getElementById('message');
const autoScrollCheckbox = document.getElementById('autoScroll');
const hideTicksCheckbox = document.getElementById('hideTicks');
// Boss UI Elements
const bossName = document.getElementById('bossName');
const bossHpText = document.getElementById('bossHpText');
const bossHealthBar = document.getElementById('bossHealthBar');
const bossHealthLabel = document.getElementById('bossHealthLabel');
// Player UI Elements
const playerHpDisplay = document.getElementById('playerHp');
const playerGoldDisplay = document.getElementById('playerGold');
/**
* Toggle WebSocket connection
@@ -59,7 +77,7 @@ function connect() {
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
logEvent(data);
handleGameEvent(data);
} catch (e) {
console.error('Failed to parse message:', e);
}
@@ -67,7 +85,132 @@ function connect() {
}
/**
* Send a mock comment to the server
* Handle incoming game events
*/
function handleGameEvent(event) {
const eventType = event.event_type;
const data = event.data || {};
// Update UI based on event type
switch (eventType) {
case 'boss_update':
updateBossUI(data);
break;
case 'attack':
updateBossFromAttack(data);
updatePlayerFromEvent(data);
break;
case 'heal':
updatePlayerFromEvent(data);
break;
case 'status':
updatePlayerFromEvent(data);
updateBossFromStatus(data);
break;
case 'system':
// Handle player death/respawn updates
if (data.user && data.player_hp !== undefined) {
updatePlayerFromEvent(data);
}
break;
case 'tick':
if (data.boss_hp !== undefined) {
updateBossUI({
boss_hp: data.boss_hp,
boss_max_hp: data.boss_max_hp
});
}
break;
}
// Log the event
logEvent(event);
}
/**
* Update Boss health bar UI
*/
function updateBossUI(data) {
if (data.boss_name) {
bossName.textContent = data.boss_name;
}
if (data.boss_hp !== undefined && data.boss_max_hp !== undefined) {
const hp = data.boss_hp;
const maxHp = data.boss_max_hp;
const percentage = maxHp > 0 ? (hp / maxHp) * 100 : 0;
bossHpText.textContent = `HP: ${hp} / ${maxHp}`;
bossHealthBar.style.width = `${percentage}%`;
bossHealthLabel.textContent = `${Math.round(percentage)}%`;
// Change color based on HP percentage
if (percentage <= 25) {
bossHealthBar.style.background = 'linear-gradient(90deg, #ff2222 0%, #ff4444 100%)';
} else if (percentage <= 50) {
bossHealthBar.style.background = 'linear-gradient(90deg, #ff6600 0%, #ff8844 100%)';
} else {
bossHealthBar.style.background = 'linear-gradient(90deg, #ff4444 0%, #ff6666 100%)';
}
}
}
/**
* Update boss from attack event
*/
function updateBossFromAttack(data) {
if (data.boss_hp !== undefined && data.boss_max_hp !== undefined) {
updateBossUI({
boss_hp: data.boss_hp,
boss_max_hp: data.boss_max_hp
});
}
}
/**
* Update boss from status event
*/
function updateBossFromStatus(data) {
if (data.boss_hp !== undefined && data.boss_max_hp !== undefined) {
updateBossUI({
boss_name: data.boss_name,
boss_hp: data.boss_hp,
boss_max_hp: data.boss_max_hp
});
}
}
/**
* Update player stats from event data
*/
function updatePlayerFromEvent(data) {
const currentUser = usernameInput.value.trim() || 'Anonymous';
// Only update if this event is for the current user
if (data.user !== currentUser) return;
if (data.player_hp !== undefined) {
playerState.hp = data.player_hp;
}
if (data.player_max_hp !== undefined) {
playerState.maxHp = data.player_max_hp;
}
if (data.player_gold !== undefined) {
playerState.gold = data.player_gold;
}
updatePlayerUI();
}
/**
* Update player stats UI
*/
function updatePlayerUI() {
playerHpDisplay.textContent = `${playerState.hp}/${playerState.maxHp}`;
playerGoldDisplay.textContent = playerState.gold;
}
/**
* Send a comment/command to the server
*/
function sendComment() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
@@ -92,6 +235,25 @@ function sendComment() {
messageInput.value = '';
}
/**
* Quick action buttons
*/
function quickAction(action) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('Not connected to server');
return;
}
const user = usernameInput.value.trim() || 'Anonymous';
const payload = {
action: 'send_comment',
payload: { user, message: action }
};
ws.send(JSON.stringify(payload));
}
/**
* Format timestamp for display
*/
@@ -110,10 +272,17 @@ function formatEventData(eventType, data) {
case 'agent_response':
return data.response;
case 'tick':
return `Tick #${data.tick}`;
return `Tick #${data.tick} | Players: ${data.player_count || 0}`;
case 'system':
case 'error':
return data.message;
case 'attack':
case 'heal':
case 'status':
case 'boss_defeated':
return data.message;
case 'boss_update':
return `Boss ${data.boss_name}: ${data.boss_hp}/${data.boss_max_hp} (${Math.round(data.boss_hp_percentage)}%)`;
default:
return JSON.stringify(data);
}
@@ -127,6 +296,16 @@ function logEvent(event) {
const timestamp = event.timestamp || Date.now() / 1000;
const data = event.data || {};
// Skip tick events if checkbox is checked
if (eventType === 'tick' && hideTicksCheckbox && hideTicksCheckbox.checked) {
return;
}
// Skip boss_update events to reduce log noise (they're reflected in the UI)
if (eventType === 'boss_update') {
return;
}
const div = document.createElement('div');
div.className = `event ${eventType}`;
div.innerHTML = `

View File

@@ -51,6 +51,88 @@
background: #44ff44;
box-shadow: 0 0 10px rgba(68, 255, 68, 0.5);
}
/* Boss Health Bar */
.boss-panel {
background: rgba(255, 68, 68, 0.1);
border: 1px solid rgba(255, 68, 68, 0.3);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.boss-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.boss-name {
color: #ff6666;
font-size: 1.3rem;
font-weight: bold;
}
.boss-hp-text {
color: #ffaaaa;
font-size: 0.9rem;
}
.health-bar-container {
width: 100%;
height: 30px;
background: rgba(0, 0, 0, 0.4);
border-radius: 15px;
overflow: hidden;
position: relative;
}
.health-bar {
height: 100%;
background: linear-gradient(90deg, #ff4444 0%, #ff6666 100%);
border-radius: 15px;
transition: width 0.3s ease;
}
.health-bar-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
}
/* Player Stats Panel */
.player-panel {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.player-header {
color: #00d4ff;
font-size: 1.1rem;
margin-bottom: 15px;
}
.player-stats {
display: flex;
gap: 30px;
flex-wrap: wrap;
}
.stat-item {
display: flex;
align-items: center;
gap: 10px;
}
.stat-icon {
font-size: 1.5rem;
}
.stat-value {
font-size: 1.2rem;
font-weight: bold;
}
.stat-label {
font-size: 0.8rem;
color: #888;
}
.panels {
display: flex;
flex-direction: column;
@@ -104,6 +186,19 @@
background: #666;
cursor: not-allowed;
}
.quick-actions {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
.quick-btn {
padding: 8px 16px;
font-size: 13px;
}
.quick-btn.attack { background: #ff6666; }
.quick-btn.heal { background: #44ff88; }
.quick-btn.status { background: #ffaa44; }
.controls {
display: flex;
gap: 15px;
@@ -117,7 +212,7 @@
font-size: 14px;
}
.event-log {
height: 400px;
height: 350px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.4);
border-radius: 8px;
@@ -133,9 +228,15 @@
}
.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.tick { border-color: #888; background: rgba(136, 136, 136, 0.05); opacity: 0.6; }
.event.system { border-color: #00d4ff; background: rgba(0, 212, 255, 0.1); }
.event.error { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
/* RPG Event Styles */
.event.attack { border-color: #ff4444; background: rgba(255, 68, 68, 0.15); color: #ff8888; }
.event.heal { border-color: #44ff88; background: rgba(68, 255, 136, 0.15); color: #88ffaa; }
.event.status { border-color: #ffaa44; background: rgba(255, 170, 68, 0.1); }
.event.boss_update { border-color: #ff6666; background: rgba(255, 102, 102, 0.1); }
.event.boss_defeated { border-color: #ffdd00; background: rgba(255, 221, 0, 0.2); color: #ffee88; font-weight: bold; }
.event-time { color: #888; font-size: 11px; }
.event-type { font-weight: bold; text-transform: uppercase; font-size: 11px; }
.event-data { margin-top: 5px; }
@@ -153,14 +254,52 @@
<button id="connectBtn" onclick="toggleConnection()">Connect</button>
</div>
<!-- Boss Health Panel -->
<div class="boss-panel">
<div class="boss-header">
<span class="boss-name" id="bossName">Dragon</span>
<span class="boss-hp-text" id="bossHpText">HP: 1000 / 1000</span>
</div>
<div class="health-bar-container">
<div class="health-bar" id="bossHealthBar" style="width: 100%"></div>
<span class="health-bar-label" id="bossHealthLabel">100%</span>
</div>
</div>
<!-- Player Stats Panel -->
<div class="player-panel">
<div class="player-header">Your Stats</div>
<div class="player-stats">
<div class="stat-item">
<span class="stat-icon">❤️</span>
<div>
<div class="stat-value" id="playerHp">100/100</div>
<div class="stat-label">HP</div>
</div>
</div>
<div class="stat-item">
<span class="stat-icon">💰</span>
<div>
<div class="stat-value" id="playerGold">0</div>
<div class="stat-label">Gold</div>
</div>
</div>
</div>
</div>
<div class="panels">
<div class="panel">
<h2>Send Mock Comment</h2>
<h2>Send Command</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...)">
<input type="text" id="message" placeholder="Message (attack, heal, status...)">
<button onclick="sendComment()">Send</button>
</div>
<div class="quick-actions">
<button class="quick-btn attack" onclick="quickAction('attack')">⚔️ Attack</button>
<button class="quick-btn heal" onclick="quickAction('heal')">💚 Heal</button>
<button class="quick-btn status" onclick="quickAction('status')">📊 Status</button>
</div>
</div>
<div class="panel">
@@ -168,6 +307,7 @@
<div class="controls">
<button onclick="clearLog()">Clear Log</button>
<label><input type="checkbox" id="autoScroll" checked> Auto-scroll</label>
<label><input type="checkbox" id="hideTicks"> Hide ticks</label>
</div>
<div class="event-log" id="eventLog"></div>
</div>

2
run.py
View File

@@ -10,7 +10,7 @@ if __name__ == "__main__":
uvicorn.run(
"backend.app.main:app",
host="0.0.0.0",
port=8000,
port=8080,
reload=True,
log_level="info"
)