Build13 min read

SDK reference

Every TwiddlePlay method and event, what it returns, when to call it, and the mistakes to avoid.

On this page18 sections

Your game talks to Twiddle Play through one small script: the Twiddle Play SDK. It adds one global object, window.TwiddlePlay. This page lists everything on it.

The SDK is one file, about 8 KB, plain ES5 with no dependencies. It works in any browser and any Android WebView.

The API at a glance#

TwiddlePlayJavaScript
TwiddlePlay.version            // '1'
TwiddlePlay.env                // 'android' | 'host' | 'standalone'
TwiddlePlay.ready()            // Promise<void>
TwiddlePlay.showInterstitial() // Promise<{ shown: boolean }>
TwiddlePlay.showRewarded()     // Promise<{ rewarded: boolean }>
TwiddlePlay.hasRewarded()      // Promise<boolean>
TwiddlePlay.showBanner(opts?)  // Promise<{ shown: boolean }>
TwiddlePlay.hideBanner()       // Promise<void>
TwiddlePlay.gameplayStart()    // no return value
TwiddlePlay.gameplayStop()     // no return value
TwiddlePlay.on(evt, cb)        // returns an "unsubscribe" function
TwiddlePlay.off(evt, cb)
TwiddlePlay.exit()             // no return value
// events: 'pause' | 'resume' | 'mute' | 'unmute'

Two rules hold for every call:

  • Nothing ever throws, and no promise ever rejects. If something goes wrong, you get the "no" answer: { shown: false }, { rewarded: false } or false.
  • You can call anything at any time, even before the SDK has found the app. Calls wait until it has, then run.

Sandbox

Run the real SDK in standalone mode, right on this page. Call showRewarded() and watch the test ad, the pause and mute events, and the result come back.

Load the SDK#

Add two script tags before your own scripts, and put a copy of the SDK file next to your index.html.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My Game</title>
</head>
<body>
  <canvas id="game"></canvas>
  <script src="/sdk/v1/twiddleplay.js"></script>
  <script src="twiddleplay-sdk.js"></script>
  <script src="game.js"></script>
</body>
</html>

Why two tags?

  1. /sdk/v1/twiddleplay.js is the platform's own copy. It exists inside the Twiddle Play app and in the portal preview, and it is always the latest v1 SDK.
  2. twiddleplay-sdk.js is your copy, shipped in your zip. It makes sure TwiddlePlay exists everywhere else, for example when you open the game on your own computer.

The SDK is idempotent: whichever copy loads first wins, and the other one does nothing. In the Android app, the SDK is also injected before your code runs (on phones that support it), so your copies may both do nothing there. That's fine.

Download your copy here: twiddleplay.js. Save it as twiddleplay-sdk.js.

The three environments#

The SDK looks for a platform when it loads, in this order, and sets TwiddlePlay.env to match.

env Where your game is running What ads do
'android' Inside the Twiddle Play Android app. The SDK talks to the app through a native bridge. Real ads (Google AdMob), with the app's limits.
'host' Inside a frame whose parent answers the SDK's hello within 400 ms: the portal's Test it preview, and the app's web version. Test ads drawn by the host. The preview has no limits unless you tick App ad limits.
'standalone' Anywhere else: your own computer, a plain web page, a new browser tab. A test-ad overlay drawn by the SDK itself, with the same limits as the app.

android: the Twiddle Play app#

  • Your game runs full screen in its own WebView, from its own private address. Its localStorage and IndexedDB belong to your game alone.
  • Ads are real. Each ad call goes through the app's ad limits.
  • A banner sits below (or above) your game, and your game's area shrinks to make room for it.
  • When the player leaves the app, opens the "Leave game?" dialog, or an ad starts, your game gets pause and mute. When they come back, it gets resume and unmute.
  • exit() closes your game.

host: the portal preview#

  • The Test it button on your Versions tab opens your game in a phone frame. The page around it is the host.
  • Ads are 3-second test ads drawn in the phone frame. Every call shows up in the SDK activity panel, so you can see exactly what your game asked for.
  • A banner is a test bar labelled "Test banner — real ads only in the Twiddle Play app". Your game's area in the phone frame shrinks to make room for it, just like in the app.
  • exit() shows a message ("In the app this closes the game.") instead of closing anything.
  • See Testing your game for every control.

standalone: anywhere else#

  • Ad calls show a test overlay: "Test ad — real ads show only in the Twiddle Play app". It counts down from 3 and closes by itself.
  • The same interstitial limits as the app apply, counted from when the SDK loaded. A skipped ad logs the reason in the console, for example [TwiddlePlay] interstitial not shown: first 60 s of a session.
  • hasRewarded() is always true, and a rewarded test ad always ends with { rewarded: true }.
  • showBanner() draws the same labelled test bar, fixed to the bottom (or top) of the page. Here it covers that strip of your game, because a web page can't make its own window smaller.
  • exit() only logs [TwiddlePlay] exit() closes the game in the app.

