peer-kit / 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
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
# ardegazu-peer-kit

A headless Node peer for the [ardegazu.ro](https://ardegazu.ro) suite. Any
Node ≥ 22 process can join the suite's game rooms as a first-class peer —
sealed-hello room membership, the suite identity in the handshake, typed
clients for all six games, co-signed leaderboard receipts, and the social
layer (friends, presence, invites) without a browser.

- **`core/`** — the games' shared libp2p transport, ported for Node: relay
  bootstrap, sealed-hello membership, varint-framed JSON, reconnection. The
  protocol prefix is a constructor argument (in the apps it is the one line
  that differs per game).
- **`games/`** — `GameRoom` (roster + host tracking with the apps' bystander
  election rules + leaderboard co-signing) and a typed client per game:
  séance, neon-grid, tessera, lampion, odeon, valley-blocks. By default
  clients never claim hostship; séance and neon-grid can opt in (below).
- **`games/host/`** — the ported host simulations for séance and neon-grid:
  pure, step()-able, seedable (provenance headers name the exact browser
  source they port), a live driver that claims an empty room, auto-starts
  matches, runs the sim on real timers and mints co-signed receipts — so a
  bot-only room actually plays.
- **`gym/`** — the same sims wrapped for offline RL: synchronous
  `reset()/step()` with per-seat views field-identical to the live clients'
  views, thousands of episodes a minute, no networking. A policy trained in
  the gym plugs into the live client hooks unchanged.
- **`social/`** — the social agent riding a bare suite node: a friend link,
  auto-accepted friend requests, presence beacons, and live invites (the
  natural way to summon a bot into a room). A peer may also advertise the
  brain/model versions it is playing (`brains`, since 1.3.0 — a thunk re-read
  at every beacon), surfaced as `brains` on friends' presence entries. Since
  2.0.0 `Social.start` also forwards social-kit's `profileSync` and `appState`
  cross-device hooks.
- **`node/`** — the Node shims: an Origin-bearing WebSocket/fetch (the managed
  relay admits browsers by Origin), file-backed storage for the small
  persisted sets, and the seed-file suite identity (no iframe bridge — the
  43-char seed IS the identity).

Since 2.0.0 the kit is **ClojureScript** (the suite canon: `dev/docs/CLJS.md`),
built by shadow-cljs into a committed, byte-deterministic ESM `dist/` with
hand-authored `types/*.d.ts`. TypeScript and JavaScript consumers see exactly
the surface v1.3.0 had, name for name — including the historical deep paths
(`ardegazu-peer-kit/games/host/rng`, …), which the build regenerates as thin
re-export shims. CLJS consumers can instead put `node_modules/ardegazu-peer-kit/src`
on their classpath.

The `src/vendor/` directory is **gone**. Until 2.0.0 peer-kit carried
hand-maintained TypeScript translations of the id-kit core and social-kit's
protocol modules; it now depends on
[ardegazu-id-kit](https://git.ardegazu.ro/id-kit/) and
[ardegazu-social-kit](https://git.ardegazu.ro/social-kit/) as ordinary
sha-pinned npm deps. Three things the translation had drifted away from come
back with the switch: the `fsync` self-origin guard (inbox room ids are
publicly derivable, so a stranger could otherwise seal a validly signed
self-sync into a peer's inbox and rewrite its friend list), the self-sync
change gate, and the `profileSync` / `appState` hooks.

## Consuming

```jsonc
// package.json
"dependencies": { "ardegazu-peer-kit": "git+https://git.ardegazu.ro/peer-kit.git#<sha>" }
```

The package ships both the ClojureScript source (`src/ardegazu/peer/`, for
CLJS classpaths) and committed compiled output (`dist/`, what the `exports`
map serves, with hand-authored `types/`) — plain `node` works with no build
step.

```ts
import { loadIdentity, SeanceClient, Social } from "ardegazu-peer-kit";

const suite = await loadIdentity({ seedFile: "./seed", name: "my-bot" });

// join a séance room (the invite-link fragment is the secret)
const game = await SeanceClient.join({
  app: "seance",
  roomSecret: "<43-char room secret>",
  nick: "my-bot",
  suite,
});

// be a friend: print the link, answer requests, follow invites
const social = await Social.start({ suite });
console.log(await social.friendLink());
social.on("invite", (inv) => console.log(`invited to ${inv.app}: ${inv.url}`));
```

### Hosting + self-play

A designated bot claims hostship only when it boots into an **empty** room —
mint a fresh secret, start the host first, invite the rest; bots invited into
rooms with browsers never claim. Split-brain claims settle by the exact
per-game browser rules (séance: lowest id; neon-grid: live-beats-idle).

```ts
import { SeanceClient, SeanceGym, newRoomSecret } from "ardegazu-peer-kit";

const host = await SeanceClient.join({
  app: "seance", roomSecret: newRoomSecret(), nick: "gazda", suite,
  host: { match: { minPlayers: 2, expectedPlayers: 3, maxMatches: 1 } },
});
host.on("matchEnd", (w) => console.log("winner", w));
host.on("receipt", (rec) => console.log("co-signed", rec.sig.length));

// train offline, then hand the policy to a live client via onTurn/onTick
const gym = new SeanceGym({ players: 3, seed: 7 });
let views = gym.reset();
const { rewards, done } = gym.step(views.map((v) => (v.isMedium ? v.hintNot : null)));
```

Steering hooks: séance `onTurn(view) → 0..2 | null` (plus `actDelayMs`),
neon-grid `onTick(view) → 0..3 | null` with `view()` exposing the per-seat
trail map and heads (`autopilot: false` implied when `onTick` is given).
The gym wrappers (`SeanceGym`, `NeonGridGym`, `ValleyBlocksGym`) run the same
sims synchronously for offline training, and their views are field-identical to
the live clients' — asserted, field names and order both.

Direct WebRTC upgrades use the platform prebuilds that ship inside
`node-datachannel`; with `ignore-scripts` set, everything still works —
game rooms run fine over the relay circuit alone.

Note when combining with `ardegazu-rooms-kit` in one process: the two kits'
WebRTC transports load two different builds of the same native library, which
cannot coexist — run rooms-kit clients in a separate process (the suite's bot
does exactly that).

## Layout

```
deps.edn        classpath: src/ + social-kit's CLJS sources from node_modules
shadow-cljs.edn one build, three modules (:index, :node, :internals)
src/ardegazu/peer/   the implementation (ClojureScript; also shipped)
dist/           committed deterministic ESM — what plain Node imports
types/          hand-authored .d.ts, one per subpath export
test/           node:test suites over the committed dist (npm test)
test/vectors/   golden fixtures extracted from the TypeScript original
scripts/        normalize-gensyms.mjs · gen-subpaths.mjs (the dist file tree)
deploy/         publish-repo.sh + check-dist.sh for the source mirror
site/           landing page for git.ardegazu.ro/peer-kit/
```

## Develop

Needs Node ≥ 22 plus a JVM ≥ 17 and the `clojure` CLI (the dev machine only —
`dist/` ships via git).

```sh
npm install
npm run check     # shadow-cljs compile + tsc over the .d.ts consumer smoke
npm run build     # dist/ (committed; cold builds are byte-identical)
npm test          # the vector suites and the black-box tests, against dist/
```

Public mirror (git dumb-HTTP — no git server):

```sh
git clone https://git.ardegazu.ro/peer-kit.git
```

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/peer-kit.git