@bountyboard/arcade-sdk 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @bountyboard/arcade-sdk
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - publish workflow and docs cleanup
8
+
9
+ ## 1.3.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 7273667: Stop prompting flexible-orientation Arcade games to rotate on mobile.
14
+
3
15
  ## 1.3.0
4
16
 
5
17
  ### Minor Changes
@@ -64,7 +76,7 @@
64
76
  - 145d951: Document the built-in relay room tier: every multiplayer-approved game gets
65
77
  hosted casual lobbies (public quick match, invite codes, host succession,
66
78
  rate-guarded public message fan-out) with zero server code, via the existing
67
- `joinRoom()` API. New `docs/relay-rooms.md` covers the wire contract — the
79
+ `joinRoom()` API. New `docs/relay-rooms.md` covers the wire contract: the
68
80
  `joinData.roomSize` founding rule (2–64, default 8), `relay`/`relay_host`
69
81
  events, the 1 KiB / 15 msg/s / 120 msg/room/s guardrails, and the
70
82
  client-trusted outcome model (relay rooms never emit `end` or report results).
package/README.md CHANGED
@@ -144,8 +144,8 @@ Hosted uploads run in an opaque-origin sandbox without `localStorage`; SDK
144
144
  cloud save is primary there. URL embeds and standalone builds need their own
145
145
  same-origin storage. Oversized blobs reject `too_large` immediately (a
146
146
  client-side precheck against the same 1 MiB cap the server enforces), and a
147
- host that never answers rejects `error` after a 15-second request timeout —
148
- the same timeout covers load, variant, and multiplayer-ticket requests. Every
147
+ host that never answers rejects `error` after a 15-second request timeout.
148
+ The same timeout covers load, variant, and multiplayer ticket requests. Every
149
149
  save/load rejection is a real `Error` with a typed `code`:
150
150
 
151
151
  ```text
@@ -156,7 +156,7 @@ unsupported | unauthenticated | too_large | rejected | error
156
156
 
157
157
  Prefer `save()`/`load()` when you control the source. The shim is for engine
158
158
  runtimes whose storage layer can't be rewired without patching engine
159
- internals — GameMaker HTML5 (`ini_open`, `ini_write_*`, and `game_save` all sit
159
+ internals: GameMaker HTML5 (`ini_open`, `ini_write_*`, and `game_save` all sit
160
160
  on `localStorage`), Godot, Unity, Construct. It replaces the throwing
161
161
  `localStorage` with a Storage-shaped object backed by your cloud save, so those
162
162
  builds persist per player and across devices with no engine changes.
@@ -181,7 +181,7 @@ startGame();
181
181
 
182
182
  Writes made before hydration are kept and merged with the cloud read (your
183
183
  write wins; a `removeItem` isn't resurrected), and nothing is pushed to the
184
- server until that read has SUCCEEDED — a failed read is retried, and a write
184
+ server until that read has SUCCEEDED. A failed read is retried, and a write
185
185
  that still can't confirm what the player had is refused rather than allowed to
186
186
  overwrite it blind, so neither a fresh-start write at boot nor a transport blip
187
187
  can wipe an existing save. `sessionStorage` is shimmed too, but memory-only. Guests and
@@ -325,11 +325,11 @@ Two tuning hooks: `joinRoom({ timeoutMs })` overrides how long a join (and
325
325
  each reconnect attempt) waits for the server's welcome before rejecting with
326
326
  `code: 'error'` (default 10000, clamped to 1000–60000), and `room.latencyMs`
327
327
  reports the join-handshake latency (socket open to server welcome, refreshed
328
- on every reconnect; `null` until the first welcome) — an estimate to tune
329
- render-side smoothing against, not a measured RTT.
328
+ on every reconnect; `null` until the first welcome). Treat it as an estimate
329
+ for tuning render-side smoothing, not a measured RTT.
330
330
 
331
331
  Rooms are lobby-based: each game module declares its player range (up to 64
332
- per room). `match: true` is public quick match — Bounty Board fills the game's
332
+ per room). `match: true` is public quick match: Bounty Board fills the game's
333
333
  open public room and starts a fresh one when no seat is free; the matched
334
334
  room's `room.code` still works as an invite code. When the last free seat is
335
335
  lost to a race the SDK re-matchmakes automatically (up to two retries) before
@@ -344,7 +344,7 @@ snapshots at the room tick rate, and every input takes a network round trip
344
344
  before its effect appears in a snapshot. There is no built-in client-side
345
345
  prediction, interpolation, or rollback. Design for it: turn-based, timing-duel,
346
346
  score-race, and party games feel native; twitch physics (fighting games,
347
- precision platform duels) need your own render-side smoothing — interpolate
347
+ precision platform duels) need your own render-side smoothing: interpolate
348
348
  between snapshots and animate optimistic feedback for the local player's input,
349
349
  but treat the next snapshot as truth. Assume 50-150 ms of input-to-snapshot
350
350
  latency on real connections when tuning game feel. (Relay rooms differ: game
@@ -7,7 +7,7 @@ adapter contract; it does not relay traffic or run a second simulation.
7
7
 
8
8
  External authorities are reviewed and enabled per game. Registration is not
9
9
  self-service. Contact Bounty Board before implementing the adapter so the game
10
- slug, endpoints, room-code rules, ticket keys, and result model can be agreed
10
+ slug, endpoints, room code rules, ticket keys, and result model can be agreed
11
11
  for staging and production.
12
12
 
13
13
  ## What stays authoritative
@@ -23,7 +23,7 @@ Your server remains the only source of truth. It must:
23
23
  - keep endless rooms endless instead of inventing a terminal result.
24
24
 
25
25
  The SDK handles ticket acquisition, WebSocket connection, reconnection, and a
26
- small room-message envelope. It never grants a client authority over identity,
26
+ small room message envelope. It never grants a client authority over identity,
27
27
  state, roles, or results.
28
28
 
29
29
  ## Registration information
@@ -32,9 +32,9 @@ Provide Bounty Board with the following for each environment:
32
32
 
33
33
  - the exact Arcade game slug;
34
34
  - one fixed `wss:` endpoint dedicated to authenticated SDK rooms;
35
- - the room-code format and maximum length your server accepts;
35
+ - the room code format and maximum length your server accepts;
36
36
  - whether missing room codes may create rooms;
37
- - any validated, game-defined `joinData` fields;
37
+ - any validated, game defined `joinData` fields;
38
38
  - whether the game has finite matches or endless sessions; and
39
39
  - an operational contact for key rotation or incident response.
40
40
 
@@ -42,9 +42,9 @@ The registered endpoint must not contain userinfo, query parameters, or a
42
42
  fragment. Use `ws:` only for loopback development. Staging and production need
43
43
  different endpoints and different secrets.
44
44
 
45
- Bounty Board provisions a dedicated ticket-verification secret for the game
45
+ Bounty Board provisions a dedicated ticket verification secret for the game
46
46
  and environment. Store it only on servers. Do not put it in the game bundle,
47
- browser storage, logs, analytics, crash reports, or client-visible environment
47
+ browser storage, logs, analytics, crash reports, or client visible environment
48
48
  variables. Do not reuse credentials from another game or room service.
49
49
 
50
50
  ## Connection flow
@@ -100,7 +100,7 @@ Before accepting a connection, verify all of the following:
100
100
  - `sub` is non-empty; and
101
101
  - required claim types and lengths are valid.
102
102
 
103
- Treat `sub` as an opaque game-scoped identity. Never attempt to map it to a
103
+ Treat `sub` as an opaque game scoped identity. Never attempt to map it to a
104
104
  Bounty Board account or correlate it with another game. `name` and `avatarUrl`
105
105
  are display data, not authorization data. `avatarUrl` may be `null` for any
106
106
  player.
@@ -129,12 +129,12 @@ if (!room.trySend({ type: 'move', x: 1, y: 0 })) {
129
129
  }
130
130
  ```
