Imported from lateralus426/ai-town-port (
AGENTS.md). Install upstream withnpx skills add lateralus426/ai-town-port. Copyright stays with the author.
AGENTS.md — AI Town Port: Workspace Root
Projects in This Workspace
| Folder | Purpose | Editable |
|---|---|---|
./ai-town/ |
Original TypeScript/Convex/PixiJS source (a16z-infra/ai-town, MIT) | ❌ READ ONLY |
./ai-town-py/ |
Python port target (FastAPI + asyncio + Raylib) | ✅ WRITE HERE |
./openspec/ |
Spec-driven development contracts (OpenSpec) | ✅ WRITE HERE |
./analysis/ |
Artifacts from source analysis (graphs, schema maps, notes) | ✅ WRITE HERE |
Never modify files inside ./ai-town/. Always read it, never write it.
Always check ./openspec/changes/ for an active proposal before implementing anything.
If no proposal exists for a module, create one first and wait for approval.
Port Overview
Porting a16z-infra/ai-town from TypeScript/Convex/PixiJS to Python.
- Frontend: React + PixiJS →
raylib-py(Raylib 5.x Python bindings) - Backend: Convex serverless functions → FastAPI + asyncio + SQLAlchemy
- Vector DB: Convex built-in vector search → ChromaDB (dev) / pgvector (prod)
- AI agents: Convex internalActions → asyncio background tasks + httpx LLM calls
Current Focus: Phase 1 — Raylib Frontend
Work only on the frontend renderer until Phase 1 is complete. Do not start backend porting (engine, agents, API) until explicitly instructed.
Frontend Source Reference
The original frontend lives in ./ai-town/src/. Key files to read before writing anything:
| Read This First | It Defines |
|---|---|
src/components/GameBoard.tsx |
Main canvas, camera pan, tile layer rendering order |
src/components/Player.tsx |
Sprite rendering, animation frame selection, facing direction |
src/components/Conversation.tsx |
Speech bubble position, typing indicator, message display |
src/hooks/useHistoricalTime.ts |
Game clock, interpolation between server ticks |
src/hooks/useQuery.ts |
How frontend polls/subscribes to world state |
data/gentle.js |
Tiled map JSON — tile layers, collision layer, object layer |
data/characters.ts |
Character definitions: name, description, sprite sheet reference |
src/App.tsx |
Top-level layout, viewport sizing |
Frontend Target
All Raylib code goes in ./ai-town-py/renderer/:
renderer/ ├── main_window.py # Raylib window init, main render loop, FPS cap ├── game_board.py # Camera, tile layer rendering (ground → objects → players) ├── player_sprite.py # Sprite sheet loading, animation state, facing direction ├── conversation_hud.py # Speech bubbles, typing dots, message text ├── input_handler.py # Keyboard/mouse → movement inputs, camera pan └── assets.py # Asset loader: textures, tile sheets, sprite atlases
Raylib Frontend Rules
- Raylib window must run on the main thread — never move it to a background thread
- Target 60 FPS render loop with
SetTargetFPS(60) - World state is stubbed as static Python dicts during Phase 1 — no backend needed yet
- Sprite sheets from
./ai-town/data/spritesheets/are reused directly viaLoadTexture() - Map tiles from
./ai-town/data/gentle.jsmust be converted once: runpython util/convert_map.py→ outputs./ai-town-py/data/gentle.json - Camera follows the human player; support WASD pan override
- Render order: ground tiles → decoration tiles → conversation indicators → players → HUD
Phase 1 Milestone Definition
Phase 1 is complete when:
- Tiled map renders correctly (all layers, correct Z-order)
- At least one character walks between two points with correct sprite animation
- Speech bubble appears above a character with static placeholder text
- Camera follows the active player
- Input handler moves the human player on screen
Phase 2 — Backend (starts after Phase 1 milestone)
Do not implement any of this during Phase 1. Listed here for context only.
port-data-models→ SQLAlchemy models mirroringconvex/schema.tsport-util→util/llm.py,util/navigation.py,util/geometry.pyport-engine→ asyncio tick loop mirroringconvex/aiTown/game.tsport-agent-memory→agent/memory.py(embed, store, retrieve)port-agent-conversation→agent/conversation.py(prompt engineering)port-agent-operations→agent/operations.py(decision cycle)port-http-api→ FastAPI routes connecting backend to Raylib renderer
General Conventions (apply to all phases)
- Python 3.11+, all async code uses
async defandawait httpx.AsyncClientfor all HTTP/LLM calls — neverrequests- Env vars mirror originals:
OPENAI_API_KEY,OLLAMA_HOST,OLLAMA_MODEL,OLLAMA_EMBEDDING_MODEL - Config loaded via
python-dotenvinconfig.py - Use
rufffor linting,blackfor formatting - Type-hint everything — use
dataclassesfor in-memory game state, Pydantic for API schemas
Analysis Artifacts
Before implementing any module, Pi must write findings to ./analysis/:
| Artifact | When to Create |
|---|---|
RENDERER_MAP.md |
Before starting Phase 1 — maps PixiJS components to Raylib equivalents |
SCHEMA_MAP.md |
Before Phase 2 data models — TS schema fields → SQLAlchemy fields |
ENGINE_FLOW.md |
Before Phase 2 engine — Convex tick sequence as Python pseudocode |
AGENT_LOOP.md |
Before Phase 2 agents — memory retrieval → LLM → action cycle |
API_SURFACE.md |
Before Phase 2 API — Convex functions → FastAPI route signatures |
OpenSpec Workflow
- Read
./openspec/AGENTS.mdat session start - For each module: check
./openspec/changes/for an active proposal - If no proposal exists: create
proposal.md+tasks.mdin a new change folder, stop, and wait - If proposal is approved: implement per
tasks.md, then runopenspec archive <change-name> - Never write implementation code without an approved OpenSpec proposal
Raylib Frontend Rules
- Raylib window must run on the main thread — never move it to a background thread
- Target 60 FPS render loop with
SetTargetFPS(60) - World state is stubbed as static Python dicts during Phase 1 — no backend needed yet
- Sprite sheets from
./ai-town/data/spritesheets/are reused directly viaLoadTexture() - Map tiles from
./ai-town/data/gentle.jsmust be converted once: runpython util/convert_map.py→ outputs./ai-town-py/data/gentle.json - Camera follows the human player; support WASD pan override
- Render order: ground tiles → decoration tiles → conversation indicators → players → HUD
Phase 1 Milestone Definition
Phase 1 is complete when:
- Tiled map renders correctly (all layers, correct Z-order)
- At least one character walks between two points with correct sprite animation
- Speech bubble appears above a character with static placeholder text
- Camera follows the active player
- Input handler moves the human player on screen
Phase 2 — Backend (starts after Phase 1 milestone)
Do not implement any of this during Phase 1. Listed here for context only.
port-data-models→ SQLAlchemy models mirroringconvex/schema.tsport-util→util/llm.py,util/navigation.py,util/geometry.pyport-engine→ asyncio tick loop mirroringconvex/aiTown/game.tsport-agent-memory→agent/memory.py(embed, store, retrieve)port-agent-conversation→agent/conversation.py(prompt engineering)port-agent-operations→agent/operations.py(decision cycle)port-http-api→ FastAPI routes connecting backend to Raylib renderer
General Conventions (apply to all phases)
- Python 3.11+, all async code uses
async defandawait httpx.AsyncClientfor all HTTP/LLM calls — neverrequests- Env vars mirror originals:
OPENAI_API_KEY,OLLAMA_HOST,OLLAMA_MODEL,OLLAMA_EMBEDDING_MODEL - Config loaded via
python-dotenvinconfig.py - Use
rufffor linting,blackfor formatting - Type-hint everything — use
dataclassesfor in-memory game state, Pydantic for API schemas
Analysis Artifacts
Before implementing any module, Pi must write findings to ./analysis/:
| Artifact | When to Create |
|---|---|
RENDERER_MAP.md |
Before starting Phase 1 — maps PixiJS components to Raylib equivalents |
SCHEMA_MAP.md |
Before Phase 2 data models — TS schema fields → SQLAlchemy fields |
ENGINE_FLOW.md |
Before Phase 2 engine — Convex tick sequence as Python pseudocode |
AGENT_LOOP.md |
Before Phase 2 agents — memory retrieval → LLM → action cycle |
API_SURFACE.md |
Before Phase 2 API — Convex functions → FastAPI route signatures |
OpenSpec Workflow
- Read
./openspec/AGENTS.mdat session start - For each module: check
./openspec/changes/for an active proposal - If no proposal exists: create
proposal.md+tasks.mdin a new change folder, stop, and wait - If proposal is approved: implement per
tasks.md, then runopenspec archive <change-name> - Never write implementation code without an approved OpenSpec proposal