# Project export: Debate RPG

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: UC Berkeley AI Hackathon 2026
- Tagline: Build, train, and evolve a squad of AI debaters, then unleash them in a roguelike where persuasion is combat, rival models are capturable, and every encounter is a battle of ideas.
- Devpost: https://devpost.com/software/debate-rpg
- GitHub: https://github.com/NicoV7/BerkeleyAIHackathon2026#
- Team: 4 GitHub contributor(s) — Nicolas Vega (112 commits), Claude Opus 4.8 (1M context) (71 commits), Edwn1x (3 commits), Aglavlr (2 commits)

## Devpost submission (written by the team)

### Inspiration

Debate RPG was inspired by the idea that learning debate should feel less like memorizing rules and more like going on an adventure. Many people struggle with argumentation because feedback is intimidating, abstract, or delayed. We wanted to turn that process into something playful: a pixel RPG where every battle helps players practice clearer reasoning, stronger rebuttals, better evidence use, and more effective communication.

### What it does

Debate RPG is a Pokémon-style pixel roguelike where players explore an overworld, encounter AI opponents, and win battles by making persuasive arguments. Instead of attacking with swords or spells, players use debate skills like logic, evidence, framing, rebuttal, and emotional appeal. The game helps players build argumentative skills by giving immediate feedback after each round. A judge evaluates how strong each argument was, then translates that into RPG-style damage, momentum, and battle outcomes. This makes communication practice interactive, low-pressure, and fun. At the start, players select an avatar type that represents a debate approach: Logos: logic, evidence, facts, and cause-and-effect reasoning Pathos: emotion, storytelling, stakes, and audience connection Ethos: credibility, authority, trust, and expertise Chaos: disruption, reframing, and flipping assumptions Socratic: questioning, probing, and exposing weak claims Rhetoric: style, framing, delivery, and memorable wording These avatar types are not just cosmetic. They connect directly to the battle system through type advantages, similar to elemental matchups in RPGs. For example, Logos is strong against Pathos, Pathos is strong against Ethos, Ethos is strong against Chaos, Chaos is strong against Logos and Rhetoric, Socratic is strong against Rhetoric and Pathos, and Rhetoric is strong against Logos. This teaches players that different debate strategies work better in different situations. During battles, players type their own argument, choose rhetorical skills, and watch the judge score each round. Stronger arguments deal more damage, while weaker arguments may fail to affect the opponent. Skills like Logical Thrust, Emotional Appeal, Authority Cite, Socratic Probe, Reframe Attack, and Rhetorical Flourish each have their own debate type, MP cost, and damage multiplier. The game also includes RPG systems like HP, MP, levels, attack and defense stats, momentum, party members, gacha-style persona pulls, enemy captures, training, and gambit-style behavior rules for AI teammates. Players can capture defeated debate opponents, add them to their party, train them, and customize how they argue in future encounters.

### How we built it

We built the frontend with Vite, React, Phaser 3, and Tailwind to create the pixel RPG overworld and battle interface. The backend uses Python FastAPI to run encounters, debate logic, judging, training, memory, and party systems. The game uses local-first AI models through Ollama, with support for models like Gemma and Qwen. We also built systems for: AI debate opponents AI debate opponents Player-written arguments Player-written arguments Round-by-round judging Round-by-round judging RPG-style damage and HP RPG-style damage and HP Capturing defeated AI agents Capturing defeated AI agents Party management Party management Gambit-style behavior rules Gambit-style behavior rules Training and evolution through debate history Training and evolution through debate history Memory/RAG so agents can learn from past battles Memory/RAG so agents can learn from past battles

### Challenges we ran into

One major challenge was making debate feel like a game without losing the educational value. We had to convert abstract debate quality into concrete RPG mechanics like damage, momentum, skills, and victory conditions. We also ran into common AI engineering issues: local model latency, inconsistent judge outputs, empty or low-quality model responses, and coordinating multiple AI agents in one battle. On the technical side, we had to handle real-time WebSocket battle streams, Redis encounter state, Postgres persistence, and frontend/backend integration across several systems. Another challenge was clarity. Debate feedback can easily become too academic or too vague, so we worked on making the judge’s feedback understandable and actionable for players.

