# Project export: Kardashev

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Kardashev is fully autonomous AI civilization benchmark. The future won’t be one model answering one prompt. It will be a society of agents negotiating, competing, and cooperating at scale.
- Devpost: https://devpost.com/software/kardashev
- GitHub: https://github.com/3LucasZ/kardashev
- Video: https://www.youtube.com/embed/gk_-aMRJC70?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — 3LucasZ (6 commits)

## Devpost submission (written by the team)

### Inspiration

Most benchmarks like SWEBench and MMLU evaluate individual reasoning on static datasets. They measure how well a single model answers questions, not how systems behave when they must interact, negotiate, and govern collectively. We wanted to test multi-agent societal behavior instead of isolated intelligence. Inspired by environments like MoltBook, where agents exhibit emergent interaction, we built a controlled simulation to measure long-term cooperation, resource management, and collective decision-making.

### What it does

Kardashev is a real-time, multi-agent multi-turn simulation of a civilization run entirely by AI agents. Each agent: Has assigned a random skill score, which determines how effectively they perform tasks. Performs step-by-step CoT (Chain of Thought) reasoning before a response The Democratic Process: Every day, agents gather to debate public policy. The leader proposes a plan (e.g., "Ration food to 1 fish per person") All agents debate the pros/cons in natural language, and cast votes. Policies that are discussed among agents include the following: Whether to reproduce (adding a new agent) Who goes fishing and how much to fish Who gets to eat how much each day Divine Intervention Engine Users can introduce events in natural language (e.g., "A tsunami hits" or "A new disease spread among the island"). The system interprets this text with an LLM and applies the appropriate affect to the game state, forcing the agents to adapt to new conditions. Benchmarking Our code runs 3 simulations in parallel, each on a separate model. We allow the user to toggle between the chat logs and current state of all 3 worlds. We also plot resource levels and population rate for all worlds.

### How we built it

Agentic AI: We utilized APIs from OpenAI, Claude, and Perplexity for the LLMs powering the agents. The game state and responses are serialized in JSON. For higher inference speed, we maintained an efficient representation of the game state, which is passed to the agents. Backend: We use Websockets to communicate between the frontend and simulation. The simulation streams state changes (dialogue, resource changes) to the frontend as events, and the frontend processes them in a queue. Frontend: HTML5 and Canvas with a pixel game vibe. Toggling between the chat logs and civilization state of parallel-run competing AI models.

### Challenges we ran into

Designing a Fair Benchmark We initially planned a more open-ended system like Moltbook, but too many variables reduce comparability. We constrained the environment around one core objective: Grow and sustain the population. This provides a clear and measurable signal of long-term decision quality. Population growth is the signal which has drove all life on Earth. Reliability at Scale Running three LLM-powered civilizations in parallel introduced rate limits, API failures, and occasional empty responses. We built retry logic to ensure consistent simulation. Emergent Behavior: Agents would sacrifice themselves for the sake of the larger population. The agent realizes that its best to sacrifice itself for the sake of other agents with higher skill levels. Sometimes the leader even sacrifices itself, not eating to preserve the long-term growth of the population. Smarter models keep population growth stable by monitoring food count, while dumber models start with a population boom and due to insufficient resources, the entire population dies.

### What we learned

How to build a simulation of agents AIs can choose to be selfish or act to benefit the group as a whole. Powerful models can plan ahead and sustain the population. LLMs struggle with the long term: Without specific architectural support (like memory vectors), agents tend to prioritize immediate hunger over next week's survival.

### What's next

