gameplay-harness · git:20260913.23d3443 · 2026-09-13 · sha256 87f166a086676bad
gameplay-harness git:20260913.23d3443A
Immutable. This exact content never changes and is served at /api/v1/blob/87f166a086676bad.
---
name: gameplay-harness
description: Use when you need an executable runner beside the game that records inputs, steps the logic clock, and writes evidence. Not the repo CI. Not a second combat clock.
---
# Gameplay harness
Ask first: logic | replay | scripted | soak | human.
Stack `gameplay-validation` for what a session may prove. This file is how to *drive* it.
## Do not reuse the pack CI
`tools/skill_test_runner.py` only checks Markdown/JSON contracts. A green GitHub Action is not a clear. The harness lives in the *game* repo.
## Bind six probes (engine adapter)
The game exposes, on the logic tick:
| Probe | Harness uses it for |
|---|---|
| `poll_input(frame)` | inject a tape row or pass through human |
| `now_logical_frame()` | identity of the tick |
| `play_pose` | optional visual; never the pass/fail |
| `query_hits()` | contact log after the tick |
| `apply_knockback` | observe, do not author |
| `juice_hook` | must be off or ignored in logic asserts |
Also publish: seed, save slot, difficulty column, cheat flags, device.
## Tape format
One JSONL row per logic frame:
```json
{"f": 1204, "held": ["jump"], "press": ["light"], "axis": {"x": 0.2, "y": 0}}
```
Rules: frame ids are logical, not wall-clock. Analog is quantized. Do not store rendered pixels in the tape. Video is `gameplay-capture`, linked by session id.
## Modes
| Column | Driver | Cheats | May prove |
|---|---|---|---|
| logic | fixtures / function calls | yes | formulas, flags, save schema |
| replay | tape | off unless labeled | same seed + tape → same hit log |
| scripted | tape + teleport/god | on, named | one arena |
| soak | idle + random legal inputs | off | leak / soft-lock over N minutes |
| human | real device | off | new-player-watch / real-input |
Teleport or phase-skip forces `scripted`. The runner writes that mode into the report even if the operator forgets.
## Session report
```json
{
"build": "<git sha>",
"mode": "replay",
"cheats": [],
"seed": 7,
"chain": {
"boot": "pass",
"teach": "pass",
"challenge": "fail",
"retry": "not-run"
},
"bucket": "game",
"evidence": "sessions/2026-09-13-a/hits.jsonl"
}
```
`bucket` is only `game` | `player` | `setup` | `harness`. A lost browser tab is `harness`. A missed parry is `player` until a tape reproduces a box error — then `game`.
## Implementation sketch
```
game-repo/
harness/
adapter.ts # bind the six probes
runner.ts # step frames, write report
tapes/ # committed golden inputs
sessions/ # gitignored evidence
```
Loop:
1. Load save + seed + mode.
2. For each tape row: `poll_input` → tick → append `query_hits`.
3. Compare hit log / flags / hp to the golden file.
4. Diff mismatch → fail. Do not "fix" by stretching stun.
5. Human mode: record a new tape instead of asserting.
Determinism check: replay the same tape at two render cadences. Logical rows must match (`custom` accept test).
## Rules
1. Juice, camera, and particles are not oracles.
2. Harness must not own hitstop length.
3. A soak that uses god mode is scripted, not soak.
4. CI in *this* pack stays contract-only. Game CI may call the harness on tapes labeled `replay`.
Accept: one command can replay a named tape and print mode, cheats, and which chain box failed. A stranger can tell harness failure from a game defect.