## TL;DR
`@gamee/sdk` is a typed, promise-based JavaScript bridge between an HTML5 game and the GAMEE platform (web iframe, iOS WKWebView, Android WebView). The wire protocol is identical across all three hosts; the SDK picks the right transport at boot. Games declare capabilities at `init()` time; gated methods reject synchronously if the capability is missing.
```ts
import { gamee } from '@gamee/sdk';
await gamee.init({ capabilities: ['saveState', 'rewardedAds'] });
gamee.gameReady();
gamee.on('start', () => game.run());
gamee.on('pause', () => game.pause());
```
## Glossary
| Term | Meaning |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Game** | The HTML5 app written with `@gamee/sdk`. |
| **Platform** | The GAMEE app that runs the game (web/iOS/Android). Older docs may call this the _host_ or _client_; we use **platform** everywhere (matches `GameInitResponse.platform`, the emulator UI, and the bridge log). |
| **Bridge** | The transport layer the SDK picks at boot — `WebBridge` (postMessage), `IosBridge` (`webkit.messageHandlers.callbackHandler`), `AndroidBridge` (`window._toDevice`). |
| **Request** | Game → platform RPC. Returns a `Promise`. |
| **Response** | Platform's reply to a specific request. Carries data **or** error. |
| **Event** | Platform → game push. Game subscribes via `gamee.on(name, handler)`. SDK auto-acks. |
| **Signal** | A fire-and-forget request the SDK sends without tracking the platform's ack (e.g. `updateScore`). Resolves immediately. |
| **Capability** | A string in `init({ capabilities: [...] })` that gates feature methods. |
## Imports
```ts
// Default singleton — what most games use:
import { gamee } from '@gamee/sdk';
// Fresh instance — for tests or multi-game setups:
import { createSdk } from '@gamee/sdk';
const sdk = createSdk({ allowedOrigins: ['https://games.gameeapp.com'] });
// Errors:
import { GameeError } from '@gamee/sdk';
// Types you'll commonly need:
import type {
Capability,
Platform,
Environment,
GameContext,
InitOptions,
GameInitResponse,
MissionData,
GameOverData,
PurchaseDetails,
PurchaseResult,
RewardedLoadResult,
RewardedResult,
GameeEvents,
GameeEventName,
GameeErrorCode,
PlatformBridge,
BridgeListener,
SdkOptions,
} from '@gamee/sdk';
```
IIFE script-tag: `` — exposes `window.Gamee` with `Gamee.gamee`, `Gamee.createSdk`, `Gamee.GameeError`.
## Public API — every method
Returns `Promise` unless noted. **Cap** = capability required at init.
### Lifecycle
| Method | Args | Returns | Cap | Notes |
| --------------------- | --------------------------------- | ------------------ | --- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `init` | `{ capabilities: Capability[] }` | `GameInitResponse` | — | Call **once per SDK instance**; second call rejects `INVALID_STATE`. Validates capability list synchronously. Sends SDK version on the wire. |
| `gameReady` | — | `void` (signal) | — | Tells the platform first frame is rendered. |
| `gameStart` | — | `void` (signal) | — | A run is starting. |
| `gameOver` | `{ saveStateData?, rewardIds?, metadata? }?` | `void` (signal) | — | Run ended. `saveStateData` and `metadata` accept `object \| string`; objects auto-stringify. |
| `requestPause` | — | `void` (signal) | — | Game-initiated pause (e.g. opened in-game menu). Distinct from the platform-pushed `pause` event. |
| `dispose` | — | `void` (sync) | — | Release resources. Pending data-returning requests reject `INVALID_STATE`. |
### Save state
| Method | Args | Cap | Notes |
| --------------- | --------------- | ----------- | --------------------------------------------------------- |
| `gameSaveState` | `data: object \| string` | `saveState` | Pass an object (auto-stringified) or a pre-stringified JSON string. Wire method is `saveState`. |
### Score & missions
| Method | Args | Cap | Validation |
| ----------------------- | -------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `updateScore` | `number \| { score, playTime?, checksum?, metadata? }` | — | `score` must be finite; `playTime` ≥ 0 and finite (if provided); `checksum` non-empty string (if provided). `score` is `Math.trunc()`-ed. Wire payload omits keys you didn't pass. |
| `updateMissionProgress` | `progress: number` | `missions` | `progress` must be a finite number in `[0, 100]`. |
### Commerce
| Method | Args | Returns | Cap | Validation |
| ---------------------- | --------------------------- | ---------------- | ------ | -------------------------------------------- |
| `purchaseItemWithGems` | `{ gemsCost, itemName }` | `{ success }` | `gems` | `gemsCost` ≥ 0, `itemName` non-empty string. |
### Rewarded ads
| Method | Args | Returns | Cap |
| ------------------- | ---- | -------------------------- | ------------- |
| `loadRewardedVideo` | — | `{ videoLoaded: boolean }` | `rewardedAds` |
| `showRewardedVideo` | — | `{ videoPlayed: boolean }` | `rewardedAds` |
### Analytics
| Method | Args | Cap | Validation |
| ---------- | ----------------------------- | ----------- | ---------------------------------------------------------- |
| `logEvent` | `string \| { name, value? }` | `logEvents` | `name` non-empty, ≤ **24** chars; `value` (optional) ≤ **160** chars. Wire payload omits `eventValue` when `value` not provided. |
### Introspection / utilities
All read-only getters on the SDK instance, or pure top-level imports.
None of these send anything on the wire.
| Member | Source | Type | Notes |
| ---------------------- | ------------------ | -------------------------- | ------------------------------------------------------------------------------------ |
| `hasPlatformContext()` | top-level import | `boolean` (sync) | `true` iff in an iframe, iOS WKWebView, or Android WebView. Call **before** `createSdk()`/`init()`; if `false`, render a fallback — `init()` would otherwise hang and reject `BRIDGE_TIMEOUT` after 30 s. |
| `gamee.platform` | instance getter | `Platform` | Bridge's platform: `'web' \| 'mobile_web' \| 'ios' \| 'android'`. Stable for SDK's lifetime. |
| `gamee.version` | instance getter | `string` | SDK version sent on the wire with `init`. |
| `gamee.initResponse` | instance getter | `GameInitResponse \| null` | The `init` payload, or `null` before init. |
| `SDK_VERSION` | top-level constant | `string` | Same as `gamee.version`, available pre-init. |
| `ALL_CAPABILITIES` | top-level constant | `readonly Capability[]` | Every capability the SDK knows. |
### Events
```ts
gamee.on(event, handler) // returns an unsubscribe function
gamee.off(event, handler?) // drop one handler, or all if no fn passed
```
| Event | Payload | When |
| ------------------- | ---------------------------- | ----------------------------------------------------------------------- |
| `start` | `{ resetState?, gameSeed? }` | New run. May fire multiple times (replay, restart). |
| `pause` | `undefined` | Platform paused (backgrounded, system interruption). Stop loop + audio. |
| `resume` | `undefined` | OK to resume. |
| `mute` | `undefined` | Audio toggled off. |
| `unmute` | `undefined` | Audio toggled on. |
| `submit` | `undefined` | Platform asks the game to call `gameOver` now. |
| `useExtraLife` | `undefined` | Player redeemed an extra life — resume from end state. |
| `avatarAnimation` | `{ animationType }` | Play a one-shot animation on the in-game avatar. |
| `avatarUpdate` | `{ avatar }` | Avatar data changed. |
| `miningEventUpdate` | `{ miningEvent }` | Mining feature payload changed. |
Listeners that throw are caught and logged; one bad handler can't starve the others. Subscribe **before** calling `gameReady()` or you may miss the first `start`.
## Capabilities
```ts
type Capability = 'saveState' | 'rewardedAds' | 'logEvents' | 'missions' | 'gems' | 'platformExtraLife';
```
Calling a gated method without its capability throws `CAPABILITY_MISSING` **synchronously, before** anything reaches the wire. The error is a `GameeError` with `.code`, `.method`, `.message`.
`platformExtraLife` does not gate any SDK method — it tells the platform to render its post-`gameOver` "extra life" overlay (rewarded ad / gem purchase). Without it, the platform will not offer extra lives. Pair it with a listener on the `useExtraLife` event.
Removed (relative to legacy `gamee-js`): `replay`, `ghostMode`, `socialData`, `playerData`.
## `GameInitResponse`
```ts
{
environment: 'production' | 'development';
platform: 'web' | 'mobile_web' | 'ios' | 'android';
country: string; // ISO 3166-1 alpha-2 ('US', 'CZ', …)
locale: string; // BCP-47 ('en-US', 'cs-CZ', …)
gameContext: 'normal' | 'battle' | 'mission';
sound: boolean;
noAds?: boolean;
saveState?: string; // previously saved state (stringified JSON the game wrote)
initData?: string; // deep-link payload (stringified JSON)
missionData?: MissionData; // present when gameContext === 'mission'
}
```
The SDK validates that `environment`, `platform`, `country`, `locale`, `gameContext`, `sound` are present. Missing fields cause the init Promise to reject with `BRIDGE_ERROR`.
## Errors
Every failure mode is a `GameeError` with one of seven `code` values:
| Code | When |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION` | Bad input that the SDK validated locally (`logEvent` over 160 chars, `updateMissionProgress` outside `[0, 100]`, unknown capability, etc.). |
| `CAPABILITY_MISSING` | Gated method called without the capability. |
| `BRIDGE_TIMEOUT` | Platform did not respond within `requestTimeoutMs` (default **30 000 ms**). Only data-returning methods can hit this. |
| `BRIDGE_ERROR` | Platform returned an explicit error response, or returned a malformed payload (e.g. init missing required fields). |
| `BRIDGE_OVERFLOW` | More than `maxPendingRequests` concurrent data-returning calls (default **256**). Oldest pending is rejected. |
| `INVALID_STATE` | `init()` called twice, or any method called after `dispose()`. |
| `NOT_SUPPORTED` | Method exists but the platform can't honor it. Reserved for future use. |
`GameeError` has `.name === 'GameeError'`, `.code`, `.method?`, `.message`, `.cause?`. Branch on `.code`, never on `.message`.
```ts
try {
await gamee.gameSaveState({ level: 3 });
} catch (err) {
if (err instanceof GameeError && err.code === 'CAPABILITY_MISSING') {
// declare 'saveState' in init()
}
}
```
## Behavior gotchas
- **Signal methods** (`gameReady`, `gameStart`, `gameOver`, `gameSaveState`, `updateScore`, `updateMissionProgress`, `requestPause`, `logEvent`) return `Promise` but resolve immediately — they don't wait for the platform's ack. Validation errors surface as Promise rejections. If you don't `await` or `.catch()`, they become unhandled-rejection events.
- **Data-returning methods** (`purchaseItemWithGems`, `loadRewardedVideo`, `showRewardedVideo`) DO wait for the platform's reply and can hit `BRIDGE_TIMEOUT` / `BRIDGE_ERROR`.
- **`init()` is single-shot.** Second call → `INVALID_STATE`. Use `createSdk()` for a fresh instance in tests.
- **Subscribe before `gameReady()`** or you'll miss the first `start` event.
- **`saveState` is a string on the wire.** The SDK auto-stringifies objects. The platform sends it back via `initResponse.saveState` exactly as it was saved.
- **`updateScore` truncates** non-integer scores to `Math.trunc(score)`.
- **`messageId` is wire-internal** — games never see it.
## SDK options (`createSdk(opts)`)
```ts
interface SdkOptions {
allowedOrigins?: readonly string[] | '*'; // postMessage origin allowlist for the web bridge. Default: '*' with first-use warning.
requestTimeoutMs?: number; // default 30_000
maxPendingRequests?: number; // default 256 (data-returning calls only)
silentMode?: boolean; // suppress runtime warnings
bridge?: PlatformBridge; // inject a custom bridge (used by emulator + MockBridge)
}
```
## Wire format
One envelope shape, both directions:
```jsonc
// Game → Platform request
{ "request": { "method": "init", "messageId": 0, "data": { "version": "1.0.0", "capabilities": { "saveState": true } } } }
// Platform → Game response (data) — replies to a request
{ "response": { "messageId": 0, "data": { /* … */ } } }
// Platform → Game response (error)
{ "response": { "messageId": 0, "error": { "code": "…", "message": "…" } } }
// Platform → Game event (looks like a request; SDK auto-acks)
{ "request": { "method": "pause", "messageId": 7, "data": null } }
```
`messageId` is a **non-negative integer (`number`)** that increments by 1 per outbound message within a single SDK instance (starts at 0). Wire-compatible with legacy gamee-js clients. It correlates request ↔ response. `init`'s wire `data.capabilities` is an **object map** (`{ saveState: true }`), not an array — game-facing API uses arrays for ergonomics, the SDK transforms.
## `@gamee/sdk-test-utils`
Sister package shipping `MockBridge` for game test suites. Inject it instead of the real bridge.
```ts
import { describe, expect, test, beforeEach } from 'vitest';
import { createSdk, type GameeSDK } from '@gamee/sdk';
import { MockBridge } from '@gamee/sdk-test-utils';
let mock: MockBridge;
let sdk: GameeSDK;
beforeEach(() => {
mock = new MockBridge();
sdk = createSdk({ bridge: mock });
});
test('updateScore reaches the wire with the right shape', async () => {
await sdk.init({ capabilities: [] });
sdk.updateScore({ score: 420, playTime: 12, checksum: 'sum' });
expect(mock.requests.find((r) => r.method === 'updateScore')?.data).toMatchObject({
score: 420,
currentPlayTime: 12,
gameChecksum: 'sum',
});
});
```
### `MockBridge` API
| Member | Purpose |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `new MockBridge(platform?)` | Construct. `platform` defaults to `'web'`; pass `'ios'` / `'android'` for tests that branch on it. |
| `mock.requests` | Every recorded outbound request: `{ method, messageId, data }[]`. |
| `mock.acked` | `messageId`s of platform events the SDK acked. |
| `mock.expect(method).respondWith(data)` | Reply to the next request matching `method` with `data`. |
| `mock.expect(method).rejectWith({ code, message })` | Reply with an error. |
| `mock.expect(method).leavePending()` | Don't auto-respond; settle later via `mock.respondTo(method, data)`. |
| `mock.respondTo(method, data)` | Manually settle the most recent matching pending request. |
| `mock.emit(name, payload, messageId?)` | Push a platform → game event. Auto-acked. |
| `mock.defaultResolution` | What happens to unexpected requests: `'ack-null'` (default) \| `'leave-pending'` \| `'error'`. |
| `mock.reset()` | Clear `requests` and unconsumed expectations. |
| `mock.dispose()` | Detach. Safe to call repeatedly. |
### Common test recipes
```ts
// 1. Platform replies with success.
mock.expect('showRewardedVideo').respondWith({ videoPlayed: true });
const r = await sdk.showRewardedVideo();
expect(r.videoPlayed).toBe(true);
// 2. Platform rejects.
mock.expect('purchaseItemWithGems').rejectWith({ code: 'BRIDGE_ERROR', message: 'insufficient gems' });
await expect(sdk.purchaseItemWithGems({ gemsCost: 99, itemName: 'X' })).rejects.toThrow('insufficient gems');
// 3. Capability gate is sync.
expect(() => sdk.gameSaveState({ x: 1 })).toThrow(/CAPABILITY_MISSING|saveState/);
// 4. Drive a platform-initiated event.
let paused = false;
sdk.on('pause', () => {
paused = true;
});
mock.emit('pause');
expect(paused).toBe(true);
// 5. Force a timeout.
sdk = createSdk({ bridge: mock, requestTimeoutMs: 50 });
await sdk.init({ capabilities: ['rewardedAds'] });
mock.expect('showRewardedVideo').leavePending();
await expect(sdk.showRewardedVideo()).rejects.toMatchObject({ code: 'BRIDGE_TIMEOUT' });
```
## Quick reference for code generation
When generating game code that uses this SDK, default to:
1. `await gamee.init({ capabilities: [...] })` — list every capability the game's logic touches; otherwise gated calls throw.
2. `gamee.on(event, handler)` **before** `gamee.gameReady()`.
3. Synchronous-feeling fire-and-forget for telemetry: `gamee.updateScore(...)`, `gamee.logEvent(...)` — no need to `await`.
4. `await` for data-returning calls: `loadRewardedVideo`, `showRewardedVideo`, `purchaseItemWithGems`.
5. Error handling: `try { await … } catch (err) { if (err instanceof GameeError) … }` with `err.code` branching.
6. Tests: inject `MockBridge` via `createSdk({ bridge: mock })`. Never use the singleton in tests.