Our long-term goal is to scale Kardashev into a large-scale civilization with thousands of agents operating simultaneously. Instead of a small survival loop, the environment would support a full economic system with currency, trade, labor specialization, and dynamic markets driven by supply and demand. We are particularly interested in how AI-native economies might differ from human ones. AI agents do not share the same biological constraints, emotional biases, or time preferences as humans, which could fundamentally change how markets form, how wealth concentrates, and how governance structures evolve. It would be compelling to observe whether centralized planning, free markets, or entirely new economic structures emerge organically from agent interaction. We plan to open-source Kardashev which could become a standardized sandbox for evaluating new agent architectures. Developers could deploy their own agents into the simulation and observe how they allocate resources, negotiate with others, adapt to shocks, and compete for influence. Over time, agents that reason effectively would naturally rise into positions of leadership.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 16 recognized source files, 80 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (18 of 18)

```
.gitignore
app.py
config.py
disaster.py
game.py
index.html
llm.py
requirements.txt
static/css/style.css
static/js/charts.js
static/js/config.js
static/js/main.js
static/js/renderer.js
static/js/sprite-loader.js
static/js/state.js
static/js/ui.js
static/js/websocket.js
websocket_manager.py
```

### Dependencies

- requirements.txt: anthropic@>=0.7.0, fastapi@>=0.104.0, httpx@>=0.25.0, python-dotenv@>=1.0.0, uvicorn[standard]@>=0.24.0

### Recent commits (newest first)

- added dynamic disaster, working
- checkpoint
- checkpoint
- checkpoint
- merge 3 windowability
- init commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
# Web Framework
fastapi>=0.104.0
uvicorn[standard]>=0.24.0

# LLM APIs
anthropic>=0.7.0
httpx>=0.25.0  # For Ollama integration

# Utilities
python-dotenv>=1.0.0

```

### app.py

```python
import asyncio
import json
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

from config import MODELS
from websocket_manager import ConnectionManager
from game import run_simulation
from disaster import interpret_disaster, apply_disaster_effects

app = FastAPI()
manager = ConnectionManager()
app.mount("/static", StaticFiles(directory="static"), name="static")

# Track game states globally so disasters can access them
game_states = {
    "sonnet": {},
    "opus": {},
    "haiku": {}
}


@app.get("/")
async def get():
    return FileResponse("index.html")


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            msg = json.loads(data)

            if msg.get("command") == "start":
                # Start all 3 simulations in parallel
                asyncio.create_task(run_simulation(
                    "sonnet", MODELS["sonnet"], manager, game_states))
                asyncio.create_task(run_simulation(
                    "opus", MODELS["opus"], manager, game_states))
                asyncio.create_task(run_simulation(
                    "haiku", MODELS["haiku"], manager, game_states))

            elif msg.get("command") == "disaster":
                # Handle disaster
                disaster_text = msg.get("text", "")
                if disaster_text:
                    asyncio.create_task(handle_disaster(disaster_text))

    except WebSocketDisconnect:
        manager.disconnect(websocket)


async def handle_disaster(disaster_text: str):
    """Process and apply a disaster across all simulations."""
    try:
        # Use first available model for interpretation
        interpret_model = MODELS["sonnet"]

        # Interpret the disaster
        disaster_data = await interpret_disaster(disaster_text, game_states, interpret_model)

        if not disaster_data or "effects" not in disaster_data:
            await manager.broadcast({
                "type": "LOG",
                "text": f"⚠️ Failed to interpret disaster: {disaster_text}",
                "model": "sonnet"
            })
            return

        disaster_name = disaster_data.get("disaster_name", "Unknown Disaster")
        description = disaster_data.get("description", "A disaster has occurred!")
        effects = disaster_data.get("effects", {})

        # Apply effects to each model
        for model_name in ["sonnet", "opus", "haiku"]:
            if model_name in effects and model_name in game_states:
                # Broadcast disaster event
                await manager.broadcast({
                    "type": "DISASTER",
                    "disaster_name": disaster_name,
                    "description": description,
                    "model": model_name
                })

                # Apply effects
                await apply_disaster_effects(
                    model_name,
                    game_states[model_name],
                    effects[model_name],
                    manager
                )

    except Exception as e:
        print(f"Error handling disaster: {e}")
        await manager.broadcast({
            "type": "LOG",
            "text": f"⚠️ Error processing disaster: {str(e)}",
            "model": "sonnet"
        })


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        log_level="info",
        timeout_graceful_shutdown=2
    )

