agentsclimarketplace

Playtest audit

Skill qmertesdorf/GameForge/.claude/skills/playtest-audit

Use to empirically verify a playable Godot game is actually WINNABLE, FAIR, and PROGRESSABLE — by driving the REAL game loop with a headless competent-player bot that asserts playability invariants (can earn/progress/clear, the first goal is reachable, no death-spiral) and reports balance metrics. The gate the logic/UI self-tests structurally cannot be.From its SKILL.md

Install
npx -y skills add qmertesdorf/GameForge --skill playtest-audit

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.5 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it

playtest-audit

Prove the game can actually be played to a good outcome, not just that its rules are correct. Emit games/<id>/playtest.gd: a headless bot that drives the real game loop with a competent policy and asserts the title is winnable, fair, and lets a player make progress. Prints per-step metrics then exactly PLAYTEST OK (exit 0) or PLAYTEST FAIL: <reason> (exit 1).

Why this exists (the gap it closes)

Every other gate is blind to balance:

  • selftest proves rules by driving the engine directly — and that is exactly its blind spot. diver-0001's selftest "proved" a cautious dive banks by calling collect() at depth 0, asserting the economy in the abstract. It never simulated the spatial reality: treasure spawned 270m below the diver, past a crush line at 200m, so a real player physically could not reach a treasure and return alive — the game was 100% unwinnable and every logic/UI gate was green. The owner found it in ten seconds of play.
  • uitest proves a tap reaches a handler. visual-audit proves the screen reads.
  • the deepen design-depth audit reads the code and reasons on paper — it judged diver "deeper, not bigger" while the same build was unwinnable, because it never played it.

Winnability/fairness/economy-solvency are emergent properties of the assembled loop (spawn geometry × collision × resource drain × difficulty ramp). The only way to see them is to play the real loop. That is this skill.

Loop position

A validation gate, like selftest/uitest — run it after builder and after every deepen pass (tuning is exactly what deepen changes). builder/deepen emit playtest.gd; the validator runs it and fails the build on PLAYTEST FAIL. It records nothing to the manifest — its outputs are the pass/fail gate + a balance-metrics report (and any code/tuning fixes that follow).

