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