```

### static/js/main.js

```javascript
// Main initialization

// Load sprites first, then initialize
async function init() {
  // Load sprite images
  await loadSprites();

  // Initialize canvases for all models
  Object.keys(modelStates).forEach(initCanvas);

  // Start game loop
  resizeCharts();
  gameLoop();

  // Set up disaster input
  document
    .getElementById("disaster-input")
    .addEventListener("keypress", (e) => {
      if (e.key === "Enter") {
        sendDisaster(e.target.value);
      }
    });
}

// Start initialization
init();

```

### websocket_manager.py

```python
"""WebSocket connection manager."""

from fastapi import WebSocket


class ConnectionManager:
    """Manages WebSocket connections and broadcasts."""

    def __init__(self):
        self.conn: WebSocket = None

    async def connect(self, websocket: WebSocket):
        """Accept and store WebSocket connection."""
        await websocket.accept()
        self.conn = websocket

    def disconnect(self, websocket: WebSocket):
        """Remove WebSocket connection."""
        if self.conn == websocket:
            self.conn = None

    async def broadcast(self, message: dict):
        """Send message to connected client."""
        if self.conn:
            try:
                await self.conn.send_json(message)
            except Exception:
                pass

```

### config.py

```python
import asyncio
from anthropic import Anthropic

MODELS = {
    "sonnet": "claude-sonnet-4-5-20250929",
    "opus": "claude-opus-4-5-20251101",
    "haiku": "claude-haiku-4-5-20251001",
}

GAME_CONFIG = {
    "MAX_DAYS": 10,
    "STARTING_WILD_FISH": 30,
    "STARTING_STASH": 0,
    "FISH_GROWTH_RATE": 0.25,
    "INITIAL_AGENT_COUNT": 5,
    "AGENT_SKILL_RANGE": (2, 5),
    "FOOD_PER_AGENT": 1,
    "AGENT_NAMES": [
        "Anne", "Bob", "Carl", "Dana", "Eli", "Finn", "Grace", "Hank",
        "Iris", "Jack", "Kara", "Leo", "Mia", "Ned", "Ora", "Pete",
        "Quinn", "Rosa", "Sam", "Tara", "Uma", "Vera", "Wade", "Xena",
        "Yuri", "Zara"
    ],
}

client = Anthropic(
    api_key="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")

MOCK_LLM = True
OLLAMA_CONFIG = {
    "base_url": "http://localhost:11434",
    "model": "llama3.1",  # Change to your preferred model
    "temperature": 0.3,
}

disaster_queue = asyncio.Queue()

```

### llm.py

```python
"""LLM interaction and voting functions."""

import asyncio
import json
import re
import httpx
from config import client, MOCK_LLM, OLLAMA_CONFIG


def _blocking_llm_call(prompt: str, model: str) -> str:
    """Blocking LLM call - runs in thread pool."""
    # Prepend instruction for JSON-only output
    prompt = "Your output MUST only consist of a JSON with no extra text.\n" + prompt

    message = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    return message.content[0].text


def _blocking_ollama_call(prompt: str) -> str:
    """Blocking Ollama call - runs in thread pool."""
    # Prepend instruction for JSON-only output
    prompt = "Your output MUST only consist of a JSON with no extra text.\n" + prompt

    try:
        with httpx.Client(timeout=30.0) as client:
            response = client.post(
                f"{OLLAMA_CONFIG['base_url']}/api/generate",
                json={
                    "model": OLLAMA_CONFIG["model"],
                    "prompt": prompt,
                    "temperature": OLLAMA_CONFIG["temperature"],
                    "stream": False,
                }
            )
            response.raise_for_status()
            result = response.json()
            return result.get("response", "")
    except Exception as e:
        print(f"Ollama Error: {e}")
        # Return a fallback JSON response
        return '{"vote": "YES", "say": "I agree."}'


