# Game formats

A game format is a mechanic: the rule a game is played by.

It describes the mechanic rather than the actual game. The game's own board,
options, copy and values are data in its internal App Manifest and then made
available to Gamestage and the Monterosa Studio for operations.

On the command line a format is `--format`, so `--format group` is how you
choose one. In the manifest and on the wire the same thing is spelled
`archetype`, which is a file format we do not break; both words mean a format.
`--archetype` still works and is no longer shown in `--help`.

Pick your format first, before you change any code. It settles three things
that are hard to change later: what the server keeps hidden from a player, what
counts as one go, and when a player finds out how they did.

## Where your game's answer lives

Every game runs on an Engine. It holds the player's attempts and score, marks
each play once however many times the request arrives, enforces the limits and
serves the leaderboard. That is not a choice to make, so this page does not ask
you to make it.

What you do decide is **where the answer sits**, and it is one question:

**Does the answer have to be worked out from data a fan must never see?**

**If no**, it goes in Studio and a producer owns it. A quiz has a right option.
A prediction gets one when the world decides. The Engine marks against it.

**If yes**, it never goes near Studio. The Limit is the case: its result comes
from career totals a fan cannot be shown, because a fan who can see every value
can solve a game that is pure arithmetic. Those values live in your dataset,
the Engine reads them server-side, and no part of them reaches a browser.

Either way the game runs the same and your code is the same. The difference is
which of the two a producer can edit.

## Decision guide

| Ask | If yes | Format | `--format` |
| --- | --- | --- | --- |
| Is the fan trying to find a known subset of a board? | Each pick can be marked against an answer set | Search | `hunt` |
| Do hidden numeric values accumulate towards a ceiling? | A pick is neither right nor wrong by itself | Push Your Luck | `push` |
| Does one guess contain several items that must form a hidden group? | Feedback applies to the whole guess | Find the Groups | `group` |
| Does the answer not exist when the fan commits? | The play must be marked later | Predict | `predict` |
| Is each fan dealt their own board from a shared pool? | A line or a full board is scored once the pool's outcomes are known | Bingo | `bingo` |
| Does the fan aim and shoot, and the server play the keeper? | One shot is one request, resolved and replayed | Penalties | `shoot` |
| Is the fan dealt a card and asked which category it fits? | Only the server knows who qualifies for what | Place the Face | `place` |

Already know which one you want?

