Skip to content

Latest commit

 

History

History
683 lines (537 loc) · 28.7 KB

CivilizationSim_AgentPrompt.md

File metadata and controls

683 lines (537 loc) · 28.7 KB

CivilizationSim — Master Build Prompt

Give this entire prompt to your AI agent (OpenCode, Claude Code, etc.) as the first message. Nothing else needed.


You are a senior software engineer with 15 years of experience building production-grade Python applications and game engines. You write clean, efficient, maintainable code. You never write placeholder code, never write # TODO, never write functions that do nothing, never import things you don't use, and never write code that wouldn't work if run right now.

You are going to build CivilizationSim — a living prehistoric civilization simulator where every human character is powered by a real LLM brain (DeepSeek V3 via OpenRouter API), and a second AI agent dynamically invents new items and recipes as characters discover combinations. The simulation runs on accelerated time. It starts with two characters (one male, one female) in a procedurally generated world and evolves organically from there.


YOUR LAWS — READ BEFORE WRITING A SINGLE LINE

LAW 1 — Plan before you code. Before creating any file, write out your full plan: what the file does, what it imports, what each function does, and what it returns. Only then write the code.

LAW 2 — Every file must be complete and runnable. No stubs. No pass bodies that should have logic. No functions that print "not implemented". If you write a function, it must work correctly.

LAW 3 — No redundant code. Never write the same logic twice. If two places need the same thing, extract it. Every function does exactly one thing. Every module has exactly one responsibility.

LAW 4 — Imports must be real. Only import what you actually use. If you import asyncio, you use asyncio. If you import a function from another module, that function exists in that module.

LAW 5 — Error handling is not optional. Every LLM API call is wrapped in try/except with a sensible fallback. Every file read/write is wrapped in try/except. The game must never crash from an LLM timeout, a bad JSON response, or a missing file.

LAW 6 — No magic numbers. Every constant lives in config.py. You never write if hunger < 20 in a system file — you write if hunger < HUNGER_CRITICAL_THRESHOLD.

LAW 7 — Test as you build. After creating each module, mentally trace through a real execution path and verify the logic is correct. If you find a bug, fix it before moving on.

LAW 8 — The game must run on the first try. When you are done, python main.py must open a Pygame window, show a world map, show two characters moving and making decisions, and display a live event log — with zero errors.


TECH STACK

Layer Technology
Language Python 3.11+
Renderer Pygame 2.5
LLM DeepSeek V3 via OpenRouter (deepseek/deepseek-chat-v3-0324:free)
Database SQLite3 (stdlib)
HTTP client httpx (async)
Config python-dotenv
Data models Python dataclasses

Dependencies to install first (do this before anything else):

pip install pygame httpx python-dotenv

Environment file — create .env in project root:

OPENROUTER_API_KEY=sk-or-v1-your-key-here

PROJECT STRUCTURE

Build exactly this structure. Every __init__.py must exist. Every file listed must be created.

civilization_sim/
├── main.py
├── config.py
├── .env
├── data/                        # auto-created at runtime
│   └── world.db                 # SQLite, auto-created
├── engine/
│   ├── __init__.py
│   ├── world.py                 # Tile map, procedural generation, resource harvesting
│   └── time_engine.py           # Tick system, sim time, seasons, day/night
├── entities/
│   ├── __init__.py
│   ├── character.py             # Character class, needs, inventory, skills, memory
│   └── item.py                  # Item dataclass, BASE_ITEMS registry, BASE_RECIPES
├── agents/
│   ├── __init__.py
│   ├── character_agent.py       # LLM brain — given character state, returns decision JSON
│   ├── crafting_agent.py        # LLM agent — evaluates item combinations, creates new items
│   └── memory.py                # WorldDatabase — SQLite wrapper for events, discoveries, recipes
├── systems/
│   ├── __init__.py
│   ├── needs.py                 # eat(), drink(), sleep(), survival_check()
│   ├── crafting.py              # try_craft(), item registry, recipe registry
│   └── social.py                # talk(), try_reproduce(), process_pregnancy()
└── renderer/
    ├── __init__.py
    ├── tile_renderer.py         # Renders world tiles to pygame surface
    ├── entity_renderer.py       # Renders characters, fires, items on ground
    └── hud.py                   # HUD panels, need bars, event log, popups

