# Arcade Game SDK

One script. It is a game's only interface to the portal: loading progress,
gameplay state, ad breaks, saved data, and who is playing.

```html
<script src="https://games.cstsolution.com/sdk/game-sdk.js"></script>
```

Load it **before** your game's own code. The SDK installs audio hooks at load
time, and it can only mute an `AudioContext` whose `connect()` calls happen
after those hooks are in place.

---

## Why the API looks like Poki's

Because it is Poki's, on purpose.

We own these games rather than licensing them, which means the same build
should be submittable to Poki or CrazyGames without a rewrite. So every method
here has the same name, the same arguments and the same return type as its
Poki equivalent, with CrazyGames spellings available as namespaced aliases.
Porting a game becomes a script-tag swap and a find-and-replace, not a project.

| Arcade | Poki | CrazyGames |
| --- | --- | --- |
| `GameSDK.init()` | `PokiSDK.init()` | `CrazyGames.SDK.init()` |
| `.gameLoadingStart()` | `.gameLoadingStart()` | `SDK.game.loadingStart()` |
| `.gameLoadingProgress(n)` | `.gameLoadingProgress(n)` | *(no equivalent)* |
| `.gameLoadingFinished()` | `.gameLoadingFinished()` | `SDK.game.loadingStop()` |
| `.gameplayStart()` | `.gameplayStart()` | `SDK.game.gameplayStart()` |
| `.gameplayStop()` | `.gameplayStop()` | `SDK.game.gameplayStop()` |
| `.commercialBreak()` | `.commercialBreak()` | `SDK.ad.requestAd('midgame')` |
| `.rewardedBreak()` | `.rewardedBreak()` | `SDK.ad.requestAd('rewarded')` |
| `.happyTime(n)` | `.happyTime(n)` | `SDK.game.happytime()` |
| `.save(k,v)` / `.load(k)` | *(use `localStorage`)* | `SDK.data.setItem/getItem` |
| `.getUser()` | *(no equivalent)* | `SDK.user.getUser()` |
| `.setDebug(bool)` | `.setDebug(bool)` | — |
| `.customEvent(name, data)` | `.customEvent()` | — |
| `.getURLParam(name)` | `.getURLParam()` | — |
| `.getLanguage()` | `.getLanguage()` | — |
| `.getDeviceInfo()` | `.getDeviceInfo()` | — |

Both spellings work here — `GameSDK.happyTime(1)` and
`GameSDK.game.happytime()` are the same function. Pick one style per game
and stay with it.

Two differences worth knowing before a port:

- **`init()` never rejects here; Poki's can.** Poki's own guidance is to load
  and play anyway on a failed init, so a game written against this SDK should
  still put a `.catch()` on `init()` if it is heading there.
- **Poki has no `data` module.** `save`/`load` here are `localStorage` with a
  namespace, which is exactly what Poki tells you to write by hand. On
  CrazyGames they map onto `SDK.data`. Nothing changes at the call site.

Not mirrored, and not worth faking: Poki's `measure()`, `displayAd()`,
`getLeaderboard()`, `sendHighscore()`, `login()`/`getToken()`, and its AUDS
data store. `customEvent()` is the nearest thing to `measure()`; the rest need
a backend we do not have. Add them when there is something behind them.

---

## Integration, in the order you should write it

Copy [`example.html`](./example.html) — it is a complete, playable
integration of every call below and it runs both standalone and inside the
portal frame. What follows is the same thing in prose.

### 1. Say you are loading, as early as possible

```js
GameSDK.gameLoadingStart();
```

Before assets, before your engine boots. It is what tells the portal to keep
its cover on the frame and what gives an ad stack its window to fetch a
pre-roll.

### 2. Initialise

```js
await GameSDK.init({
  gameId: 'bulb-tap',        // must match the slug in games.json
  adapter: 'house',          // 'house' | 'adsense' | 'none' | your own object

  // The SDK mutes your audio by itself. It cannot stop your clock — do that
  // here, and start it again in onResume.
  onPause:  () => engine.pause(),
  onResume: () => engine.resume(),
});
```

`init()` never rejects. If the portal is unreachable the promise still
resolves and every method still works — a game must start even when the
surrounding site is broken.

### 3. Say you have finished loading

```js
GameSDK.gameLoadingFinished();
```

The portal lifts its cover off the frame when it sees this.

### 4. Bracket actual gameplay

```js
GameSDK.gameplayStart();   // the player is playing right now
GameSDK.gameplayStop();    // paused, dead, in a menu, on a score screen
```

This is the pair that matters most. "Gameplay" means fingers-on-controls, not
"the game is open". Call `gameplayStop()` when a level ends, when a modal
opens, when the tab is hidden. If you get nothing else right, get these right:
every decision about when to interrupt a player is made from them.

### 5. Ask for a break between rounds