* ![](/docs/formats/hunt.png) [Search](#search): find the marked few among many
* ![](/docs/formats/push.png) [Push Your Luck](#push-your-luck): add hidden value towards a limit, and stop before you bust
* ![](/docs/formats/group.png) [Find the Groups](#find-the-groups): form the hidden sets in a board
* ![](/docs/formats/predict.png) [Predict](#predict): call it before it happens
* ![](/docs/formats/bingo.png) [Bingo](#bingo): each fan gets their own board, dealt from a pool
* ![](/docs/formats/shoot.png) [Penalties](#penalties): aim, choose a power, shoot; the Engine plays the keeper
* [Place the Face](#place-the-face): drop a dealt card on a category it fits

If none fits, stop and describe the missing rule. A new rule needs a human
decision on whether it is a new format, a Variant or customer-specific work.

We are taking suggestions for new formats and features, and the tool will send
one for you. No account needed:

```
gamestage suggest "a game where a fan ranks five players, scored on how close the order is"
gamestage suggest "..." --closest group --send
```

Without `--send` it prints the exact payload and sends nothing, so you can see
what leaves your machine before it does. That payload is four strings: what you
were building, the format that came nearest, the CLI version, and a marker
saying it is a suggestion. **Never the game.** No manifest, no round, no
answers, no source, and there is nowhere in the shape to put one.

## Live formats

The schema, local Engine and internal hosted Engine share the same set of
container contracts. The headings below are the names a creator sees.

One place still shows the older names. A deployed game's container type in
Monterosa Studio reads Find the answer, Under the limit, Puzzle, Prediction,
Bingo, Penalties or Place the Face, because that label is declared in a published App Spec
version and moving it costs a version of its own. Same formats, in the same
order.

### 探索  Search

![Three panels: a board of empty squares; the same board with two squares marked as the hidden answer set the Engine keeps back; the board with each pick marked right or wrong.](/docs/formats/hunt.png)

The fan picks the right selections from a board of options. Trivia with one question per
container uses this rule set.

The Engine keeps `targets` private and returns per-pick feedback and the
committed score.

| Field | Required | Meaning |
| --- | --- | --- |
| `board` | yes | Public choices, at least two |
| `targets` | yes | Private answer set, at least one |
| `required` | no | Targets needed to finish, defaults to all targets |
| `pointsPerTarget` | no | Score for each target, defaults to 10 |
| `completionBonus` | no | Added on completion, defaults to 0 |
| `attemptsAllowed` | no | Total plays allowed, defaults to 10 |
| `mistakesAllowed` | no | Wrong plays allowed, defaults to 4 |
| `open` | no | Whether play is accepted, defaults to true |

Validation rejects unreachable or repeated targets and a `required` count
larger than the answer set.

### 運試し  Push Your Luck

![Three panels: three named picks; three hidden values the Engine keeps back; a running total against a limit line.](/docs/formats/push.png)

Each pick adds a private value to a running total.

The fan tries to get close to `target` without going over.

The Engine keeps `entries[].value` private. The public challenge contains the
entry keys and labels, not their values.

| Field | Required | Meaning |
| --- | --- | --- |
| `target` | yes | Positive ceiling |
| `targetLabel` | no | Display label for the total |
| `slates` | yes | Ordered pick groups, each with `id`, `label` and entries |
| `tiers` | no | Result bands ordered from closest to furthest |
| `floorLabel` | no | Result below every declared tier |
| `bustLabel` | no | Result after passing the target |
| `open` | no | Whether play is accepted, defaults to true |

Entry keys must be unique across the whole container. Validation also rejects
a container whose cheapest possible total already exceeds its target.

### 共通点探し  Find the Groups

![Three panels: a board of empty squares; the same board with two hidden groups ringed, kept back by the Engine; one submitted group ringed and judged as a whole guess.](/docs/formats/group.png)

Submit a set, find the hidden groups and receive whole-guess feedback, for
example a near miss.

The Engine keeps group labels and membership private until a group is solved.
A round publishes the board, how many groups there are and how many items are in
one; which items belong together, and what any group is called, reach a player
only once they have found it.

`gamestage create --format group` scaffolds this one. Its round is two
groups of two, small enough that `gamestage verify` can play the page it wrote:
verify makes a guess by clicking the first two picks. A real board is usually
four groups of four, and the scaffolded page reads the size from the round, so
growing the groups needs no change to the page.

| Field | Required | Meaning |
| --- | --- | --- |
| `board` | yes | Public items, at least four |
| `groups` | yes | Private groups with `key`, `label` and `members` |
| `pointsPerGroup` | no | Score for each solved group, defaults to 10 |
| `completionBonus` | no | Added when all groups are solved, defaults to 0 |
| `attemptsAllowed` | no | Total guesses allowed, defaults to 10 |
| `mistakesAllowed` | no | Wrong guesses allowed, defaults to 4 |

Groups cannot overlap, every member must be on the board and every board item
must belong to a group.

### 予想  Predict

![Three panels: the fan's call, one option marked among three; an empty box with no answer yet; a tick arriving later once the result is settled.](/docs/formats/predict.png)

The fan commits a call before the result exists. Marking happens at settlement
rather than at commit.

The options are public. `result` is absent while the outcome is unknown and is
added when settling the same container. A committed play then returns
`marked: false` until the result exists. The client must render that as pending,
not as a loss.

| Field | Required | Meaning |
| --- | --- | --- |
| `options` | one of | Public options with a unique `key` and `label` |
| `questions` | one of | Several questions in one round, see below |
| `result` | no | Winning option once known, where the round asks one thing |
| `pointsForCorrect` | no | Default score for a correct call |
| `attemptsAllowed` | no | Calls a player may make, defaults to one |
| `open` | no | Whether new calls are accepted |

Validation rejects a result that was not one of the available options.

### Several questions in one round

A round asks one thing or several. `options` is the one-question form and
`questions` is the several-question form, and a round declares one or the
other: two homes for one question is how they come apart.

```yaml
questions:
  - id: winner
    kind: choice
    prompt: Who lifts it?
    options:
      - { key: real, label: Real Madrid }
      - { key: city, label: Manchester City, points: 25 }
  - id: goals
    kind: number
    prompt: Goals in the final?
    min: 0
    max: 10
    tolerance: 2
```

`id` is how a play names the question it answered, so it has to stay put across
a settlement. A round written the one-question way is read as a single question
with the id `1`, so nothing already provisioned changes.

**A number is marked by distance, not by equality.** `tolerance` is how far out
a fan can be and still score: points fall away linearly and reach nothing at
the edge of the band. A result of 20 with a tolerance of 5 pays in full at 20,
sixty per cent at 18 or 22, and nothing at 15 or 25. Without a tolerance a
number is exact, which is right for a count and wrong for anything a fan moves
a slider to: nobody hits a number on a continuum, so exact-only marking makes
the slider decorative.

`min` and `max` are required on a number and are enforced on the way in. A value
past either end is refused rather than clamped, because a clamped answer is one
the fan did not give.

**A prediction commits whole.** Every question is answered in one play, not one
play each: three plays would let a fan answer two and lose the third to a
dropped request, leaving a prediction half made against a round that has taken
their one attempt. A play missing an answer is refused and costs nothing.

**A round settles when every question has a result**, not when the first does.
Until then a play stays pending, because a fan told they scored 10 while two
questions are open has been given a number that will change.

Outcomes gain `partial`: a round of three where a fan called two right is
neither won nor lost. It counts as played, never as won, and it breaks a win
streak, because a win streak that survives not winning is not a win streak.

### ビンゴ  Bingo

![Three panels: a dealt board of filled squares; the same board blank, with which squares are right kept back by the Engine; one row ringed as the scored line.](/docs/formats/bingo.png)

Each fan is dealt several whole boards from a shared pool of cards, swipes
between them, and locks the one they want. They never see the rest of the
pool. Which cards come true is decided later, on the pool rather than on any
one fan's board, so every locked board is marked against the same decision.

A line, or the whole board, scores as soon as every card on it has a result.
That happens during the game rather than at the end: a bingo line stays
complete once complete, so a fan's score climbs as the world decides. The
outcome stays `pending` until every card on their board is settled.

#### A card is a Studio element, and a producer resolves it

**The round is the edition. Each card is one element on it.** That is the same
shape predict uses, and for the same reason: a card is resolved as a
prediction, so it is a prediction element, and the platform's own
`correct_option` holds its outcome.

So a producer opens the edition in Studio, sees one element per card, and
marks each one as it happens. Setting `correct_option` on a card is what makes
it come true for every fan whose board holds it, live, without republishing
anything.

A deploy creates the pool for you: every card in your manifest's `cardPool`
becomes an element. Cards a producer has already written are matched on their
own words and left alone, so a redeploy tops the edition up rather than
replacing it.

The board's shape is the **edition's** settings rather than any card's,
because a three by three board is a fact about the round:

| Setting | Default | Meaning |
| --- | --- | --- |
| `bingo_rows`, `bingo_columns` | 3 | The board's size |
| `bingo_centre_mode` | `free` | `none`, `free` or `choice` |
| `bingo_shuffle_allowance` | 3 | How many times a fan may redeal; with the first deal, that's how many boards they see in total |
| `bingo_prompt` | empty | What the page says above the board |
| `bingo_points_per_line` | 10 | Score for each completed line |
| `bingo_points_for_full_board` | 0 | Added once the whole board is correct |

The lock deadline is not a field you set. It is the edition's own scheduled
end.

#### The manifest fields

A manifest round is what a game plays before a producer has authored
anything, and what a deploy builds the pool from.

| Field | Required | Meaning |
| --- | --- | --- |
| `rows`, `columns` | yes | The board's size, at least 2 by 2 |
| `cardPool` | yes | Every card that could be dealt, at least enough to fill the board |
| `centreMode` | no | `none`, `free` or `choice`, defaults to `none` |
| `centreOptions` | one of | Cards the fan may choose for the centre, required when `centreMode` is `choice` |
| `allowCentreDuplicate` | no | Whether the centre may repeat a card already on the board, defaults to false |
| `lineTypes` | no | Which of `row`, `column`, `diagonal`, `full_board` score, defaults to `row` and `column` |
| `pointsPerLine` | no | Score for each completed line, defaults to 10 |
| `pointsForFullBoard` | no | Added once the whole board is correct, defaults to 0 |
| `shuffleAllowance` | no | How many times a fan may redeal. Defaults to 3, so with the first deal a fan sees four |
| `open` | no | Whether play is accepted, defaults to true |

#### What a fan's page has to do

Two operations rather than a list of picks, because there is no public board
to choose from.

`deal` asks for the carousel. It is refused a second time: the offer is made
whole, and a top-up would be a fan rerolling the set they were meant to choose
within. The boards come back in `progress.boards`, and the words for every
card on them in `progress.cards`.

`lock` commits one of them, and **must name which** by its position in the
carousel. A lock that names no board is refused rather than defaulting to the
first, because a page that lost track of which board a fan was looking at
would otherwise commit a different one silently, and the fan would find out at
settlement.

```js
await game.play({ op: "deal" });
await game.play({ op: "lock", board: 2 });
```

#### The first deploy of a bingo game plays its manifest

A deploy creates the cards, and the round in that same deploy is built before
they exist. So a brand-new bingo game serves its manifest round until you
deploy once more. Nothing is broken in between: the game plays, and the second
deploy is what moves it onto Studio.

### ペナルティ  Penalties

![Three panels: aim and power chosen towards the goal; the keeper diving one way, kept back by the Engine; the result, goal or save.](/docs/formats/shoot.png)

The fan picks a spot in the goal and a power, and shoots. The Engine plays the
keeper: it decides where the keeper dives, resolves goal, save, wide or off
the frame, scores it, and returns a short replay the page animates. One shot
is one request. The fan takes a fixed number of penalties and the shootout is
won by reaching a threshold of goals; every shot still scores after that, so
the round ends on the last shot and not the moment it is won.

#### What the first release is, plainly

`penalty_v1` is a bounded arcade approximation, and the name is the promise.
The ball is adjudicated at the goal line: power sets how long it takes to get
there and how far it can stray from the aim, and a ball touching a post or
the bar is a miss. The keeper's dive, and how far the ball strays, are drawn
from entropy the Engine mints when the round is provisioned, before any shot
is taken, so the keeper cannot react to where a fan aimed. Two fans on the
same round face the same keeper distribution and different actual dives:
comparable difficulty rather than identical challenges, and the leaderboard
should be read that way.

Nothing is timed. A power control chooses power, and the Engine awards
nothing for how fast a fan pressed or how close to a mark a meter stopped,
because a request cannot prove a browser's input event and a number the page
chose is a number the page can choose again. What the Engine can stand behind
is server-authoritative outcomes over bounded inputs and a bounded number of
shots. Bot resistance and one-person-one-entry are separate requirements.

#### The manifest fields

| Field | Required | Meaning |
| --- | --- | --- |
| `shotsAllowed` | no | Penalties a fan takes, 1 to 20. Defaults to 5, and `gamestage verify` asks you to write it down |
| `goalsToWin` | no | Goals that win the shootout. Defaults to 3. Must be reachable from `shotsAllowed` |
| `pointsPerGoal` | no | What each goal scores. Defaults to 100 |
| `difficulty` | no | The keeper: `easy_v1`, `balanced_v1` or `hard_v1`. Defaults to `balanced_v1` |
| `model` | no | `penalty_v1`, the only model. Naming another is refused rather than defaulted |
| `open` | no | Whether play is accepted, defaults to true |

There is no answer field, and there must never be one. The keeper's entropy is
the Engine's and is never in a manifest, an element or a browser.

#### The Studio settings

Every one of these sits on the round's element and every one is public,
because none is an answer. A producer who leaves a box empty gets the
default the box names.

| Setting | Default | Meaning |
| --- | --- | --- |
| `round_prompt` | empty | The line above the goal |
| `shoot_shots_allowed` | 5 | How many penalties a fan takes |
| `shoot_goals_to_win` | 3 | How many goals win the shootout |
| `shoot_points_per_goal` | 100 | What each goal is worth |
| `shoot_difficulty` | `balanced_v1` | Which keeper the fan faces |

#### What a fan's page has to do

Send the aim, the power and which shot it is, and draw what comes back. Aim
is two whole numbers, `aimX` from -1200 to 1200 with the posts at ±1000 and
`aimY` from -200 to 1200 with the bar at 1000, so a fan may aim wide and
miss. Power is 0 to 1000. A value outside those bounds is refused rather than
clamped, and so is any extra field: a page that sends `goal: true` or a
score of its own has its whole shot refused.

```js
const result = await game.shoot({ shotIndex: state.attempts_used, aimX: 800, aimY: 200, power: 650 });
result.shot.result;        // "goal", "saved", "wide" or "frame"
result.shot.replay;        // where the ball and the keeper ended, and when
```

`shotIndex` must be the next shot, so a stale tab cannot spend one. The
replay carries the ball's end position, the keeper's start and end, the
flight time and where any contact happened, all in the same units the Engine
adjudicated in; animate those and never compute an outcome. The whole
history is in `round_state.progress.shots`, so a reload draws settled state
without replaying anything.

The scaffold `gamestage create --format shoot` writes offers six aim zones
and three powers as real buttons, with sliders to fine-tune, and keeps a shot
whose response was lost so a reload retries the same penalty under the same
key. `gamestage verify` drives those buttons, watches the request, and fails a
page whose screen shows a result or a score the Engine did not commit.

### Place the Face

The fan is dealt a deck one card at a time and drops each onto a board of
category cells. A drop is right when that card qualifies for that category, and
only the Engine holds the map of who qualifies for what. A right drop fills the
cell and scores; a wrong drop spends a life and wastes the card; a fan may pass
a limited number of cards. The board is won by filling every cell and lost by
running out of lives.

The map from a card to the cells it may fill is the whole answer, so it never
reaches a browser: it is the exact parallel of a group round's hidden
membership. The board's labels and the deck's faces are public; which cells a
card fits is not.

This format is authored in the manifest. There is no `gamestage create`
scaffold for it, so a place game is built directly from its manifest rather
than from a starter template.

#### The manifest fields

| Field | Required | Meaning |
| --- | --- | --- |
| `board` | yes | The cells, each with a `key` and a shown `label` |
| `deck` | yes | The cards, in deal order, each with a `key` and a shown `name` |
| `qualifies` | yes | For each card, the cells it may fill. Private; never sent to a browser |
| `pointsPerPlacement` | no | What each correct placement scores. Defaults to 1 |
| `fullHouseBonus` | no | Added when every cell is filled. Defaults to 0 |
| `mistakesAllowed` | no | Wrong drops before the board is lost. Defaults to 4 |
| `skipsAllowed` | no | Cards a fan may pass. Defaults to 3 |
| `open` | no | Whether play is accepted, defaults to true |

Validation rejects a qualification naming a cell or a card that does not exist,
a cell no card can fill, and a deck shorter than the board.

We can build Engines for custom game formats. Get in touch if you want one.

## Shared fields

Every container has:

* **`archetype`**: the discriminator the Engine uses.

* **`number`**: a positive human-facing number, default 1. The stored id is derived as `round-<number>`.

* **`prompt`**: player-facing instruction, default empty.

* **`open`**: whether the container accepts plays when provisioned.

* **`playsPerSecond`**: the fair-use ceiling. Leave it out and a deploy sets
  2,560, which is far above any real audience and low enough to stop a runaway
  before it becomes a bill. Set your own number and yours is used. Held at the
  ceiling across four shards for thirty seconds inside a two-minute window
  stops the game taking plays for half an hour and tells us; two shards for
  ten seconds warns first.

Use the game's `experience.container_label` in creator-facing copy. The wire
type remains `Round` until a deliberate breaking change.

## Where a round's content lives, and how to protect it

A round can reach the Engine two ways. In the game's manifest, which is what
`gamestage dev` reads and what a first deploy carries. Or from a URL a producer
sets in Studio, on the element's **Content source** field, which the control
plane fetches when the round is provisioned.

**The second is the one that matters after launch**, because it is how a
producer changes a round without a deploy.

### What Content source takes

The field is called **Content source** on a producer's screen and `source_url`
underneath, and it sits on the element, which is one round. It takes one URL.

**The document behind it is the same JSON as `backend.round` in the manifest.**
That is the useful fact: there is no second shape to learn, no wrapper and no
envelope. Whatever your manifest's `round` holds, put exactly that at the URL.
It is parsed against the same schema either way, so a round the manifest would
have refused is refused here too.

A push round, cut down to the smallest document the schema accepts:

```json
{
  "archetype": "push",
  "number": 4,
  "prompt": "Pick one from each slate. Stay under the target.",
  "target": 60000,
  "targetLabel": "Career Points",
  "slates": [
    {
      "id": "guards",
      "label": "Guards",
      "entries": [
        { "key": "jordami01", "label": "Michael Jordan", "value": 32292 },
        { "key": "paulch01", "label": "Chris Paul", "value": 21000 }
      ]
    },
    {
      "id": "forwards",
      "label": "Forwards",
      "entries": [
        { "key": "birdla01", "label": "Larry Bird", "value": 21791 },
        { "key": "duncati01", "label": "Tim Duncan", "value": 26496 }
      ]
    }
  ]
}
```

The rules the control plane applies when it reads one:

- **`http` or `https` only.** Anything else is refused by name, because `file:`
  would read the control plane's own disk.
- **It must be JSON, and it must parse as a round.** A document that is neither
  fails the deploy and the refusal says which.
- **A source that cannot be read never falls back to the manifest.** The deploy
  fails loudly instead. A game quietly serving yesterday's round while Studio
  shows today's is worse than a game that does not deploy.
- **The value is withheld from a fan's browser.** An element's other fields
  travel in the platform's public feeds; this one is marked private, because
  whoever holds the link holds the answers.

**The deployed control plane does not read this field.** This path runs in the
control plane the Studio prototype and the test suite use. The deployed
control plane builds a round the other way, from the content fields a producer
fills in against your dataset, and has no fetcher wired in for a Content
source URL, so setting one on a live game has no effect today. Ask before
designing a game's operations around it.

### Protect that URL

**A round's content usually holds the answers.** Which options are right, what
each pick is worth, which items belong in which group. The Engine keeps all of
that away from a fan's browser, and that is the whole reason a game needs an
Engine at all.

**None of which helps if the URL itself can be found.** It is fetched
server-side, so it never appears in a page, but a file sitting at a guessable
address on a public host is a file anybody can read, and search engines index
what they can reach.

So, and we recommend this rather than enforce it:

- **Put the secret in the URL, because the URL is all we send.** The control
  plane fetches with the address and nothing else: there is nowhere to configure
  a header, so a shared secret in one is not an option today. What does work is
  a signed URL, or a path long and random enough that nobody guesses it.
- **Do not serve it from the same place as the game.** A deploy publishes a
  directory, and anything in that directory is public. This has already cost
  once: a deploy shipped `gamestage.yaml` alongside the game and put a round's
  forty pick values at a public URL, while `verify` correctly reported the page
  itself clean. The answers were beside the game rather than in it.
- **Keep it out of a public repository**, for the same reason and with the same
  consequence.
- **Add `noindex`** if it must live somewhere reachable, and treat that as the
  weakest of these rather than the answer.

**A username and password in the URL does not work**, so do not reach for it.
`https://user:pass@example.com/round.json` is refused before a request is made:
*"Request cannot be constructed from a URL that includes credentials"*. That is
the web's own rule rather than ours, so a deploy fails outright instead of
fetching. If you need a credential, put it in the path as a signed URL or a long
random segment, which is the first point above.

> **These are good habits, not a security control.** Everything above makes a
> URL hard to guess. None of it makes the content safe from someone who obtains
> the link, because the link is the whole credential: it can be forwarded,
> logged by a proxy, or left in a browser's history.
>
> **Get in touch before you run anything where being wrong is expensive.** A
> prize, a regulated promotion, anything with a compliance obligation behind it:
> we have stronger options and they are worth a conversation rather than a
> paragraph in a chapter.

**A leak here does not break the game, it spoils it**, which is worse in the one
way that matters: nothing errors, nobody is alerted, and the first sign is a
leaderboard that stops making sense.
