{"doc":"game-formats","title":"Game formats","markdown":"# Game formats\n\nA game format is a mechanic: the rule a game is played by.\n\nIt describes the mechanic rather than the actual game. The game's own board,\noptions, copy and values are data in its internal App Manifest and then made\navailable to Gamestage and the Monterosa Studio for operations.\n\nOn the command line a format is `--format`, so `--format group` is how you\nchoose one. In the manifest and on the wire the same thing is spelled\n`archetype`, which is a file format we do not break; both words mean a format.\n`--archetype` still works and is no longer shown in `--help`.\n\nPick your format first, before you change any code. It settles three things\nthat are hard to change later: what the server keeps hidden from a player, what\ncounts as one go, and when a player finds out how they did.\n\n## Where your game's answer lives\n\nEvery game runs on an Engine. It holds the player's attempts and score, marks\neach play once however many times the request arrives, enforces the limits and\nserves the leaderboard. That is not a choice to make, so this page does not ask\nyou to make it.\n\nWhat you do decide is **where the answer sits**, and it is one question:\n\n**Does the answer have to be worked out from data a fan must never see?**\n\n**If no**, it goes in Studio and a producer owns it. A quiz has a right option.\nA prediction gets one when the world decides. The Engine marks against it.\n\n**If yes**, it never goes near Studio. The Limit is the case: its result comes\nfrom career totals a fan cannot be shown, because a fan who can see every value\ncan solve a game that is pure arithmetic. Those values live in your dataset,\nthe Engine reads them server-side, and no part of them reaches a browser.\n\nEither way the game runs the same and your code is the same. The difference is\nwhich of the two a producer can edit.\n\n## Decision guide\n\n| Ask | If yes | Format | `--format` |\n| --- | --- | --- | --- |\n| Is the fan trying to find a known subset of a board? | Each pick can be marked against an answer set | Search | `hunt` |\n| Do hidden numeric values accumulate towards a ceiling? | A pick is neither right nor wrong by itself | Push Your Luck | `push` |\n| Does one guess contain several items that must form a hidden group? | Feedback applies to the whole guess | Find the Groups | `group` |\n| Does the answer not exist when the fan commits? | The play must be marked later | Predict | `predict` |\n| 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` |\n| Does the fan aim and shoot, and the server play the keeper? | One shot is one request, resolved and replayed | Penalties | `shoot` |\n| Is the fan dealt a card and asked which category it fits? | Only the server knows who qualifies for what | Place the Face | `place` |\n\nAlready know which one you want?\n\n* ![](/docs/formats/hunt.png) [Search](#search): find the marked few among many\n* ![](/docs/formats/push.png) [Push Your Luck](#push-your-luck): add hidden value towards a limit, and stop before you bust\n* ![](/docs/formats/group.png) [Find the Groups](#find-the-groups): form the hidden sets in a board\n* ![](/docs/formats/predict.png) [Predict](#predict): call it before it happens\n* ![](/docs/formats/bingo.png) [Bingo](#bingo): each fan gets their own board, dealt from a pool\n* ![](/docs/formats/shoot.png) [Penalties](#penalties): aim, choose a power, shoot; the Engine plays the keeper\n* [Place the Face](#place-the-face): drop a dealt card on a category it fits\n\nIf none fits, stop and describe the missing rule. A new rule needs a human\ndecision on whether it is a new format, a Variant or customer-specific work.\n\nWe are taking suggestions for new formats and features, and the tool will send\none for you. No account needed:\n\n```\ngamestage suggest \"a game where a fan ranks five players, scored on how close the order is\"\ngamestage suggest \"...\" --closest group --send\n```\n\nWithout `--send` it prints the exact payload and sends nothing, so you can see\nwhat leaves your machine before it does. That payload is four strings: what you\nwere building, the format that came nearest, the CLI version, and a marker\nsaying it is a suggestion. **Never the game.** No manifest, no round, no\nanswers, no source, and there is nowhere in the shape to put one.\n\n## Live formats\n\nThe schema, local Engine and internal hosted Engine share the same set of\ncontainer contracts. The headings below are the names a creator sees.\n\nOne place still shows the older names. A deployed game's container type in\nMonterosa Studio reads Find the answer, Under the limit, Puzzle, Prediction,\nBingo, Penalties or Place the Face, because that label is declared in a published App Spec\nversion and moving it costs a version of its own. Same formats, in the same\norder.\n\n### 探索  Search\n\n![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)\n\nThe fan picks the right selections from a board of options. Trivia with one question per\ncontainer uses this rule set.\n\nThe Engine keeps `targets` private and returns per-pick feedback and the\ncommitted score.\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `board` | yes | Public choices, at least two |\n| `targets` | yes | Private answer set, at least one |\n| `required` | no | Targets needed to finish, defaults to all targets |\n| `pointsPerTarget` | no | Score for each target, defaults to 10 |\n| `completionBonus` | no | Added on completion, defaults to 0 |\n| `attemptsAllowed` | no | Total plays allowed, defaults to 10 |\n| `mistakesAllowed` | no | Wrong plays allowed, defaults to 4 |\n| `open` | no | Whether play is accepted, defaults to true |\n\nValidation rejects unreachable or repeated targets and a `required` count\nlarger than the answer set.\n\n### 運試し  Push Your Luck\n\n![Three panels: three named picks; three hidden values the Engine keeps back; a running total against a limit line.](/docs/formats/push.png)\n\nEach pick adds a private value to a running total.\n\nThe fan tries to get close to `target` without going over.\n\nThe Engine keeps `entries[].value` private. The public challenge contains the\nentry keys and labels, not their values.\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `target` | yes | Positive ceiling |\n| `targetLabel` | no | Display label for the total |\n| `slates` | yes | Ordered pick groups, each with `id`, `label` and entries |\n| `tiers` | no | Result bands ordered from closest to furthest |\n| `floorLabel` | no | Result below every declared tier |\n| `bustLabel` | no | Result after passing the target |\n| `open` | no | Whether play is accepted, defaults to true |\n\nEntry keys must be unique across the whole container. Validation also rejects\na container whose cheapest possible total already exceeds its target.\n\n### 共通点探し  Find the Groups\n\n![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)\n\nSubmit a set, find the hidden groups and receive whole-guess feedback, for\nexample a near miss.\n\nThe Engine keeps group labels and membership private until a group is solved.\nA round publishes the board, how many groups there are and how many items are in\none; which items belong together, and what any group is called, reach a player\nonly once they have found it.\n\n`gamestage create --format group` scaffolds this one. Its round is two\ngroups of two, small enough that `gamestage verify` can play the page it wrote:\nverify makes a guess by clicking the first two picks. A real board is usually\nfour groups of four, and the scaffolded page reads the size from the round, so\ngrowing the groups needs no change to the page.\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `board` | yes | Public items, at least four |\n| `groups` | yes | Private groups with `key`, `label` and `members` |\n| `pointsPerGroup` | no | Score for each solved group, defaults to 10 |\n| `completionBonus` | no | Added when all groups are solved, defaults to 0 |\n| `attemptsAllowed` | no | Total guesses allowed, defaults to 10 |\n| `mistakesAllowed` | no | Wrong guesses allowed, defaults to 4 |\n\nGroups cannot overlap, every member must be on the board and every board item\nmust belong to a group.\n\n### 予想  Predict\n\n![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)\n\nThe fan commits a call before the result exists. Marking happens at settlement\nrather than at commit.\n\nThe options are public. `result` is absent while the outcome is unknown and is\nadded when settling the same container. A committed play then returns\n`marked: false` until the result exists. The client must render that as pending,\nnot as a loss.\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `options` | one of | Public options with a unique `key` and `label` |\n| `questions` | one of | Several questions in one round, see below |\n| `result` | no | Winning option once known, where the round asks one thing |\n| `pointsForCorrect` | no | Default score for a correct call |\n| `attemptsAllowed` | no | Calls a player may make, defaults to one |\n| `open` | no | Whether new calls are accepted |\n\nValidation rejects a result that was not one of the available options.\n\n### Several questions in one round\n\nA round asks one thing or several. `options` is the one-question form and\n`questions` is the several-question form, and a round declares one or the\nother: two homes for one question is how they come apart.\n\n```yaml\nquestions:\n  - id: winner\n    kind: choice\n    prompt: Who lifts it?\n    options:\n      - { key: real, label: Real Madrid }\n      - { key: city, label: Manchester City, points: 25 }\n  - id: goals\n    kind: number\n    prompt: Goals in the final?\n    min: 0\n    max: 10\n    tolerance: 2\n```\n\n`id` is how a play names the question it answered, so it has to stay put across\na settlement. A round written the one-question way is read as a single question\nwith the id `1`, so nothing already provisioned changes.\n\n**A number is marked by distance, not by equality.** `tolerance` is how far out\na fan can be and still score: points fall away linearly and reach nothing at\nthe edge of the band. A result of 20 with a tolerance of 5 pays in full at 20,\nsixty per cent at 18 or 22, and nothing at 15 or 25. Without a tolerance a\nnumber is exact, which is right for a count and wrong for anything a fan moves\na slider to: nobody hits a number on a continuum, so exact-only marking makes\nthe slider decorative.\n\n`min` and `max` are required on a number and are enforced on the way in. A value\npast either end is refused rather than clamped, because a clamped answer is one\nthe fan did not give.\n\n**A prediction commits whole.** Every question is answered in one play, not one\nplay each: three plays would let a fan answer two and lose the third to a\ndropped request, leaving a prediction half made against a round that has taken\ntheir one attempt. A play missing an answer is refused and costs nothing.\n\n**A round settles when every question has a result**, not when the first does.\nUntil then a play stays pending, because a fan told they scored 10 while two\nquestions are open has been given a number that will change.\n\nOutcomes gain `partial`: a round of three where a fan called two right is\nneither won nor lost. It counts as played, never as won, and it breaks a win\nstreak, because a win streak that survives not winning is not a win streak.\n\n### ビンゴ  Bingo\n\n![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)\n\nEach fan is dealt several whole boards from a shared pool of cards, swipes\nbetween them, and locks the one they want. They never see the rest of the\npool. Which cards come true is decided later, on the pool rather than on any\none fan's board, so every locked board is marked against the same decision.\n\nA line, or the whole board, scores as soon as every card on it has a result.\nThat happens during the game rather than at the end: a bingo line stays\ncomplete once complete, so a fan's score climbs as the world decides. The\noutcome stays `pending` until every card on their board is settled.\n\n#### A card is a Studio element, and a producer resolves it\n\n**The round is the edition. Each card is one element on it.** That is the same\nshape predict uses, and for the same reason: a card is resolved as a\nprediction, so it is a prediction element, and the platform's own\n`correct_option` holds its outcome.\n\nSo a producer opens the edition in Studio, sees one element per card, and\nmarks each one as it happens. Setting `correct_option` on a card is what makes\nit come true for every fan whose board holds it, live, without republishing\nanything.\n\nA deploy creates the pool for you: every card in your manifest's `cardPool`\nbecomes an element. Cards a producer has already written are matched on their\nown words and left alone, so a redeploy tops the edition up rather than\nreplacing it.\n\nThe board's shape is the **edition's** settings rather than any card's,\nbecause a three by three board is a fact about the round:\n\n| Setting | Default | Meaning |\n| --- | --- | --- |\n| `bingo_rows`, `bingo_columns` | 3 | The board's size |\n| `bingo_centre_mode` | `free` | `none`, `free` or `choice` |\n| `bingo_shuffle_allowance` | 3 | How many times a fan may redeal; with the first deal, that's how many boards they see in total |\n| `bingo_prompt` | empty | What the page says above the board |\n| `bingo_points_per_line` | 10 | Score for each completed line |\n| `bingo_points_for_full_board` | 0 | Added once the whole board is correct |\n\nThe lock deadline is not a field you set. It is the edition's own scheduled\nend.\n\n#### The manifest fields\n\nA manifest round is what a game plays before a producer has authored\nanything, and what a deploy builds the pool from.\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `rows`, `columns` | yes | The board's size, at least 2 by 2 |\n| `cardPool` | yes | Every card that could be dealt, at least enough to fill the board |\n| `centreMode` | no | `none`, `free` or `choice`, defaults to `none` |\n| `centreOptions` | one of | Cards the fan may choose for the centre, required when `centreMode` is `choice` |\n| `allowCentreDuplicate` | no | Whether the centre may repeat a card already on the board, defaults to false |\n| `lineTypes` | no | Which of `row`, `column`, `diagonal`, `full_board` score, defaults to `row` and `column` |\n| `pointsPerLine` | no | Score for each completed line, defaults to 10 |\n| `pointsForFullBoard` | no | Added once the whole board is correct, defaults to 0 |\n| `shuffleAllowance` | no | How many times a fan may redeal. Defaults to 3, so with the first deal a fan sees four |\n| `open` | no | Whether play is accepted, defaults to true |\n\n#### What a fan's page has to do\n\nTwo operations rather than a list of picks, because there is no public board\nto choose from.\n\n`deal` asks for the carousel. It is refused a second time: the offer is made\nwhole, and a top-up would be a fan rerolling the set they were meant to choose\nwithin. The boards come back in `progress.boards`, and the words for every\ncard on them in `progress.cards`.\n\n`lock` commits one of them, and **must name which** by its position in the\ncarousel. A lock that names no board is refused rather than defaulting to the\nfirst, because a page that lost track of which board a fan was looking at\nwould otherwise commit a different one silently, and the fan would find out at\nsettlement.\n\n```js\nawait game.play({ op: \"deal\" });\nawait game.play({ op: \"lock\", board: 2 });\n```\n\n#### The first deploy of a bingo game plays its manifest\n\nA deploy creates the cards, and the round in that same deploy is built before\nthey exist. So a brand-new bingo game serves its manifest round until you\ndeploy once more. Nothing is broken in between: the game plays, and the second\ndeploy is what moves it onto Studio.\n\n### ペナルティ  Penalties\n\n![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)\n\nThe fan picks a spot in the goal and a power, and shoots. The Engine plays the\nkeeper: it decides where the keeper dives, resolves goal, save, wide or off\nthe frame, scores it, and returns a short replay the page animates. One shot\nis one request. The fan takes a fixed number of penalties and the shootout is\nwon by reaching a threshold of goals; every shot still scores after that, so\nthe round ends on the last shot and not the moment it is won.\n\n#### What the first release is, plainly\n\n`penalty_v1` is a bounded arcade approximation, and the name is the promise.\nThe ball is adjudicated at the goal line: power sets how long it takes to get\nthere and how far it can stray from the aim, and a ball touching a post or\nthe bar is a miss. The keeper's dive, and how far the ball strays, are drawn\nfrom entropy the Engine mints when the round is provisioned, before any shot\nis taken, so the keeper cannot react to where a fan aimed. Two fans on the\nsame round face the same keeper distribution and different actual dives:\ncomparable difficulty rather than identical challenges, and the leaderboard\nshould be read that way.\n\nNothing is timed. A power control chooses power, and the Engine awards\nnothing for how fast a fan pressed or how close to a mark a meter stopped,\nbecause a request cannot prove a browser's input event and a number the page\nchose is a number the page can choose again. What the Engine can stand behind\nis server-authoritative outcomes over bounded inputs and a bounded number of\nshots. Bot resistance and one-person-one-entry are separate requirements.\n\n#### The manifest fields\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `shotsAllowed` | no | Penalties a fan takes, 1 to 20. Defaults to 5, and `gamestage verify` asks you to write it down |\n| `goalsToWin` | no | Goals that win the shootout. Defaults to 3. Must be reachable from `shotsAllowed` |\n| `pointsPerGoal` | no | What each goal scores. Defaults to 100 |\n| `difficulty` | no | The keeper: `easy_v1`, `balanced_v1` or `hard_v1`. Defaults to `balanced_v1` |\n| `model` | no | `penalty_v1`, the only model. Naming another is refused rather than defaulted |\n| `open` | no | Whether play is accepted, defaults to true |\n\nThere is no answer field, and there must never be one. The keeper's entropy is\nthe Engine's and is never in a manifest, an element or a browser.\n\n#### The Studio settings\n\nEvery one of these sits on the round's element and every one is public,\nbecause none is an answer. A producer who leaves a box empty gets the\ndefault the box names.\n\n| Setting | Default | Meaning |\n| --- | --- | --- |\n| `round_prompt` | empty | The line above the goal |\n| `shoot_shots_allowed` | 5 | How many penalties a fan takes |\n| `shoot_goals_to_win` | 3 | How many goals win the shootout |\n| `shoot_points_per_goal` | 100 | What each goal is worth |\n| `shoot_difficulty` | `balanced_v1` | Which keeper the fan faces |\n\n#### What a fan's page has to do\n\nSend the aim, the power and which shot it is, and draw what comes back. Aim\nis two whole numbers, `aimX` from -1200 to 1200 with the posts at ±1000 and\n`aimY` from -200 to 1200 with the bar at 1000, so a fan may aim wide and\nmiss. Power is 0 to 1000. A value outside those bounds is refused rather than\nclamped, and so is any extra field: a page that sends `goal: true` or a\nscore of its own has its whole shot refused.\n\n```js\nconst result = await game.shoot({ shotIndex: state.attempts_used, aimX: 800, aimY: 200, power: 650 });\nresult.shot.result;        // \"goal\", \"saved\", \"wide\" or \"frame\"\nresult.shot.replay;        // where the ball and the keeper ended, and when\n```\n\n`shotIndex` must be the next shot, so a stale tab cannot spend one. The\nreplay carries the ball's end position, the keeper's start and end, the\nflight time and where any contact happened, all in the same units the Engine\nadjudicated in; animate those and never compute an outcome. The whole\nhistory is in `round_state.progress.shots`, so a reload draws settled state\nwithout replaying anything.\n\nThe scaffold `gamestage create --format shoot` writes offers six aim zones\nand three powers as real buttons, with sliders to fine-tune, and keeps a shot\nwhose response was lost so a reload retries the same penalty under the same\nkey. `gamestage verify` drives those buttons, watches the request, and fails a\npage whose screen shows a result or a score the Engine did not commit.\n\n### Place the Face\n\nThe fan is dealt a deck one card at a time and drops each onto a board of\ncategory cells. A drop is right when that card qualifies for that category, and\nonly the Engine holds the map of who qualifies for what. A right drop fills the\ncell and scores; a wrong drop spends a life and wastes the card; a fan may pass\na limited number of cards. The board is won by filling every cell and lost by\nrunning out of lives.\n\nThe map from a card to the cells it may fill is the whole answer, so it never\nreaches a browser: it is the exact parallel of a group round's hidden\nmembership. The board's labels and the deck's faces are public; which cells a\ncard fits is not.\n\nThis format is authored in the manifest. There is no `gamestage create`\nscaffold for it, so a place game is built directly from its manifest rather\nthan from a starter template.\n\n#### The manifest fields\n\n| Field | Required | Meaning |\n| --- | --- | --- |\n| `board` | yes | The cells, each with a `key` and a shown `label` |\n| `deck` | yes | The cards, in deal order, each with a `key` and a shown `name` |\n| `qualifies` | yes | For each card, the cells it may fill. Private; never sent to a browser |\n| `pointsPerPlacement` | no | What each correct placement scores. Defaults to 1 |\n| `fullHouseBonus` | no | Added when every cell is filled. Defaults to 0 |\n| `mistakesAllowed` | no | Wrong drops before the board is lost. Defaults to 4 |\n| `skipsAllowed` | no | Cards a fan may pass. Defaults to 3 |\n| `open` | no | Whether play is accepted, defaults to true |\n\nValidation rejects a qualification naming a cell or a card that does not exist,\na cell no card can fill, and a deck shorter than the board.\n\nWe can build Engines for custom game formats. Get in touch if you want one.\n\n## Shared fields\n\nEvery container has:\n\n* **`archetype`**: the discriminator the Engine uses.\n\n* **`number`**: a positive human-facing number, default 1. The stored id is derived as `round-<number>`.\n\n* **`prompt`**: player-facing instruction, default empty.\n\n* **`open`**: whether the container accepts plays when provisioned.\n\n* **`playsPerSecond`**: the fair-use ceiling. Leave it out and a deploy sets\n  2,560, which is far above any real audience and low enough to stop a runaway\n  before it becomes a bill. Set your own number and yours is used. Held at the\n  ceiling across four shards for thirty seconds inside a two-minute window\n  stops the game taking plays for half an hour and tells us; two shards for\n  ten seconds warns first.\n\nUse the game's `experience.container_label` in creator-facing copy. The wire\ntype remains `Round` until a deliberate breaking change.\n\n## Where a round's content lives, and how to protect it\n\nA round can reach the Engine two ways. In the game's manifest, which is what\n`gamestage dev` reads and what a first deploy carries. Or from a URL a producer\nsets in Studio, on the element's **Content source** field, which the control\nplane fetches when the round is provisioned.\n\n**The second is the one that matters after launch**, because it is how a\nproducer changes a round without a deploy.\n\n### What Content source takes\n\nThe field is called **Content source** on a producer's screen and `source_url`\nunderneath, and it sits on the element, which is one round. It takes one URL.\n\n**The document behind it is the same JSON as `backend.round` in the manifest.**\nThat is the useful fact: there is no second shape to learn, no wrapper and no\nenvelope. Whatever your manifest's `round` holds, put exactly that at the URL.\nIt is parsed against the same schema either way, so a round the manifest would\nhave refused is refused here too.\n\nA push round, cut down to the smallest document the schema accepts:\n\n```json\n{\n  \"archetype\": \"push\",\n  \"number\": 4,\n  \"prompt\": \"Pick one from each slate. Stay under the target.\",\n  \"target\": 60000,\n  \"targetLabel\": \"Career Points\",\n  \"slates\": [\n    {\n      \"id\": \"guards\",\n      \"label\": \"Guards\",\n      \"entries\": [\n        { \"key\": \"jordami01\", \"label\": \"Michael Jordan\", \"value\": 32292 },\n        { \"key\": \"paulch01\", \"label\": \"Chris Paul\", \"value\": 21000 }\n      ]\n    },\n    {\n      \"id\": \"forwards\",\n      \"label\": \"Forwards\",\n      \"entries\": [\n        { \"key\": \"birdla01\", \"label\": \"Larry Bird\", \"value\": 21791 },\n        { \"key\": \"duncati01\", \"label\": \"Tim Duncan\", \"value\": 26496 }\n      ]\n    }\n  ]\n}\n```\n\nThe rules the control plane applies when it reads one:\n\n- **`http` or `https` only.** Anything else is refused by name, because `file:`\n  would read the control plane's own disk.\n- **It must be JSON, and it must parse as a round.** A document that is neither\n  fails the deploy and the refusal says which.\n- **A source that cannot be read never falls back to the manifest.** The deploy\n  fails loudly instead. A game quietly serving yesterday's round while Studio\n  shows today's is worse than a game that does not deploy.\n- **The value is withheld from a fan's browser.** An element's other fields\n  travel in the platform's public feeds; this one is marked private, because\n  whoever holds the link holds the answers.\n\n**The deployed control plane does not read this field.** This path runs in the\ncontrol plane the Studio prototype and the test suite use. The deployed\ncontrol plane builds a round the other way, from the content fields a producer\nfills in against your dataset, and has no fetcher wired in for a Content\nsource URL, so setting one on a live game has no effect today. Ask before\ndesigning a game's operations around it.\n\n### Protect that URL\n\n**A round's content usually holds the answers.** Which options are right, what\neach pick is worth, which items belong in which group. The Engine keeps all of\nthat away from a fan's browser, and that is the whole reason a game needs an\nEngine at all.\n\n**None of which helps if the URL itself can be found.** It is fetched\nserver-side, so it never appears in a page, but a file sitting at a guessable\naddress on a public host is a file anybody can read, and search engines index\nwhat they can reach.\n\nSo, and we recommend this rather than enforce it:\n\n- **Put the secret in the URL, because the URL is all we send.** The control\n  plane fetches with the address and nothing else: there is nowhere to configure\n  a header, so a shared secret in one is not an option today. What does work is\n  a signed URL, or a path long and random enough that nobody guesses it.\n- **Do not serve it from the same place as the game.** A deploy publishes a\n  directory, and anything in that directory is public. This has already cost\n  once: a deploy shipped `gamestage.yaml` alongside the game and put a round's\n  forty pick values at a public URL, while `verify` correctly reported the page\n  itself clean. The answers were beside the game rather than in it.\n- **Keep it out of a public repository**, for the same reason and with the same\n  consequence.\n- **Add `noindex`** if it must live somewhere reachable, and treat that as the\n  weakest of these rather than the answer.\n\n**A username and password in the URL does not work**, so do not reach for it.\n`https://user:pass@example.com/round.json` is refused before a request is made:\n*\"Request cannot be constructed from a URL that includes credentials\"*. That is\nthe web's own rule rather than ours, so a deploy fails outright instead of\nfetching. If you need a credential, put it in the path as a signed URL or a long\nrandom segment, which is the first point above.\n\n> **These are good habits, not a security control.** Everything above makes a\n> URL hard to guess. None of it makes the content safe from someone who obtains\n> the link, because the link is the whole credential: it can be forwarded,\n> logged by a proxy, or left in a browser's history.\n>\n> **Get in touch before you run anything where being wrong is expensive.** A\n> prize, a regulated promotion, anything with a compliance obligation behind it:\n> we have stronger options and they are worth a conversation rather than a\n> paragraph in a chapter.\n\n**A leak here does not break the game, it spoils it**, which is worse in the one\nway that matters: nothing errors, nobody is alerted, and the first sign is a\nleaderboard that stops making sense.\n"}