Platform rules
Every game on the portal follows the same rules, the portal's own games included. They exist so that a game cannot lose a player's progress, cannot reach the portal, the player or another game, and cannot make the portal look broken. The rules are one machine-readable list (packages/sdk-types/src/platform-rules.ts): the AI integrator reads it as instructions, the validators apply it, and the review panel reports results by rule id. This page is generated from that list. The reasons behind each rule and the way it is checked are written up in docs/PLATFORM-RULES.md in the repository (in Turkish).
Rules version 4: 72 rules, 50 blockers and 22 warnings.
| Level | New blockers | New warnings | What a game at this level must satisfy |
|---|---|---|---|
| Level 0 (wrap) | 28 | 12 | 28 blockers, 12 warnings |
| Level 1 (bridge) | 15 | 7 | 43 blockers, 19 warnings |
| Level 2 (deep) | 7 | 3 | 50 blockers, 22 warnings |
How to read a rule #
Each rule has an id (R-SDK-03: category, number; ids are never reused), a severity, one or more ways it is checked, and the lowest integration level it applies to.
| Severity | Meaning | When it fails |
|---|---|---|
blocker | The game cannot ship while it breaks the rule. | The report goes back to the author (or to the AI integrator, which fixes and retries; after three failed tries it lowers the level, after that a human takes over). A reviewer cannot waive a blocker for one game: that needs a rule change. |
warning | Quality, or a risk for later. | Reported with its evidence. The reviewer may accept it for one game version with a written reason; the acceptance does not carry over to the next version. |
| Checked by | What it is |
|---|---|
manifest | The manifest schema and cross-checks between manifest.json and the build. |
static | A scan or measurement of the build output without running it: files, sizes, patterns in the source. A scan is pattern matching, not a parser, and can be wrong in minified code; that is why most static findings are confirmed at runtime or sent to a human as "please look". |
runtime | The fake-portal runner: the game really opens in a browser, portal and game on different origins, and a scripted session is recorded (messages, network, storage, errors, screenshots). |
human | A reviewer decides. An AI pre-screen may help; it never decides. |
When a rule lists several checks, the first one is the primary check.
Integration levels. Level 0 wraps a game as it is, with no code change: only the rules marked level 0 apply (security, network, build, content). Level 1 adds the bridge (handshake, saves, language, pause). Level 2 adds scores, the daily challenge metric and game-specific events. A rule applies from its level upward. A rule applies only when the game uses the feature: a daily-seed rule passes trivially for a game with no daily content.
Blocker checklists by level #
Print these. A game at a level must satisfy that level's list and every list above it. pnpm check tells you which of these fail and why.
Level 0: wrapped as it is #
R-SDK-03 postMessage never targets "*". Every window.postMessage call names an exact target origin from the portal origin list; never "*".
R-SDK-07 Outside the portal: standalone after 2 s, without errors. Opened outside any iframe, start in standalone mode after 2 s with the game's own save and language switch, without errors.
R-STO-02 The game sets no cookies. Never read or write document.cookie or cookieStore; the games origin carries no cookies at all.
R-PRV-01 The game neither asks for nor collects identity. Never ask for or store the player's name, e-mail, phone, account or password, and never try to learn the portal user; the game knows only player.guest.
R-NET-01 The game requests files only from its own version folder. Every same-origin request stays inside /<gameId>/<version>/; never request other games' folders, the games-host root or the portal (its API is not for games).
R-NET-02 No third-party requests. Make no request to any other host (fonts, CDNs, APIs, WebSocket, WebRTC, beacons) unless the host is on the platform allow list.
R-NET-03 No remote code and no eval. Run only code shipped in the build: no eval, new Function, string timers, remote scripts or imports, and WebAssembly only from the game's own folder.
R-NET-04 No tracking or analytics scripts. Ship no analytics, tracking, crash-reporting or fingerprinting code; the portal measures play itself.
R-SEC-01 No access to the parent window or the portal. Never touch window.parent/top beyond postMessage: no reading their document, location or storage, no top navigation, no window.open.
R-SEC-02 Works under the iframe sandbox and allow list. Run fully under sandbox="allow-scripts allow-same-origin allow-pointer-lock" and allow="autoplay; fullscreen; gamepad": no alert/confirm/prompt, popups, form submits, downloads, camera, microphone, geolocation or clipboard.
R-SEC-04 No service worker. Register no service worker and do not rely on one; the games origin is shared by every game.
R-SEC-05 No secrets in the build output. Ship no keys, tokens, .env files, private keys or internal URLs in the build output.
R-BLD-01 Static files only; the entry is in the manifest. Ship a folder of static files whose entry (manifest entry, normally index.html) is at its root; no server code, rewrites or build step at serve time.
R-BLD-02 First load at most 1.5 MB compressed. Keep everything loaded until ready at most 1.5 MB gzip; load later levels and music lazily.
R-BLD-04 Build output at most 50 MB. Keep the whole build output at most 50 MB uncompressed.
R-BLD-05 Asset paths are relative. Reference every file with a relative path (./), never root-absolute (/assets/...) or with a hard-coded host, so the game runs from any /<gameId>/<version>/ path.
R-BLD-06 One immutable version. Use one version everywhere (manifest version, release folder, hello.gameVersion) and ship any change as a new version; a released folder never changes.
R-BLD-08 No cross-origin isolation needed. Do not need COOP/COEP or SharedArrayBuffer (e.g. threaded engine builds); games-host does not send those headers.
R-MAN-01 The manifest is valid and ships with the build. Ship manifest.json next to the entry, valid against gameManifestSchema, with id and version matching the release folder.
R-MAN-02 Catalogue fields are complete and true. Fill name, genre, session (quick for runs under about 5 minutes, otherwise deep) and languages truthfully.
R-UX-04 Playable by touch in portrait on a phone. Be fully playable by touch on a 360x640 portrait viewport, without hover-only controls or page scroll and zoom stealing input.
R-I18N-02 Catalogue name and description in Turkish and English. Provide the catalogue name, one-line description and tags in Turkish and English (A-7 drafts, a human reads).
R-A11Y-02 No more than three flashes per second. Never flash the screen or a large area more than three times in any one second.
R-CNT-01 No in-game ads. Show no ads and ship no ad SDK; ads belong to the portal (a portal-approved SDK may be allowed later, PRD K-07).
R-CNT-02 Rights to code and assets are documented. Document the licence and source of the code and of every third-party asset (fonts, images, sounds, libraries); no other brand's names, art or text.
R-CNT-03 Follows the content rules and age rating. Stay within the portal content rules: no real-money gambling, sexual content, hate, graphic violence or other content above the portal's age rating.
R-CNT-04 No payments, purchases or outbound links. Offer no purchases, payments, donations or links that leave the game (store, social, "more games").
R-QLT-01 No uncaught errors in the first 60 s. Produce no uncaught error, unhandled rejection or failed request during the first 60 s, in the portal and standalone.
Level 1: add these #
R-SDK-02 hello, init, ready completes within 3 s. Inside the portal iframe send hello every 250 ms until init arrives, then send ready over the port once the game is playable, within 3 s of the iframe starting to load.
R-SDK-04 After init every message goes over the port. After init, send and receive only over the MessageChannel port from init; stop listening to window messages and never post to the parent window again (a reload starts with hello).
R-SDK-05 init is accepted only from the portal. Accept init only when event.source is window.parent and event.origin is in the portal origin list games-host injects (meta gameportal:portal-origins); ignore everything else.
R-SDK-06 Never standalone inside an iframe. Inside an iframe keep waiting for init however late it is; never switch to standalone mode or local saves there.
R-SDK-08 A reload inside the iframe gets a fresh init. After a reload inside the iframe, handshake again from hello and rebuild the game state from the new init (nothing carried over from local storage).
R-SDK-10 Message rate limits are respected. Send score at most once per second, event at most 5 per second and save at most once per 30 s (plus one flush on pause or hidden tab), so a normal session never gets rate_limited.
R-STO-01 In portal mode saves go through the SDK only. In portal mode keep progress only through the bridge's storage interface (save message); write nothing to localStorage, sessionStorage, IndexedDB or the Cache API.
R-STO-04 The save is JSON under 256 KB. Keep the save JSON-serializable and its UTF-8 JSON length under 256 KB, also after long play; derive what can be derived instead of storing it.
R-STO-05 The save format is versioned and old saves are upgraded. Send the manifest's saveVersion with ready and every save; when the format changes, raise saveVersion and upgrade every older format (upgradeSave), never lowering it.
R-STO-06 A broken or unexpected save never crashes the game. Parse the save defensively: a missing, broken or unexpected save still leads to ready and a playable game, without uncaught errors.
R-STO-07 A save in a newer format is never overwritten. If the server save has a higher saveVersion than this build, send no save at all and ask the player to reload.
R-STO-08 A save conflict loses no data. On conflict either ask the player which save to keep or merge both without loss; never drop either side silently.
R-TIM-01 No device clock for rewards in portal mode. In portal mode compute offline earnings, buffs, cooldowns and daily counters from init/resume serverTime plus a monotonic clock, never Date.now() or new Date().
R-UX-01 pause stops play, time and sound. On pause or a hidden tab freeze game time, animation and audio; on resume continue (mid-run: wait for a tap), with time from resume.serverTime.
R-I18N-01 In-game text in Turkish and English; language from the portal. Provide every player-visible string in Turkish (default) and English, list both in manifest languages, and switch on init.lang and the lang message without a reload.
Level 2: add these #
R-MAN-03 Every sent event is declared with typed props. Declare every event name and prop type the game sends in manifest events; a session must produce no invalid acks.
R-MAN-04 Every score mode has plausibility bounds. Give every scores mode a maxPerSecond derived from the game's real top scoring rate, plus integer and max where they hold, and monotonic only for ever-growing totals.
R-MAN-05 The challenge definition is consistent. If challenge is set, its metric is a scores mode (best run) or an event with a numeric value prop (the day's largest value), with 0 < easy < medium < hard, and the scripted session makes progress on it.
R-MAN-08 A leaderboard score mode has a realistic ceiling. When the challenge metric is a leaderboard score mode (finite max, maxPerSecond at most 1000), keep its max at most 20 times challenge.targets.hard: about twice the best measured run, or the game's own end.
R-MAN-09 Counted events have daily limits. Give every event the portal counts into progress (a game_event badge, the challenge stage's event_count) a perDay cap, and every number prop it sums a max, at least 3 times what a dedicated player reaches in a day; never set the challenge metric event's max.value below the hard target.
R-TIM-02 The day boundary comes from the portal. Take the day from init.time.day and reset daily counters only on the day message, never at local midnight.
R-TIM-03 Daily seeds come from the server day. Seed daily content with dailySeed(gameId, init.time.day) so every player gets the same board on the same day, whatever their device clock.
Numbers and policy #
| Limit | Value | Rule |
|---|---|---|
First load until ready, compressed | 1536 KB | R-BLD-02 (blocker) |
First load of a quick game, compressed | 300 KB | R-BLD-03 (warning) |
| Whole build, uncompressed | 50 MB | R-BLD-04 (blocker) |
ready after the iframe starts loading | 3 s | R-SDK-02 (blocker) |
| Error-free window | 60 s | R-QLT-01 (blocker) |
| Third-party hosts a game may contact | none | R-NET-02 (blocker) |
The runner holds every game to this Content-Security-Policy, sent as Content-Security-Policy-Report-Only: every violation report is a finding (R-SEC-03). It is meant to become the enforced policy before external developers arrive, so build for it now: no inline scripts or event-handler attributes, nothing from another host.
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
media-src 'self' data: blob:;
connect-src 'self';
worker-src 'self';
frame-src 'none';
object-src 'none';
base-uri 'none';
form-action 'none'The portal's iframe is sandbox="allow-scripts allow-same-origin allow-pointer-lock" with allow="autoplay; fullscreen; gamepad". allow-same-origin lets the game use its own storage; the game is on a different origin from the portal (in production a subdomain of the portal's domain), so it gives no access to the portal's pages or storage, and the portal's cookies are __Host- cookies a game page can neither read nor set. games-host adds Content-Security-Policy: frame-ancestors <portal origin> (only the portal can embed a game), X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer to every response and never sets a cookie.
All rules #
SDK and handshake #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-SDK-01 Only the bridge module talks to the portal | Use @gameportal/game-bridge (or game-kit's host on top of it) for every portal message; do not hand-write the protocol. | warning | static, human | 1 |
| R-SDK-02 hello, init, ready completes within 3 s | Inside the portal iframe send hello every 250 ms until init arrives, then send ready over the port once the game is playable, within 3 s of the iframe starting to load. | blocker | runtime | 1 |
| R-SDK-03 postMessage never targets "*" | Every window.postMessage call names an exact target origin from the portal origin list; never "*". | blocker | static, runtime | 0 |
| R-SDK-04 After init every message goes over the port | After init, send and receive only over the MessageChannel port from init; stop listening to window messages and never post to the parent window again (a reload starts with hello). | blocker | runtime | 1 |
| R-SDK-05 init is accepted only from the portal | Accept init only when event.source is window.parent and event.origin is in the portal origin list games-host injects (meta gameportal:portal-origins); ignore everything else. | blocker | runtime | 1 |
| R-SDK-06 Never standalone inside an iframe | Inside an iframe keep waiting for init however late it is; never switch to standalone mode or local saves there. | blocker | runtime | 1 |
| R-SDK-07 Outside the portal: standalone after 2 s, without errors | Opened outside any iframe, start in standalone mode after 2 s with the game's own save and language switch, without errors. | blocker | runtime | 0 |
| R-SDK-08 A reload inside the iframe gets a fresh init | After a reload inside the iframe, handshake again from hello and rebuild the game state from the new init (nothing carried over from local storage). | blocker | runtime | 1 |
| R-SDK-09 An unsupported SDK version does not crash the game | When the portal answers unsupported_version, stop talking to it and stay on a calm screen without uncaught errors or local saves. | warning | runtime | 1 |
| R-SDK-10 Message rate limits are respected | Send score at most once per second, event at most 5 per second and save at most once per 30 s (plus one flush on pause or hidden tab), so a normal session never gets rate_limited. | blocker | runtime | 1 |
| R-SDK-11 ack errors are handled as specified | Handle every ack error code as TECH-DESIGN specifies: retry later on rate_limited and network, shrink on too_large, ask or merge on conflict, only log invalid; never loop or show raw errors. | warning | runtime, human | 1 |
| R-SDK-12 ready is sent only when the game is playable | Send ready once per init, only when the first screen is drawn and accepts input. | warning | runtime, human | 1 |
| R-SDK-13 session reports real activity changes only | Send session only when the player's activity changes, with active: true only while the player is really playing; never as a heartbeat. | warning | runtime, human | 1 |
Storage and saves #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-STO-01 In portal mode saves go through the SDK only | In portal mode keep progress only through the bridge's storage interface (save message); write nothing to localStorage, sessionStorage, IndexedDB or the Cache API. | blocker | runtime, static | 1 |
| R-STO-02 The game sets no cookies | Never read or write document.cookie or cookieStore; the games origin carries no cookies at all. | blocker | static, runtime | 0 |
| R-STO-03 Browser storage keys start with the game id | Prefix every standalone localStorage key with "<gameId>:" because all games share one origin; legacy keys need a reviewer's note. | warning | runtime, static | 0 |
| R-STO-04 The save is JSON under 256 KB | Keep the save JSON-serializable and its UTF-8 JSON length under 256 KB, also after long play; derive what can be derived instead of storing it. | blocker | runtime | 1 |
| R-STO-05 The save format is versioned and old saves are upgraded | Send the manifest's saveVersion with ready and every save; when the format changes, raise saveVersion and upgrade every older format (upgradeSave), never lowering it. | blocker | manifest, runtime | 1 |
| R-STO-06 A broken or unexpected save never crashes the game | Parse the save defensively: a missing, broken or unexpected save still leads to ready and a playable game, without uncaught errors. | blocker | runtime | 1 |
| R-STO-07 A save in a newer format is never overwritten | If the server save has a higher saveVersion than this build, send no save at all and ask the player to reload. | blocker | runtime | 1 |
| R-STO-08 A save conflict loses no data | On conflict either ask the player which save to keep or merge both without loss; never drop either side silently. | blocker | runtime, human | 1 |
| R-STO-09 Pause and a hidden tab flush the save | When pause arrives or the tab is hidden, send the pending save immediately (the one exempt save). | warning | runtime | 1 |
Privacy #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-PRV-01 The game neither asks for nor collects identity | Never ask for or store the player's name, e-mail, phone, account or password, and never try to learn the portal user; the game knows only player.guest. | blocker | static, human | 0 |
Network and code loading #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-NET-01 The game requests files only from its own version folder | Every same-origin request stays inside /<gameId>/<version>/; never request other games' folders, the games-host root or the portal (its API is not for games). | blocker | runtime, static | 0 |
| R-NET-02 No third-party requests | Make no request to any other host (fonts, CDNs, APIs, WebSocket, WebRTC, beacons) unless the host is on the platform allow list. | blocker | runtime, static | 0 |
| R-NET-03 No remote code and no eval | Run only code shipped in the build: no eval, new Function, string timers, remote scripts or imports, and WebAssembly only from the game's own folder. | blocker | static, runtime | 0 |
| R-NET-04 No tracking or analytics scripts | Ship no analytics, tracking, crash-reporting or fingerprinting code; the portal measures play itself. | blocker | static, runtime | 0 |
Isolation and security #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-SEC-01 No access to the parent window or the portal | Never touch window.parent/top beyond postMessage: no reading their document, location or storage, no top navigation, no window.open. | blocker | static, runtime | 0 |
| R-SEC-02 Works under the iframe sandbox and allow list | Run fully under sandbox="allow-scripts allow-same-origin allow-pointer-lock" and allow="autoplay; fullscreen; gamepad": no alert/confirm/prompt, popups, form submits, downloads, camera, microphone, geolocation or clipboard. | blocker | runtime, static | 0 |
| R-SEC-03 Works under the games-host headers and the target CSP | Run without violations under the games-host headers (frame-ancestors, nosniff, no-referrer) and the target game CSP: no inline scripts or event-handler attributes. | warning | runtime | 0 |
| R-SEC-04 No service worker | Register no service worker and do not rely on one; the games origin is shared by every game. | blocker | static, runtime | 0 |
| R-SEC-05 No secrets in the build output | Ship no keys, tokens, .env files, private keys or internal URLs in the build output. | blocker | static | 0 |
| R-SEC-06 No cryptocurrency mining | Run no cryptocurrency miner, mining pool connection or proof-of-work: the player's device works for the game only. | warning | static, human | 0 |
| R-SEC-07 Code is reviewable, not obfuscated | Ship ordinary (optionally minified) code: no obfuscators, packed eval loaders, encoded code or code hidden in data; give the reviewer the source or a source map on request. | warning | static, human | 0 |
Build output #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-BLD-01 Static files only; the entry is in the manifest | Ship a folder of static files whose entry (manifest entry, normally index.html) is at its root; no server code, rewrites or build step at serve time. | blocker | static, manifest | 0 |
| R-BLD-02 First load at most 1.5 MB compressed | Keep everything loaded until ready at most 1.5 MB gzip; load later levels and music lazily. | blocker | static, runtime | 0 |
| R-BLD-03 Quick games load under 300 KB | For a session: "quick" game keep the first load under 300 KB gzip. | warning | static, runtime | 0 |
| R-BLD-04 Build output at most 50 MB | Keep the whole build output at most 50 MB uncompressed. | blocker | static | 0 |
| R-BLD-05 Asset paths are relative | Reference every file with a relative path (./), never root-absolute (/assets/...) or with a hard-coded host, so the game runs from any /<gameId>/<version>/ path. | blocker | static, runtime | 0 |
| R-BLD-06 One immutable version | Use one version everywhere (manifest version, release folder, hello.gameVersion) and ship any change as a new version; a released folder never changes. | blocker | manifest, static, runtime | 0 |
| R-BLD-07 File types from the games-host list | Use only file extensions games-host serves with a proper content type (nosniff blocks scripts and styles served as octet-stream). | warning | static | 0 |
| R-BLD-08 No cross-origin isolation needed | Do not need COOP/COEP or SharedArrayBuffer (e.g. threaded engine builds); games-host does not send those headers. | blocker | static, runtime | 0 |
Manifest #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-MAN-01 The manifest is valid and ships with the build | Ship manifest.json next to the entry, valid against gameManifestSchema, with id and version matching the release folder. | blocker | manifest, static | 0 |
| R-MAN-02 Catalogue fields are complete and true | Fill name, genre, session (quick for runs under about 5 minutes, otherwise deep) and languages truthfully. | blocker | manifest, human | 0 |
| R-MAN-03 Every sent event is declared with typed props | Declare every event name and prop type the game sends in manifest events; a session must produce no invalid acks. | blocker | manifest, runtime | 2 |
| R-MAN-04 Every score mode has plausibility bounds | Give every scores mode a maxPerSecond derived from the game's real top scoring rate, plus integer and max where they hold, and monotonic only for ever-growing totals. | blocker | manifest, runtime, human | 2 |
| R-MAN-05 The challenge definition is consistent | If challenge is set, its metric is a scores mode (best run) or an event with a numeric value prop (the day's largest value), with 0 < easy < medium < hard, and the scripted session makes progress on it. | blocker | manifest, runtime | 2 |
| R-MAN-06 Declared events and scores are really sent | Declare only events and score modes the game really sends; the scripted session should produce each of them. | warning | runtime | 2 |
| R-MAN-07 Event metrics state their rate bound | When the challenge metric is an event, set challenge.maxPerSecond from the game's real top earning rate instead of relying on the default (hard target in 10 minutes). | warning | manifest, human | 2 |
| R-MAN-08 A leaderboard score mode has a realistic ceiling | When the challenge metric is a leaderboard score mode (finite max, maxPerSecond at most 1000), keep its max at most 20 times challenge.targets.hard: about twice the best measured run, or the game's own end. | blocker | manifest, human | 2 |
| R-MAN-09 Counted events have daily limits | Give every event the portal counts into progress (a game_event badge, the challenge stage's event_count) a perDay cap, and every number prop it sums a max, at least 3 times what a dedicated player reaches in a day; never set the challenge metric event's max.value below the hard target. | blocker | manifest, human | 2 |
Time and determinism #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-TIM-01 No device clock for rewards in portal mode | In portal mode compute offline earnings, buffs, cooldowns and daily counters from init/resume serverTime plus a monotonic clock, never Date.now() or new Date(). | blocker | runtime, static | 1 |
| R-TIM-02 The day boundary comes from the portal | Take the day from init.time.day and reset daily counters only on the day message, never at local midnight. | blocker | runtime | 2 |
| R-TIM-03 Daily seeds come from the server day | Seed daily content with dailySeed(gameId, init.time.day) so every player gets the same board on the same day, whatever their device clock. | blocker | runtime, human | 2 |
| R-TIM-04 Scores do not depend on frame rate | Step game logic with a fixed time step and a seeded RNG so a scored run plays the same on 30, 60 and 120 Hz displays. | warning | human, runtime | 2 |
Player experience #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-UX-01 pause stops play, time and sound | On pause or a hidden tab freeze game time, animation and audio; on resume continue (mid-run: wait for a tap), with time from resume.serverTime. | blocker | runtime | 1 |
| R-UX-02 Sound toggle; no sound before the first tap | Offer a visible sound toggle that is remembered (in the save in portal mode) and start audio only after a user gesture. | warning | runtime, human | 0 |
| R-UX-03 Reduced motion is honoured | With prefers-reduced-motion: reduce, turn off screen shake, parallax and large slides, and cut particles. | warning | runtime, human | 0 |
| R-UX-04 Playable by touch in portrait on a phone | Be fully playable by touch on a 360x640 portrait viewport, without hover-only controls or page scroll and zoom stealing input. | blocker | runtime, human | 0 |
| R-UX-05 Adapts to the frame size and safe areas | Fill the frame at any size and on resize (portal fullscreen overlay), and keep controls inside env(safe-area-inset-*). | warning | runtime | 0 |
| R-UX-06 Own language and pause buttons hidden in the portal | In portal mode hide the game's own language and pause controls; the portal owns them. | warning | runtime | 1 |
| R-UX-07 The first 30 seconds | Give a concrete win in the first seconds, at most a one-sentence tutorial, and a one-tap restart after a loss. | warning | human | 0 |
Language #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-I18N-01 In-game text in Turkish and English; language from the portal | Provide every player-visible string in Turkish (default) and English, list both in manifest languages, and switch on init.lang and the lang message without a reload. | blocker | manifest, runtime, human | 1 |
| R-I18N-02 Catalogue name and description in Turkish and English | Provide the catalogue name, one-line description and tags in Turkish and English (A-7 drafts, a human reads). | blocker | human | 0 |
Accessibility #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-A11Y-01 Accessibility basics | Offer keyboard play where it fits (e.g. Space), label the stage and buttons for screen readers, keep touch targets at least 44 px and text readable. | warning | human, runtime | 0 |
| R-A11Y-02 No more than three flashes per second | Never flash the screen or a large area more than three times in any one second. | blocker | human | 0 |
Content and rights #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-CNT-01 No in-game ads | Show no ads and ship no ad SDK; ads belong to the portal (a portal-approved SDK may be allowed later, PRD K-07). | blocker | static, runtime | 0 |
| R-CNT-02 Rights to code and assets are documented | Document the licence and source of the code and of every third-party asset (fonts, images, sounds, libraries); no other brand's names, art or text. | blocker | human, static | 0 |
| R-CNT-03 Follows the content rules and age rating | Stay within the portal content rules: no real-money gambling, sexual content, hate, graphic violence or other content above the portal's age rating. | blocker | human | 0 |
| R-CNT-04 No payments, purchases or outbound links | Offer no purchases, payments, donations or links that leave the game (store, social, "more games"). | blocker | static, human | 0 |
Stability and performance #
| Rule | What the game must do | Severity | Checked by | From level |
|---|---|---|---|---|
| R-QLT-01 No uncaught errors in the first 60 s | Produce no uncaught error, unhandled rejection or failed request during the first 60 s, in the portal and standalone. | blocker | runtime | 0 |
| R-QLT-02 Smooth on a mid-range phone | Keep play smooth on a mid-range phone: the runner's throttled session should hold a near-60 FPS median. | warning | runtime | 0 |
How the runner checks a game #
pnpm check plays your game in a real browser against a fake portal that enforces the portal's limits and answers with the portal's own error codes. Each scenario runs in a clean browser context:
| Scenario | What happens |
|---|---|
startup | The iframe loads; hello, init and ready are timed. The 75th percentile of several clean starts counts. |
main | A 60 second session: idle, random play, a plain window message pretending to be pause (it must change nothing), a real pause and resume, lang, day. |
standalone | The page opened on its own: standalone after 2 seconds, saves survive a reload. |
late-init | init arrives 8 seconds late; the game must keep waiting and then start. |
reload | The frame reloads: a new hello, a new init, nothing from the old port. |
unsupported | The portal answers unsupported_version. |
saves, newer-save | Broken saves and a save in a newer format. |
acks | Every ack error code, one by one. |
hostile | The game is framed from an origin it does not trust: a forged init must be ignored, nothing is posted to "*". |
mobile | 360x640 portrait with touch, resizing, button sizes. |
reduced-motion | prefers-reduced-motion: reduce. |
throttled | The CPU slowed four times. |
clock-shift | The device clock a day ahead: nothing in the game state may change because of it. |
hooked | If the game exposes the test hook, a bot plays and time is fast-forwarded to grow the save. |
Some rules cannot be decided by a machine and show up in the report as manual: a reviewer looks at them. Read each manual line once before you submit.
Changing the rules #
Adding, removing or changing the meaning of a rule changes PLATFORM_RULES_VERSION and this page in the same commit; every validator report records the version it ran with. Adding, removing or lowering a blocker needs the owner's approval, because it affects games that are already live. Raising a warning to a blocker is tried against the live games first.