"""Main Pygame application for the Village Simulation frontend.""" import sys import pygame from frontend.client import SimulationClient, SimulationState from frontend.renderer.map_renderer import MapRenderer from frontend.renderer.agent_renderer import AgentRenderer from frontend.renderer.ui_renderer import UIRenderer from frontend.renderer.settings_renderer import SettingsRenderer from frontend.renderer.stats_renderer import StatsRenderer # Window configuration WINDOW_WIDTH = 1200 WINDOW_HEIGHT = 800 WINDOW_TITLE = "Village Economy Simulation" FPS = 30 # Layout configuration TOP_PANEL_HEIGHT = 50 RIGHT_PANEL_WIDTH = 200 class VillageSimulationApp: """Main application class for the Village Simulation frontend.""" def __init__(self, server_url: str = "http://localhost:8000"): # Initialize Pygame pygame.init() pygame.font.init() # Create window self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(WINDOW_TITLE) # Clock for FPS control self.clock = pygame.time.Clock() # Fonts self.font = pygame.font.Font(None, 24) # Network client self.client = SimulationClient(server_url) # Calculate map area self.map_rect = pygame.Rect( 0, TOP_PANEL_HEIGHT, WINDOW_WIDTH - RIGHT_PANEL_WIDTH, WINDOW_HEIGHT - TOP_PANEL_HEIGHT, ) # Initialize renderers self.map_renderer = MapRenderer(self.screen, self.map_rect) self.agent_renderer = AgentRenderer(self.screen, self.map_renderer, self.font) self.ui_renderer = UIRenderer(self.screen, self.font) self.settings_renderer = SettingsRenderer(self.screen) self.stats_renderer = StatsRenderer(self.screen) # State self.state: SimulationState | None = None self.running = True self.hovered_agent: dict | None = None self._last_turn: int = -1 # Track turn changes for stats update # Polling interval (ms) self.last_poll_time = 0 self.poll_interval = 100 # Poll every 100ms for smoother updates # Setup settings callbacks self._setup_settings_callbacks() def _setup_settings_callbacks(self) -> None: """Set up callbacks for the settings panel.""" # Override the apply and reset callbacks original_apply = self.settings_renderer._apply_config original_reset = self.settings_renderer._reset_config def apply_config(): config = self.settings_renderer.get_config() if self.client.update_config(config): # Restart simulation with new config if self.client.initialize(): self.state = self.client.get_state() self.settings_renderer.status_message = "Config applied & simulation restarted!" self.settings_renderer.status_color = (80, 180, 100) else: self.settings_renderer.status_message = "Config saved but restart failed" self.settings_renderer.status_color = (200, 160, 80) else: self.settings_renderer.status_message = "Failed to apply config" self.settings_renderer.status_color = (200, 80, 80) def reset_config(): if self.client.reset_config(): # Reload config from server config = self.client.get_config() if config: self.settings_renderer.set_config(config) self.settings_renderer.status_message = "Config reset to defaults" self.settings_renderer.status_color = (200, 160, 80) else: self.settings_renderer.status_message = "Failed to reset config" self.settings_renderer.status_color = (200, 80, 80) self.settings_renderer._apply_config = apply_config self.settings_renderer._reset_config = reset_config def _load_config(self) -> None: """Load configuration from server into settings panel.""" config = self.client.get_config() if config: self.settings_renderer.set_config(config) def handle_events(self) -> None: """Handle Pygame events.""" for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False # Let stats panel handle events first if visible if self.stats_renderer.handle_event(event): continue # Let settings panel handle events first if visible if self.settings_renderer.handle_event(event): continue if event.type == pygame.KEYDOWN: self._handle_keydown(event) elif event.type == pygame.MOUSEMOTION: self._handle_mouse_motion(event) def _handle_keydown(self, event: pygame.event.Event) -> None: """Handle keyboard input.""" if event.key == pygame.K_ESCAPE: if self.stats_renderer.visible: self.stats_renderer.toggle() elif self.settings_renderer.visible: self.settings_renderer.toggle() else: self.running = False elif event.key == pygame.K_SPACE: # Advance one turn if self.client.connected and not self.settings_renderer.visible and not self.stats_renderer.visible: if self.client.advance_turn(): # Immediately fetch new state self.state = self.client.get_state() elif event.key == pygame.K_r: # Reset simulation if self.client.connected and not self.settings_renderer.visible and not self.stats_renderer.visible: if self.client.initialize(): self.state = self.client.get_state() self.stats_renderer.clear_history() self._last_turn = -1 elif event.key == pygame.K_m: # Toggle mode if self.client.connected and self.state and not self.settings_renderer.visible and not self.stats_renderer.visible: new_mode = "auto" if self.state.mode == "manual" else "manual" if self.client.set_mode(new_mode): self.state = self.client.get_state() elif event.key == pygame.K_g: # Toggle statistics/graphs panel if not self.settings_renderer.visible: self.stats_renderer.toggle() elif event.key == pygame.K_s: # Toggle settings panel if not self.stats_renderer.visible: if not self.settings_renderer.visible: self._load_config() self.settings_renderer.toggle() def _handle_mouse_motion(self, event: pygame.event.Event) -> None: """Handle mouse motion for agent hover detection.""" if not self.state or self.settings_renderer.visible: self.hovered_agent = None return mouse_pos = event.pos self.hovered_agent = None # Check if mouse is in map area if not self.map_rect.collidepoint(mouse_pos): return # Check each agent for agent in self.state.agents: if not agent.get("is_alive", False): continue pos = agent.get("position", {"x": 0, "y": 0}) screen_x, screen_y = self.map_renderer.grid_to_screen(pos["x"], pos["y"]) # Check if mouse is near agent dx = mouse_pos[0] - screen_x dy = mouse_pos[1] - screen_y distance = (dx * dx + dy * dy) ** 0.5 cell_w, cell_h = self.map_renderer.get_cell_size() agent_radius = min(cell_w, cell_h) / 2 if distance < agent_radius + 5: self.hovered_agent = agent break def update(self) -> None: """Update game state by polling the server.""" current_time = pygame.time.get_ticks() # Check if we need to poll if current_time - self.last_poll_time >= self.poll_interval: self.last_poll_time = current_time if not self.client.connected: self.client.check_connection() if self.client.connected: new_state = self.client.get_state() if new_state: # Update map dimensions if changed if ( new_state.world_width != self.map_renderer.world_width or new_state.world_height != self.map_renderer.world_height ): self.map_renderer.update_dimensions( new_state.world_width, new_state.world_height, ) self.state = new_state # Update stats history when turn changes if new_state.turn != self._last_turn: self.stats_renderer.update_history(new_state) self._last_turn = new_state.turn def draw(self) -> None: """Draw all elements.""" # Clear screen self.screen.fill((30, 35, 45)) if self.state: # Draw map self.map_renderer.draw(self.state) # Draw agents self.agent_renderer.draw(self.state) # Draw UI self.ui_renderer.draw(self.state) # Draw agent tooltip if hovering if self.hovered_agent and not self.settings_renderer.visible: mouse_pos = pygame.mouse.get_pos() self.agent_renderer.draw_agent_tooltip(self.hovered_agent, mouse_pos) # Draw connection status overlay if disconnected if not self.client.connected: self.ui_renderer.draw_connection_status(self.client.connected) # Draw settings panel if visible self.settings_renderer.draw() # Draw stats panel if visible self.stats_renderer.draw(self.state) # Draw hints at bottom if not self.settings_renderer.visible and not self.stats_renderer.visible: hint_font = pygame.font.Font(None, 18) hint = hint_font.render("S: Settings | G: Statistics & Graphs", True, (100, 100, 120)) self.screen.blit(hint, (5, self.screen.get_height() - 20)) # Update display pygame.display.flip() def run(self) -> None: """Main game loop.""" print("Starting Village Simulation Frontend...") print("Connecting to backend at http://localhost:8000...") # Try to connect initially if not self.client.check_connection(): print("Backend not available. Will retry in the main loop.") else: print("Connected!") self.state = self.client.get_state() print("\nControls:") print(" SPACE - Advance turn") print(" R - Reset simulation") print(" M - Toggle auto/manual mode") print(" S - Open settings") print(" G - Open statistics & graphs") print(" ESC - Close panel / Quit") print() while self.running: self.handle_events() self.update() self.draw() self.clock.tick(FPS) pygame.quit() def main(): """Entry point for the frontend application.""" # Get server URL from command line if provided server_url = "http://localhost:8000" if len(sys.argv) > 1: server_url = sys.argv[1] app = VillageSimulationApp(server_url) app.run() if __name__ == "__main__": main()