[new] web-based frontend
This commit is contained in:
parent
1423fc0dc9
commit
67dc007283
@ -121,6 +121,12 @@ class StatisticsSchema(BaseModel):
|
|||||||
total_agents_died: int
|
total_agents_died: int
|
||||||
total_money_in_circulation: int
|
total_money_in_circulation: int
|
||||||
professions: dict[str, int]
|
professions: dict[str, int]
|
||||||
|
# Wealth inequality metrics
|
||||||
|
avg_money: float = 0.0
|
||||||
|
median_money: int = 0
|
||||||
|
richest_agent: int = 0
|
||||||
|
poorest_agent: int = 0
|
||||||
|
gini_coefficient: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
class ActionLogSchema(BaseModel):
|
class ActionLogSchema(BaseModel):
|
||||||
@ -137,6 +143,18 @@ class TurnLogSchema(BaseModel):
|
|||||||
agent_actions: list[ActionLogSchema]
|
agent_actions: list[ActionLogSchema]
|
||||||
deaths: list[str]
|
deaths: list[str]
|
||||||
trades: list[dict]
|
trades: list[dict]
|
||||||
|
resources_produced: dict[str, int] = {}
|
||||||
|
resources_consumed: dict[str, int] = {}
|
||||||
|
resources_spoiled: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceStatsSchema(BaseModel):
|
||||||
|
"""Schema for resource statistics."""
|
||||||
|
produced: dict[str, int] = {}
|
||||||
|
consumed: dict[str, int] = {}
|
||||||
|
spoiled: dict[str, int] = {}
|
||||||
|
in_inventory: dict[str, int] = {}
|
||||||
|
in_market: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
class WorldStateResponse(BaseModel):
|
class WorldStateResponse(BaseModel):
|
||||||
@ -152,6 +170,7 @@ class WorldStateResponse(BaseModel):
|
|||||||
mode: str
|
mode: str
|
||||||
is_running: bool
|
is_running: bool
|
||||||
recent_logs: list[TurnLogSchema]
|
recent_logs: list[TurnLogSchema]
|
||||||
|
resource_stats: ResourceStatsSchema = ResourceStatsSchema()
|
||||||
|
|
||||||
|
|
||||||
# ============== Control Schemas ==============
|
# ============== Control Schemas ==============
|
||||||
|
|||||||
@ -31,6 +31,10 @@ class TurnLog:
|
|||||||
agent_actions: list[dict] = field(default_factory=list)
|
agent_actions: list[dict] = field(default_factory=list)
|
||||||
deaths: list[str] = field(default_factory=list)
|
deaths: list[str] = field(default_factory=list)
|
||||||
trades: list[dict] = field(default_factory=list)
|
trades: list[dict] = field(default_factory=list)
|
||||||
|
# Resource tracking for this turn
|
||||||
|
resources_produced: dict = field(default_factory=dict)
|
||||||
|
resources_consumed: dict = field(default_factory=dict)
|
||||||
|
resources_spoiled: dict = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@ -38,6 +42,9 @@ class TurnLog:
|
|||||||
"agent_actions": self.agent_actions,
|
"agent_actions": self.agent_actions,
|
||||||
"deaths": self.deaths,
|
"deaths": self.deaths,
|
||||||
"trades": self.trades,
|
"trades": self.trades,
|
||||||
|
"resources_produced": self.resources_produced,
|
||||||
|
"resources_consumed": self.resources_consumed,
|
||||||
|
"resources_spoiled": self.resources_spoiled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -66,6 +73,17 @@ class GameEngine:
|
|||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self.turn_logs: list[TurnLog] = []
|
self.turn_logs: list[TurnLog] = []
|
||||||
self.logger = get_simulation_logger()
|
self.logger = get_simulation_logger()
|
||||||
|
|
||||||
|
# Resource statistics tracking (cumulative)
|
||||||
|
self.resource_stats = {
|
||||||
|
"produced": {}, # Total resources produced
|
||||||
|
"consumed": {}, # Total resources consumed
|
||||||
|
"spoiled": {}, # Total resources spoiled
|
||||||
|
"traded": {}, # Total resources traded (bought/sold)
|
||||||
|
"in_market": {}, # Currently in market
|
||||||
|
"in_inventory": {}, # Currently in all inventories
|
||||||
|
}
|
||||||
|
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def reset(self, config: Optional[WorldConfig] = None) -> None:
|
def reset(self, config: Optional[WorldConfig] = None) -> None:
|
||||||
@ -80,6 +98,16 @@ class GameEngine:
|
|||||||
self.market = OrderBook()
|
self.market = OrderBook()
|
||||||
self.turn_logs = []
|
self.turn_logs = []
|
||||||
|
|
||||||
|
# Reset resource statistics
|
||||||
|
self.resource_stats = {
|
||||||
|
"produced": {},
|
||||||
|
"consumed": {},
|
||||||
|
"spoiled": {},
|
||||||
|
"traded": {},
|
||||||
|
"in_market": {},
|
||||||
|
"in_inventory": {},
|
||||||
|
}
|
||||||
|
|
||||||
# Reset and start new logging session
|
# Reset and start new logging session
|
||||||
self.logger = reset_simulation_logger()
|
self.logger = reset_simulation_logger()
|
||||||
sim_config = get_config()
|
sim_config = get_config()
|
||||||
@ -183,6 +211,25 @@ class GameEngine:
|
|||||||
# Complete agent action with result - this updates the indicator to show what was done
|
# Complete agent action with result - this updates the indicator to show what was done
|
||||||
if result:
|
if result:
|
||||||
agent.complete_action(result.success, result.message)
|
agent.complete_action(result.success, result.message)
|
||||||
|
# Log to agent's personal history
|
||||||
|
agent.log_action(
|
||||||
|
turn=current_turn,
|
||||||
|
action_type=decision.action.value,
|
||||||
|
result=result.message,
|
||||||
|
success=result.success,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track resources produced
|
||||||
|
for res in result.resources_gained:
|
||||||
|
res_type = res.type.value
|
||||||
|
turn_log.resources_produced[res_type] = turn_log.resources_produced.get(res_type, 0) + res.quantity
|
||||||
|
self.resource_stats["produced"][res_type] = self.resource_stats["produced"].get(res_type, 0) + res.quantity
|
||||||
|
|
||||||
|
# Track resources consumed
|
||||||
|
for res in result.resources_consumed:
|
||||||
|
res_type = res.type.value
|
||||||
|
turn_log.resources_consumed[res_type] = turn_log.resources_consumed.get(res_type, 0) + res.quantity
|
||||||
|
self.resource_stats["consumed"][res_type] = self.resource_stats["consumed"].get(res_type, 0) + res.quantity
|
||||||
|
|
||||||
turn_log.agent_actions.append({
|
turn_log.agent_actions.append({
|
||||||
"agent_id": agent.id,
|
"agent_id": agent.id,
|
||||||
@ -215,6 +262,11 @@ class GameEngine:
|
|||||||
# 6. Decay resources in inventories
|
# 6. Decay resources in inventories
|
||||||
for agent in self.world.get_living_agents():
|
for agent in self.world.get_living_agents():
|
||||||
expired = agent.decay_inventory(current_turn)
|
expired = agent.decay_inventory(current_turn)
|
||||||
|
# Track spoiled resources
|
||||||
|
for res in expired:
|
||||||
|
res_type = res.type.value
|
||||||
|
turn_log.resources_spoiled[res_type] = turn_log.resources_spoiled.get(res_type, 0) + res.quantity
|
||||||
|
self.resource_stats["spoiled"][res_type] = self.resource_stats["spoiled"].get(res_type, 0) + res.quantity
|
||||||
|
|
||||||
# 7. Mark newly dead agents as corpses (don't remove yet for visualization)
|
# 7. Mark newly dead agents as corpses (don't remove yet for visualization)
|
||||||
newly_dead = self._mark_dead_agents(current_turn)
|
newly_dead = self._mark_dead_agents(current_turn)
|
||||||
@ -297,9 +349,17 @@ class GameEngine:
|
|||||||
elif action == ActionType.CONSUME:
|
elif action == ActionType.CONSUME:
|
||||||
if decision.target_resource:
|
if decision.target_resource:
|
||||||
success = agent.consume(decision.target_resource)
|
success = agent.consume(decision.target_resource)
|
||||||
|
consumed_list = []
|
||||||
|
if success:
|
||||||
|
consumed_list.append(Resource(
|
||||||
|
type=decision.target_resource,
|
||||||
|
quantity=1,
|
||||||
|
created_turn=self.world.current_turn,
|
||||||
|
))
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
action_type=action,
|
action_type=action,
|
||||||
success=success,
|
success=success,
|
||||||
|
resources_consumed=consumed_list,
|
||||||
message=f"Consumed {decision.target_resource.value}" if success else "Nothing to consume",
|
message=f"Consumed {decision.target_resource.value}" if success else "Nothing to consume",
|
||||||
)
|
)
|
||||||
return ActionResult(action_type=action, success=False, message="No resource specified")
|
return ActionResult(action_type=action, success=False, message="No resource specified")
|
||||||
@ -318,6 +378,7 @@ class GameEngine:
|
|||||||
success=True,
|
success=True,
|
||||||
energy_spent=abs(config.energy_cost),
|
energy_spent=abs(config.energy_cost),
|
||||||
heat_gained=fire_heat,
|
heat_gained=fire_heat,
|
||||||
|
resources_consumed=[Resource(type=ResourceType.WOOD, quantity=1, created_turn=self.world.current_turn)],
|
||||||
message="Built a warm fire",
|
message="Built a warm fire",
|
||||||
)
|
)
|
||||||
return ActionResult(action_type=action, success=False, message="No wood for fire")
|
return ActionResult(action_type=action, success=False, message="No wood for fire")
|
||||||
@ -350,6 +411,7 @@ class GameEngine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check required materials
|
# Check required materials
|
||||||
|
resources_consumed = []
|
||||||
if config.requires_resource:
|
if config.requires_resource:
|
||||||
if not agent.has_resource(config.requires_resource, config.requires_quantity):
|
if not agent.has_resource(config.requires_resource, config.requires_quantity):
|
||||||
agent.restore_energy(energy_cost) # Refund energy
|
agent.restore_energy(energy_cost) # Refund energy
|
||||||
@ -359,6 +421,11 @@ class GameEngine:
|
|||||||
message=f"Missing required {config.requires_resource.value}",
|
message=f"Missing required {config.requires_resource.value}",
|
||||||
)
|
)
|
||||||
agent.remove_from_inventory(config.requires_resource, config.requires_quantity)
|
agent.remove_from_inventory(config.requires_resource, config.requires_quantity)
|
||||||
|
resources_consumed.append(Resource(
|
||||||
|
type=config.requires_resource,
|
||||||
|
quantity=config.requires_quantity,
|
||||||
|
created_turn=self.world.current_turn,
|
||||||
|
))
|
||||||
|
|
||||||
# Get relevant skill for this action
|
# Get relevant skill for this action
|
||||||
skill_name = self._get_skill_for_action(action)
|
skill_name = self._get_skill_for_action(action)
|
||||||
@ -434,6 +501,7 @@ class GameEngine:
|
|||||||
success=True,
|
success=True,
|
||||||
energy_spent=energy_cost,
|
energy_spent=energy_cost,
|
||||||
resources_gained=resources_gained,
|
resources_gained=resources_gained,
|
||||||
|
resources_consumed=resources_consumed,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -483,6 +551,10 @@ class GameEngine:
|
|||||||
self.world.current_turn,
|
self.world.current_turn,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Track traded resources
|
||||||
|
res_type = result.resource_type.value
|
||||||
|
self.resource_stats["traded"][res_type] = self.resource_stats["traded"].get(res_type, 0) + result.quantity
|
||||||
|
|
||||||
# Deduct money from buyer
|
# Deduct money from buyer
|
||||||
agent.money -= result.total_paid
|
agent.money -= result.total_paid
|
||||||
|
|
||||||
@ -612,6 +684,10 @@ class GameEngine:
|
|||||||
self.world.current_turn
|
self.world.current_turn
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Track traded resources
|
||||||
|
res_type = result.resource_type.value
|
||||||
|
self.resource_stats["traded"][res_type] = self.resource_stats["traded"].get(res_type, 0) + result.quantity
|
||||||
|
|
||||||
resource = Resource(
|
resource = Resource(
|
||||||
type=result.resource_type,
|
type=result.resource_type,
|
||||||
quantity=result.quantity,
|
quantity=result.quantity,
|
||||||
@ -684,6 +760,31 @@ class GameEngine:
|
|||||||
"recent_logs": [
|
"recent_logs": [
|
||||||
log.to_dict() for log in self.turn_logs[-5:]
|
log.to_dict() for log in self.turn_logs[-5:]
|
||||||
],
|
],
|
||||||
|
"resource_stats": self._get_resource_stats(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_resource_stats(self) -> dict:
|
||||||
|
"""Get comprehensive resource statistics."""
|
||||||
|
# Calculate current inventory totals
|
||||||
|
in_inventory = {}
|
||||||
|
for agent in self.world.get_living_agents():
|
||||||
|
for res in agent.inventory:
|
||||||
|
res_type = res.type.value
|
||||||
|
in_inventory[res_type] = in_inventory.get(res_type, 0) + res.quantity
|
||||||
|
|
||||||
|
# Calculate current market totals
|
||||||
|
in_market = {}
|
||||||
|
for order in self.market.get_active_orders():
|
||||||
|
res_type = order.resource_type.value
|
||||||
|
in_market[res_type] = in_market.get(res_type, 0) + order.quantity
|
||||||
|
|
||||||
|
return {
|
||||||
|
"produced": self.resource_stats["produced"].copy(),
|
||||||
|
"consumed": self.resource_stats["consumed"].copy(),
|
||||||
|
"spoiled": self.resource_stats["spoiled"].copy(),
|
||||||
|
"traded": self.resource_stats["traded"].copy(),
|
||||||
|
"in_inventory": in_inventory,
|
||||||
|
"in_market": in_market,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -246,6 +246,10 @@ class Agent:
|
|||||||
total_trades_completed: int = 0
|
total_trades_completed: int = 0
|
||||||
total_money_earned: int = 0
|
total_money_earned: int = 0
|
||||||
|
|
||||||
|
# Personal action log (recent actions with results)
|
||||||
|
action_history: list = field(default_factory=list)
|
||||||
|
MAX_HISTORY_SIZE: int = 20
|
||||||
|
|
||||||
# Configuration - loaded from config
|
# Configuration - loaded from config
|
||||||
INVENTORY_SLOTS: int = field(default=-1) # -1 signals to use config value
|
INVENTORY_SLOTS: int = field(default=-1) # -1 signals to use config value
|
||||||
MOVE_SPEED: float = 0.8 # Grid cells per turn
|
MOVE_SPEED: float = 0.8 # Grid cells per turn
|
||||||
@ -283,6 +287,19 @@ class Agent:
|
|||||||
if action_type in self.actions_performed:
|
if action_type in self.actions_performed:
|
||||||
self.actions_performed[action_type] += 1
|
self.actions_performed[action_type] += 1
|
||||||
|
|
||||||
|
def log_action(self, turn: int, action_type: str, result: str, success: bool = True) -> None:
|
||||||
|
"""Add an action to the agent's personal history log."""
|
||||||
|
entry = {
|
||||||
|
"turn": turn,
|
||||||
|
"action": action_type,
|
||||||
|
"result": result,
|
||||||
|
"success": success,
|
||||||
|
}
|
||||||
|
self.action_history.append(entry)
|
||||||
|
# Keep only recent history
|
||||||
|
if len(self.action_history) > self.MAX_HISTORY_SIZE:
|
||||||
|
self.action_history = self.action_history[-self.MAX_HISTORY_SIZE:]
|
||||||
|
|
||||||
def record_trade(self, money_earned: int) -> None:
|
def record_trade(self, money_earned: int) -> None:
|
||||||
"""Record a completed trade for statistics."""
|
"""Record a completed trade for statistics."""
|
||||||
self.total_trades_completed += 1
|
self.total_trades_completed += 1
|
||||||
@ -511,4 +528,6 @@ class Agent:
|
|||||||
"actions_performed": self.actions_performed.copy(),
|
"actions_performed": self.actions_performed.copy(),
|
||||||
"total_trades": self.total_trades_completed,
|
"total_trades": self.total_trades_completed,
|
||||||
"total_money_earned": self.total_money_earned,
|
"total_money_earned": self.total_money_earned,
|
||||||
|
# Personal action history
|
||||||
|
"action_history": self.action_history.copy(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
"""FastAPI entry point for the Village Simulation backend."""
|
"""FastAPI entry point for the Village Simulation backend."""
|
||||||
|
|
||||||
|
import os
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from backend.api.routes import router
|
from backend.api.routes import router
|
||||||
from backend.core.engine import get_engine
|
from backend.core.engine import get_engine
|
||||||
|
|
||||||
|
# Path to web frontend
|
||||||
|
WEB_FRONTEND_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "web_frontend")
|
||||||
|
|
||||||
# Create FastAPI app
|
# Create FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Village Simulation API",
|
title="Village Simulation API",
|
||||||
@ -48,6 +53,7 @@ def root():
|
|||||||
"name": "Village Simulation API",
|
"name": "Village Simulation API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
|
"web_frontend": "/web/",
|
||||||
"status": "running",
|
"status": "running",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +69,14 @@ def health_check():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Web Frontend Static Files ==============
|
||||||
|
|
||||||
|
# Mount static files for web frontend
|
||||||
|
# Access at http://localhost:8000/web/
|
||||||
|
if os.path.exists(WEB_FRONTEND_PATH):
|
||||||
|
app.mount("/web", StaticFiles(directory=WEB_FRONTEND_PATH, html=True), name="web_frontend")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run the server."""
|
"""Run the server."""
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
|
|||||||
279
web_frontend/index.html
Normal file
279
web_frontend/index.html
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>VillSim - Village Economy Simulation</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header id="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h1 class="title">VillSim</h1>
|
||||||
|
<span class="subtitle">Village Economy Simulation</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-center">
|
||||||
|
<div class="time-display">
|
||||||
|
<span id="day-display">Day 1</span>
|
||||||
|
<span class="separator">·</span>
|
||||||
|
<span id="time-display">☀️ Day</span>
|
||||||
|
<span class="separator">·</span>
|
||||||
|
<span id="turn-display">Turn 0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="connection-status" id="connection-status">
|
||||||
|
<span class="status-dot disconnected"></span>
|
||||||
|
<span class="status-text">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main-content">
|
||||||
|
<aside id="left-panel" class="panel">
|
||||||
|
<div class="panel-section">
|
||||||
|
<h3 class="section-title">Population</h3>
|
||||||
|
<div class="stat-grid" id="population-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value" id="stat-alive">0</span>
|
||||||
|
<span class="stat-label">Alive</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value" id="stat-dead">0</span>
|
||||||
|
<span class="stat-label">Dead</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-section">
|
||||||
|
<h3 class="section-title">Professions</h3>
|
||||||
|
<div class="profession-list" id="profession-list">
|
||||||
|
<!-- Filled by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-section">
|
||||||
|
<h3 class="section-title">Economy</h3>
|
||||||
|
<div class="economy-stats" id="economy-stats">
|
||||||
|
<div class="economy-item">
|
||||||
|
<span class="economy-label">Money in Circulation</span>
|
||||||
|
<span class="economy-value" id="stat-money">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="game-container">
|
||||||
|
<!-- Phaser canvas will be inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="right-panel" class="panel">
|
||||||
|
<div class="panel-section agent-section scrollable-section">
|
||||||
|
<h3 class="section-title">Selected Agent</h3>
|
||||||
|
<div id="agent-details" class="agent-details">
|
||||||
|
<p class="no-selection">Click an agent to view details</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-section">
|
||||||
|
<h3 class="section-title">Market Prices</h3>
|
||||||
|
<div class="market-prices" id="market-prices">
|
||||||
|
<!-- Filled by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-section">
|
||||||
|
<h3 class="section-title">Activity Log</h3>
|
||||||
|
<div class="activity-log" id="activity-log">
|
||||||
|
<!-- Filled by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer id="footer">
|
||||||
|
<div class="controls">
|
||||||
|
<button id="btn-initialize" class="btn btn-secondary" title="Reset Simulation">
|
||||||
|
<span class="btn-icon">⟳</span> Reset
|
||||||
|
</button>
|
||||||
|
<button id="btn-step" class="btn btn-primary" title="Advance one turn">
|
||||||
|
<span class="btn-icon">▶</span> Step
|
||||||
|
</button>
|
||||||
|
<button id="btn-auto" class="btn btn-toggle" title="Toggle auto mode">
|
||||||
|
<span class="btn-icon">⏯</span> Auto
|
||||||
|
</button>
|
||||||
|
<button id="btn-stats" class="btn btn-secondary" title="View Statistics">
|
||||||
|
<span class="btn-icon">📊</span> Stats
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="speed-control">
|
||||||
|
<label for="speed-slider">Speed</label>
|
||||||
|
<input type="range" id="speed-slider" min="50" max="1000" value="150" step="50">
|
||||||
|
<span id="speed-display">150ms</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Stats Screen (Full View) -->
|
||||||
|
<div id="stats-screen" class="stats-screen hidden">
|
||||||
|
<div class="stats-header">
|
||||||
|
<div class="stats-header-left">
|
||||||
|
<h2>📊 Simulation Statistics</h2>
|
||||||
|
<span class="stats-subtitle">Real-time metrics and charts</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-header-center">
|
||||||
|
<div class="stats-tabs">
|
||||||
|
<button class="tab-btn active" data-tab="prices">Prices</button>
|
||||||
|
<button class="tab-btn" data-tab="wealth">Wealth</button>
|
||||||
|
<button class="tab-btn" data-tab="population">Population</button>
|
||||||
|
<button class="tab-btn" data-tab="professions">Professions</button>
|
||||||
|
<button class="tab-btn" data-tab="resources">Resources</button>
|
||||||
|
<button class="tab-btn" data-tab="market">Market</button>
|
||||||
|
<button class="tab-btn" data-tab="agents">Agents</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-header-right">
|
||||||
|
<button id="btn-close-stats" class="btn btn-secondary">
|
||||||
|
<span class="btn-icon">◀</span> Back to Game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-body">
|
||||||
|
<!-- Prices Tab -->
|
||||||
|
<div id="tab-prices" class="tab-panel active">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-prices"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Wealth Tab -->
|
||||||
|
<div id="tab-wealth" class="tab-panel">
|
||||||
|
<div class="chart-grid three-col">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-wealth-dist"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-wealth-prof"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-wealth-time"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Population Tab -->
|
||||||
|
<div id="tab-population" class="tab-panel">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-population"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Professions Tab -->
|
||||||
|
<div id="tab-professions" class="tab-panel">
|
||||||
|
<div class="chart-grid two-col">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-prof-pie"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-prof-time"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Resources Tab -->
|
||||||
|
<div id="tab-resources" class="tab-panel">
|
||||||
|
<div class="chart-grid four-col">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-produced"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-consumed"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-spoiled"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-stock"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-grid four-col" style="margin-top: 16px;">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-cum-produced"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-cum-consumed"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-cum-spoiled"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-res-cum-traded"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Market Tab -->
|
||||||
|
<div id="tab-market" class="tab-panel">
|
||||||
|
<div class="chart-grid two-col">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-market-supply"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-market-activity"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Agents Tab -->
|
||||||
|
<div id="tab-agents" class="tab-panel">
|
||||||
|
<div class="chart-grid four-col">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-stat-energy"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-stat-hunger"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-stat-thirst"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="chart-stat-heat"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-footer">
|
||||||
|
<div class="stats-summary-bar">
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Turn</span>
|
||||||
|
<span class="summary-value" id="stats-turn">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Living</span>
|
||||||
|
<span class="summary-value highlight" id="stats-living">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Deaths</span>
|
||||||
|
<span class="summary-value danger" id="stats-deaths">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Total Gold</span>
|
||||||
|
<span class="summary-value gold" id="stats-gold">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Avg Wealth</span>
|
||||||
|
<span class="summary-value" id="stats-avg-wealth">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Gini Index</span>
|
||||||
|
<span class="summary-value" id="stats-gini">0.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Load game modules -->
|
||||||
|
<script type="module" src="src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
132
web_frontend/src/api.js
Normal file
132
web_frontend/src/api.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* VillSim API Client
|
||||||
|
* Handles all communication with the backend simulation server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Auto-detect API base from current page location (same origin)
|
||||||
|
function getApiBase() {
|
||||||
|
// When served by the backend, use same origin
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
// Fallback for development
|
||||||
|
return 'http://localhost:8000';
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimulationAPI {
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = getApiBase();
|
||||||
|
this.connected = false;
|
||||||
|
this.lastState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(endpoint, options = {}) {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connected = true;
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API Error (${endpoint}):`, error.message);
|
||||||
|
this.connected = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
async checkHealth() {
|
||||||
|
try {
|
||||||
|
const data = await this.request('/health');
|
||||||
|
this.connected = data.status === 'healthy';
|
||||||
|
return this.connected;
|
||||||
|
} catch {
|
||||||
|
this.connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get full simulation state
|
||||||
|
async getState() {
|
||||||
|
const data = await this.request('/api/state');
|
||||||
|
this.lastState = data;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all agents
|
||||||
|
async getAgents() {
|
||||||
|
return await this.request('/api/agents');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get specific agent
|
||||||
|
async getAgent(agentId) {
|
||||||
|
return await this.request(`/api/agents/${agentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get market orders
|
||||||
|
async getMarketOrders() {
|
||||||
|
return await this.request('/api/market/orders');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get market prices
|
||||||
|
async getMarketPrices() {
|
||||||
|
return await this.request('/api/market/prices');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control: Initialize simulation
|
||||||
|
async initialize(numAgents = 8, worldWidth = 20, worldHeight = 20) {
|
||||||
|
return await this.request('/api/control/initialize', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
num_agents: numAgents,
|
||||||
|
world_width: worldWidth,
|
||||||
|
world_height: worldHeight,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control: Advance one step
|
||||||
|
async nextStep() {
|
||||||
|
return await this.request('/api/control/next_step', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control: Set mode (manual/auto)
|
||||||
|
async setMode(mode) {
|
||||||
|
return await this.request('/api/control/mode', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ mode }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control: Get status
|
||||||
|
async getStatus() {
|
||||||
|
return await this.request('/api/control/status');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config: Get configuration
|
||||||
|
async getConfig() {
|
||||||
|
return await this.request('/api/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs: Get recent logs
|
||||||
|
async getLogs(limit = 10) {
|
||||||
|
return await this.request(`/api/logs?limit=${limit}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const api = new SimulationAPI();
|
||||||
|
export default api;
|
||||||
|
|
||||||
71
web_frontend/src/constants.js
Normal file
71
web_frontend/src/constants.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* VillSim Constants
|
||||||
|
* Shared constants for the Phaser game.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Profession icons and colors
|
||||||
|
export const PROFESSIONS = {
|
||||||
|
hunter: { icon: '🏹', color: 0xc45c5c, name: 'Hunter' },
|
||||||
|
gatherer: { icon: '🌿', color: 0x6bab5e, name: 'Gatherer' },
|
||||||
|
woodcutter: { icon: '🪓', color: 0xa67c52, name: 'Woodcutter' },
|
||||||
|
trader: { icon: '💰', color: 0xd4a84b, name: 'Trader' },
|
||||||
|
crafter: { icon: '🧵', color: 0x8b6fc0, name: 'Crafter' },
|
||||||
|
villager: { icon: '👤', color: 0x7a8899, name: 'Villager' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resource icons and colors
|
||||||
|
export const RESOURCES = {
|
||||||
|
meat: { icon: '🥩', color: 0xc45c5c, name: 'Meat' },
|
||||||
|
berries: { icon: '🫐', color: 0xa855a8, name: 'Berries' },
|
||||||
|
water: { icon: '💧', color: 0x5a8cc8, name: 'Water' },
|
||||||
|
wood: { icon: '🪵', color: 0xa67c52, name: 'Wood' },
|
||||||
|
hide: { icon: '🦴', color: 0x8b7355, name: 'Hide' },
|
||||||
|
clothes: { icon: '👕', color: 0x6b6560, name: 'Clothes' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action icons
|
||||||
|
export const ACTIONS = {
|
||||||
|
hunt: { icon: '🏹', verb: 'hunting' },
|
||||||
|
gather: { icon: '🌿', verb: 'gathering' },
|
||||||
|
chop_wood: { icon: '🪓', verb: 'chopping wood' },
|
||||||
|
get_water: { icon: '💧', verb: 'getting water' },
|
||||||
|
weave: { icon: '🧵', verb: 'weaving' },
|
||||||
|
build_fire: { icon: '🔥', verb: 'building fire' },
|
||||||
|
trade: { icon: '💰', verb: 'trading' },
|
||||||
|
rest: { icon: '💤', verb: 'resting' },
|
||||||
|
sleep: { icon: '😴', verb: 'sleeping' },
|
||||||
|
consume: { icon: '🍽️', verb: 'consuming' },
|
||||||
|
idle: { icon: '⏳', verb: 'idle' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Time of day
|
||||||
|
export const TIME_OF_DAY = {
|
||||||
|
day: { icon: '☀️', name: 'Day' },
|
||||||
|
night: { icon: '🌙', name: 'Night' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// World zones (approximate x-positions as percentages)
|
||||||
|
export const WORLD_ZONES = {
|
||||||
|
river: { start: 0.0, end: 0.15, color: 0x3a6ea5, name: 'River' },
|
||||||
|
bushes: { start: 0.15, end: 0.35, color: 0x4a7c59, name: 'Berry Bushes' },
|
||||||
|
village: { start: 0.35, end: 0.65, color: 0x8b7355, name: 'Village' },
|
||||||
|
forest: { start: 0.65, end: 1.0, color: 0x2d5016, name: 'Forest' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Colors for stats
|
||||||
|
export const STAT_COLORS = {
|
||||||
|
energy: 0xd4a84b,
|
||||||
|
hunger: 0xc87f5a,
|
||||||
|
thirst: 0x5a8cc8,
|
||||||
|
heat: 0xc45c5c,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Game display settings
|
||||||
|
export const DISPLAY = {
|
||||||
|
TILE_SIZE: 32,
|
||||||
|
AGENT_SIZE: 24,
|
||||||
|
MIN_ZOOM: 0.5,
|
||||||
|
MAX_ZOOM: 2.0,
|
||||||
|
DEFAULT_ZOOM: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
73
web_frontend/src/main.js
Normal file
73
web_frontend/src/main.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* VillSim - Phaser Web Frontend
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
|
||||||
|
import BootScene from './scenes/BootScene.js';
|
||||||
|
import GameScene from './scenes/GameScene.js';
|
||||||
|
|
||||||
|
// Calculate game dimensions based on container
|
||||||
|
function getGameDimensions() {
|
||||||
|
const container = document.getElementById('game-container');
|
||||||
|
if (!container) {
|
||||||
|
return { width: 800, height: 600 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
width: Math.floor(rect.width),
|
||||||
|
height: Math.floor(rect.height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phaser game configuration
|
||||||
|
const { width, height } = getGameDimensions();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
type: Phaser.AUTO,
|
||||||
|
parent: 'game-container',
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
backgroundColor: '#151921',
|
||||||
|
scene: [BootScene, GameScene],
|
||||||
|
scale: {
|
||||||
|
mode: Phaser.Scale.RESIZE,
|
||||||
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||||
|
},
|
||||||
|
render: {
|
||||||
|
antialias: true,
|
||||||
|
pixelArt: false,
|
||||||
|
roundPixels: true,
|
||||||
|
},
|
||||||
|
physics: {
|
||||||
|
default: 'arcade',
|
||||||
|
arcade: {
|
||||||
|
gravity: { y: 0 },
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dom: {
|
||||||
|
createContainer: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize game when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('VillSim Web Frontend starting...');
|
||||||
|
|
||||||
|
// Create Phaser game
|
||||||
|
const game = new Phaser.Game(config);
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
const { width, height } = getGameDimensions();
|
||||||
|
game.scale.resize(width, height);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store game reference globally for debugging
|
||||||
|
window.villsimGame = game;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for debugging
|
||||||
|
export { config };
|
||||||
|
|
||||||
141
web_frontend/src/scenes/BootScene.js
Normal file
141
web_frontend/src/scenes/BootScene.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* BootScene - Initial loading and setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { api } from '../api.js';
|
||||||
|
|
||||||
|
export default class BootScene extends Phaser.Scene {
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'BootScene' });
|
||||||
|
}
|
||||||
|
|
||||||
|
preload() {
|
||||||
|
// Create loading graphics
|
||||||
|
const { width, height } = this.cameras.main;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
this.add.rectangle(width / 2, height / 2, width, height, 0x151921);
|
||||||
|
|
||||||
|
// Loading text
|
||||||
|
this.loadingText = this.add.text(width / 2, height / 2 - 40, 'VillSim', {
|
||||||
|
fontSize: '48px',
|
||||||
|
fontFamily: 'Crimson Pro, Georgia, serif',
|
||||||
|
color: '#d4a84b',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.statusText = this.add.text(width / 2, height / 2 + 20, 'Connecting to server...', {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontFamily: 'Crimson Pro, Georgia, serif',
|
||||||
|
color: '#a8a095',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Progress bar background
|
||||||
|
const barWidth = 300;
|
||||||
|
const barHeight = 8;
|
||||||
|
this.progressBg = this.add.rectangle(
|
||||||
|
width / 2, height / 2 + 60,
|
||||||
|
barWidth, barHeight,
|
||||||
|
0x242b3d
|
||||||
|
).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.progressBar = this.add.rectangle(
|
||||||
|
width / 2 - barWidth / 2, height / 2 + 60,
|
||||||
|
0, barHeight,
|
||||||
|
0xd4a84b
|
||||||
|
).setOrigin(0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
// Animate progress bar
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.progressBar,
|
||||||
|
width: 100,
|
||||||
|
duration: 500,
|
||||||
|
ease: 'Power2',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attempt to connect to server
|
||||||
|
this.statusText.setText('Connecting to server...');
|
||||||
|
|
||||||
|
let connected = false;
|
||||||
|
let retries = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
|
|
||||||
|
while (!connected && retries < maxRetries) {
|
||||||
|
try {
|
||||||
|
connected = await api.checkHealth();
|
||||||
|
if (connected) {
|
||||||
|
this.statusText.setText('Connected! Loading simulation...');
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.progressBar,
|
||||||
|
width: 200,
|
||||||
|
duration: 300,
|
||||||
|
ease: 'Power2',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
retries++;
|
||||||
|
this.statusText.setText(`Connecting... (attempt ${retries}/${maxRetries})`);
|
||||||
|
await this.delay(1000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
retries++;
|
||||||
|
this.statusText.setText(`Connection failed. Retrying... (${retries}/${maxRetries})`);
|
||||||
|
await this.delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
this.statusText.setText('Could not connect to server. Is the backend running?');
|
||||||
|
this.statusText.setColor('#c45c5c');
|
||||||
|
|
||||||
|
// Add retry button
|
||||||
|
const retryBtn = this.add.text(
|
||||||
|
this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2 + 100,
|
||||||
|
'[ Click to Retry ]',
|
||||||
|
{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Crimson Pro, Georgia, serif',
|
||||||
|
color: '#d4a84b',
|
||||||
|
}
|
||||||
|
).setOrigin(0.5).setInteractive({ useHandCursor: true });
|
||||||
|
|
||||||
|
retryBtn.on('pointerup', () => {
|
||||||
|
this.scene.restart();
|
||||||
|
});
|
||||||
|
|
||||||
|
retryBtn.on('pointerover', () => retryBtn.setColor('#e8e4dc'));
|
||||||
|
retryBtn.on('pointerout', () => retryBtn.setColor('#d4a84b'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial state
|
||||||
|
try {
|
||||||
|
const state = await api.getState();
|
||||||
|
this.registry.set('simulationState', state);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.progressBar,
|
||||||
|
width: 300,
|
||||||
|
duration: 300,
|
||||||
|
ease: 'Power2',
|
||||||
|
onComplete: () => {
|
||||||
|
this.statusText.setText('Starting simulation...');
|
||||||
|
this.time.delayedCall(500, () => {
|
||||||
|
this.scene.start('GameScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.statusText.setText('Error loading simulation state');
|
||||||
|
this.statusText.setColor('#c45c5c');
|
||||||
|
console.error('Failed to load state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1633
web_frontend/src/scenes/GameScene.js
Normal file
1633
web_frontend/src/scenes/GameScene.js
Normal file
File diff suppressed because it is too large
Load Diff
7
web_frontend/src/scenes/index.js
Normal file
7
web_frontend/src/scenes/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Scene exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as BootScene } from './BootScene.js';
|
||||||
|
export { default as GameScene } from './GameScene.js';
|
||||||
|
|
||||||
1053
web_frontend/styles.css
Normal file
1053
web_frontend/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user