# Arcade SDK Reference for `public/sdk/v0/bananai-sdk.js`, served at [`/sdk/v0/bananai-sdk.js`](/sdk/v0/bananai-sdk.js). Current version: **0.2.0**. Every game must load the SDK and use it for all communication with the portal. Games run in a sandboxed iframe and must never touch `window.parent` directly. --- ## A note on prior art The API surface here deliberately follows the shape that portal SDKs have converged on — a loading phase, explicit gameplay start/stop, a game-over signal, and ad hooks. That convention is worth adopting for a concrete reason: a developer who has shipped on another portal can port a game here in an afternoon. **The implementation and this documentation are our own.** We do not copy another portal's SDK source or docs — that is someone else's copyrighted work, and shipping it would be both a legal problem and an obstacle to the independence we actually want. Following an established interface convention is normal engineering practice; copying an implementation is not. Advertising is part of that convention and works here for real as of 0.1.1. `commercialBreak()` and `rewardedBreak()` were shims that resolved immediately in 0.1.0; they now cross to the shell, which owns the entire ad stack. A game written against an ad-funded portal runs here unmodified either way, which was the point of matching the shape in the first place. **Your game never loads an ad script.** It asks; the shell answers. That is what keeps your folder self-contained, keeps third-party ad code off your origin, and lets the portal change ad providers without you redeploying anything. --- ## Wire protocol One envelope, as of **0.2.0**: ```js // game -> shell { source: 'bananai-sdk', sdkVersion: '0.2.0', gameId: 'gemburst', type, payload } // shell -> game { source: 'bananai-shell', type, payload } ``` Replies are typed `:reply`. Three reasons this is the right shape, all of which the previous one lacked: - **`gameId` travels with every message.** The host would otherwise have to infer which game is talking from which frame sent the message, which stops working the moment more than one frame exists. - **`sdkVersion` is declared.** The host can adapt to older games instead of guessing, which is what makes supporting third-party submissions realistic. - **Both ends are named.** `source` says who sent it; a shared `channel` constant only says the message belongs to us, not which direction it travels. Until 0.2.0 the SDK sent `{ channel: 'turbigo', type, payload }` and the host accepted both shapes, replying in whichever it was addressed in. That compatibility layer is gone: it was carrying the old product name, and an alias kept for a name nothing uses is an alias that never gets removed. **If you have a copy older than 0.2.0, it can no longer talk to the portal.** Nothing partially works — the handshake times out, and your game runs as though it were standalone: no loading bar, no score record, no playtime attribution, and `bananai:restart` never arrives. Take a fresh copy of the file. --- ## Lifecycle ```js await Bananai.init({ gameId: 'gemburst' }); // while loading assets Bananai.loadingProgress(0.4); Bananai.loadingFinished(); // when the player actually starts playing Bananai.gameplayStart(); // on pause, menu, or any interruption Bananai.gameplayStop(); // when a run ends Bananai.gameOver({ score: 12500, level: 7, durationMs: 84000 }); ``` --- ## API ### `init(options): Promise` Must be awaited before any other call. Resolves once the shell acknowledges, or immediately when the game runs standalone. | Option | Type | Description | |---|---|---| | `gameId` | string | Required. Must match the catalogue ID. | | `debug` | boolean | Logs SDK activity to the console. | ### `loadingProgress(fraction)` `fraction` is clamped to 0..1. Drives the shell's loading indicator. Call it during asset loading, not after. ### `loadingFinished()` Assets are ready. The shell hides its loading state. ### `gameplayStart()` / `gameplayStop()` Bracket every period of active play. Idempotent — repeated calls in the same state are ignored. The shell uses these to avoid interrupting live gameplay and to measure engagement. **These two calls are how you get paid.** Active playtime is the basis of the revenue share, and a game that never brackets its play looks to the portal like a game nobody plays. They are also what stops an ad appearing over a live run. See [the revenue share terms, sent on request](mailto:hello@bananai.games). Call `gameplayStop()` on pause, on opening a menu, and whenever the tab loses visibility. ### `gameOver(result)` Ends the run and produces a score record. Implicitly calls `gameplayStop()`. | Field | Type | Required | |---|---|---| | `score` | number | yes | | `level` | number | no, defaults to 1 | | `durationMs` | number | recommended — used for plausibility checks | ### `happyMoment(intensity)` Signals player delight: a cleared level, a beaten personal best. Telemetry only today; the shell may later use it to time non-intrusive prompts. Do not call it on every point scored. ### `commercialBreak(): Promise` Asks for a commercial break and resolves when it is over. Call it **between activities only** — a menu, a level transition, a game over — and call `gameplayStop()` first. Your game is suspended for as long as the promise is pending, and the shell refuses the break outright if its own gameplay signals say a run is still live. It always resolves, and resolves quickly when there is nothing to show: no fill, frequency caps, no consent, advertising disabled, a blocked ad script. **Nothing in your game may depend on a break having actually happened.** ### `rewardedBreak(): Promise` Plays a rewarded video and resolves `true` only if it was watched to the end. Ask the player first — the offer is yours, since only your game knows what it is offering. The shell just plays the video. **Games must handle `false` gracefully**, and `false` is the common case: no fill, a dismissal, no consent, an ad blocker, or a portal with advertising switched off. A game that gates progress behind a rewarded video is not publishable here. ```js Bananai.gameplayStop(); const earned = await Bananai.rewardedBreak(); if (earned) grantExtraLife(); else showNormalGameOver(); // must be a complete experience on its own Bananai.gameplayStart(); ``` Rewarded pays more per impression than any other format, so a natural offer — a continue, an extra life, a hint — is worth implementing. How that turns into money is in [the revenue share terms, sent on request](mailto:hello@bananai.games). ### `setDebug(enabled)` --- ## Events from the shell The SDK re-dispatches shell messages as `CustomEvent` on `window`: ```js window.addEventListener('bananai:restart', () => resetGame()); window.addEventListener('bananai:pause', () => pauseGame()); window.addEventListener('bananai:resume', () => resumeGame()); ``` `bananai:restart` fires when the player chooses to play again after a game over. Handling it is mandatory — a game that requires a page reload to restart fails QA. --- ## Standalone mode When a game is opened directly rather than embedded, `window.parent === window` and every SDK call becomes a safe no-op. Games must remain fully playable in this mode; it is how developers work locally and how QA reviews submissions. --- ## Migration from the v0 event contract Gem Burst predates the SDK and uses raw `window` events (`arcade:gameover`, `arcade:restart`). Those still work but are deprecated. | v0 | v0.1 | |---|---| | `dispatchEvent('arcade:gameover', {score, level})` | `Bananai.gameOver({score, level})` | | `addEventListener('arcade:restart')` | `addEventListener('bananai:restart')` | Gem Burst gets migrated in phase 2, alongside score persistence. --- ## Loading the SDK Two supported options. Both are accepted in review, but they are not equally good, so this section says which to prefer and why. ### Preferred: link to the portal's copy ```html ``` Your game is served from the portal's own origin, under `/games//`, so this path resolves without any configuration. There is one copy of the SDK on the whole site, and fixes reach your game without you redeploying — which is the entire point of the versioning policy below. ### Accepted: vendor a copy into your folder ```html ``` The folder stays self-contained and portable to another portal. The cost is that **the wire protocol freezes at the version you copied**, including the advertising methods. Declare the exact version in `bananai.json` under `sdkVersion` so review can tell what your game is actually running. Vendor when you have a reason to — you ship the same build to several portals, or you need the game to run from a folder that is not under a portal root. Link otherwise. ### Never write the origin ```html ``` The absolute form looks more explicit and is strictly worse. It is a *cross-origin* request everywhere the portal is not live production: on your machine, on the preview domain, and in `npm run check`, which fails any request leaving the origin under test. It also stops working the day the portal changes domain — and a game folder is not ours to edit, so we could not fix it for you. The root-relative form is correct in all of those places at once, and `npm run check` rejects the absolute one. If you develop with your game folder as the document root, `/sdk/v0/...` will 404 locally. Put the file at `/sdk/v0/bananai-sdk.js` while you work: the path is then identical to production, and what you test is what ships. **Vendor 0.2.0, and download the file rather than writing your own.** Both halves of that matter: - Anything **older than 0.2.0** speaks the previous envelope and cannot reach the portal at all. Before that, **0.1.0** had a second defect worth knowing about: its two advertising methods were local stubs that resolved without sending anything, so a game vendoring it could not show an ad or earn from one, and nothing about it looked broken — it simply never requested. One game in our own catalogue is in exactly that state, and it is the reason this paragraph exists. - A **reimplementation written from this document** will be subtly wrong in ways neither of us will enjoy diagnosing: the envelope, the reply types, the timeouts and the duplicate-suppression rules are all part of the contract, and prose is not a specification. Take the file from [`/sdk/v0/bananai-sdk.js`](/sdk/v0/bananai-sdk.js) and ship that. Whichever you choose, `sdkVersion` must state what the package actually contains. Declaring a version you have not shipped is worse than shipping an old one. --- ## Versioning The SDK follows semver. Games pin a major version: ```html ``` Breaking changes ship as a new major with the old one kept alive for at least six months. Third-party developers cannot be expected to redeploy on our schedule. **0.2.0 is a breaking change that did not get a new major, and here is why.** The rule above protects deployed games. There were none to protect: the file was documented at `/sdk/v0/` while it actually sat at `/sdk/`, so the URL this document has always given returned 404 for its entire existence — confirmed by an external developer, who reimplemented the SDK from prose because they could not download it. Nothing was ever served from `/sdk/v0/`, so nothing could break by changing what lives there. Minting `/sdk/v1/` to preserve a `/sdk/v0/` that never existed would document a history that did not happen. The next breaking change will not have that excuse, and gets `/sdk/v1/`. **0.2.0** unified the envelope and carried the rename from the product's previous name. Both were breaking and both were unavoidable, so they went out together rather than asking every developer to migrate twice for one product decision. **0.1.1** turned the two advertising methods from stubs into real calls; **0.1.0** was the first release. Two older URLs still redirect here rather than 404, because a game folder is not ours to edit and neither is anyone else's `