""" Renderer with Z-Layer Support Renders: Background → Graves → Resources → Buildings → Smurfs → UI """ import numpy as np from PIL import Image, ImageDraw, ImageFont from typing import TYPE_CHECKING if TYPE_CHECKING: from game_world import SmurfWorld from entities import LifeStage class Renderer: """Handles all rendering with proper Z-layering""" @staticmethod def _get_bold_font(size=10): """Get a bold font, falling back to default if not available""" try: # Try common system fonts on macOS/Linux font_paths = [ "/System/Library/Fonts/Helvetica.ttc", "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/Library/Fonts/Arial Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", ] for path in font_paths: try: return ImageFont.truetype(path, size) except (OSError, IOError): continue except Exception: pass # Fallback to default font (may not be bold, but won't crash) return ImageFont.load_default() @staticmethod def render(world: 'SmurfWorld') -> np.ndarray: """Render the complete game world""" canvas = world.background.copy() img = Image.fromarray(canvas) draw = ImageDraw.Draw(img) # Layer 1: Graves (background) Renderer._render_graves(img, draw, world) # Layer 2: Resources Renderer._render_resources(img, draw, world) # Layer 4: Smurfs (UNDER buildings, OVER resources) Renderer._render_smurfs(img, draw, world) # Layer 3: Buildings Renderer._render_buildings(img, draw, world) # Layer 5: UI/HUD (always on top) Renderer._render_hud(draw, world) # Draw static charmander test (top right corner) from rendering.sprite_generator_pixel_art import PixelArtSpriteGenerator charmander_x = world.width - 100 charmander_y = 20 PixelArtSpriteGenerator.draw_charmander_test(img, draw, charmander_x, charmander_y, scale=2) return np.array(img) @staticmethod def _render_graves(img: Image, draw: ImageDraw, world): """Render graves""" for grave in world.graves: sprite_array = grave.sprite.get_current_frame() sprite_img = Image.fromarray(sprite_array) pos = (int(grave.x - 16), int(grave.y - 16)) img.paste(sprite_img, pos, sprite_img) # Name above grave (gray) draw.text((int(grave.x - len(grave.smurf_name)*3), int(grave.y - 30)), grave.smurf_name, fill=(150, 150, 150)) @staticmethod def _render_resources(img: Image, draw: ImageDraw, world): """Render resources""" for resource in world.resources: if not resource.is_depleted(): sprite_array = resource.sprite.get_current_frame() sprite_img = Image.fromarray(sprite_array) pos = (int(resource.x - 16), int(resource.y - 16)) img.paste(sprite_img, pos, sprite_img) # Amount indicator amount_text = f"{int(resource.amount)}" draw.text((int(resource.x - 8), int(resource.y + 18)), amount_text, fill=(255, 255, 255)) @staticmethod def _render_buildings(img: Image, draw: ImageDraw, world): """Render buildings - Smurfs will walk UNDER these""" for building in world.buildings: sprite_array = building.sprite.get_current_frame() sprite_img = Image.fromarray(sprite_array) size = sprite_array.shape[0] pos = (int(building.x - size//2), int(building.y - size//2)) img.paste(sprite_img, pos, sprite_img) # Storage info storage_text = f"{sum(building.storage.values())}/{building.max_storage}" draw.text((int(building.x - 20), int(building.y + size//2 + 5)), storage_text, fill=(255, 255, 200)) # Level indicator if building.level > 1: draw.text((int(building.x - 15), int(building.y - size//2 - 15)), f"Lv.{building.level}", fill=(255, 215, 0)) @staticmethod def _render_smurfs(img: Image, draw: ImageDraw, world): """Render smurfs with facial expressions""" for smurf in world.smurfs: if not smurf.is_alive: continue sprite = smurf.get_current_sprite() sprite_array = sprite.get_current_frame() sprite_img = Image.fromarray(sprite_array) sprite_size = sprite_array.shape[0] pos = (int(smurf.x - sprite_size//2), int(smurf.y - sprite_size//2)) img.paste(sprite_img, pos, sprite_img) # Name above smurf (color by life stage) name_color = (255, 255, 255) if smurf.life_stage == LifeStage.BABY: name_color = (255, 200, 200) elif smurf.life_stage == LifeStage.ELDER: name_color = (200, 200, 200) bold_font = Renderer._get_bold_font(size=12) draw.text((int(smurf.x - len(smurf.smurf_name)*3), int(smurf.y - sprite_size//2 - 12)), smurf.smurf_name, fill=name_color, font=bold_font) # Life stage emoji # stage_emoji = {"baby": "B", "child": "C", "adult": "A", "elder": "E"} # stage_text = stage_emoji.get(smurf.life_stage.value, "") draw.text((int(smurf.x - sprite_size//2 - 18), int(smurf.y - sprite_size//2)), f"{smurf.social_interaction_count}", fill=(200, 200, 200)) # Inventory indicator (if carrying) # total_inv = sum(smurf.inventory.values()) # if total_inv > 0: # inv_text = f"[{total_inv}]" # draw.text((int(smurf.x + sprite_size//2), int(smurf.y - sprite_size//2)), # inv_text, fill=(255, 255, 150)) if smurf.current_decision: # Use bright yellow for good contrast against green grass, bold like the name draw.text((int(smurf.x + sprite_size//2), int(smurf.y - sprite_size//2)), f"{smurf.current_decision.reasoning}", fill=(255, 255, 0), font=bold_font) # Energy bar (only if low) if smurf.needs.energy < 50: bar_width = 24 energy_width = int((smurf.needs.energy / 100) * bar_width) bar_y = int(smurf.y + sprite_size//2 + 4) draw.rectangle([int(smurf.x - bar_width//2), bar_y, int(smurf.x + bar_width//2), bar_y + 3], fill=(50, 50, 50)) draw.rectangle([int(smurf.x - bar_width//2), bar_y, int(smurf.x - bar_width//2 + energy_width), bar_y + 3], fill=(0, 255, 0) if smurf.needs.energy > 30 else (255, 0, 0)) # Social activity indicator (text instead of emoji) from ai_behavior import SocialActivity if smurf.social_activity == SocialActivity.CHATTING.value: draw.text((int(smurf.x + sprite_size//2 + 10), int(smurf.y - sprite_size//2 + 10)), "CHAT", fill=(200, 100, 255)) elif smurf.social_activity == SocialActivity.PLAYING.value: draw.text((int(smurf.x + sprite_size//2 + 10), int(smurf.y - sprite_size//2 + 10)), "PLAY", fill=(255, 100, 200)) @staticmethod def _render_hud(draw: ImageDraw, world): """Render HUD overlay""" # Top left - Main stats alive_smurfs = len([s for s in world.smurfs if s.is_alive]) babies = len([s for s in world.smurfs if s.is_alive and s.life_stage == LifeStage.BABY]) # Get village goals building_goals = world.get_building_goals() village_goal = world.get_village_goal() # Calculate HUD height based on content hud_height = 130 if building_goals.get("needs_wood_for_building") or building_goals.get("needs_wood_for_upgrade"): hud_height += 40 if village_goal: hud_height += 20 draw.rectangle([5, 5, 250, hud_height], fill=(0, 0, 0, 180)) draw.text((10, 10), f"Smurf Village", fill=(255, 255, 150)) draw.text((10, 30), f"Smurfs: {alive_smurfs} (Babies: {babies})", fill=(255, 255, 255)) draw.text((10, 50), f"Buildings: {len(world.buildings)}", fill=(255, 255, 255)) draw.text((10, 70), f"Resources: {len([r for r in world.resources if not r.is_depleted()])}", fill=(255, 255, 255)) draw.text((10, 90), f"Village Score: {world.village_score}", fill=(255, 215, 0)) draw.text((10, 110), f"Deaths: {len(world.graves)}", fill=(150, 150, 150)) # Display village goals y_offset = 130 if building_goals.get("needs_wood_for_building") or building_goals.get("needs_wood_for_upgrade"): wood_needed = building_goals.get("wood_needed", 0) goal_text = "Build" if building_goals.get("needs_wood_for_building") else "Upgrade" draw.text((10, y_offset), f"Goal: {goal_text} - Need {wood_needed} wood", fill=(255, 200, 100)) y_offset += 20 # Display voted goal (all smurfs except babies vote) voters = [s for s in world.smurfs if s.is_alive and s.life_stage != LifeStage.BABY] if village_goal and len(voters) > 0: goal_display = "Have Fun! 🎮" if village_goal == "have_fun" else "Work Hard! 💪" goal_color = (200, 255, 200) if village_goal == "have_fun" else (255, 200, 200) draw.text((10, y_offset), f"Village Goal: {goal_display}", fill=goal_color) # Show vote counts have_fun_votes = world.children_votes.get("have_fun", 0) work_votes = world.children_votes.get("work", 0) draw.text((10, y_offset + 15), f"Votes: Fun={have_fun_votes} Work={work_votes}", fill=(200, 200, 200), font=Renderer._get_bold_font(size=9)) # Bottom left - Storage if world.buildings: draw.rectangle([5, world.height - 85, 200, world.height - 5], fill=(0, 0, 0, 180)) from entities.enums import ItemType draw.text((10, world.height - 80), "Village Storage:", fill=(255, 255, 150)) total_food = sum(b.storage.get(ItemType.FOOD.value, 0) for b in world.buildings) total_wood = sum(b.storage.get(ItemType.WOOD.value, 0) for b in world.buildings) draw.text((10, world.height - 60), f"Food: {total_food}", fill=(200, 255, 200)) draw.text((10, world.height - 40), f"Wood: {total_wood}", fill=(200, 200, 255)) draw.text((10, world.height - 20), f"Tick: {world.tick}", fill=(200, 200, 200))