game1 / docs / NEW-GAME-PROMPT.md
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# PROMPT: build & publish a serverless p2p game on ardegazu.ro

> **CLJS-canon note (2026-08):** neon-grid (this repo) is now the suite's
> **ClojureScript app canon** — `client/src/game1/net/`, `id/boot.cljs` and
> `i18n/runtime.cljs` are the CLJS-line vendored canon (shadow-cljs +
> deps.edn per `dev/docs/CLJS.md`; wire, crypto and referee semantics are
> unchanged and golden-vector tested against the TS originals in
> `client/test/`). **The TS line is retired for games**: valley-blocks
> (game2) was the last TypeScript game and is now CLJS too, so no
> `client/src/net/` or `id/boot.ts` survives anywhere in the suite. A new
> game copies the CLJS namespaces from here — there is no TS variant to
> copy. Every contract below (protocol prefix rule, sealed hello, authority
> model, i18n, PWA bar) still holds verbatim.
>
> One contract worth restating, because game2's port nearly tripped on it:
> **the protocol prefix is whatever the deployed build already speaks, not
> the codename.** valley-blocks' is `/game2/2/`, its repo name — the only
> game in the suite whose prefix is not its codename. Read the retiring
> implementation (and cross-check `peer-kit`'s app table) before writing
> that line; guessing it wrong kills interop silently, as a protocol
> negotiation failure that just drops peers as "wrong room".

You are an agentic developer with access to the **ird MCP tools** (IPFS pins,
IPNS names, gateway domains, DNS). Build a multiplayer browser game exactly in the house
style described below, then deploy it and publish its repo. Everything you need
is public; follow the flow in order. Verify each stage before moving on.

---

## 1. GAME SPEC (filled in by the requester)

```
GAME NAME:        <e.g. "hex-clash">
SUBDOMAIN:        <name>.ardegazu.ro        (game at <name>., source at git.ardegazu.ro/<name>)
PLAYERS:          <min–max; no relay-side cap — enforce your max in game code>
GAME DESCRIPTION: <what the game is, win condition, round/match structure>
RULES:            <mechanics, tick rate if real-time, board/arena shape>
CONTROLS:         <keyboard mapping + a touch scheme that works one-handed>
AESTHETIC:        <palette, mood, effects>
EXTRAS:           <anything else: sounds, spectators, chat, ...>
```

Everything below is fixed house style — do not ask the requester about it.

## 2. House architecture (non-negotiable)

- **Static client only. No servers, no accounts, no database.** A shadow-cljs
  ClojureScript app (`shadow-cljs.edn` + `deps.edn`, per `dev/docs/CLJS.md`;
  build = `node client/scripts/build.mjs`) whose production build is plain
  static files.
- **The p2p layer is js-libp2p (v3) on the shared app relay.** Copy
  `client/src/game1/net/` from the reference repo — it is small (~700 lines)
  and almost entirely game-agnostic:

  ```bash
  git clone https://git.ardegazu.ro/game1.git neon-grid   # a complete game built this way
  cp -R neon-grid/client/src/game1/net <yourgame>/client/src/<yourgame>/net
  # then rename the namespaces: game1. → <yourgame>. (the only edit)
  ```

  Keep `bootstrap.cljs` / `room_crypto.cljs` / `turn.cljs` / `node.cljs`
  unchanged (modulo the namespace rename); in
  `peers.cljs` change only the protocol prefix (`/<yourgame>/2/…`). Copy the
  `dependencies` block from neon-grid's `client/package.json` (libp2p +
  websockets + webrtc + circuit-relay-v2 + noise + yamux + gossipsub +
  pubsub-peer-discovery + identify + utils + multiaddr, plus the pinned
  `ardegazu-id-kit` git dependency). Treat **neon-grid as
  the working example** of everything in this prompt: project layout, game/net
  boundary, deploy scripts. When in doubt, do what neon-grid does.
