240 lines
8.7 KiB
Python
240 lines
8.7 KiB
Python
"""UI renderer for the Village Simulation."""
|
|
|
|
import pygame
|
|
from typing import TYPE_CHECKING, Optional
|
|
|
|
if TYPE_CHECKING:
|
|
from frontend.client import SimulationState
|
|
|
|
|
|
class Colors:
|
|
# UI colors
|
|
PANEL_BG = (35, 40, 50)
|
|
PANEL_BORDER = (70, 80, 95)
|
|
TEXT_PRIMARY = (230, 230, 235)
|
|
TEXT_SECONDARY = (160, 165, 175)
|
|
TEXT_HIGHLIGHT = (100, 180, 255)
|
|
TEXT_WARNING = (255, 180, 80)
|
|
TEXT_DANGER = (255, 100, 100)
|
|
|
|
# Day/Night indicator
|
|
DAY_COLOR = (255, 220, 100)
|
|
NIGHT_COLOR = (100, 120, 180)
|
|
|
|
|
|
class UIRenderer:
|
|
"""Renders UI elements (HUD, panels, text info)."""
|
|
|
|
def __init__(self, screen: pygame.Surface, font: pygame.font.Font):
|
|
self.screen = screen
|
|
self.font = font
|
|
self.small_font = pygame.font.Font(None, 20)
|
|
self.title_font = pygame.font.Font(None, 28)
|
|
|
|
# Panel dimensions
|
|
self.top_panel_height = 50
|
|
self.right_panel_width = 200
|
|
|
|
def _draw_panel(self, rect: pygame.Rect, title: Optional[str] = None) -> None:
|
|
"""Draw a panel background."""
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BORDER, rect, 1)
|
|
|
|
if title:
|
|
title_text = self.small_font.render(title, True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(title_text, (rect.x + 8, rect.y + 4))
|
|
|
|
def draw_top_bar(self, state: "SimulationState") -> None:
|
|
"""Draw the top information bar."""
|
|
rect = pygame.Rect(0, 0, self.screen.get_width(), self.top_panel_height)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect)
|
|
pygame.draw.line(
|
|
self.screen,
|
|
Colors.PANEL_BORDER,
|
|
(0, self.top_panel_height),
|
|
(self.screen.get_width(), self.top_panel_height),
|
|
)
|
|
|
|
# Day/Night and Turn info
|
|
is_night = state.time_of_day == "night"
|
|
time_color = Colors.NIGHT_COLOR if is_night else Colors.DAY_COLOR
|
|
time_text = "NIGHT" if is_night else "DAY"
|
|
|
|
# Draw time indicator circle
|
|
pygame.draw.circle(self.screen, time_color, (25, 25), 12)
|
|
pygame.draw.circle(self.screen, Colors.PANEL_BORDER, (25, 25), 12, 1)
|
|
|
|
# Time/day text
|
|
info_text = f"{time_text} | Day {state.day}, Step {state.step_in_day} | Turn {state.turn}"
|
|
text = self.font.render(info_text, True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(text, (50, 15))
|
|
|
|
# Mode indicator
|
|
mode_color = Colors.TEXT_HIGHLIGHT if state.mode == "auto" else Colors.TEXT_SECONDARY
|
|
mode_text = f"Mode: {state.mode.upper()}"
|
|
text = self.small_font.render(mode_text, True, mode_color)
|
|
self.screen.blit(text, (self.screen.get_width() - 120, 8))
|
|
|
|
# Running indicator
|
|
if state.is_running:
|
|
status_text = "RUNNING"
|
|
status_color = (100, 200, 100)
|
|
else:
|
|
status_text = "STOPPED"
|
|
status_color = Colors.TEXT_DANGER
|
|
|
|
text = self.small_font.render(status_text, True, status_color)
|
|
self.screen.blit(text, (self.screen.get_width() - 120, 28))
|
|
|
|
def draw_right_panel(self, state: "SimulationState") -> None:
|
|
"""Draw the right information panel."""
|
|
panel_x = self.screen.get_width() - self.right_panel_width
|
|
rect = pygame.Rect(
|
|
panel_x,
|
|
self.top_panel_height,
|
|
self.right_panel_width,
|
|
self.screen.get_height() - self.top_panel_height,
|
|
)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect)
|
|
pygame.draw.line(
|
|
self.screen,
|
|
Colors.PANEL_BORDER,
|
|
(panel_x, self.top_panel_height),
|
|
(panel_x, self.screen.get_height()),
|
|
)
|
|
|
|
y = self.top_panel_height + 10
|
|
|
|
# Statistics section
|
|
y = self._draw_statistics_section(state, panel_x + 10, y)
|
|
|
|
# Market section
|
|
y = self._draw_market_section(state, panel_x + 10, y + 20)
|
|
|
|
# Controls help section
|
|
self._draw_controls_help(panel_x + 10, self.screen.get_height() - 100)
|
|
|
|
def _draw_statistics_section(self, state: "SimulationState", x: int, y: int) -> int:
|
|
"""Draw the statistics section."""
|
|
# Title
|
|
title = self.title_font.render("Statistics", True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(title, (x, y))
|
|
y += 30
|
|
|
|
stats = state.statistics
|
|
living = len(state.get_living_agents())
|
|
|
|
# Population
|
|
pop_color = Colors.TEXT_PRIMARY if living > 2 else Colors.TEXT_DANGER
|
|
text = self.small_font.render(f"Population: {living}", True, pop_color)
|
|
self.screen.blit(text, (x, y))
|
|
y += 18
|
|
|
|
# Deaths
|
|
deaths = stats.get("total_agents_died", 0)
|
|
if deaths > 0:
|
|
text = self.small_font.render(f"Deaths: {deaths}", True, Colors.TEXT_WARNING)
|
|
self.screen.blit(text, (x, y))
|
|
y += 18
|
|
|
|
# Total money
|
|
total_money = stats.get("total_money_in_circulation", 0)
|
|
text = self.small_font.render(f"Total Coins: {total_money}", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 18
|
|
|
|
# Professions
|
|
professions = stats.get("professions", {})
|
|
if professions:
|
|
y += 5
|
|
text = self.small_font.render("Professions:", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 16
|
|
|
|
for prof, count in professions.items():
|
|
text = self.small_font.render(f" {prof}: {count}", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 14
|
|
|
|
return y
|
|
|
|
def _draw_market_section(self, state: "SimulationState", x: int, y: int) -> int:
|
|
"""Draw the market section."""
|
|
# Title
|
|
title = self.title_font.render("Market", True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(title, (x, y))
|
|
y += 30
|
|
|
|
# Order count
|
|
order_count = len(state.market_orders)
|
|
text = self.small_font.render(f"Active Orders: {order_count}", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 20
|
|
|
|
# Price summary for each resource with available stock
|
|
prices = state.market_prices
|
|
for resource, data in prices.items():
|
|
if data.get("total_available", 0) > 0:
|
|
price = data.get("lowest_price", "?")
|
|
qty = data.get("total_available", 0)
|
|
text = self.small_font.render(
|
|
f"{resource}: {qty}x @ {price}c",
|
|
True,
|
|
Colors.TEXT_SECONDARY,
|
|
)
|
|
self.screen.blit(text, (x, y))
|
|
y += 16
|
|
|
|
return y
|
|
|
|
def _draw_controls_help(self, x: int, y: int) -> None:
|
|
"""Draw controls help at bottom of panel."""
|
|
pygame.draw.line(
|
|
self.screen,
|
|
Colors.PANEL_BORDER,
|
|
(x - 5, y - 10),
|
|
(self.screen.get_width() - 5, y - 10),
|
|
)
|
|
|
|
title = self.small_font.render("Controls", True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(title, (x, y))
|
|
y += 20
|
|
|
|
controls = [
|
|
"SPACE - Next Turn",
|
|
"R - Reset Simulation",
|
|
"M - Toggle Mode",
|
|
"S - Settings",
|
|
"ESC - Quit",
|
|
]
|
|
|
|
for control in controls:
|
|
text = self.small_font.render(control, True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 16
|
|
|
|
def draw_connection_status(self, connected: bool) -> None:
|
|
"""Draw connection status overlay when disconnected."""
|
|
if connected:
|
|
return
|
|
|
|
# Semi-transparent overlay
|
|
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
|
overlay.fill((0, 0, 0, 180))
|
|
self.screen.blit(overlay, (0, 0))
|
|
|
|
# Connection message
|
|
text = self.title_font.render("Connecting to server...", True, Colors.TEXT_WARNING)
|
|
text_rect = text.get_rect(center=(self.screen.get_width() // 2, self.screen.get_height() // 2))
|
|
self.screen.blit(text, text_rect)
|
|
|
|
hint = self.small_font.render("Make sure the backend is running on localhost:8000", True, Colors.TEXT_SECONDARY)
|
|
hint_rect = hint.get_rect(center=(self.screen.get_width() // 2, self.screen.get_height() // 2 + 30))
|
|
self.screen.blit(hint, hint_rect)
|
|
|
|
def draw(self, state: "SimulationState") -> None:
|
|
"""Draw all UI elements."""
|
|
self.draw_top_bar(state)
|
|
self.draw_right_panel(state)
|
|
|