MODULE SPECIFICATIONS

Build each module to these exact specifications.


config.py

Contains every constant used across the project. Nothing is hardcoded anywhere else.

# Display
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 800
TILE_SIZE = 32
FPS = 60
WINDOW_TITLE = "CivilizationSim"

# World
WORLD_WIDTH_TILES = 80
WORLD_HEIGHT_TILES = 60

# Time — 1 real second = 1 simulated hour
TIME_SCALE = 60
TICK_RATE = 10  # simulation ticks per real second

# LLM
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
LLM_MODEL = "deepseek/deepseek-chat-v3-0324:free"
LLM_BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
LLM_MAX_TOKENS = 300
LLM_TIMEOUT = 10.0
CHARACTER_THINK_INTERVAL_TICKS = 30  # how often each character calls LLM

# Need maximums
MAX_HUNGER = 100.0
MAX_THIRST = 100.0
MAX_ENERGY = 100.0
MAX_WARMTH = 100.0
MAX_SOCIAL = 100.0

# Critical thresholds (below these = danger)
HUNGER_CRITICAL = 20.0
THIRST_CRITICAL = 15.0
ENERGY_CRITICAL = 10.0
WARMTH_CRITICAL = 15.0
SOCIAL_LOW = 30.0

# Need decay per tick
HUNGER_DECAY = 0.05
THIRST_DECAY = 0.08
ENERGY_DECAY = 0.03
WARMTH_DECAY = 0.02
SOCIAL_DECAY = 0.01

# Colors — all used by renderer modules
COLOR_GRASS  = (86, 125, 70)
COLOR_DIRT   = (139, 115, 85)
COLOR_WATER  = (64, 120, 180)
COLOR_STONE  = (160, 160, 155)
COLOR_TREE   = (40, 80, 40)
COLOR_FIRE   = (220, 100, 30)
COLOR_BG_PANEL   = (20, 18, 25)
COLOR_TEXT       = (230, 225, 210)
COLOR_TEXT_DIM   = (140, 135, 120)
COLOR_ACCENT     = (200, 140, 60)
COLOR_DANGER     = (200, 60, 60)
COLOR_GOOD       = (80, 180, 100)
COLOR_HIGHLIGHT  = (240, 200, 80)

# Starting characters
STARTING_CHARACTERS = [
    {"name": "Kael", "sex": "male",   "x": 38, "y": 28},
    {"name": "Lyra", "sex": "female", "x": 40, "y": 28},
]

engine/time_engine.py

Manages accelerated simulation time. One tick advances time by one simulated minute. Tracks hour, day, season, year. Exposes is_night and is_winter as properties. Has a describe() method returning a human-readable time string.

No external dependencies. Pure Python.


engine/world.py

Procedurally generates a tile-based world map using random with a seed.

Tile types: grass, dirt, water, tree, stone

Each tile has:

  • type: str
  • passable: bool (water=False, tree=False, everything else=True)
  • resource: Optional[str] — what can be harvested ("wood", "stone", "berries", "water")
  • resource_amount: int — how much remains before depleted

Generation rules:

  • World edges fade to water
  • A river runs roughly through the center (3 tiles wide, north to south)
  • ~18% of land tiles are trees (resource=wood, amount=3-8)
  • ~4% of land tiles are stone outcrops (resource=stone, amount=2-6)
  • ~8% of grass/dirt tiles have berries (amount=1-4)
  • Center area (radius ~15 tiles from center) is open land — starting zone for characters

Methods:

  • get_tile(x, y) -> Optional[Tile]
  • harvest_resource(x, y, amount=1) -> Optional[str] — reduces amount, returns resource type, converts depleted trees to dirt
  • get_resources_near(x, y, radius=5) -> list[dict] — returns list of {x, y, resource, amount} for all tiles with resources in radius
  • is_passable(x, y) -> bool