131
131
 
132
- Script-tag builds use `BBArcade.multiplayer.joinRoom(...)`.
132
+ Script tag builds use `BBArcade.multiplayer.joinRoom(...)`.
133
133
 
134
134
  `joinData` is an optional JSON object capped at 1 KiB. Validate every field,
135
135
  reject unknown or oversized values, and never accept credentials or identity
136
136
  claims from it. If your server distinguishes create from join, use a documented
137
- game-defined intent field and rate-limit room creation. That field is still
137
+ game defined intent field and rate limit room creation. That field is still
138
138
  untrusted and grants no role or permission by itself.
139
139
 
140
140
  ## WebSocket URL
@@ -147,7 +147,7 @@ wss://multiplayer.example.com/bountyboard?ticket=<encoded>&join=<encoded-json>
147
147
 
148
148
  The `join` parameter is omitted when no `joinData` was supplied. Keep this path
149
149
  separate from any anonymous or legacy socket endpoint so a guessed room code
150
- cannot cross the authentication boundary. Redact or disable request-URI logging
150
+ cannot cross the authentication boundary. Redact or disable request URI logging
151
151
  on this route because the short-lived ticket is in the query string.
152
152
 
153
153
  ## SDK room envelope
@@ -174,7 +174,7 @@ Send authoritative state snapshots as:
174
174
  }
175
175
  ```
176
176
 
177
- Send game-defined transient events without changing their payload:
177
+ Send game defined transient events without changing their payload:
178
178
 
179
179
  ```json
180
180
  { "t": "event", "data": { "type": "round_started", "round": 2 } }
@@ -204,23 +204,23 @@ Reject an invalid ticket before `welcome` with an error and close the socket:
204
204
  ```
205
205
 
206
206
  Use `rejected` for invalid admission or `joinData`. After admission, error codes
207
- remain game-defined strings and are surfaced through `room.on('error', ...)`.
207
+ remain game defined strings and are surfaced through `room.on('error', ...)`.
208
208
 
209
209
  ## Reconnection
210
210
 
211
211
  The SDK automatically makes a small number of reconnect attempts. It requests
212
212
  a fresh ticket for the same room and reuses the original `joinData`. Rebind the
213
213
  seat using the verified `(ticket.sub, ticket.roomId)` pair. Never trust a
214
- client-supplied player id or reconnect token as the proof of identity.
214
+ client supplied player id or reconnect token as the proof of identity.
215
215
 
216
- Send the latest complete viewer-safe state in the new `welcome` frame. If your
216
+ Send the latest complete viewer safe state in the new `welcome` frame. If your
217
217
  client uses prediction, include the last processed input sequence in that state
218
218
  so it can discard acknowledged inputs. Clear held controls while the socket is
219
219
  absent.
220
220
 
221
221
  ## Finite matches and endless rooms
222
222
 
223
- Only a server-authoritative finite game may emit `match_end`:
223
+ Only a server authoritative finite game may emit `match_end`:
224
224
 
225
225
  ```json
