Spaces:
Sleeping
Sleeping
| """ | |
| 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""" | |
| 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() | |
| 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) | |
| 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)) | |
| 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)) | |
| 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)) | |
| 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)) | |
| 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)) | |