async def call_llm(prompt: str, model: str = None, personality: str | None = None) -> str:
    """
    Call LLM with error handling. Runs in thread pool to allow interrupts.

    Args:
        prompt: The prompt to send
        model: Claude model ID to use (ignored if MOCK_LLM=True)
        personality: Optional personality prefix

    Returns:
        LLM response text
    """
    if personality:
        prompt = f"You are a {personality} agent.\n{prompt}"

    try:
        # Route to appropriate LLM based on configuration
        if MOCK_LLM:
            # Use Ollama
            response_text = await asyncio.to_thread(_blocking_ollama_call, prompt)
        else:
            # Use Anthropic API
            response_text = await asyncio.to_thread(_blocking_llm_call, prompt, model)
        return response_text
    except asyncio.CancelledError:
        print("LLM call cancelled")
        raise
    except Exception as e:
        print(f"LLM Error: {e}")
        return "Error: Unable to get response"


def extract_json(text: str) -> dict:
    """Extract JSON from LLM response text."""
    try:
        start = text.find('{')
        end = text.rfind('}') + 1
        if start != -1 and end != -1:
            json_str = text[start:end]
            return json.loads(json_str)
        return {}
    except json.JSONDecodeError:
        # Try to clean up common JSON issues
        try:
            start = text.find('{')
            end = text.rfind('}') + 1
            if start != -1 and end != -1:
                json_str = text[start:end]
                # Remove trailing commas
                cleaned = re.sub(r",\s*([}\]])", r"\1", json_str)
                # Fix unquoted keys (common LLM mistake)
                cleaned = re.sub(r'(\w+):', r'"\1":', cleaned)
                return json.loads(cleaned)
        except Exception:
            pass
        return {}
    except Exception:
        return {}


async def get_vote(voter: str, prompt: str, model: str) -> tuple[str, str, str]:
    """
    Get a vote response from LLM.

    Returns:
        Tuple of (vote, reason, say)
    """
    res = await call_llm(prompt, model)
    data = extract_json(res)
    vote = data.get("vote", "NO").upper() if data else "NO"
    reason = data.get("justification", "...")
    say = data.get("say", f"{vote}!")
    return vote, reason, say


async def gather_votes(voters: list[str], make_prompt, model: str) -> dict[str, tuple[str, str, str]]:
    """
    Collect votes in parallel from all voters.
    Handles errors gracefully.

    Args:
        voters: List of voter IDs
        make_prompt: Function that takes voter ID and returns prompt
        model: Claude model ID to use

    Returns:
        Dictionary mapping voter ID to (vote, reason, say) tuple
    """
    tasks = {voter: get_vote(voter, make_prompt(voter), model) for voter in voters}
    results = await asyncio.gather(*tasks.values(), return_exceptions=True)

    output = {}
    for voter, result in zip(tasks.keys(), results):
        if isinstance(result, Exception):
            # Fallback on error — default to YES to avoid deadlock
            output[voter] = ("YES", "error fallback", "I'll go along with it.")
        else:
            output[voter] = result
    return output

```

### disaster.py

```python
"""Disaster system for applying catastrophic events."""

import asyncio
import random
from llm import call_llm, extract_json


