Manifest reference
manifest.json is the game's declaration to the portal: who it is, which version this is, what it sends and what the portal may believe about it. It sits next to index.html in the build output. The schema is gameManifestSchema in @gameportal/sdk-types; the platform validators read the same file out of your build (rules R-MAN-*, R-BLD-06, R-SDK-*), and a manifest that does not parse means the game does not ship (R-MAN-01).
JSON has no comments, so this page explains every field. The table of all fields is generated from the schema; the sections after it say what each field is for and which rule watches it.
{
"id": "my-game",
"version": "1.0.0",
"entry": "index.html",
"sdkVersion": 1,
"name": "Oyunum",
"genre": "arcade",
"session": "quick",
"languages": ["tr", "en"],
"saveVersion": 1,
"challenge": {
"metric": "round_score",
"targets": { "easy": 150, "medium": 300, "hard": 500 }
},
"events": {
"round_end": { "props": { "score": "number", "hits": "number" } },
"new_best": { "props": { "score": "number" } }
},
"scores": {
"round_score": { "maxPerSecond": 60, "integer": true, "max": 1200 }
}
}All fields #
| Field | Type | Required | Rules | Meaning |
|---|---|---|---|---|
id | string | yes | matches ^[a-z0-9][a-z0-9-]{0,63}$ | The game's permanent name: folder, URL segment /<id>/<version>/, portal page /games/<id> and prefix of every browser storage key. Never change it after release. |
version | string | yes | 1 to 32 characters; matches ^[0-9A-Za-z.+-]+$ | This build's release (use semver, 1.0.0). Every change that ships is a new version; a released folder never changes. |
entry | string | yes | at least 1 character | The HTML file that starts the game, at the root of the build. index.html for a Vite game. |
sdkVersion | 1 | yes | The SDK major version the game speaks. Always 1 today. | |
name | string | yes | at least 1 character | The catalogue name in Turkish, the default language. English text lives in the portal's copy file. |
genre | string | yes | at least 1 character | The shelf the game goes on (arcade, puzzle, tycoon, lane, ...). Use an existing genre unless you are adding a shelf. |
session | "quick" or "deep" | yes | quick: a session or round under about 5 minutes (and a first load under 300 KB gzip). deep: longer. | |
languages | array of "tr" or "en" | yes | at least 1 item | The languages the game really supports. Games with the bridge support both, switching on init.lang and lang messages. |
saveVersion | integer | yes | at least 0 | The format of the save the game writes. Raise it when the format changes and add an upgrade step; never lower it. |
challenge | object | no | Optional. Takes the game into the portal's daily challenge. | |
challenge.metric | string | yes | matches ^[a-z][a-z0-9_]{0,63}$ | What is counted: the name of a scores mode (the best round today) or of an event with a number property value (the largest value today). |
challenge.targets | object | yes | Three numbers, one per stage of player. | |
challenge.targets.easy | number | yes | The target for new players. Must be above 0 and below medium. | |
challenge.targets.medium | number | yes | The target for the middle stage. Must be above easy and below hard. | |
challenge.targets.hard | number | yes | The target for experienced players. Must be above medium. | |
challenge.maxPerSecond | number | no | greater than 0 | For an event metric: the most the metric can grow per second of active play. Without it the portal assumes the hard target in 10 minutes. |
events | object | yes | keys matches ^[a-z][a-z0-9_]{0,63}$ | Every event the game sends with bridge.event(). Anything not listed is answered with invalid. |
events.<event> | object | per entry | One declared event; the key is its name (snake_case). | |
events.<event>.props | object | no | keys matches ^[a-z][a-z0-9_]{0,63}$ | The properties the event may carry, with their types. Leave out for an event without properties. |
events.<event>.props.<prop> | "string" or "number" or "boolean" | per entry | The type of one property: string, number or boolean. A property is accepted only if declared here with a matching type. | |
events.<event>.perDay | integer | no | greater than 0 | Optional (T-45). The most events of this name the portal records per player and day (the player's day on the server clock); later ones that day are ignored. Required for an event the portal counts (R-MAN-09). |
events.<event>.max | object | no | keys matches ^[a-z][a-z0-9_]{0,63}$ | Optional (T-45). Upper bounds of number properties; an event with a property above its bound is ignored. |
events.<event>.max.<name> | number | per entry | The largest value of one number property declared in props. | |
scores | object | yes | keys matches ^[a-z][a-z0-9_]{0,63}$ | Every score mode the game reports with bridge.score(). Anything not listed is answered with invalid. |
scores.<mode> | object | per entry | The plausibility rules of one score mode; the key is its name (snake_case). | |
scores.<mode>.monotonic | boolean | no | The value may never decrease. Only for counters that never go down (a lifetime total); a per-round score is not monotonic. | |
scores.<mode>.maxPerSecond | number | no | greater than 0 | The most the value can grow per second of wall time since the previous accepted score. Derive it from the rules of the game, then prove it with a bot. |
scores.<mode>.max | number | no | The largest value that is possible at all. | |
scores.<mode>.integer | boolean | no | The value is a whole number. | |
multiplayer | object | no | Optional (T-29 addendum). Declares online two-player matches; the server referees every move. | |
multiplayer.seats | 2 | yes | Seats per match. Only 2 exists for now. | |
multiplayer.turnSeconds | integer | yes | at least 10; at most 120 | Seconds a player has for a move, on the server's clock. A missed turn is played by the server. |
events and scores are maps: the key is a name you choose (snake_case), the value says what that event or score mode may look like. In the table <event>, <prop> and <mode> stand for those keys.
Identity #
| Field | Notes |
|---|---|
id | Lower-case letters, digits and -, at most 64 characters. It is the folder, the URL segment, the portal page and the prefix of every browser storage key (<id>:save, <id>:lang). Never change it after release: saves are filed under it. Checked by R-MAN-01 (build folder and manifest agree) and R-STO-03 (storage keys start with it). |
version | Semver (1.0.0) or a pre-release (1.0.0-beta.1). It goes into hello.gameVersion and must be the same everywhere: manifest, release folder, package.json (R-BLD-06). Every change that ships is a new version; a released folder never changes. |
entry | The HTML file that starts the game, at the root of the build. index.html for a Vite game (R-BLD-01). |
sdkVersion | 1. The SDK major version the game speaks (hello.sdkVersions). A portal that no longer supports it answers unsupported_version and shows its own error screen. |
Catalogue #
| Field | Notes |
|---|---|
name | The catalogue name in Turkish, the default language (R-MAN-02). |
genre | The shelf the game goes on. The home page builds one row per genre that has games. Use an existing one unless you are adding a shelf. |
session | quick: a round or session under about 5 minutes. deep: longer. The home page sections "quick games" and "deep games" come from it, and a quick game should load in under 300 KB gzipped (R-BLD-03). |
languages | Both tr and en for every game with the bridge: the language comes from the portal and changes without a reload (R-I18N-01). |
Save #
saveVersion is the format of the save the game writes. It goes into ready and every save. When the format changes, raise it here and in createBridge(), and add an upgrade step (see save formats). Never lower it: the portal refuses to let an older build overwrite a save with a higher version (conflict), and the bridge then asks the player to reload (R-STO-05, R-STO-07).
Events #
events lists every event the game sends with bridge.event(name, props), with the type of each property. The portal answers an event it does not know, or one with an undeclared or wrongly typed property, with invalid. Events feed badges and daily challenges, so a refused event is a reward the player never gets.
"events": { "round_end": { "props": { "score": "number", "hits": "number" } } }The event name and every property name match
^[a-z][a-z0-9_]{0,63}$.A property type is
"string","number"or"boolean"; a property is accepted only if it is declared here.propsmay be left out for an event without properties.At most 5 events per second may be sent.
List only what the game really sends: a declared event that never arrives is reported by
R-MAN-06.
Rules: R-MAN-03 (every sent event is declared and typed; a session never receives invalid) and R-MAN-06.
Daily limits #
An event may carry two optional limits. The portal stores an event only within them; one outside is ignored like a repeated event id (the game still gets ok: true, the response's accepted does not count it), and the rest of the batch is stored as usual.
"events": { "run_end": { "props": { "score": "number" }, "perDay": 29000, "max": { "score": 5000 } } }| Field | The portal ignores an event when ... |
|---|---|
perDay | the player already has this many events of the name today: the player's day (their time zone) on the server clock, never the device's. A positive integer. |
max.<prop> | the number property is above it. Only for properties declared as "number" in props. |
The portal counts some events into a player's progress: a badge that counts an event (Simit Sarayı's first franchise), and the stage rule that picks the daily challenge's tier (one event per run, match or board). Every such event needs perDay, and a number property the portal adds up needs max (R-MAN-09). Which events these are is the portal's data, not the manifest's, so the reviewer asks for them when a badge or stage rule is set up. Make the numbers generous: at least 3 times what a dedicated player reaches in a day. The portal's own games use 86 400 seconds divided by the fastest the event can happen (the shortest run plus a 2 s restart, the shortest board, one prestige a minute). Do not cap the challenge metric's value below the hard target.
Rules: R-MAN-09.
Scores and plausibility #
scores lists the score modes the game reports with bridge.score(mode, value). Everything the game reports runs on the player's device, so any number may be forged. The portal checks each score against the rules of its mode, in this order:
| Rule | The portal rejects a score when ... |
|---|---|
integer | it is true and the value has a fraction. |
max | the value is above it. The largest score that is possible at all. |
monotonic | it is true and the value is below the previous accepted score. Only for counters that never go down (a lifetime total). A per-round score is not monotonic. |
maxPerSecond | the increase over the previous accepted score is above maxPerSecond times the seconds since that score (at least 1 s). Before the first accepted score the baseline is 0 at the start of the player's first play of the game. |
A rejected score is accepted at the message level (the game gets ok: true and is not told) but stored as suspicious, and it never reaches a leaderboard. Too tight a number rejects honest players; one far too loose lets forged scores through.
Derive the numbers, then prove them. Compute maxPerSecond from the rules of your game (the most points the fastest possible player can earn per second), take a margin of up to 2x, and write a test with a bot that plays as fast as the rules allow and never beats it. The starter's test/plausibility.test.ts is the example: its bound is about 55 points per second, the manifest says 60, and the best bot never beats it over any window.
The mode name matches
^[a-z][a-z0-9_]{0,63}$.At most one score per second may be sent.
A score is whatever number your game ranks by; send it at the end of a round, not on every frame.
Rules: R-MAN-04 (every mode has maxPerSecond, and integer and max where they hold) and R-MAN-06.
Leaderboards #
A score mode gets a public leaderboard only when its manifest bounds the value on both axes, because a board of numbers the player's own device reports would otherwise rank the fakers. Everything else still works (the scores are accepted, stored and drive challenges); it just has no board.
| Number | Value | Where it matters |
|---|---|---|
Leaderboard eligibility: maxPerSecond | at most 1000 score units per second | A score mode gets a public leaderboard only when its manifest bounds the value on both axes: a finite max and a maxPerSecond no higher than this. |
| Top-10 badge | at least 20 distinct players | The leaderboard_top10 badge needs a real board: the all-time board of the mode must have this many players with an accepted score above zero. |
| Challenge default rate | hard target in 10 minutes | An event metric without challenge.maxPerSecond may grow by the hard target divided by 600 per second of active play. |
Daily challenge #
"challenge": { "metric": "round_score", "targets": { "easy": 150, "medium": 300, "hard": 500 } }A game with a challenge takes part in the portal's daily challenge: each day the player gets one of three targets, picked by their stage, and the portal shows the progress.
| Field | Meaning |
|---|---|
metric | Either the name of a scores mode ("the best round today") or the name of an event with a number property value ("the largest value today"). Anything else never advances. |
targets | easy, medium, hard, with 0 < easy < medium < hard. Tune them with real play data; first numbers are guesses. |
maxPerSecond | For an event metric: the most the metric can grow per second of active play, measured by the portal's heartbeats. Without it the portal assumes the hard target in 10 minutes. A score metric is bounded by the mode's own maxPerSecond. |
Rules: R-MAN-05, R-MAN-07, R-TIM-02 (daily counters restart only on the portal's day message).
Not in the manifest #
The English name, the tagline, the description and the tags: the portal keeps them in its copy file; you hand them over with the submission (
R-I18N-02).Cover pictures.
Which version is live: that is a value in the portal's database. A rollback changes the value, not the files.
Rights and credits:
public/CREDITS.txt, shipped in the build (R-CNT-02).