# Tapverse Game Spec — v1

> **To the AI reading this:** you are building a browser game for **Tapverse**, a platform where people
> play games instantly on phones and computers. Follow every **MUST** in this document exactly;
> treat **SHOULD** as strong defaults. The game idea comes from the person talking to you. If the idea
> conflicts with a MUST, keep the MUST and explain the change briefly. Reply to the person in the
> language they wrote to you in, but keep code comments and identifiers in English.

---

## 1. What to deliver

If the person attached a **Tapverse starter template** (`single-player.html` or `multiplayer.html`),
build on it: keep its structure, platform helpers and phases, and replace the game-specific parts.

1. **One file, `index.html`**, containing all HTML, CSS and JavaScript inline — unless the game truly
   needs separate asset files, in which case deliver a folder with `index.html` at its root (the
   person will zip it). Libraries MAY be loaded from `cdnjs.cloudflare.com`, `cdn.jsdelivr.net` or
   `unpkg.com`; pin exact versions.
2. **A game manifest** inside `index.html`, as described in §9.
3. **Two image prompts** at the end of your reply (not in the code) that the person can paste into an
   image generator for the covers in §8: one landscape 16:9 and one portrait 9:16, with no text in the
   image except the game's title.
4. A short **"How to play"** paragraph for the person to use as the game description.

Size limits: at most 50 MB zipped, 200 MB unzipped, 1000 files. Aim for **under 5 MB** total so
the game opens in a couple of seconds on phones. Use **relative paths** (`assets/hero.png`, never
`/assets/hero.png`).

## 2. Required structure

Every game MUST have these screens or states:

- **Start screen:** title, one big **Play** button, and a short "how to play" line. Nothing starts
  until the player taps Play (browsers only allow sound after a tap).
- **Playing:** the game itself, with a small **pause** button (top-right) and a **mute** toggle.
- **Paused:** Resume and Restart.
- **Game over / level complete:** the result (score, time or stars), **Play again**, and — if the game
  has a natural "second chance" — a **"Watch an ad to continue"** button (see §6).

Keep all UI text short; show numbers big. Follow `navigator.language` for UI text when you can
(at least English and Spanish), falling back to English.

## 3. Screen and orientation

Tapverse shows the game **full screen inside a frame** that can be any size: a phone held upright
(≈ 390×800), a tablet, or a desktop window (≈ 1280×720 and larger).

- The game MUST fill the viewport exactly: `html, body { margin: 0; height: 100%; overflow: hidden; }`
  and size the canvas from `innerWidth` / `innerHeight`, re-laying out on `resize`.
- Include `<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">`.
- Pick one orientation in the manifest:
  - `"portrait"` (**recommended**; most players are on phones): design for 9:16. On wide screens,
    keep the play area centered at 9:16 and fill the sides with the background.
  - `"landscape"`: design for 16:9. On phones held upright, show a friendly "rotate your phone"
    screen instead of a squashed game.
  - `"any"`: the layout genuinely adapts to both.
- Render sharp: multiply the canvas size by `Math.min(devicePixelRatio, 2)`.
- Keep important UI away from the edges (≥ 16px, and respect `env(safe-area-inset-*)`).
- No page scrolling, no pinch zoom, no text selection during play
  (`touch-action: none; user-select: none` on the game area).

## 4. Controls

- MUST work with **touch** (phones) and **mouse**. SHOULD also work with **keyboard** on desktop
  (arrows/WASD, Space, Esc to pause). Use Pointer Events (`pointerdown`, `pointermove`, `pointerup`).
- Games that need directional input on phones MUST show **on-screen controls** (virtual joystick or
  buttons ≥ 56px) only on touch devices (`matchMedia("(pointer: coarse)")`).
- Tap targets ≥ 44px. No hover-only interactions. No right-click or long-press requirements.

## 5. Behavior and performance

