181 lines
6.0 KiB
Python
181 lines
6.0 KiB
Python
"""HTTP client for communicating with the Village Simulation backend."""
|
|
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Optional, Any
|
|
|
|
import requests
|
|
from requests.exceptions import RequestException
|
|
|
|
|
|
@dataclass
|
|
class SimulationState:
|
|
"""Parsed simulation state from the API."""
|
|
turn: int
|
|
day: int
|
|
step_in_day: int
|
|
time_of_day: str
|
|
world_width: int
|
|
world_height: int
|
|
agents: list[dict]
|
|
market_orders: list[dict]
|
|
market_prices: dict
|
|
statistics: dict
|
|
mode: str
|
|
is_running: bool
|
|
recent_logs: list[dict]
|
|
|
|
@classmethod
|
|
def from_api_response(cls, data: dict) -> "SimulationState":
|
|
"""Create from API response data."""
|
|
return cls(
|
|
turn=data.get("turn", 0),
|
|
day=data.get("day", 1),
|
|
step_in_day=data.get("step_in_day", 0),
|
|
time_of_day=data.get("time_of_day", "day"),
|
|
world_width=data.get("world_size", {}).get("width", 20),
|
|
world_height=data.get("world_size", {}).get("height", 20),
|
|
agents=data.get("agents", []),
|
|
market_orders=data.get("market", {}).get("orders", []),
|
|
market_prices=data.get("market", {}).get("prices", {}),
|
|
statistics=data.get("statistics", {}),
|
|
mode=data.get("mode", "manual"),
|
|
is_running=data.get("is_running", False),
|
|
recent_logs=data.get("recent_logs", []),
|
|
)
|
|
|
|
def get_living_agents(self) -> list[dict]:
|
|
"""Get only living agents."""
|
|
return [a for a in self.agents if a.get("is_alive", False)]
|
|
|
|
|
|
class SimulationClient:
|
|
"""HTTP client for the Village Simulation backend."""
|
|
|
|
def __init__(self, base_url: str = "http://localhost:8000"):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.api_url = f"{self.base_url}/api"
|
|
self.session = requests.Session()
|
|
self.last_state: Optional[SimulationState] = None
|
|
self.connected = False
|
|
self._retry_count = 0
|
|
self._max_retries = 3
|
|
|
|
def _request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
json: Optional[dict] = None,
|
|
timeout: float = 5.0,
|
|
) -> Optional[dict]:
|
|
"""Make an HTTP request to the API."""
|
|
url = f"{self.api_url}{endpoint}"
|
|
|
|
try:
|
|
response = self.session.request(
|
|
method=method,
|
|
url=url,
|
|
json=json,
|
|
timeout=timeout,
|
|
)
|
|
response.raise_for_status()
|
|
self.connected = True
|
|
self._retry_count = 0
|
|
return response.json()
|
|
except RequestException as e:
|
|
self._retry_count += 1
|
|
if self._retry_count >= self._max_retries:
|
|
self.connected = False
|
|
return None
|
|
|
|
def check_connection(self) -> bool:
|
|
"""Check if the backend is reachable."""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.base_url}/health",
|
|
timeout=2.0,
|
|
)
|
|
self.connected = response.status_code == 200
|
|
return self.connected
|
|
except RequestException:
|
|
self.connected = False
|
|
return False
|
|
|
|
def get_state(self) -> Optional[SimulationState]:
|
|
"""Fetch the current simulation state."""
|
|
data = self._request("GET", "/state")
|
|
if data:
|
|
self.last_state = SimulationState.from_api_response(data)
|
|
return self.last_state
|
|
return self.last_state # Return cached state if request failed
|
|
|
|
def advance_turn(self) -> bool:
|
|
"""Advance the simulation by one step."""
|
|
result = self._request("POST", "/control/next_step")
|
|
return result is not None and result.get("success", False)
|
|
|
|
def set_mode(self, mode: str) -> bool:
|
|
"""Set the simulation mode ('manual' or 'auto')."""
|
|
result = self._request("POST", "/control/mode", json={"mode": mode})
|
|
return result is not None and result.get("success", False)
|
|
|
|
def initialize(
|
|
self,
|
|
num_agents: int = 8,
|
|
world_width: int = 20,
|
|
world_height: int = 20,
|
|
) -> bool:
|
|
"""Initialize or reset the simulation."""
|
|
result = self._request("POST", "/control/initialize", json={
|
|
"num_agents": num_agents,
|
|
"world_width": world_width,
|
|
"world_height": world_height,
|
|
})
|
|
return result is not None and result.get("success", False)
|
|
|
|
def get_status(self) -> Optional[dict]:
|
|
"""Get simulation status."""
|
|
return self._request("GET", "/control/status")
|
|
|
|
def get_agents(self) -> Optional[list[dict]]:
|
|
"""Get all agents."""
|
|
result = self._request("GET", "/agents")
|
|
if result:
|
|
return result.get("agents", [])
|
|
return None
|
|
|
|
def get_market_orders(self) -> Optional[list[dict]]:
|
|
"""Get all market orders."""
|
|
result = self._request("GET", "/market/orders")
|
|
if result:
|
|
return result.get("orders", [])
|
|
return None
|
|
|
|
def get_market_prices(self) -> Optional[dict]:
|
|
"""Get market prices."""
|
|
return self._request("GET", "/market/prices")
|
|
|
|
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
|
"""Wait for backend connection with timeout."""
|
|
start = time.time()
|
|
while time.time() - start < timeout:
|
|
if self.check_connection():
|
|
return True
|
|
time.sleep(0.5)
|
|
return False
|
|
|
|
def get_config(self) -> Optional[dict]:
|
|
"""Get current simulation configuration."""
|
|
return self._request("GET", "/config")
|
|
|
|
def update_config(self, config_data: dict) -> bool:
|
|
"""Update simulation configuration."""
|
|
result = self._request("POST", "/config", json=config_data)
|
|
return result is not None and result.get("success", False)
|
|
|
|
def reset_config(self) -> bool:
|
|
"""Reset configuration to defaults."""
|
|
result = self._request("POST", "/config/reset")
|
|
return result is not None and result.get("success", False)
|
|
|