### Accomplishments we're proud of

We are proud that Debate RPG turns communication practice into a complete interactive game loop. Players can explore, encounter opponents, argue, receive feedback, win battles, capture agents, and improve their team. We are especially proud of connecting debate skills to RPG mechanics in a way that feels natural. A strong rebuttal is not just “correct”; it lands like a powerful move. Weak evidence does not just lose points; it affects the battle. This makes learning argumentation more memorable and engaging.

### What we learned

We learned that educational tools work best when feedback is immediate, specific, and tied to action. Debate becomes easier to practice when players can experiment, fail safely, and see exactly why an argument worked or did not work. We also learned that building with local AI requires careful fallback systems, timeouts, and simple UX patterns. The game needs to stay responsive even when models are slow or inconsistent.

### What's next

Next, we want to improve the coaching layer so players get clearer suggestions after each debate round. We also want to add richer debate topics, stronger judge models, better skill progression, more enemy types, and more visible growth over time. Long term, Debate RPG could become a full learning platform where players practice debate, public speaking, critical thinking, and persuasive writing through a fun pixel RPG adventure.

## README (from the GitHub repository)

# Debate RPG — BerkeleyAIHackathon2026

Debate RPG is a local-first creature-collector RPG where the monsters are
autonomous debate agents. Explore a tile overworld, meet villagers and rival
agents, descend into nearby dungeons, and trigger turn-based argument battles
against 1-3 enemy LLM personas. You do not micromanage every line your party
says: you collect agents, give them gambit-style behavior rules, train their
prompt genomes, and let them argue for you.

The core loop blends Pokémon-style capture, old-school RPG overworld traversal,
and AI debate tactics. Winning means making the stronger case, using agent
skills at the right moment, remembering prior encounters, and evolving your
party into faster, sharper rhetorical specialists. The stack runs locally with
Ollama by default, with optional hosted model keys for lower-latency fallback
providers.

## Stack
- **Frontend:** Vite + React + Phaser 3 (overworld) + Tailwind. (`apps/web`)
- **Backend:** Python FastAPI — agents, debate engine, RL training, hybrid RAG. (`apps/api`)
- **Models:** local-first gateway → Ollama (Gemma 3 / Qwen) with Claude/OpenAI pluggable.
- **Data:** Postgres + pgvector (memories/RAG), Redis (live encounter cache).

## Quickstart
```bash
pnpm install:game
pnpm game:start
```

`pnpm install:game` installs workspace dependencies, prepares `.env`, pulls and
builds Docker dependencies, and opens API-key pages for any missing optional
hosted model providers. The game still runs fully local with Ollama if you leave
those keys blank.

`pnpm game:start` starts Postgres, Redis, Ollama, the FastAPI server, and the
Vite/Phaser web client. It also pulls the default Ollama models and opens
<http://localhost:5173>. API docs are available at <http://localhost:8000/docs>.

Useful commands:

```bash
pnpm install:game --no-open       # install without opening API-key pages
pnpm game:start --no-open         # start services and print the game URL
pnpm game:start --skip-model-pull # start faster if models are already local
pnpm logs                         # follow Docker service logs
pnpm down                         # stop the local stack
```

## Hosted Model Fallbacks

The API gateway can route battle actors and judges through fast hosted providers
with a local Ollama fallback. Put provider keys only in the ignored root `.env`
file. Latency-first defaults can use `GROQ_API_KEY`, `CEREBRAS_API_KEY`,
`GEMINI_API_KEY`, and `OPENROUTER_API_KEY`; `ANTHROPIC_API_KEY` and
`OPENAI_API_KEY` are available for explicit provider routing. `.env.example`
intentionally contains placeholders only.

Default latency-first candidates are configured with:

```bash
GATEWAY_ACTOR_CANDIDATES=groq/llama-3.1-8b-instant,cerebras/llama-3.3-70b,gemini/gemini-2.5-flash-lite,openrouter/openrouter/free,ollama/gemma3:1b
GATEWAY_JUDGE_CANDIDATES=groq/llama-3.3-70b-versatile,cerebras/llama-3.3-70b,gemini/gemini-2.5-flash,ollama/gemma3:1b
```

