Quickstart
From nothing to a game that passes the platform's checks and is ready to submit: create it from the starter, run it in the dev console, validate it, check it, hand it in. Plan on an afternoon for the first game; most of it is your game.
Not
The SDK, the starter and the tools are packages of the GamePortal repository today, and a game is a folder in it (games/<id>). Every command below runs from the repository root. External publishing (accounts, upload, a developer panel) opens in V2; until then, games come in through the GamePortal team, see Packaging and submission.
What you need #
Node.js 22.12 or newer and pnpm 10 (the repository pins the exact version).
The repository, installed once with
pnpm install.A Chromium for the runner: the one Playwright installs for the repository is used.
1. Create the game #
pnpm create:game my-game --name "My Game" --name-tr "Oyunum"
pnpm installThe id (my-game) becomes the folder games/my-game, the URL segment, the portal page and the prefix of every storage key: lower-case letters, digits and -. pnpm create:game copies templates/game-starter, fills in the id and the names, and prints the steps that connect a finished game to the portal catalogue. The second command links the new workspace package.
What you get is a small, working game (tap targets before they shrink) wired to the portal end to end:
games/my-game/
index.html markup: HUD, canvas, card, pause overlay
manifest.json what the portal needs to know about the game
public/CREDITS.txt rights and credits, shipped in the build: fill it in
src/host.ts THE PORTAL WIRING: handshake, saves, scores, events, pause, language, day. Keep it.
src/constants.ts ids, versions, tuning numbers; must agree with manifest.json
src/save.ts the save: versioned, parsed defensively, merged without loss
src/pacing.ts score and event pacing under the SDK limits
src/world.ts the example game's rules (pure: no DOM, no clock, no Math.random)
src/run.ts title, playing, over: the flow around a round (pure)
src/render.ts canvas drawing, fitted to any frame size
src/main.ts the shell: DOM, input, ties the above together, exposes the test hook
test/ unit tests: rules, flow, save, pacing, manifest, score plausibility2. Run it in the dev console #
pnpm --filter @gameportal/game-my-game devThis starts vite build --watch and two servers on two different origins, so the origin checks run for real from the first minute:
| What | Address |
|---|---|
| The fake portal and its console (open this) | http://localhost:3760/ |
| Your game, in the portal's iframe | http://127.0.0.1:4160/my-game/1.0.0/ |
| Your game on its own, as a top-level page (standalone mode) | the same game address, opened in a tab |
The game address is served by the real games-host request handler: the same headers, the same <meta name="gameportal:portal-origins">, no cookies. The fake portal is the one the platform's runner uses, so what you see in the console is what check will do. Saving a file rebuilds the game and reloads the frame.
Things to try in the first five minutes (all in Testing):
Watch
hello,initandreadyin the log, and how long they took.Press pause, resume, a language switch and a new day, and see the game react.
Edit the
initpayload: a new player, a returning player, a save from a newer build, a broken save.Delay
initfor ten seconds and see that the game waits instead of switching to standalone mode.Make the next save fail with
conflict,rate_limitedornetworkand see what your game does.
3. Make it yours #
Replace world.ts, run.ts, render.ts and the strings with your game. Keep the shape of main.ts, and keep host.ts as it is unless the SDK contract changes: it is the part that has to be right, and the runner checks it on every run.
Keep manifest.json and src/constants.ts in step (a unit test fails when they drift). Declare every event and score you send in the manifest, and write the plausibility proof for each score (Manifest reference).
Sending a score and an event at the end of a round looks like this:
// a round just ended with 240 points and 18 hits
const ack = await bridge.score("round_score", 240);
if (ack && !ack.ok && ack.error === "rate_limited") {
// too soon after the previous score: the starter's src/pacing.ts spaces them for you
}
void bridge.event("round_end", { score: 240, hits: 18 });XP is never in a message: the portal gives XP itself, from play time and from what your events unlock.
4. Validate and check #
pnpm --filter @gameportal/game-my-game test # your unit tests
pnpm --filter @gameportal/game-my-game validate # build + the static rules: seconds
pnpm --filter @gameportal/game-my-game check # build + static rules + the fake-portal runner: 3 to 4 minutesvalidate scans the build output (manifest, files, sizes, patterns). check also plays the game in a real browser against the fake portal: 15 scenarios, a 60 second error window. Both write rules-report/<id>.<command>.json and print a summary. A rule is pass, warn, fail, manual (a machine cannot decide; a reviewer will look) or pending (validate leaves the runtime rules for check).
The exit code tells scripts what happened: 0 no blocker failed, 1 a blocker failed, 2 bad usage (for example no build yet), 3 the tool itself failed.
Fix blockers first; the rule id says where to look (R-SDK-*, R-STO-*, R-NET-*, ...). FAQ and troubleshooting lists the failures people hit most. Read every manual line once. A shorter run while you iterate: pnpm --filter @gameportal/game-my-game check --error-window 10000 --startup-runs 1 (the platform's rule is the 60 second window, so run the full one before you submit).
5. Package and submit #
Fill
public/CREDITS.txt: who made the code, art, sound and fonts and under which licence, or that there are none (R-CNT-02).Bump
versioninmanifest.json(andGAME_VERSION,package.json) for every change that ships: a released folder never changes.Run
pnpm --filter @gameportal/game-my-game checkonce more on the final build.Hand the game over: the repository folder or the build output, with the report. What the review does with it, and what happens to the release, is in Packaging and submission.
If the game already exists #
You do not have to start from the starter. The integration levels let an existing game join with as little change as it needs:
Level 0, wrap: no code change. The portal runs your game in its iframe and gives play-time XP while the game is visible and focused; saves stay in the game's own browser storage.
Level 1, bridge: add
@gameportal/game-bridge, move saves behindbridge.storage, let the portal own language and pause. A few dozen lines; the sample at the top of the SDK reference is the whole of it.Level 2, deep: scores, the daily challenge metric and game-specific badge events, declared in the manifest.
Details and what each level asks of the game: Packaging and submission.