298 lines
11 KiB
Python
298 lines
11 KiB
Python
"""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
|
|
|
|
|
|
# Window configuration
|
|
WINDOW_WIDTH = 1000
|
|
WINDOW_HEIGHT = 700
|
|
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)
|
|
|
|
# State
|
|
self.state: SimulationState | None = None
|
|
self.running = True
|
|
self.hovered_agent: dict | None = None
|
|
|
|
# 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 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.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:
|
|
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:
|
|
if self.client.initialize():
|
|
self.state = self.client.get_state()
|
|
|
|
elif event.key == pygame.K_m:
|
|
# Toggle mode
|
|
if self.client.connected and self.state and not self.settings_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_s:
|
|
# Toggle settings panel
|
|
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
|
|
|
|
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 settings hint
|
|
if not self.settings_renderer.visible:
|
|
hint = pygame.font.Font(None, 18).render("Press S for Settings", 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(" ESC - Close settings / 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()
|