Spaces:
Sleeping
Sleeping
File size: 11,289 Bytes
82fa936 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
"""
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))
|