TwiddlePlay.version#

JavaScript
TwiddlePlay.version // '1'
  • Type: string. Always '1' for this SDK.
  • When to use it: logging, if you want to know which SDK your game got. You never need to check it before calling anything.

TwiddlePlay.env#

JavaScript
TwiddlePlay.env // 'android' | 'host' | 'standalone'
  • Type: string. See the three environments.
  • When to use it: debugging, or showing a small "test mode" label during development.
game.jsJavaScript
TwiddlePlay.ready().then(function () {
  console.log('Running in', TwiddlePlay.env);
});

Common mistakes

  • Reading env at the very top of your script, before ready(). In a frame it may still say 'standalone'.
  • Changing gameplay based on env. Your game should behave the same everywhere. Let the SDK decide about ads.

TwiddlePlay.ready()#

JavaScript
TwiddlePlay.ready(): Promise<void>

Tells the platform your game has loaded.

  • Returns: a promise that resolves with no value once the platform has answered, or after 5 seconds if it doesn't. It never rejects.
  • When to call: once, when your game has finished loading and can be played. Calling it again returns the same promise.
game.jsJavaScript
loadImages().then(function () {
  return TwiddlePlay.ready();
}).then(function () {
  showTitleScreen();
});

Common mistakes

  • Forgetting it. Your game still works, but call it anyway: it's how hosts and test tools know your game started.
  • Waiting for ready() before drawing anything. Draw your loading screen first; ready() can take up to 400 ms in a frame.

TwiddlePlay.showInterstitial()#

JavaScript
TwiddlePlay.showInterstitial(): Promise<{ shown: boolean }>

Asks for a full-screen ad at a natural break, such as between levels.

  • Returns: { shown: true } after an ad was shown and closed, or { shown: false } if no ad was shown. It never rejects.
  • When to call: at natural breaks only, like after the player taps "Next level". Wait for the promise before you continue.
  • Most calls return { shown: false }. The app limits interstitials (nothing in the first 60 s, at least 90 s apart, at most one per 3 minutes of play), so call it at every natural break and let the app decide. See Ad limits.
  • While the ad is on screen, your game gets pause and mute, then resume and unmute when it closes.
game.jsJavaScript
async function onNextLevelTapped() {
  TwiddlePlay.gameplayStop();
  await TwiddlePlay.showInterstitial(); // shown or not, carry on
  startLevel(level + 1);
  TwiddlePlay.gameplayStart();
}

Common mistakes

  • Calling it during play, on a timer, or when the game starts. Players hate it and reviewers reject it.
  • Not waiting for the promise, so the next level starts behind the ad.
  • Treating { shown: false } as an error. It's the normal answer most of the time.
  • Calling it right after a tap that meant something else (like "Pause"), so the player taps the ad by accident.

TwiddlePlay.showRewarded()#

JavaScript
TwiddlePlay.showRewarded(): Promise<{ rewarded: boolean }>

Plays an ad the player chose to watch, in exchange for a reward.

  • Returns: { rewarded: true } if the player watched long enough to earn the reward, otherwise { rewarded: false } (no ad available, ads are off, another ad is on screen, or the player closed it early). It never rejects.
  • When to call: only right after the player taps a button that clearly says what they'll get, like "+10 seconds" or "Double coins".
  • Only one ad plays at a time. There's no other limit on rewarded ads.
game.jsJavaScript
continueBtn.addEventListener('click', async function () {
  continueBtn.disabled = true;
  const result = await TwiddlePlay.showRewarded();
  if (result.rewarded) {
    timeLeft += 10;          // give exactly what the button promised
    resumeRound();
  } else {
    showToast('No ad right now. Try again soon!');
    continueBtn.disabled = false;
  }
});

Common mistakes

  • Giving the reward when rewarded is false, or not giving it when it's true.
  • Calling it without a tap from the player (for example, automatically after a game over).
  • A button that doesn't say what the reward is, or a reward that's smaller than promised.
  • Letting the player tap the button twice. Disable it while the ad runs.

TwiddlePlay.hasRewarded()#

JavaScript
TwiddlePlay.hasRewarded(): Promise<boolean>

Tells you whether offering a rewarded ad makes sense right now.

  • Returns: true if a rewarded ad can play now. false if ads are off for your game, an ad is already on screen, or (in the app) no rewarded ad is loaded yet. Gives up and returns false after 5 seconds.
  • When to call: just before you show a rewarded button. Hide the button when it's false.
game.jsJavaScript
async function showTimeUpPanel() {
  timeUpPanel.hidden = false;
  continueBtn.hidden = !(await TwiddlePlay.hasRewarded());
}