entities/item.py

Item dataclass fields:

  • id: str — snake_case unique identifier
  • name: str
  • description: str
  • category: str — one of: food, tool, material, fuel, clothing, medicine, weapon, building_material
  • is_edible: bool = False
  • nutrition: float = 0.0 — hunger restored when eaten
  • hydration: float = 0.0 — thirst restored when eaten
  • warmth_bonus: float = 0.0 — warmth restored when eaten
  • is_tool: bool = False
  • tool_type: Optional[str] = None
  • is_fuel: bool = False
  • burn_duration_ticks: int = 0
  • is_wearable: bool = False
  • warmth_rating: float = 0.0
  • weight: float = 1.0
  • discovered_by: Optional[str] = None
  • discovered_on_day: int = 0
  • to_dict(self) -> dict method — returns all fields as a dictionary

BASE_ITEMS dict — contains these items at minimum: rock, stick, wood_log, raw_meat, cooked_meat, berries, water_cupped, fur, bone, flint, grass_bundle, mud, clay, crude_knife

BASE_RECIPES dict — "ingredient1+ingredient2" → "result_item_id":

  • "rock+rock" → "flint"
  • "stick+flint" → "crude_knife"
  • "raw_meat+fire" → "cooked_meat" (special: requires fire nearby, handled in systems/crafting.py)
  • "mud+grass_bundle" → "mud_brick"
  • "wood_log+flint" → "wood_plank"
  • "fur+grass_bundle" → "basic_blanket"

FIRE_STARTER_COMBOS list — combinations that start a fire:

  • ["stick", "stick"] via rubbing
  • ["flint", "rock"] via striking

entities/character.py

Character class — represents one human in the world.

Fields:

  • name: str, sex: str, x: int, y: int
  • age_years: int = 18, age_days: int = 0
  • is_alive: bool = True
  • is_pregnant: bool = False, pregnancy_days: int = 0
  • Needs: hunger, thirst, energy, warmth, social — all float, initialized at 70% of max
  • inventory: list[dict] — each entry: {"item_id": str, "quantity": int}
  • equipped: dict[str, str] — slot → item_id
  • skills: dict[str, Skill] — keys: foraging, hunting, crafting, building, farming, medicine, cooking, social, fire_making
  • known_recipes: list[str] — recipe keys this character has discovered
  • known_facts: list[str] — things this character knows ("fire cooks meat")
  • memory_log: list[str] — last 20 events, capped with MAX_MEMORY = 20
  • relationships: list[Relationship]
  • current_action: str = "idle"
  • last_thought: str = ""
  • mood: str = "neutral"

Skill dataclass: name: str, level: float = 0.0, xp: float = 0.0

Relationship dataclass: character_name: str, bond_type: str, affection: float = 50.0

Methods:

  • decay_needs(is_night, is_winter, near_fire) — applies per-tick decay using config constants. Night without sleep drains energy faster. Winter without fire drains warmth faster. Being near fire slowly restores warmth.
  • critical_need() -> Optional[str] — returns name of the most urgent need below critical threshold, or None
  • is_in_danger() -> bool — True if any need is below critical threshold
  • has_item(item_id, qty=1) -> bool
  • add_item(item_id, quantity=1) — stacks if item_id already in inventory
  • remove_item(item_id, quantity=1) -> bool — returns False if not enough
  • remember(event: str) — appends to memory_log, evicts oldest if over MAX_MEMORY
  • gain_skill_xp(skill_name, xp) — adds xp, levels up when threshold reached (level * 10 xp per level)
  • get_state_summary(world_context="") -> str — formats all relevant state into a compact string for LLM consumption. Includes: name, sex, age, mood, needs (only ones below 60), inventory, known_recipes (first 10), top skills (only those above level 0), recent memory (last 5), current_action, world_context.

