# séance
A p2p haunted-house party game for 1–16 phones and **three real rooms of a
house** — live at **[game4.ardegazu.ro](https://game4.ardegazu.ro)**, repo
published at **[git.ardegazu.ro/game4](https://git.ardegazu.ro/game4)**.
Every time the bell tolls, everyone physically walks to one of three rooms
(the Study, the Parlour, the Cellar) and taps it on their phone. A ghost
sweeps one room — it favours wherever the crowd stood last bell. Everyone
caught loses a candle; three candles and you're out; the last lit candle wins
the séance. Each bell one player is secretly the **medium** and learns a room
the spirit will shun — they can share it out loud, stay silent, or lie.
Installable PWA (works saved to the iOS home screen), safe-area aware.
## Architecture
- **No game server.** Browsers are [js-libp2p](https://libp2p.io) peers: they
dial a shared circuit-relay-v2 peer on `signal.ardegazu.ro` over WSS, find
each other through gossipsub pubsub-peer-discovery, and upgrade to direct
WebRTC (the circuit doubles as signaling and a rate-limited fallback path).
The p2p layer under [`client/src/game4/net/`](client/src/game4/net/) is
vendored from [neon-grid](https://git.ardegazu.ro/game1/) and is
game-agnostic except for the protocol id — the game only adds its own wire
protocol and logic in [`client/src/game4/game/`](client/src/game4/game/).
- **Rooms are cryptographic.** Discovery is app-wide; membership in a room is
enforced by a room-scoped protocol id (`/seance/2/<HKDF(secret)>` — wrong
rooms fail protocol negotiation) plus a mutual sealed hello: one AES-256-GCM
envelope keyed from the room secret, bound to both noise-authenticated peer
ids. Undecryptable means not a member. After that, frames ride libp2p's
noise encryption, end-to-end per pair even across the relay.
- **One peer conducts.** The lowest peer id runs the authoritative bell timers
and the ghost ([`game/game.cljs`](client/src/game4/game/game.cljs)): it collects secret
room picks, resolves each bell, and broadcasts compact phase frames. Séances
are seat-indexed so roster churn can't corrupt one in flight. Host loss voids
the séance and the next-lowest id takes over. Late joiners get a snapshot and
spectate until the next séance. The 16-guest cap is enforced by the conductor
(the old relay's room cap no longer exists on this stack).
- **Where you stand is the honor system.** The game can't see your feet — your
tap is your room. Walking is the point, not the enforcement.
- **Hosting without an origin server.** `client/dist/` is pinned to IPFS,
named by IPNS and served straight from the gateway on the app's own domain
(DNSLink). The published repo works the same way (`deploy/publish-repo.sh`
+ the shared `git.ardegazu.ro` source root).
## Develop
Needs node ≥ 22, a JVM ≥ 17 and the `clojure` CLI (shadow-cljs).
```bash
cd client && npm install && npm run dev # watch build → http://localhost:5173
```
Open two tabs on the same `#room` URL to play against yourself
(keys 1/2/3 pick rooms on desktop). While the shadow server runs, port 4173
serves the last release build from `client/dist`. `npm test` compiles the pure
game/net namespaces and runs the golden vectors (extracted from the TypeScript
implementation while it was canon). Regenerate icons with `npm run icons`.
## Deploy
```bash
cd client && npm run build # → client/dist
# then: ird ipfs add client/dist → ird ipfs ipns publish game4.ardegazu.ro <cid>
deploy/publish-repo.sh # → deploy/.site (landing page + clonable bare mirror)
# then: ../git/assemble.sh game4 (updates git.ardegazu.ro)
```
MIT — see [LICENSE](LICENSE).