Skip to content
Back to skills

Game Balance And Bots

ASecurity

Tunes game difficulty with seeded headless bots: sim reports, balance bands, tuning knobs, a fun-in-seconds test. Use when balancing, tuning pacing or adding bots or sims. Not for solvers or par (level-generation-and-solvers).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentjavascriptrustgojavareactnodeawstestingdebugginggit

Security analysis

A100/100

Pro scans all 20 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill game-balance-and-bots --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Game Balance And Bots?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Game Balance And Bots
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-game-balance-and-bots/badge)](https://www.skillsdirectory.com/skills/zakariaf-game-balance-and-bots)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: game-balance-and-bots
description: "Tunes game difficulty with seeded headless bots: sim reports, balance bands, tuning knobs, a fun-in-seconds test. Use when balancing, tuning pacing or adding bots or sims. Not for solvers or par (level-generation-and-solvers)."
---

# Game balance and bots

Every game is played thousands of times headless by seeded bots before a person plays it. The sims
measure the difficulty curve, whether skill matters, how soon the first win moment comes and whether
the twist happens; a bands file says what "balanced" means; every balance number lives in one tuning
file. This skill builds the harness once (`src/core/game-balance/`, `test/sims/write-sim-report.ts`),
each game's bots, sims and bands, and the check that judges the numbers.

## Rules that must hold

1. **Bots play only through the pure engine contract** (`create`, `listMoves`, `applyMove`,
   `outcome`), headless in plain Node via `jest.sim.config.js`, never through the UI. Why: the same
   rules the app runs are measured, in seconds, with no React Native mock.
2. **Every run is deterministic:** seeds `1…seedsPerCell`, the bot's own seeded RNG, no
   `Math.random`, `Date`, `performance.now`, retries, skips or focus; the report has no timestamps.
   Why: a rerun gives identical bytes, so a diff of the report shows exactly what a change did, and
   chosen seeds could shop for numbers.
3. **Every run has a hard move cap, and no run may reach it** (`capHits` = 0). Why: a stuck or
   endless game is a rules bug that a bot finds long before a player does.
4. **Use the three standard players:** `random` (the baseline), a reasonable player (the testing
   spec's `bot`: `greedyPolicy` by default) and `lookahead` (the upper bound; it sees the seeded
   future). In a game that draws during play (dice, cards, spawns), the reasonable player is a rule a
   person could follow and never peeks at the next draw: `greedyPolicy` would. Skill must matter:
   random < reasonable < lookahead by a clear step. Why: if naive play does as well as sensible play,
   the choices are fake; a peeking player measures nothing a person meets.
5. **A sim asserts properties of at least 100 seeds per cell, never a single outcome,** and writes
   `reports/sim/<game-id>.json` before its assertions. Why: one game proves nothing about balance, and
   a failing run must still leave numbers to read.
6. **Every balance number lives in `src/games/<game-id>/rules/<game-id>-tuning.ts`** (real-time games:
   `sim/<game-id>-tuning.ts`, a `'worklet'` module): documented knobs plus a per-difficulty table read
   through `knobsFor`; the rules contain no other balance numbers. Why: one place to tune, and a diff
   shows every balance change.
7. **Difficulty is one scale, whole numbers 0..100, never a table index.** `knobsFor(d)` returns the
   endless row at 100 (`ENDLESS_DIFFICULTY`, no goal, never won) and otherwise the row
   `rowFor(d, rows)` picks, with a `reduce` over the table; the grid samples the lower bound of each
   row (`[0, 25, 50, 75]` for four rows), and an endless mode gets its own `endless` block at 100,
   never a step of the curve. Why: levels (0..99), a daily and saved runs all use this scale; a 0..3
   index makes every level above 3 play the hardest row.
8. **Balance is a contract in `test/sims/<game-id>/balance-bands.json`:** the bands hold, the curve
   moves monotonically by `minStep`, the skill gap holds, the first payoff comes early, the twist
   happens, and the endless run is never won and stays inside its own bands. Never widen a band, cut
   or change seeds, or tune the bot to go green. Why: bands are the design record; moving them hides
   the problem instead of fixing the game.
9. **The fun-within-seconds kill test:** on the easiest level the reasonable player reaches the first
   payoff within 1-3 moves (2 for single-tap moves; seconds for real-time games: `runSimBot` with
   `payoffStepTicks: 120`) in at least 90 % of runs, and the twist fires at least once per run. If
   even the lookahead bot cannot, stop and report to the developer. Why: prototype bots showed ideas
   fail here structurally (a loop-closing snake caught nothing in 13 foreseen moves; a crash puzzle's
   twist came 0.25 times per game).
10. **A report counts only for the code it measured:** its fingerprint covers the game's `rules/`,
    `levels/` (without the generated `pack-*.json`), `sim/`, `geom/` and `testing/` folders, the sim
    file and the `src/core` files they import (the sim's import closure), and game logic imports
    nothing outside them; rerun `npm run test:sim` after any change there. Why: stale numbers approve
    code that no longer exists.
11. **Change the game, not the player:** improve a bot's evaluation only to match reasonable human
    play, never to reach a band; decide the payoff and twist tags before the first run. Why: a bot or
    a tag tuned to the bands measures nothing.
12. **A person's play-test is the final judge, and it never blocks:** bands stay `"proposed"` until a
    person has played on an iPhone and an Android phone and agreed, then become `"approved"` with a
    date. Until then every report lists it under "Human checks (not blocking)", and
    `check-balance.mjs --release` prints a `HUMAN CHECK (not blocking)` line, not a problem. Why: bots
    measure pace, not joy; the person's feel wins whenever it comes, and nothing waits for it.
13. **Bot numbers hold on both platforms only through pure, seeded rules and fixed ticks.** Sims run in
    Node (V8), the app on Hermes on iOS and Android; a real-time sim counts fixed ticks, never display
    frames (60 Hz Android phones, 120 Hz iPhones). Why: a balance claim that depends on the engine,
    the frame rate or the device is not a claim about the game.

## Workflow

1. **Pick the job.** First harness setup: step 2. A game's bots and sims: steps 3-8. Retuning: steps
   6-8. Judging a new idea early: read [references/fun-and-kill-test.md](references/fun-and-kill-test.md),
   then steps 3-5 at the easiest difficulty only. Always finish with steps 9-10.
2. **Harness (once per app).** Read [references/bots-and-sims.md](references/bots-and-sims.md). The
   game kit (`src/core/game/`, `src/core/rng/sfc32.ts`) comes from game-rules-engine. Run
   `node ${CLAUDE_SKILL_DIR}/scripts/check-balance.mjs .`; for every `harness-missing`,
   `harness-outdated` or `sim-config` line copy the file from `templates/` verbatim
   (`src/core/game-balance/*` with tests, `test/sims/write-sim-report.ts` with its test,
   `jest.sim.config.js`), keep `"test:sim": "jest --ci --config jest.sim.config.js"` in
   `package.json`, and make the unit project of `jest.config.js` ignore `\\.sim\\.test\\.` (both are
   in the canonical configs). Run `npx jest src/core/game-balance test/sims/write-sim-report.test.ts --ci --selectProjects unit`
   (paths first: `--selectProjects` swallows every following word).
3. **Tuning file.** Read [references/tuning-playbook.md](references/tuning-playbook.md) first. The
   game already has `rules/<game-id>-tuning.ts` from game-rules-engine's template. Name the knobs for
   the game (document each per-difficulty knob once, on the knobs type), move every balance number of
   the rules into it and read them through `TUNING` and `knobsFor(difficulty)`, keeping the `rowFor` +
   `reduce` lookup. A game with an endless mode adds the endless row and returns it first for
   `isEndlessDifficulty(difficulty)`: [examples/pig/src/games/pig/rules/pig-tuning.ts](examples/pig/src/games/pig/rules/pig-tuning.ts).
4. **Bot hooks, test-first.** Copy `templates/src/games/__GAME_ID__/testing/__GAME_ID__-bot.ts` and
   its test (Lights Out's hooks; replace `__GAME_ID__`, `__GAME_PASCAL__` and `__GAME_CAMEL__`) and
   rewrite them for the game: `evaluate<Game>`, `scoreOf<Game>` (the number the HUD shows),
   `tag<Game>Event` (payoff and twist, decided now) and the reasonable player; then set it as `bot` in
   `testing/<game-id>-testing.ts`. A game that draws during play writes the reasonable player as a rule
   ([examples/pig/src/games/pig/testing/pig-bot.ts](examples/pig/src/games/pig/testing/pig-bot.ts)).
   When the levels are proven by the reasonable player (a witness), put the evaluation in
   `rules/<game-id>-evaluate.ts` and re-export it: `levels/` may not import `testing/`. A real-time
   game exports its `SimBotGame` hooks and the `idle`, `wander` and `dodge` command policies
   ([examples/arena-chase/src/games/arena-chase/testing/arena-chase-bot.ts](examples/arena-chase/src/games/arena-chase/testing/arena-chase-bot.ts)).
5. **Sim and bands.** Copy `templates/test/sims/__GAME_ID__/` to `test/sims/<game-id>/`, replace the
   placeholders, set the grid to the lower bound of each tuning row (the template's `[0, 34, 67]` are
   Lights Out's three), name the policies, and for an endless mode replace `"endless": null` with its
   block ([references/balance-contract.md](references/balance-contract.md), "Endless mode"). Run
   `npx eslint --fix` and `npx prettier --write` on it, then
   `npx jest test/sims/<game-id> --ci --config jest.sim.config.js`. The first run writes the report
   even if the bands fail. Real-time games play through `runSimBot` instead of `traceBot`
   ([examples/arena-chase/test/sims/arena-chase/balance.sim.test.ts](examples/arena-chase/test/sims/arena-chase/balance.sim.test.ts)).
6. **Read and tune.** Fix what is broken first (cap hits, replay differences, no early payoff, no
   twist), then tune one knob at a time with the tuning loop and the symptom table, rerunning the sim
   after each change and reading `lossReasons`.
7. **Write the bands** around the tuned numbers with a margin and a `why` per band, and the story in
   `notes` in plain words; keep `"status": "proposed"`.
8. **Kill test verdict.** If the first payoff or the twist cannot be reached after reasonable tuning,
   stop and tell the developer (fun-and-kill-test.md, "When a game fails"). Do not polish a game that
   fails it.
9. **Run the check** (run, fix, rerun until `RESULT: PASS`): `npm run test:sim`, then
   `node ${CLAUDE_SKILL_DIR}/scripts/check-balance.mjs .` (add `--game <game-id>` for one game).
   Options, config keys and troubleshooting: [references/checker.md](references/checker.md).
10. **Report and hand over.** Put the plain-language "Bots" line per difficulty (numbers from the
    report) in the evidence report and "Play-test <game> on an iPhone and an Android phone" under
    "Human checks (not blocking)" until a person has done it; when they agree, set
    `"status": "approved"` and `"approvedOn"`. Before a release run the check with `--release`: it
    passes with proposed bands and prints the `HUMAN CHECK (not blocking)` line to copy into the
    release report. The sims pass before level-generation-and-solvers generates the level tables; a
    tuning change after they exist regenerates them in the same commit with a `Gate-Change:` trailer.

## Definition of done

- [ ] The harness files and their unit tests are in place and green; `npm run test:sim` exists and
      never runs inside `npm test`.
- [ ] Each game has a documented tuning file on the 0..100 scale (`rowFor` + `reduce`, an endless row
      only with an endless mode) with its test, bot hooks with tests (a non-peeking reasonable player
      when the game draws during play), `test/sims/<game-id>/balance.sim.test.ts` and
      `balance-bands.json` with `notes` (grid 0..99, the endless block when the game has one).
- [ ] `npm run test:sim` passes: no cap hits, identical replays, every band, the curve, the skill gap,
      the first payoff and the twist; one sim file runs in about 60 s or less.
- [ ] `reports/sim/<game-id>.json` is fresh and its numbers are in the evidence report in plain words.
- [ ] For a release: the check passes with `--release`; each `HUMAN CHECK (not blocking)` line is in
      the report's "Human checks (not blocking)" list.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-balance.mjs .` prints `RESULT: PASS`

A passing run may print `SKIP src/core/game/testing/play-bot.ts [harness-missing] not yet created (game-rules-engine creates it)`
before the kit is installed. An app with no game prints
`NOT APPLICABLE: the app has no game (no src/games/<game-id>/ folder with rules/ or sim/)`.

## Anti-patterns

- **Balancing by playing a few games by hand, or by one seed.** Balance is a distribution; run 100+
  seeds per cell and read the spread (`p10Moves`, `p90Moves`, `lossReasons`).
- **Loosening a band, lowering `minStep` or trimming seeds to get green.** Change a knob, or bring the
  developer a design question.
- **A smarter bot to rescue a failing curve, or a greedy bot that peeks at the next roll.** If random ≈
  reasonable, the game's choices are weak; fix the rules. A peeking bot never loses a turn's points and proves nothing.
- **Renaming the payoff after a failing run.** Decide the tags first and write why in `notes`.
- **Balance numbers scattered through the rules** (`hearts: 3` in `create.ts`). Move them into the
  tuning file with a comment.
- **A sim that needs React Native, a clock or `Math.random`.** The rules must be pure and seeded.
- **Trusting a report after changing the rules.** The fingerprint makes it stale; rerun the sims.
- **Tuning the whole difficulty table at once.** Change one knob, rerun, compare the two reports.
- **Difficulty as a table index, or 100 in the grid.** Use `rowFor` and the `endless` block.
- **`table[index] ?? table[0]`.** The fallback never runs and fails the per-file branch threshold.
- **Polishing a game that fails the kill test.** Art and sound cannot fix a first payoff that never
  comes; stop and ask the developer.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/bots-and-sims.md](references/bots-and-sims.md) | The harness, what a bot needs, the three players, non-peeking players, bot hooks, tags, the sim file, real-time games, determinism on iOS and Android, speed, debugging, reporting, multi-package | Workflow steps 2, 4 and 5 |
| [references/balance-contract.md](references/balance-contract.md) | The report fields, the fingerprint, the bands file, endless mode, setting bands, approval as a human check, how the check judges | Steps 6, 7 and 10 |
| [references/tuning-playbook.md](references/tuning-playbook.md) | The tuning file, knob families, the curve, the tuning loop, symptoms and what to turn, the Pig tuning log, levels and daily | Steps 3 and 6 |
| [references/fun-and-kill-test.md](references/fun-and-kill-test.md) | Why seconds matter, the kill test, what prototype bots revealed, openings built for an early payoff, choosing payoff and twist, when to stop | Steps 1 and 8 |
| [references/checker.md](references/checker.md) | The checker's folders, options, config keys, every rule, expected SKIP/NOT APPLICABLE/HUMAN CHECK lines, troubleshooting | Step 9, when a line is unclear |
| `templates/src/core/game-balance/` | `trace-bot`, `bot-policies`, `sim-stats`, `balance-bands`, `parse-balance-bands`, `run-sim-bot`, each with its test | Step 2 (copy verbatim) |
| [templates/test/sims/write-sim-report.ts](templates/test/sims/write-sim-report.ts) | `SIM_LAYOUT`, `rulesFingerprint` over the sim's import closure (`fingerprintFiles`) and `writeSimReport` | Step 2 |
| [templates/test/sims/write-sim-report.test.ts](templates/test/sims/write-sim-report.test.ts) | Its test: unrelated core or screen files and generated level tables leave the fingerprint unchanged; an imported core file (even through another), a rules file or the level plan changes it | Step 2 |
| [templates/jest.sim.config.js](templates/jest.sim.config.js) | Plain-Node Jest config for `*.sim.test.ts` (synced canonical file; do not edit here) | Step 2 |
| [templates/test/sims/__GAME_ID__/balance.sim.test.ts](templates/test/sims/__GAME_ID__/balance.sim.test.ts) | A game's sim: plays the grid, writes the report, asserts the bands | Step 5 |
| [templates/test/sims/__GAME_ID__/balance-bands.json](templates/test/sims/__GAME_ID__/balance-bands.json) | The template game's balance contract (Lights Out's measured bands on three rows, `endless: null`) | Steps 5 and 7 |
| [templates/src/games/__GAME_ID__/testing/__GAME_ID__-bot.ts](templates/src/games/__GAME_ID__/testing/__GAME_ID__-bot.ts) | Lights Out's bot hooks: `evaluate` (fewer lit cells, a board one press from dark valued as a person sees it), `scoreOf`, `tag`, the greedy reasonable player | Step 4 |
| [templates/src/games/__GAME_ID__/testing/__GAME_ID__-bot.test.ts](templates/src/games/__GAME_ID__/testing/__GAME_ID__-bot.test.ts) | Its test | Step 4 |
| `templates/src/core/game/` | Synced copies (assets/shared.json) of game-rules-engine's `game-engine.ts`, `difficulty.ts` and `testing/play-bot.ts` (with tests), so the templates compile alone; never edit here, keep that skill's files | Never copy over that skill's kit |
| `templates/src/games/__GAME_ID__/rules/` | Synced copies of game-rules-engine's template game (Lights Out: types, tuning with its test, create, toggle-cells, list-moves, outcome, apply-move) that the bot template and the self-test need; never edit here | Step 3, as the tuning model |
| `examples/pig/` | Pig, the turn-based model: the tuned tuning file with an endless row, a non-peeking reasonable player and its test, the sim, bands with an endless block and the real report (the game's rules are game-rules-engine's Pig example) | Steps 3-7 for a game that draws during play or has an endless mode |
| `examples/arena-chase/` | The real-time model: a fixed-step sim and `'worklet'` tuning file (+ replay tests), `SimBotGame` hooks and the idle/wander/dodge command policies (+ tests), a `runSimBot` sim, bands in ticks and seconds, and its real report | Steps 3-7 for a real-time game |
| `scripts/check-balance.mjs` | Checks the sim config, harness, sims, seeds, bot hooks, tuning file, fingerprint scope and bands, and judges each report; `--release` prints unapproved bands as a HUMAN CHECK line | Steps 2, 9 and 10 |
| `scripts/lib/balance.mjs` | Folders and options, the JavaScript port of the fingerprint and the bands rules | When changing the checker |
| `scripts/selftest.mjs` | Proves the checker passes the templates and both examples and catches every planted bug | After changing the checker, a template or an example |
| `scripts/check-lib.mjs` | Shared script library, synced (do not edit here) | Never by hand |
| `assets/shared.json` | Declares the synced files (check-lib, the sim Jest config, game-rules-engine's kit and template rules copies) | When adding a shared file |
| `assets/template-placeholders.json` | The values the self-test and template verification fill in (`lights-out`) | When adding a placeholder |
| `tests/build-fixtures.mjs` | Writes the overlay cases (ops, EXPECT lines with computed file:line, re-stamped fingerprints) | After changing a template, an example or the checker |
| `tests/bases/app/` | The minimal app the fixtures lay the templates and examples on, with Lights Out's real report | When the fixture base changes |
| `tests/fixtures/` | `check-balance/`: `good/`, one `bad-*` per rule and planted bug, `pass-*` (before the kit, wrapped table, release with proposed bands, core growth, packs after sims, installed skills, no game, custom games folder, one game) and `error-*` cases | When adding a rule |

## Related skills

- `game-rules-engine` - the engine contract, the kit, the determinism policy and the sfc32 PRNG the bots drive.
- `level-generation-and-solvers` - solvers, par, stars, level tables and the witness that plays them.
- `realtime-game-loop` - the fixed-step sims that `runSimBot` plays headless.
- `new-game-scaffold` - where a new game's tuning file, bot hooks and sim start.
- `quality-gates` - `test:sim` inside the verify plan and the gated paths.
- `git-commits-and-reporting` - the evidence report, Human checks (not blocking) and the Gate-Change trailer.
- `unit-and-component-tests` - the Jest projects, `jest.sim.config.js` and the coverage gates.
- `audio-and-haptics-feedback` - sounds and haptics for the payoff moments the sims find.

Files in this skill

  • SKILL.md19.7 KB
  • assets/shared.json1.7 KB
  • assets/template-placeholders.json384 B
  • examples/arena-chase/reports/sim/arena-chase.json3.7 KB
  • examples/arena-chase/src/games/arena-chase/sim/arena-chase-sim.test.ts2.9 KB
  • examples/arena-chase/src/games/arena-chase/sim/arena-chase-sim.ts8.2 KB
  • examples/arena-chase/src/games/arena-chase/sim/arena-chase-tuning.ts3 KB
  • examples/arena-chase/src/games/arena-chase/testing/arena-chase-bot.test.ts3 KB
  • examples/arena-chase/src/games/arena-chase/testing/arena-chase-bot.ts4.4 KB
  • examples/arena-chase/test/sims/arena-chase/balance-bands.json2.4 KB
  • examples/arena-chase/test/sims/arena-chase/balance.sim.test.ts3.3 KB
  • examples/pig/reports/sim/pig.json5.5 KB
  • examples/pig/src/games/pig/rules/pig-tuning.ts2.1 KB
  • examples/pig/src/games/pig/testing/pig-bot.test.ts2.7 KB
  • examples/pig/src/games/pig/testing/pig-bot.ts2.5 KB
  • examples/pig/test/sims/pig/balance-bands.json3.4 KB
  • examples/pig/test/sims/pig/balance.sim.test.ts3.5 KB
  • references/balance-contract.md12.1 KB
  • references/bots-and-sims.md21.3 KB
  • references/checker.md7.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.