# playcal

> Open-source toolkit for AI agents working on games (Unity first). Python CLI, every command supports --json. Pre-alpha: `ingest`, `report`, `calibrate`, `logs`, `query`, `snap`, `observe`, `replay`, `baseline`, `visual`, `tests`, `affected`, `lock`, `checkpoint`, `doctor`, `mcp`, `skill` and the Unity SDK work today; `loop` is early.

## Install
- `pipx install playcal` (Python 3.10+)
- Check: `playcal modules --json` lists the installed modules.
- MCP server: `claude mcp add --transport stdio playcal -- playcal mcp`. Tools are named `playcal_<module>`.
- Skill file instead of MCP: `playcal skill > .claude/skills/playcal/SKILL.md`.

## Commands that work
- `playcal ingest FILE... [--paste] [--out accepted.jsonl] [--rejected rejected.jsonl] [--json]`: validates gameplay events against schema/event-v0.json. `--paste` pulls `PC0:` blobs out of free text such as a chat export. Exit 0 = ran, 2 = file error.
- `playcal report FILE... [--json]`: per-level pass rate per player and per attempt with 95% Wilson CI, quit rate, median attempts to pass. Feed it ingest's accepted.jsonl.
- `playcal loop settle [--project-path P] [--timeout S] [--json]`: waits until the Unity Editor reports ready on two polls in a row. Reasons: ready (exit 0), timeout / no_editor (3), safe_mode / editor_blocked (4: Unity is up but can't take commands, usually compile errors or a dialog; a person must act), ambiguous (2, pass --project-path), compile_error (6, with --compile). Busy (compiling, reloading) shows up as no instance in `unity status`; settle treats that as not ready.
- `playcal loop run NAME [--project-path P] [--verify "READCMD ARGS"] [--expect PATH=VALUE]... [--diff] [--compile] [--reload-wait S] [--checkpoint] [--json] -- PARAMS...`: settle, `unity command NAME PARAMS`, settle again, optionally recompile, re-read and assert. Command names are two words, quoted: "gameobject create", "scene get_hierarchy". Expect paths start at the command's data, e.g. `result.roots.*.name=Probe`. Digest has `ok`, `reason`, `fault` (infra = retry, task = fix your command), `checks`, `diff`, `compile`. Exit 5 = verify_failed, 6 = compile_error. Read, write and --expect checked against a live Unity 6000.3 Editor.
- `playcal checkpoint save [MSG] | list | diff ID | restore ID [--repo P] [--json]`: file snapshots on refs/playcal/checkpoints; never moves HEAD or the index. IDs: short sha, latest, ~N. restore saves the current state first and returns `undo`. `loop run --checkpoint` saves before the write.
- `playcal query [FILE|-] [--unity "CMD ARGS"] [--from PATH] [--where EXPR]... [--select PATH]... [--limit N] [--json]`: returns `count`, `items`, `bytes_in`, `bytes_out`. Paths: dotted, `*` any item, `**` any depth. Use it instead of reading a full dump.
- `playcal snap save NAME [FILE|-] [--unity "CMD"]`, `playcal snap diff A [B] [--unity "CMD"] [--ignore KEY]... [--json]`, `playcal snap list`: save a read, then get only what changed (`same`, `added`, `removed`, `changed`, `changes`). instanceIds are ignored, objects are matched by hierarchyPath, duplicates become name#2. diff exits 1 when something changed.
- `playcal observe [--seconds 3] [--read "CMD"] [--expect PATH=VALUE]... [--no-play] [--shots DIR] [--name N] [--no-shot] [--project-path P] [--json]`: enters Play Mode, waits for `editor status` to say playing, runs the game --seconds, then reports console errors logged since the start (`console.errors_seen`, `console.first_cause`), `checks` on the read during play, and `diff` of the scene before vs during play (reported, not gated). Red runs (exit 1: reason errors, expect_failed, read_failed, play_failed) save a Game view screenshot under `shot`; exit 3/4 = Editor problem like loop; exit 3 with reason `console_unread` = the console wasn't read, or was cleared or overflowed, so errors may have been missed (not clean). Always leaves Play Mode it started, even on failure; an Editor already playing or paused is left as it was. `played_ms` {min, max} = how long the game ran before the second read. A clean run takes ~33 s on a live Editor: every `unity` call costs 3–5 s, so the reads are batched through `job batch` (7 calls per run).
- `playcal replay record NAME [--frames 600] [--fps 60] [--seed 0] [--force]`, `playcal replay run NAME [--stop-at FRAME] [--tolerance 1e-4]`, `playcal replay step [--frames 1]`, `playcal replay check NAME | A B` (all take `--project-path P --json`): needs the SDK and input read through `PlaycalInput.Axis/Button`, results noted with `PlaycalReplay.Mark(name, value)`. record/run press Play, wait for the SDK's file in `<project>/.playcal/replays/`, leave Play. run compares marks: reason `match` (exit 0), `diverged` with `first_divergence` {frame, expected, got}, `ended_early`, `errors` (console errors during the run), `no_marks` (nothing marked, nothing compared) (exit 1); `console_unread` (exit 3); `no_sdk` = the request was never read. `--stop-at` leaves the Editor paused on that frame (reason `paused`); `step` advances it and returns `frame`. Whole-number marks compare exactly, `--tolerance` is for decimals. Fixed Time.captureFramerate and UnityEngine.Random seed; physics matched exactly on a live Editor in a small scene.
- `playcal baseline save NAME FILE`, `playcal baseline check NAME FILE [--percentile 95] [--tolerance 10] [--floor METRIC=VALUE]... [--metric M]... [--json]`, `playcal baseline show NAME`: perf gate on per-frame samples (`{"frame_ms": [...]}` or a bare list). check gives each metric a verdict (`slower`, `faster`, `same`, `inconclusive`) from a bootstrap 95% interval and exits 1 only when one is `slower`. Treat `inconclusive` as "run longer", not as a failure.
- `playcal tests check FILE... [--commit SHA] [--no-record] [--json]`, `playcal tests record FILE...`, `playcal tests flaky`: test history from NUnit3 (Unity -testResults) or JUnit XML. A test that passed and failed on the same commit is flaky; check exits 1 only on failures of non-flaky tests and lists flaky failures under `quarantined`. `playcal tests affected [PATH...] [--base REF] [--project DIR]` prints the `-runTests -assemblyNames` for the test assemblies a change can reach through the .asmdef graph; when `run_all` is true (a scene, prefab or setting changed) run everything.
- `playcal visual save NAME shot.png`, `playcal visual check NAME shot.png [--tolerance 8] [--max-changed 0.1] [--ignore X0,Y0,X1,Y1] [--json]`: screenshot gate. check exits 1 when too many pixels changed and gives `box` [x0,y0,x1,y1] plus `diff_png` (changed pixels red); open the diff image to see what moved. Exit 2: no baseline or not an 8-bit PNG.
- `playcal calibrate --human FILE... --bot FILE... [--min-attempts 5] [--json]`: predicted human attempt pass rate per level from bot runs, fit on levels a human panel played (random-effects logit regression). Each level has `predicted`, `ci` (95%), `calibration` (true if humans played it) and `human_pass_rate` when seen. `leave_one_out` gives `inside_interval`/`levels` and `median_abs_error`; it is null under 4 calibration levels. Exit 2: fewer than 3 calibration levels or no valid events.
- `playcal doctor [--project P] [--json]`: checks Python, git, the Unity CLI, open Editors (running, blocked by a dialog, Safe Mode) and whether P has the SDK. Run it when a loop command fails before anything happened; exit 1 means a check failed.
- `playcal lock take PATH... --owner NAME [--ttl 30m]`, `playcal lock check PATH...`, `playcal lock release PATH... --owner NAME`, `playcal lock list`: advisory locks for scenes and assets when several agents share a project. Take the lock before you edit a scene. take is all or nothing and exits 1 with `held: {path: {owner, expires_in}}` when someone else has it; wait or pick other work.
- Unity SDK perf: `PlaycalFrames.Begin("name")` in play mode, then `Save()` writes `persistentDataPath/playcal/frames-name.json` for `playcal baseline`. Editor GC numbers include Editor overhead; baseline a player build.
- Unity SDK screenshots: `yield return PlaycalShots.Capture("name")` or `PlaycalShots.CaptureCamera("name", cam)` writes `persistentDataPath/playcal/shots/name.png` for `playcal visual`. In batch mode Capture renders Camera.main (no overlay UI); `-nographics` can't capture.
- `playcal logs [PATH|-] [--since TEXT] [--limit N] [--json]`: summarizes a Unity log; `first_cause` is the error to fix first. Exit 1 = errors found.
- Unity SDK: add `https://github.com/RizgarOzan/playcal.git?path=/unity/com.playcal.sdk` in Package Manager. API in unity/com.playcal.sdk/README.md.

## Docs
- [README](README.md): what it is, roadmap, live benchmark (writes during a domain reload: plain CLI 0/3 landed, loop run 3/3; loop is ~6x slower)
- [CONTRIBUTING](CONTRIBUTING.md): setup and ground rules
- [Event schema](schema/event-v0.json): JSON Schema 2020-12, unknown fields rejected
- [Privacy](PRIVACY.md): what is and isn't recorded
- [Module contract](src/playcal/registry.py): how to add a module (entry point group `playcal.modules`)

## Rules for agents
- Don't edit the user's project files during install.
- Don't add fields to events. The schema rejects them, and personal data is not allowed.