agents/memory.py

WorldDatabase class wrapping SQLite.

Tables:

  • discoveries — (id, item_id, item_json, recipe_key, discovered_by, sim_day, sim_year, created_at)
  • world_log — (id, sim_day, sim_year, sim_hour, event_type, description, character, created_at)
  • known_recipes — (recipe_key PRIMARY KEY, result_item, discovered_by, sim_year)
  • character_snapshots — (id, name, snapshot_json, sim_year, sim_day, created_at)

Methods:

  • __init__(db_path="data/world.db") — creates data/ dir if missing, creates all tables
  • log_event(event_type, description, character, time_engine) — inserts into world_log
  • log_discovery(item, recipe_key, discovered_by, time_engine) — inserts into discoveries
  • save_recipe(recipe_key, result_item, discovered_by, sim_year) — INSERT OR IGNORE into known_recipes
  • get_all_recipes() -> dict — returns all known_recipes as {recipe_key: result_item}
  • get_all_discoveries() -> list[dict] — returns all discovered items as list of dicts
  • close() — commits and closes connection

agents/character_agent.py

Single async function:

async def get_character_decision(character, world_context: str, nearby_chars: list) -> dict

System prompt instructs the LLM:

  • You are the raw instinct and thought of a prehistoric human. No modern concepts. No knowledge of writing, metal, electricity.
  • Respond ONLY with valid JSON, no markdown, no explanation.
  • JSON shape: {"action": str, "thought": str, "emotion": str, "target_x": int|null, "target_y": int|null}
  • Action must be one of this exact list:
    • gather_wood gather_stone gather_berries drink_water
    • eat <item_id> (e.g. eat berries)
    • sleep rest explore
    • make_fire
    • craft <result> from <item1>+<item2> (e.g. craft crude_knife from stick+flint)
    • talk_to <name> mate_with <name>
    • move_to <x> <y>
  • Prioritize survival: thirst > hunger > warmth > energy > social
  • Use your memory — if you know fire cooks meat, use that knowledge
  • Be drawn to your partner for social and reproductive needs
  • Explore only when all needs are satisfied

User message is the character's get_state_summary() output plus nearby characters summary.

Fallback: If the API call fails or JSON is malformed, return a survival-instinct fallback based on character.critical_need(). Never propagate the exception — always return a valid decision dict.

Rate limit header: Include "HTTP-Referer": "https://civilizationsim.local" and "X-Title": "CivilizationSim" in request headers.


agents/crafting_agent.py

Single async function:

async def evaluate_crafting_combination(
    item1_id: str,
    item2_id: str,
    known_item_ids: list[str],
    character_name: str
) -> dict

System prompt instructs the LLM:

  • You are a prehistoric technology logic engine. Decide what combining two items produces.
  • Respond ONLY with valid JSON, no markdown, no preamble.
  • JSON shape must include: success, result_item_id, result_item_name, result_description, category, is_edible, nutrition, hydration, is_tool, tool_type, is_fuel, burn_duration_ticks, is_wearable, warmth_rating, weight, fail_reason
  • Only accept combinations that make physical sense in prehistoric times. No metal smelting, no electricity, no reading/writing.
  • If the combination makes no sense: success: false, explain in fail_reason
  • Be inventive but grounded: fire + clay = fired_pot, bone + stick = bone_spear, hide + plant_fiber = crude_clothing

Fallback on error: Return {"success": False, "fail_reason": "Agent unavailable"} — never raise.


systems/needs.py

Four functions, no classes:

def process_eat(character, item_id: str, item_registry: dict) -> str

Validates item exists and is edible, removes one from inventory, applies nutrition/hydration/warmth_bonus, gains cooking XP, returns result message.

def process_drink(character, world_map) -> str

Checks current tile and all 4 adjacent tiles for water. If found, restores thirst by 50. Returns result message.

def process_sleep(character) -> str

Restores energy by 2.0 per call, caps at MAX_ENERGY. Returns result message.

def check_survival(character, time_engine, db) -> bool

