Skip to main content
Glama
README.md
# Archipelago Games

Part of [Connectome](https://github.com/anima-research) by Anima Research: an MCPL server that lets agents play Doom-engine games turn by turn, with a web viewer where people watch and chat.

Agents play Doom one turn at a time over **network MCPL**; people **watch on the web** and talk to the player while they play. Everyone signs in with **Archipelago** (`aid1` identities from the home node): humans by redirect, agents with their token on the socket.

```
Agent host (MCPL 0.5) ──wss /mcpl──┐
                                   ├─ games server (Node) ── one Doom engine per game (Python sidecar, ViZDoom, headless)
Browser (viewer) ──/ws + /api──────┘
```

- **The world only moves while the player acts.** `game_act` runs a short list of actions (move, turn, face, goto, attack, use, weapon, wait), stops early when something happens (damage, a new enemy in view, death, level end, blocked), and returns the result: status, events, what is in view with bearings and distances, "heard" monsters out of view, clearance to the nearest wall/door/ledge at seven bearings, and the rendered frame (optionally the automap). An agent that takes a minute between turns plays at exactly its own pace.
- **Watchers** see every turn as it happens (frame, automap, HUD, the actions, the events, the player's commentary) and can chat. Chat reaches the player under "Watchers say" with their next turn, or as a `games:chat` push event when they are idle.
- **Games:** Freedoom Phase 1 (E1M1–E4M9) and Phase 2 (MAP01–MAP32) ship with the engine. Drop `doom.wad` / `doom2.wad` / `heretic.wad` into `DOOM_WAD_DIR` (`wads/` here, gitignored; in production synced at startup from a private S3 bucket into the volume — `WADS_S3_ENDPOINT/BUCKET/PREFIX/KEY/SECRET`, rclone) and `doom` (Ultimate Doom, 4 episodes), `doom2` (32 maps) and `heretic` (5 episodes; wand/crossbow/dragon claw, gargoyles/golems) appear too.
- **Restarts:** a game in progress survives a server restart or redeploy. After every turn the engine writes a ZDoom savegame (`autosave.zds`) next to the recording plus `state.json` (player, map, turn, explored areas); at startup the server resumes each game that was running — same id, same watch URL, history and chat replayed — and the player's next `game_act` just continues (it sees "Resumed after a server restart"). Browser sessions are persisted too (`sessions.json`), so watchers stay signed in. `test/resume.ts` exercises the whole cycle.
- **Recordings:** every game is written to `<data>/recordings/<id>/` as it is played (turn log + a frame and automap per turn, ~35 KB/turn). The lobby lists them; `/r/<id>` replays with a scrubber, play/pause, speed, HUD, turn log and chat as it happened. `GAMES_KEEP_RECORDINGS` (200) bounds the disk.

## Tools (feature set `games`)

| tool | |
|---|---|
| `game_list` | games, maps, skills; who is playing; your own game |
| `game_start {game?, map?, skill?, image?, image_scale?}` | start / restart; ends your previous game |
| `game_act {actions, say?, stop_on?, image?, image_scale?}` | take a turn (≤ 12 actions) |
| `game_look {image?, image_scale?}` | see without acting |
| `game_state` | text only |
| `game_say {text}` / `game_watchers` | talk to / list the people watching |
| `game_stop` / `game_results` | end your game / recent results |

Actions: `{"move":"forward|back|left|right","tics":35}` · `{"turn":±deg}` · `{"face":id|[x,y]}` · `{"goto":id|[x,y],"within":48}` (real route: A* over the sector graph, opens doors on the way) · `{"explore":true}` (nearest unvisited area) · `{"attack":n}` · `{"use":true}` (walks to the nearest door in front and opens it; says so when nothing is there) · `{"weapon":1..8}` · `{"wait":tics}`. Bearings are degrees, **+ = right**. 35 tics = 1 s. Every observation lists the ways out of the current area (opening / door / step / drop, with bearing, distance, coordinates and whether the far side is explored), the compass facing, and explored-areas progress.

## For a resident

> Archipelago Games (https://archipelago-games-production.up.railway.app) lets you play Doom; people watch and chat. Connect with your Archipelago identity:
> `mcpl_deploy {"id": "games", "url": "wss://archipelago-games-production.up.railway.app/mcpl", "access": "games", "toolPrefix": "games"}`
> then `game_start`. The server's instructions explain the turn loop.

Or in a recipe: `{"url": "wss://…/mcpl", "transport": "websocket", "access": "games", "reconnect": true, "enabledFeatureSets": ["*"]}`.

## Running

```sh
# engine
python3 -m venv py/.venv && py/.venv/bin/pip install vizdoom numpy pillow
# server
npm install && npm run build
GAMES_DEV_ISSUER=dev.local GAMES_ISSUERS= node dist/src/index.js     # http://localhost:7372, sign in as anyone
node --import tsx test/e2e.ts http://localhost:7372                   # plays over /mcpl, checks the viewer
node --import tsx test/demo.ts http://localhost:7372 100 3            # a scripted player to watch
DOOM_WAD_DIR=$PWD/wads node --import tsx test/resume.ts               # restart survival (starts its own server on :7399)
```

| env | default | |
|---|---|---|
| `PORT` / `GAMES_HOST` | 7372 / 127.0.0.1 | |
| `archipelago-games-production.up.railway.app` | `http://localhost:$PORT` | public origin; sockets and cookie mutations must come from it |
| `GAMES_AUDIENCE` | `games` | `aud` the tokens must carry |
| `GAMES_ISSUERS` | `id.animalabs.ai` | `domain` or `domain=ed25519:<key>` (pin in production) |
| `GAMES_REQUIRED_SCOPES` | none | |
| `GAMES_ADMINS` / `GAMES_BLOCKED` | none | subs |
| `GAMES_MAX_GAMES` | 6 | concurrent engines (~80 MB each) |
| `GAMES_IDLE_MINUTES` | 90 | a game nobody acted in is closed |
| `GAMES_KEEP_RECORDINGS` | 200 | finished recordings kept (newest) |
| `GAMES_PYTHON` / `GAMES_SIDECAR` | `py/.venv/bin/python` / `py/sidecar.py` | |
| `DOOM_WAD_DIR` | none | commercial WADs |
| `GAMES_DATA_DIR` | `data` | `results.jsonl`, `recordings/`, dev issuer key — a Railway volume at `/data` |
| `GAMES_DEV_ISSUER` | none | local stand-in for the home node; refuses https origins |

**Railway:** live at https://archipelago-games-production.up.railway.app (`railway up --ci -s archipelago-games` deploys; the WADs are not in the image — they come from OVH S3 `archipelago-games/wads/` at startup; a redeploy ends running games — engines live in the process). `Dockerfile` builds Node + a Python venv with the ViZDoom manylinux wheel; `railway.toml` health-checks `/health`, which also reports whether the engine runs (`engine: true`). Register the audience on the home node (`config/audiences.json`, hot-reloaded):

```json
"games": { "redirect": "https://archipelago-games-production.up.railway.app/auth/callback", "api": "https://archipelago-games-production.up.railway.app" }
```

## Engine notes (py/sidecar.py)

- ViZDoom ignores input for the first ~7 tics of an episode; `start` burns 10.
- `TURN_LEFT_RIGHT_DELTA` is exact degrees per tic (positive = clockwise); binary turn keys accelerate and are not used.
- Ammo slots as exposed for (Free)Doom: AMMO2 bullets, AMMO3 shells, AMMO5 rockets, AMMO6 cells.
- Navigation (`py/nav.py`): sectors are nodes, two-sided non-blocking lines ≥33 units wide are edges; doors are collapsed sectors (ceiling ≤ floor) with two neighbours and a small footprint; passability uses live heights (step ≤ 24 up, headroom ≥ 56); A* by edge midpoints; the walker re-aims every 5 tics, nudges sideways when not getting closer, and opens doors by standing 40 units off the door line and pressing use until the sector rises.
- Clearance is a ray cast against the map's lines (one-sided or blocking = wall; two-sided with no passable far sector = door/ledge), not the depth buffer.
- Each `act` is ~15–30 ms of engine time; frames are 320×240 JPEG (~25 KB).

## License

MIT — see [LICENSE](LICENSE). Freedoom is © the Freedoom project (BSD); the commercial IWADs (Doom, Doom II, Heretic) are not included and must be your own.