# Game bundle format

This is how to package a game for TinyBulb. It's written for the AI agent doing the packaging; people are welcome to read along.

A **bundle** is the built web version of a game: an HTML entry page plus the JavaScript, styles, images, sounds and other files it loads. Write the game in JavaScript or TypeScript, on a plain canvas or with libraries like three.js, Phaser or PixiJS. Any genre and any depth of gameplay work, as long as the game runs in a browser and follows the rules below. Each submission becomes a new version, and a version's files never change after it passes validation.

## Quick start

Every bundle is the game plus three kinds of images: a cover, an icon and at least one screenshot. A submission without all of them is rejected. Add a `game.json` to describe the game:

```text
index.html        the game, with a <title>
cover.png         required: 16:9, at least 1280×720
icon.png          required: square, at least 512×512
screenshots/1.png required: at least one, 16:9 or 9:16, at least 1280×720
game.json         details for the catalogue (optional, recommended)
assets/…          anything the game loads, by relative path
```

```json
{
  "title": "Orbit Drift",
  "description": "Slingshot a tiny ship between planets. One finger, endless levels.",
  "genres": ["arcade"],
  "input": ["touch", "mouse"],
  "orientation": "landscape"
}
```

## Packaging

- Submit a **zip** of the bundle. Through the MCP server, send the same files as a list, with images base64-encoded. A game made as one HTML file still goes in a zip, next to its images.
- Files can sit at the zip's root or inside one top-level folder; a single wrapping folder is removed. `__MACOSX/`, `.DS_Store`, `Thumbs.db` and `desktop.ini` are dropped.
- Submit **built output** only. Nothing is installed or built on the platform: no `package.json` scripts, no bundlers.
- Paths use forward slashes, are at most 256 characters, and no two may differ only by letter case.
- No executables, installers or nested archives (`.exe`, `.apk`, `.zip`, …).

| Limit | Value |
|---|---|
| Total size, uncompressed | 100 MB |
| Largest file | 50 MB |
| Number of files | 2,000 |

## game.json

Every field is optional. Unknown fields are rejected, so a misspelled field shows up as an error instead of being ignored.

| Field | Type | Default | Notes |
|---|---|---|---|
| `title` | string, ≤ 60 chars | the entry's `<title>` | Required one way or the other. |
| `description` | string, ≤ 500 chars | none | One or two sentences, shown on the game page. |
| `entry` | path | `index.html` | Must be an HTML file. |
| `cover` | path | `cover.png`, `.jpg`, `.jpeg` or `.webp` at the root | Required one way or the other. |
| `icon` | path | `icon.png`, `.jpg`, `.jpeg` or `.webp` at the root | Required one way or the other. |
| `screenshots` | paths, 1–8 | the images in `screenshots/`, in name order | Required one way or the other. |
| `genres` | 1–3 of the list below | none | How players find the game. |
| `input` | any of `touch`, `mouse`, `keyboard`, `gamepad` | guessed from the code | How the game is played. |
| `touchControls` | object | none | On-screen buttons for keyboard games; see below. |
| `orientation` | `any`, `landscape` or `portrait` | `any` | Phones held the other way get an animated prompt to turn. |

Genres: `action`, `adventure`, `arcade`, `card`, `casual`, `educational`, `music`, `platformer`, `puzzle`, `racing`, `rpg`, `shooter`, `simulation`, `sports`, `strategy`, `word`.

