# TipTap Games — the complete agent guide > Everything an AI agent needs to build, validate and submit a game for TipTap Games, in one document. > Canonical URL: https://tiptapgames.com/llms-full.txt If you are an agent and someone pointed you here: read section 2 before you write any code, follow the workflow in section 4, and do not stop until `validate_game_draft` returns `passed: true`. Nothing in this API publishes a game — a human moderator does that, so always say "submitted for review". ## Table of contents 1. [What TipTap Games is](#1-what-tiptap-games-is) 2. [Hard constraints — read before writing code](#2-hard-constraints--read-before-writing-code) 3. [Connecting your client](#3-connecting-your-client) 4. [The build workflow](#4-the-build-workflow) 5. [Tool reference](#5-tool-reference) 6. [Validation: what fails and what to do](#6-validation-what-fails-and-what-to-do) 7. [Errors from the MCP server](#7-errors-from-the-mcp-server) 8. [How review works](#8-how-review-works) 9. [SDK specification](#9-sdk-specification) 10. [Cookbook](#10-cookbook) ## 1. What TipTap Games is TipTap Games (https://tiptapgames.com) is a vertical swipe feed of instantly playable browser games. A player opens the feed and swipes; each game starts in place with no install, no download and no account. A game is **one self-contained HTML file**. The platform injects an SDK into it at runtime that provides scores, leaderboards, achievements, a loading screen and the focus, mute and pause lifecycle. There is no build step and no bundler. ### 1.1 What you can do here as an agent Through TipTap's MCP server, acting for the account whose credential you hold, you can: - Open a build session, read its spec, and read the SDK specification. - Upload a draft game, validate it, repair what the validator finds, and submit it for human review. - Declare achievements and set a cover image. - List and read the games that account already owns, including their HTML, so you can improve a game you did not write in this conversation. - Read a rejection's moderator note and fix the game against it. - Read analytics, leaderboard distributions and player comments for those games. - Take an approved game down. ### 1.2 What you cannot do - **Publish.** Nothing in this API makes a game public. Submitting puts it in a queue where a human moderator decides. Always report the outcome as "submitted for review". - **Reach anything you do not own.** No tool anywhere accepts a user identifier; ownership comes only from the credential. A game or session belonging to someone else answers exactly as one that does not exist. - **Replace a live game's HTML directly.** That goes through an update session, so a change to a live game passes the same validation gate as a new one. - **See another creator's data.** The only third-party text in the whole API is player comments on your own games, and it arrives explicitly fenced as untrusted. ## 2. Hard constraints — read before writing code Games run in a cross-origin iframe with `sandbox="allow-scripts"` and a strict CSP, on an **opaque origin**. These are not preferences. Each one fails silently or throws at runtime, and the validator blocks a submission that violates any of them. | Constraint | Detail | |---|---| | One file | A single self-contained `.html` document. Every byte of CSS and JavaScript inline. No build step, no second file. | | No network | `fetch`, `XMLHttpRequest`, `WebSocket` and `EventSource` are all blocked by `connect-src 'none'`. | | No external resources | ` ``` --- #### 4. Game design expectations - **Instant.** Playable within ~2 seconds. No splash screen, no menu, no "press start" unless the game truly needs one. The player is mid-scroll. - **Vertical/portrait first.** Most play happens on a phone in a feed. Handle resize and make the canvas fill its container. - **Touch and keyboard.** Both, wherever it makes sense. - **Dark background** (`#0f0f0f`, `#111827` or similar) — it sits on a dark feed. - **Score always visible**, top-centre or top-left. - **Short runs.** 30–90 seconds is the sweet spot for a feed. - **A real game-over.** Every terminal state must call `submitScore()`. --- #### 5. Testing before upload Open your `.html` directly in a browser. `window.TipTap` will be absent — the shim in the template keeps it running. To exercise the real SDK, add this during development only: ```html ``` Remove it before uploading. (Leaving it in is harmless — the platform's CSP blocks external scripts and the injected SDK is idempotent — but it does nothing.) --- #### 6. Pre-flight checklist Before you hand the file over, verify every line: - [ ] Single `.html` file, all CSS/JS inline, no external references of any kind - [ ] No `fetch`, `XMLHttpRequest`, `WebSocket`, `localStorage`, `sessionStorage`, cookies - [ ] Every `window.TipTap` use is feature-detected - [ ] `updateScore()` called as the score changes - [ ] `submitScore()` called at **every** terminal state - [ ] `showLeaderboard()` called after `submitScore()` at a real game-over — **`submitScore` alone shows the player nothing** - [ ] All audio gated on `canPlayAudio()`, re-checked on `tiptap:focus` / `tiptap:blur` / `tiptap:mutechange` - [ ] Game loop stops on `tiptap:pause` and restarts on `tiptap:resume` - [ ] `onLeaderboardClose` handled — the game was **not** reloaded - [ ] Canvas resizes correctly; playable in portrait on a phone - [ ] Playable within ~2 seconds of load - [ ] If you used `getPlayer`: the screen still reads correctly with `handle`, `displayName` **and** `avatarDataUrl` all null — that is the signed-out majority - [ ] If you used `share`: it is behind a button the player presses, not fired automatically, and any `imageDataUrl` is under 2 MB --- Full human-readable docs: https://tiptapgames.com/docs/sdk Upload: https://tiptapgames.com/create/upload Building through TipTap's MCP server rather than handing the file over yourself? https://tiptapgames.com/llms-full.txt is this document plus the complete tool reference, the build workflow, the validation error codes and the prompt cookbook. ## 10. Cookbook 14 proven prompts. Each one is complete and usable verbatim, and each ends with the platform's hard constraints so that following it produces a game that validates. A connected agent can pull the same catalog live with `list_cookbook_recipes` and `get_cookbook_recipe`; a person can read it at https://tiptapgames.com/docs/cookbook. | id | Title | Category | |---|---|---| | `one-thumb-runner` | One-Thumb Endless Runner | genre-starters | | `tap-timing-bar` | Tap-Timing Precision Bar | genre-starters | | `swipe-grid-puzzle` | Swipe Grid Puzzle | genre-starters | | `sixty-second-idle` | Sixty-Second Idle Clicker | genre-starters | | `ramping-difficulty` | Difficulty That Ramps Without a Wall | mechanics | | `combo-multiplier` | Combo Multiplier Scoring | mechanics | | `near-miss-reward` | Near-Miss Rewards | mechanics | | `juice-shake-particles` | Screen Shake, Hit-Stop and Particles | game-feel | | `synth-audio-guard` | Synth Sound With the Audio Guard | game-feel | | `share-worthy-moments` | Share-Worthy Moments | game-feel | | `achievements-that-validate` | Achievements the Validator Accepts | sdk-patterns | | `pause-resume-correct` | Pause, Resume and Focus, Correctly | sdk-patterns | | `leaderboard-moments` | Choosing the Leaderboard Moment | sdk-patterns | | `personal-touch-avatar` | The Player, In The Game | sdk-patterns | ### 10.1 Genre starters Complete briefs for a whole game, from nothing. #### One-Thumb Endless Runner - **id:** `one-thumb-runner` - **category:** `genre-starters` - **tags:** runner, one-touch, endless, canvas An auto-running dodge-and-jump game driven by a single tap anywhere on screen. Legible in one glance, hard to put down. **Technique notes — the things that are wrong in most first attempts:** - Step physics on a fixed 1/60 s accumulator rather than on raw dt. A 120 Hz phone and a throttled tab otherwise produce different jump arcs, and the second one is unplayable. - Spawn obstacles against a distance counter, never a timer. When the scroll speed ramps, a timer quietly shrinks the gaps below what the jump arc can clear. - Keep obstacles in one array of plain objects and reuse dead entries. Allocating a new object per spawn is what makes a runner hitch every few seconds. - Clamp dt to about 1/20 s before integrating. One long frame otherwise teleports the player through an obstacle. **Prompt — usable verbatim:** ```text Build a one-thumb endless runner for TipTap Games. The shape of it: - The character auto-runs at a constant speed on the left third of the screen and the world scrolls toward them. - ONE input: a tap or click anywhere on the canvas, plus Space and ArrowUp for keyboard. No on-screen buttons, no swipe, no second gesture. - That input jumps. Variable jump height is the one variation worth adding: apply reduced gravity while the pointer is still held, for at most 180 ms. - Two obstacle kinds is enough — something to jump over and a gap to clear. Never spawn a pair that cannot be passed; check every spawn against the jump arc at the current speed before you commit it. - One hit ends the run. No health bar. A good player should last 30 to 90 seconds. Scoring: - Distance travelled, plus a small bonus per obstacle cleared. Integers only. - Call TipTap.updateScore on every change so the platform's top bar tracks the run. Difficulty: - Ramp scroll speed from comfortable to roughly 1.8x over 90 seconds, on a curve that flattens rather than a straight line. - Ramp obstacle density with it, but hold a minimum gap that scales with the current speed so the game never becomes unclearable. Feel: - Two or three parallax background layers at different speeds. Flat colours and simple shapes are fine; this is not a sprite exercise. - A squash on landing, two or three particles on takeoff, a short colour flash on impact. - 3 to 5 px of screen shake for about 120 ms on death only, never during normal play. Game over: - Freeze the world for about 250 ms so the player sees what killed them, then submit the score and raise the leaderboard. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Tap-Timing Precision Bar - **id:** `tap-timing-bar` - **category:** `genre-starters` - **tags:** timing, one-touch, arcade, precision A sweeping marker and a shrinking target zone. One tap per round, scored on accuracy. Understood in two seconds, mastered in fifty runs. **Technique notes — the things that are wrong in most first attempts:** - Drive the marker from accumulated time, not from a per-frame increment. A dropped frame otherwise changes where the marker is, and the player is punished for your frame budget. - Judge accuracy against the marker position at the moment of the pointerdown event, not at the next animation frame. That one line is the difference between tight and mushy. - Use pointerdown rather than click, and set touch-action:none on the body. click adds roughly 100 ms of browser delay on some mobile browsers. - Shrink the zone on a curve with a hard floor (never below about 4% of the bar). A zone that keeps shrinking linearly ends the game on luck rather than skill. **Prompt — usable verbatim:** ```text Build a tap-timing precision game for TipTap Games. The shape of it: - A horizontal bar across the middle of the screen. A marker sweeps left to right and back, continuously. - A target zone sits somewhere on the bar. One tap per round locks the marker. - Score the round by how close the marker was to the centre of the zone: three bands is right. Perfect (the middle fifth of the zone), Good (the rest of the zone), Miss (outside it). - Perfect and Good advance the round. A Miss costs one of three lives. - Every round: move the zone somewhere new, shrink it slightly, and speed the sweep up slightly. Scoring: - Perfect scores triple a Good. Chain perfects for a multiplier that caps out — see the combo-multiplier recipe if you want the full treatment. - Show the band you hit as large text for about 400 ms. This is the entire feedback loop of the game; do not make it subtle. Feel: - Snap the marker to the exact lock position and hold it there for the feedback beat before the next round starts. A marker that keeps moving through the result reads as a bug. - A perfect is worth a brief flash, a rising tone and a handful of particles at the lock point. A miss is worth a red pulse and a short screen shake. - Keep the whole loop at one tap: no confirm, no continue button between rounds. Game over: - On the third miss, hold the final frame for about 250 ms, then submit and show the board. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Swipe Grid Puzzle - **id:** `swipe-grid-puzzle` - **category:** `genre-starters` - **tags:** puzzle, swipe, grid, turn-based A four-by-four board where one swipe slides everything and merges matching tiles. Turn-based, no timer, thumb-sized targets. **Technique notes — the things that are wrong in most first attempts:** - Resolve a swipe on pointerup from the dominant axis of the total delta, with a threshold of about 24 px. Reading direction per pointermove produces diagonal swipes that fire twice. - Set touch-action:none on the play area or the browser scrolls the feed instead of taking your swipe. - Merge each tile at most once per move, and resolve from the far edge toward the near one. Both bugs are invisible until a row of four identical tiles collapses to one instead of two. - Ignore a move that changes nothing: no slide, no merge, no spawn. Spawning on a no-op move is the classic way these games become unwinnable. - Animate from the previous board to the next one rather than animating the model. Keeping the model instantaneous is what makes rapid swipes stay correct. **Prompt — usable verbatim:** ```text Build a swipe grid puzzle for TipTap Games. The shape of it: - A four-by-four board of tiles, each holding a value. The board fills the width of the portrait viewport with a comfortable margin. - A swipe in any of the four directions slides every tile as far as it can go in that direction. Two adjacent tiles of equal value merge into one of the next value, and each tile merges at most once per move. - Every move that actually changed the board spawns one new low tile in a random empty cell. - The game ends when no move can change the board. - Keyboard arrows do the same thing as swipes. Scoring: - Score is the sum of the values created by merges, so a merge into a high tile is worth more than several low ones. - Call TipTap.updateScore after each move resolves. Feel: - Slide animations of about 120 ms and a merge pop of about 100 ms. Anything slower and rapid play feels blocked; anything faster and the player cannot follow what moved. - The board should be readable at a glance: a distinct colour per value, big numerals, high contrast. - No timer, no pressure. This is the game someone plays while thinking. Game over: - Detect the no-legal-move state immediately after the spawn, not at the start of the next input. Dim the board, submit the score, and raise the leaderboard. Because this game has no run timer, treat the dead board as the only terminal state, and let the player pick up a fresh board from the leaderboard's Continue. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Sixty-Second Idle Clicker - **id:** `sixty-second-idle` - **category:** `genre-starters` - **tags:** idle, incremental, tap, one-minute A whole incremental arc compressed into one minute: tap, buy generators, watch the curve run away, bank the score. **Technique notes — the things that are wrong in most first attempts:** - Nothing survives a reload — there is no storage on the sandbox origin — so the run IS the save file. Design the arc to complete inside the timer instead of trying to persist it. - Accumulate production from dt, not from a setInterval tick. An interval drifts and stops being paid while the tab is backgrounded, which silently makes the run shorter. - Price generators geometrically (cost = base * 1.15^owned) and recompute the affordable set once per frame, not per button. - Numbers pass 1e6 inside a minute: write one formatter (12.4K, 3.1M, 880M) and use it everywhere, including the score you submit. **Prompt — usable verbatim:** ```text Build a one-minute idle clicker for TipTap Games. The premise: an idle game has no save file here — the sandbox has no storage at all — so compress the entire arc into a single 60-second run. The player should feel the curve run away from them before the timer ends. The shape of it: - A big tappable target in the lower half of the screen, thumb-reachable. Each tap adds to the resource directly. - Three or four generator types in a list above it, each with a cost, an owned count and a per-second yield. Buying one raises its own price by about 15%. - A 60-second countdown across the top, running from the first input. - Production accrues continuously from elapsed time, not on a tick. The arc, and this is the design work: - Seconds 0-10: only tapping matters. - Seconds 10-30: the first generator out-earns tapping, and the player notices. - Seconds 30-50: the third generator turns the curve vertical. - Seconds 50-60: nothing left to buy, just watching the number climb. Tune the base costs and yields until that is what actually happens. Scoring: - The score is the total resource EARNED over the run, not the balance left after purchases. Spending must never lower the score, or buying feels like a punishment. Feel: - The tap target wants a scale-down on press and a number that floats up and fades. - A generator becoming affordable should announce itself — a glow, a subtle pulse. - Keep every buy button at least 44 px tall. This game is played with one thumb at speed. Game over: - At zero, freeze the counters, submit the earned total, and show the board. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` ### 10.2 Mechanics Systems to drop into a game that already runs. #### Difficulty That Ramps Without a Wall - **id:** `ramping-difficulty` - **category:** `mechanics` - **tags:** difficulty, pacing, tuning, balance Replace a linear speed-up with a saturating curve, a provably clearable floor, and parameters that ramp on their own schedules. **Technique notes — the things that are wrong in most first attempts:** - d = 1 - Math.exp(-t / TAU) with TAU around 45 saturates near 1 and never runs off. A linear ramp always ends in a wall; the only question is when. - Derive difficulty from elapsed time, never from score. Score-driven difficulty is a feedback loop: the player who is doing well gets punished for it, and a combo spike can double the speed in one frame. - Assert the feasibility floor in code — minGap >= speed * REACTION_BUDGET with a budget of about 0.28 s — and clamp rather than trusting the tuning to stay honest. - Ramp one parameter per axis of difficulty and stagger their curves. Speed, density and variety all ramping together is what produces a cliff at 40 seconds. **Prompt — usable verbatim:** ```text Rework the difficulty curve of this game so it ramps continuously and never becomes unclearable. Replace whatever linear ramp is in there with a single normalised difficulty value: d = 1 - Math.exp(-elapsedSeconds / TAU) with TAU around 45 seconds. d starts at 0, passes 0.5 near 30 seconds, and approaches but never reaches 1 — so the game keeps getting harder forever without ever reaching a speed nobody can play. Then: - Drive difficulty from elapsed time only, never from the score. Score-driven difficulty punishes the player for doing well, and a combo spike can double the speed inside one frame. - Map each parameter separately, from its own min to its own max, so they do not all peak together: scrollSpeed = lerp(SPEED_MIN, SPEED_MAX, d), spawnGap = lerp(GAP_MAX, GAP_MIN, d), and so on. - Stagger them. Let speed lead, bring density in after about 15 seconds, and hold new obstacle types back until d passes 0.4. Everything ramping at once is what produces a cliff. - Enforce a feasibility floor. Whatever the difficulty says, the gap between hazards must stay at least (currentSpeed * 0.28 s) — one human reaction time. Clamp it in code, do not merely tune for it. - Give the first eight seconds a guaranteed-gentle opening, identical every run. Players judge a feed game in its first three seconds and a random hard opener reads as the game being broken. Then tune by playing: the target is 30 to 90 seconds for a competent player and a death that always feels like the player's fault. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Combo Multiplier Scoring - **id:** `combo-multiplier` - **category:** `mechanics` - **tags:** scoring, combo, scoreMax, balance A streak bonus that rewards mastery without exploding past scoreMax: capped multiplier, decaying window, and a ceiling you can defend. **Technique notes — the things that are wrong in most first attempts:** - The platform enforces scoreMax server-side and a submission above it is rejected with a 400 — the player's whole run is lost. Clamp the score before you submit, and set scoreMax deliberately with update_game_metadata rather than leaving the 999999 default. - Compute the ceiling: maxRounds * basePoints * MULT_CAP, plus roughly 20% headroom. If that number is above a million, lower the base points rather than raising the cap. - Cap the multiplier at about 8x. An uncapped multiplier means the top of the leaderboard is one lucky run rather than the best player. - Show the combo timer as a shrinking bar. A streak the player cannot see the deadline for is a streak they will lose to nothing they can name. **Prompt — usable verbatim:** ```text Add combo multiplier scoring to this game. The mechanic: - Every successful action extends a streak and refreshes a combo window of about 2 seconds. Letting the window expire, or failing an action, resets the streak to 0. - The multiplier is derived, not accumulated: mult = Math.min(1 + Math.floor(streak / 5), MULT_CAP) with MULT_CAP of 8. Deriving it means the multiplier can never drift out of step with the streak after a bug. - Points for an action are basePoints * mult, rounded to an integer. Scores are integers on this platform. Make it visible: - The multiplier is displayed whenever it is above 1x, and it grows and pulses each time it steps up. - The combo window is a shrinking bar under the multiplier. A player who cannot see the deadline loses streaks to nothing they can name, which feels arbitrary. - Losing a streak deserves as much feedback as gaining one: a short desaturation, the number falling away. Now the part most games get wrong — the ceiling: - Work out the theoretical maximum score for a very long, perfect run: maxActions * basePoints * MULT_CAP. Add about 20% headroom. - Set the game's scoreMax to that number (through update_game_metadata if you are working through the MCP server). The default is 999999, which does no anti-cheat work for a game that tops out at 4000 and rejects legitimate runs from a game that tops out at two million. - Clamp the score in the game as well, immediately before submitting. A submission above scoreMax is rejected with a 400 and the player loses the entire run — an arithmetic overflow in a bonus must never cost somebody their best score. - If the theoretical maximum is above a million, reduce basePoints rather than the cap. Small numbers that climb are more readable in a feed than large ones. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Near-Miss Rewards - **id:** `near-miss-reward` - **category:** `mechanics` - **tags:** risk-reward, feel, scoring Pay the player for almost dying. A proximity band, a slow-motion beat and a bonus turn dodges into the best moment of a run. **Technique notes — the things that are wrong in most first attempts:** - Award the near miss when the hazard has passed the player, not when it is closest. Awarding on approach pays out for a dodge the player has not survived yet. - Flag each hazard as already-awarded. Without it a single obstacle pays every frame it spends inside the band, which is a score exploit anyone finds in one run. - Time dilation is a multiplier on dt, not a change to the frame rate. Keep rendering at full rate and scale the simulation, or the reward beat reads as a stutter. - Scale the bonus by how close the pass was, but bound it. An unbounded closeness bonus makes a one-pixel graze worth more than the rest of the run. **Prompt — usable verbatim:** ```text Add near-miss rewards to this game, so surviving narrowly is worth more than playing safe. The mechanic: - Around the player's hitbox, define a band about 1.5 to 2 times its radius. A hazard that passes through the band without touching the hitbox is a near miss. - Award it at the moment the hazard has passed the player, not at closest approach. Paying out on approach rewards a dodge the player has not actually survived. - Mark each hazard as awarded so it can only ever pay once. Without that flag, one obstacle pays every frame it spends inside the band, and it is the first exploit anyone finds. - The bonus scales with closeness, bounded: bonus = Math.round(BASE * (1 + (1 - distance / bandRadius))), so a graze is worth roughly double a wide pass and never more. The beat, which is what makes it feel good: - On the award, scale dt by about 0.35 for 120 ms. Keep rendering at full rate and slow the simulation, so it reads as slow motion rather than as a dropped frame. - Flash the band outline, emit a few particles along the hazard's path, and float the bonus number where the pass happened. - A rising pitch that steps up with consecutive near misses, if the game has sound. Interaction with the rest of the game: - If there is a combo system, a near miss should extend the streak, not just add points. That is what turns near misses from a bonus into a playstyle. - Do not let a near miss ever be safer than a wide dodge — the risk has to be real, or the reward means nothing. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` ### 10.3 Game feel The cheap upgrades players notice immediately. #### Screen Shake, Hit-Stop and Particles - **id:** `juice-shake-particles` - **category:** `game-feel` - **tags:** juice, particles, screen-shake, canvas The three cheapest upgrades to how a game feels, written to stay inside the sandbox CSP and the platform's pause contract. **Technique notes — the things that are wrong in most first attempts:** - Shake from a trauma value: trauma += 0.4 on a hit, decay it at about 1.5 per second, and use shake = trauma * trauma for the offset. Squaring is what makes a big hit feel different from a small one instead of everything rattling equally. - Apply shake with ctx.save() / ctx.translate() / ctx.restore() around the world draw, and leave the HUD outside it. A shaking score readout is nausea, not juice. - Hit-stop freezes the simulation while rendering continues. Skip the update call for 60 to 90 ms; do not skip the frame, and never do it with a blocking loop. - Preallocate a fixed particle pool (256 is plenty) with an alive flag. Allocating per burst is what turns a satisfying explosion into a garbage-collection hitch. - No external images: img-src allows only data: and blob:. Draw shapes, or generate a sprite once into an offscreen canvas at startup. - Honour prefers-reduced-motion by cutting shake amplitude to zero and halving particle counts. Everything else can stay. **Prompt — usable verbatim:** ```text Add game feel to this game with the three effects that pay for themselves, and keep all of them inside the platform's sandbox. 1. Screen shake, driven by trauma - Keep a single trauma value in 0..1. An event adds to it (a big hit 0.5, a small one 0.2) and it decays at about 1.5 per second. - The offset is trauma squared, times a maximum of about 8 px, times a smoothly varying random direction. Squaring is what makes a big hit feel different from a small one instead of everything rattling the same. - Apply it with ctx.save(), ctx.translate(ox, oy), draw the world, ctx.restore(). Draw the HUD outside that transform — a shaking score is nausea, not juice. 2. Hit-stop - On a significant impact, stop advancing the simulation for 60 to 90 ms while continuing to render. The frozen frame is what sells the weight. - Implement it by skipping the update call, never by blocking. Never with a busy loop, and never with an await that outlives a pause. - Use it sparingly: on death, on a boss hit, on a perfect. On every collision it just reads as lag. 3. Particles - One preallocated pool of about 256 particles, each with position, velocity, life and colour, and an alive flag. Never allocate during a burst. - Emit 8 to 16 on an impact, with randomised speed in a cone away from the point of contact, gravity, and alpha fading with remaining life. - Draw them as filled circles or squares. No external images — only data: and blob: URLs load in the sandbox, so generate anything fancier into an offscreen canvas at startup. Two rules that keep this correct: - Every one of these effects must stop when the platform pauses the game. They live inside the same update the tiptap:pause handler stops; nothing here gets its own timer. - Respect prefers-reduced-motion: drop shake to zero and halve the particle counts. Some players cannot use a game that shakes. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Synth Sound With the Audio Guard - **id:** `synth-audio-guard` - **category:** `game-feel` - **tags:** audio, web-audio, focus, mute Web Audio built from oscillators alone, wrapped in the focus and mute checks that stop two games playing at once in the feed. **Technique notes — the things that are wrong in most first attempts:** - canPlayAudio() already folds together on-screen and not-globally-muted. Check that one function rather than composing isFocused() and isMuted() yourself. - Create the AudioContext lazily inside the first real user gesture and call resume() if its state is suspended. Building it at load leaves it suspended forever on most mobile browsers. - Route everything through one master GainNode. Mute is then a single ramp instead of a hunt through every live voice. - Ramp gain to 0.0001, never to exactly 0 — exponentialRampToValueAtTime cannot reach zero and will throw or silently do nothing. - On tiptap:blur, ramp the master down over about 40 ms and then suspend the context. Cutting gain instantly clicks; leaving the context running drains battery in a feed. - Two games playing at once is the worst bug on this platform, and the platform cannot reach inside your audio graph. Stopping your own sound is your job. **Prompt — usable verbatim:** ```text Give this game sound, synthesized entirely in Web Audio, and wire it into the platform's focus and mute contract. Why synthesized: the sandbox allows media only from data: and blob: URLs, and embedding real audio files would eat the 5 MB budget. Oscillators and noise buffers cost nothing and are enough for an arcade game. The voices to build: - A short blip for a positive action: square or triangle, 60 to 120 ms, a quick pitch rise, sharp attack and exponential decay. - A lower thud for a negative one: sine or filtered noise, around 200 ms, pitch falling. - Optional bed: two detuned oscillators through a lowpass, gain around 0.05. Keep it quiet enough that a player never reaches for their volume. - Pitch the positive blip up a semitone per combo step, capped after about eight steps. This is the single most satisfying line of audio code in an arcade game. The guard, which is not optional: - Create the AudioContext lazily inside the first real user gesture, and call resume() if its state is suspended. Creating it at load leaves it suspended forever on most mobile browsers. - Route every voice through one master GainNode, so muting is one ramp rather than a hunt through live voices. - Check TipTap.canPlayAudio() at the top of every sound function and return immediately when it is false. It already folds together "on screen" and "not globally muted" — do not recompose that from isFocused() and isMuted(). - Re-check on tiptap:focus, tiptap:blur and tiptap:mutechange. On blur, ramp the master gain down over about 40 ms and then suspend the context; on focus, resume it only if canPlayAudio() is true. - Ramp to 0.0001 rather than 0. An exponential ramp cannot reach zero. Players swipe between games constantly, and the platform cannot reach inside your audio graph. Two games playing at once is the worst bug on this platform and preventing it is the game's job, not the platform's. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Share-Worthy Moments - **id:** `share-worthy-moments` - **category:** `game-feel` - **tags:** share, sdk, retention, canvas Turn a personal best into a share: the canvas snapshot, the caption, and the one rule that keeps it from becoming a nag. **Technique notes — the things that are wrong in most first attempts:** - TipTap.share is SDK 2.1 and optional. Feature-detect it with typeof TipTap.share === 'function' — a game that assumes it exists breaks when opened directly during development. - Offer the share, never trigger it. share() opens a sheet over the game; calling it automatically at every game over is the fastest way to teach players to dismiss your end screen without reading it. - Snapshot budget: the image must be a data: URL of type png, jpeg or webp and at most 2 MB decoded. A full-resolution canvas PNG blows through that easily — downscale to about 1080px on the long edge first. - Draw the snapshot on a SEPARATE offscreen canvas: final frame, then the score composited large. The live canvas is sized for a phone viewport and its raw pixels rarely read well as a shared image. - toDataURL works here because everything on your canvas came from this document. There are no external images in the sandbox to taint it with. - Pass no image and the platform generates a branded score card for you. That is the right default — only send your own when the final frame genuinely says something, like a finished maze or a tower the player built. - share() is fire and forget. There is no callback and no result: the game is never told whether the player actually shared, so never gate a reward or progression on it. **Prompt — usable verbatim:** ```text Add sharing to this game at the moments that deserve it. The API, which is SDK 2.1 and optional: TipTap.share({ score: 4120 }) TipTap.share({ score: 4120, text: 'Cleared floor 9 without taking a hit' }) TipTap.share({ score: 4120, imageDataUrl: snapshot }) Feature-detect it (typeof TipTap.share === 'function') like every other SDK call. It opens the platform's share sheet, and that is all it does — there is no callback, no result, and the game is never told whether anything was shared. Choosing the moment, which is the whole recipe: - A personal best. This is the one that matters — the player just did something they had not done before and they know it. - A first clear, a rare outcome, a run that ended in a way worth showing. - NOT every death. A share prompt on every game over is a nag, and players learn to dismiss the end screen without reading it. You lose the prompt AND the end screen. Offer it, do not fire it. Put your own Share button on your end screen next to the score, and light it up only when the run cleared the bar. The player taps it, you call share(). Calling share() unprompted throws UI over a game the player was still looking at. The caption: one specific line about what happened, not a generic boast. "Cleared floor 9 without taking a hit" is worth reading; "I scored 4120" is what the platform already writes for you. Building the snapshot, if the final frame is worth showing: - Draw it on a SEPARATE offscreen canvas rather than reusing the live one. Your live canvas is sized for a phone viewport and its raw pixels rarely read well shared. - Compose deliberately: the final frame, then the score large over it, then anything that explains the run. Aim for a landscape-ish frame around 1080px on the long edge. - Export with toDataURL. Use image/png for flat colour and sharp edges, image/jpeg or image/webp at about 0.85 quality for anything with gradients or particles. - Stay under 2 MB decoded. Over that the platform drops the image, logs why, and opens the sheet with its generated card instead — so an oversized snapshot degrades rather than failing, but you lose the picture you meant to send. If you send no image at all the platform generates a branded score card with the score, the game and the player on it. That is a good default. Only send your own when the final frame says something the score alone cannot. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` ### 10.4 SDK patterns Getting the platform integration right the first time. #### Achievements the Validator Accepts - **id:** `achievements-that-validate` - **category:** `sdk-patterns` - **tags:** achievements, validation, sdk Declare the keys first, unlock them from a plain condition, and pass the cross-check that compares your code against your declarations. **Technique notes — the things that are wrong in most first attempts:** - Validation cross-checks declared keys against the unlockAchievement calls it can see in the source. Pass literal strings — TipTap.unlockAchievement('combo_10') — never a variable, a template literal or a computed key, or the scan finds nothing and the check fails on a game that works. - Declare the achievements BEFORE calling validate_game_draft (set_game_achievements stages them on the session). Validating first means cross-checking against an empty list. - Unlocks are idempotent server-side and unknown keys are ignored, so the call can sit directly inside the condition with no has-it-fired bookkeeping. - Key format is [a-zA-Z0-9_-] up to 64 characters, name up to 80, description up to 200, icon a single emoji of at most 8 characters, at most 50 achievements. - Do not invent achievements the user did not ask for. If they did, three to six with a real spread of difficulty beats twenty. **Prompt — usable verbatim:** ```text Add achievements to this game so they work at runtime and pass validation. How the pieces fit: - Achievement keys are declared on the game, and the game calls TipTap.unlockAchievement('the_key') when the condition is met. Validation cross-checks the two: every declared key must be reachable from the code, and every key the code unlocks must be declared. An unknown key is ignored server-side, so a mismatch is silent at runtime and only validation will tell you. - Declare them first. If you are working through the MCP server, call set_game_achievements before validate_game_draft — validating first cross-checks against an empty list. Writing the calls: - Always a literal string: TipTap.unlockAchievement('survive_60s'). Never a variable, never a template literal, never a key built from a loop index. The cross-check reads the source, so a computed key is invisible to it and the check fails on a game that works perfectly. - Unlocks are idempotent and repeat calls are no-ops, so put the call directly inside the condition. No "have I already fired this" bookkeeping is needed. - Feature-detect like every other SDK call, so the file still runs when opened directly. Choosing them — three to six, with a spread: - One almost everyone gets in their first run, so the mechanic announces itself. - Two or three at genuine skill milestones: a score threshold, a streak, surviving past a difficulty step. - One that rewards playing differently rather than playing longer — a perfect round, a clear without using the main mechanic, a full board. Each needs a key ([a-zA-Z0-9_-], up to 64 characters), a name (up to 80), a short description (up to 200) and one emoji as its icon. Do not add achievements nobody asked for. If the brief did not mention them, ask before spending the file size and the validation surface on them. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Pause, Resume and Focus, Correctly - **id:** `pause-resume-correct` - **category:** `sdk-patterns` - **tags:** lifecycle, pause, raf, sdk The lifecycle wiring that stops a game double-speeding after a resume, draining battery off-screen, or playing over the next game. **Technique notes — the things that are wrong in most first attempts:** - One place calls requestAnimationFrame. A second call site is how a resume ends up running two loops at once, and the game runs at double speed with no error anywhere. - Reset lastT to 0 on resume and treat a zero lastT as dt = 0. Without it, the first frame back carries the entire time the game spent off-screen and teleports everything. - Clamp dt to about 1/20 s on every frame regardless. That covers a backgrounded tab and a slow first frame in one line. - Keep the rAF handle and cancelAnimationFrame on pause. Returning early from the callback without cancelling leaves the browser scheduling frames you throw away. - setInterval and setTimeout keep firing while paused. Anything that changes game state belongs in the update function, not on a timer. - The platform's pause is not your game-over. Keep them as separate flags, or the leaderboard overlay ends the run underneath it. **Prompt — usable verbatim:** ```text Fix this game's lifecycle handling so it behaves correctly when the player scrolls away, comes back, or opens the leaderboard. The four bugs to eliminate: 1. Double speed after a resume. Exactly one place in the file may call requestAnimationFrame. Keep the handle, cancelAnimationFrame on pause, and on resume start the chain from that single place. A second call site means two loops running at once, which shows up as double speed with no error anywhere. 2. The teleport on return. Reset the timestamp on resume (lastT = 0) and treat a zero lastT as dt = 0 for that frame. Otherwise the first frame back carries the entire time the game was off-screen and everything jumps. Clamp dt to about 1/20 s on every frame regardless. 3. State changing while paused. setInterval and setTimeout keep firing when the game is paused. Anything that mutates game state belongs in the update function, driven by dt, and nowhere else. 4. Sound over the next game. On tiptap:blur, stop your audio — the platform pauses the game but cannot reach inside your audio graph. Gate every sound on TipTap.canPlayAudio(). The wiring: - tiptap:pause sets paused = true and cancels the frame. tiptap:resume clears it, resets lastT, and restarts the chain. - tiptap:focus, tiptap:blur and tiptap:mutechange all re-run the same audio-sync function. - TipTap.onLeaderboardClose fires when the player pressed Continue. Nothing was reloaded and no state was lost, so resume the existing run if the game has progression, or start a fresh one if it does not. Decide which deliberately. Keep the platform's paused flag separate from your own running / game-over flag. Collapsing them into one is how the leaderboard overlay ends the run underneath itself. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### Choosing the Leaderboard Moment - **id:** `leaderboard-moments` - **category:** `sdk-patterns` - **tags:** leaderboard, sdk, score, ux submitScore is silent in SDK v2. Where you call showLeaderboard is what decides whether a run ends well or merely stops. **Technique notes — the things that are wrong in most first attempts:** - submitScore() no longer opens the leaderboard — that changed in SDK v2. Ported code that relied on the old behaviour submits successfully and shows the player nothing. - submitScore is the database write and updateScore is the live top-bar number. Calling submitScore every frame is a write per frame; call updateScore instead. - Continue does not reload the game and destroys nothing. Play again remounts from scratch. Next game moves on through the feed. Only the first one hands control back to your code. - Hold the final frame about 250 ms before raising the board, so the player sees what ended the run. - onResult delivers rank, percentile, personalBest, ghostScore and ghostHandle. Read it on the way in and spend it on the next run, not on the game-over screen the platform already owns. **Prompt — usable verbatim:** ```text Get this game's score submission and leaderboard timing right. The rule that catches everyone: TipTap.submitScore(score) is SILENT. It writes to the leaderboard and returns, and your game keeps running. It used to raise the board automatically and it no longer does, so ported code submits successfully and shows the player nothing. TipTap.showLeaderboard() is what the player actually sees. Where each call goes: - TipTap.updateScore(score) on every score change. No database write, free, and it is what drives the platform's live top bar. - TipTap.submitScore(score) at EVERY terminal state — death, timer expiry, board solved, level complete. Not every frame, and not on a pause. - TipTap.showLeaderboard() immediately after the submit, but only at a real game over. Hold the last frame for about 250 ms first so the player sees what ended the run. Games with progression need the split: - A roguelike run, a level, a partially filled board — submit the score and let the player decide. Continue closes the overlay and hands control straight back with nothing reloaded and no state lost. - Play again remounts the game from scratch and destroys everything. Next game moves on through the feed. Only Continue returns to your code, through TipTap.onLeaderboardClose. - So handle onLeaderboardClose deliberately: resume the run in progress if the game has one, start a fresh run if it does not. Doing neither leaves a frozen game behind a dismissed overlay. Spending the result well: - TipTap.onResult gives you rank, percentile, personalBest, ghostScore and ghostHandle. The platform already shows the board, so use these for something it cannot: a "beat your best by 40" line on the NEXT run, or a ghost marker on the bar showing the score to beat. - Scores are integers, and each game has a server-enforced scoreMax. A submission above it is rejected with a 400 and the run is lost, so clamp before you submit. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` #### The Player, In The Game - **id:** `personal-touch-avatar` - **category:** `sdk-patterns` - **tags:** player, avatar, sdk, identity Put the player's own avatar and name into the game, with the null guard that keeps it working for the signed-out majority. **Technique notes — the things that are wrong in most first attempts:** - Every field is independently null. The callback always receives an OBJECT, so p itself never needs a null check — handle, displayName and avatarDataUrl each do. - Most of the feed is signed out. The anonymous player — all three fields null — is the common case, not the edge case. Build that version of the screen first and treat the avatar as the enhancement. - The reply is cached by the SDK for the life of the document, so calling getPlayer again is free. Still keep your own reference, so your draw path is synchronous and never waits on a callback mid-frame. - avatarDataUrl is a 96x96 data URL. Load it into an Image and wait for onload before drawImage — a data URL decodes fast but not instantly. Handle onerror too and fall back to the initial. - Do not scale the avatar much past 128px. It is 96 pixels square and it turns to mush. - Ask for it only if you use it. getPlayer costs the host a canvas draw and a base64 encode; calling it and ignoring the answer is pure waste. **Prompt — usable verbatim:** ```text Use the player's own identity inside this game. The API, which is SDK 2.1 and optional: TipTap.getPlayer(function (p) { // p.handle, p.displayName, p.avatarDataUrl }); It also returns a Promise where one exists, so await works too. Feature-detect (typeof TipTap.getPlayer === 'function') like every other SDK call. The contract, and the part that breaks games: - The callback ALWAYS receives an object, so p itself never needs a null check. - handle, displayName and avatarDataUrl are each INDEPENDENTLY null. A signed-out player has no handle and no avatar. A signed-in player may still have no avatar. - Most of the feed is signed out. The all-null player is the common case. Write that version of the screen first and treat the avatar as decoration on top of it. - The reply is cached for the life of the document. Call it once at boot, keep the result in a variable, and draw from that variable — never wait on a callback in the middle of a frame. The guard, every time: var me = { handle: null, avatar: null }; TipTap.getPlayer(function (p) { me.handle = p.handle; if (!p.avatarDataUrl) return; var img = new Image(); img.onload = function () { me.avatar = img; }; img.src = p.avatarDataUrl; }); Then at the draw site: if me.avatar, draw it; else if me.handle, draw its first letter in a coloured disc; else draw a neutral silhouette. Never a broken image, never the word "null", never a gap where a face should be. Where it is worth doing: - On the podium or trophy on your own end screen. This is the best one — the player just earned it and their face is on it. - On the ghost or rival marker, so the score you are chasing has someone attached. - Beside their row in an in-game score list. - Their name in a single line of result text: "Nice run, nova." Where it is not: - Plastered on every enemy, projectile or tile. It stops being a personal touch and becomes a joke at the player's expense. - On the loading screen or a splash. The game must be playable in about two seconds and this is not what that time is for. - Anywhere the game breaks without it. If a signed-out player gets a worse game rather than the same game with less decoration, the feature is wrong. Why this is safe to hand a game: it is the same handle and picture already shown on every leaderboard and profile page, so nothing private crosses the boundary. And the game sandbox runs under connect-src 'none' — it has no network at all — so the data physically cannot leave the frame it was drawn in. Platform rules, all enforced by the validator: - ONE self-contained .html file. Every byte of CSS and JavaScript inline. No build step, no second file, no external script, stylesheet, font or image. - No network at runtime. fetch, XMLHttpRequest, WebSocket and EventSource are all blocked by connect-src 'none'. - No localStorage, sessionStorage, indexedDB or cookies. The game runs on an opaque sandbox origin where those THROW rather than returning null, so keep every piece of state in memory. - Assets as data: or blob: URLs only. 5 MB for the whole file. - Portrait-first. The game is played inside a phone-shaped frame in a vertical feed: fill the container, handle resize, and treat landscape as a bonus. - Dark background, score always visible, playable within about two seconds of load. No splash screen and no menu unless the game genuinely needs one. - window.TipTap is injected by the platform. Never define it, import it, or add a script tag for it. Feature-detect every use so the file still runs when opened directly during development. - Call TipTap.updateScore(score) as the score changes, TipTap.submitScore(score) at every terminal state, and TipTap.showLeaderboard() after the submit at a real game over. submitScore is silent and shows the player nothing on its own. - Gate every sound on TipTap.canPlayAudio() and re-check it on the tiptap:focus, tiptap:blur and tiptap:mutechange events. - Stop the requestAnimationFrame chain on tiptap:pause and restart it on tiptap:resume. Handle TipTap.onLeaderboardClose: the player pressed Continue, nothing was reloaded, and your state is intact. - No API keys or credentials anywhere in the file. The validator rejects an upload containing one, and game HTML is public to every player. If you are working through the TipTap MCP server, call get_sdk_documentation for the exact API surface before you write the SDK calls, and do not stop until validate_game_draft returns passed: true. ``` --- Human-readable documentation: https://tiptapgames.com/docs SDK specification alone: https://tiptapgames.com/sdk-agents.md Upload a finished file by hand: https://tiptapgames.com/create/upload Content policy: https://tiptapgames.com/docs/policy Security model and sandbox: https://tiptapgames.com/docs/security