Common mistakes

  • Checking once at startup and caching the answer. Ads load and run out; check each time you show the button.
  • Showing the button anyway when it's false. The tap would just answer { rewarded: false }.

TwiddlePlay.showBanner()#

JavaScript
TwiddlePlay.showBanner(opts?: { position?: 'bottom' | 'top' }): Promise<{ shown: boolean }>

Shows a banner: a small strip-shaped ad along the bottom (or top) of the screen, while your game shows a menu or sits between rounds.

  • Returns: { shown: true } once a banner is on screen, or { shown: false } if none could be shown: your game's ads are off, Google had no banner to give right now, or there was no answer within 15 seconds. It never rejects.
  • Options: position: 'bottom' (the default) or position: 'top'. Anything else means bottom.
  • When to call: when a title screen, menu, level select, round-clear or game-over screen appears. Never during play.
  • Safe to call again. While a banner is showing, another call keeps that one banner. If you ask for the other position, it moves there.
  • In the app, your game shrinks to make room. The banner sits next to your game, never on top of it. Your page's viewport (the part of the screen it can draw in) gets shorter by the banner's height, about 50 to 90 pixels depending on the phone, and your page gets a normal resize event, the same one a phone rotation sends. Size your game from window.innerWidth and window.innerHeight (or its container) every time resize fires.
  • A banner doesn't pause or mute your game, and it doesn't count toward the interstitial limits.
  • In the preview, a test bar appears and your game's area shrinks the same way. In standalone mode, the test bar is fixed to the bottom (or top) of the page, on top of your game. See the three environments.
game.jsJavaScript
window.addEventListener('resize', fitCanvas); // fires when a banner appears, and again when it goes

function showGameOver() {
  state = 'over';
  TwiddlePlay.gameplayStop();
  gameOverPanel.hidden = false;
  TwiddlePlay.showBanner();                   // no need to wait: the screen works the same either way
}

playAgainBtn.addEventListener('click', function () {
  TwiddlePlay.hideBanner();                   // the banner goes before play starts
  gameOverPanel.hidden = true;
  startRound();
});

Common mistakes

  • Showing it during play, or leaving it up when play starts.
  • Reading the screen size once at startup. When the banner appears, the bottom of your game gets cut off. Re-fit on every resize.
  • Putting your own buttons right next to the banner. Players tap the ad by accident; those clicks don't count, and too many get your game flagged.
  • Showing and hiding it every second or so, for example on each page of a menu the player flicks through. Show it once when the menu opens and leave it there.
  • Making the player wait for { shown: true }. Nothing in your game should depend on the answer.

TwiddlePlay.hideBanner()#

JavaScript
TwiddlePlay.hideBanner(): Promise<void>

Takes the banner away.

  • Returns: a promise that resolves with no value once the platform has answered, or after 5 seconds if it doesn't. It never rejects. You don't need to wait for it.
  • When to call: as play starts, when the player taps Play, Next level, Try again or Continue.
  • In the app, your game's viewport grows back to full size, and your page gets another resize event.
  • Calling it with no banner on screen does nothing, so it's safe to call at the start of every round.
game.jsJavaScript
function startRound() {
  TwiddlePlay.hideBanner();
  state = 'playing';
  TwiddlePlay.gameplayStart();
}

Common mistakes

  • Hiding it in only one of the places where play starts. If Play, Try again and a rewarded Continue all start play, call hideBanner() in the one function they share.

TwiddlePlay.gameplayStart() and gameplayStop()#

JavaScript
TwiddlePlay.gameplayStart(): void
TwiddlePlay.gameplayStop(): void

Optional hints that tell the platform when real play starts and stops.

  • Returns: nothing. They're fire-and-forget.
  • When to call: gameplayStart() when play begins or resumes after a menu. gameplayStop() when play stops: a menu, a level-clear screen, a game-over screen.
  • What they do today: the app and the preview accept them, and the preview lists them in SDK activity. They don't change ads or limits. Calling them is good practice and costs nothing.
game.jsJavaScript
function startRound() {
  state = 'playing';
  TwiddlePlay.gameplayStart();
}
function endRound() {
  state = 'clear';
  TwiddlePlay.gameplayStop();
}

TwiddlePlay.on() and off()#

JavaScript
TwiddlePlay.on(evt: 'pause' | 'resume' | 'mute' | 'unmute', cb: () => void): () => void
TwiddlePlay.off(evt, cb): void

Listen for the platform asking your game to pause or go quiet.

  • Returns: on() returns a function. Call it to stop listening (same as off(evt, cb)).
  • When to call: once, at startup, before play begins.
  • Your callback gets no arguments. If it throws, the SDK catches the error and logs it; other listeners still run.
Event When it fires What your game should do
pause An ad is about to show, the app goes to the background, the browser tab is hidden, or the "Leave game?" dialog opens. Freeze your game loop, timers and physics.
resume All of those are over. Unfreeze. Reset your frame timer so the game doesn't jump forward.
mute Same moments as pause. Silence all sound.
unmute Same moments as resume. Turn sound back on, unless the player muted it themselves.