- **Relay & TURN (shared, already running):** the p2p app's libp2p
  circuit-relay-v2 peer on `signal.ardegazu.ro`.
  - Fetch `https://signal.ardegazu.ro/.well-known/ap2p` at boot for the current
    `relay.peer_id` + `relay.multiaddrs` (compiled-in fallback in `config.cljs`;
    see `net/bootstrap.cljs`).
  - Browsers listen on `<relay>/p2p-circuit`, announce `/webrtc`, discover each
    other via gossipsub pubsub-peer-discovery, and upgrade to direct WebRTC;
    the circuit is signaling + bandwidth-limited fallback (256 KiB/s).
  - TURN creds: `https://signal.ardegazu.ro/turn-credentials`, fed to the
    webRTC transport per connection attempt (`net/turn.cljs`).
  - `APP_SALT = "<name>.ardegazu.ro/v2"` — this namespaces your rooms; never
    reuse another app's salt.
  - The relay's origin allowlist already covers `*.ardegazu.ro`,
    `http://localhost:4173` and `http://localhost:5173`. Dev server MUST run on
    port 4173 or 5173 (the `:dev-http {5173 … 4173 …}` entries in
    `client/shadow-cljs.edn`, served via `client/scripts/dev.mjs`) or the
    relay will refuse you.
- **Room = URL fragment.** A `#<base64url secret>` (from `newRoomSecret()`);
  it never leaves the browser. Discovery is app-wide, so rooms are enforced by
  a room-scoped protocol id (cheap pre-filter) plus a **mutual sealed hello**:
  each pair proves knowledge of the secret with an AES-GCM envelope bound to
  the room id and both noise-authenticated peer ids (`net/room_crypto.cljs`);
  an undecryptable hello means "not a member" and the peer is dropped. New
  visitors with no/invalid hash get a fresh secret written into the URL. The
  invite link IS the room key.
- **Every frame is E2E-encrypted** per pair by libp2p's noise layer — even
  across the relay circuit — and the sender on every frame is its authenticated
  peer id. Define your game's wire frames as one tagged, versioned codec
  namespace (see neon-grid `client/src/game1/game/frames.cljs`, golden-tested
  in `client/test/frames.test.mjs`) and validate incoming payloads.
- **Authority model — one peer referees.** Copy neon-grid's proven logic
  (`client/src/game1/game/game.cljs`) rather than reinventing:
  - first peer in an empty room claims host; on host loss the lowest
    open-channel id takes over and voids the round;
  - suspend/resume split-brain (two self-proclaimed hosts) resolves by
    "lowest id wins" on conflicting roster claims;
  - host runs the fixed-rate simulation and broadcasts compact per-tick
    deltas; clients render from them (interpolate between ticks);
  - rounds are **seat-indexed** (roster churn must not corrupt a live round);
  - late joiners get a snapshot (RLE for grids) and spectate to the next round;
  - non-host players send only their own inputs to the host.
- **PWA + iOS home-screen quality bar:** manifest + workbox-build service
  worker generated by `client/scripts/build.mjs`,
  `viewport-fit=cover` with `env(safe-area-inset-*)` padding on every fixed
  element, `black-translucent` status bar, no pinch/double-tap zoom or
  rubber-banding, touch controls, audio unlocked on first user gesture.
  The lib already handles socket revival on iOS wake.
