Gives every game a shipped level table that is generated from seeds, proven winnable (by an exact solver at a recorded par, or, for games that draw their future at random, by a witness bot), laid out in packs that unlock by stars, pinned by data goldens, and a daily level that is the same on every iPhone and Android phone with no server. A checker regenerates each table from the game's plan and fails on any difference in values or bytes, a bad curve, an import outside the levels zone or a missing golden.
Rules that must hold
- Levels are generated, never drawn: a level is
{ level, seed, difficulty, stars }and its board is the engine'screate(seed, difficulty), with level and daily difficulties on 0..99 (100 is the endless run). Why: the same seed gives the same level everywhere, the tables stay tiny and saved progress replays exactly. pack-<n>.jsonfiles are written only byscripts/levels/generate-levels.tsfrom the game'sLevelPlan, through the repo's Prettier; the committed bytes equal what the plan generates. Never edit, hand-format, copy or.prettierignorea pack. Why: a hand-edited table drifts from the generator, and the checker regenerates and compares values (pack-stale) and bytes (pack-format).- Every shipped level is proven winnable and its proof line is replayed through the real engine
(
verifyLine): par games by an exact solver at the recorded par; games that draw during play bycreateWitnessSolver, the game's reasonable player from a fixed bot seed.budget-exceededis neverunsolvable. Why: "no level is impossible", and a solver is never trusted on its own. - One star formula,
starsFor: par levels 3 stars at or under par, 2 up to par + 2, 1 for finishing; score levels[win, two, three], three ascending whole numbers,thresholds[0]the lowest winning score, a witness game's two stars at the witness score and three above it (default: 20 % more, at least 1); a loss is 0. Why: the result and levels screens show these stars to every player. - The difficulty curve never falls, later packs are never easier, and no level after the third is a
one-mover; the plan's
rate()enforces the bands while generating. Why: players expect the game to rise; the checker reads the generated table (difficulty-curve,par-curve,trivial-level). - The daily level is
create(dailySeed(dateKey, salt), dailyDifficulty)with a 16-bit salt no other game uses and a daily difficulty the developer approves (default: medium, 40 to 50), both fixed forever once shipped; "today" comes from the injected clock at the edge. Why: every player gets the same daily level on the same date, and a changed salt changes it for everyone. - Data goldens pin the pack boundary levels and at least three daily dates (a year-boundary pair
included) in a
[contract] <game-id> daily goldensdescribe registered incontracts/goldens.json; snapshots are committed and change only with-uon one file and aGate-Change:trailer. Why: a silent generator or tuning change would give players different boards under the same level numbers. contracts/levels.jsondeclares each game'spackCount,levelsPerPack,starRule,dailyandendless, and the packs, theLevelsSpecand the contract test agree with it; the first pack needs 0 stars and later packs exactly half the stars of the packs before them (0, 45, 90 for packs of 30). A daily- or endless-only game is an entry withpackCount0,levelsPerPack0 and nostarRule. Why: one place says what the game ships, so the table and the modes can never disagree.- Level code is pure and deterministic: files in
src/games/<game-id>/levels/import onlysrc/coreand the game's ownrules/,levels/andgeom/(nevertesting/: a witness's policy lives inrules/), and use noMath.random, clock,**,Intlor inexactMath. Why: Jest, the generator, Hermes on iOS and Hermes on Android must generate bit-identical levels. - Pack names are catalog keys present in every catalog of the app. Why: the levels screen shows them in the user's language, left to right or right to left.
- Tuning first, packs second: the balance sims pass before the packs are generated, and any
later change to the tuning,
create, the solver or the witness regenerates the packs and goldens; after release, stop and ask the developer before players' levels change. Why: every level iscreate(seed, difficulty)on the current tuning.
Workflow
- Settle the tuning, then read references/levels-model.md (types, the curve and the tuning rows, the contract, packs, stars, daily, endless). The game's balance sims (game-balance-and-bots) pass first. Ask the developer for the bands: packs and levels per pack, minimum par per pack, star rule (par or score), daily difficulty, an endless run; defaults are 3 x 30, minimum par 2/3/4, par stars, daily 40 to 50, no endless run.
- Install the level kit once per app. The game kit and the RNG come first: game-rules-engine
(
src/core/game/*.ts,testing/play-bot.ts) and seeded-determinism-and-golden-vectors (src/core/rng/sfc32.ts,src/core/dates/{date-key,daily-seed}.ts). Pin fast-check exactly (npm install -D -E fast-check@4.10.2; verified on 2026-10-03, re-verify withnpm view fast-check version). Runnode ${CLAUDE_SKILL_DIR}/scripts/check-levels.mjs .; for eachkit-file-missingcopy the file fromtemplates/to the path its first line names, with its test:src/core/game/solver/,src/core/game/level-kit/,src/core/game/testing/render-cells.tsandscripts/levels/.templates/src/core/game/*.ts,testing/play-bot.tsandtemplates/src/games/__GAME_ID__/rules/are synced copies of game-rules-engine's files, there only so the templates compile alone: never copy them over the app's own. - Write the solver test-first. Read references/solvers.md. A game whose
whole future is fixed by
creategets an exact search: copytemplates/src/games/__GAME_ID__/levels/__GAME_ID__-solver.tsand its test tosrc/games/<game-id>/levels/(rename files; replace__GAME_ID__,__GAME_PASCAL__,__GAME_CONST__), choose the search state, move order and key, BFS or IDA*, and prove it with examples, theverifyLineproperty and a budget test. A game that draws during play usescreateWitnessSolverwith its policy inrules/: imitateexamples/pig-levels/. - Write the level plan test-first. Read references/generators-and-plans.md.
Copy
__GAME_ID__-level-plan.tsand its test: packs, the curve (difficultyForfrom 0 at level 1 to its cap), the budget, andrate({ level, par, line, final })with the bands (a witness game:scoreThresholdsfrom the final score, a minimum witness length). - Declare the game in
contracts/levels.json(copytemplates/contracts/levels.jsonandlevels.schema.jsononce; one entry per game; a daily- or endless-only game haspackCount0 and skips the plan and packs: levels-model.md, "Daily-only and endless-only games"). Merge the pack name keys oftemplates/src/i18n/catalogs/en.jsonandar.json(English and Arabic examples) into every catalog the app ships, with your game id and pack names, and send the texts through the app's review queue (i18n-strings-and-catalogs owns the catalogs). - Generate the tables:
node scripts/levels/generate-levels.ts --game <game-id>writespack-<n>.jsonthrough the repo's Prettier. Fix anylevel N: no candidateby loosening the band or the curve, rerun until 0 failures. Thennode scripts/levels/generate-levels.ts --game <game-id> --checkandnpx prettier --check src/games/<game-id>/levelsboth exit 0. Commit exactly what it wrote. - Assemble and prove the table. Copy
__GAME_ID__-levels.ts(theLevelsSpec: plan, packs,toLevelEntries, solver, daily salt and difficulty, endless) with the game's own salt (the template's0x7c9eislights-out's;daily-salt-sharedprints the default for your id) and__GAME_ID__-levels.test.ts(runslevelsContractProblemswith the contract entry asCONFIG). Setlevelsinsrc/games/<game-id>/<game-id>-game.ts(game-rules-engine's module) to it. A puzzle game with hints wireshints: { kind: 'solver', suggest }in its rules (never for a witness). - Pin the goldens. Read references/goldens-and-quality.md.
Copy
__GAME_ID__-describe.ts(with its test) and writedescribeLevel(state)for the game. Copy__GAME_ID__-levels.golden.test.ts, delete the template's.snap.ios, runnpx jest src/games/<game-id>/levels/<game-id>-levels.golden.test.ts --selectProjects golden -uonce, open the.snap.iosand read the boards, register the test incontracts/goldens.jsoncontracts(golden-and-snapshot-tests), thennpx jest src/games/<game-id>/levels --ci --selectProjects goldenpasses. - Format, lint, type-check, test:
npx eslint --fix src/games/<game-id>/levels src/core/game scripts/levels,npx prettier --write src/games/<game-id>/levels,npm run typecheck, thennpx jest src/games/<game-id>/levels src/core/game scripts/levels --ci --selectProjects unit --coverage --collectCoverageFrom='src/core/game/**/*.ts' --coverageThreshold='{}'and read the table (src/corestays at 95/95/95/90; paths before--selectProjects, which reads every later word as a project name). - Run the check
node ${CLAUDE_SKILL_DIR}/scripts/check-levels.mjs . --game <game-id> --report; fix everyFAILline (file, rule, fix) and rerun until it printsRESULT: PASS. Rules, options and troubleshooting: references/checker.md. Put the--reportpack lines in the report and commit tables and goldens with aGate-Change:trailer (git-commits-and-reporting).
Definition of done
- The balance sims passed before the packs were generated (or the packs were regenerated after the
last change to the tuning,
create, the solver or the witness). - The root
package.jsonpinsfast-checkexactly;src/core/game/solver/,level-kit/,testing/render-cells.tsandscripts/levels/are installed with their tests, on top of the game kit and the RNG and date keys from their owner skills. -
contracts/levels.jsonlists the game; its pack name keys are in every catalog. -
src/games/<game-id>/levels/has the solver, the plan,describeLevel, theLevelsSpec, one generatedpack-<n>.jsonper pack and their tests;generate-levels.ts --game <game-id> --checkandnpx prettier --check src/games/<game-id>/levelsexit 0; no__GAME_placeholder is left. -
<game-id>-levels.test.tsrunslevelsContractProblemsand returns[](every level proven by a replayed line, at its par for par levels; packs matching the contract; endless at 100 exactly when the contract says so). - The golden test pins the pack boundary levels and three daily dates with a year-boundary pair in a
[contract]describe; the test is listed incontracts/goldens.jsoncontracts(golden-and- snapshot-tests' check passes too); its.snap.ioswas read and committed with aGate-Change:trailer. -
npm run typecheck,npx eslint --max-warnings 0,npx prettier --checkandnpx jest <paths> --ci --selectProjects unitand--selectProjects goldenpass. -
node ${CLAUDE_SKILL_DIR}/scripts/check-levels.mjs . --game <game-id>printsRESULT: PASS
A passing run may print SKIP src/i18n/catalogs [pack-name-key] not yet created (i18n-strings-and-catalogs creates it)
before the app has catalogs, and SKIP contracts/goldens.json [daily-golden-contract] not yet created (golden-and-snapshot-tests creates it)
before the goldens register exists. An app without levels prints
NOT APPLICABLE: the app has no levels (no src/games/<game-id>/levels/, no contracts/levels.json and no src/core/game/solver or src/core/game/level-kit).
Anti-patterns
- Editing a pack file to "fix" one level. The next regeneration undoes it and
pack-stalefails; change the plan orcreateand regenerate. - Copying the template's packs or snapshot for a new game. They belong to
lights-out; generate the packs and write the snapshot with-ufor your game id. - Raising a test's node budget until a level passes. A level the solver cannot prove within the plan's budget should never have been accepted; regenerate or improve the key and move order.
- Treating
budget-exceededas unsolvable (or the reverse). Onlyunsolvablemeans the whole space was searched. - A heuristic that overestimates "to be faster". IDA* then returns a longer line than par and players beat par; keep it admissible, or use 0.
- Changing a game's daily salt, the daily difficulty or
dailySeed. Every player's daily level changes between versions; ask the developer first. jest -uover a whole folder or in a script. Goldens change one file at a time, after reading the new boards.Math.randomor the clock to pick candidate seeds. Tables must regenerate identically; seeds come fromcandidateSeed(gameId, level, attempt).- Ad-hoc star numbers in a screen. Stars come from the table's
StarRulethroughstarsFor. - Formatting packs by hand or adding them to
.prettierignore.prettier --checkandgenerate-levels --checkthen disagree; run the generator. solver: nullor bot sims instead of a proof for a game that draws at random. Prove each level withcreateWitnessSolver; a lost witness is a retry with the next seed, neverunsolvable.- Importing the bot from
testing/into a level file. It breaks the levels zone (pure-import); keep the policy inrules/and lettesting/re-export it. - Generating the packs before the tuning is settled. Every later tuning change regenerates the packs and their goldens; run the balance sims first.
- Adding
"type"to the app's rootpackage.jsonto silence a Node warning. The generator's hooks loadsrc/as TypeScript modules; Metro and the Expo config read the root manifest.
Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| references/levels-model.md | Defaults, the types, the difficulty scale, the levels contract, packs and unlocking, stars, the daily level, endless, who uses what | Workflow step 1 |
| references/solvers.md | The search kit, custom problems and keys, BFS or IDA*, heuristics, budgets, the witness solver, verifyLine, hints, solver tests |
Step 3 |
| references/generators-and-plans.md | The pipeline, tuning first, generator strategies, curve, LevelPlan, candidate seeds, rate(), witness games, running the generator, how it loads the app's TypeScript, changing tables, multi-package |
Steps 1, 4 and 6 |
| references/goldens-and-quality.md | Data goldens and describeLevel, the -u policy, the daily contract, the levels contract test, the quality metrics |
Steps 7 and 8 |
| references/checker.md | The checker's options, config keys, every rule, expected SKIP and NOT APPLICABLE lines, multi-package, troubleshooting | Step 10, when a line is unclear |
templates/src/core/game/solver/ |
search-problem, bfs-solve, ida-star-solve, engine-solver, verify-line, witness-solver, with tests |
Step 2 |
templates/src/core/game/level-kit/ |
level-table, levels-contract (replays every proof line), pack-progress, star-rating (the one star formula) and daily-start (the one daily seed and difficulty), both with canonical copies synced into game-session-host too, level-plan, plan-level-table, with tests |
Step 2 |
templates/src/core/game/testing/ |
render-cells.ts (text boards for goldens) with its test |
Step 2 |
templates/scripts/levels/ |
generate-levels.ts (the CLI), run-level-plan.ts (plan, Prettier, write or compare), app-imports.ts (Node module hooks for @/ and extensionless imports), with tests |
Steps 2 and 6 |
templates/contracts/ |
levels.json (one entry per game) and levels.schema.json |
Step 5 |
templates/src/games/__GAME_ID__/levels/ |
The template game (Lights Out): exact solver, plan, describeLevel, LevelsSpec, unit, contract and golden tests, its generated packs and golden snapshot (for lights-out only) |
Steps 3 to 8 |
templates/src/i18n/catalogs/ |
en.json and ar.json holding only the template's three __GAME_ID__.pack-name.N keys, merged into the app's catalogs |
Step 5 |
templates/src/core/game/levels.ts |
Synced copy of game-rules-engine's kit file, there so the templates compile alone (assets/shared.json; never edit here, never copied over the app's own) | Never by hand |
templates/src/core/game/difficulty.ts |
Synced copy of game-rules-engine's kit file, there so the templates compile alone (assets/shared.json; never edit here, never copied over the app's own) | Never by hand |
templates/src/core/game/game-engine.ts |
Synced copy of game-rules-engine's kit file, there so the templates compile alone (assets/shared.json; never edit here, never copied over the app's own) | Never by hand |
templates/src/core/game/messages.ts |
Synced copy of game-rules-engine's kit file, there so the templates compile alone (assets/shared.json; never edit here, never copied over the app's own) | Never by hand |
templates/src/core/game/testing/play-bot.ts |
Synced copy of game-rules-engine's kit file, there so the templates compile alone (assets/shared.json; never edit here, never copied over the app's own) | Never by hand |
templates/src/games/__GAME_ID__/rules/ |
Synced copies of game-rules-engine's Lights Out rules (types, tuning, create, toggle-cells, list-moves, outcome, apply-move) the level templates import (assets/shared.json; never edit here) | Never by hand |
examples/pig-levels/ |
Pig, a dice game that draws during play: the witness policy in rules/ (a synced copy of game-rules-engine's Pig rules/pig-hold-policy.ts), witness solver, score-rated plan, describeLevel, a LevelsSpec with daily and endless, tests, golden snapshot, contract entry |
Steps 3 to 8, for a game that draws during play |
| assets/template-placeholders.json | The values the self-test and the template verification fill in (lights-out) |
When adding a placeholder |
| assets/shared.json | Declares the synced files (check-lib, app-modules, the game kit and rules copies, star-rating.ts, the canonical RNG, date keys and Prettier config for the fixtures) |
When adding a shared file |
scripts/check-levels.mjs |
Checker: kit, prerequisites, generator, fast-check pin, the star rule (run), the levels contract, packs, curves, modes, salts, pack names, goldens, contract test, import zone, determinism, placeholders, regeneration by value and by byte | Steps 2 and 10, and at the end |
scripts/lib/ |
level-layout.mjs (paths, options, contract), level-checks.mjs, level-run.mjs, pack-format.mjs (Prettier's JSON layout when Prettier is absent), ts-scan.mjs; app-modules.mjs is synced (do not edit here) |
When changing the checker |
scripts/selftest.mjs |
Proves the checker: the templates must pass, the score-rated variant must pass, every planted bug must fail under its rule | After changing the checker or a template |
scripts/check-lib.mjs |
Shared script library, synced (do not edit here) | Never by hand |
tests/bases/app/ |
The minimal app the fixtures lay the templates on (package.json, tsconfig, synced core files and Prettier config; the catalogs come from the templates) | When the fixture base changes |
tests/fixtures/ |
check-levels/ (good/, one bad-* per rule, pass-* and error-* cases) and check-levels-score/ (score.json, good/, two pack cases) |
When adding a rule to the checker |
Related skills
game-rules-engine-create,listMoves,applyMove,outcome, the game kit and the difficulty scale the levels are built on.seeded-determinism-and-golden-vectors-sfc32,dailySeed, date keys and the determinism policy.game-balance-and-bots- the tuning and bot sims that pass before the packs are generated.game-session-host- starting level, daily and endless runs, stars at run end and "Next level".dates-streaks-and-statistics- today from the clock, streaks and the daily calendar.golden-and-snapshot-tests- the golden project,[contract]entries andcontracts/goldens.json.run-goldens-rebaseline- re-blessing goldens on purpose.screen-contracts- the levels and result screens that show packs and stars.i18n-strings-and-catalogs- the catalogs that hold pack names.new-game-scaffold- declaring a game (contracts/games.json) before its rules and levels, and its completeness gate.performance-budgets- measuring a hint's solver budget on a low-end Android phone.git-commits-and-reporting- theGate-Change:trailer for tables and goldens.