# Adding your game to OVERSCAN

**This document is for an AI session working on a game that will live on
OVERSCAN.** If you are a human contributor from outside, you want
[the contributor kit](./submit.html) instead — same platform, friendlier tone,
and it does not assume you have a shell on this machine.

Canonical copy: `https://74-207-234-62.ip.linodeusercontent.com/docs/adding-your-game.md`

Fetch it rather than trusting a copy in your working directory. If the two
disagree, this one is right.

---

## 1. Who does what

There is one arcade and several games, and they are worked on by different
sessions in different directories. The split is deliberate.

| | Owns | Working directory |
|---|---|---|
| **The arcade session** | OVERSCAN (the portal, the API, the database, deployment) and RUSTLANDS | `~/projects/battlezone` |
| **A game session** | One game, and nothing else | `~/projects/<your-game>` |

**You own your game's code. You do not own the platform.** Concretely:

- **Do not edit anything outside your directory.** The portal, the API, the
  nginx config and the deploy script belong to the arcade session. If you need
  a change there, write it down and say so — do not reach over and make it.
- **Do not deploy.** See §6. Deployment swaps a live symlink and restarts
  services; one hand on that lever.
- **Do not add a backend.** No Firebase, no Supabase, no service of your own.
  Everything you need is in §5, and if it genuinely is not, say so.
- **Do not create infrastructure.** No databases, no domains, no repos under
  the wrong GitHub account. Ask.

What you *do* own is everything about how your game plays, looks and sounds.
Nobody is going to second-guess that.

---

## 2. Where your game runs

Your game is served as **static files from a subdirectory**: `/<your-slug>/`.

Three consequences, and the first one causes most of the broken deploys:

- **Every path must be relative.** `./js/thing.js`, never `/js/thing.js`. An
  absolute path resolves against the arcade root and 404s. It will work fine
  when you test at the root of a local server, then break the moment it is
  deployed. The single exception is `/arcade.js` (§5), which is absolute on
  purpose.
- **A build step is fine, but the output has to be self-contained.** What ends
  up in your published directory is exactly what gets served. There is no
  bundler, transpiler or SSR at the edge.
- **You have no server.** nginx serves files. Anything dynamic goes through the
  arcade API.

---

## 3. What has to be in your directory

```
your-game/
├── index.html      <- entry point. Required.
├── version.json    <- build stamp. Required, regenerated every release.
├── game.json       <- cabinet manifest. Required.
├── OVERSCAN.md     <- your copy of the platform contract. Required.
└── ...             <- everything else is yours
```

Plus, in whatever file your session actually reads on startup — `CLAUDE.md`,
`AGENTS.md`, or both — **a pointer to `OVERSCAN.md` in the first few lines.**
This is the part that makes any of the rest discoverable. A future session
starting cold in your directory will read its own instructions file and nothing
else; if the platform contract is not referenced there, it will not be found,
and you will get a game that hardcodes absolute paths and reinvents a login
screen.

---

## 4. The two files the platform reads

### version.json

Regenerated **every release**. The portal reads it to print a build stamp on
your cabinet, so a stale file tells visitors your game has been abandoned.

```json
{
  "version":  "1.05.000",
  "released": "2026-08-16T23:47:07Z",
  "commit":   "1675243",
  "release":  "auto-boss-round-and-exit"
}
```

| Field | |
|---|---|
| `version` | `X.YY.ZZZ`, zero-padded. Required. |
| `released` | ISO-8601 UTC. Must parse — an unparseable date renders as nothing rather than as a wrong date, so you lose the stamp silently. |
| `commit` | Short git hash. Optional, and useful when somebody reports a bug against a build. |
| `release` | Free text. Ignored by the portal; use it for your own notes. |

Keep the number itself in a `VERSION` file with nothing else in it, and
generate `version.json` from that, so the version exists in one place rather
than in two that eventually disagree:

```bash
printf '{\n  "version": "%s",\n  "released": "%s",\n  "commit": "%s"\n}\n' \
  "$(tr -d '[:space:]' < VERSION)" \
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  "$(git rev-parse --short HEAD)" > version.json
```

**The padding is not a style preference.** `X.YY.ZZZ` sorts lexically the same
way it sorts semantically. Unpadded, `1.13.0` sorts before `1.2.0` — in `sort`,
in a directory listing, in an `ORDER BY`, and in every throwaway script anybody
writes later. Each of those then needs a version-aware comparator to work
around a problem that three zeroes prevent. Two digits for minor, three for
patch.

Bump **patch** for a fix or a tweak, **minor** for a feature or a real
behaviour change, **major** for a rewrite or anything that invalidates saved
data.

### game.json

The cabinet manifest — what your game is called and how its card reads.

```json
{
  "id": "dream-catcher",
  "title": "DREAM CATCHER",
  "kind": "Arcade · Single player",
  "blurb": "Catch the good stuff. Dodge the rest. It gets faster.",
  "accent": "#e0784d",
  "author": "Chip Futch",
  "accounts": true,
  "leaderboard": { "enabled": true, "perSeed": false, "scoreLabel": "POINTS" },
  "save": { "enabled": false }
}
```