Order is always pause then mute, and resume then unmute. If several reasons overlap (an ad starts while the app is in the background), you still get one pause and one resume.

game.jsJavaScript
let paused = false;
let platformMuted = false;
let playerMuted = loadSetting('muted');   // the player's own sound button

TwiddlePlay.on('pause', function () { paused = true; });
TwiddlePlay.on('resume', function () {
  paused = false;
  lastTime = performance.now();           // no giant time step after a long pause
});
TwiddlePlay.on('mute', function () { platformMuted = true; applyVolume(); });
TwiddlePlay.on('unmute', function () { platformMuted = false; applyVolume(); });

function applyVolume() {
  masterGain.gain.value = (platformMuted || playerMuted) ? 0 : 1;
}

Common mistakes

  • Using one muted flag for both the platform and the player. Then unmute turns on sound the player switched off.
  • Not resetting the frame timer on resume, so your character teleports or the round timer drops 30 seconds at once.
  • Pausing only the drawing but not the timers (setTimeout, setInterval), so the round ends during an ad.

TwiddlePlay.exit()#

JavaScript
TwiddlePlay.exit(): void

Asks the app to close your game and go back to the store.

  • Returns: nothing.
  • When to call: only when the player taps your own "Quit" or "Exit" button.
  • In the app it closes the game straight away, without the "Leave game?" question, so ask the player first if they could lose progress. In the preview it shows a message. In standalone it only logs.
game.jsJavaScript
quitBtn.addEventListener('click', function () {
  saveProgress();
  TwiddlePlay.exit();
});

Common mistakes

  • Using window.close(), history.back() or a link to leave. Games can't navigate away; use exit().
  • Forgetting to save before calling it.

You don't need an exit button at all: players can always leave with the phone's Back button.

Timeouts#

The SDK never lets your game hang waiting for an answer.

Call Gives up after Then resolves with
ready() 5 s (nothing)
hasRewarded() 5 s false
showInterstitial() 60 s { shown: false }
showRewarded() 60 s { rewarded: false }
showBanner() 15 s { shown: false }
hideBanner() 5 s (nothing)

Once an ad is actually on screen, the 60-second limit no longer applies, so a long ad the player watches to the end still pays out. There's only a 10-minute safety cap.

TypeScript#

The SDK doesn't ship type definitions. If you use TypeScript, copy this declaration into a file like twiddleplay.d.ts:

twiddleplay.d.tsTypeScript
type TwiddlePlayEvent = 'pause' | 'resume' | 'mute' | 'unmute';

interface TwiddlePlaySDK {
  readonly version: string;
  readonly env: 'android' | 'host' | 'standalone';
  ready(): Promise<void>;
  showInterstitial(): Promise<{ shown: boolean }>;
  showRewarded(): Promise<{ rewarded: boolean }>;
  hasRewarded(): Promise<boolean>;
  showBanner(opts?: { position?: 'bottom' | 'top' }): Promise<{ shown: boolean }>;
  hideBanner(): Promise<void>;
  gameplayStart(): void;
  gameplayStop(): void;
  on(evt: TwiddlePlayEvent, cb: () => void): () => void;
  off(evt: TwiddlePlayEvent, cb: () => void): void;
  exit(): void;
}

declare const TwiddlePlay: TwiddlePlaySDK;
interface Window { TwiddlePlay: TwiddlePlaySDK }

What the SDK can't do#

So you don't go looking for them:

  • No ads of your own. Only Twiddle Play's interstitials, rewarded ads and banners, through the SDK.
  • No banner sizes or places of your own. A banner is always full width, at the bottom or the top, and its height is set by the phone.
  • No in-app purchases.
  • No leaderboards, accounts or cloud saves. Save progress with localStorage on the device.
  • No way to read the player's identity or device ID.
  • No custom events or analytics. Installs, sessions and ad views are counted for you; see your game's stats in the portal.

How it works under the hood#

You never need this to make a game. It's here for engine plugin authors.

  • In the app, the SDK sends JSON strings through window.TwiddlePlayNativeBridge.postMessage(...) and receives replies through window.__twiddleplayReceive(obj).
  • In a frame, it posts { pb: 'hello' } to the parent. A { pb: 'host' } reply within 400 ms selects the frame transport.
  • Requests look like { pb: 'req', id, cmd, args? }, where cmd is ready, showInterstitial, showRewarded, hasRewarded, showBanner, hideBanner, gameplayStart, gameplayStop or exit. Only showBanner sends args: { position: 'bottom' | 'top' }, already cleaned up by the SDK.
  • Replies look like { pb: 'res', id, ok, result }. Events look like { pb: 'evt', name }.