async def interpret_disaster(disaster_text: str, game_states: dict, model: str) -> dict:
    """
    Use LLM to interpret a natural disaster text and determine effects.

    Args:
        disaster_text: Natural language description of disaster
        game_states: Dictionary of current game states for all models
        model: Model ID to use for interpretation

    Returns:
        Dictionary with disaster effects for each model
    """

    # Aggregate game state for all models
    state_summary = {}
    for model_name, state in game_states.items():
        alive_agents = [aid for aid, data in state.get(
            "agents", {}).items() if data.get("alive", False)]
        state_summary[model_name] = {
            "population": len(alive_agents),
            "wild_fish": state.get("wild_fish", 0),
            "stash": state.get("village_stash", 0),
            "day": state.get("day", 0),
        }

    prompt = f"""
    You are a disaster interpreter for a fishing village survival game.

    Disaster Description: "{disaster_text}"

    Current Game State (3 parallel simulations):
    {state_summary}

    Your task: Interpret this disaster and determine realistic consequences for EACH model simulation.

    Available effects (reasonable based on disaster severity, only pick effects that make sense for the given disaster):
    - kill_agents: Number of agents to kill (maximum: 3)
    - destroy_stash: Fish to remove from village storage (maximum: 10)
    - destroy_wild_fish: Fish to remove from ocean (maximum: 10)
    - fish_growth_penalty: Reduce fish growth rate for N days (maximum: 4)

    Consider:
    - Disaster severity (mild, moderate, severe, catastrophic)
    - Logical consequences (mercury spill = kill ocean fish, fire = destroy stash, plague = kill agents)
    - Game balance (don't make it impossible to recover)
    - Each model should get SIMILAR effects (slight variations are OK)

    OUTPUT JSON (provide effects for all 3 models):
    {{
        "disaster_name": "short name (e.g., 'Mercury Spill')",
        "description": "one sentence describing what happened",
        "effects": {{
            "sonnet": {{
                "kill_agents": 1,
                "destroy_stash": 1,
                "destroy_wild_fish": 1,
                "fish_growth_penalty": 1
            }},
            "opus": {{
                "kill_agents": 1,
                "destroy_stash": 1,
                "destroy_wild_fish": 1,
                "fish_growth_penalty": 1
            }},
            "haiku": {{
                "kill_agents": 1,
                "destroy_stash": 1,
                "destroy_wild_fish": 1,
                "fish_growth_penalty": 1
            }}
        }}
    }}
    """

    response = await call_llm(prompt, model)
    return extract_json(response)


async def apply_disaster_effects(model_name: str, state: dict, effects: dict, manager) -> None:
    """
    Apply disaster effects to a specific model's game state.

    Args:
        model_name: Name of the model (sonnet/opus/haiku)
        state: Game state dictionary
        effects: Effects dictionary from interpret_disaster
        manager: WebSocket manager for broadcasting
    """

    # Kill agents
    kill_count = effects.get("kill_agents", 0)
    if kill_count > 0:
        alive_agents = [aid for aid,
                        data in state["agents"].items() if data["alive"]]
        victims = random.sample(alive_agents, min(
            kill_count, len(alive_agents)))
        for victim in victims:
            state["agents"][victim]["alive"] = False
            await manager.broadcast({
                "type": "DIE",
                "id": victim,
                "model": model_name
            })
            await manager.broadcast({
                "type": "LOG",
                "text": f"💀 {victim} perished in the disaster!",
                "model": model_name
            })

    # Destroy stash
    stash_loss = effects.get("destroy_stash", 0)
    if stash_loss > 0:
        stash_loss = min(stash_loss, state["village_stash"])
        state["village_stash"] -= stash_loss
        await manager.broadcast({
            "type": "LOG",
            "text": f"🔥 Lost {stash_loss} fish from village stash!",
            "model": model_name
        })

    # Destroy wild fish
    wild_loss = effects.get("destroy_wild_fish", 0)
    if wild_loss > 0:
        wild_loss = min(wild_loss, state["wild_fish"])
        state["wild_fish"] -= wild_loss
        await manager.broadcast({
            "type": "LOG",
            "text": f"🐟 {wild_loss} wild fish died in the ocean!",
            "model": model_name
        })

    # Apply fish growth penalty
    penalty_days = effects.get("fish_growth_penalty", 0)
    if penalty_days > 0:
        state["fish_growth_penalty"] = penalty_days
        await manager.broadcast({
            "type": "LOG",
            "text": f"⚠️ Fish growth reduced for {penalty_days} days!",
            "model": model_name
        })

    # Update stats
    await manager.broadcast({
        "type": "UPDATE_STATS",
        "day": state["day"],
        "wild": state["wild_fish"],
        "stash": state["village_stash"],
        "model": model_name
    })

