game2 / README.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
# Valley Blocks — serene Tetris, now with room-link multiplayer

A Monument Valley-inspired Tetris PWA, running on a serverless
[js-libp2p](https://libp2p.io) mesh. No accounts, no game servers, no database:
the production build is plain static files, and multiplayer rooms live entirely
in the players' browsers. The invite link **is** the room key.

- **Play:** https://game2.ardegazu.ro/
- **Clone:** `git clone https://git.ardegazu.ro/game2.git`

## How multiplayer works

- Open the game — the URL fragment (`#…`) is a fresh room secret. Share the
  link; up to 16 players can walk in (a full mesh, so ~12 is the practical
  ceiling, enforced by the host).
- Browsers are libp2p peers (`client/src/game2/net/`, vendored from
  [neon-grid](https://git.ardegazu.ro/game1/) and game-agnostic except for the
  protocol id): they dial the app's
  circuit-relay-v2 node on `signal.ardegazu.ro` over WSS, discover each other
  through gossipsub, and upgrade to direct WebRTC (the circuit doubles as
  signaling and a rate-limited fallback path). The relay's identity is fetched
  from `/.well-known/ap2p` at boot with a compiled-in fallback, so relay
  rotations never need a client release.
- Rooms are enforced twice: a room-scoped protocol id
  (`/game2/2/<HKDF(secret)[:16]>` — wrong rooms fail protocol negotiation) and
  a mutual sealed hello (AES-256-GCM under per-sender keys derived from the
  room secret, AAD-bound to both noise-authenticated peer ids). After the
  hello, game frames ride one long-lived stream per pair — libp2p's noise
  layer already encrypts every pair end-to-end, even across the relay. TURN
  fallback carries still-encrypted traffic when a direct connection is
  impossible.
- **One peer referees.** Host conflicts (cold boots claiming before discovery
  finishes, suspend/resume split-brain) resolve by a shared precedence rule:
  an active round outranks an idle claim, a joined player outranks a parked
  tab, ties break to lowest peer id. The host heartbeats its roster so
  late-booting peers learn the incumbent instead of electing themselves; on
  host loss the round voids and the lowest surviving id takes over. Rounds are
  seat-indexed so roster churn can't corrupt a live round; late joiners get a
  snapshot and spectate until the next round.
- Tetris boards are latency-sensitive, so each player simulates their own
  board from the host-dealt shared seed (identical pieces for everyone) and
  broadcasts it as their input — compact, throttled, deduped `bs` frames.
  The host referees everything global: roster, seating, modes
  (Race / Race + Survival / Last Standing), win/lose, and scores. The board
  simulation ([`game/core/game.cljs`](client/src/game2/game/core/game.cljs)),
  the wire protocol ([`game/frames.cljs`](client/src/game2/game/frames.cljs))
  and the referee ([`game/party.cljs`](client/src/game2/game/party.cljs)) are
  pure enough to be driven headlessly, and golden vectors extracted from the
  previous TypeScript implementation pin all three byte for byte.

## The game

SRS rotation with wall kicks, 7-bag randomizer, ghost piece, hold, lock delay,
guideline scoring, speed curve, five Monument Valley palettes (light/dark),
synthesized Web Audio sound, and the rare steerable 🐍 snake bonus piece.
Solo play works fully offline (installable PWA, iOS safe-area aware — the p2p
layer failing to boot never blocks the game); the same build hosts multiplayer
rooms.

Controls: ← → / A D move · ↑ X / Z rotate · ↓ S soft drop · Space hard drop ·
C/Shift hold · P pause. Touch: drag / flick / tap gestures plus an optional
one-handed button bar.

## 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
npm test           # golden vectors + catalog parity + the source lint
```

Open two tabs on the same `#room` URL to test multiplayer locally. While the
shadow server runs, port 4173 serves the last release build from `client/dist`
(the relay's origin allowlist covers 4173/5173 only). `window.__app` and
`window.__party` expose the app and the net brain in the console.

## Build & deploy

```bash
cd client && npm run build   # → client/dist/, plain static files (~416 KB gz with libp2p)
```

Deployed by pinning `client/dist/` to IPFS and publishing its CID to the
site's IPNS name (`ird ipfs add` + `ird ipfs ipns publish game2.ardegazu.ro`);
the domain is an IPFS gateway domain (DNSLink) — no origin server at all.
The repo itself is published the same way: `deploy/publish-repo.sh` builds a
dumb-HTTP-clonable bare mirror (loose objects — IPFS-gateway-friendly) plus a
landing page into `deploy/.site/`, assembled into the shared source root by
`../git/assemble.sh game2` and served at
https://git.ardegazu.ro/game2/ (clone: https://git.ardegazu.ro/game2.git).

## Layout

```
client/
  deps.edn shadow-cljs.edn      the ClojureScript build (release only; the
                                committed dist is byte-deterministic)
  public/                       the static shell: index.html, style.css, icons,
                                webmanifest
  scripts/                      dev / build wrappers + the gensym normalizer
  src/game2/net/                libp2p layer (bootstrap, node, peers, room
                                crypto, TURN) — kept byte-identical to
                                neon-grid's so fixes can be re-copied; the only
                                game-specific line is the protocol id in
                                peers.cljs
  src/game2/id/boot.cljs        the suite identity bridge (vendored, verbatim)
  src/game2/i18n/               runtime.cljs (vendored) + en/ro/hu catalogs
  src/game2/game/               the game: core/ render/ input/ ui/ audio/
                                storage/ + frames.cljs (wire) and party.cljs
                                (the net brain / referee)
  src/game2/main.cljs           wiring: room secret → create-net → party → app
  src/game2/social.cljs         the lazy social chunk (friends, invites,
                                leaderboards, cross-device sync)
  test/                         node --test suites + the golden vectors
site/                           landing page for the published repo
deploy/                         publish-repo.sh (mirror builder with anonymity
                                gate)
```

MIT — see LICENSE.

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