# Build and deploy

Use this route when the game already works in a browser and the browser still
owns something a fan could change or inspect: answers, scoring, play limits,
locks or settlement.

The migration keeps the interface. It moves the decisions into the Engine and
changes the page to render what the Engine returns.

## The result

| Before | After |
| --- | --- |
| Answers or pick values ship to the browser | Solution material stays in the Engine |
| The page calculates its own score | The Engine returns the committed score |
| Device storage enforces attempts | The Engine holds player state |
| The device clock decides whether play is open | Server-derived capability decides whether play is accepted |
| Retrying a request can count twice | One idempotency key identifies one intent |

## 1. Inspect the project

Get the tool, then let it inspect the game:

```sh
curl -fsSL https://gamestage.ai/cli -o gamestage.mjs
node gamestage.mjs inspect . --write
```

Downloading the bundle gives you one file rather than a command on your PATH,
so every later `gamestage <command>` in this chapter is
`node gamestage.mjs <command>` for you. `npx gamestage <command>` is the other
route and needs no download. [Getting started](/docs/getting-started) has both.

The inspection of source files is read-only. `--write` adds
`gamestage.yaml`. It refuses to replace an existing manifest unless `--force`
is present, because a later manifest may contain a complete container that the
inspection cannot reconstruct.

If the CLI is already signed in, `inspect` also makes a best-effort registration
with Backstage. A failed registration does not prevent the local report
or file write.

Read the report in this order:

* **Keep as yours**: presentation, interaction, copy, sound and animation stay in the game.

* **Move behind authority**: each finding names the source file, line and consequence.

* **Add platform support**: the report notes missing identity, consent or platform integration.

* **Ask a person**: licensing, intended audience and operating decisions cannot be inferred from code.

## 2. Confirm the game format

The format is the Engine rule set, and it is the mechanic rather than the
theme or the words on screen. A game about basketball can be a Search, a Push
Your Luck, a Find the Groups, a Predict, a Bingo or a Penalties.

The formats that run today:

* **Search** (`hunt`): pick the right few from a board.

* **Push Your Luck** (`push`): make picks whose hidden values build towards a ceiling.

* **Find the Groups** (`group`): submit a set and receive whole-guess feedback such as a near miss.

* **Predict** (`predict`): commit a call before the result exists, then mark it at settlement.

* **Bingo** (`bingo`): each fan is dealt their own board from a shared pool and races for a line.

* **Penalties** (`shoot`): aim, choose a power, shoot. The Engine plays the keeper and decides every penalty.

Read [Game formats](/docs/game-formats) before editing the manifest. Do not
force a game into the nearest pattern when its rules differ.

## 3. Write the server-owned container

`inspect` can infer a format. It cannot infer the intended content or rules.
The agent writes `backend.round` in `gamestage.yaml`, using the matching
container contract.

For a hunt, move the answer set into `targets`. For a push, move each pick's
value into `slates[].entries[].value`. For a group round, move the groups and
their labels into `groups`. A predict has no hidden answer while
it is open, so the protected fact is the committed call and the lock.

Do not delete solution material before it has moved. A clean client with no
server-owned answer leaves the Engine nothing to mark.