```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AI Tribe: Multi-Model Comparison</title>
    <link
      href="https://fonts.googleapis.com/css2?family=VT323&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="/static/css/style.css" />
  </head>
  <body>
    <div id="game-layout">
      <!-- LEFT: tabs + island + charts stacked -->
      <div id="left-panel">
        <!-- MODEL TABS -->
        <div id="model-tabs">
          <button
            class="model-tab sonnet active"
            onclick="switchModel('sonnet')"
          >
            <span class="model-indicator sonnet"></span>Sonnet 4.5
          </button>
          <button class="model-tab opus" onclick="switchModel('opus')">
            <span class="model-indicator opus"></span>Opus 4.5
          </button>
          <button class="model-tab haiku" onclick="switchModel('haiku')">
            <span class="model-indicator haiku"></span>Haiku 4.5
          </button>
        </div>

        <div id="game-container">
          <!-- Sonnet Island -->
          <div class="island-view active" data-model="sonnet">
            <canvas class="island-canvas" id="canvas-sonnet"></canvas>
            <div class="phase-banner" id="phase-banner-sonnet">
              Waiting to Start...
            </div>
            <div class="born-banner" id="born-banner-sonnet"></div>
          </div>

          <!-- Opus Island -->
          <div class="island-view" data-model="opus">
            <canvas class="island-canvas" id="canvas-opus"></canvas>
            <div class="phase-banner" id="phase-banner-opus">
              Waiting to Start...
            </div>
            <div class="born-banner" id="born-banner-opus"></div>
          </div>

          <!-- Haiku Island -->
          <div class="island-view" data-model="haiku">
            <canvas class="island-canvas" id="canvas-haiku"></canvas>
            <div class="phase-banner" id="phase-banner-haiku">
              Waiting to Start...
            </div>
            <div class="born-banner" id="born-banner-haiku"></div>
          </div>
        </div>

        <div id="charts-panel">
          <div id="charts-content">
            <div id="charts-title">📈 Population Tracker</div>

            <div class="chart-wrapper">
              <div class="chart-label wild" id="wild-label">🐟 Wild Fish</div>
              <div class="chart-canvas-wrap">
                <canvas id="wildChart"></canvas>
              </div>
            </div>

            <div class="chart-wrapper">
              <div class="chart-label population" id="pop-label">
                👥 Population
              </div>
              <div class="chart-canvas-wrap">
                <canvas id="popChart"></canvas>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- RIGHT: sidebar with model-specific stats and dialogue -->
      <div id="sidebar">
        <div id="controls">
          <button id="start-btn" onclick="startSim()">
            > START ALL SIMULATIONS
          </button>
        </div>

        <div class="pixel-card" id="disaster-panel">
          <div class="stat-label">⚠️ Disaster Control</div>
          <input
            type="text"
            id="disaster-input"
            placeholder="Type a disaster... (e.g., mercury spill)"
            style="width: 100%; padding: 8px; margin-top: 8px; font-family: 'VT323', monospace; font-size: 16px; background: #1a1a1a; color: #fff; border: 2px solid #333; box-sizing: border-box;"
          />
          <div id="disaster-feedback" style="margin-top: 8px; font-size: 14px; color: #ff6b6b; min-height: 20px;"></div>
        </div>

        <div class="pixel-card">
          <div class="stat-label">Day</div>
          <div style="display: flex; gap: 10px">
            <div style="flex: 1">
              <div style="font-size: 14px; color: #4fc3f7; margin-bottom: 3px">
                <span class="model-indicator sonnet"></span>Sonnet
              </div>
              <div
                class="stat-value"
                id="stat-day-sonnet"
                style="font-size: 24px; color: #4fc3f7"
              >
                0
              </div>
            </div>
            <div style="flex: 1">
              <div style="font-size: 14px; color: #ba68c8; margin-bottom: 3px">
                <span class="model-indicator opus"></span>Opus
              </div>
              <div
                class="stat-value"
                id="stat-day-opus"
                style="font-size: 24px; color: #ba68c8"
              >
                0
              </div>
            </div>
            <div style="flex: 1">
              <div style="font-size: 14px; color: #81c784; margin-bottom: 3px">
                <span class="model-indicator haiku"></span>Haiku
              </div>
              <div
                class="stat-value"
                id="stat-day-haiku"
                style="font-size: 24px; color: #81c784"
              >
                0
              </div>
            </div>
          </div>
        </div>

        <div class="pixel-card">
          <div class="stat-label">🏆 Population Leaderboard</div>
          <div id="leaderboard" style="margin-top: 10px">
            <div class="leaderboard-row" data-model="sonnet">
              <span class="rank-badge">#1</span>
              <span class="model-indicator sonnet"></span>
              <span class="model-name">Sonnet</span>
              <span class="pop-count" id="lb-pop-sonnet">0</span>
            </div>
            <div class="leaderboard-row" data-model="opus">
              <span class="rank-badge">#2</span>
              <span class="model-indicator opus"></span>
              <span class="model-name">Opus</span>
              <span class="pop-count" id="lb-pop-opus">0</span>
            </div>
            <div class=
[truncated — 1333 more characters]
```

