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
npx -y skills add qmertesdorf/GameForge --skill playtest-auditAssembled 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:
selftestproves rules by driving the engine directly — and that is exactly its blind spot. diver-0001's selftest "proved" a cautious dive banks by callingcollect()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.uitestproves a tap reaches a handler.visual-auditproves the screen reads.- the
deependesign-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
_processyourself with a fixeddtafterset_process(false)— never rely on engine frame timing (variabledt, non-determinism). Seed every RNG; with a fixed seed + fixeddtthe 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,preloaddata layers — same gotchas asselftest/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
FAILon 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 FAILby weakeningselftest; fix the tuning (spawn geometry, gate depths, costs, the ramp). - Stepping
_processmanually while the engine also auto-runs it double-ticks the world —set_process(false)first. - Manual stepping means
_drawnever runs, so RNG used only in_draw(shake/parallax) won't desync the spawn RNG — keep spawn RNG in_processfor 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.