"""Map renderer for the Village Simulation. Beautiful dark theme with oil fields, temples, and terrain features. """ import math import random import pygame from typing import TYPE_CHECKING if TYPE_CHECKING: from frontend.client import SimulationState # Color palette - Cyberpunk dark theme class Colors: # Background colors DAY_BG = (28, 35, 42) NIGHT_BG = (12, 14, 20) # Terrain GRASS_LIGHT = (32, 45, 38) GRASS_DARK = (26, 38, 32) GRASS_ACCENT = (38, 52, 44) WATER_SPOT = (25, 45, 65) WATER_DEEP = (18, 35, 55) # Grid GRID_LINE = (45, 55, 60) GRID_LINE_NIGHT = (25, 30, 38) # Special locations OIL_FIELD = (35, 35, 35) OIL_GLOW = (80, 70, 45) TEMPLE_GLOW = (100, 80, 140) # Religion colors RELIGIONS = { "solaris": (255, 180, 50), # Golden sun "aquarius": (50, 150, 220), # Ocean blue "terranus": (140, 100, 60), # Earth brown "ignis": (220, 80, 40), # Fire red "naturis": (80, 180, 80), # Forest green "atheist": (100, 100, 100), # Gray } # Faction colors FACTIONS = { "northlands": (100, 150, 200), # Ice blue "riverfolk": (60, 140, 170), # River teal "forestkin": (80, 140, 70), # Forest green "mountaineer": (130, 110, 90), # Mountain brown "plainsmen": (180, 160, 100), # Plains gold "neutral": (100, 100, 100), # Gray } class MapRenderer: """Renders the map/terrain background with special locations.""" def __init__( self, screen: pygame.Surface, map_rect: pygame.Rect, world_width: int = 30, world_height: int = 30, ): self.screen = screen self.map_rect = map_rect self.world_width = world_width self.world_height = world_height self._cell_width = map_rect.width / world_width self._cell_height = map_rect.height / world_height # Animation state self.animation_tick = 0 # Pre-generate terrain self._terrain_cache = self._generate_terrain() # Surface cache for static elements self._terrain_surface: pygame.Surface | None = None self._cached_dimensions = (world_width, world_height, map_rect.width, map_rect.height) def _generate_terrain(self) -> list[list[int]]: """Generate terrain variation using noise-like pattern.""" random.seed(42) # Consistent terrain terrain = [] for y in range(self.world_height): row = [] for x in range(self.world_width): # Create organic-looking patterns noise = ( math.sin(x * 0.3) * math.cos(y * 0.3) + math.sin(x * 0.7 + y * 0.5) * 0.5 ) if noise > 0.8: row.append(2) # Water elif noise > 0.3: row.append(1) # Dark grass elif noise < -0.5: row.append(3) # Accent grass else: row.append(0) # Light grass terrain.append(row) return terrain def update_dimensions(self, world_width: int, world_height: int) -> None: """Update world dimensions and recalculate cell sizes.""" if world_width != self.world_width or world_height != self.world_height: self.world_width = world_width self.world_height = world_height self._cell_width = self.map_rect.width / world_width self._cell_height = self.map_rect.height / world_height self._terrain_cache = self._generate_terrain() self._terrain_surface = None # Invalidate cache def grid_to_screen(self, grid_x: int, grid_y: int) -> tuple[int, int]: """Convert grid coordinates to screen coordinates (center of cell).""" screen_x = self.map_rect.left + (grid_x + 0.5) * self._cell_width screen_y = self.map_rect.top + (grid_y + 0.5) * self._cell_height return int(screen_x), int(screen_y) def get_cell_size(self) -> tuple[int, int]: """Get the size of a single cell.""" return int(self._cell_width), int(self._cell_height) def _render_terrain_surface(self, is_night: bool) -> pygame.Surface: """Render terrain to a cached surface.""" surface = pygame.Surface((self.map_rect.width, self.map_rect.height)) # Fill background bg_color = Colors.NIGHT_BG if is_night else Colors.DAY_BG surface.fill(bg_color) # Draw terrain cells for y in range(self.world_height): for x in range(self.world_width): cell_rect = pygame.Rect( x * self._cell_width, y * self._cell_height, self._cell_width + 1, self._cell_height + 1, ) terrain_type = self._terrain_cache[y][x] if is_night: if terrain_type == 2: color = (15, 25, 40) elif terrain_type == 1: color = (18, 25, 22) elif terrain_type == 3: color = (22, 30, 26) else: color = (20, 28, 24) else: if terrain_type == 2: color = Colors.WATER_SPOT elif terrain_type == 1: color = Colors.GRASS_DARK elif terrain_type == 3: color = Colors.GRASS_ACCENT else: color = Colors.GRASS_LIGHT pygame.draw.rect(surface, color, cell_rect) # Draw subtle grid grid_color = Colors.GRID_LINE_NIGHT if is_night else Colors.GRID_LINE for x in range(self.world_width + 1): start_x = x * self._cell_width pygame.draw.line( surface, grid_color, (start_x, 0), (start_x, self.map_rect.height), 1, ) for y in range(self.world_height + 1): start_y = y * self._cell_height pygame.draw.line( surface, grid_color, (0, start_y), (self.map_rect.width, start_y), 1, ) return surface def _draw_oil_field(self, oil_field: dict, is_night: bool) -> None: """Draw an oil field with pulsing glow effect.""" pos = oil_field.get("position", {"x": 0, "y": 0}) screen_x, screen_y = self.grid_to_screen(pos["x"], pos["y"]) cell_w, cell_h = self.get_cell_size() radius = min(cell_w, cell_h) // 2 - 2 # Pulsing glow pulse = 0.7 + 0.3 * math.sin(self.animation_tick * 0.05) glow_color = tuple(int(c * pulse) for c in Colors.OIL_GLOW) # Outer glow for i in range(3, 0, -1): alpha = int(30 * pulse / i) glow_surface = pygame.Surface((radius * 4, radius * 4), pygame.SRCALPHA) pygame.draw.circle( glow_surface, (*glow_color, alpha), (radius * 2, radius * 2), radius + i * 3, ) self.screen.blit( glow_surface, (screen_x - radius * 2, screen_y - radius * 2), ) # Oil derrick shape pygame.draw.circle(self.screen, Colors.OIL_FIELD, (screen_x, screen_y), radius) pygame.draw.circle(self.screen, glow_color, (screen_x, screen_y), radius, 2) # Derrick icon (triangle) points = [ (screen_x, screen_y - radius + 2), (screen_x - radius // 2, screen_y + radius // 2), (screen_x + radius // 2, screen_y + radius // 2), ] pygame.draw.polygon(self.screen, glow_color, points) pygame.draw.polygon(self.screen, (40, 40, 40), points, 1) # Oil remaining indicator oil_remaining = oil_field.get("oil_remaining", 1000) if oil_remaining < 500: # Low oil warning warning_pulse = 0.5 + 0.5 * math.sin(self.animation_tick * 0.1) warning_color = (int(200 * warning_pulse), int(60 * warning_pulse), 0) pygame.draw.circle( self.screen, warning_color, (screen_x + radius, screen_y - radius), 4, ) def _draw_temple(self, temple: dict, is_night: bool) -> None: """Draw a temple with religion-colored glow.""" pos = temple.get("position", {"x": 0, "y": 0}) religion_type = temple.get("religion_type", "atheist") screen_x, screen_y = self.grid_to_screen(pos["x"], pos["y"]) cell_w, cell_h = self.get_cell_size() radius = min(cell_w, cell_h) // 2 - 2 # Get religion color religion_color = Colors.RELIGIONS.get(religion_type, Colors.RELIGIONS["atheist"]) # Pulsing glow pulse = 0.6 + 0.4 * math.sin(self.animation_tick * 0.03 + hash(religion_type) % 10) glow_color = tuple(int(c * pulse) for c in religion_color) # Outer divine glow for i in range(4, 0, -1): alpha = int(40 * pulse / i) glow_surface = pygame.Surface((radius * 5, radius * 5), pygame.SRCALPHA) pygame.draw.circle( glow_surface, (*glow_color, alpha), (radius * 2.5, radius * 2.5), int(radius + i * 4), ) self.screen.blit( glow_surface, (screen_x - radius * 2.5, screen_y - radius * 2.5), ) # Temple base pygame.draw.circle(self.screen, (40, 35, 50), (screen_x, screen_y), radius) pygame.draw.circle(self.screen, glow_color, (screen_x, screen_y), radius, 2) # Temple icon (cross/star pattern) half = radius // 2 pygame.draw.line(self.screen, glow_color, (screen_x, screen_y - half), (screen_x, screen_y + half), 2) pygame.draw.line(self.screen, glow_color, (screen_x - half, screen_y), (screen_x + half, screen_y), 2) # Religion initial font = pygame.font.Font(None, max(10, radius)) initial = religion_type[0].upper() if religion_type else "?" text = font.render(initial, True, (255, 255, 255)) text_rect = text.get_rect(center=(screen_x, screen_y)) self.screen.blit(text, text_rect) def draw(self, state: "SimulationState") -> None: """Draw the map background with all features.""" self.animation_tick += 1 is_night = state.time_of_day == "night" # Draw terrain (cached for performance) current_dims = (self.world_width, self.world_height, self.map_rect.width, self.map_rect.height) if self._terrain_surface is None or self._cached_dimensions != current_dims: self._terrain_surface = self._render_terrain_surface(is_night) self._cached_dimensions = current_dims self.screen.blit(self._terrain_surface, self.map_rect.topleft) # Draw oil fields for oil_field in state.oil_fields: self._draw_oil_field(oil_field, is_night) # Draw temples for temple in state.temples: self._draw_temple(temple, is_night) # Draw border with glow effect border_color = (50, 55, 70) if not is_night else (35, 40, 55) pygame.draw.rect(self.screen, border_color, self.map_rect, 2) # Corner accents corner_size = 15 accent_color = (80, 100, 130) if not is_night else (60, 75, 100) corners = [ (self.map_rect.left, self.map_rect.top), (self.map_rect.right - corner_size, self.map_rect.top), (self.map_rect.left, self.map_rect.bottom - corner_size), (self.map_rect.right - corner_size, self.map_rect.bottom - corner_size), ] for cx, cy in corners: pygame.draw.rect(self.screen, accent_color, (cx, cy, corner_size, corner_size), 1)