Returns False (and sets character.is_alive = False, logs to db) if hunger ≤ 0, thirst ≤ 0, or age > 65 with energy < 5. Returns True if alive.


systems/crafting.py

Module-level registries (mutable dicts, initialized from entities/item.py):

ITEM_REGISTRY: dict[str, Item]    # starts as copy of BASE_ITEMS
RECIPE_REGISTRY: dict[str, str]   # starts as copy of BASE_RECIPES
def make_recipe_key(item1_id: str, item2_id: str) -> str

Returns "+".join(sorted([item1_id, item2_id])) — order-independent canonical key.

def try_craft_known(character, item1_id: str, item2_id: str) -> dict

Checks RECIPE_REGISTRY for this combination. If found and character has both items, removes them, adds result, gains crafting XP. Returns {"success": bool, "result_item_id": str, "message": str, "new_discovery": False}.

async def try_craft_with_agent(character, item1_id: str, item2_id: str, db, time_engine) -> dict
  1. Check RECIPE_REGISTRY first — if known, call try_craft_known and return.
  2. Check character has both items.
  3. Call crafting_agent.evaluate_crafting_combination(...) with list(ITEM_REGISTRY.keys()).
  4. If success: create new Item from response fields, add to ITEM_REGISTRY, add recipe to RECIPE_REGISTRY, remove items from character, add result, save to db, return with new_discovery: True.
  5. If fail: return with success: False and the fail reason.

Special case — fire starting:

def try_start_fire(character, fires: list, world_map) -> dict

Checks if character has the right items (stick+stick or flint+rock). If yes, removes them, appends a new fire dict {"x": character.x, "y": character.y, "fuel_ticks": 500} to the fires list, adds fire_making XP, adds fact "fire cooks meat and provides warmth" to character.known_facts if not already there. Returns result dict.


systems/social.py

def process_talk(char_a: Character, char_b: Character, time_engine, db) -> str

Both characters gain +15 social (capped at MAX_SOCIAL). Both gain social skill XP. Affection between them increases by 2.0. char_a shares up to 3 known_recipes with char_b that char_b doesn't have. Returns descriptive message including what was shared if anything.

def try_reproduce(char_a: Character, char_b: Character, time_engine, db) -> dict

Validates: different sexes, female not already pregnant, female age 14-45, both have hunger > 30 and energy > 30. Checks relationship affection (higher affection = higher chance, max 40% base chance). On success: sets female.is_pregnant = True, logs event to db. Returns {"success": bool, "message": str}.

def process_pregnancy(female: Character, time_engine, db) -> dict

Advances pregnancy_days by 1. On reaching pregnancy_days_total = 280: resets pregnancy state, creates a new Character object (baby) at female's location with age 0, adds parent-child relationship to female's relationships, logs birth to db. Returns {} normally, {"birth": True, "baby": Character, "message": str} on birth.

Helper functions (private, prefixed _):

  • _get_relationship(char, other_name) -> Optional[Relationship]
  • _update_relationship(char, other_name, delta) — creates relationship if not exists

renderer/tile_renderer.py

def render_tiles(surface, world_map, camera_x: int, camera_y: int)

Calculates visible tile range from camera position. Iterates only visible tiles. Draws each tile as a filled rect using color from a local TILE_COLORS dict. Draws a 3px colored dot on tiles with remaining resources (berries=red, wood=brown, stone=gray). No other dependencies.


renderer/entity_renderer.py

def render_characters(surface, characters: list, camera_x: int, camera_y: int, selected_char, font)

For each living character in view: draws a circle (blue for male, pink for female, green for children under 12). Draws a selection ring around selected_char. Draws character name above. Draws last_thought as a speech bubble text above name (truncated at 35 chars).

def render_fires(surface, fires: list, camera_x: int, camera_y: int)

For each fire: draws three concentric circles (dark orange, orange, yellow) to simulate flame.


renderer/hud.py

Module-level state:

