smurfs / src /rendering /renderer.py
BolyosCsaba
initial commit
82fa936
"""
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))