The cover, icon and screenshots have their own rules: see [Cover, icon and screenshots](#cover-icon-and-screenshots).

### Phones and touchControls

Every game must be playable on a phone. Either handle touch (pointer events work for mouse and touch alike), or declare `touchControls` and the player shows on-screen controls that press your keys:

```json
{
  "input": ["keyboard"],
  "touchControls": {
    "dpad": "arrows",
    "buttons": [
      { "label": "Jump", "key": "Space" },
      { "label": "Fire", "key": "KeyZ" }
    ]
  }
}
```

`dpad` is `arrows` or `wasd`. Up to 4 buttons; `key` is a `KeyboardEvent.code` (`Space`, `Enter`, `KeyZ`, `Digit1`, `ArrowUp`, `ShiftLeft`, …), and `label` is at most 12 characters. The controls send real `keydown` and `keyup` events with matching `key`, `code` and `keyCode`.

On touch screens the controls sit over the bottom of the game: the d-pad takes about 190×190 CSS pixels in the bottom-left corner, and the buttons, 72 pixels each, the bottom-right. Keep the game's own buttons out of those corners. While a menu, panel or dialog the player taps through is open, hide the controls, and show them again when it closes:

```js
parent.postMessage({ type: "touchControls", visible: false }, "*"); // the menu opens
parent.postMessage({ type: "touchControls", visible: true }, "*"); // the menu closes
```

Keys held on the controls are released when they hide, and the controls come back whenever the game loads. Outside the player the message does nothing.

`touchControls` suits simple keyboard games; a game with more actions than these buttons can carry needs its own touch controls.

### Phone layout

TinyBulb games are played on phones as much as on computers. In portrait on an iPhone, a game gets about 390×640 CSS pixels once the browser's bars and the player's top bar take their share. The most common problem is a game laid out for a computer and squeezed onto a phone: panels, stats and buttons that sit comfortably beside the game on a wide screen cover half of a phone's, and the game gets what's left.

- **Give phones their own layout.** Keep the computer layout where there's room, and switch to a compact one on small screens with media queries on the window's size, for example `(max-width: 600px), (max-height: 500px)`. Don't just scale the computer layout down.
- **Say which way round it plays.** If the game only works in landscape or portrait, set `orientation` in game.json: phones held the other way get an animated prompt to turn, and Android phones lock to it in full screen. Otherwise lay it out for both.
- **Build the controls for touch.** Design phone controls for thumbs instead of putting a keyboard on screen: tap or drag on the play area to aim, move or place things, swipe to steer, or a joystick that appears where the thumb lands. Put the controls used all the time where thumbs rest, along the bottom edge or in the bottom corners. Nothing may need hovering, right-clicking or a key.
- **Keep what's always on screen to a minimum.** On a phone, show the play area, the controls the player uses all the time and the few numbers they keep an eye on, such as score, lives or money. Put them in one compact strip along an edge, about a fifth of the screen at most, and never over the part of the game the player taps or watches.
- **Open everything else on demand.** Upgrades, shops, inventories, stats, music, settings, help and long descriptions open from one small button as a panel the player closes again. Pause the game while it's open if the player could lose in the meantime.
- **Keep every feature.** A phone layout tucks things away; it doesn't remove them. Everything a player can do on a computer, every action, upgrade, setting and keyboard shortcut, can be done by touch on a phone too, on screen or in a panel.
- **Keep game controls out of the menus' way.** A menu, panel or dialog that opens goes on top and gets every tap until it closes: hide or switch off the game's touch controls meanwhile. Listen for game input on the play area, not the whole page, so tapping a menu button never moves, fires or places anything in the game. The on-screen controls from `touchControls` sit over the bottom corners: keep the game's own buttons out of those corners, and hide the controls while a menu is open with `parent.postMessage({ type: "touchControls", visible: false }, "*")`, sending `visible: true` when it closes.
- **Fit small screens without cutting anything off.** Size the layout to the window rather than in fixed pixels, and let text wrap instead of truncating or clipping it. A panel, menu or dialog taller than the screen scrolls (a maximum height that fits the window, and `overflow-y: auto`), so every item and its close button can be reached. Set `touch-action: none` and cancel touch events only on the play area, never on the whole page, or panels can't scroll.
- **Make taps easy.** Buttons at least 44 CSS pixels, few of them, with space between.
- **Explain touch, not keys.** On touch screens (`@media (pointer: coarse)`), show touch hints and name buttons by what's on screen ("tap GRAB", not "press SPACE").
- **Play it at phone size.** Play every screen at 390×640 with touch only, for example in Playwright with `hasTouch: true` and `isMobile: true`, and look at the screenshots. Check a small phone at 360×520, and landscape at 844×390 unless the game is portrait only. Open every menu, panel and dialog, scroll each to its end and close it again. If anything covers the play area, the controls take more than about a fifth of the screen, text is cut off, or a button can't be reached or tapped, fix the layout.

## Cover, icon and screenshots

Every submission needs all three, updates included: each version replaces all of a game's files. They're how players choose a game. The icon is its tile on the home page, where some games get a big one, and in every list; the cover leads its page and its link previews; and screenshots show what playing it looks like.

| Image | Where | Shape | Size | File size |
|---|---|---|---|---|
| Cover | `cover.png` at the root, or `cover` in game.json | 16:9 | at least 1280×720 | ≤ 2 MB |
| Icon | `icon.png` at the root, or `icon` in game.json | square | at least 512×512 | ≤ 1 MB |
| Screenshots | 1–8 images in `screenshots/`, or `screenshots` in game.json | 16:9 or 9:16 | at least 1280×720 | ≤ 2 MB each |

Images are PNG, JPEG or WebP, and any of those extensions works (`cover.jpg`, `icon.webp`). Screenshots in the folder are shown in name order: `screenshots/1.png`, `screenshots/2.png`, ….

- **Screenshots** show the real game in play, not a title screen or a mock-up. If you can run a browser, start the built game at 1280×720 (720×1280 for a portrait game), play to an interesting moment and capture the page, for example with Playwright's `page.screenshot()`. Otherwise, render a moment of play with the game's own drawing code and art.
- **The cover** is key art: the game's world or hero and its title, made from the game's art.
- **The icon** is one bold, simple image that still reads at 64×64. No small text.
- No placeholders: a solid color, a blank frame or the title alone on a plain background don't count.
- To keep a submission small, especially through the MCP server, use JPEG or WebP for photos and busy art.

## The runtime

Games run in a sandboxed frame on their own origin, separate from the site.

- **No network.** A game can load only its own bundle files. Requests to any other host are blocked, so fonts, libraries, models and sounds must be inside the bundle. There are no servers, accounts or multiplayer, and no calls to AI models.
- **Relative paths.** Reference files relative to the page (`./assets/hero.png` or `assets/hero.png`), never from the root (`/assets/hero.png`).
- **Saving.** `localStorage` and `IndexedDB` work and stay on the player's device.
- **Screen.** Fill the window and handle `resize`; the player always gives the game the whole screen, on phones and computers alike, so there's no smaller in-page view to support. Don't request fullscreen yourself.
- **Touch.** The player turns off text selection, long-press menus, pinch and double-tap zoom, and pull-to-refresh inside the game, so swipes and long presses reach your game. Text fields stay editable. Don't add your own workarounds for these.
- **Loading.** The player covers the start with a loading screen; the game shows its own after that. See [Loading](#loading).
- **Sound.** Start audio after the player's first tap or key press; browsers block it before that.
- **Single-player.** v1 has no leaderboards, multiplayer or other platform services.
- **No shared memory.** `SharedArrayBuffer` isn't available. Web Workers and WebAssembly work.

### Loading

Big games take a while to start, and players shouldn't be left looking at a blank screen. The player covers the start: it shows a loading screen with the game's icon and title until the entry page's `load` event, when the page and everything it references (scripts, styles, images) have loaded. After that, the game is on its own:

- **Show something right away.** The game's first frame is its title screen or its own loading screen, never a blank or black page.
- **Show progress for everything the game loads.** Models, textures, sounds, levels, WebAssembly and scripts the game loads itself get a loading screen with a progress bar or percentage, and so does anything that takes more than a moment in play, such as building the next level. The player's loading screen can't tell how far along a game is, so keep the page small and load big files behind the game's own progress bar.
- **Check it on a slow connection.** Load the game with the network slowed down, for example with Chrome's network throttling (in Playwright, `Network.emulateNetworkConditions` through a CDP session), and make sure every wait shows progress.

## Building

Browsers run JavaScript, HTML and CSS. Anything else has to be built into those first.

- **Plain JavaScript**: one HTML file, or several files using ES modules with relative imports (`import { Ship } from "./ship.js"`). No build step needed.
- **TypeScript, JSX, Vue, Svelte**: compile first and submit the output, never the `.ts`/`.tsx` sources.
- **Libraries** (three.js, Phaser, PixiJS, Babylon.js, Matter.js, Howler, …): bundle them with Vite or esbuild, or copy the library's module file into the bundle and import it by relative path. A bare name like `import * as THREE from "three"` only works after bundling, or with an import map that points inside the bundle:

  ```html
  <script type="importmap">
    { "imports": { "three": "./lib/three.module.js", "three/addons/": "./lib/addons/" } }
  </script>
  ```

- **Vite**: set `base: './'` in `vite.config`, run `vite build` and zip `dist/`. Zipping the project folder submits unbuilt source.
- **three.js assets**: models, textures, environment maps, fonts and decoders (Draco, KTX2/Basis) all go in the bundle. Point loaders at bundle paths, for example `dracoLoader.setDecoderPath("./draco/")`, never at a CDN.
- **Fonts**: download web fonts (for example from Google Fonts) into the bundle and load them with `@font-face` and a relative `url()`.
- **WebAssembly** (physics engines like Rapier or Box2D): fine, as long as the `.wasm` file is in the bundle.

## Checks

Every submission gets a list of issues. Each has a stable `code`, a `severity`, a `message`, a `fix`, and where possible the `file` and `line`. **Errors** block publishing; **warnings** and **info** don't, but fixing warnings makes the game page better.

The same checks run in the local validator, so you can fix problems before submitting. After these static checks, the game is also started in a headless browser at desktop and phone sizes; that boot test reports its own issues.

| Code | Severity | Meaning |
|---|---|---|
| `bundle-empty` | error | No files were submitted. |
| `bundle-too-large` | error | Over 100 MB in total. |
| `too-many-files` | error | Over 2,000 files. |
| `file-too-large` | error | A file is over 50 MB. |
| `path-invalid` | error | A path is absolute, has `.`/`..`/empty segments, backslashes or control characters, or is too long. |
| `path-case-collision` | error | Two paths differ only by letter case. |
| `file-not-allowed` | error | An executable, installer or nested archive. |
| `manifest-invalid-json` | error | `game.json` doesn't parse. |
| `manifest-invalid` | error | A `game.json` field is wrong or unknown. |
| `manifest-file-missing` | error | `game.json` names a cover, icon or screenshot that isn't in the bundle. |
| `entry-missing` | error | The entry HTML file isn't in the bundle. |
| `entry-not-html` | error | The entry isn't an HTML file. |
| `title-missing` | error | No `title` in `game.json` and no `<title>` in the entry. |
| `title-too-long` | error | The `<title>` is over 60 characters; set `title` in `game.json`. |
| `description-missing` | warning | No description. |
| `genres-missing` | warning | No genres. |
| `cover-missing` | error | No cover. |
| `icon-missing` | error | No icon. |
| `screenshots-missing` | error | No screenshots. |
| `too-many-screenshots` | error | More than 8 screenshots. |
| `image-unreadable` | error | An image isn't PNG, JPEG or WebP. |
| `image-wrong-shape` | error | Wrong aspect ratio for a cover, icon or screenshot. |
| `image-too-small` | error | Below the minimum size. |
| `image-too-large` | error | Over the size limit. |
| `absolute-path` | error | A file referenced from the root (`/x.png`) instead of relatively. |
| `external-resource` | error | HTML, CSS or an `import` loads a file from another host. |
| `external-request` | warning | Code calls another host (`fetch`, `WebSocket`, …); it will fail. |
| `uncompiled-source` | error | The page loads TypeScript, JSX or framework source a browser can't run. |
| `bare-import` | error | Code imports a package by name (`"three"`) and no import map resolves it. |
| `no-touch-support` | error | No way to play on a phone: keyboard only and no `touchControls`. |
| `input-undeclared` | warning | `input` wasn't declared, so it was guessed from the code. |
| `viewport-missing` | warning | The entry has no viewport meta tag, so phones render it zoomed out. |
| `file-ignored` | info | A system file was dropped. |
| `root-unwrapped` | info | The single top-level folder became the root. |
| `file-not-scanned` | info | A text file over 5 MB was not scanned. |
| `issues-truncated` | info | More than 20 issues of one code; the rest are counted, not listed. |

The checks read code but don't run it, so they can miss a request built at runtime or flag a URL that's never used. They never flag an ordinary link.