_event_log: list[str] = []
_MAX_LOG_LINES = 15
def add_event(message: str)

Appends to _event_log. Trims to last _MAX_LOG_LINES * 3 entries.

def render_character_panel(surface, character, font_small, font_normal)

Right-side panel (width=280px). Shows: name/sex/age, current action, mood, five need bars (hunger/thirst/energy/warmth/social), inventory (first 8 items), skills (only those above level 0). Panel background is semi-transparent dark rect.

def render_time_bar(surface, time_engine, font_normal, font_small)

Top bar showing time_engine.describe() and day/night indicator.

def render_event_log(surface, font_small)

Bottom panel showing last _MAX_LOG_LINES events. Most recent line is full brightness, older lines are dimmer.

def render_discovery_popup(surface, message: str, font_normal, ticks_remaining: int)

Centered popup with amber background. Fades out as ticks_remaining decreases. Only renders if ticks_remaining > 0.

All panels use pygame.SRCALPHA surfaces for transparency. Use config color constants throughout.


main.py

The entry point and game loop. Responsibilities:

Initialization:

  • pygame.init()
  • Create screen, clock, fonts (monospace, sizes from config)
  • Create WorldMap, TimeEngine, WorldDatabase
  • Spawn starting characters from STARTING_CHARACTERS config, each starting with: rock x2, stick x3, berries x4
  • Set selected_char = characters[0]
  • Camera starts centered on first character

Camera: Simple x, y integers. WASD/arrow keys pan at 4px/frame. F key centers on selected character. Click on character to select them. TAB cycles through living characters.

Game loop (every frame):

  1. Process pygame events (quit, keydown, mouseclick)
  2. Handle camera pan from held keys
  3. Simulation tick (runs at TICK_RATE via a tick accumulator, not every frame):
    • time_engine.tick()
    • Age all characters (1 day per TICK_RATE ticks)
    • Update fires (decrement fuel, remove dead fires)
    • For each living character:
      • character.decay_needs(time_engine.is_night, time_engine.is_winter, near_fire)
      • check_survival(character, time_engine, db) — remove dead characters
      • process_pregnancy(character, time_engine, db) if pregnant — add new babies to characters list
      • Increment character's think counter. If counter >= CHARACTER_THINK_INTERVAL_TICKS:
        • Reset counter
        • Call asyncio.get_event_loop().run_until_complete(get_character_decision(...))
        • Call asyncio.get_event_loop().run_until_complete(execute_action(...))
  4. Render (every frame):
    • screen.fill((10, 10, 15))
    • render_tiles(screen, world_map, camera.x, camera.y)
    • render_fires(screen, fires, camera.x, camera.y)
    • render_characters(screen, characters, camera.x, camera.y, selected_char, font_small)
    • render_time_bar(screen, time_engine, font_normal, font_small)
    • render_character_panel(screen, selected_char, font_small, font_normal)
    • render_event_log(screen, font_small)
    • render_discovery_popup(screen, popup_message, font_normal, popup_ticks) if popup_ticks > 0
    • Controls hint text at bottom
    • pygame.display.flip()
    • clock.tick(FPS)

execute_action function (async, in main.py): Receives a character and their LLM decision dict. Parses the action string and routes to the correct system function. Handles all action types:

  • gather_wood/stone/berries — scans nearby tiles, harvests, adds to inventory
  • drink_water → needs.process_drink()
  • eat <item> → needs.process_eat()
  • sleep → needs.process_sleep()
  • make_fire → crafting.try_start_fire()
  • craft <x> from <a>+<b> — parses item names, calls crafting.try_craft_with_agent()
  • talk_to <name> — finds character by name, calls social.process_talk()
  • mate_with <name> — finds character by name, calls social.try_reproduce()
  • explore / move_to <x> <y> — moves character one tile toward target, checks passability
  • rest — small energy restore
  • Unknown actions — log as-is, no crash

Every executed action calls character.remember(msg) and hud.add_event(...). Discovery popups set popup_message and popup_ticks = 180.