```js
GameSDK.gameplayStop();
await GameSDK.commercialBreak();
showScoreScreen();
```

`commercialBreak()` **always resolves** — ad shown, ad unfilled, ad blocked,
frequency-capped, all the same. There is nothing to catch and nothing to
branch on, so it is safe to call at the end of every round; the portal decides
how often that actually becomes an ad (see *Frequency*, below).

Never call it mid-round, and never on a timer.

### 6. Offer a rewarded video the player chooses

```js
const earned = await GameSDK.rewardedBreak();
if (earned) grantExtraLife();     // and nothing at all otherwise
```

Rules, and they are not negotiable:

- It must be **opt-in** — a button the player pressed, never automatic.
- Grant the reward **only** on `true`. No consolation prize on `false`, or the
  video stops being worth watching and the placement stops being worth money.
- Say what the reward is **before** the video starts, on the button.

### 7. Tell the SDK when things are going well

```js
GameSDK.happyTime(1);      // 0..1
```

Call it on a win, a new record, a level cleared. Ad stacks use it to avoid
interrupting the exact moment a player is enjoying themselves.

### 8. Save

```js
GameSDK.save('best', 4200);
const best = GameSDK.load('best', 0);   // second argument is the fallback
```

`localStorage`, namespaced `cst:<gameId>:<key>`, JSON-encoded, and wrapped —
a failed write returns `false` rather than throwing, because Safari private
browsing throws on `setItem` and a save failing is not a reason to crash.

### 9. The player

```js
const user = await GameSDK.getUser();
// { id: 'guest-4f2a…', username: 'Guest', avatar: null, isGuest: true }
```

There are no accounts yet, so this is a stable per-device guest id. The shape
will not change when accounts arrive.

---

## What happens around every ad break

The SDK does all of this for you, in this order, on both `commercialBreak()`
and `rewardedBreak()`:

1. `gameplayStop()`, if gameplay was running.
2. Your `beforeAd` callback, if you passed one.
3. **Audio is muted.**
4. Your `onPause` handler.
5. The adapter shows the ad.
6. **Audio is unmuted** — only the tracks the SDK muted.
7. Your `onResume` handler.
8. `gameplayStart()`, if gameplay had been running before step 1.

Steps 6–8 are in a `finally`. An adapter that throws, a host page that
vanishes mid-break, a network that dies: the game still comes back with its
sound on. That is the guarantee.

### How muting works without your help

Two paths cover essentially every web game:

- **Web Audio.** `AudioNode.prototype.connect` is patched. Anything connecting
  to a context's `destination` is quietly rerouted through a gain node the SDK
  owns, which is itself connected to the real destination. Muting sets that
  gain to 0. The context is deliberately **not** suspended — suspending stops
  `currentTime`, and any game scheduling against it comes back out of sync.
- **`<audio>` / `<video>`.** Registered when `play()` is called, plus a sweep
  of the document at mute time. Only elements the SDK muted are unmuted, so a
  track you had already muted stays muted.

This is why the script tag must come first. If your engine creates and
connects its graph before this file runs, its output never passes through the
gain node and the SDK cannot turn it down.

---

## Frequency

The **portal** decides how often a player is interrupted, not the game. Asking
for a break is a suggestion — "this is a good moment" — which is why calling
it at the end of every round is correct rather than aggressive.

Defaults:

| Setting | Default | Meaning |
| --- | --- | --- |
| `firstBreakAfterSeconds` | 45 | No interstitial in the first 45 seconds of a session. |
| `minSecondsBetweenBreaks` | 120 | At most one interstitial every two minutes. |
| `rewardedCooldownSeconds` | 5 | Only exists to swallow a double-tap. |

Rewarded breaks ignore the interstitial cap: the player asked for that one.

Override per game in `init({ adPolicy: { … } })`, but raise the numbers rather
than lowering them unless there is a reason you can defend.

---

## Adapters

No ad code lives in the SDK. Every break goes through an adapter:

```js
{
  name: 'my-network',
  show(ctx) {            // ctx = { kind: 'interstitial'|'rewarded', gameId, sdk }
    return Promise.resolve(true);   // true = completed / reward earned
  }
}
```

`show()` must **never reject**. A rejection is treated as `false` and logged,
but an adapter that rejects is a bug.

| Adapter | What it does |
| --- | --- |
| `house` | Draws a stub countdown overlay. No network. This is what runs in development and what everything else falls back to. |
| `adsense` | Scaffold for Google **H5 Games Ads** (`adConfig` / `adBreak`). Does not serve an ad yet — see below. |
| `none` | Resolves instantly. For automated tests, and for a build handed to a portal that runs its own ads. |

### Turning `adsense` on

The AdSense surface that does interstitials and rewarded video for web games
is **H5 Games Ads**, not ordinary display units. The adapter is already wired
to its `adConfig()` / `adBreak()` API. To go live:

1. Set `GameSDK.adapters.adsense.config.client` to the `ca-pub-…` id.
2. Remove `s.dataset.adbreakTest` (it is `''` in the file, marked TODO —
   set it to `'on'` while testing and **delete it before launch**).
3. Serve `ads.txt` from the site root.
4. Put a consent flow (TCF v2 for the EU/UK) in front of the first break.
5. Switch the portal over: `portal/js/game.js`, the `adapter:` option in
   `GameSDK.host.attach`.

Until step 1 is done the adapter logs a warning and delegates to `house`, so
nothing breaks and the flow stays testable.

---

## Running inside the portal

A game runs in an iframe. An ad that only covers the iframe is a worse ad and
a worse experience, so when the portal is hosting, the break is handed up to
the parent page over `postMessage` and drawn over the whole thing.

That handshake happens inside `init()` and needs nothing from the game. If no
portal answers within ~900 ms — the game was opened directly, or embedded
somewhere else — the SDK runs the adapter in its own document instead. Both
paths are exercised by opening `example.html` standalone and then through the
portal frame.

The portal side is one call, already wired in `portal/js/game.js`:

```js
GameSDK.host.attach(iframe, {
  gameId: 'bulb-tap',
  adapter: 'house',
  onEvent: (name) => { if (name === 'loadingFinished') liftCover(); },
});
```

Messages are accepted only from that iframe's `contentWindow`, and only from
an origin in `allowedOrigins` (default: the portal's own). Games are
same-origin today; widen it deliberately, not by reflex.

---

## Debugging

Add `?cstdebug=1` to the URL, or call `GameSDK.setDebug(true)`, and the SDK
narrates every call — which adapter ran, whether the host answered, how many
audio contexts it muted, why the policy declined a break.

```js
GameSDK.on('adstart',    ({ kind }) => console.log('ad in', kind));
GameSDK.on('adcomplete', ({ ok })   => console.log('ad out', ok));
```

The same events also fire on `window` as `cstsdk:adstart` and friends, which
is the easier hook if your game engine already has an event bus.

---

## Full surface

```
GameSDK.version
GameSDK.init(options)                → Promise<sdk>   (never rejects)
GameSDK.gameLoadingStart()
GameSDK.gameLoadingProgress(0..1)
GameSDK.gameLoadingFinished()
GameSDK.gameplayStart()
GameSDK.gameplayStop()
GameSDK.isPlaying                    → boolean
GameSDK.commercialBreak(beforeAd?)   → Promise<void>     (never rejects)
GameSDK.rewardedBreak(beforeAd?)     → Promise<boolean>  (never rejects)
GameSDK.happyTime(intensity)         0..1
GameSDK.isAdBlocked()                → Promise<boolean>
GameSDK.save(key, value)             → boolean
GameSDK.load(key, fallback?)         → any
GameSDK.remove(key) / .clearSaves()
GameSDK.getUser()                    → Promise<user>
GameSDK.setDebug(bool)
GameSDK.customEvent(name, data)
GameSDK.getURLParam(name)            → string   (checks ?gd<name> first)
GameSDK.getLanguage()                → 'en'     (?iso_lang wins)
GameSDK.getDeviceInfo()              → { category: 'mobile'|'tablet'|'desktop' }
GameSDK.shareableURL(params)         → Promise<string>
GameSDK.on(type, fn) / .off(type, fn)

GameSDK.game.{loadingStart, loadingProgress, loadingStop, gameplayStart, gameplayStop, happytime}
GameSDK.ad.{requestAd(type), hasAdblock}
GameSDK.data.{setItem, getItem, removeItem, clear}
GameSDK.user.{getUser, isUserAccountAvailable}
GameSDK.banner.{requestBanner, clearBanner}     // not implemented, see below
GameSDK.adapters.{house, adsense, none}
GameSDK.host.attach(iframe, opts)               // portal side only
```

`init` options: `gameId`, `adapter`, `debug`, `onPause`, `onResume`,
`adPolicy: { firstBreakAfterSeconds, minSecondsBetweenBreaks,
rewardedCooldownSeconds }`.

---

## Known gaps

- **`banner.requestBanner()` is not implemented.** The portal renders its own
  slots around the frame, which keeps ad markup out of the game's document
  entirely. If in-game banners are ever wanted they belong in an adapter like
  every other ad, not inline in the game.
- **`getUser()` is always a guest.** Accounts do not exist yet. When they do,
  the host will answer over the bridge and the return shape stays the same.
- **No analytics.** `customEvent()` reaches the host and stops there. Whatever
  the portal eventually reports to is not this SDK's business.
- **`isAdBlocked()` only answers after an adapter has tried to load.** Before
  the first break it resolves `false`.