- Keep payload frames well under 64 KiB (the net layer's frame cap) and design
  for a full mesh (practical ceiling ~12 simultaneous riders).
- **Suite identity (id-kit).** Every app shares one Ed25519 identity via the
  hidden same-site iframe bridge at `https://ardegazu.ro/id/`; the shared code
  is the `ardegazu-id-kit` package (`git+https://git.ardegazu.ro/id-kit.git#<tag>`,
  npm pins the commit). **It is consumed as ClojureScript SOURCE, not as its
  dist:** put `node_modules/ardegazu-id-kit/src` on `client/deps.edn`'s
  `:paths` and require the DEFINING namespaces
  (`ardegazu.id.{bridge-client,identity,profile,xkey}`), never
  `ardegazu.id.index` — that is a re-export shim for the ESM surface. The npm
  sha pin stays the dependency mechanism; the path only changes how the pinned
  tree is read. Copy `client/src/game1/id/boot.cljs` from
  neon-grid verbatim (modulo the namespace rename) and call
  `bootIdentity(APP_SALT)` before `createNet`, passing
  `identity: ident.netIdentity(APP_SALT)`. That makes the sealed hello carry an
  optional `id` block `{pub, sig, name, hue, glyph}` — old clients validate
  only `v === 2` and ignore it; membership proof never depends on it; verified
  identities surface via `identityOf(peerId)`. Prefill the nick input with
  `ident.boot.profile.name` (game-local nick key as fallback) and push a typed
  nick back with `bridge.putProfile({ name })`. Games auto-adopt on identity
  conflicts (nothing in a game is bound to an old key). `room_crypto.cljs` is
  therefore "unchanged except the versioned hello payload".
- **The social layer (social-kit).** Depend on `ardegazu-social-kit`
  (`git+https://git.ardegazu.ro/social-kit.git#<tag>`, plus `@noble/curves`).
  Source-consumed exactly like id-kit — `node_modules/ardegazu-social-kit/src`
  on `:paths`, and both kits must convert together, because social-kit's dist
  compiles id-kit in: one dist import alongside one source path means two
  id-kit copies in the bundle. Require `ardegazu.social.join` for the
  first-paint chips (it pulls only `ardegazu.social.text`),
  `ardegazu.social.{gamelb,app-boot}` from the lazy module, and
  `ardegazu.social.net` never — that is the kit's only libp2p-bearing
  namespace and a game builds its own node. Then copy neon-grid's lazy social
  block
  in `main.cljs`: `attachSocial({...})` rides your node's pubsub
  (`net.libp2p.services.pubsub`) so friends on ardegazu.ro see "in
  <yourgame>", get live invite toasts, and a per-session "friends can join"
  toggle can share the room link. For leaderboards, construct `GameLb`
  (route its `lbq`/`lbs`/`lbr` frames FIRST in `onMessage` — old clients
  ignore them) and call `lb.hostMatchEnd([[peerId, nick, score], …])` at your
  host-decided match end (see neon-grid's `setMatchEndHook`). Receipts count
  only with ≥2 identity-ful co-signers; a scoreless/cooperative game simply
  never calls `hostMatchEnd`. `net/index.cljs` exposes `libp2p` + `roomId` for
  exactly this wiring.
- **Languages (i18n).** Every app ships en/ro/hu — see `docs/I18N.md`
  (canonical). Copy `client/src/game1/i18n/runtime.cljs` from neon-grid
  verbatim (byte-identical rule, like `net/`, modulo the namespace rename);
  write your game's `en.cljs` (source of
  truth) + `ro.cljs`/`hu.cljs` catalogs and a thin `i18n.cljs` binder.
  English stays
  baked into `index.html` behind `data-i18n` tags; all code-side text goes
  through `t()`/`tn()` (plurals NEVER hand-concatenated). Put
  `langPickerEl()` in the join panel, `absorbSuiteLang(ident.boot.profile.lang)`
  after identity boot, and pass `t` into `joinPasteChip`/`attachSocial`.
  Definition of done: all three languages render, plural sites are right at
  n = 1 / 2 / 20 in Romanian, nothing overflows the HUD.

## 3. Project layout

```
<yourgame>/
  README.md  LICENSE(MIT)  .gitignore(node_modules/ client/dist/ deploy/.site/)
  client/
    package.json  shadow-cljs.edn  deps.edn  public/index.html
    public/icons/           # 192/512 + apple-touch-icon PNGs (generate them)
    scripts/                # dev.mjs + build.mjs from neon-grid (shadow-cljs + workbox)
    src/<name>/net/         # libp2p layer from neon-grid — ns rename + the protocol prefix
    src/<name>/id/          # boot.cljs from neon-grid, verbatim (modulo ns)
    src/<name>/i18n/        # runtime.cljs from neon-grid VERBATIM (byte-identical
                            # modulo ns, like net/) + en/ro/hu catalogs (docs/I18N.md)
    src/<name>/i18n.cljs    # the thin per-game binder
    src/<name>/game/        # your game code (frames/state/sim/render/ui)
    src/<name>/main.cljs    # wiring: createNet(secret) ↔ game callbacks
  site/index.html           # landing page for the published repo (see §6)
  deploy/publish-repo.sh    # adapted from neon-grid (see §6)
  (local launch config)     # dev server on port 4173/5173 — gitignored, never tracked
```

## 4. Anonymity — set up BEFORE the first commit

The repo will be published. It must never contain the account owner's identity.

```bash
git init
git config user.name  <yourgame>
git config user.email <yourgame>@noreply.local
```

All commits stay `<yourgame> <yourgame@noreply.local>`. No real names, emails,
usernames or machine hostnames anywhere in tracked files (watch out for SSH key
comments and `$HOME` paths in scripts). The publish script's gate (§6) enforces
this — if it aborts, fix history before publishing, e.g. with a filter-branch
identity rewrite.

## 5. Build, test, deploy the game (ird MCP)

1. Develop with two browser tabs on the same `#room` URL (`npm run dev`, port
   4173). A round must fully work locally: lobby roster on both tabs, host
   badge, countdown, gameplay, win/lose, scores, host-leave recovery.
2. `npm run build` → `client/dist/`.
3. Create the site once, when you're ready to deploy — an IPNS name served on
   its own gateway domain (no tunnel; storage bills per MB-hour, traffic per MB):
   - `ipns_create(hostname="<name>.ardegazu.ro", serve=true)` — creates the
     IPNS name, attaches the gateway domain, and writes the DNS records
     (A + `_dnslink` TXT) automatically. Cert issuance takes ~1 min.
4. Publish: `ird ipfs add client/dist --json --yes` → CID, then
   `ird ipfs ipns publish <name>.ardegazu.ro <cid>` (equivalently the MCP
   `ipfs_add` + `ipns_publish`). Propagates within a minute.
5. Verify live: poll until `https://<name>.ardegazu.ro/` returns 200, then play
   a real round in two tabs ON THE LIVE URL.
6. Redeploys: rebuild, `ird ipfs add` the new dist, `ipns publish` the new CID,
   then `ird ipfs pin rm` the previous CID so its storage billing stops
   (IPFS dedupes unchanged files, so incremental cost is only what changed).

## 6. Publish the repo (the "this page is also the git repo" pattern)

The repo is served as static files over git's **dumb-HTTP protocol** from the
shared source host **git.ardegazu.ro** — one gateway domain for the whole
ecosystem, one path per app: `https://git.ardegazu.ro/<name>.git` (clone) and
`https://git.ardegazu.ro/<name>/` (landing page). No new DNS entries per game.

1. Write `site/index.html` — a landing page in the game's aesthetic: what it
   is, how it works, `git clone https://git.ardegazu.ro/<name>.git`,
   a link to the live game, honest guarantees/limits. Model it on
   `https://git.ardegazu.ro/game1/`.
2. Copy `deploy/publish-repo.sh` AND `deploy/gen-browse.py` from neon-grid
   and set `APP=<name>` / `IDENT=<yourgame>` at the top of the former. It:
   - generates the in-browser source browser (`/browse/`, linked from the
     landing page),
   - clones `--no-local --bare` into `deploy/.site/<yourgame>.git` (transport
     clone so rewritten/unreachable history can't tag along),
   - explodes packs into **loose objects** (the IPFS gateway 200-fallbacks
     missing paths, which breaks git's pack probing — all-loose avoids it),
   - runs `git update-server-info`, strips `hooks/` and `config`,
   - **anonymity gate**: aborts unless every author/committer is
     `<yourgame> <yourgame@noreply.local>`, no nested site build is committed,
     and no identity strings appear in any decompressed object (keep the
     grep pattern assembled from string pieces so the script never matches
     itself).
3. Register the app in `../git/assemble.sh` (add a `repo_for`
   case and the default app list) and add its card to
   `../git/index.html`, then run `./assemble.sh <name>` — it
   runs the repo's publish script, lays `deploy/.site` into the shared root,
   pins the root and re-points the `git.ardegazu.ro` IPNS name at it.
4. Verify: `git clone https://git.ardegazu.ro/<name>.git` into a temp dir;
   check `git log` shows only the anonymous identity and the tree is complete.
5. After every commit that should go public: `./assemble.sh <name>`.

## 7. Definition of done

- [ ] Two-tab round played end-to-end on `https://<name>.ardegazu.ro` (not just localhost)
- [ ] Late joiner mid-round spectates, then plays the next round
- [ ] Host tab killed mid-round → survivor becomes host, next round works
- [ ] Mobile viewport: safe areas respected, touch steering works, PWA installable
- [ ] `git clone https://git.ardegazu.ro/<name>.git` succeeds; log is 100% anonymous
- [ ] README documents dev/build/deploy; repo tree has no `node_modules`, `dist`, or `.site`
- [ ] Report the two URLs and total pinned size (no tunnels — hosting is IPNS + gateway domains)

## 8. Cost summary to include in your report

No tunnels. IPFS storage (hourly per MB — typically <1 MB
total), TURN relay traffic per MB only when peers can't connect directly.
Signaling is free. The shared relay app (`signal.ardegazu.ro`) already exists —
do NOT create a new p2p app.

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game1.git