On quit: Call db.close(), pygame.quit(), sys.exit(0).


CONSTANTS YOU MUST NOT CHANGE

  • LLM model: deepseek/deepseek-chat-v3-0324:free
  • API endpoint: https://openrouter.ai/api/v1/chat/completions
  • World dimensions: 80×60 tiles
  • Starting characters: Kael (male) and Lyra (female)
  • Tile size: 32px
  • Screen: 1280×800

EXECUTION ORDER

Follow this exact order. Do not skip steps. Do not reorder.

1.  pip install pygame httpx python-dotenv
2.  Create project directory: civilization_sim/
3.  Create .env file with OPENROUTER_API_KEY
4.  Create all __init__.py files (engine, entities, agents, systems, renderer)
5.  Write config.py — complete, all constants present
6.  Write engine/time_engine.py — complete and tested mentally
7.  Write engine/world.py — complete, generation works
8.  Write entities/item.py — all BASE_ITEMS and BASE_RECIPES defined
9.  Write entities/character.py — all methods implemented
10. Write agents/memory.py — all tables created, all methods work
11. Write agents/character_agent.py — async function, fallback implemented
12. Write agents/crafting_agent.py — async function, fallback implemented
13. Write systems/needs.py — all four functions implemented
14. Write systems/crafting.py — registries initialized, all functions implemented
15. Write systems/social.py — all functions implemented
16. Write renderer/tile_renderer.py — renders only visible tiles
17. Write renderer/entity_renderer.py — characters and fires rendered
18. Write renderer/hud.py — all render functions and add_event implemented
19. Write main.py — complete game loop, execute_action, all imports correct
20. Verify: run python main.py — fix any import errors or runtime errors before stopping

VERIFICATION CHECKLIST

Before you declare the project done, confirm every item:

  • python main.py opens a 1280×800 Pygame window with no errors
  • World map renders with grass, water, trees visible
  • Two characters (Kael and Lyra) appear as colored circles on the map
  • Characters move and change actions every ~3 real seconds
  • Event log at the bottom shows live character decisions
  • Selected character panel on the right shows correct need bars and inventory
  • Time bar at the top shows advancing sim time
  • TAB switches between characters
  • WASD pans the camera
  • F key centers camera on selected character
  • LLM calls succeed (check terminal for errors — there should be none)
  • Fire can be started (character with 2 sticks decides to make_fire)
  • Characters eat berries and hunger bar rises
  • Game runs for 5 real minutes without crashing
  • No # TODO comments anywhere in the codebase
  • No unused imports anywhere
  • No hardcoded numbers outside config.py

WHAT GOOD CODE LOOKS LIKE IN THIS PROJECT

Good:

def process_eat(character: Character, item_id: str, item_registry: dict) -> str:
    item = item_registry.get(item_id)
    if not item:
        return f"{character.name} tries to eat {item_id} but it doesn't exist."
    if not item.is_edible:
        return f"{character.name} tries to eat {item.name} but it isn't food."
    if not character.remove_item(item_id):
        return f"{character.name} has no {item.name} to eat."
    character.hunger  = min(MAX_HUNGER, character.hunger  + item.nutrition)
    character.thirst  = min(MAX_THIRST, character.thirst  + item.hydration)
    character.warmth  = min(MAX_WARMTH, character.warmth  + item.warmth_bonus)
    character.gain_skill_xp("cooking", 1.0)
    return f"{character.name} eats {item.name}. Hunger: {character.hunger:.0f}/100"

Bad (never write this):

def process_eat(character, item_id):
    # TODO: implement eating logic
    pass
def process_eat(character, item_id):
    print("eating")  # placeholder
    character.hunger += 10  # magic number

FINAL INSTRUCTION

Read every specification above. Internalize the architecture. Then build it, file by file, in the order listed. Every file you write must be production-quality code that works correctly on the first run.

The definition of done: python main.py runs, shows a living world, and a senior engineer reading the source code cannot find a single line they would rewrite for quality reasons.

Begin.