Use the Pareto pseudo-models from code paths that should prefer the fastest
acceptable provider:

```python
await gateway.complete(messages, model="pareto-actor")
await gateway.complete(messages, model="pareto-judge", json_mode=True)
```

To refresh the in-process benchmark frontier, run from `apps/api`:

```bash
uv run python -m app.scripts.bench_models --role actor --runs 3
uv run python -m app.scripts.bench_models --role judge --runs 3
```

The redacted runtime status is available at `GET /api/models/pareto`.

## Battle Harness Training

Run a small prompt-genome loop that trains both the party agent and the enemy
agent against each other. The loop uses the Pareto actor model by default,
scores latency first, and keeps only mutations that clear quality/reliability
floors.

```bash
cd apps/api
uv run python -m app.scripts.run_battle_training --cycles 1 --rounds 1 --variants 1 --model pareto-actor
```

The runner prints JSON with party/enemy score deltas, accepted mutation ops,
latency measurements, and final genomes. It does not fine-tune weights and does
not print provider secrets.

Encounter pacing is controlled by `BATTLE_DAMAGE_MULTIPLIER` (default `1.0`).
Raise it to shorten battles, lower it if playtests feel too abrupt.

## Layout
```
apps/web/         Vite + React + Phaser
apps/api/         FastAPI (gateway, debate, memory, training, party, routers, db)
packages/shared/  shared TS enums + generated API types
infra/            docker-compose, postgres init, ollama model pull
prompts/          plan files + per-workstream subagent briefs
memories/         in-repo agent memory log (handoff between waves/sessions)
```

See [prompts/ok-claude-use-toasty-pascal.md](prompts/ok-claude-use-toasty-pascal.md) for the full build plan.


## Detected evidence (automated analysis)

Indexed codebase: 318 recognized source files, 2333 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- Ollama (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 621)

