# Common failures Every entry here has happened, in this catalogue, to a real submission. None of them is hypothetical and none is a matter of taste. They are ordered by how much they cost to find, which is not the same as how hard they are to fix — the expensive ones are the failures that leave the game looking fine. A working game with all of this already correct: [the starter](/starter/index.html). Copying it is faster than reading this page. --- ## The silent ones These do not crash, do not log, and do not look wrong. They are the reason this page is ordered the way it is. ### An SDK older than 0.2.0 ```json "sdkMinVersion": "0.1.1" ``` **What happens.** Nothing. The game loads, plays, saves its own high scores and reports absolutely nothing to the portal: no playtime, no scores, no ads, no earnings. 0.2.0 renamed the global, the events and the manifest and changed the message envelope, so an older SDK is talking to a shell that cannot hear it. **How to see it.** `await Bananai.init({ gameId: 'x', debug: true })` logs `initialised { standalone: true }`. Inside the portal, `standalone: true` is always a bug. **Fix.** Link `/sdk/v0/bananai-sdk.js` rather than carrying a copy. If you must vendor one, vendor the official file at 0.2.0 or newer — not a reimplementation. ### `gameOver()` sent from the draw loop ```js function draw() { if (gameOver && !fired) { fired = true; Bananai.gameOver({ score }); } } ``` **What happens.** Usually nothing. Occasionally the run vanishes. Between the frame that raises the flag and the frame that sends the event there is a gap, and an input landing in it can restart the game first — resetting `gameOver` and `fired` before the event is ever sent. The player's score is never recorded, and no error appears anywhere. **Fix.** Send it where the run actually ends, not where it is next drawn. Guard the function that ends the run so the send is idempotent, and you will not need a separate flag: ```js function endRun() { if (over) return; over = true; Bananai.gameOver({ score, level, durationMs }); } ``` ### No `gameplayStop()` **What happens.** One session that starts when the player does and ends when the tab closes. Pauses, menus and backgrounded tabs all count as play. The pair is what measures the playtime you are paid on, so this is not a rounding error in your favour — it is a number nobody can trust. **Fix.** Stop on pause, on settings, and on `visibilitychange` when the document is hidden. `gameOver()` implies `gameplayStop()`; calling both is harmless. ### Restarting by reloading the page ```js window.addEventListener('bananai:restart', () => location.reload()); ``` **What happens.** The reload throws away the SDK handshake and the session with it. The game comes back looking correct and reporting nothing. **Fix.** Reset your own state. If that is hard, it is worth making easy — the shell's restart button is not the only caller, and an in-game replay wants the same function. --- ## The loud ones These fail the automated checks on submission, which load a game standalone and inside the real shell at several viewport sizes. ### A fixed height ```css .game { min-height: 480px; } ``` **What happens.** In landscape on a phone the frame is shorter than 480px, so the canvas overflows it, the game document scrolls, and on iOS that scroll chains outwards and moves the portal instead of playing. This one broke **five of the six games in this catalogue at once**. It is the single most common reason a submission is sent back. **Fix.** Read the container and never assume: `canvas.width = el.clientWidth`, recomputed on every resize. No `min-height`, no `aspect-ratio`, no fixed pixel size. See section 2 of the [submission rules](submission-rules.md). ### An absolute URL for the SDK ```html ``` **What happens.** It is a cross-origin request everywhere except live production, so it fails in local development, in review, and in the checks. It is also how this portal's SDK came to be reimplemented from scratch: a developer got a 404 on a documented URL, concluded the file did not exist, and wrote their own from the prose. That reimplementation then could not be migrated, because it was never our code. **Fix.** `/sdk/v0/bananai-sdk.js`, root-relative, no origin in front of it. ### A request to anywhere else ```html ``` **What happens.** Rejected. There is no allowed CDN and no allowed font host — the rule is zero requests to any origin other than the one the game is served from, and the checks assert it directly. **Fix.** Bundle everything inside your folder. Yes, that means your own copy of the library, even if another game already ships the same one. Five identical copies of `p5.min.js` were deduplicated into a shared folder once and it had to be reverted: a game folder is self-contained, and cross-folder sharing creates a dependency you never agreed to and cannot control. ### The folder name and the manifest id disagree ``` public/games/pallanoids/ ← folder { "id": "pallanoid" } ← manifest ``` **What happens.** This one reached the catalogue and was corrected by hand. Nothing in the automated checks opens `bananai.json` yet, so it is caught by a person reading it, or not at all. **Fix.** Folder name, manifest `id`, the constant in your source and the catalogue entry are one string, four times. It is permanent once published. --- ## The ones that only appear on a phone The automated checks do not simulate touch. Nothing below is caught by them, or by us until a person picks up a phone — so these are the ones that reach players. ### Touch handlers that do not return `false` **What happens.** The page scrolls under the player's finger while they are trying to play. **Fix.** Return `false` from `touchStarted` / `touchMoved` / `touchEnded`, and set `touch-action: none` on the game's root container. The CSS is not optional — returning false does not stop the browser claiming a pinch or a double-tap zoom before your handler sees it. ### Both an on-screen pad and a declared `virtualControls` **What happens.** The shell draws its pad on top of yours and swallows the presses meant for it. The game looks like it has stopped responding. **Fix.** Pick one. Native touch is what the rules require and what should win; the shell's pad exists for a keyboard-only game that has no other way to be playable on a phone. No entry in this catalogue declares it today. ### A synthetic click after a tap **What happens.** A touch device fires `touchstart` and then a synthetic `click`, so a single tap runs your handler twice. In a game with an ad break between runs, that is two breaks requested for one tap. **Fix.** Use one `pointerdown` handler for mouse, touch and pen. If you must keep both paths separate, guard the mouse one — `if (touches.length > 0) return;`. --- ## The rest ### `localStorage` without a guard ```js localStorage.setItem('best', score); // throws in private browsing ``` **Fix.** Wrap every access in try/catch and degrade to not saving, never to a broken game. Namespace under `game::`. ### Audio before the first interaction **What happens.** Browsers block it. A `AudioContext` created on load starts `suspended`, and everything played into it is silent — including everything played after the player finally does interact, unless you resume it. **Fix.** Create or resume the context inside the first real input handler: `audioContext.resume()`. And provide a mute control, which the rules require of any game with audio at all. ### A thumbnail outside the game folder **Fix.** `assets/thumb.png` — or `.svg`, or `.webp` — inside your own folder, square, 400×400, legible at the 180px the grid actually renders. A submission should be one directory that can be added or removed as a unit, with no orphaned image left behind when a game is withdrawn. ### A screenshot of the starting screen used as a thumbnail Five of the seven catalogued games did this, HUD and all, so it is the most common failure on this page rather than a hypothetical one. **Why it fails.** The starting screen is the moment before anything has happened: score zero, level one, board untouched. And the card is 180px wide, where a HUD becomes an illegible smear of information the card already prints beside the image in text. **Fix.** Compose the image instead of capturing it. No HUD, one subject filling the frame, few large shapes, checked at 180px. See section 5 of [SUBMISSION-RULES.md](submission-rules.md). --- ## Before you send it Walk the [QA checklist](qa-checklist.md) rather than trusting this page. It is the manual pass review actually performs, published so you can run it first, and it catches more than a list of known mistakes can. On submission your game also goes through our automated checks, which load it standalone and inside the real shell at five viewport sizes and fail it if the canvas does not match its frame, if the game document scrolls, or if anything outside the origin is requested. Those run here rather than on your machine, so there is no tooling of ours for you to install — and [the starter](/starter/index.html) is the practical way to know you will pass them, because it already does. The [submission rules](submission-rules.md) are what a game is judged against; this page is only the shortlist of ways they get broken. --- ## Document history - **2026-08-19** — Example manifest field renamed from `sdkVersion` to `sdkMinVersion`. - **2026-08-16** — Thumbnail rule spells out what the image must show, not just its size. - **2026-08-12** — Manifest enum values (`category`, `ageRating`, `controls`, `orientation`) published. - **2026-08-11** — Internal runbook content taken out; the starter added as the canonical working reference.