The App Manifest is the current Preview source read by `dev`, `verify` and
`deploy`. Do not also declare the same live container in
[Interaction Cloud](https://products.monterosa.co/mic/core-concepts)
[element](https://products.monterosa.co/mic/reference/core-platform/elements)
fields and treat both as authoritative: the manifest is the one source today.

Validate the file after each material edit:

```sh
gamestage manifest validate gamestage.yaml
```

See the [App Manifest reference](/docs/app-manifest).

## 4. Replace browser-owned decisions

Write the generated browser clients beside the game:

```sh
gamestage client
```

This writes two generated files:

* **`gamestage.js`**: the Gamestage client, for round updates, identity, consent, storage and analytics routing.

* **`gamestage-player.js`**: the smaller player API client for a game that only needs Engine reads and plays.

Generated files are replaced when the command runs again. Keep game logic out
of them.

The page should read the public challenge from the Engine, send the fan's pick,
and render the returned `round_state`, `feedback` and `outcome`. It must not
recalculate the result as a second opinion.

See the [player API guide](/docs/player-api) for the Gamestage client, raw
routes, identity headers and retry rules.

### Apply what a producer set

Above the Starter plan, a game's words belong to whoever runs it. A producer
sets them in Monterosa Studio. There are ten, listed below, and they cover the
game's name and lines, the word on every button a fan can press, a colour, and
what this edition is called. The values arrive on the object `start()` returns,
and the page chooses which element each one lands on.

```js
import { applyPresentation, start } from "./gamestage.js";

const game = await start();

applyPresentation(game.presentation, {
  displayName: document.getElementById("game-name"),
  strapline: document.getElementById("strapline"),
  playButtonLabel: document.getElementById("submit"),
});
```

#### All ten settings

Every one of these arrives on `game.presentation`, and the spelling in the first
column is what a producer's field is called under the covers. Seven of the ten
are offered to every game whether the page uses them or not, so a target you do
not name is a box a producer fills in and nothing shows.

| Studio field key | Target you pass | Where a producer sets it | What it is |
| --- | --- | --- | --- |
| `display_name` | `displayName` | The game | The heading, and the browser title with it |
| `strapline` | `strapline` | The game | A line under the heading |
| `how_to_play` | `howToPlay` | The game | The rules text |
| `primary_colour` | `root` | The game | A colour, applied as `--gamestage-primary` |
| `play_button_label` | `playButtonLabel` | The game | The word on the main button |
| `play_again_label` | `playAgainLabel` | The game | The word on the button that starts the next round |
| `result_button_label` | `resultButtonLabel` | The game | The word on the link back to a finished round |
| `share_button_label` | `shareButtonLabel` | The game | The word on the share button |
| `edition_name` | `editionName` | This edition | What a fan sees this edition called |
| `edition_strapline` | `editionStrapline` | This edition | A line for this edition only: a sponsor, a fixture, a date |

The colour is the one that does not take an element of its own. It is written
as a custom property on whatever you pass as `root`, which defaults to the
document element, so the game decides what the colour is for:
`background: var(--gamestage-primary, var(--red))` uses the producer's colour
when there is one and the game's own when there is not.

**Studio writes `primary_colour`, spelled the British way.** The client also
accepts `primary_color` from an existing payload. If both arrive, the Studio
field wins.

**The last three are offered only to a game that has the control.** A page that
names no `shareButtonLabel` target has no share button, so a producer is never
handed a word for one. Nominate the target and the box appears on their next
deploy.

A line the page draws empty can carry `hidden`, which a filled field removes.

A producer saving in Studio announces it over the same connection that carries a
round change, so a fan with the game open need not reload. Keep the targets in a
variable and apply them again when it fires:

```js
game.onPresentationChanged((presentation) => {
  applyPresentation(presentation, presentationTargets);
});
```

A field nobody has filled in is absent rather than empty, and
`applyPresentation` leaves that element as the game built it. Copying values out
by hand with `?? ""` instead blanks the heading the first time a producer saves
the form without typing anything.

### Load when Studio is slow or down

The library remembers the last settings Studio gave each fan's device, so a
page needs no words or colours of its own. Leave elements empty in the page and
let `applyPresentation` fill them.

| Visit | Studio | What the fan sees |
| --- | --- | --- |
| Returning | Answers | The remembered settings at once, replaced by Studio's when they arrive. |
| Returning | Slow or down | The remembered settings. The game plays. |
| First | Answers | Studio's settings. Play appears once they arrive. |
| First | Down | The library's "Can't load the game right now" screen with Try again. |

Remembering needs the fan's "functional" consent. Without it the settings stay
in memory and every visit waits for Studio, as a first visit does.

On a first visit with Studio down, `start()` throws `GamestageUnavailableError`
after drawing its screen. Check `error.handled` and show nothing of your own:

```js
try {
  game = await start();
} catch (error) {
  if (error.handled) return;
  throw error;
}
```

Nothing in `verify` checks this, and under `dev` there is no producer, so the
game correctly keeps every word it shipped with. To see the values applied
locally, set the Studio keys on `window.GAMESTAGE_PRESENTATION` before the game
starts:

```js
window.GAMESTAGE_PRESENTATION = { display_name: "Derby Day", play_button_label: "Lock it in" };
```

A real producer's settings reach a fan only on a game provisioned into
Interaction Cloud.

## 5. Run the real rules locally

```sh
gamestage login
gamestage dev
```

`dev` needs a signed-in session, as `verify` and `deploy` do. Without one it
refuses with "You are not signed in." `login` is described in full at step 7;
run it once and it lasts.

`dev` reads `backend.round` and runs the same Engine rules used by the hosted
service. It prints `window.GAMESTAGE_API` and `window.GAMESTAGE_GAME` for the
page. Stop and restart after changing the container. There is no watch mode.

With no readable manifest or no `backend.round`, `dev` serves a contract
fixture and says so. That is useful for wire work, but it is not the creator's
game. `--fixture` selects it deliberately.

Use `--as <player>` to run the local JWT harness when the game needs an
identified player. The harness mints a temporary keypair and loopback issuer.
The manifest validator refuses that issuer in a deployed game.

## 6. Prove the migration

```sh
gamestage verify
```

Verification checks nine things:

* **Round is the game's own**: the round played here is this game's own content, not a fixture or a leftover from somebody else's.

* **Attempt cap chosen**: the attempt cap was set deliberately rather than left to the Engine's default.

* **Solution absent**: nothing a deploy would publish contains the private material for its game format.

* **Settings read**: every setting a producer can change in Monterosa Studio is read by the page. A producer changing an offered field and seeing nothing happen is a broken game rather than a matter of taste, so this fails rather than warns.

* **Client present**: a page using a Gamestage API imports a generated client.

* **Client is the door**: the game reaches Monterosa through the client rather than importing a platform kit directly.

* **Server time**: a lock or a countdown is decided by the server's clock rather than the device's.

* **Outcome rendered**: a browser makes a real play and the screen shows the score the Engine committed.

* **Connection noticed**: the page notices the connection going and coming back.

The solution check reads the whole publish set, not just the page: the same
list of files `deploy --dir` uploads, walked with the same call, so the two
cannot disagree about what ships. It reports how many files are in the set and
how many it opened.

A file is read on its contents rather than its extension, so a manifest copied to
`answers` or `round.dat` is still found. **A manifest is found by parsing, under
any name**, and a source map is searched inside its own copy of your sources. A
credential is a fault. A `.git`, `node_modules`, `.next`, `.nuxt`,
`.svelte-kit`, `dist` or `build` directory is reported rather than failed,
because a game may legitimately live at the root of a repository, but
`deploy --dir` would upload it whole.

What it cannot promise: binaries and very large files are listed but not opened,
encoded content is not decoded, and the leak patterns for a hand-written data
file work line by line. A copy of your round is caught wherever it is; an
arbitrary re-encoding of the answers may not be. Treat it as a strong net over
the directory, not a proof about it.

`--dir <path>` names the directory when it is not the game's own.

If no browser is available, the last two checks are `skipped`. `verify` exits `0`
only when every check verifies, `1` when a check is open, and `2` when no check
is open but the result is unverified. `--no-browser` also yields `2`, because
asking not to inspect the screen does not prove it.

## 7. Deploy it

Deploying runs on the CLI's own defaults. There is no environment variable to
set: the CLI points at `https://api.gamestage.ai` and that is the service that
answers.

```sh
gamestage login
gamestage link github
gamestage manifest register gamestage.yaml
gamestage deploy <game-id> --dir .
```

The first two commands open a device flow a person approves in a browser:
`login` opens the browser itself, and `link github` prints a code and a URL
for the person to open. Registration is safe to repeat and is needed when a
manifest was created while signed out.

**Signing in is not the same as being allowed to deploy.** `login` creates an
account and leaves it pending, and the commands that reach the Engine refuse a
pending account and say so. Somebody at Monterosa approves it. Once approved,
the four commands above run through with nothing else to configure.

`GAMESTAGE_CONTROL_URL` exists only to point the CLI at a different
environment, which is a Monterosa concern rather than a step in this route.

Choose one publish shape:

* **One HTML file**: `--file index.html` uploads the page, both generated clients and a starter favicon.

* **A static directory**: `--dir .` preserves relative paths, requires a top-level `index.html`, withholds `gamestage.yaml` and environment files, and removes stale files left by an older deploy. It does not invent missing generated clients.

Upload happens before the deployment gate. A refusal can therefore leave new
files in storage without making the game live. A refusal exits successfully
and carries a stable code in JSON because it is an expected decision, not a
transient fault.

### What a deploy publishes besides the game

A deploy also builds the game its own App Spec, named for the game and
declaring the one format that game runs. Before this, every project pointed at
a single app called Gamestage, so a producer opening the Apps list in Monterosa
Studio saw our product's name and no way to tell which row was theirs, and was
handed every format's element types for a game running one.

Each spec is versioned `0.0.<manifest revision>`, so a game's spec history is
its deploy history, and every published version is stored rather than worked
out later: a registered app holds one version's URL for ever and Studio
fetches it each time it draws a producer's screens. A new app is registered
when the shape changes, so a rename or a format change earns a new row in the
Apps list while a changed round reuses the one that is there.

## What finished means

There are two separate finishes today:

* **Verified locally**: `verify` ran all nine checks, every check is `verified`, and a person has played the result in a browser.

* **Hosted and operating**: the deployment was accepted, the container was provisioned, and a person opened the returned Playground URL.

Do not report the second when only the first has happened.