```
.agents/skills/battle-skill-effect-testing/SKILL.md
.env.example
.gitignore
apps/api/app/__init__.py
apps/api/app/config.py
apps/api/app/db/__init__.py
apps/api/app/db/models.py
apps/api/app/db/session.py
apps/api/app/debate/__init__.py
apps/api/app/debate/coach.py
apps/api/app/debate/damage.py
apps/api/app/debate/gambits.py
apps/api/app/debate/judge.py
apps/api/app/debate/latency_metrics.py
apps/api/app/debate/materialize.py
apps/api/app/debate/orchestrator.py
apps/api/app/debate/skill_engine.py
apps/api/app/debate/topics.py
apps/api/app/economy/__init__.py
apps/api/app/economy/award.py
apps/api/app/economy/catalog.py
apps/api/app/gateway/__init__.py
apps/api/app/gateway/candidates.py
apps/api/app/gateway/gateway.py
apps/api/app/gateway/models.py
apps/api/app/gateway/pareto.py
apps/api/app/llm/__init__.py
apps/api/app/llm/hosted_adapter.py
apps/api/app/main.py
apps/api/app/memory/__init__.py
apps/api/app/memory/embeddings.py
apps/api/app/memory/redis_index.py
apps/api/app/memory/retriever.py
apps/api/app/memory/store.py
apps/api/app/party/__init__.py
apps/api/app/party/archetypes.py
apps/api/app/party/balance.py
apps/api/app/party/generator.py
apps/api/app/party/hydrate.py
apps/api/app/party/persona.py
apps/api/app/party/personas_seed.py
apps/api/app/party/progress.py
apps/api/app/redis_state.py
apps/api/app/routers/__init__.py
apps/api/app/routers/debate.py
apps/api/app/routers/economy.py
apps/api/app/routers/encounter.py
apps/api/app/routers/gacha.py
apps/api/app/routers/health.py
apps/api/app/routers/map.py
apps/api/app/routers/memory.py
apps/api/app/routers/models.py
apps/api/app/routers/onboarding.py
apps/api/app/routers/party.py
apps/api/app/routers/runs.py
apps/api/app/routers/training.py
apps/api/app/routers/world.py
apps/api/app/schemas.py
apps/api/app/scripts/__init__.py
apps/api/app/scripts/bench_latency.py
apps/api/app/scripts/bench_models.py
apps/api/app/scripts/run_battle_training.py
apps/api/app/scripts/seed_catalog.py
apps/api/app/serializers.py
apps/api/app/skills/analogy_strike.md
apps/api/app/skills/anecdote.md
apps/api/app/skills/audience_pulse.md
apps/api/app/skills/authority_cite.md
apps/api/app/skills/authority_reversal.md
apps/api/app/skills/brevity_curse.md
apps/api/app/skills/burden_shift.md
apps/api/app/skills/cadence_boost.md
apps/api/app/skills/callback_crescendo.md
apps/api/app/skills/causal_lock.md
apps/api/app/skills/closing_image.md
apps/api/app/skills/contradiction_ledger.md
apps/api/app/skills/credential_drop.md
apps/api/app/skills/credibility_shield.md
apps/api/app/skills/cross_examination.md
apps/api/app/skills/data_lens.md
apps/api/app/skills/emotional_appeal.md
apps/api/app/skills/empathy_mirror.md
apps/api/app/skills/error_budget.md
apps/api/app/skills/evidence_echo.md
apps/api/app/skills/expert_witness.md
apps/api/app/skills/frame_glitch.md
apps/api/app/skills/heartstring_amplifier.md
apps/api/app/skills/hypocrisy_hook.md
apps/api/app/skills/judge_question.md
apps/api/app/skills/leading_question.md
apps/api/app/skills/logical_thrust.md
apps/api/app/skills/memory_recall.md
apps/api/app/skills/memory_trap.md
apps/api/app/skills/metaphor_screen.md
apps/api/app/skills/motive_audit.md
apps/api/app/skills/narrowing_funnel.md
apps/api/app/skills/noise_screen.md
apps/api/app/skills/paradox_seed.md
apps/api/app/skills/pattern_break.md
apps/api/app/skills/phrase_reversal.md
apps/api/app/skills/premise_recall.md
apps/api/app/skills/quiet_devastation.md
apps/api/app/skills/reframe_attack.md
apps/api/app/skills/reputation_crosscheck.md
apps/api/app/skills/rhetorical_flourish.md
apps/api/app/skills/socratic_probe.md
apps/api/app/skills/source_triangulation.md
apps/api/app/skills/stakes_shield.md
apps/api/app/skills/steel_man.md
apps/api/app/skills/trapdoor_read.md
apps/api/app/skills/trust_anchor.md
apps/api/app/skills/whataboutism.md
apps/api/app/skills/wild_premise.md
apps/api/app/skills/wound_callback.md
apps/api/app/training/__init__.py
apps/api/app/training/battle_loop.py
apps/api/app/training/benchmark.py
apps/api/app/training/genome.py
apps/api/app/training/gepa.py
apps/api/app/training/grpo_hitl.py
[501 more files omitted for size]
```

### Dependencies

- apps/api/pyproject.toml: asyncpg@>=0.30, dspy-ai@>=2.5, fastapi@>=0.115, greenlet@>=3.1, httpx@>=0.27, json-repair@>=0.30, numpy@>=2.1, pgvector@>=0.3.6, pydantic@>=2.9, pydantic-settings@>=2.6, pytest@>=8.3, pytest-asyncio@>=0.24, pytest-cov@>=5.0, python-json-logger@>=2.0, redis@>=5.2, redisvl@>=0.4, ruff@>=0.7, sqlmodel@>=0.0.22, uvicorn[standard]@>=0.32, websockets@>=13.1
- apps/web/package.json: @debate/shared@workspace:*, @playwright/test@^1.48.0, @tailwindcss/vite@^4.0.0, @tanstack/react-query@^5.59.0, @testing-library/jest-dom@^6.6.0, @testing-library/react@^16.1.0, @types/react@^19.0.0, @types/react-dom@^19.0.0, @vitejs/plugin-react@^4.3.3, @vitest/coverage-v8@^2.1.8, jsdom@^25.0.0, openapi-typescript@^7.4.0, phaser@^4.2.0, react@^19.0.0, react-dom@^19.0.0, react-router-dom@^6.27.0, tailwindcss@^4.0.0, tone@^15.1.22, typescript@^5.6.0, vite@^6.0.0, vitest@^2.1.8, zustand@^5.0.0