Discovery vs. confirmation — where this gate sits relative to generate-and-verify. When a game generates discrete content instances (puzzle boards, procedural maps, wave compositions, shop economies), builder's generate-and-verify gate already guarantees each instance is solvable at creation via an in-game solver (make_verified + Solver). For those games this bot is confirmation, not discovery: it verifies the per-instance guarantee survived assembly — that collision × resource-drain × the difficulty ramp didn't make a provably-solvable instance unwinnable in practice. It stays a hard gate regardless. Where no such upstream gate exists (winnability is a pure tuning/spatial property of the assembled loop — diver-0001's crush line vs. treasure depth), this bot remains the only place winnability is checked, and is in full discovery mode. Either way: a PLAYTEST FAIL is a tuning/generator bug, never license to weaken selftest or Solver.

The harness — drive the REAL loop, deterministically

A SceneTree script that instantiates the actual scene and steps it by hand so the bot sees real spawns, real collision, real resource math:

extends SceneTree
var main
func _initialize() -> void: _run()
func _run() -> void:
    var scene: PackedScene = load("res://Main.tscn")
    main = scene.instantiate()
    root.add_child(main)
    await process_frame          # let _ready() run
    main.set_process(false)      # WE drive _process at a fixed dt — deterministic, no engine variance
    var DT := 1.0 / 60.0
    while main.<active> and guard < CAP:
        <set the bot's intent on the real input surface, e.g. main.target_x = ...>
        main._process(DT)        # the real spawn/move/collide/tick, stepped by us
    # read real state off main.<state> and assert invariants
  • Step the real _process yourself with a fixed dt after set_process(false) — never rely on engine frame timing (variable dt, non-determinism). Seed every RNG; with a fixed seed + fixed dt the whole playthrough is reproducible.
  • The bot acts through the real input surface the game already exposes (the steer target, the action methods) — not by reaching past the rules. If you must poke private state to set up a scenario, do it sparingly and say so.
  • Reuse the genre's existing headless boot assumptions (no autoloads under --script, preload data layers — same gotchas as selftest/uitest).

The bot — COMPETENT, not optimal

Model a reasonable player, not a theoretical-best solver and not a random flailer:

  • A simple greedy-but-cautious policy (head for the nearest reward; retreat when the resource budget only just covers a safe exit, with a margin).
  • If a competent, cautious player cannot earn / progress / survive, the tuning is broken — that is the finding. (diver's bot turned back at a safe margin and still banked 0, because nothing was reachable.)
  • Keep the bot's competence honest: a deliberately weak bot false-fails good games; an optimal one hides difficulty. When the bot is weaker than a human at a sub-skill (precise dodging/aiming), REPORT what it couldn't do rather than FAIL on it — flag it for the human playtest instead of gating on it (diver: the crude lerp-steering bot couldn't grab the sparse Trench treasures, so deep-commission fill is reported, not failed).

Invariants to assert (instantiate per genre)

Gate on the ones that mean "a player can succeed":

  • Solvency / winnable. A competent run reaches a good outcome at least sometimes: earns positive currency, clears the encounter, survives a wave, scores above zero. The anti-unwinnable gate. (diver: a careful rig-0 dive banks > 0.)
  • First goal reachable. The first objective/commission/boss/quota is achievable from the starting loadout — not gated behind a resource you can only get by doing it (a deadlock).
  • Fairness / no death-spiral. The player never dies on a turn where they played safely (no unavoidable hit, no spawn you cannot react to); a setback is recoverable, not terminal.
  • Progression solvency. Across several sessions (buying upgrades / advancing), resources accumulate and the next tier is affordable — measured as gross earnings, not net balance (a bot that spends on upgrades has a low balance while clearly progressing).
  • No trivial dominant win. A single repeated cheap action does not run the score to infinity (the inverse failure — the loop is solved). Pairs with deepen's dominant-strategy test.

Metrics to report (every run, pass or fail)

Print enough to tune from, not just a verdict: earnings/clear per session, the minimum resource margin seen (how close to death a careful player runs), depth/wave/turn reached, time-to-first-upgrade, commission/objective fill rate. These numbers are how the owner sets difficulty — surface them even when the run passes.

Machine-readable contract (the tuning signal). In addition to the human lines and the PLAYTEST OK/FAIL verdict, print exactly one final line PLAYTEST METRICS {json} carrying the numbers above plus the invariant booleans (solvent, first_goal_reachable, no_death_spiral, no_trivial_dominant) so they coincide with the gate verdict. This line is the contract deepen's balance-search (tools/balance.mjs) consumes: it aggregates the metrics across K seeds, hard-rejects configs whose invariants are false, and scores the rest against target bands. The metrics are not just a human dump — they are the tuning oracle. Nothing new is simulated to produce the line; it serialises what the bot already computed.

Honest reporting

Gate on the winnability invariants; report (don't FAIL) anything that's a bot-skill limitation rather than a game flaw, and say which is which. A PLAYTEST OK must mean "a competent player can succeed," and the metrics must name any difficulty the bot only barely cleared — never let a silent cap read as "fully balanced."

Gotchas (carry these)

  • A logic self-test that drives the engine directly cannot see spatial/economic winnability — that is precisely why this gate exists. Do not "fix" a PLAYTEST FAIL by weakening selftest; fix the tuning (spawn geometry, gate depths, costs, the ramp).
  • Stepping _process manually while the engine also auto-runs it double-ticks the world — set_process(false) first.
  • Manual stepping means _draw never runs, so RNG used only in _draw (shake/parallax) won't desync the spawn RNG — keep spawn RNG in _process for a reproducible run.
  • Clear any user:// save at start AND end (a persisted economy leaks across runs and makes "can a fresh player win?" non-deterministic).

Dogfood lesson (diver-0001)

The bug that motivated this skill: crush line (200m) shallower than both the commission zone (Reef@240m) and the nearest treasure spawn (332m), so banking anything was impossible while every logic/UI/design gate stayed green. The bot caught it on the first run (earned 0 across 8 dives). The fix was pure tuning — collectible treasure pre-seeded into safe water, the rig-0 safe depth raised to cover the Shallows + first commission, deeper zones pulled up — verified by re-running until PLAYTEST OK (dive 1 banks 168 incl. a filled commission).

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,835. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.