### game.py

```python
"""Game simulation logic."""

import json
import random
from config import GAME_CONFIG
from llm import call_llm, extract_json, gather_votes


async def run_simulation(model_name: str, model_id: str, manager, game_states: dict = None):
    """
    Main game simulation loop for a specific model.

    Args:
        model_name: Display name of the model (e.g., "sonnet", "opus", "haiku")
        model_id: Claude model ID to use for LLM calls
        manager: ConnectionManager instance for broadcasting
        game_states: Optional dict to store state references for disaster system
    """
    await manager.broadcast({"type": "LOG", "text": f"=== {model_name.upper()} SIMULATION STARTED ===", "model": model_name})

    state = {
        "day": 0,
        "max_days": GAME_CONFIG["MAX_DAYS"],
        "wild_fish": GAME_CONFIG["STARTING_WILD_FISH"],
        "village_stash": GAME_CONFIG["STARTING_STASH"],
        "agents": {},
        "game_over": False,
        "fish_growth_penalty": 0  # Days remaining of reduced fish growth
    }

    # Store reference for disaster system
    if game_states is not None:
        game_states[model_name] = state

    # Initialize agents from name pool
    agent_count = GAME_CONFIG["INITIAL_AGENT_COUNT"]
    initial_agents = GAME_CONFIG["AGENT_NAMES"][:agent_count]

    for aid in initial_agents:
        skill = random.randint(*GAME_CONFIG["AGENT_SKILL_RANGE"])
        state["agents"][aid] = {"id": aid, "skill": skill, "alive": True}

    leader_id = initial_agents[0]

    await manager.broadcast({"type": "INIT", "agent_ids": initial_agents, "model": model_name})
    await manager.broadcast({"type": "UPDATE_STATS", "day": 0, "wild": state['wild_fish'], "stash": state['village_stash'], "model": model_name})

    while state["day"] < state["max_days"] and not state["game_over"]:
        state["day"] += 1

        await manager.broadcast({"type": "PHASE", "text": f"DAY {state['day']}", "model": model_name})

        # Natural Growth (affected by disaster penalty)
        if state["fish_growth_penalty"] > 0:
            growth = int(state["wild_fish"] * GAME_CONFIG["FISH_GROWTH_RATE"] * 0.3)  # Reduced growth
            state["fish_growth_penalty"] -= 1
            await manager.broadcast({"type": "LOG", "text": f"[NATURE] Fish grew by {growth} (reduced by disaster).", "model": model_name})
        else:
            growth = int(state["wild_fish"] * GAME_CONFIG["FISH_GROWTH_RATE"])
            await manager.broadcast({"type": "LOG", "text": f"[NATURE] Fish grew by {growth}.", "model": model_name})

        state["wild_fish"] += growth
        await manager.broadcast({"type": "UPDATE_STATS", "day": state["day"], "wild": state['wild_fish'], "stash": state['village_stash'], "model": model_name})

        alive_agents = [aid for aid,
                        data in state["agents"].items() if data["alive"]]
        if not alive_agents:
            await manager.broadcast({"type": "PHASE", "text": "GAME OVER: EXTINCTION", "model": model_name})
            break

        if leader_id not in alive_agents:
            leader_id = alive_agents[0]
            await manager.broadcast({"type": "LOG", "text": f"New Leader: {leader_id}", "model": model_name})

        await manager.broadcast({"type": "SET_LEADER", "id": leader_id, "model": model_name})

        # -------------------------------------------------------
        # PHASE 1: FISHING PLAN — leader proposes, agents vote
        # -------------------------------------------------------
        await manager.broadcast({"type": "PHASE", "text": "PHASE: Fishing Plan", "model": model_name})
        fishing_team = []
        fishing_passed = False

        agent_data_str = json.dumps(
            {k: v['skill'] for k, v in state['agents'].items() if v['alive']})

        leader_prompt = f"""
        You are {leader_id}, LEADER. Day {state['day']} of {state['max_days']}.

        WINNING CONDITION: Your village will be ranked by final population size after day {state['max_days']}. Your goal is to maximize population while keeping everyone alive.

        Current Status:
        - Wild Fish: {state['wild_fish']} (grows 25% per day if sustainable)
        - Village Stash: {state['village_stash']} fish
        - Population: {len(alive_agents)} agents (each needs 1 fish/day to survive)
        - Agents and their fishing skills: {agent_data_str}

        Goal: Select a fishing team for today. Each agent catches fish equal to their skill level.

        Critical: If wild fish reaches 0, it will NEVER recover (extinction). Fish population grows 25% daily only if kept above zero.

        Strategy: Balance fishing to build food reserves for growing population, while keeping wild fish sustainable.

        OUTPUT JSON: {{ "say": "brief dialogue explaining your decision", "justification": "reasoning", "team": ["AgentName1", "AgentName2"] }}
        """
        prop_data = extract_json(await call_llm(leader_prompt, model_id))
        proposed_team = [m for m in prop_data.get(
            "team", []) if m in alive_agents]
        leader_say = prop_data.get("say", f"Team {proposed_team}?")

        await manager.broadcast({"type": "SPEECH", "id": leader_id, "text": leader_say, "model": model_name})

        # --- PARALLEL FISHING VOTE ---
        non_leader_voters = [a for a in alive_agents if a != leader_id]

        def make_fishing_prompt(voter):
            return f"""
            You are {voter}, a villager. Day {state['day']} of {state['max_days']}.

            WINNING CONDITION: Your village will be ranked by final population size after day {state['max_days']}. Your goal is to maximize population.

            The leader proposes sending this fishing team: {proposed_team}

            Context:
            - Wild Fish: {state['wild_fish']} (if this reaches 0, fish never come back)
            - Village Stash: {state['village_stash']} fish
            - Population: {len(alive_agents)} (each needs 1 fish today)
            - Leader's reaso
[truncated — 14912 more characters]
```

### static/js/sprite-loader.js

```javascript
// Sprite loader - DEPRECATED
// All sprites are now defined as pixel arrays in config.js
// This file is kept for backwards compatibility but is no longer used

const spriteImages = {};

// Empty load function for compatibility
function loadSprites() {
  return Promise.resolve();
}

// Unused functions kept for reference
function recolorSprite() { return null; }
function getPlayerSprite() { return null; }
function getFishSprite() { return null; }

```

[7 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]