### Recent commits (newest first)

- fix(npc-dialogue): route chat through llm gateway
- Merge pull request #39 from NicoV7/fix/restore-encounter-stage-ui
- fix(battle): ground enemy rebuttals in player turns
- fix(battle): restore encounter stage UI
- chore(git): merge main into battle branch
- fix(battle): repair malformed rebuttal formatting
- Merge pull request #38 from NicoV7/codex/fix/interior-exit-freeze
- feat(battle): live enemy rebuttals via Claude Haiku candidate chain (#34)
- fix(game): resume overworld before stopping interior
- fix(battle): complete enemy rebuttals before emitting
- Merge pull request #37 from NicoV7/codex/fix/interior-overworld-exit
- fix(game): resume overworld after interior exit
- fix(battle): ground enemy rebuttals in player turns
- fix(game): gate interior exit tiles
- fix(npc-dialogue): route chat through llm gateway
- fix(overworld): spawn on open ground at the curated start, not inside a town building
- Merge pull request #33 from NicoV7/staging
- Merge pull request #32 from NicoV7/codex/feat/install-and-start-game
- feat(setup): add install and game start commands
- Merge pull request #31 from NicoV7/staging

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

### TODOS.md

```markdown

## Deferred from autoplan (battle-feel wave) 2026-06-20
- Speculative enemy-opening pre-gen (only if quality experiment shows CPU headroom)
- Player picks side per battle (Topic option C)
- Hosted-model default (contingent on quality experiment outcome)
- Instrument a 'player is improving' signal (CEO suggestion — the real moat)

## Deferred from autoplan (latency + world-engine review) 2026-06-20
- CEO reframe (declined for now): pivot demo around the coach/debate moment + judge legibility; cut Track B tail
- Re-litigate "everything stays local" for demo-only (bigger/hosted model for debate+coach)
- Single-slot inference queue/manager (both eng voices' top ask — root constraint; > hackathon scope)
- Judge score → RPG language ("Logic hit", "Evidence weak", "Fallacy exposed") + coach explains the correction
- Track B Waves 2-3 procedural biomes/dungeons/interiors hardening; POI needs stable `id` + tile payload for interior render
- A3 speculative rebuttal: enforce cancellation below asyncio (Ollama keeps slot); only after measuring enemy model-vs-template rate

```

### prompts/ws-d-memory-rag.md

```markdown
# WS-D — Memory / Hybrid RAG

## Goal
Persistent per-monster memory of run events (BATTLE / PLAYER / CHARACTER), with hybrid
retrieval (vector similarity + keyword/trigram) injected into agents during debates and
training.

## You OWN
- `apps/api/app/memory/embeddings.py` — `embed(texts) -> list[vec]` via the gateway's embed (nomic-embed-text)
- `apps/api/app/memory/store.py` — `write_event(session, monster_id, run_id, event_type, content, ...)`: summarize (short, via gateway), embed, populate `keywords` (lowercased keyword string for trigram index), insert Memory row
- `apps/api/app/memory/retriever.py` — `retrieve(session, monster_id, query, k=4, event_type=None) -> list[MemoryItem-like]`: hybrid search = pgvector cosine (`embedding <=> q`) + trigram/ILIKE keyword match on `keywords`, merged via Reciprocal Rank Fusion, top-k
- `apps/api/app/routers/memory.py` — `GET /api/monsters/{id}/memories?q=&type=` (-> MemoryQueryResult) for the inspector/debug UI

## Interfaces
- Expose: `retrieve(...)` (WS-B injects results into agent context — it imports `from app.memory.retriever import retrieve`, falling back to [] if absent, so ship it), and `write_event(...)` (WS-B/WS-E call on notable events).
- Consume: `gateway.embed`, models (Memory, EMBED_DIM=768), schemas (MemoryItem, MemoryQueryResult), get_session.

## Details
- Embedding dim is 768 (nomic-embed-text). The Memory.embedding column is `Vector(768)`.
- For pgvector cosine in SQLAlchemy, use the `pgvector` package's operators (e.g. `Memory.embedding.cosine_distance(q)`); order ascending, limit.
- `keywords` is a plain text column with a GIN trigram index (`gin_trgm_ops`). Populate it with a normalized bag of salient words from content; query with ILIKE/`%` or `pg_trgm` similarity.
- RRF: rank lists from each method, score = sum(1/(60+rank)); sort desc; take top-k.
- Keep summaries short (one sentence) — they get injected into small-model context.

## Definition of done
- A pytest (or script) that: writes 3-4 memories for a monster, then `retrieve` returns the most relevant by a query — runs against the live postgres+ollama (embeddings) stack.
- `GET /api/monsters/{id}/memories` returns items.

## Rules
- Do NOT edit frozen shared files (models, redis_state, schemas, enums, main.py) — `memory` router auto-mounts. If you need a Memory column you don't have, STOP and note it in your memory file (the orchestrator will reconcile) rather than editing models.py.
- Do NOT edit pyproject.toml; note new deps in `memories/ws-d-memory.md`.
- When done: write `memories/ws-d-memory.md` and commit.

```

### package.json

```
{
  "name": "debate-rpg",
  "version": "0.0.0",
  "private": true,
  "description": "AI Debate RPG — Pokémon-style roguelike where you debate enemy LLM agents",
  "scripts": {
    "install:game": "bash scripts/install.sh",
    "game:start": "bash scripts/start-game.sh",
    "dev:web": "pnpm --filter web dev",
    "build:web": "pnpm --filter web build",
    "gen:types": "pnpm --filter web gen:types",
    "up": "docker compose -f infra/docker-compose.yml up -d --build",
    "down": "docker compose -f infra/docker-compose.yml down",
    "logs": "docker compose -f infra/docker-compose.yml logs -f",
    "pull-models": "bash infra/ollama/pull-models.sh"
  },
  "packageManager": "pnpm@10.28.0"
}

```

### infra/docker-compose.yml

```yaml
name: debate-rpg

services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-debate}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-debate}
      POSTGRES_DB: ${POSTGRES_DB:-debate}
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-debate} -d ${POSTGRES_DB:-debate}"]
      interval: 5s
      timeout: 3s
      retries: 20

  redis:
    # Redis 8 bundles the search & query engine (RediSearch) needed by RedisVL
    # for the memory vector hot-cache. Backward-compatible with the enc:* keys.
    image: redis:8-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 20

  ollama:
    image: ollama/ollama:latest
    ports:
      - "11434:11434"
    volumes:
      - ollama:/root/.ollama
    healthcheck:
      test: ["CMD-SHELL", "ollama list || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 30

  api:
    build:
      context: ../apps/api
      dockerfile: Dockerfile
    env_file:
      - ../.env
    environment:
      # Override host-style URLs with in-network service names.
      DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-debate}:${POSTGRES_PASSWORD:-debate}@postgres:5432/${POSTGRES_DB:-debate}
      REDIS_URL: redis://redis:6379/0
      OLLAMA_BASE_URL: http://ollama:11434
    ports:
      - "${API_PORT:-8000}:8000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      ollama:
        condition: service_started
    volumes:
      - ../apps/api:/app
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

  web:
    build:
      # Repo-root context so the pnpm workspace (@debate/shared) resolves.
      context: ..
      dockerfile: apps/web/Dockerfile
    environment:
      # Empty base => the browser uses same-origin and Vite proxies /api -> api:8000.
      VITE_API_BASE: ""
      VITE_PROXY_TARGET: "http://api:8000"
    ports:
      - "${WEB_PORT:-5173}:5173"
    depends_on:
      - api
    volumes:
      - ../apps/web:/repo/apps/web
      - ../packages:/repo/packages
      - /repo/node_modules
      - /repo/apps/web/node_modules
    command: pnpm dev --host 0.0.0.0

volumes:
  pgdata:
  ollama:

```

### packages/shared/package.json

```
{
  "name": "@debate/shared",
  "version": "0.0.0",
  "type": "module",
  "main": "enums.ts",
  "exports": {
    "./enums": "./enums.ts",
    "./types": "./types.gen.ts"
  }
}

```

### apps/api/Dockerfile

```
# Pin 3.12 for broad wheel availability (host may be 3.14).
FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

# Install uv for fast dependency resolution.
RUN pip install --no-cache-dir uv

# Install deps first for layer caching.
COPY pyproject.toml ./
RUN uv pip install --system -e .

COPY . .

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

```

### apps/web/Dockerfile

```
# Build context is the REPO ROOT (see docker-compose) so the pnpm workspace
# (@debate/shared) resolves. Dev-server image with hot reload via bind mounts.
FROM node:22-slim

RUN corepack enable

WORKDIR /repo

# Install workspace deps using just the manifests first (layer caching).
COPY pnpm-workspace.yaml package.json ./
COPY apps/web/package.json apps/web/package.json
COPY packages/shared/package.json packages/shared/package.json
RUN pnpm install

# Copy the rest (overridden by bind mounts at runtime for hot reload).
COPY . .

WORKDIR /repo/apps/web
EXPOSE 5173
CMD ["pnpm", "dev", "--host", "0.0.0.0"]

```

### apps/web/package.json

```
{
  "name": "web",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "test": "vitest run",
    "test:cov": "vitest run --coverage",
    "e2e": "playwright test",
    "gen:types": "openapi-typescript http://localhost:8000/openapi.json -o ../../packages/shared/types.gen.ts"
  },
  "dependencies": {
    "@debate/shared": "workspace:*",
    "@tanstack/react-query": "^5.59.0",
    "phaser": "^4.2.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router-dom": "^6.27.0",
    "tone": "^15.1.22",
    "zustand": "^5.0.0"
  },
  "devDependencies": {
    "@playwright/test": "^1.48.0",
    "@tailwindcss/vite": "^4.0.0",
    "@testing-library/jest-dom": "^6.6.0",
    "@testing-library/react": "^16.1.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@vitejs/plugin-react": "^4.3.3",
    "@vitest/coverage-v8": "^2.1.8",
    "jsdom": "^25.0.0",
    "openapi-typescript": "^7.4.0",
    "tailwindcss": "^4.0.0",
    "typescript": "^5.6.0",
    "vite": "^6.0.0",
    "vitest": "^2.1.8"
  }
}

```

### apps/api/pyproject.toml

```
[project]
name = "debate-rpg-api"
version = "0.0.0"
description = "AI Debate RPG backend — agents, debate engine, RL training, hybrid RAG"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.115",
    "uvicorn[standard]>=0.32",
    "pydantic>=2.9",
    "pydantic-settings>=2.6",
    "sqlmodel>=0.0.22",
    "asyncpg>=0.30",
    "pgvector>=0.3.6",
    "greenlet>=3.1",
    "redis>=5.2",
    "redisvl>=0.4",
    "httpx>=0.27",
    "numpy>=2.1",
    "python-json-logger>=2.0",
    "json-repair>=0.30",
    "websockets>=13.1",
]

[project.optional-dependencies]
# Heavier / optional: GEPA via DSPy. Falls back to a hand-rolled loop if absent.
training = ["dspy-ai>=2.5"]
dev = ["pytest>=8.3", "pytest-asyncio>=0.24", "pytest-cov>=5.0", "ruff>=0.7"]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
# Coverage is opt-in via `--cov` flags below; we don't force it on every run so a
# bare `pytest` stays fast while the impl fleet is mid-edit.

[tool.coverage.run]
source = ["app"]
branch = true
omit = [
    "*/migrations/*",
    "*/__main__.py",
]

[tool.coverage.report]
show_missing = true
skip_covered = false
# fail_under intentionally not set yet (coverage gate deferred until the
# implementation fleet lands the source under test).
exclude_lines = [
    "pragma: no cover",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
]

```

### apps/web/src/main.tsx

```typescript
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import "./index.css";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
);

```

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