`id` is your slug: lowercase, hyphens, and it becomes both your URL and your
key in the database. **Pick it once.** Changing it later orphans every save and
every score, because those key on it.

`accent` is your cabinet's colour. The portal deliberately has no colour of its
own — every hue on that page belongs to a game.

**Honest note on how this is consumed today:** the floor is currently written
out in the portal's HTML, and the arcade session transcribes your manifest into
it when wiring up your cabinet. So `game.json` is read by a person, not by the
page, and changing it does not change the site until somebody says so. Making
the floor read manifests directly is on the arcade roadmap; write the file as
if it were already automatic, because one day it will be.

---

## 5. The hook: accounts, saves, leaderboards

One file, served by the arcade at `/arcade.js`. No dependencies.

**Do not vendor a copy of it.** A local copy keeps calling an endpoint that has
moved on, and fails quietly months later.

```js
import { Arcade } from '/arcade.js';
const arcade = new Arcade('your-slug');

const me = await arcade.me();                    // null if nobody is signed in
await arcade.submitScore(9100, { shortName: 'CAS' });
const board = await arcade.leaderboard({ limit: 10 });
await arcade.save({ v: 1, ... }, revision);
const { document, revision } = await arcade.load();
```

Rules that matter, most of which exist because breaking them fails silently:

- **Nothing throws.** Every method resolves. A dead arcade must never stop the
  game, so check return values instead of wrapping calls in try/catch.
- **Signing in is optional, and stays optional.** Anonymous players get the
  full game with local scores. Signing in is only what makes a score leave the
  browser. **Never gate play behind a login.**
- **Do not build a login screen.** Sign-in happens at the arcade's front door.
  Somebody who signed in on the portal is already signed in when they reach
  you — `arcade.me()` simply returns them.
- **There is no token to handle.** Auth is an HTTP-only cookie owned by the
  platform. You will never see a credential, and nothing belongs in
  `localStorage` except your own game data.
- **Names are per-game.** The same person can be "Rusty Joe" in one cabinet and
  "Annihilator 1969" in another, on one login. Whatever name your game collects
  becomes their alias *for your game*.
- **Saves are opaque.** The server never looks inside the document, so the
  shape is entirely yours. Put a version field in it and migrate on load.
- **Scores are append-only.** Submit one score; the board is a server-side
  query. Never write the whole board — two players finishing at once would
  silently lose one.
- **Never publish placeholder scores.** Fake names on your own attract screen
  are fine. Seeding them to the shared board buries the first real player under
  ten people who do not exist.

---

## 6. Releasing

**You do not deploy. You make a release deployable, and the arcade session
picks it up.**

Not bureaucracy — the deploy script rebuilds, swaps a live symlink, restarts
services and prunes old releases across every component. It needs one owner.

Your side of it, in order:

1. Make the change and make sure it actually runs.
2. Bump `VERSION`.
3. Regenerate `version.json`.
4. Commit, with a message that says what changed and why.

That is the whole handshake. **There is no note to send** — the arcade session
can see a pending release directly, by comparing your local `VERSION` against
what is live:

```bash
scripts/deploy.sh status
```

A version newer than the live one *is* the request. This is deliberately not a
message to a human, because messages get lost and version numbers do not.

If something about your release is unusual — it needs a config change, it
breaks saves, it must go out with another game — put that in the commit
message, where it is attached to the thing it describes.

### Git

If your directory has a remote, push it; that is your offsite backup. If it
does not, commit locally anyway and say so. **Do not create a GitHub repo**
— the `gh` CLI on this machine may be signed in to the wrong account, and a
private repo in the wrong org is annoying to unpick.

---

## 7. House rules

Every cabinet agrees to these. Breaking one gets the game pulled.

1. **Nothing that picks a pocket.** No ads, no battle pass, no lootboxes, no
   timers you can pay to skip.
2. **Nothing phoning home.** No analytics, no ad pixels, no third-party
   trackers, no harvesting location or contacts. Submissions get opened with
   the network tab up. The rule is against doing it *quietly* — if your game
   genuinely needs to call something external, say so up front.
3. **You made it.**
4. **It is still breathing.** The build stamp on your cabinet is public, which
   is rather the point of it.
5. **Have a mouth on it.** Attitude is a feature here.

---

## 8. Testing before you hand it over

```bash
python3 -m http.server 8080 --bind 0.0.0.0    # then http://localhost:8080/
```

**Zero console errors is the standard**, and a 404 for an asset counts. Two
things that are expected rather than broken:

- `arcade.me()` returning null locally — nothing is proxying `/api` to the
  arcade, so there is no session to find.
- A 401 from `/api/me` while signed out. That is the platform correctly saying
  nobody is there.

The failure this catches that nothing else will: serving your game from the
root of a local server hides every absolute-path bug in it. If you can, test
from a subdirectory that matches your real slug.
