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