422 lines
12 KiB
Python
422 lines
12 KiB
Python
"""FastAPI routes for the Village Simulation API."""
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from backend.core.engine import get_engine, SimulationMode
|
|
from backend.core.world import WorldConfig
|
|
from backend.config import get_config, set_config, SimulationConfig, reset_config
|
|
from .schemas import (
|
|
WorldStateResponse,
|
|
ControlResponse,
|
|
SetModeRequest,
|
|
InitializeRequest,
|
|
ErrorResponse,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ============== State Endpoints ==============
|
|
|
|
@router.get(
|
|
"/state",
|
|
response_model=WorldStateResponse,
|
|
summary="Get full simulation state",
|
|
description="Returns the complete state of the simulation including agents, market, and world info.",
|
|
)
|
|
def get_state():
|
|
"""Get the full simulation state."""
|
|
engine = get_engine()
|
|
return engine.get_state()
|
|
|
|
|
|
@router.get(
|
|
"/agents",
|
|
summary="Get all agents",
|
|
description="Returns a list of all agents in the simulation.",
|
|
)
|
|
def get_agents():
|
|
"""Get all agents."""
|
|
engine = get_engine()
|
|
return {
|
|
"agents": [a.to_dict() for a in engine.world.agents],
|
|
"count": len(engine.world.agents),
|
|
"alive_count": len(engine.world.get_living_agents()),
|
|
}
|
|
|
|
|
|
@router.get(
|
|
"/agents/{agent_id}",
|
|
summary="Get a specific agent",
|
|
description="Returns detailed information about a specific agent.",
|
|
)
|
|
def get_agent(agent_id: str):
|
|
"""Get a specific agent by ID."""
|
|
engine = get_engine()
|
|
agent = engine.world.get_agent(agent_id)
|
|
if agent is None:
|
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
return agent.to_dict()
|
|
|
|
|
|
# ============== Market Endpoints ==============
|
|
|
|
@router.get(
|
|
"/market/orders",
|
|
summary="Get market orders",
|
|
description="Returns all active market orders.",
|
|
)
|
|
def get_market_orders():
|
|
"""Get all active market orders."""
|
|
engine = get_engine()
|
|
return engine.market.get_state_snapshot()
|
|
|
|
|
|
@router.get(
|
|
"/market/prices",
|
|
summary="Get market prices",
|
|
description="Returns current market prices for all resource types.",
|
|
)
|
|
def get_market_prices():
|
|
"""Get market price summary."""
|
|
engine = get_engine()
|
|
return engine.market.get_market_prices()
|
|
|
|
|
|
# ============== Control Endpoints ==============
|
|
|
|
@router.post(
|
|
"/control/initialize",
|
|
response_model=ControlResponse,
|
|
summary="Initialize simulation",
|
|
description="Initialize or reset the simulation. Uses config.json values if no params provided.",
|
|
)
|
|
def initialize_simulation(request: InitializeRequest = None):
|
|
"""Initialize or reset the simulation.
|
|
|
|
If request is provided with specific values, use those.
|
|
Otherwise, use values from config.json.
|
|
"""
|
|
engine = get_engine()
|
|
|
|
if request and (request.num_agents != 8 or request.world_width != 20 or request.world_height != 20):
|
|
# Custom values provided - use them
|
|
from backend.core.world import WorldConfig
|
|
config = WorldConfig(
|
|
width=request.world_width,
|
|
height=request.world_height,
|
|
initial_agents=request.num_agents,
|
|
)
|
|
engine.reset(config)
|
|
num_agents = request.num_agents
|
|
else:
|
|
# Use values from config.json
|
|
engine.reset() # This now loads from config.json automatically
|
|
num_agents = engine.world.config.initial_agents
|
|
|
|
return ControlResponse(
|
|
success=True,
|
|
message=f"Simulation initialized with {num_agents} agents",
|
|
turn=engine.world.current_turn,
|
|
mode=engine.mode.value,
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/control/next_step",
|
|
response_model=ControlResponse,
|
|
summary="Advance simulation",
|
|
description="Advance the simulation by one step. Only works in MANUAL mode.",
|
|
)
|
|
def next_step():
|
|
"""Advance the simulation by one step."""
|
|
engine = get_engine()
|
|
|
|
if engine.mode == SimulationMode.AUTO:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Cannot manually advance while in AUTO mode. Switch to MANUAL first.",
|
|
)
|
|
|
|
if not engine.is_running:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Simulation is not running. Initialize first.",
|
|
)
|
|
|
|
turn_log = engine.next_step()
|
|
|
|
return ControlResponse(
|
|
success=True,
|
|
message=f"Advanced to turn {engine.world.current_turn}",
|
|
turn=engine.world.current_turn,
|
|
mode=engine.mode.value,
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/control/mode",
|
|
response_model=ControlResponse,
|
|
summary="Set simulation mode",
|
|
description="Set the simulation mode to MANUAL or AUTO.",
|
|
)
|
|
def set_mode(request: SetModeRequest):
|
|
"""Set the simulation mode."""
|
|
engine = get_engine()
|
|
|
|
try:
|
|
mode = SimulationMode(request.mode)
|
|
engine.set_mode(mode)
|
|
return ControlResponse(
|
|
success=True,
|
|
message=f"Mode set to {mode.value}",
|
|
turn=engine.world.current_turn,
|
|
mode=mode.value,
|
|
)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Invalid mode: {request.mode}. Use 'manual' or 'auto'.",
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/control/status",
|
|
summary="Get simulation status",
|
|
description="Returns the current simulation status.",
|
|
)
|
|
def get_status():
|
|
"""Get simulation status."""
|
|
engine = get_engine()
|
|
return {
|
|
"is_running": engine.is_running,
|
|
"mode": engine.mode.value,
|
|
"current_turn": engine.world.current_turn,
|
|
"current_day": engine.world.current_day,
|
|
"time_of_day": engine.world.time_of_day.value,
|
|
"living_agents": len(engine.world.get_living_agents()),
|
|
}
|
|
|
|
|
|
# ============== Logs Endpoints ==============
|
|
|
|
@router.get(
|
|
"/logs",
|
|
summary="Get turn logs",
|
|
description="Returns the most recent turn logs.",
|
|
)
|
|
def get_logs(limit: int = 10):
|
|
"""Get recent turn logs."""
|
|
engine = get_engine()
|
|
logs = engine.turn_logs[-limit:] if engine.turn_logs else []
|
|
return {
|
|
"logs": [log.to_dict() for log in logs],
|
|
"total_turns": len(engine.turn_logs),
|
|
}
|
|
|
|
|
|
@router.get(
|
|
"/logs/{turn}",
|
|
summary="Get specific turn log",
|
|
description="Returns the log for a specific turn.",
|
|
)
|
|
def get_turn_log(turn: int):
|
|
"""Get log for a specific turn."""
|
|
engine = get_engine()
|
|
|
|
for log in engine.turn_logs:
|
|
if log.turn == turn:
|
|
return log.to_dict()
|
|
|
|
raise HTTPException(status_code=404, detail=f"Log for turn {turn} not found")
|
|
|
|
|
|
# ============== Config Endpoints ==============
|
|
|
|
@router.get(
|
|
"/config",
|
|
summary="Get simulation configuration",
|
|
description="Returns all configurable simulation parameters.",
|
|
)
|
|
def get_simulation_config():
|
|
"""Get current simulation configuration."""
|
|
config = get_config()
|
|
return config.to_dict()
|
|
|
|
|
|
@router.post(
|
|
"/config",
|
|
response_model=ControlResponse,
|
|
summary="Update simulation configuration",
|
|
description="Update simulation parameters. Requires simulation restart to take effect.",
|
|
)
|
|
def update_simulation_config(config_data: dict):
|
|
"""Update simulation configuration."""
|
|
try:
|
|
new_config = SimulationConfig.from_dict(config_data)
|
|
set_config(new_config)
|
|
return ControlResponse(
|
|
success=True,
|
|
message="Configuration updated. Restart simulation to apply changes.",
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid configuration: {str(e)}")
|
|
|
|
|
|
@router.post(
|
|
"/config/reset",
|
|
response_model=ControlResponse,
|
|
summary="Reset configuration to defaults",
|
|
description="Reset all configuration parameters to their default values.",
|
|
)
|
|
def reset_simulation_config():
|
|
"""Reset configuration to defaults."""
|
|
reset_config()
|
|
return ControlResponse(
|
|
success=True,
|
|
message="Configuration reset to defaults.",
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/config/save",
|
|
response_model=ControlResponse,
|
|
summary="Save configuration to file",
|
|
description="Save current configuration to config.json file.",
|
|
)
|
|
def save_config_to_file():
|
|
"""Save configuration to file."""
|
|
try:
|
|
config = get_config()
|
|
config.save("config.json")
|
|
return ControlResponse(
|
|
success=True,
|
|
message="Configuration saved to config.json",
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to save config: {str(e)}")
|
|
|
|
|
|
@router.post(
|
|
"/config/load",
|
|
response_model=ControlResponse,
|
|
summary="Load configuration from file",
|
|
description="Load configuration from config.json file.",
|
|
)
|
|
def load_config_from_file():
|
|
"""Load configuration from file."""
|
|
try:
|
|
config = SimulationConfig.load("config.json")
|
|
set_config(config)
|
|
return ControlResponse(
|
|
success=True,
|
|
message="Configuration loaded from config.json",
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to load config: {str(e)}")
|
|
|
|
|
|
# ============== GOAP Debug Endpoints ==============
|
|
|
|
@router.get(
|
|
"/goap/debug/{agent_id}",
|
|
summary="Get GOAP debug info for an agent",
|
|
description="Returns detailed GOAP decision-making info including goals, actions, and plans.",
|
|
)
|
|
def get_agent_goap_debug(agent_id: str):
|
|
"""Get GOAP debug information for a specific agent."""
|
|
engine = get_engine()
|
|
agent = engine.world.get_agent(agent_id)
|
|
|
|
if agent is None:
|
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
|
|
if not agent.is_alive():
|
|
raise HTTPException(status_code=400, detail=f"Agent {agent_id} is not alive")
|
|
|
|
from backend.core.goap.debug import get_goap_debug_info
|
|
|
|
debug_info = get_goap_debug_info(
|
|
agent=agent,
|
|
market=engine.market,
|
|
step_in_day=engine.world.step_in_day,
|
|
day_steps=engine.world.config.day_steps,
|
|
is_night=engine.world.is_night(),
|
|
)
|
|
|
|
return debug_info.to_dict()
|
|
|
|
|
|
@router.get(
|
|
"/goap/debug",
|
|
summary="Get GOAP debug info for all agents",
|
|
description="Returns GOAP decision-making info for all living agents.",
|
|
)
|
|
def get_all_goap_debug():
|
|
"""Get GOAP debug information for all living agents."""
|
|
engine = get_engine()
|
|
|
|
from backend.core.goap.debug import get_all_agents_goap_debug
|
|
|
|
debug_infos = get_all_agents_goap_debug(
|
|
agents=engine.world.agents,
|
|
market=engine.market,
|
|
step_in_day=engine.world.step_in_day,
|
|
day_steps=engine.world.config.day_steps,
|
|
is_night=engine.world.is_night(),
|
|
)
|
|
|
|
return {
|
|
"agents": [info.to_dict() for info in debug_infos],
|
|
"count": len(debug_infos),
|
|
"current_turn": engine.world.current_turn,
|
|
"is_night": engine.world.is_night(),
|
|
}
|
|
|
|
|
|
@router.get(
|
|
"/goap/goals",
|
|
summary="Get all GOAP goals",
|
|
description="Returns a list of all available GOAP goals.",
|
|
)
|
|
def get_goap_goals():
|
|
"""Get all available GOAP goals."""
|
|
from backend.core.goap import get_all_goals
|
|
|
|
goals = get_all_goals()
|
|
return {
|
|
"goals": [
|
|
{
|
|
"name": g.name,
|
|
"type": g.goal_type.value,
|
|
"max_plan_depth": g.max_plan_depth,
|
|
}
|
|
for g in goals
|
|
],
|
|
"count": len(goals),
|
|
}
|
|
|
|
|
|
@router.get(
|
|
"/goap/actions",
|
|
summary="Get all GOAP actions",
|
|
description="Returns a list of all available GOAP actions.",
|
|
)
|
|
def get_goap_actions():
|
|
"""Get all available GOAP actions."""
|
|
from backend.core.goap import get_all_actions
|
|
|
|
actions = get_all_actions()
|
|
return {
|
|
"actions": [
|
|
{
|
|
"name": a.name,
|
|
"action_type": a.action_type.value,
|
|
"target_resource": a.target_resource.value if a.target_resource else None,
|
|
}
|
|
for a in actions
|
|
],
|
|
"count": len(actions),
|
|
}
|
|
|