226
226
  {
@@ -239,7 +239,7 @@ Only a server-authoritative finite game may emit `match_end`:
239
239
 
240
240
  The client receives this as `room.on('end', ({ results }) => ...)`. Persistent
241
241
  Bounty Board results, when enabled, use a separately provisioned
242
- server-to-server reporting credential. Never accept a client-originated result
242
+ server to server reporting credential. Never accept a client originated result
243
243
  or reporting credential.
244
244
 
245
245
  Endless rooms must not emit `match_end`. Death, respawn, a round transition, or
@@ -248,18 +248,18 @@ a player leaving is not automatically a terminal match result.
248
248
  ## Production checklist
249
249
 
250
250
  - Use an isolated staging endpoint and secrets before production.
251
- - Confirm tampered, expired, wrong-slug, and wrong-room tickets fail closed.
252
- - Test both guest and logged-in opaque subjects.
251
+ - Confirm tampered, expired, wrong slug, and wrong room tickets fail closed.
252
+ - Test both guest and logged in opaque subjects.
253
253
  - Reject malformed, unknown, or oversized `joinData`.
254
254
  - Send `welcome` only after ticket and admission validation succeeds.
255
255
  - Verify every snapshot is safe for its specific viewer.
256
- - Test disconnect, held-input cleanup, fresh-ticket reconnect, and `leave()`.
257
- - Rate-limit upgrades, room creation, joins, and inputs.
256
+ - Test disconnect, held input cleanup, fresh ticket reconnect, and `leave()`.
257
+ - Rate limit upgrades, room creation, joins, and inputs.
258
258
  - Redact tickets and join payloads from access logs and error telemetry.
259
259
  - Rotate the dedicated ticket secret without reusing another environment's key.
260
260
  - Emit final results only from a real authoritative terminal condition.
261
261
  - Keep the game playable when multiplayer is unsupported or temporarily down.
262
262
 
263
263
  For SDK integration basics, see https://www.bountyboard.gg/arcade/sdk. For the
264
- agent-readable API contract, see
264
+ agent readable API contract, see
265
265
  https://www.bountyboard.gg/arcade/sdk/llms.txt.
@@ -1,17 +1,17 @@
1
1
  # Bounty Board game design playbook (the "design brain")
2
2
 
3
3
  What actually performs on the Bounty Board arcade, distilled from operating it. Read this
4
- BEFORE designing a new game or porting one in — the SDK wiring is the easy part; these choices
4
+ BEFORE designing a new game or porting one in. The SDK wiring is the easy part; these choices
5
5
  decide whether the game earns plays, retention, and revenue.
6
6
 
7
- > Draft co-owned with our partner studios — challenge anything here with data.
7
+ > Draft co-owned with our partner studios. Challenge anything here with data.
8
8
 
9
9
  ## Session shape
10
10
 
11
- - **Target a 60–180 second core loop.** Arcade traffic arrives mid-browse; games that deliver a
11
+ - **Target a 60 to 180 second core loop.** Arcade traffic arrives mid browse; games that deliver a
12
12
  complete emotional arc (start → tension → payoff) in under three minutes get replays, and
13
- replays drive feed ranking. Longer-form games need checkpointed sessions via cloud saves.
14
- - **Time-to-first-input under 5 seconds.** Call `gameLoadingFinished()` honestly; players who
13
+ replays drive feed ranking. Longer form games need checkpointed sessions via cloud saves.
14
+ - **Time to first input under 5 seconds.** Call `gameLoadingFinished()` honestly; players who
15
15
  bounce on a spinner never come back. Defer heavy assets past the first playable moment.
16
16
  - **Instant restart.** Death → new run should be ONE input and under a second. Restart friction
17
17
  is the top killer of "one more run".
@@ -19,31 +19,31 @@ decide whether the game earns plays, retention, and revenue.
19
19
  ## Score design (this is leaderboard design)
20
20
 
21
21
  - **Scores must be integers with a meaningful gradient.** A good score curve separates a casual
22
- run from a great one by 10–100x, not 2x — that's what makes a board worth climbing.
22
+ run from a great one by 10 to 100x, not 2x. That's what makes a board worth climbing.
23
23
  - **Skill ceiling over grind ceiling.** If score scales with time played rather than skill, the
24
- board saturates and goes stale. Cap or decay pure-survival scoring; reward risk.
25
- - **Design the "one point short" feeling.** Near-miss visibility (show the player's best and the
24
+ board saturates and goes stale. Cap or decay pure survival scoring; reward risk.
25
+ - **Design the "one point short" feeling.** Near miss visibility (show the player's best and the
26
26
  next board rank in-game via your own UI) measurably lifts replays.
27
- - **Daily mode**: if your game has procedural content, ship a shared-seed daily run and submit
27
+ - **Daily mode**: if your game has procedural content, ship a shared seed daily run and submit
28
28
  it with `{ mode: 'daily' }`. Daily boards reset at midnight UTC and are the strongest
29
- retention surface on the platform — everyone plays the SAME level, so the board is fair chat.
29
+ retention surface on the platform. Everyone plays the SAME level, so the board is fair chat.
30
30
 
31
- ## Multiplayer design (for room-based games)
31
+ ## Multiplayer design (for room based games)
32
32
 
33
- - **Latency-tolerant mechanics win.** Positional games at 10–20Hz snapshots with client
33
+ - **Latency tolerant mechanics win.** Positional games at 10 to 20Hz snapshots with client
34
34
  interpolation feel great for chase/tag/social deduction; twitch duels don't. Design around
35
- prediction-friendly movement (momentum, grid steps) rather than instant-hit actions.
35
+ prediction friendly movement (momentum, grid steps) rather than instant hit actions.
36
36
  - **Information asymmetry is a server feature.** The room server sends each player only what
37
37
  they may know (hiders invisible to the seeker). Lean into designs where hidden information IS
38
- the game — it's cheat-proof by construction here.
39
- - **2-minute rounds, drop-in lobbies.** Rooms fill from friends sharing codes; short rounds
40
- forgive mid-round joins as spectators and keep groups cycling.
38
+ the game. It's cheat proof by construction here.
39
+ - **2-minute rounds, drop in lobbies.** Rooms fill from friends sharing codes; short rounds
40
+ forgive mid round joins as spectators and keep groups cycling.
41
41
  - **Send inputs, not outcomes.** If your design needs the client to decide who got tagged, the
42
- design is wrong — move the rule server-side.
42
+ design is wrong. Move the rule server-side.
43
43
 
44
44
  ## Monetization etiquette (rewarded ads)
45
45
 
46
- - **Ads are a player's trade, never a toll.** Best-performing placements: revive ("continue this
46
+ - **Ads are a player's trade, never a toll.** Best performing placements: revive ("continue this
47
47
  run?"), doubler ("2x this run's coins"), cosmetic unlock. Never gate core progression.
48
48
  - **One organic placement beats three pushy ones.** Interrupting flow trains players to leave;
49
49
  prepare one rewarded placement at a natural fail state and offer it once.
@@ -54,13 +54,13 @@ decide whether the game earns plays, retention, and revenue.
54
54
 
55
55
  ## Platform fit
56
56
 
57
- - **Mobile-first inputs.** Most arcade sessions are touch. One-thumb controls, generous hit
58
- targets, no hover dependence, portrait-friendly if possible. Keyboard is the enhancement.
59
- - **Performance budget: 60fps on a mid-range phone.** Cap DPR, pool objects, avoid layout
57
+ - **Mobile first inputs.** Most arcade sessions are touch. One thumb controls, generous hit
58
+ targets, no hover dependence, portrait friendly if possible. Keyboard is the enhancement.
59
+ - **Performance budget: 60fps on a mid range phone.** Cap DPR, pool objects, avoid layout
60
60
  thrash. Players don't report jank, they just leave.
61
- - **Own your standalone build.** The same bundle must run off-platform (the SDK no-ops). Don't
62
- fork builds; feature-detect through the SDK's own fallbacks.
63
- - **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game-over, and
61
+ - **Own your standalone build.** The same bundle must run off platform (the SDK no-ops). Don't
62
+ fork builds; feature detect through the SDK's own fallbacks.
63
+ - **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game over, and
64
64
  greet returning players with their progress (pair with `getPlayer()` for the name). Hosted
65
65
  builds have no localStorage, so wire this early, not as a retrofit.
66
66
 
@@ -71,5 +71,5 @@ decide whether the game earns plays, retention, and revenue.
71
71
  - [ ] The score of a great run embarrasses the score of a lucky run
72
72
  - [ ] Daily mode if content is procedural
73
73
  - [ ] Rewarded placement is a trade the player initiates
74
- - [ ] Playable one-thumb on a phone at 60fps
74
+ - [ ] Playable one thumb on a phone at 60fps
75
75
  - [ ] Boots and plays with the SDK fully offline
package/docs/llms.txt CHANGED
@@ -1,23 +1,23 @@
1
- # Bounty Board Arcade SDK — agent-readable integration contract
1
+ # Bounty Board Arcade SDK: agent readable integration contract
2
2
 
3
3
  Human guide: https://www.bountyboard.gg/arcade/sdk
4
- Package: @bountyboard/arcade-sdk
5
- Stable npm release: 1.2.0
4
+ Package: @bountyboard/arcade-sdk (https://www.npmjs.com/package/@bountyboard/arcade-sdk)
5
+ Stable npm release: 1.4.0
6
6
  Wire protocol: 1
7
7
 
8
8
  This file is the complete integration contract for coding agents integrating an
9
9
  HTML5 game. The package TypeScript declarations remain the exact public type
10
- reference. npm 1.2.0, the /arcade-sdk/v1.js browser artifact, and repository
10
+ reference. npm 1.4.0, the /arcade-sdk/v1.js browser artifact, and repository
11
11
  source all expose the same surface. Rooms open on all three approved rails:
12
- Bounty shared-service referee modules, Bounty shared-service relay rooms, and
13
- host-routed registered external authorities. code/create works on every rail;
14
- match: true works on both Bounty shared-service tiers. The external-authority
12
+ Bounty shared service referee modules, Bounty shared service relay rooms, and
13
+ host routed registered external authorities. code/create works on every rail;
14
+ match: true works on both Bounty shared service tiers. The external authority
15
15
  adapter guide ships in the package and is public at the URL above.
16
16
 
17
17
  ## Non-negotiable integration rules
18
18
 
19
19
  1. The SDK is never load-bearing. A game must boot and remain fully playable
20
- with no Bounty Board parent, no logged-in player, no ad inventory, no cloud
20
+ with no Bounty Board parent, no logged in player, no ad inventory, no cloud
21
21
  save, and no multiplayer authority.
22
22
  2. Call gameOver() exactly once per run and use integer scores. The host/server
23
23
  enforces per-game plausibility caps.
@@ -25,17 +25,19 @@ adapter guide ships in the package and is public at the URL above.
25
25
  death prompts, and ad breaks are not active play.
26
26
  4. getPlayer() can be null and avatarUrl can be null. The SDK never exposes
27
27
  account ids, emails, or roles.
28
- 5. Store all progress in one save(string) blob (about 1 MB). Save at
29
- checkpoints/game over, not in a frame loop, and catch every rejection. When
30
- the game is an engine export whose storage layer you cannot rewire, use
31
- storage.install() instead and let the shim carry localStorage to the cloud.
32
- 6. For new rewarded-ad work, prepare first, enable the game's button only when
28
+ 5. Store all progress in one save(string) blob (about 1 MB). Gate the account
29
+ backed calls on getPlayer() rather than discovering the guest case through a
30
+ thrown unauthenticated. Save at checkpoints/game over, not in a frame loop,
31
+ and catch every rejection. When the game is an engine export whose storage
32
+ layer you cannot rewire, use storage.install() instead and let the shim
33
+ carry localStorage to the cloud.
34
+ 6. For new rewarded ad work, prepare first, enable the game's button only when
33
35
  status is ready, call prepared.show() directly from the click/tap handler,
34
36
  and grant only when the final status is viewed.
35
37
  7. Multiplayer has three rails. Referee modules and external authorities own
36
38
  simulation/results and receive client inputs. The casual relay owns signed
37
39
  admission, roster, capacity, host succession, reconnect grace, and public
38
- message fan-out, but it does not referee game state or outcomes. Catch every
40
+ message fan out, but it does not referee game state or outcomes. Catch every
39
41
  joinRoom() rejection and keep solo/standalone play available.
40
42
  8. lockToHost() is an optional anti-rehosting deterrent, not DRM. Call it before
41
43
  boot. allow lists the domains the STUDIO hosts the build on, so a build that
@@ -49,16 +51,21 @@ Preferred for games with a build step:
49
51
  npm install @bountyboard/arcade-sdk
50
52
  import { BBArcade } from '@bountyboard/arcade-sdk';
51
53
 
52
- The package is zero-dependency, typed, ESM + CommonJS, and SSR-safe. Importing
54
+ The package is zero dependency, typed, ESM + CommonJS, and SSR safe. Importing
53
55
  the module does not install a window.BBArcade global.
54
56
 
55
- No-build script tag:
57
+ Script tag, no build step:
56
58
 
57
59
  <script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
58
60
 
59
61
  This installs window.BBArcade. Standalone declarations:
60
62
  https://www.bountyboard.gg/arcade-sdk.d.ts
61
63
 
64
+ The hosted file is served with Cross-Origin-Resource-Policy: cross-origin and
65
+ Access-Control-Allow-Origin: *, so the tag also loads on a cross-origin-isolated
66
+ page (Cross-Origin-Embedder-Policy: require-corp). Vendoring a copy of the file
67
+ into the build and loading it same-origin is equally supported.
68
+
62
69
  Do not mix npm and the script tag in one page. A bundler that specifically
63
70
  wants the global may import @bountyboard/arcade-sdk/global.
64
71
 
@@ -66,7 +73,7 @@ Multiplayer module import:
66
73
 
67
74
  import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
68
75
 
69
- Script-tag multiplayer is BBArcade.multiplayer.joinRoom(...).
76
+ Script tag multiplayer is BBArcade.multiplayer.joinRoom(...).
70
77
 
71
78
  Public package exports:
72
79
 
@@ -81,12 +88,12 @@ Public package exports:
81
88
 
82
89
  Distribution format does not determine capabilities; the embedding host does.
83
90
 
84
- - Bounty-hosted upload:
91
+ - Bounty hosted upload:
85
92
  lifecycle/scores supported; player identity/variants supported; cloud
86
- save/load available to logged-in players; native localStorage/sessionStorage/
93
+ save/load available to logged in players; native localStorage/sessionStorage/
87
94
  IndexedDB/cookies all THROW (opaque origin), so engine exports need
88
95
  storage.install(); ads available to approved games; multiplayer available
89
- after per-game approval, with relay as the default Bounty shared-service tier
96
+ after per-game approval, with relay as the default Bounty shared service tier
90
97
  when no bespoke referee module is registered.
91
98
  - Approved external URL embed inside the Bounty Board player:
92
99
  lifecycle/scores, identity, variants, and approved multiplayer (including the
@@ -94,11 +101,11 @@ Distribution format does not determine capabilities; the embedding host does.
94
101
  per-game attribution yet); cloud save/load is unsupported.
95
102
  - Standalone or opened directly on the game's own site:
96
103
  fire-and-forget calls no-op; init resolves immediately; getPlayer returns
97
- null; getVariant returns the alphabetical control; host-only promise APIs
104
+ null; getVariant returns the alphabetical control; host only promise APIs
98
105
  reject unsupported or return an unavailable outcome. An unanswered non-Bounty
99
106
  embed uses an approximately 1.5-second init/getPlayer grace instead.
100
107
 
101
- Guests are normal. getPlayer resolves null and account-backed calls may reject
108
+ Guests are normal. getPlayer resolves null and account backed calls may reject
102
109
  with code unauthenticated.
103
110
 
104
111
  Leaderboards need one thing outside the code: the studio must declare
@@ -122,7 +129,7 @@ ceilings live beside that declaration and reject implausible values.
122
129
  BBArcade.gameplayStop(); // pause/death/menu/ad
123
130
  BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run
124
131
 
125
- For a shared-seed Daily, pass { mode: 'daily' } to submitScore and gameOver.
132
+ For a shared seed Daily, pass { mode: 'daily' } to submitScore and gameOver.
126
133
 
127
134
  ## Exact shared types and configuration
128
135
 
@@ -164,8 +171,8 @@ Core configuration shapes:
164
171
  lockToHost defaults allow bountyboard.gg and its subdomains, the fixed Bounty
165
172
  staging host, localhost, and 127.0.0.1. allow extends rather than replaces that
166
173
  list. signed mode requests an ECDSA origin attestation; it falls back to the
167
- best-effort browser-origin check only when the host reports no signing key or
168
- Web Crypto is unavailable. A missing host or present-but-invalid attestation
174
+ best effort browser origin check only when the host reports no signing key or
175
+ Web Crypto is unavailable. A missing host or present but invalid attestation
169
176
  blocks.
170
177
 
171
178
  Player and score shapes:
@@ -187,7 +194,7 @@ Storage compatibility shapes:
187
194
  readonly mode: BBArcadeStorageMode;
188
195
  }
189
196
 
190
- Rewarded-ad options and results:
197
+ Rewarded ad options and results:
191
198
 
192
199
  interface BBArcadeRewardedAdOptions {
193
200
  placement?: string; name?: string; reward?: string; adBreakId?: string;
@@ -279,24 +286,26 @@ Lifecycle and scoring:
279
286
  Player data and experiments:
280
287
 
281
288
  - save(blob: string): Promise<void>
282
- One blob, about 1 MB. Requires a Bounty-hosted upload and logged-in player.
289
+ One blob, about 1 MB. Requires a Bounty hosted upload and logged in player.
283
290
  Oversized blobs reject too_large immediately via a client-side precheck
284
291
  against the same 1 MiB cap the server enforces.
285
292
  - load(): Promise<string | null>
286
- Returns null when no save exists. Rejects like save().
293
+ Returns null when no save exists. Rejects like save(). Gate on getPlayer() so
294
+ the guest case is a branch rather than a catch.
287
295
  - storage: BBArcadeStorage
288
296
  localStorage compatibility for hosted builds. install(): BBArcadeStorageMode
289
- replaces a throwing window.localStorage with a Storage-shaped object backed
297
+ replaces a throwing window.localStorage with a Storage shaped object backed
290
298
  by the cloud save, and is a no-op where a real Storage works. ready():
291
299
  Promise<BBArcadeStorageMode> resolves once the cloud read has landed.
292
300
  flush(): Promise<void> forces the debounced write out now. mode is the
293
- current BBArcadeStorageMode ('native' | 'cloud' | 'memory'). The script-tag
301
+ current BBArcadeStorageMode ('native' | 'cloud' | 'memory'). The script tag
294
302
  build calls install() for you at load; module consumers call it themselves
295
303
  before boot.
296
304
  - getPlayer(): Promise<{ name: string, avatarUrl: string | null } | null>
297
- Display identity only; always handle null.
305
+ Display identity only; always handle null. Never rejects and never hangs, so
306
+ it is also the cheapest signed in check to gate save()/load() on.
298
307
  - onPlayerChange(handler): () => void
299
- Subscribes to CHANGES in the display identity (mid-session login/logout).
308
+ Subscribes to CHANGES in the display identity (mid session login/logout).
300
309
  The handler receives the same { name, avatarUrl } | null shape as
301
310
  getPlayer() and runs only when the identity actually changes; subscribing
302
311
  does not replay the current value. Returns an unsubscribe function.
@@ -307,9 +316,11 @@ Player data and experiments:
307
316
  item. Do not re-randomize client-side.
308
317
 
309
318
  save/load can reject with every BBArcadeErrorCode. load resolves null only when
310
- the logged-in hosted player has no save; a guest can reject unauthenticated.
311
- Every host request (save, load, variant, multiplayer ticket) rejects with code
312
- error after a 15-second timeout when no answer arrives.
319
+ the logged in hosted player has no save; a guest can reject unauthenticated.
320
+ Gate on getPlayer() so account state is a branch you took rather than an error
321
+ you caught, and keep the catch for transport failures. Every host request
322
+ (save, load, variant, multiplayer ticket) rejects with code error after a
323
+ 15-second timeout when no answer arrives.
313
324
 
314
325
  unsupported | unauthenticated | too_large | rejected | error
315
326
 
@@ -318,7 +329,7 @@ Rewarded ads:
318
329
  - prepareRewardedAd(options?): Promise<BBArcadeRewardedAdPreparation>
319
330
  Recommended. Alias: prepareRewardedBreak(). Prepared show() is one-shot.
320
331
  - rewardedAd(options?): Promise<BBArcadeRewardedAdResult>
321
- Low-level structured-result API. Alias: showRewardedAd().
332
+ Low-level structured result API. Alias: showRewardedAd().
322
333
  - rewardedBreak(options | onStart): Promise<boolean>
323
334
  Deprecated compatibility helper. New games must use the prepared flow.
324
335
  - preloadRewardedAds(options?): Promise<boolean>
@@ -341,7 +352,7 @@ Multiplayer:
341
352
  type BBArcadeMpJoinOptions = {
342
353
  code?: string;
343
354
  create?: boolean;
344
- match?: boolean; // public quick match; Bounty shared-service tiers only
355
+ match?: boolean; // public quick match; Bounty shared service tiers only
345
356
  joinData?: Readonly<Record<string, unknown>>;
346
357
  roomUrl?: string; // local development override; provide with ticket
347
358
  ticket?: string; // local development override; provide with roomUrl
@@ -379,7 +390,7 @@ Multiplayer:
379
390
  players: BBArcadeMpPlayer[];
380
391
  state: unknown;
381
392
  connected: boolean;
382
- latencyMs: number | null; // join-handshake estimate, refreshed on reconnect
393
+ latencyMs: number | null; // join handshake estimate, refreshed on reconnect
383
394
  send(input: unknown): void;
384
395
  trySend(input: unknown): boolean;
385
396
  on<K extends keyof BBArcadeMpRoomEvents>(
@@ -393,7 +404,7 @@ Multiplayer:
393
404
  joinRoom(options?: BBArcadeMpJoinOptions): Promise<BBArcadeMpRoom>;
394
405
  }
395
406
 
396
- Relay runtime payloads use these exact shapes through the otherwise-unknown
407
+ Relay runtime payloads use these exact shapes through the otherwise unknown
397
408
  room.state and room.on('event') values:
398
409
 
399
410
  interface BBArcadeRelayState {
@@ -411,16 +422,16 @@ room.state and room.on('event') values:
411
422
  | { type: 'relay_host'; hostId: string | null };
412
423
 
413
424
  BBArcadeRelayState and BBArcadeRelayEvent are documentation names, not package
414
- exports; the public SDK intentionally types game/tier-defined state and events
425
+ exports; the public SDK intentionally types game/tier defined state and events
415
426
  as unknown.
416
427
 
417
428
  - joinRoom(options?): Promise<BBArcadeMpRoom>
418
429
  No options and { create: true } both generate a 4-character invite code from
419
430
  ABCDEFGHJKLMNPQRSTUVWXYZ23456789. A supplied code is uppercased. match: true
420
- is Bounty-hosted public quick match and excludes code, create, roomUrl, and
431
+ is Bounty hosted public quick match and excludes code, create, roomUrl, and
421
432
  ticket. roomUrl+ticket bypass the host handshake for local development only;
422
433
  provide both (a lone value is not an override).
423
- - joinData must be a non-null, non-array JSON-serializable object whose UTF-8
434
+ - joinData must be a non-null, non-array JSON serializable object whose UTF-8
424
435
  JSON encoding is at most 1 KiB. Cycles, arrays, primitives, and oversized data
425
436
  reject with code rejected. Never include credentials or secrets.
426
437
  - joinRoom resolves only after the authority sends welcome (10-second default
@@ -429,17 +440,17 @@ as unknown.
429
440
  At resolution, code/playerId/players/state/connected already hold the initial
430
441
  lobby state. There is no replayed initial snapshot: render those fields first,
431
442
  then subscribe to future events.
432
- - room.latencyMs reports the join-handshake latency in ms (socket open to
443
+ - room.latencyMs reports the join handshake latency in ms (socket open to
433
444
  server welcome: ticket verification plus one round trip), refreshed on every
434
445
  successful (re)connect and null until the first welcome. Use it to tune
435
- render-side smoothing; it is an estimate, not a measured RTT.
436
- - Values passed to send/trySend must be JSON-serializable. A referee room
446
+ render side smoothing; it is an estimate, not a measured RTT.
447
+ - Values passed to send/trySend must be JSON serializable. A referee room
437
448
  treats the value as an input; a relay room treats it as a public game
438
449
  message. send discards local write status. trySend
439
450
  returns true only when JSON was written to an open socket; it does NOT mean
440
451
  the authority accepted the input. false means disconnected, raced closed, or
441
452
  serialization/WebSocket.send failed. The authority still validates,
442
- sequences, and may rate-drop inputs.
453
+ sequences, and may drop inputs above its rate limit.
443
454
  - on returns an unsubscribe function. The SDK updates room.players before
444
455
  playerJoin/playerLeave handlers and room.state before snapshot handlers.
445
456
  - leave intentionally closes the Room and permanently disables reconnect for it.
@@ -455,28 +466,49 @@ BBArcade.version and the exported PROTOCOL_VERSION are the core bb-arcade
455
466
  postMessage wire-protocol version. They are not npm semver and are not a
456
467
  version field on multiplayer WebSocket frames.
457
468
 
458
- ## Cloud-save recipe
469
+ ## Cloud save recipe
459
470
 
460
471
  Hosted uploads have an opaque origin where a real localStorage is not merely
461
- empty — reading window.localStorage THROWS a SecurityError, as do
472
+ empty. Reading window.localStorage THROWS a SecurityError, as do
462
473
  sessionStorage, IndexedDB, and document.cookie. SDK save is the primary store
463
474
  there. URL embeds and standalone builds need their own same-origin fallback.
464
475
 
476
+ Ask who is playing before reaching for the account backed calls. getPlayer()
477
+ resolves null for guests, standalone play, and embeds off Bounty Board, and it
478
+ never rejects and never hangs, so it turns the guest case into a branch instead
479
+ of a thrown error:
480
+
481
+ const player = await BBArcade.getPlayer();
482
+
483
+ if (player) {
484
+ const blob = await BBArcade.load(); // null = signed in, no save yet
485
+ restore(blob ? JSON.parse(blob) : defaults);
486
+ } else {
487
+ restoreStandaloneProgress(); // and offer "Sign in to save"
488
+ }
489
+
490
+ Still catch save()/load(), but for transport failures, not for account state
491
+ you already branched on:
492
+
465
493
  const blob = JSON.stringify(progress);
466
494
  try {
467
495
  await BBArcade.save(blob);
468
496
  } catch (error) {
469
497
  if (error.code === 'unsupported') localStorage.setItem('progress', blob);
498
+ else if (error.code !== 'unauthenticated') reportSaveFailure(error.code);
470
499
  }
471
500
 
472
- try {
473
- const blob = await BBArcade.load();
474
- restore(blob ? JSON.parse(blob) : defaults);
475
- } catch {
476
- restoreStandaloneProgress();
477
- }
501
+ save() rejecting for a guest is deliberate, not an oversight. A write that did
502
+ not happen must never resolve as if it had, or the game reports "Saved" over
503
+ progress that is already gone. It is also the only place "Sign in to keep your
504
+ progress" can be offered in context, which is worth more than a silent no-op.
505
+
506
+ Do not assume load() resolves null for a guest either. null means the logged in
507
+ player has no save yet; a guest rejects unauthenticated.
478
508
 
479
- Do not assume load() resolves null for a guest; it can reject unauthenticated.
509
+ A game that does not want to model accounts at all should use the storage shim
510
+ below instead. It folds guest and standalone play into 'memory' mode and never
511
+ rejects on either.
480
512
 
481
513
  ## localStorage shim for engine exports
482
514
 
@@ -485,13 +517,19 @@ runtimes whose storage layer cannot be rewired without patching engine
485
517
  internals: GameMaker HTML5 (ini_open/ini_write_*/game_save all sit on
486
518
  localStorage), Godot, Unity, and Construct.
487
519
 
488
- With the script tag, nothing is required beyond load order — the SDK installs
520
+ It doubles as the tier for any game that does not want to model accounts at
521
+ all. Guests, standalone play, and embeds off Bounty Board settle into 'memory'
522
+ mode, so nothing rejects and no auth state has to be handled. The cost is that
523
+ the game never learns a save was not persisted, so it cannot offer the player a
524
+ chance to sign in and keep it. Check storage.mode for that.
525
+
526
+ With the script tag, nothing is required beyond load order. The SDK installs
489
527
  the shim at load and window.localStorage starts working:
490
528
 
491
529
  <script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
492
530
  <script src="html5game/YourGame.js"></script>
493
531
 
494
- Module consumers keep a side-effect-free import and install it themselves,
532
+ Module consumers keep a side effect free import and install it themselves,
495
533
  before any engine code runs:
496
534
 
497
535
  import { BBArcade } from '@bountyboard/arcade-sdk';
@@ -505,13 +543,13 @@ play and URL embeds are untouched. Behavior once installed:
505
543
  - Progress becomes per-player and cross-device, not per-browser.
506
544
  - setItem throws QuotaExceededError when the write would exceed the ~1 MB save
507
545
  cap, matching a real Storage.
508
- - sessionStorage is shimmed too, but memory-only — it is per-session by
546
+ - sessionStorage is shimmed too, but memory only. It is per session by
509
547
  definition and is never synced.
510
548
  - Guests and standalone play settle in 'memory' mode: storage still works for
511
549
  the session, it is just never persisted. Nothing rejects; nothing hangs.
512
550
 
513
551
  The one real limitation is that getItem is synchronous while the cloud read is
514
- not. Gate boot-time reads on ready():
552
+ not. Gate boot time reads on ready():
515
553
 
516
554
  await BBArcade.storage.ready(); // 'cloud' | 'memory' | 'native'
517
555
  startGame();
@@ -520,8 +558,8 @@ Writes made before hydration are kept and merged with the cloud read (the
520
558
  game's own write wins, and a removeItem is not resurrected), and nothing is
521
559
  pushed to the server until that read has SUCCEEDED. A failed read is retried on
522
560
  the next write, and a write that still cannot confirm what the player had is
523
- refused (rejecting with code error) rather than allowed to overwrite it blind —
524
- so neither a fresh-start write at boot nor a transport blip can wipe an existing
561
+ refused (rejecting with code error) rather than allowed to overwrite it blind,
562
+ so neither a fresh start write at boot nor a transport blip can wipe an existing
525
563
  save. A game that reads
526
564
  at boot WITHOUT awaiting ready() may still see an empty map on the first frame.
527
565
 
@@ -531,9 +569,9 @@ unwraps it and returns only what save() wrote. A raw blob written before the
531
569
  shim existed is read back untouched, so turning the shim on never orphans a
532
570
  save.
533
571
 
534
- ## Safe rewarded-ad recipe
572
+ ## Safe rewarded ad recipe
535
573
 
536
- Prepare at the natural break. Keep the game-owned button disabled until ready.
574
+ Prepare at the natural break. Keep the game owned button disabled until ready.
537
575
  Call show() as the first operation in the direct click/tap handler: no await,
538
576
  timer, microtask, animation, state transition, or network call before it.
539
577
 
@@ -604,79 +642,79 @@ Bounty referee module, or registered external authority. The signed ticket
604
642
  binds that tier. joinData cannot select or downgrade it; a ticket/registry
605
643
  mismatch fails closed, and a registered external slug never falls back to
606
644
  relay. All tiers use the same joinRoom transport, room codes, and client
607
- connection lifecycle; tier/game-specific state and event schemas still differ.
645
+ connection lifecycle; tier/game specific state and event schemas still differ.
608
646
 
609
647
  create/code/match select a room. Relay supplies the host contract below, but
610
648
  games build any ready/team/start/kick/rematch protocol on top and those rules
611
- remain client-trusted. Referee modules and external authorities define their
649
+ remain client trusted. Referee modules and external authorities define their
612
650
  own state, input, event, spectator, lobby, and rematch schemas. A module's
613
651
  minPlayers does not automatically gate simulation or start a match. Quick match
614
- may place a player into a joinable room already in progress; late-join behavior
615
- is tier/game-defined.
652
+ may place a player into a joinable room already in progress; late join behavior
653
+ is tier/game defined.
616
654
 
617
655
  joinData is always untrusted JSON. The SDK only validates its shape and 1 KiB
618
656
  cap. A relay reads roomSize only from the founding seat. Referee modules and
619
- external authorities validate any game-specific admission fields themselves.
657
+ external authorities validate any game specific admission fields themselves.
620
658
 
621
659
  ### Built-in Bounty relay tier
622
660
 
623
- The relay is the default Bounty shared-service tier for a multiplayer-approved
661
+ The relay is the default Bounty shared service tier for a multiplayer approved
624
662
  slug that has no bespoke referee module and is not routed to an external
625
- authority. It is a casual, zero-server-code lobby—not an authoritative game
663
+ authority. It is a casual lobby with no server code, not an authoritative game
626
664
  simulation or anti-cheat boundary.
627
665
 
628
666
  - The first admitted seat fixes capacity from joinData.roomSize. It must be an
629
- integer from 2 through 64; missing, invalid, or out-of-range means 8. The room
667
+ integer from 2 through 64; missing, invalid, or out of range means 8. The room
630
668
  may operate with one current occupant. Later joiners cannot change size. Once
631
669
  the room is completely empty, the next founder may choose it again. Ship the
632
- same roomSize from every client so quick-matched rooms found consistently.
670
+ same roomSize from every client so quick matched rooms found consistently.
633
671
  - Welcome and later 1 Hz metadata snapshots are identical for every viewer and
634
672
  expose exactly
635
673
  { mode: 'relay', hostId: string | null, size: number, dropped: number }.
636
674
  - The oldest retained seat is host. A transient disconnect preserves its seat
637
675
  and hostId through the 15-second grace. After the host actually leaves or its
638
- seat expires, the next-oldest retained seat becomes host and live clients get
676
+ seat expires, the next oldest retained seat becomes host and live clients get
639
677
  { type: 'relay_host', hostId } through room.on('event'). The founding welcome
640
678
  already carries hostId, so no initial relay_host event is required.
641
679
  - room.send(data) and room.trySend(data) publicly fan the JSON payload to EVERY
642
680
  player, including the sender. There are no private messages, hidden state, or
643
681
  viewer filtering. Batches arrive at 20 Hz through room.on('event') as
644
- { type: 'relay', messages: [{ from, data }] }, where from is a room-scoped
682
+ { type: 'relay', messages: [{ from, data }] }, where from is a room scoped
645
683
  player id. Tick batching adds at most about 50 ms before network latency.
646
684
  - Each relay payload's UTF-8 JSON encoding is capped at 1024 bytes. The relay
647
685
  accepts at most 15 messages per player per second and 120 per room per second.
648
686
  Byte/rate excess is silently dropped and increments cumulative state.dropped,
649
687
  visible on a later metadata snapshot. state.dropped does not count malformed,
650
- stale-sequence, or outer-envelope drops.
688
+ stale sequence, or outer envelope drops.
651
689
  - The common outer room guard still caps a client frame at 4096 characters and
652
690
  90 accepted envelopes per player per second; server frames are capped at
653
691
  256 KiB.
654
692
  - Relay rooms are endless. They never emit match_end/room.on('end'), never close
655
693
  because a game outcome was claimed, and never report results to Bounty Board.
656
- All game rules and outcomes are client-trusted; do not use relay claims for
694
+ All game rules and outcomes are client trusted; do not use relay claims for
657
695
  trusted rewards, standings, or anti-cheat decisions. Implement round boundaries
658
696
  in game messages and call room.leave() when the player exits.
659
697
 
660
698
  ### Refereed Bounty modules and external authorities
661
699
 
662
700
  A referee validates every input, runs the only game simulation, clears held
663
- controls on disconnect when its game requires it, and sends viewer-safe state.
701
+ controls on disconnect when its game requires it, and sends viewer safe state.
664
702
  Clients never report authoritative results. A finite referee may emit one
665
- server-authored match_end; the SDK treats it as terminal and does not reconnect.
703
+ server authored match_end; the SDK treats it as terminal and does not reconnect.
666
704
  There is no SDK reset/rematch method. Endless authorities do not fabricate
667
705
  match_end. Registered external authorities define their own socket shutdown and
668
- subsequent-join behavior.
706
+ subsequent join behavior.
669
707
 
670
708
  Bounty referee modules declare integer min/max players with
671
709
  1 <= min <= max <= 64. tickHz is 1-60. snapshotHz may be lower, from 1 through
672
- tickHz; not every simulation tick emits a snapshot. Simulation-backed sync has
710
+ tickHz; not every simulation tick emits a snapshot. Simulation backed sync has
673
711
  a full network round trip, and the SDK provides no client-side prediction,
674
- interpolation, or rollback. Expect roughly 50-150 ms input-to-snapshot latency
675
- on real connections; smooth locally, then correct to the next viewer-safe
712
+ interpolation, or rollback. Expect roughly 50-150 ms from input to snapshot
713
+ on real connections; smooth locally, then correct to the next viewer safe
676
714
  snapshot. Relay game payloads instead arrive in the public 20 Hz event batches
677
715
  described above; its 1 Hz snapshot contains metadata only.
678
716
 
679
- ### Common Bounty shared-service room behavior
717
+ ### Common Bounty shared service room behavior
680
718
 
681
719
  - Generated rooms use four-character invite codes. match: true works for relay
682
720
  and referee rooms: it uses the current public room while live occupancy plus
@@ -684,12 +722,12 @@ described above; its 1 Hz snapshot contains metadata only.
684
722
  failed occupancy probes, or reservations roll a new room, so live occupancy
685
723
  can roll before reaching the room's capacity. A matched room.code is also an
686
724
  invite code.
687
- - A lost last-seat/ended-room race triggers at most 2 re-matchmaking retries
725
+ - A lost last seat or ended room race triggers at most 2 re-matchmaking retries
688
726
  (3 total attempts). Final rejection has code rejected and detail room_full or
689
727
  room_over, whichever the last attempt returned.
690
728
  - When a finite Bounty referee module ends, it broadcasts match_end once, closes
691
729
  its sockets, and subsequent joins to that room reject with room_over. This
692
- finite-close rule does not apply to the endless relay tier.
730
+ closing rule does not apply to the endless relay tier.
693
731
  - Disconnected seats are reserved for 15 seconds. The SDK obtains fresh tickets
694
732
  and makes up to 3 reconnect attempts. During grace, the player remains in
695
733
  room.players and playerLeave is delayed until the seat expires. room.leave()
@@ -701,7 +739,7 @@ described above; its 1 Hz snapshot contains metadata only.
701
739
 
702
740
  Public quick match is unavailable on registered external authorities, which
703
741
  own their capacity, lobby lifecycle, and any matchmaking. Their reviewed
704
- limits can differ from the Bounty shared-service defaults, and an external
742
+ limits can differ from the Bounty shared service defaults, and an external
705
743
  authority may be endless.
706
744
 
707
745
  joinRoom rejection map:
@@ -721,12 +759,12 @@ or room_over. Generic rejected has multiple causes; do not blind retry or show
721
759
  Multiplayer is curated per game slug. After approval, Bounty Board assigns one
722
760
  of three rails: (a) the built-in casual relay, (b) a bespoke Bounty referee
723
761
  module, or (c) a registered external authority with a dedicated ticket key.
724
- Relay is the shared-service default when no bespoke module or external route is
762
+ Relay is the shared service default when no bespoke module or external route is
725
763
  registered; clients cannot select the tier. External servers keep their existing
726
- simulation and add only an isolated signed-ticket adapter plus the SDK room
727
- envelope—never create a second simulation beside one.
764
+ simulation and add only an isolated signed ticket adapter plus the SDK room
765
+ envelope. Never create a second simulation beside one.
728
766
 
729
- Local Bounty shared-service development bypasses the host ticket handshake only
767
+ Local Bounty shared service development bypasses the host ticket handshake only
730
768
  with paired roomUrl and ticket values (for example wrangler dev with
731
769
  DEV_ALLOW_UNSIGNED=1). A registered slug resolves to its referee module; another
732
770
  well-formed slug resolves to relay:
@@ -754,28 +792,34 @@ https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
754
792
  - Mock rewarded ready, unavailable, dismissed, error, and viewed; only viewed grants.
755
793
  - Assert prepared.show() is called directly from the player gesture.
756
794
  - Test initial room.state/players rendering before the first later event.
757
- - Test multiplayer reconnect/grace, trySend false, tier-appropriate events/state,
795
+ - Test multiplayer reconnect/grace, trySend false, tier appropriate events/state,
758
796
  and leave.
759
- - Test create/code, and match on the Bounty shared-service tiers.
760
- - For relay, test roomSize default/range, public fan-out (including sender), host
797
+ - Test create/code, and match on the Bounty shared service tiers.
798
+ - For relay, test roomSize default/range, public fan out (including sender), host
761
799
  succession after grace, byte/rate drops, state.dropped, and absence of end.
762
- - For referee/external rooms, test viewer-safe snapshots, authoritative results,
763
- and game-defined start/ready/late-join rules against that authority.
800
+ - For referee/external rooms, test viewer safe snapshots, authoritative results,
801
+ and game defined start/ready/late join rules against that authority.
764
802
  - Test the identical artifact in its standalone location and inside Bounty Board.
765
803
 
766
804
  ## Troubleshooting map
767
805
 
806
+ - BBArcade is not defined, script blocked with
807
+ ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep -> the
808
+ embedding page is cross-origin isolated -> the hosted file already sends CORP
809
+ cross-origin; if a proxy strips it, add crossorigin="anonymous" to the tag or
810
+ vendor the file into the build.
768
811
  - Boot waits forever -> game startup depends on an SDK promise -> start init
769
812
  fire-and-forget and give every promise feature a standalone outcome.
770
- - save/load unsupported -> not a Bounty-hosted upload -> use own same-origin store.
771
- - save/load unauthenticated -> guest player -> continue with defaults/fallback.
813
+ - save/load unsupported -> not a Bounty hosted upload -> use own same-origin store.
814
+ - save/load unauthenticated -> guest player -> continue with defaults/fallback,
815
+ and gate on getPlayer() so the guest case is a branch instead of a catch.
772
816
  - SecurityError touching localStorage/sessionStorage/document.cookie -> hosted
773
817
  builds run on an opaque origin -> use save()/load(), or storage.install() for
774
818
  an engine export that cannot be rewired.
775
819
  - Shimmed storage reads empty at boot -> the cloud read had not landed yet ->
776
820
  await BBArcade.storage.ready() before restoring progress.
777
- - storage.mode is 'memory' -> guest, standalone, or off-host -> storage works
778
- for the session but is never persisted; keep first-run defaults sane.
821
+ - storage.mode is 'memory' -> guest, standalone, or off host -> storage works
822
+ for the session but is never persisted; keep first run defaults sane.
779
823
  - direct_user_action_required -> show() lost browser activation -> make show()
780
824
  the first line of the click/tap handler.
781
825
  - host_disabled/unavailable -> host or inventory is not ready -> keep the normal
@@ -787,14 +831,15 @@ https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
787
831
  - trySend false -> disconnected/raced socket or local serialization/send failure
788
832
  -> stop sending, show reconnecting when disconnected, and resume only after
789
833
  connection.connected is true. A true result still does not prove acceptance.
790
- - match rejected on an external authority -> external matchmaking is authority-
834
+ - match rejected on an external authority -> external matchmaking is authority
791
835
  owned -> use code/create or that authority's separately documented flow.
792
836
 
793
837
  ## More
794
838
 
795
839
  - Human guide: https://www.bountyboard.gg/arcade/sdk
840
+ - npm package: https://www.npmjs.com/package/@bountyboard/arcade-sdk
796
841
  - Relay room guide: https://www.bountyboard.gg/arcade/sdk/relay-rooms.txt
797
842
  - Standalone types: https://www.bountyboard.gg/arcade-sdk.d.ts
798
843
  - Submit a game: https://www.bountyboard.gg/arcade/submit
799
- - Package README, changelog, AGENTS.md, design playbook, relay-room guide, and
800
- external-authority guide all ship in the npm package.
844
+ - Package README, changelog, AGENTS.md, design playbook, relay room guide, and
845
+ external authority guide all ship in the npm package.
@@ -1,12 +1,12 @@
1
1
  # Relay rooms
2
2
 
3
- Relay rooms are the zero-server-code multiplayer tier. Every game with SDK
4
- multiplayer approved gets them automatically: hosted lobbies, shareable invite
5
- codes, public quick match, reconnect handling, and message fan-out — without
6
- registering a server module or running any backend. The same `joinRoom()` API
7
- drives all tiers, so a game can start on relay rooms and graduate to a
8
- refereed module or an external authoritative server later without client
9
- rewrites.
3
+ Relay rooms are the multiplayer tier that needs no server code. Every game with
4
+ SDK multiplayer approved gets them automatically: hosted lobbies, shareable
5
+ invite codes, public quick match, reconnect handling, and message fan out,
6
+ without registering a server module or running any backend. The same
7
+ `joinRoom()` API drives all tiers, so a game can start on relay rooms and
8
+ graduate to a refereed module or an external authoritative server later without
9
+ client rewrites.
10
10
 
11
11
  ## What the server owns (and what it refuses to)
12
12
 
@@ -15,13 +15,13 @@ The relay server authoritatively owns everything it can own *generically*:
15
15
  - the roster and seat cap,
16
16
  - host designation and succession,
17
17
  - admission (signed tickets, room binding, per-seat connection caps),
18
- - message fan-out with rate and size guardrails.
18
+ - message fan out with rate and size guardrails.
19
19
 
20
20
  It deliberately does not interpret game payloads, so it cannot referee them.
21
- Relay outcomes are client-trusted: relay rooms never emit `match_end`, never
21
+ Relay outcomes are client trusted: relay rooms never emit `match_end`, never
22
22
  report results to Bounty Board, and never feed win/loss records or
23
- leaderboards. Games that need authoritative results — anything ranked,
24
- recorded, or reward-adjacent — must use a refereed Bounty-hosted module or a
23
+ leaderboards. Games that need authoritative results (anything ranked,
24
+ recorded, or reward adjacent) must use a refereed Bounty hosted module or a
25
25
  registered external authority instead.
26
26
 
27
27
  ## Joining
@@ -39,7 +39,7 @@ const room = await joinRoom({
39
39
  - `code` joins a friend's room.
40
40
  - `match: true` enters the game's open public room, or founds one.
41
41
 
42
- All three go through the standard signed-ticket flow; nothing about relay
42
+ All three go through the standard signed ticket flow; nothing about relay
43
43
  rooms weakens admission.
44
44
 
45
45
  ### Room size
@@ -52,7 +52,7 @@ next arrival re-founds it. Ship the same `roomSize` from every client of
52
52
  your game so matchmade rooms are founded consistently.
53
53
 
54
54
  Quick match fills rooms to the founded size. Because matchmaking reserves
55
- seats conservatively, a burst of simultaneous quick-match joins can briefly
55
+ seats conservatively, a burst of simultaneous quick match joins can briefly
56
56
  overshoot a small room; losers of that race receive `room_full` and the SDK
57
57
  automatically retries into the next room.
58
58
 
@@ -61,7 +61,7 @@ automatically retries into the next room.
61
61
  `room.send(payload)` relays `payload` to **every** player in the room,
62
62
  including the sender. There are no private messages: every client sees every
63
63
  payload, so never send secrets (hidden roles, private hands) through a relay
64
- room. Payload semantics are entirely yours — the server never reads them.
64
+ room. Payload semantics are entirely yours. The server never reads them.
65
65
 
66
66
  Messages are batched per server tick and delivered through `room.on('event')`:
67
67
 
@@ -78,7 +78,7 @@ room.on('event', event => {
78
78
  });
79
79
  ```
80
80
 
81
- Guardrails (over-limit traffic is dropped silently; the state snapshot's
81
+ Guardrails (traffic over the limit is dropped silently; the state snapshot's
82
82
  `dropped` counter is the debugging breadcrumb):
83
83
 
84
84
  | Limit | Value |
@@ -101,9 +101,9 @@ grace keeps both the seat and the host role, so a brief network blip does not
101
101
  thrash host succession.
102
102
 
103
103
  Use the host as your game's coordinator: it can own spawn timing, level
104
- seeds, or authoritative-enough game state for casual play. Remember the trust
105
- model — a modified client can lie, which is acceptable for casual co-op and
106
- couch-style games and not acceptable for anything with stakes.
104
+ seeds, or authoritative enough game state for casual play. Remember the trust
105
+ model. A modified client can lie, which is acceptable for casual co-op and
106
+ couch style games and not acceptable for anything with stakes.
107
107
 
108
108
  ## State snapshots
109
109
 
@@ -123,12 +123,12 @@ current.
123
123
  Relay rooms never settle: there is no `end` event, and a room lives while it
124
124
  has players (seats survive a 15-second reconnect grace). Implement your own
125
125
  notion of rounds or matches in game messages, and call `room.leave()` when
126
- the player exits — leaving is immediate and never reconnects.
126
+ the player exits. Leaving is immediate and never reconnects.
127
127
 
128
128
  ## Graduating to a refereed tier
129
129
 
130
- If your game outgrows client trust — ranked results, tournaments, anything
131
- paid — the ticket flow, room codes, and quick match all stay the same; the
130
+ If your game outgrows client trust (ranked results, tournaments, anything
131
+ paid), the ticket flow, room codes, and quick match all stay the same; the
132
132
  room's simulation moves server-side. Contact Bounty Board about a refereed
133
- Bounty-hosted module, or keep your own server and register it under the
133
+ Bounty hosted module, or keep your own server and register it under the
134
134
  external authority contract (see `external-authoritative-servers.md`).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bountyboard/arcade-sdk",
3
- "version": "1.3.0",
4
- "description": "Bounty Board Arcade SDK — leaderboards, cloud saves, rewarded ads, A/B variants, and multiplayer rooms for games on bountyboard.gg",
3
+ "version": "1.4.0",
4
+ "description": "Bounty Board Arcade SDK: leaderboards, cloud saves, rewarded ads, A/B variants, and multiplayer rooms for games on bountyboard.gg",
5
5
  "keywords": [
6
6
  "bountyboard",
7
7
  "arcade",