Game formats

The seven Engine rule sets that run, and the four that are declared only.

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

AskIf yesFormat--format
Is the fan trying to find a known subset of a board?Each pick can be marked against an answer setSearchhunt
Do hidden numeric values accumulate towards a ceiling?A pick is neither right nor wrong by itselfPush Your Luckpush
Does one guess contain several items that must form a hidden group?Feedback applies to the whole guessFind the Groupsgroup
Does the answer not exist when the fan commits?The play must be marked laterPredictpredict
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 knownBingobingo
Does the fan aim and shoot, and the server play the keeper?One shot is one request, resolved and replayedPenaltiesshoot
Is the fan dealt a card and asked which category it fits?Only the server knows who qualifies for whatPlace the Faceplace

Already know which one you want?

  • Search: find the marked few among many
  • Push Your Luck: add hidden value towards a limit, and stop before you bust
  • Find the Groups: form the hidden sets in a board
  • Predict: call it before it happens
  • Bingo: each fan gets their own board, dealt from a pool
  • Penalties: aim, choose a power, shoot; the Engine plays the keeper
  • 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.

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.

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.

FieldRequiredMeaning
boardyesPublic choices, at least two
targetsyesPrivate answer set, at least one
requirednoTargets needed to finish, defaults to all targets
pointsPerTargetnoScore for each target, defaults to 10
completionBonusnoAdded on completion, defaults to 0
attemptsAllowednoTotal plays allowed, defaults to 10
mistakesAllowednoWrong plays allowed, defaults to 4
opennoWhether 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.

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.

FieldRequiredMeaning
targetyesPositive ceiling
targetLabelnoDisplay label for the total
slatesyesOrdered pick groups, each with id, label and entries
tiersnoResult bands ordered from closest to furthest
floorLabelnoResult below every declared tier
bustLabelnoResult after passing the target
opennoWhether 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.

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.

FieldRequiredMeaning
boardyesPublic items, at least four
groupsyesPrivate groups with key, label and members
pointsPerGroupnoScore for each solved group, defaults to 10
completionBonusnoAdded when all groups are solved, defaults to 0
attemptsAllowednoTotal guesses allowed, defaults to 10
mistakesAllowednoWrong 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.

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.

FieldRequiredMeaning
optionsone ofPublic options with a unique key and label
questionsone ofSeveral questions in one round, see below
resultnoWinning option once known, where the round asks one thing
pointsForCorrectnoDefault score for a correct call
attemptsAllowednoCalls a player may make, defaults to one
opennoWhether 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.

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.

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:

SettingDefaultMeaning
bingo_rows, bingo_columns3The board's size
bingo_centre_modefreenone, free or choice
bingo_shuffle_allowance3How many times a fan may redeal; with the first deal, that's how many boards they see in total
bingo_promptemptyWhat the page says above the board
bingo_points_per_line10Score for each completed line
bingo_points_for_full_board0Added 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.

FieldRequiredMeaning
rows, columnsyesThe board's size, at least 2 by 2
cardPoolyesEvery card that could be dealt, at least enough to fill the board
centreModenonone, free or choice, defaults to none
centreOptionsone ofCards the fan may choose for the centre, required when centreMode is choice
allowCentreDuplicatenoWhether the centre may repeat a card already on the board, defaults to false
lineTypesnoWhich of row, column, diagonal, full_board score, defaults to row and column
pointsPerLinenoScore for each completed line, defaults to 10
pointsForFullBoardnoAdded once the whole board is correct, defaults to 0
shuffleAllowancenoHow many times a fan may redeal. Defaults to 3, so with the first deal a fan sees four
opennoWhether 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.

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.

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

FieldRequiredMeaning
shotsAllowednoPenalties a fan takes, 1 to 20. Defaults to 5, and gamestage verify asks you to write it down
goalsToWinnoGoals that win the shootout. Defaults to 3. Must be reachable from shotsAllowed
pointsPerGoalnoWhat each goal scores. Defaults to 100
difficultynoThe keeper: easy_v1, balanced_v1 or hard_v1. Defaults to balanced_v1
modelnopenalty_v1, the only model. Naming another is refused rather than defaulted
opennoWhether 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.

SettingDefaultMeaning
round_promptemptyThe line above the goal
shoot_shots_allowed5How many penalties a fan takes
shoot_goals_to_win3How many goals win the shootout
shoot_points_per_goal100What each goal is worth
shoot_difficultybalanced_v1Which 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.

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

FieldRequiredMeaning
boardyesThe cells, each with a key and a shown label
deckyesThe cards, in deal order, each with a key and a shown name
qualifiesyesFor each card, the cells it may fill. Private; never sent to a browser
pointsPerPlacementnoWhat each correct placement scores. Defaults to 1
fullHouseBonusnoAdded when every cell is filled. Defaults to 0
mistakesAllowednoWrong drops before the board is lost. Defaults to 4
skipsAllowednoCards a fan may pass. Defaults to 3
opennoWhether 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:

{
  "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.

> > 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.