@bountyboard/arcade-sdk 1.0.1 → 1.2.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/AGENTS.md +10 -5
- package/CHANGELOG.md +68 -0
- package/README.md +331 -75
- package/dist/{chunk-OWOESH4X.js → chunk-KDBBR532.js} +51 -9
- package/dist/chunk-KDBBR532.js.map +1 -0
- package/dist/global-sdk.d.ts +66 -3
- package/dist/global.js +175 -26
- package/dist/index.cjs +50 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/multiplayer.cjs +175 -26
- package/dist/multiplayer.cjs.map +1 -1
- package/dist/multiplayer.d.cts +2 -2
- package/dist/multiplayer.d.ts +2 -2
- package/dist/multiplayer.js +126 -19
- package/dist/multiplayer.js.map +1 -1
- package/dist/{types-BKChtV3H.d.cts → types-dtsNXYZW.d.cts} +69 -3
- package/dist/{types-BKChtV3H.d.ts → types-dtsNXYZW.d.ts} +69 -3
- package/docs/external-authoritative-servers.md +265 -0
- package/docs/llms.txt +715 -0
- package/docs/relay-rooms.md +134 -0
- package/package.json +6 -3
- package/dist/chunk-OWOESH4X.js.map +0 -1
package/docs/llms.txt
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
# Bounty Board Arcade SDK — agent-readable integration contract
|
|
2
|
+
|
|
3
|
+
Human guide: https://www.bountyboard.gg/arcade/sdk
|
|
4
|
+
Package: @bountyboard/arcade-sdk
|
|
5
|
+
Stable npm release: 1.1.0
|
|
6
|
+
Current browser v1/source addition: public quick match (queued for npm 1.2.0)
|
|
7
|
+
Wire protocol: 1
|
|
8
|
+
|
|
9
|
+
This file is the complete integration contract for coding agents integrating an
|
|
10
|
+
HTML5 game. The package TypeScript declarations remain the exact public type
|
|
11
|
+
reference. Version availability matters: npm 1.1.0 supports code/create rooms
|
|
12
|
+
on all three approved rails: Bounty shared-service referee modules, Bounty
|
|
13
|
+
shared-service relay rooms, and host-routed registered external authorities.
|
|
14
|
+
match: true works on both Bounty shared-service tiers in the current
|
|
15
|
+
/arcade-sdk/v1.js browser artifact and repository source, and will enter the
|
|
16
|
+
npm package in 1.2.0. BBArcadeError.detail follows that same version gate. The
|
|
17
|
+
external-authority adapter guide is public now and will also join the packaged
|
|
18
|
+
docs in 1.2.0.
|
|
19
|
+
|
|
20
|
+
## Non-negotiable integration rules
|
|
21
|
+
|
|
22
|
+
1. The SDK is never load-bearing. A game must boot and remain fully playable
|
|
23
|
+
with no Bounty Board parent, no logged-in player, no ad inventory, no cloud
|
|
24
|
+
save, and no multiplayer authority.
|
|
25
|
+
2. Call gameOver() exactly once per run and use integer scores. The host/server
|
|
26
|
+
enforces per-game plausibility caps.
|
|
27
|
+
3. Bracket active play with gameplayStart()/gameplayStop(). Menus, pauses,
|
|
28
|
+
death prompts, and ad breaks are not active play.
|
|
29
|
+
4. getPlayer() can be null and avatarUrl can be null. The SDK never exposes
|
|
30
|
+
account ids, emails, or roles.
|
|
31
|
+
5. Store all progress in one save(string) blob (about 1 MB). Save at
|
|
32
|
+
checkpoints/game over, not in a frame loop, and catch every rejection.
|
|
33
|
+
6. For new rewarded-ad work, prepare first, enable the game's button only when
|
|
34
|
+
status is ready, call prepared.show() directly from the click/tap handler,
|
|
35
|
+
and grant only when the final status is viewed.
|
|
36
|
+
7. Multiplayer has three rails. Referee modules and external authorities own
|
|
37
|
+
simulation/results and receive client inputs. The casual relay owns signed
|
|
38
|
+
admission, roster, capacity, host succession, reconnect grace, and public
|
|
39
|
+
message fan-out, but it does not referee game state or outcomes. Catch every
|
|
40
|
+
joinRoom() rejection and keep solo/standalone play available.
|
|
41
|
+
8. lockToHost() is an optional anti-rehosting deterrent, not DRM. Call it before boot.
|
|
42
|
+
|
|
43
|
+
## Install and distribution
|
|
44
|
+
|
|
45
|
+
Preferred for games with a build step:
|
|
46
|
+
|
|
47
|
+
npm install @bountyboard/arcade-sdk
|
|
48
|
+
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
49
|
+
|
|
50
|
+
The package is zero-dependency, typed, ESM + CommonJS, and SSR-safe. Importing
|
|
51
|
+
the module does not install a window.BBArcade global.
|
|
52
|
+
|
|
53
|
+
No-build script tag:
|
|
54
|
+
|
|
55
|
+
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
|
|
56
|
+
|
|
57
|
+
This installs window.BBArcade. Standalone declarations:
|
|
58
|
+
https://www.bountyboard.gg/arcade-sdk.d.ts
|
|
59
|
+
|
|
60
|
+
Do not mix npm and the script tag in one page. A bundler that specifically
|
|
61
|
+
wants the global may import @bountyboard/arcade-sdk/global.
|
|
62
|
+
|
|
63
|
+
Multiplayer module import:
|
|
64
|
+
|
|
65
|
+
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
|
|
66
|
+
|
|
67
|
+
Script-tag multiplayer is BBArcade.multiplayer.joinRoom(...).
|
|
68
|
+
|
|
69
|
+
Public package exports:
|
|
70
|
+
|
|
71
|
+
- @bountyboard/arcade-sdk: default BBArcade, named BBArcade,
|
|
72
|
+
PROTOCOL_VERSION, and the core public types.
|
|
73
|
+
- @bountyboard/arcade-sdk/multiplayer: joinRoom, multiplayer, and all BBArcadeMp*
|
|
74
|
+
types.
|
|
75
|
+
- @bountyboard/arcade-sdk/global: installs window.BBArcade for bundlers that
|
|
76
|
+
explicitly need the global. The normal module import does not install it.
|
|
77
|
+
|
|
78
|
+
## Runtime capability matrix
|
|
79
|
+
|
|
80
|
+
Distribution format does not determine capabilities; the embedding host does.
|
|
81
|
+
|
|
82
|
+
- Bounty-hosted upload:
|
|
83
|
+
lifecycle/scores supported; player identity/variants supported; cloud
|
|
84
|
+
save/load available to logged-in players; ads available to approved games;
|
|
85
|
+
multiplayer available after per-game approval, with relay as the default
|
|
86
|
+
Bounty shared-service tier when no bespoke referee module is registered.
|
|
87
|
+
- Approved external URL embed inside the Bounty Board player:
|
|
88
|
+
lifecycle/scores, identity, variants, and approved multiplayer (including the
|
|
89
|
+
Bounty relay tier) are supported; rewarded ads are unavailable (no payable
|
|
90
|
+
per-game attribution yet); cloud save/load is unsupported.
|
|
91
|
+
- Standalone or opened directly on the game's own site:
|
|
92
|
+
fire-and-forget calls no-op; init resolves immediately; getPlayer returns
|
|
93
|
+
null; getVariant returns the alphabetical control; host-only promise APIs
|
|
94
|
+
reject unsupported or return an unavailable outcome. An unanswered non-Bounty
|
|
95
|
+
embed uses an approximately 1.5-second init/getPlayer grace instead.
|
|
96
|
+
|
|
97
|
+
Guests are normal. getPlayer resolves null and account-backed calls may reject
|
|
98
|
+
with code unauthenticated.
|
|
99
|
+
|
|
100
|
+
## Minimal correct lifecycle
|
|
101
|
+
|
|
102
|
+
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
103
|
+
|
|
104
|
+
BBArcade.lockToHost({ allow: ['play.yourgame.com'] }); // optional, before boot
|
|
105
|
+
void BBArcade.init(); // never gate boot on it
|
|
106
|
+
BBArcade.gameLoadingFinished(); // first playable scene ready
|
|
107
|
+
|
|
108
|
+
BBArcade.gameplayStart();
|
|
109
|
+
BBArcade.submitScore(Math.trunc(score)); // may repeat; host throttles
|
|
110
|
+
BBArcade.gameplayStop(); // pause/death/menu/ad
|
|
111
|
+
BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run
|
|
112
|
+
|
|
113
|
+
For a shared-seed Daily, pass { mode: 'daily' } to submitScore and gameOver.
|
|
114
|
+
|
|
115
|
+
## Exact shared types and configuration
|
|
116
|
+
|
|
117
|
+
All rejected SDK promises use a real Error. Stable npm 1.1.0 has this shape:
|
|
118
|
+
|
|
119
|
+
type BBArcadeErrorCode =
|
|
120
|
+
| 'unsupported' // no compatible host/capability
|
|
121
|
+
| 'unauthenticated' // login/ticket identity required or invalid
|
|
122
|
+
| 'too_large' // save blob exceeds the cap
|
|
123
|
+
| 'rejected' // host/authority refused the payload or room
|
|
124
|
+
| 'error'; // timeout, transport, or server failure
|
|
125
|
+
|
|
126
|
+
interface BBArcadeError extends Error {
|
|
127
|
+
code: BBArcadeErrorCode;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
The current browser v1/source build, and npm 1.2.0 when released, add:
|
|
131
|
+
|
|
132
|
+
interface BBArcadeError extends Error {
|
|
133
|
+
code: BBArcadeErrorCode;
|
|
134
|
+
detail?: string; // raw authority reason when available
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Core configuration shapes:
|
|
138
|
+
|
|
139
|
+
interface BBArcadeRewardedAdsConfig {
|
|
140
|
+
enabled?: boolean; // default true; host approval still wins
|
|
141
|
+
adChannel?: string; // legacy; ignored for Bounty attribution
|
|
142
|
+
testMode?: boolean; // defaults true on localhost/file:
|
|
143
|
+
preload?: boolean; // default false
|
|
144
|
+
loadTimeoutMs?: number; // default 6000, maximum 15000
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface BBArcadeConfig {
|
|
148
|
+
rewardedAds?: BBArcadeRewardedAdsConfig;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
interface BBArcadeLockToHostOptions {
|
|
152
|
+
allow?: string[]; // hosts/host:port/full URLs; subdomains match
|
|
153
|
+
signed?: boolean;
|
|
154
|
+
onBlocked?: () => void;
|
|
155
|
+
redirect?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lockToHost defaults allow bountyboard.gg and its subdomains, the fixed Bounty
|
|
159
|
+
staging host, localhost, and 127.0.0.1. allow extends rather than replaces that
|
|
160
|
+
list. signed mode requests an ECDSA origin attestation; it falls back to the
|
|
161
|
+
best-effort browser-origin check only when the host reports no signing key or
|
|
162
|
+
Web Crypto is unavailable. A missing host or present-but-invalid attestation
|
|
163
|
+
blocks.
|
|
164
|
+
|
|
165
|
+
Player and score shapes:
|
|
166
|
+
|
|
167
|
+
interface BBArcadePlayer { name: string; avatarUrl: string | null }
|
|
168
|
+
// submitScore/gameOver use the inline option shape { mode?: 'daily' }.
|
|
169
|
+
|
|
170
|
+
Rewarded-ad options and results:
|
|
171
|
+
|
|
172
|
+
interface BBArcadeRewardedAdOptions {
|
|
173
|
+
placement?: string; name?: string; reward?: string; adBreakId?: string;
|
|
174
|
+
size?: 'small' | 'medium' | 'large'; // legacy SDK analytics only
|
|
175
|
+
adChannel?: string; testMode?: boolean; loadTimeoutMs?: number;
|
|
176
|
+
beforeReward?: (showAd: () => void) => void; // low-level API only
|
|
177
|
+
adViewed?: (result: BBArcadeRewardedAdResult) => void;
|
|
178
|
+
adDismissed?: (result: BBArcadeRewardedAdResult) => void;
|
|
179
|
+
adBreakDone?: (placementInfo: unknown) => void;
|
|
180
|
+
onReward?: (result: BBArcadeRewardedAdResult) => void; // adViewed alias
|
|
181
|
+
onDismissed?: (result: BBArcadeRewardedAdResult) => void; // dismissed alias
|
|
182
|
+
onDone?: (result: BBArcadeRewardedAdResult) => void;
|
|
183
|
+
onError?: (result: BBArcadeRewardedAdResult) => void;
|
|
184
|
+
onUnavailable?: (result: BBArcadeRewardedAdResult) => void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
type BBArcadePrepareRewardedAdOptions =
|
|
188
|
+
Omit<BBArcadeRewardedAdOptions, 'beforeReward'> & {
|
|
189
|
+
onStart?: () => void;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
type BBArcadeRewardedAdStatus =
|
|
193
|
+
'viewed' | 'dismissed' | 'ready' | 'unavailable' | 'error';
|
|
194
|
+
|
|
195
|
+
interface BBArcadeRewardedAdResult {
|
|
196
|
+
status: BBArcadeRewardedAdStatus;
|
|
197
|
+
placement: string; reward: string; adBreakId: string;
|
|
198
|
+
size?: 'small' | 'medium' | 'large';
|
|
199
|
+
error?: string; breakStatus?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface BBArcadePreparedRewardedAd {
|
|
203
|
+
status: 'ready'; placement: string; reward: string; adBreakId: string;
|
|
204
|
+
size?: 'small' | 'medium' | 'large';
|
|
205
|
+
show(): Promise<BBArcadeRewardedAdResult>; // one-shot
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type BBArcadeRewardedAdPrepareFailure = BBArcadeRewardedAdResult & {
|
|
209
|
+
status: 'unavailable' | 'error';
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
type BBArcadeRewardedAdPreparation =
|
|
213
|
+
| BBArcadePreparedRewardedAd
|
|
214
|
+
| BBArcadeRewardedAdPrepareFailure;
|
|
215
|
+
|
|
216
|
+
interface BBArcadeRewardedBreakOptions extends BBArcadeRewardedAdOptions {
|
|
217
|
+
onStart?: () => void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Defaults are placement 'rewarded' and reward 'reward'; ids are generated unless
|
|
221
|
+
supplied. BBArcadePreparedRewardedAd.show is one-shot; repeated calls share its
|
|
222
|
+
final promise.
|
|
223
|
+
The Bounty host's ad config overrides per-call adChannel/testMode when present.
|
|
224
|
+
|
|
225
|
+
## API surface
|
|
226
|
+
|
|
227
|
+
BBArcadeSDK is the aggregate interface implemented by the named/default
|
|
228
|
+
BBArcade export and window.BBArcade. Its methods are all listed below. The
|
|
229
|
+
interface also has version: number and multiplayer?: BBArcadeMultiplayer;
|
|
230
|
+
multiplayer is installed by the script/global build, while normal module users
|
|
231
|
+
import the dedicated multiplayer subpath.
|
|
232
|
+
|
|
233
|
+
Lifecycle and scoring:
|
|
234
|
+
|
|
235
|
+
- lockToHost({ allow?: string[], signed?: boolean, onBlocked?, redirect? }): void
|
|
236
|
+
Early anti-rehosting check. Defaults allow Bounty Board hosts and localhost.
|
|
237
|
+
When the embedder cannot be read from browser signals (opaque-origin
|
|
238
|
+
sandbox), the SDK asks the embedding page and blocks if nothing answers
|
|
239
|
+
within about 5 seconds.
|
|
240
|
+
- init({ rewardedAds? }?): Promise<void>
|
|
241
|
+
Announces the game and receives host config. Resolves when answered,
|
|
242
|
+
immediately when top-level/standalone, or after about 1.5 seconds in an
|
|
243
|
+
unanswered embed; awaiting is optional and must not gate boot.
|
|
244
|
+
- configure(options?): void
|
|
245
|
+
Applies the same module configuration without another host handshake.
|
|
246
|
+
- ready() / gameLoadingFinished(): void
|
|
247
|
+
Signals that the game is loaded and the player can start.
|
|
248
|
+
- gameplayStart() / gameplayStop(): void
|
|
249
|
+
Brackets active play.
|
|
250
|
+
- submitScore(score: number, { mode?: 'daily' }?): void
|
|
251
|
+
Current integer score; transport is throttled by the host. Fractional
|
|
252
|
+
values are truncated toward zero (Math.trunc semantics).
|
|
253
|
+
- gameOver(score: number, { mode?: 'daily' }?): void
|
|
254
|
+
Final integer score; call exactly once per run. Same truncation as
|
|
255
|
+
submitScore.
|
|
256
|
+
- xrSessionStart() / xrSessionEnd(): void
|
|
257
|
+
Brackets immersive WebXR play.
|
|
258
|
+
|
|
259
|
+
Player data and experiments:
|
|
260
|
+
|
|
261
|
+
- save(blob: string): Promise<void>
|
|
262
|
+
One blob, about 1 MB. Requires a Bounty-hosted upload and logged-in player.
|
|
263
|
+
Oversized blobs reject too_large immediately via a client-side precheck
|
|
264
|
+
against the same 1 MiB cap the server enforces.
|
|
265
|
+
- load(): Promise<string | null>
|
|
266
|
+
Returns null when no save exists. Rejects like save().
|
|
267
|
+
- getPlayer(): Promise<{ name: string, avatarUrl: string | null } | null>
|
|
268
|
+
Display identity only; always handle null.
|
|
269
|
+
- onPlayerChange(handler): () => void // current browser/source; npm 1.2.0 when released
|
|
270
|
+
Subscribes to CHANGES in the display identity (mid-session login/logout).
|
|
271
|
+
The handler receives the same { name, avatarUrl } | null shape as
|
|
272
|
+
getPlayer() and runs only when the identity actually changes; subscribing
|
|
273
|
+
does not replay the current value. Returns an unsubscribe function.
|
|
274
|
+
- getVariant(key: string, variants: string[]): Promise<string | null>
|
|
275
|
+
Deterministic even split per player+game+key. Alphabetical first is the
|
|
276
|
+
standalone/error control. At least 2 variants are needed for a host
|
|
277
|
+
assignment; an empty list resolves null and a one-item list resolves that
|
|
278
|
+
item. Do not re-randomize client-side.
|
|
279
|
+
|
|
280
|
+
save/load can reject with every BBArcadeErrorCode. load resolves null only when
|
|
281
|
+
the logged-in hosted player has no save; a guest can reject unauthenticated.
|
|
282
|
+
Every host request (save, load, variant, multiplayer ticket) rejects with code
|
|
283
|
+
error after a 15-second timeout when no answer arrives.
|
|
284
|
+
|
|
285
|
+
unsupported | unauthenticated | too_large | rejected | error
|
|
286
|
+
|
|
287
|
+
Rewarded ads:
|
|
288
|
+
|
|
289
|
+
- prepareRewardedAd(options?): Promise<BBArcadeRewardedAdPreparation>
|
|
290
|
+
Recommended. Alias: prepareRewardedBreak(). Prepared show() is one-shot.
|
|
291
|
+
- rewardedAd(options?): Promise<BBArcadeRewardedAdResult>
|
|
292
|
+
Low-level structured-result API. Alias: showRewardedAd().
|
|
293
|
+
- rewardedBreak(options | onStart): Promise<boolean>
|
|
294
|
+
Deprecated compatibility helper. New games must use the prepared flow.
|
|
295
|
+
- preloadRewardedAds(options?): Promise<boolean>
|
|
296
|
+
Warms the ads library. Alias: preloadRewardedAd().
|
|
297
|
+
|
|
298
|
+
Rewarded result status:
|
|
299
|
+
|
|
300
|
+
viewed | dismissed | ready | unavailable | error
|
|
301
|
+
|
|
302
|
+
Only final viewed grants. A prepared ad whose break ends before show() is ever
|
|
303
|
+
called resolves dismissed (never the non-terminal ready); ready survives as a
|
|
304
|
+
terminal state only in the legacy edge where the ad WAS shown but Google sent
|
|
305
|
+
no view/dismiss callback. Useful error detail includes host_disabled,
|
|
306
|
+
ad_break_unavailable, ad_break_timeout, no_rewarded_ad, ad_in_progress,
|
|
307
|
+
show_ad_error, direct_user_action_required, before_reward_error, and
|
|
308
|
+
ad_break_error.
|
|
309
|
+
|
|
310
|
+
Multiplayer (current browser v1/source shape; stable npm 1.1.0 omits match):
|
|
311
|
+
|
|
312
|
+
type BBArcadeMpJoinOptions = {
|
|
313
|
+
code?: string;
|
|
314
|
+
create?: boolean;
|
|
315
|
+
match?: boolean; // current browser/source; npm 1.2.0 when released
|
|
316
|
+
joinData?: Readonly<Record<string, unknown>>;
|
|
317
|
+
roomUrl?: string; // local development override; provide with ticket
|
|
318
|
+
ticket?: string; // local development override; provide with roomUrl
|
|
319
|
+
timeoutMs?: number; // current browser/source; npm 1.2.0 when released
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
interface BBArcadeMpPlayer {
|
|
323
|
+
id: string; // room-scoped, never a Bounty account id
|
|
324
|
+
name: string;
|
|
325
|
+
avatarUrl: string | null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface BBArcadeMpResult {
|
|
329
|
+
playerId: string;
|
|
330
|
+
name: string;
|
|
331
|
+
placement: number;
|
|
332
|
+
score: number;
|
|
333
|
+
outcome: 'win' | 'loss' | 'draw';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface BBArcadeMpRoomEvents {
|
|
337
|
+
snapshot: { tick: number; state: unknown };
|
|
338
|
+
playerJoin: { player: BBArcadeMpPlayer };
|
|
339
|
+
playerLeave: { playerId: string };
|
|
340
|
+
event: unknown;
|
|
341
|
+
end: { results: BBArcadeMpResult[] };
|
|
342
|
+
error: { code: string };
|
|
343
|
+
connection: { connected: boolean; reconnecting: boolean };
|
|
344
|
+
close: { reconnecting: boolean };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface BBArcadeMpRoom {
|
|
348
|
+
code: string;
|
|
349
|
+
playerId: string;
|
|
350
|
+
players: BBArcadeMpPlayer[];
|
|
351
|
+
state: unknown;
|
|
352
|
+
connected: boolean;
|
|
353
|
+
latencyMs: number | null; // current browser/source; npm 1.2.0 when released
|
|
354
|
+
send(input: unknown): void;
|
|
355
|
+
trySend(input: unknown): boolean;
|
|
356
|
+
on<K extends keyof BBArcadeMpRoomEvents>(
|
|
357
|
+
event: K,
|
|
358
|
+
handler: (data: BBArcadeMpRoomEvents[K]) => void
|
|
359
|
+
): () => void;
|
|
360
|
+
leave(): void;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
interface BBArcadeMultiplayer {
|
|
364
|
+
joinRoom(options?: BBArcadeMpJoinOptions): Promise<BBArcadeMpRoom>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
Relay runtime payloads use these exact shapes through the otherwise-unknown
|
|
368
|
+
room.state and room.on('event') values:
|
|
369
|
+
|
|
370
|
+
interface BBArcadeRelayState {
|
|
371
|
+
mode: 'relay';
|
|
372
|
+
hostId: string | null;
|
|
373
|
+
size: number;
|
|
374
|
+
dropped: number;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
type BBArcadeRelayEvent =
|
|
378
|
+
| {
|
|
379
|
+
type: 'relay';
|
|
380
|
+
messages: Array<{ from: string; data: unknown }>;
|
|
381
|
+
}
|
|
382
|
+
| { type: 'relay_host'; hostId: string | null };
|
|
383
|
+
|
|
384
|
+
BBArcadeRelayState and BBArcadeRelayEvent are documentation names, not package
|
|
385
|
+
exports; the public SDK intentionally types game/tier-defined state and events
|
|
386
|
+
as unknown.
|
|
387
|
+
|
|
388
|
+
- joinRoom(options?): Promise<BBArcadeMpRoom>
|
|
389
|
+
No options and { create: true } both generate a 4-character invite code from
|
|
390
|
+
ABCDEFGHJKLMNPQRSTUVWXYZ23456789. A supplied code is uppercased. match: true
|
|
391
|
+
is Bounty-hosted public quick match and excludes code, create, roomUrl, and
|
|
392
|
+
ticket. roomUrl+ticket bypass the host handshake for local development only;
|
|
393
|
+
provide both (a lone value is not an override).
|
|
394
|
+
- joinData must be a non-null, non-array JSON-serializable object whose UTF-8
|
|
395
|
+
JSON encoding is at most 1 KiB. Cycles, arrays, primitives, and oversized data
|
|
396
|
+
reject with code rejected. Never include credentials or secrets.
|
|
397
|
+
- joinRoom resolves only after the authority sends welcome (10-second default
|
|
398
|
+
welcome timeout; timeoutMs overrides it, clamped to 1000–60000 ms, and
|
|
399
|
+
applies to every automatic reconnect attempt too).
|
|
400
|
+
At resolution, code/playerId/players/state/connected already hold the initial
|
|
401
|
+
lobby state. There is no replayed initial snapshot: render those fields first,
|
|
402
|
+
then subscribe to future events.
|
|
403
|
+
- room.latencyMs reports the join-handshake latency in ms (socket open to
|
|
404
|
+
server welcome: ticket verification plus one round trip), refreshed on every
|
|
405
|
+
successful (re)connect and null until the first welcome. Use it to tune
|
|
406
|
+
render-side smoothing; it is an estimate, not a measured RTT.
|
|
407
|
+
- Values passed to send/trySend must be JSON-serializable. A referee room
|
|
408
|
+
treats the value as an input; a relay room treats it as a public game
|
|
409
|
+
message. send discards local write status. trySend
|
|
410
|
+
returns true only when JSON was written to an open socket; it does NOT mean
|
|
411
|
+
the authority accepted the input. false means disconnected, raced closed, or
|
|
412
|
+
serialization/WebSocket.send failed. The authority still validates,
|
|
413
|
+
sequences, and may rate-drop inputs.
|
|
414
|
+
- on returns an unsubscribe function. The SDK updates room.players before
|
|
415
|
+
playerJoin/playerLeave handlers and room.state before snapshot handlers.
|
|
416
|
+
- leave intentionally closes the Room and permanently disables reconnect for it.
|
|
417
|
+
A welcome that arrives after leave() (e.g. racing a reconnect) is ignored and
|
|
418
|
+
never flips the room back to connected.
|
|
419
|
+
- A pre-welcome room error rejects joinRoom with BBArcadeError. In the current
|
|
420
|
+
browser/source build and npm 1.2.0+, raw reasons such as room_full/room_over
|
|
421
|
+
are carried in error.detail when available. Stable npm 1.1.0 exposes only
|
|
422
|
+
error.code. An error after welcome emits room.on('error', { code }).
|
|
423
|
+
Connection loss emits connection/close events and may start automatic
|
|
424
|
+
reconnect.
|
|
425
|
+
|
|
426
|
+
BBArcade.version and the exported PROTOCOL_VERSION are the core bb-arcade
|
|
427
|
+
postMessage wire-protocol version. They are not npm semver and are not a
|
|
428
|
+
version field on multiplayer WebSocket frames.
|
|
429
|
+
|
|
430
|
+
## Cloud-save recipe
|
|
431
|
+
|
|
432
|
+
Hosted uploads have an opaque origin and no localStorage, so SDK save is the
|
|
433
|
+
primary store there. URL embeds and standalone builds need their own same-origin
|
|
434
|
+
fallback.
|
|
435
|
+
|
|
436
|
+
const blob = JSON.stringify(progress);
|
|
437
|
+
try {
|
|
438
|
+
await BBArcade.save(blob);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error.code === 'unsupported') localStorage.setItem('progress', blob);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const blob = await BBArcade.load();
|
|
445
|
+
restore(blob ? JSON.parse(blob) : defaults);
|
|
446
|
+
} catch {
|
|
447
|
+
restoreStandaloneProgress();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
Do not assume load() resolves null for a guest; it can reject unauthenticated.
|
|
451
|
+
|
|
452
|
+
## Safe rewarded-ad recipe
|
|
453
|
+
|
|
454
|
+
Prepare at the natural break. Keep the game-owned button disabled until ready.
|
|
455
|
+
Call show() as the first operation in the direct click/tap handler: no await,
|
|
456
|
+
timer, microtask, animation, state transition, or network call before it.
|
|
457
|
+
|
|
458
|
+
const prepared = await BBArcade.prepareRewardedAd({
|
|
459
|
+
placement: 'death_revive',
|
|
460
|
+
reward: 'extra_life',
|
|
461
|
+
onStart: () => pauseGameAndAudio(),
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (prepared.status === 'ready') {
|
|
465
|
+
button.disabled = false;
|
|
466
|
+
button.addEventListener('click', () => {
|
|
467
|
+
const resultPromise = prepared.show(); // keep first
|
|
468
|
+
button.disabled = true;
|
|
469
|
+
void resultPromise.then(result => {
|
|
470
|
+
resumeGameAndAudio();
|
|
471
|
+
if (result.status === 'viewed') revivePlayer();
|
|
472
|
+
else keepNormalFallbackAvailable();
|
|
473
|
+
});
|
|
474
|
+
}, { once: true });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
Never auto-trigger, loop, or auto-retry. Unavailable ad inventory on localhost
|
|
478
|
+
is normal even though test mode is enabled automatically.
|
|
479
|
+
|
|
480
|
+
## Multiplayer room and lobby contract
|
|
481
|
+
|
|
482
|
+
import type { BBArcadeError } from '@bountyboard/arcade-sdk';
|
|
483
|
+
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const room = await joinRoom({
|
|
487
|
+
create: true, // or code: 'ABCD'; browser v1/npm 1.2 adds match: true
|
|
488
|
+
// Only a relay founder uses roomSize; every joinData field is untrusted.
|
|
489
|
+
joinData: { roomSize: 4, avatar: 'golem' },
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// welcome is already reflected here; no initial snapshot event is replayed.
|
|
493
|
+
renderLobby(room.code, room.players, room.state);
|
|
494
|
+
room.on('playerJoin', () => renderPlayers(room.players));
|
|
495
|
+
room.on('playerLeave', () => renderPlayers(room.players));
|
|
496
|
+
room.on('snapshot', ({ tick, state }) => renderRoomState(tick, state));
|
|
497
|
+
room.on('event', event => handleRoomEvent(event));
|
|
498
|
+
room.on('connection', ({ connected, reconnecting }) =>
|
|
499
|
+
setConnectionState({ connected, reconnecting })
|
|
500
|
+
);
|
|
501
|
+
let ended = false;
|
|
502
|
+
room.on('close', ({ reconnecting }) => {
|
|
503
|
+
if (!reconnecting && !ended) showConnectionClosed();
|
|
504
|
+
});
|
|
505
|
+
room.on('end', ({ results }) => {
|
|
506
|
+
ended = true;
|
|
507
|
+
showResults(results); // finite refereed rooms; relay never emits end
|
|
508
|
+
});
|
|
509
|
+
if (!room.trySend(input)) {
|
|
510
|
+
if (!room.connected) showReconnecting(true);
|
|
511
|
+
else showSendFailure(); // serialization/WebSocket.send failed locally
|
|
512
|
+
}
|
|
513
|
+
onExit(() => room.leave());
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const { code } = error as BBArcadeError;
|
|
516
|
+
showSoloFallback(code);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
The SDK is room transport, not a universal lobby rules engine. Bounty Board
|
|
520
|
+
selects exactly one tier for the approved game slug: built-in relay, bespoke
|
|
521
|
+
Bounty referee module, or registered external authority. The signed ticket
|
|
522
|
+
binds that tier. joinData cannot select or downgrade it; a ticket/registry
|
|
523
|
+
mismatch fails closed, and a registered external slug never falls back to
|
|
524
|
+
relay. All tiers use the same joinRoom transport, room codes, and client
|
|
525
|
+
connection lifecycle; tier/game-specific state and event schemas still differ.
|
|
526
|
+
|
|
527
|
+
create/code/match select a room. Relay supplies the host contract below, but
|
|
528
|
+
games build any ready/team/start/kick/rematch protocol on top and those rules
|
|
529
|
+
remain client-trusted. Referee modules and external authorities define their
|
|
530
|
+
own state, input, event, spectator, lobby, and rematch schemas. A module's
|
|
531
|
+
minPlayers does not automatically gate simulation or start a match. Quick match
|
|
532
|
+
may place a player into a joinable room already in progress; late-join behavior
|
|
533
|
+
is tier/game-defined.
|
|
534
|
+
|
|
535
|
+
joinData is always untrusted JSON. The SDK only validates its shape and 1 KiB
|
|
536
|
+
cap. A relay reads roomSize only from the founding seat. Referee modules and
|
|
537
|
+
external authorities validate any game-specific admission fields themselves.
|
|
538
|
+
|
|
539
|
+
### Built-in Bounty relay tier
|
|
540
|
+
|
|
541
|
+
The relay is the default Bounty shared-service tier for a multiplayer-approved
|
|
542
|
+
slug that has no bespoke referee module and is not routed to an external
|
|
543
|
+
authority. It is a casual, zero-server-code lobby—not an authoritative game
|
|
544
|
+
simulation or anti-cheat boundary.
|
|
545
|
+
|
|
546
|
+
- The first admitted seat fixes capacity from joinData.roomSize. It must be an
|
|
547
|
+
integer from 2 through 64; missing, invalid, or out-of-range means 8. The room
|
|
548
|
+
may operate with one current occupant. Later joiners cannot change size. Once
|
|
549
|
+
the room is completely empty, the next founder may choose it again. Ship the
|
|
550
|
+
same roomSize from every client so quick-matched rooms found consistently.
|
|
551
|
+
- Welcome and later 1 Hz metadata snapshots are identical for every viewer and
|
|
552
|
+
expose exactly
|
|
553
|
+
{ mode: 'relay', hostId: string | null, size: number, dropped: number }.
|
|
554
|
+
- The oldest retained seat is host. A transient disconnect preserves its seat
|
|
555
|
+
and hostId through the 15-second grace. After the host actually leaves or its
|
|
556
|
+
seat expires, the next-oldest retained seat becomes host and live clients get
|
|
557
|
+
{ type: 'relay_host', hostId } through room.on('event'). The founding welcome
|
|
558
|
+
already carries hostId, so no initial relay_host event is required.
|
|
559
|
+
- room.send(data) and room.trySend(data) publicly fan the JSON payload to EVERY
|
|
560
|
+
player, including the sender. There are no private messages, hidden state, or
|
|
561
|
+
viewer filtering. Batches arrive at 20 Hz through room.on('event') as
|
|
562
|
+
{ type: 'relay', messages: [{ from, data }] }, where from is a room-scoped
|
|
563
|
+
player id. Tick batching adds at most about 50 ms before network latency.
|
|
564
|
+
- Each relay payload's UTF-8 JSON encoding is capped at 1024 bytes. The relay
|
|
565
|
+
accepts at most 15 messages per player per second and 120 per room per second.
|
|
566
|
+
Byte/rate excess is silently dropped and increments cumulative state.dropped,
|
|
567
|
+
visible on a later metadata snapshot. state.dropped does not count malformed,
|
|
568
|
+
stale-sequence, or outer-envelope drops.
|
|
569
|
+
- The common outer room guard still caps a client frame at 4096 characters and
|
|
570
|
+
90 accepted envelopes per player per second; server frames are capped at
|
|
571
|
+
256 KiB.
|
|
572
|
+
- Relay rooms are endless. They never emit match_end/room.on('end'), never close
|
|
573
|
+
because a game outcome was claimed, and never report results to Bounty Board.
|
|
574
|
+
All game rules and outcomes are client-trusted; do not use relay claims for
|
|
575
|
+
trusted rewards, standings, or anti-cheat decisions. Implement round boundaries
|
|
576
|
+
in game messages and call room.leave() when the player exits.
|
|
577
|
+
|
|
578
|
+
### Refereed Bounty modules and external authorities
|
|
579
|
+
|
|
580
|
+
A referee validates every input, runs the only game simulation, clears held
|
|
581
|
+
controls on disconnect when its game requires it, and sends viewer-safe state.
|
|
582
|
+
Clients never report authoritative results. A finite referee may emit one
|
|
583
|
+
server-authored match_end; the SDK treats it as terminal and does not reconnect.
|
|
584
|
+
There is no SDK reset/rematch method. Endless authorities do not fabricate
|
|
585
|
+
match_end. Registered external authorities define their own socket shutdown and
|
|
586
|
+
subsequent-join behavior.
|
|
587
|
+
|
|
588
|
+
Bounty referee modules declare integer min/max players with
|
|
589
|
+
1 <= min <= max <= 64. tickHz is 1-60. snapshotHz may be lower, from 1 through
|
|
590
|
+
tickHz; not every simulation tick emits a snapshot. Simulation-backed sync has
|
|
591
|
+
a full network round trip, and the SDK provides no client-side prediction,
|
|
592
|
+
interpolation, or rollback. Expect roughly 50-150 ms input-to-snapshot latency
|
|
593
|
+
on real connections; smooth locally, then correct to the next viewer-safe
|
|
594
|
+
snapshot. Relay game payloads instead arrive in the public 20 Hz event batches
|
|
595
|
+
described above; its 1 Hz snapshot contains metadata only.
|
|
596
|
+
|
|
597
|
+
### Common Bounty shared-service room behavior
|
|
598
|
+
|
|
599
|
+
- Generated rooms use four-character invite codes. match: true works for relay
|
|
600
|
+
and referee rooms: it uses the current public room while live occupancy plus
|
|
601
|
+
conservative 15-second reservations show a safe seat. Full/ended rooms,
|
|
602
|
+
failed occupancy probes, or reservations roll a new room, so live occupancy
|
|
603
|
+
can roll before reaching the room's capacity. A matched room.code is also an
|
|
604
|
+
invite code.
|
|
605
|
+
- A lost last-seat/ended-room race triggers at most 2 re-matchmaking retries
|
|
606
|
+
(3 total attempts). In the current browser/source build and npm 1.2.0+, final
|
|
607
|
+
rejection has code rejected and detail room_full or room_over, whichever the
|
|
608
|
+
last attempt returned.
|
|
609
|
+
- When a finite Bounty referee module ends, it broadcasts match_end once, closes
|
|
610
|
+
its sockets, and subsequent joins to that room reject with room_over. This
|
|
611
|
+
finite-close rule does not apply to the endless relay tier.
|
|
612
|
+
- Disconnected seats are reserved for 15 seconds. The SDK obtains fresh tickets
|
|
613
|
+
and makes up to 3 reconnect attempts. During grace, the player remains in
|
|
614
|
+
room.players and playerLeave is delayed until the seat expires. room.leave()
|
|
615
|
+
disables client reconnect, but other players still observe leave after the
|
|
616
|
+
server processes the closed seat/grace.
|
|
617
|
+
- At most 2 concurrent sockets may occupy one seat. An excess connection is a
|
|
618
|
+
terminal generic admission rejection; the public SDK does not expose the
|
|
619
|
+
WebSocket close code/reason, so do not label every rejected error as this case.
|
|
620
|
+
|
|
621
|
+
Public quick match is available in the current browser v1 artifact/source and
|
|
622
|
+
is queued for npm 1.2.0. npm 1.1.0 consumers must use create/code. Quick match
|
|
623
|
+
is unavailable on registered external authorities, which own their capacity,
|
|
624
|
+
lobby lifecycle, and any matchmaking. Their reviewed limits can differ from
|
|
625
|
+
the Bounty shared-service defaults, and an external authority may be endless.
|
|
626
|
+
|
|
627
|
+
joinRoom rejection map:
|
|
628
|
+
|
|
629
|
+
- unsupported: SSR/no WebSocket, standalone play, or the room rail is not
|
|
630
|
+
configured (including a temporary 503 from the shared service).
|
|
631
|
+
- rejected: invalid option combinations, joinData, room code/session, disabled
|
|
632
|
+
approval, rate limiting, unsupported external matchmake, or authority refusal.
|
|
633
|
+
- unauthenticated: the room server rejected the ticket before welcome.
|
|
634
|
+
- error: malformed host grant, postMessage/socket transport failure, server
|
|
635
|
+
failure, or the 10-second welcome timeout.
|
|
636
|
+
|
|
637
|
+
In the current browser/source build and npm 1.2.0+, use error.detail only when
|
|
638
|
+
it is a specific documented value such as room_full or room_over. Stable npm
|
|
639
|
+
1.1.0 has no detail field. Generic rejected has multiple causes; do not blind
|
|
640
|
+
retry or show “already playing elsewhere” without a distinct detail.
|
|
641
|
+
|
|
642
|
+
Multiplayer is curated per game slug. After approval, Bounty Board assigns one
|
|
643
|
+
of three rails: (a) the built-in casual relay, (b) a bespoke Bounty referee
|
|
644
|
+
module, or (c) a registered external authority with a dedicated ticket key.
|
|
645
|
+
Relay is the shared-service default when no bespoke module or external route is
|
|
646
|
+
registered; clients cannot select the tier. External servers keep their existing
|
|
647
|
+
simulation and add only an isolated signed-ticket adapter plus the SDK room
|
|
648
|
+
envelope—never create a second simulation beside one.
|
|
649
|
+
|
|
650
|
+
Local Bounty shared-service development bypasses the host ticket handshake only
|
|
651
|
+
with paired roomUrl and ticket values (for example wrangler dev with
|
|
652
|
+
DEV_ALLOW_UNSIGNED=1). A registered slug resolves to its referee module; another
|
|
653
|
+
well-formed slug resolves to relay:
|
|
654
|
+
|
|
655
|
+
await joinRoom({
|
|
656
|
+
code: 'TEST',
|
|
657
|
+
joinData: { roomSize: 4, avatar: 'golem' },
|
|
658
|
+
roomUrl: 'ws://localhost:8787/parties/rooms/game-slug:TEST',
|
|
659
|
+
ticket: 'dev:1:Alice',
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
Never enable DEV_ALLOW_UNSIGNED in production.
|
|
663
|
+
|
|
664
|
+
External authority contract:
|
|
665
|
+
https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
|
|
666
|
+
|
|
667
|
+
## Verification checklist
|
|
668
|
+
|
|
669
|
+
- Run the production artifact with no parent host; the whole game still works.
|
|
670
|
+
- Verify gameLoadingFinished fires only when play is possible.
|
|
671
|
+
- Verify gameplayStart/Stop on play, pause, resume, death prompt, and ad break.
|
|
672
|
+
- Verify integer scoring, realistic caps, and one gameOver per run.
|
|
673
|
+
- Verify guest, no-save, unsupported, too-large, and transport-failure save paths.
|
|
674
|
+
- Verify getPlayer null and avatarUrl null.
|
|
675
|
+
- Mock rewarded ready, unavailable, dismissed, error, and viewed; only viewed grants.
|
|
676
|
+
- Assert prepared.show() is called directly from the player gesture.
|
|
677
|
+
- Test initial room.state/players rendering before the first later event.
|
|
678
|
+
- Test multiplayer reconnect/grace, trySend false, tier-appropriate events/state,
|
|
679
|
+
and leave.
|
|
680
|
+
- Test create/code, and match only when using the current browser build/npm 1.2+.
|
|
681
|
+
- For relay, test roomSize default/range, public fan-out (including sender), host
|
|
682
|
+
succession after grace, byte/rate drops, state.dropped, and absence of end.
|
|
683
|
+
- For referee/external rooms, test viewer-safe snapshots, authoritative results,
|
|
684
|
+
and game-defined start/ready/late-join rules against that authority.
|
|
685
|
+
- Test the identical artifact in its standalone location and inside Bounty Board.
|
|
686
|
+
|
|
687
|
+
## Troubleshooting map
|
|
688
|
+
|
|
689
|
+
- Boot waits forever -> game startup depends on an SDK promise -> start init
|
|
690
|
+
fire-and-forget and give every promise feature a standalone outcome.
|
|
691
|
+
- save/load unsupported -> not a Bounty-hosted upload -> use own same-origin store.
|
|
692
|
+
- save/load unauthenticated -> guest player -> continue with defaults/fallback.
|
|
693
|
+
- direct_user_action_required -> show() lost browser activation -> make show()
|
|
694
|
+
the first line of the click/tap handler.
|
|
695
|
+
- host_disabled/unavailable -> host or inventory is not ready -> keep the normal
|
|
696
|
+
fallback; inspect the live bb-arcade-host config before debugging Google.
|
|
697
|
+
- joinRoom unsupported -> standalone/SSR or the room rail is unavailable ->
|
|
698
|
+
hide multiplayer, preserve solo play, and verify the reviewed deployment.
|
|
699
|
+
- joinRoom rejected -> generic invalid request/admission failure -> inspect only a
|
|
700
|
+
documented specific detail, do not infer one cause or blindly retry.
|
|
701
|
+
- trySend false -> disconnected/raced socket or local serialization/send failure
|
|
702
|
+
-> stop sending, show reconnecting when disconnected, and resume only after
|
|
703
|
+
connection.connected is true. A true result still does not prove acceptance.
|
|
704
|
+
- match rejected on an external authority -> external matchmaking is authority-
|
|
705
|
+
owned -> use code/create or that authority's separately documented flow.
|
|
706
|
+
|
|
707
|
+
## More
|
|
708
|
+
|
|
709
|
+
- Human guide: https://www.bountyboard.gg/arcade/sdk
|
|
710
|
+
- Relay room guide: https://www.bountyboard.gg/arcade/sdk/relay-rooms.txt
|
|
711
|
+
- Standalone types: https://www.bountyboard.gg/arcade-sdk.d.ts
|
|
712
|
+
- Submit a game: https://www.bountyboard.gg/arcade/submit
|
|
713
|
+
- Package README, changelog, AGENTS.md, and design playbook ship in npm 1.1.0.
|
|
714
|
+
The external-authority guide is public at the URL above and joins the package
|
|
715
|
+
in 1.2.0.
|