- Target **60 fps on a mid-range phone**. Use `requestAnimationFrame` and scale by elapsed time
  (cap each step, e.g. to 50 ms, so a hidden tab doesn't jump ahead).
- **Pause automatically** when `document.hidden` becomes true, and mute audio.
- Show a **loading indicator** if assets take more than a moment.
- **Audio:** create/resume the `AudioContext` only after the Play tap; keep sound effects short;
  include the mute toggle and remember it.
- **Saving:** `localStorage` keys MUST start with the game's slug-like name (e.g. `space-cat:best`), and
  every read/write MUST be wrapped in `try/catch` (storage can be blocked).
- **Never:** `alert`/`confirm`/`prompt`, opening popups or new tabs, redirecting the page, asking for
  personal data, passwords or payments, loading trackers or analytics, or linking out of the game
  (the invite button is the only exception, via the SDK).
- Show player-typed text (names, chat) with `textContent`, never `innerHTML`.

## 6. The Tapverse SDK

Load it first thing in `<head>`:

```html
<script src="/_tapverse/sdk.js"></script>
```

It only exists when the game runs on Tapverse. The game MUST still start and be playable when
opened locally without it, so always guard with `window.Tapverse`:

```js
const TV = window.Tapverse; // undefined when testing locally
const inTapverse = !!TV?.inside;
```

| API | What it does |
|---|---|
| `Tapverse.inside` | `true` when running inside Tapverse. |
| `Tapverse.params` | `URLSearchParams` the player arrived with (e.g. `get("room")`). |
| `await Tapverse.share({ params, text })` | Opens the share sheet with a Tapverse link to this game (plus `params`). |
| `await Tapverse.inviteLink(params)` | Returns that link without opening anything. |
| `await Tapverse.ads.rewarded()` | Shows a rewarded ad; resolves `true` **only** if it was watched to the end. |
| `await Tapverse.ads.interstitial()` | A short ad at a natural break (between levels). Tapverse may skip it. |
| `await Tapverse.rooms.join(code?, { name })` | Multiplayer room; see §7. |

**Ads rules:** give rewards only when `rewarded()` resolves `true`; offer at most one rewarded ad per
game over; call `interstitial()` only between levels or rounds, never during play; pause game and
audio while awaiting either. Outside Tapverse both resolve immediately without an ad.

## 7. Multiplayer

Tapverse hosts real-time rooms — no server of your own. Up to **16 players** per room.

### 7.1 The room API

```js
const room = await Tapverse.rooms.join(Tapverse.params.get("room"), { name }); // no code = new room
room.code          // "K7P2Q" — show it big, players can read it out loud
room.me            // { id, name }
room.players       // [{ id, name }] in join order
room.host          // id of the host (longest-connected player)
room.isHost        // true on the host's device
room.state         // shared object, persists for late joiners
room.connected     // false while reconnecting

room.on("join", (player) => {});
room.on("leave", (player) => {});
room.on("host", (hostId) => {});            // the host left; someone else is host now
room.on("message", (data, from) => {});
room.on("state", (state, fromId) => {});
room.on("reconnecting", () => {}); room.on("reconnected", () => {}); room.on("close", () => {});

room.send(data)             // to everyone else
room.send(data, playerId)   // to one player
room.setState({ key: value, other: null })  // merge; null deletes a key
room.invite("Join my game!")                // share sheet with a link to this room
room.leave()
```

Limits: JSON messages up to **16 KB**; about **30 messages per second per player** (bursts of 60);
shared state up to **64 KB**, cleared when everyone leaves.

### 7.2 Required flow

1. **Lobby screen:** name field (remember it in `localStorage`), **Create room**, **Join with code**
   (4–8 letters/numbers, case-insensitive), and when arriving with `?room=CODE` join it immediately.
2. **Waiting room:** the room code in big letters, an **Invite friends** button (`room.invite()`), the
   list of players (update on `join`/`leave`), and a **Start** button **only for the host**, enabled
   when there are enough players (`minPlayers` from the manifest).
3. **Game**, then **results** with "Play again" (the host restarts for everyone) and "Leave".
4. Show a small "Reconnecting…" badge on `reconnecting`, remove it on `reconnected`, and go back to the
   lobby with a message on `close`.
5. **Without the SDK** (opened locally), the Create button becomes **"Practice alone"** and runs the same
   game with a one-player stand-in room, so the game is still testable.

### 7.3 Architecture: host-authoritative

The **host's device runs the real game**; everyone else sends inputs and draws what the host says.

- **Clients** send inputs to the host only, e.g. `room.send({ t: "input", dx, dy, fire }, room.host)`,
  at most every 50–100 ms (or on change).
- **The host** applies inputs, runs the simulation and broadcasts snapshots at **10–20 Hz**:
  `room.send({ t: "snap", tick, players: {...}, items: [...] })`. Clients interpolate between snapshots
  for smooth movement.
- Put anything a **late joiner or a new host** needs in `room.setState` — phase (`"lobby" | "playing" |
  "results"`), round, scores, seed, turn — and update it when it changes (not every frame).
- **Host migration:** on `host`, if `room.isHost` is now true, rebuild the simulation from `room.state`
  plus the last snapshot and continue.
- **Turn-based games:** keep the board and whose turn it is in `room.state`; the current player sends
  their move to the host, the host validates it and calls `setState`.
- Tag every message with a short type (`t`) and ignore types you don't know. Never trust a client:
  the host validates every input (ranges, cooldowns, whose turn it is).
- Use a shared random `seed` in state for anything random that all devices must agree on.

## 8. Covers and media

Provide these to the person as image prompts (§1); they upload them in Tapverse's Creator Studio.

| Asset | Size | Used for |
|---|---|---|
| **Landscape cover** (required) | 16:9 — 1920×1080 (min 1280×720) | Desktop cards and the featured banner |
| **Portrait cover** (recommended) | 9:16 — 1080×1920 (min 720×1280) | The phone feed |
| **Icon** (optional) | 1:1 — 512×512 | Lists and profiles |
| **Trailer** (optional) | MP4 (H.264) or WebM, 9:16, 5–30 s, max 20 MB | Plays muted and looping in the phone feed |

Covers: PNG, JPEG or WebP, max 2 MB (Tapverse resizes them). Keep the main character or action in
the **center 60%** (cards crop the edges), use bright colors on a dark background, and no small text.
Trailers: real gameplay, no black intro, the first frame must already look good, no essential audio.

## 9. Manifest

Put this in `index.html`, inside `<head>`. Tapverse reads it to fill in the game's details on upload.

```html
<script type="application/json" id="tapverse-manifest">
{
  "spec": 1,
  "title": "Space Cat",
  "description": "Guide the cat through the asteroid field. Tap to boost, collect fish, don't crash.",
  "tags": ["arcade", "space", "casual"],
  "orientation": "portrait",
  "multiplayer": false,
  "permissions": [],
  "permissionReason": "",
  "aiTool": "Claude"
}
</script>
```

- `title` ≤ 60 characters; `description` ≤ 500; up to 8 lowercase `tags`.
- `orientation`: `"portrait" | "landscape" | "any"`.
- `multiplayer`: `false`, or `{ "minPlayers": 2, "maxPlayers": 8 }` (max 16).
- `permissions`: `[]`, `["camera"]`, `["microphone"]` or both. Only if the game can't work without
  them; then `permissionReason` MUST say why in one friendly sentence (players see it before
  allowing). The game MUST handle the player saying no.
- `aiTool`: the AI or engine used.

## 10. Content rules

Games MUST follow the Tapverse Community Guidelines: no sexual content, no content sexualizing
minors, no graphic or gratuitous violence, no hate or harassment, no real-money gambling, no scams or
attempts to collect personal data. Use only original assets or assets you're allowed to use; don't
copy existing games' characters, names or art.

## 11. Before you answer, check

- [ ] Opens and is playable by double-clicking `index.html`, without Tapverse.
- [ ] Start, playing, paused and game-over states all exist.
- [ ] Works with touch and mouse; on-screen controls on phones where needed.
- [ ] Fills the screen in the chosen orientation; nothing important at the edges.
- [ ] Pauses and mutes when the tab is hidden; sound only after the first tap.
- [ ] `localStorage` wrapped in `try/catch`; no `alert`, popups, trackers or outside links.
- [ ] SDK loaded and every use guarded; rewards only when `rewarded()` is `true`.
- [ ] Multiplayer (if any): lobby, waiting room with invite and host-only Start, host-authoritative
      loop, state for late joiners, host migration, reconnect badge.
- [ ] Manifest present and valid.
- [ ] Two cover prompts and a "How to play" paragraph at the end of the reply.
