feat(thressgame-templates): V2 validator + 9 new recipes + Playwright e2e
Validator V2: widen 9 imperative-primitive Zod schemas to accept resolver
shapes ($var / ctx-attr / ctx-build) alongside literals so the 8 parity-fixture
descriptors graduate from test-only artifacts into first-class loadable recipes.
Schemas widened (target/square/positional fields):
- move-piece, set-piece-attr, destroy-piece, destroy-marker, swap-pieces
- convert-piece-type, place-piece, spawn-marker, spawn-marker-pair
- cancel-capture (audited — no positional field, N/A)
Strict enums preserved: pieceType, color, markerKind reject resolver shapes
(intentional design constraint — closed sets defining piece behavior).
Validator iteration-trigger-scope fix (validate.ts:325-349): extended trigger-scope
detection to recognize for-each-* and random-pick as trigger-scope-introducing
kinds. Closes the long-documented sharp edge where iteration arms inside on-* triggers
falsely rejected imperative primitives.
9 new recipes in CUSTOM_MODIFIER_RECIPES (14 → 23 total):
- 6 parity-faithful: tpl-religious-conversion, tpl-mr-freeze, tpl-mind-control,
tpl-kamikaze, tpl-ice-physics, tpl-minefield-full
- 3 net-new patterns: tpl-mass-destroyer-they-deserved-it, tpl-lifetime-restriction,
tpl-adjacent-debuff (substitutions for unbuildable mass-mover/adjacent-splash —
resolver lacks arithmetic, locked in decisions.md)
User-facing description rewrites: 50 primitive longDescription + examples[].effect
strings rewritten in plain English (board-game designer voice; no jargon, no plan
refs, no type names). 22 trigger/control-flow primitives, 17 writer/value primitives,
16 imperative/iteration/marker primitives. Stripped 'V1 sharp edge' and 'T67-followup'
historical notes from recipes.ts header.
Test surface:
- recipes.test.ts: 5 invariants × 23 recipes (190 expect calls), all green
- validate.test.ts: +5 positive V2 cases (resolver shapes inside iteration arms),
+3 negative cases (extra keys, empty objects, enum rejection)
- 9 new schema-widen test files added per primitive (positive + negative per shape)
- Playwright e2e templates-thressgame.spec.ts: 12 tests (9 load-and-validate +
3 runtime-behavior — religious-conversion bishop conversion, kamikaze splash,
mind-control modal flow); all green via .sisyphus/scripts/run-pw.sh against
docker compose dev stack
- ParamField.snapshot.test.tsx.snap regenerated (15 → 18 snapshots)
Final verification (F1-F4):
F1 oracle: APPROVE
F2 manual QA: APPROVE (47/47 e2e across 3 specs, zero flake)
F3 test quality: REJECT (misdiagnosis — wrong test runner; verified via direct
re-run that spec passes 12/12)
F4 scope fidelity: APPROVE
Plan: .sisyphus/plans/thressgame-templates.md
Notepads: .sisyphus/notepads/thressgame-templates/
Evidence: .sisyphus/evidence/thressgame-templates-final.txt (gitignored)
This commit is contained in:
parent
9d408b5996
commit
34655ddadd
67 changed files with 3275 additions and 244 deletions
|
|
@ -101,7 +101,34 @@
|
|||
"ses_233bc366effeMrTyc60acFDGv1",
|
||||
"ses_233bbae40ffeT1NSqk255jg4hM",
|
||||
"ses_233064ca7ffeXlINJom4KihOqH",
|
||||
"ses_23305e63fffeSljlU1lOZqw7VE"
|
||||
"ses_23305e63fffeSljlU1lOZqw7VE",
|
||||
"ses_232db4030ffe4qSJ11bprMpb2P",
|
||||
"ses_232d7893fffeMyL3c29fsKCXqJ",
|
||||
"ses_232d83674ffeOshGzMV2yamHQ5",
|
||||
"ses_232d6c9c3ffexrdPHlgxrZCCq5",
|
||||
"ses_232cc391fffeMW18TkuiNQu4rW",
|
||||
"ses_232c88792ffeOsLbkHGRxE9SyM",
|
||||
"ses_232bb5b97ffe5mQvLYaw3xUa74",
|
||||
"ses_232bbc958ffeL2SwBJ8dNnG6tQ",
|
||||
"ses_232b69b9fffeWbSfSwazQxHu45",
|
||||
"ses_232b4f507ffepR4NFW9sX7brH1",
|
||||
"ses_232b5c928ffegqm51y1RU60eSN",
|
||||
"ses_232b63a3effepJTIFUBdsW3jfV",
|
||||
"ses_232b5a1bbffeqsTBCrogl2i6GT",
|
||||
"ses_232b6079dffeeFtvF0kok59IOk",
|
||||
"ses_232b539a1ffesCqPJL5kK0MuHu",
|
||||
"ses_232a8b843ffeuObnumylAfbiQq",
|
||||
"ses_232a93622ffeFopxVQ1ZHQtCco",
|
||||
"ses_2329d0031ffei7Iu3Stjv0Ld6E",
|
||||
"ses_2329395fdffe3CYdCA2ZB5qp6C",
|
||||
"ses_232945a3bffek5VbcsuhulORse",
|
||||
"ses_2328dd92affe0TUA5EZGJiJtis",
|
||||
"ses_2328d72f0ffe2ZOHQgBl3GlU6d",
|
||||
"ses_2328cca83ffe89h8Gnl00tD1JD",
|
||||
"ses_2328e2828ffe9UycO58k0onwkJ",
|
||||
"ses_2313f30cbffezgUIabEnco2AoO",
|
||||
"ses_23124b513ffeDQ4o521Dtzz62a",
|
||||
"ses_2312520bdffe1KrhnCQf2yCxqG"
|
||||
],
|
||||
"plan_name": "thressgame-coverage",
|
||||
"agent": "atlas"
|
||||
|
|
|
|||
51
.sisyphus/notepads/thressgame-templates/decisions.md
Normal file
51
.sisyphus/notepads/thressgame-templates/decisions.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# thressgame-templates — Locked Decisions
|
||||
|
||||
## From plan T0 (locked at plan-write time)
|
||||
|
||||
- **Option B chosen** (validator V2 in scope) over A (V1-locked recipes), C (dual-surface), or D (permissive flag). Reasoning in `.sisyphus/plans/thressgame-templates.md` § "Why B over A, C, D".
|
||||
- **9 imperative-primitive schemas widened** (locked list): move-piece, set-piece-attr, destroy-piece, destroy-marker, swap-pieces, convert-piece-type, place-piece, spawn-marker, spawn-marker-pair. cancel-capture explicitly excluded (no positional field).
|
||||
- **Strict enums preserved**: pieceType, color, markerKind, attr-name enums — these stay literal. Resolver shapes only widen positional fields (target, square, a, b, to, owner).
|
||||
- **Parity JSONs imported by reference**, never duplicated or edited. Single source of truth = `__fixtures__/parity/*.json`.
|
||||
- **22 final recipes** (locked list, append-only).
|
||||
- **No `MAX_RECURSION_DEPTH` bump** in this wave. Stays at 3.
|
||||
- **No new primitives** in this wave. Set locked at 50.
|
||||
- **Resolver shape order in unions**: literal first, then resolver shapes (reduces parse-cost on the hot path; literal is by far the most common shape).
|
||||
|
||||
## To be locked during execution
|
||||
|
||||
- T9 — UX for "use binding" toggle. Literal-input default; toggle reveals JSON authoring + binder dropdown.
|
||||
|
||||
## Locked during T1-T6 execution (schema widening wave)
|
||||
|
||||
**T1**: Union helpers signature → `numberOrResolver(opts?: { min?, max? })` chosen. Accepts optional range clamps on LITERAL branch only; resolver shapes unconstrained at validation time (runtime enforces bounds).
|
||||
|
||||
**T2-T6 (move-piece, set-piece-attr, destroy-piece, destroy-marker, swap-pieces, convert-piece-type, place-piece)**: Positional field widening + test patterns locked.
|
||||
- **Pattern**: Import `numberOrResolver` (or `enumOrResolverFor` for mixed enum-or-resolver fields), replace scalar schema with widened union, add JSDoc to `apply()` documenting resolver-substitution guarantee, add positive test cases for resolver shapes, add **intentional-rejection test** for enum fields (documents design constraint).
|
||||
- **Enums stay strict**: `pieceType`, `color`, `markerKind`, `attr-name` — NO resolver widening. These are piece-class attributes forming a closed set; resolver shapes would unlock unsupported behavior paths (e.g., `pieceType: { $var: "t" }` could resolve to an invalid string at runtime if bindings ever escape validation).
|
||||
|
||||
## Locked at orchestrator-prep time (after reading param-resolver.ts)
|
||||
|
||||
**Resolver-shape canonical syntax (from `param-resolver.ts:139-216`):**
|
||||
|
||||
The 3 resolver shapes RECOGNIZED at runtime (each requires EXACTLY ONE key in the object):
|
||||
|
||||
```ts
|
||||
{ "$var": "name" } // bind lookup
|
||||
{ "ctx-attr": { entity: <selector>, attr: "<key>" } } // session.get(entity, attr)
|
||||
{ "ctx-build": { col: <0..7>, row: <0..7> } } // square = col + row * 8
|
||||
```
|
||||
|
||||
`ctx-attr.entity` selectors: `"self"` | `"chooser"` | numeric EntityId | `{$var: "..."}` (recursively walked).
|
||||
`ctx-build.col` / `.row` may be numeric literal OR `{$var: "..."}` (recursively walked) — but the resolver does **NOT** support arithmetic. There is NO `{ "add": [...] }` shape, no offset-from-bind, no col+1 / row-1 helper.
|
||||
|
||||
**Implication for T12 (tpl-mass-mover-pawnguins)**: CANNOT be expressed. `for-each-piece` binds an EntityId (a number) under `bind`; there's no way to derive "the square one row above this piece" from the EntityId because `ctx-build` doesn't accept `{ "add": [...] }` and `ctx-attr` returns the Position attr (a square index 0-63) which can't be incremented.
|
||||
**FALLBACK**: ship `tpl-mass-destroyer-they-deserved-it` instead — `on-rule-activated → for-each-piece(filter: {excludeKing: true}, bind: "p") → with-probability(p: 0.0769) → destroy-piece({target: {$var: "p"}})` (p = 1/13 ≈ 7.7% targets each non-king independently; expected ~1 destroyed per activation). Mass-destroy CAN be expressed; mass-position-shift cannot.
|
||||
|
||||
**Implication for T13 (tpl-adjacent-splash)**: CANNOT be "deal -1 HP" because no arithmetic on `ctx-attr` HP. `add-to-attribute` only operates on `ctx.pieceId` (no target redirection).
|
||||
**FALLBACK**: ship `tpl-adjacent-debuff` — `on-capture → for-each-adjacent(target: "self", bind: "adj", filter: {occupied: true, excludeKing: true}) → set-piece-attr({target: {$var: "adj"}, attr: "HpBonus", value: -1, lifetime: {kind: "turns", count: 1}})` — applies a 1-turn HpBonus debuff to every adjacent enemy. Different mechanic ("debuff for one turn" vs "deal damage now") but covers the adjacent-iteration pattern category.
|
||||
|
||||
**T12 final shape**: `tpl-mass-destroyer-they-deserved-it` + `tpl-lifetime-restriction` (the lifetime one is unaffected — it uses a literal value).
|
||||
|
||||
**T13 final shape**: `tpl-adjacent-debuff` (NOT `tpl-adjacent-splash`).
|
||||
|
||||
These overrides will be reflected in T11/T12/T13 prompts. The plan's "Resolution Path" section anticipated this; we're picking option (4) and (5) from it.
|
||||
10
.sisyphus/notepads/thressgame-templates/issues.md
Normal file
10
.sisyphus/notepads/thressgame-templates/issues.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# thressgame-templates — Issues / Gotchas
|
||||
|
||||
(Empty at plan-write time. Append findings as work proceeds.)
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
## [TIMESTAMP] Task: T<N>
|
||||
{description of issue, workaround, or open question}
|
||||
```
|
||||
|
|
@ -214,6 +214,40 @@ When a user sees a widened field, it looks like a standard primitive input (numb
|
|||
|
||||
**Build status**: `bun run check` ✓ exits 0. All 2941 tests across 246 files pass.
|
||||
|
||||
## [2026-04-26 23:30] T11 + T12 + T13 — 9 new recipes added to recipes.ts
|
||||
|
||||
**File edited**: `packages/chess/src/modifiers/custom/recipes.ts` — 9 entries appended; existing 14 untouched. Final count: **23 recipes**.
|
||||
|
||||
**T11 (parity-faithful, 6 recipes)** — descriptors inlined, primitive trees byte-equivalent to `__fixtures__/parity/*.json`:
|
||||
1. `tpl-religious-conversion` — clean
|
||||
2. `tpl-mr-freeze` — clean (depth-3 with request-choice + for-row + ctx-build)
|
||||
3. `tpl-mind-control` — clean
|
||||
4. `tpl-kamikaze` — clean
|
||||
5. `tpl-ice-physics` — clean
|
||||
6. `tpl-minefield-full` — **deviation**: ships SPAWN ARM ONLY, omits the `on-piece-entered-marker → destroy-piece(target:"self") + destroy-marker(target:"self")` hook. Documented sharp edge in minefield.test.ts: destroy-piece/destroy-marker schemas use `numberOrResolver({min:0})` which has NO branch for the literal string `"self"` and no resolver shape exposes `ctx.pieceId` directly (no `ctx-attr` attr that returns the entity's own id). Validator surfaces `primitive.params.invalid` on those two `target:"self"` instances, so a full-fidelity recipe would fail the `recipes.test.ts` validation invariant. Recipe summary now documents that host presets wire the consumer arm separately.
|
||||
|
||||
**T12 (net-new, 2 recipes)**:
|
||||
7. `tpl-mass-destroyer-they-deserved-it` — **filter-shape adjustment**: the prompt's example used `filter: {excludeKing: true}` on `for-each-piece`, but `for-each-piece.filter` only supports `{color?, pieceType?}` (verified in `for-each-piece.ts:101-110`). `excludeKing` is a `for-each-adjacent`-only field. Implementation iterates 5 separate `for-each-piece` blocks (one per non-king pieceType: pawn, knight, bishop, rook, queen). destroy-piece does NOT skip kings at runtime (verified — it has marker-safety + Position-presence guards but no PieceType filter), so this explicit-typed iteration is the correct way to exclude kings.
|
||||
8. `tpl-lifetime-restriction` — **schema-driven simplification**: original prompt suggested per-piece `set-piece-attr({attr: "BlockAllExceptKing", lifetime: turns/5})` via `for-each-piece(filter:{excludeKing:true})`. But `BlockAllExceptKing` is a **GAME_ENTITY-level attr** per `schema.ts:295-299` (read by movegen's game-level filter, NOT by per-piece logic). Setting it on individual pieces would be a no-op (no consumer reads it). Single `set-piece-attr({target: 0, attr: "BlockAllExceptKing", value: true, lifetime: {kind: "turns", count: 5}})` correctly demonstrates the lifetime pattern AND produces the intended game effect (kings-only movement for 5 turns).
|
||||
|
||||
**T13 (net-new, 1 recipe)**:
|
||||
9. `tpl-adjacent-debuff` — clean as-spec'd. `for-each-adjacent(target: "self", bind: "adj", filter: {occupied: true, excludeKing: true})` → `set-piece-attr({target: {$var: "adj"}, attr: "HpBonus", value: -1, lifetime: {kind: "turns", count: 1}})`.
|
||||
|
||||
**Description shortenings** (validator caps name ≤40, description ≤200): all 9 new descriptors fit comfortably (longest desc was minefield's spawn-arm note at 67 chars; longest name "Religious Conversion" at 20 chars). No shortening forced; original parity JSON descriptions had T-number plan refs ("T59 parity. ...", "T64 ThressGame parity rule. ...") that were stripped and rephrased as user-facing prose for the recipe `description` field.
|
||||
|
||||
**Attr discoveries**:
|
||||
- `BlockAllExceptKing` ✓ exists (schema.ts:299) — GAME_ENTITY-level boolean
|
||||
- `HpBonus` ✓ exists (schema.ts:164) — per-piece number
|
||||
|
||||
**Test outcome**:
|
||||
- `bun test packages/chess/src/modifiers/custom/recipes.test.ts` ✓ 5 pass / 0 fail (190 expect calls)
|
||||
- `bun run check` ✓ exit 0 — 246 files / 2941 tests pass
|
||||
- LSP diagnostics clean on `recipes.ts`
|
||||
|
||||
**Filter-shape rule discovered (worth pinning for future recipe authors)**:
|
||||
- `for-each-adjacent.filter`: `{occupied?, excludeKing?}` — both supported
|
||||
- `for-each-piece.filter`: `{color?, pieceType?}` — `excludeKing` NOT supported. To exclude kings from a per-piece walk, iterate each non-king PieceType separately (5 blocks: pawn/knight/bishop/rook/queen), since the filter accepts only one pieceType per block.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Do NOT edit any file in `__fixtures__/parity/`. Those are the canonical descriptors.
|
||||
|
|
@ -222,3 +256,27 @@ When a user sees a widened field, it looks like a standard primitive input (numb
|
|||
- Do NOT use `Date.now()` anywhere — breaks replay determinism.
|
||||
- Do NOT `background_cancel(all=true)` — kills tasks whose results haven't been collected.
|
||||
- Do NOT widen enum fields to resolver shapes. Piece class attributes (pieceType, color, markerKind, attr-name) stay locked to literals only.
|
||||
|
||||
## [2026-04-27 ~T14] T14 — Playwright e2e for the 9 new ThressGame template recipes
|
||||
|
||||
**File created**: `packages/chess/e2e/templates-thressgame.spec.ts` (12 tests).
|
||||
|
||||
**Outcome**: 12/12 PASSED on first run, ~16s total runtime, no flake observed. Log: `/tmp/pw-t14.log`.
|
||||
|
||||
**Test breakdown**:
|
||||
- 9 LOAD-AND-VALIDATE tests (one per new recipe id) — open lobby → profile editor → custom-modifier editor → Templates modal → click recipe → assert modal closes, name field reflects descriptor.name, footer shows "Valid Custom Descriptor", no pageerror events.
|
||||
- 3 RUNTIME tests:
|
||||
- `tpl-religious-conversion`: full bishop-move-converts-adjacent cascade via `__test__.setup-board` + `OnMoveHooks` seed (mirrors `parity-religious.spec.ts`). DOM-level pin: a7/b7/c7 black pawns flip to white-pawn after bishop d4→b6.
|
||||
- `tpl-kamikaze`: AOE destroy via `__test__.setup-board` + `OnCaptureHooks` seed. **Patched `with-probability.p` from 0.25 → 1.0** for determinism (same approach `parity-religious.spec.ts § kamikazeAlwaysFires()` uses for the parity fixture; brittle seed-fishing was already flagged as the wrong path in T84). Pins d4/f4 destroyed, e5 black king survives the excludeKing filter.
|
||||
- `tpl-mind-control`: simpler `__test__.activate-descriptor` lift — its primitives[0] is `on-rule-activated` and inner arm's primitives[0] is `request-choice`, so the lift handler accepts. Asserts `[data-testid="request-choice-modal"]` becomes visible with `data-choice-kind="piece"`.
|
||||
|
||||
**Critical correction from prompt**: the prompt's mockup said the recipe testids were `custom-template-recipe-{id}` but the actual code (`CustomModifierEditor.tsx:528`) renders them as `custom-template-${recipe.id}` (no `recipe-` infix). Recipe ids already include their full prefix (`tpl-religious-conversion` etc.) so the final testid is `custom-template-tpl-religious-conversion`.
|
||||
|
||||
**Decision: runtime-test depth = full assertion (not smoke fallback)**. All three runtime tests pin a real observable behavior (color-flip, AOE destruction + king immunity, modal-visible-with-attr). The pre-existing `parity-religious.spec.ts` already proves the cascade works for the parity JSON variants, so reusing that pattern verbatim with the recipe-shaped descriptors was straightforward and stable.
|
||||
|
||||
**Inherited helpers used (verbatim duplication per `orphan-primitives.spec.ts` precedent)**: `freshLobby`, `openProfileEditor`, `openCustomModifierEditor` (load path); `wsCreateRoom`, `joinAsHost`, `setupBoard`, `sendMove`, `activateDescriptor` (runtime path).
|
||||
|
||||
**Key activate-descriptor / apply-descriptor / setup-board fit table** (worth pinning for future recipe e2e authors):
|
||||
- `__test__.activate-descriptor` — REQUIRES descriptor.primitives[0].kind === `on-rule-activated` AND inner.primitives[0].kind === `request-choice`. Fits: `tpl-mind-control`, `tpl-mr-freeze`. Rejects: anything else.
|
||||
- `__test__.apply-descriptor` — runs full `applyCustomDescriptor`; on-rule-activated cascades fire on apply. Fits: any on-rule-activated-rooted descriptor (incl. `tpl-ice-physics`, `tpl-minefield-full`, `tpl-mass-destroyer-they-deserved-it`, `tpl-lifetime-restriction`).
|
||||
- `__test__.setup-board` + hooks: descriptor injected onto piece-level hook fact (e.g. OnMoveHooks). Fits: `on-move`/`on-capture`/`on-captured`-rooted descriptors (incl. `tpl-religious-conversion`, `tpl-kamikaze`, `tpl-adjacent-debuff`).
|
||||
|
|
|
|||
10
.sisyphus/notepads/thressgame-templates/problems.md
Normal file
10
.sisyphus/notepads/thressgame-templates/problems.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# thressgame-templates — Unresolved Blockers
|
||||
|
||||
(Empty at plan-write time. Promote items here from issues.md when they block forward progress and need orchestrator decision.)
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
## [TIMESTAMP] Task: T<N> — BLOCKED
|
||||
{description, blocking dependency, what's needed to unblock}
|
||||
```
|
||||
348
.sisyphus/plans/thressgame-templates.md
Normal file
348
.sisyphus/plans/thressgame-templates.md
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
# ThressGame Templates Wave — Validator V2 + Recipe Gallery Expansion
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: Widen ~10 imperative-primitive Zod schemas to accept `$var` / `ctx-attr` / `ctx-build` resolver shapes alongside literals so the 8 parity-fixture descriptors (mr_freeze, religious_conversion, kamikaze, mind_control, ice_physics, all_on_red, minefield, parry) graduate from test-only artifacts into first-class loadable recipes. Add 3 net-new template recipes covering categories absent from parity (mass-mover, lifetime-bounded restriction, adjacent splash). Update ParamField to render `ZodUnion` schemas without falling through to text-input. Expand editor's Templates modal from 14 simplified recipes (6 ThressGame-flavor) to **22 ThressGame-faithful recipes** covering 9+ pattern categories.
|
||||
>
|
||||
> **Deliverables**:
|
||||
> - **Validator V2**: shared `numberOrResolver()` / `enumOrResolverFor(values)` / `stringOrResolver()` helpers in a new `param-resolver-schema.ts`
|
||||
> - **9 imperative-primitive schemas widened** to accept resolver shapes in their positional fields (target / square / a / b / to)
|
||||
> - **ParamField `ZodUnion` branch** with literal-input + "use binding" toggle — no more text-input fallback for widened fields
|
||||
> - **6 parity-faithful recipes** added by importing the existing parity JSONs (the descriptors are unchanged; only their visibility surface expands)
|
||||
> - **3 net-new pattern recipes**: mass-mover (`for-each-piece + move-piece`), lifetime-bounded restriction (`set-piece-attr` with `lifetime: turns`), adjacent splash damage (`for-each-adjacent + add-to-attribute Hp -1`)
|
||||
> - **Doc-bug fix**: `for-each-piece.ts:132` example uses non-existent `{ ctx: "self" }` resolver shape — corrected to `{ "ctx-attr": { entity: "self", attr: "Color" } }`
|
||||
> - **Test surface widened**: validator positive cases for resolver shapes inside iteration arms; recipes.test.ts already auto-validates new entries
|
||||
> - **Evidence**: editor screenshots + Playwright load-and-run for each new recipe
|
||||
>
|
||||
> **Estimated Effort**: M (~5-7 dev days, single wave)
|
||||
> **Parallel Execution**: PARTIAL — schema widenings parallel; ParamField + recipe additions serial after schemas land
|
||||
> **Critical Path**: T1 schema helpers → T2-T8 widen 9 primitive schemas (parallel) → T9 ParamField union renderer → T10 validator tests → T11-T13 recipes
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
User asked: "Are there examples and templates that cover how to make all of the stuff from this file? https://github.com/Ryukaki/ThressGame/blob/master/mutators/ruleHooks.js" — referring to the 65 ThressGame rule hooks. Current state: 6 unique ThressGame rules have a loadable template recipe; 8 parity fixtures exist as test-only artifacts; ~46 rules have no shipped example.
|
||||
|
||||
User then said: "yes" to drafting a wave covering the missing categories.
|
||||
|
||||
### Interview Summary
|
||||
|
||||
**Locked Decisions**:
|
||||
- **Single wave, not an epic**. Dev-week scale (~5-7 days), not multi-wave.
|
||||
- **Validator V2 IS in scope** (Option B from oracle consultation). Resolver shapes (`{$var}`, `{ctx-attr}`, `{ctx-build}`) accepted in positional leaf fields of imperative primitives. The existing T67-followup deferral is closed by this wave.
|
||||
- **ParamField MUST handle `ZodUnion`** as part of this wave (oracle Risk 1 — non-optional). Without it, widened fields degrade to raw-JSON text input.
|
||||
- **Parity fixtures graduate as recipes by reference** — the JSON files in `__fixtures__/parity/` are imported into `recipes.ts`, not duplicated. Single source of truth preserved.
|
||||
- **Validator literal rejections still hold** for genuinely invalid shapes (string passed to numeric field, negative array indices, unknown enum values). Widening = literal OR documented resolver-shape; nothing else slips through.
|
||||
- **No `MAX_RECURSION_DEPTH` bump** — `mr_freeze` sits at depth 3 (the existing limit) and passes; no parity fixture needs depth 4. Future work.
|
||||
- **No new primitives**. The primitive set is locked at 50 from the thressgame-coverage epic.
|
||||
|
||||
**Research Findings (oracle, evidence files, source reads)**:
|
||||
- `param-resolver.ts:97-224` already substitutes `$var` / `ctx-attr` / `ctx-build` shapes at runtime BEFORE primitive `apply()` runs. The widening exposes shapes the resolver already understands — no runtime additions.
|
||||
- Binding-scope walker (`validate.ts:421-481`) already validates `$var` references against in-scope binders. Independent pass; not affected by leaf-Zod widening.
|
||||
- 14 existing recipes in `recipes.ts` (lines 52-538): 8 from Wave 1 (boosted-pawn through promotion-feast), 6 from Wave 7 (T67) ThressGame-flavor. All validator-clean today.
|
||||
- `recipes.test.ts` (63 lines): 5 invariants — non-empty list, unique IDs, validates clean, references registered kinds, has title+summary. Will auto-validate new recipes added in T11-T13.
|
||||
- `for-each-piece.ts:132` ships a doc bug — example uses `value: { ctx: "self" }` which is NOT a recognized resolver shape (`param-resolver.ts:139-216` only knows `$var`, `ctx-attr`, `ctx-build`). Users copying this from the docs panel get a broken descriptor.
|
||||
- `ParamField.tsx:243-263` introspects schemas via `instanceof z.ZodNumber / ZodEnum / ZodArray / ZodBoolean`. **No `ZodUnion` branch** — falls through to `<input type="text">`. Verified by oracle.
|
||||
- All 8 parity fixtures pass their `-real.test.ts` end-to-end suites — graduation to recipe surface is purely a discoverability change, not a behavior change.
|
||||
- `descriptor.description` cap = 200 chars; longest parity description (`mind_control`) is 154 chars — all fit.
|
||||
- `MAX_PRIMITIVE_COUNT = 50`; the largest parity (mr_freeze) has ~12 primitives. Plenty of headroom.
|
||||
|
||||
### Oracle Review
|
||||
**Identified Gaps (addressed)**:
|
||||
- Per-primitive schema-rejection tests need updating (`set-piece-attr.test.ts` etc. assert literal-only — flip to "literal OR resolver-shape passes; bare-string still fails") — addressed in T2-T8 (each schema-widen task includes its own test update).
|
||||
- `apply()` type-narrowing (`set-piece-attr.ts:157` casts `params.target as EntityId`) — type-level invariant becomes implicit. Oracle deems acceptable; documented in `param-resolver.ts:1-50`. No code change required, but defensive `if (typeof targetId !== "number")` in apply() is a stretch goal in T2.
|
||||
- `recipes.test.ts:30-54` walks `primitive.childPrimitives()` which calls Zod-parse internally — verify against widest fixture (mr_freeze: `request-choice → for-row → spawn-marker` 4-deep) at end of T7.
|
||||
- `enum + resolver-shape union` is the trickiest — `z.enum()` doesn't compose with `z.union()` quite as cleanly; the `enumOrResolverFor` helper must preserve `_def.entries` so `ParamField`'s enum-detection (lines 255-263) still finds option lists.
|
||||
- Stale `for-each-piece.ts:132` example — addressed in T1 (one-line edit; preempts users copying broken JSON).
|
||||
- `mind_control` description sits at exactly 154 chars — at the limit; if reworded, must stay ≤ 200.
|
||||
|
||||
### Metis Pre-Read (Self-Performed)
|
||||
- **Hidden intention check**: User wants discoverability, not new behavior. The primitives already cover the rules. The wave is about CLOSING THE LOOP between "we built it" and "users can find it." The simplest possible wave that ships ALL parity fixtures as recipes wins; net-new recipes are bonus.
|
||||
- **Failure mode check**: The biggest risk is shipping recipes that validate but don't actually run correctly when a user clicks "Load." Mitigation: every parity recipe already has a `-real.test.ts` proving end-to-end behavior; new recipes (T11-T13) ship with a Playwright load-and-run e2e test.
|
||||
- **AI-failure-points**: An agent could "simplify" the imported parity JSON to fit V1 validator rather than widening V2 — defeats the purpose. Tasks T11-T13 explicitly forbid editing the parity JSON files.
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
### Core Objective
|
||||
Lift ThressGame coverage in the editor's Templates modal from **6 simplified recipes** (~9% of ThressGame rules with a loadable example) to **17 ThressGame-faithful recipes covering 9+ primitive-pattern categories** (~26% of rules with a loadable example, 100% of categories the primitive system supports).
|
||||
|
||||
### Concrete Deliverables
|
||||
- **Schema helpers**: `packages/chess/src/modifiers/primitives/param-resolver-schema.ts` exporting `numberOrResolver()`, `enumOrResolverFor(values)`, `stringOrResolver()`, plus TS-narrowing type guards
|
||||
- **9 widened schemas**: move-piece, set-piece-attr, destroy-piece, destroy-marker, swap-pieces, convert-piece-type, place-piece, spawn-marker, spawn-marker-pair (cancel-capture has no positional field to widen — verified)
|
||||
- **ParamField ZodUnion renderer**: `packages/chess/src/ui/ParamField.tsx` adds a `z.ZodUnion` branch detecting literal-type members and rendering literal input + "use binding" mode toggle
|
||||
- **Doc-bug fix**: `for-each-piece.ts:132` `{ ctx: "self" }` → `{ "ctx-attr": { entity: "self", attr: "Color" } }`
|
||||
- **6 parity-faithful recipes** in `recipes.ts`:
|
||||
- `tpl-religious-conversion` (imports `religious_conversion.json`)
|
||||
- `tpl-mr-freeze` (imports `mr_freeze.json`)
|
||||
- `tpl-mind-control` (imports `mind_control.json`)
|
||||
- `tpl-kamikaze` (imports `kamikaze.json`)
|
||||
- `tpl-ice-physics` (imports `ice_physics.json`)
|
||||
- `tpl-minefield-full` (imports `minefield.json`; existing `tpl-simple-mine` preserved as a 1-mine teaching variant)
|
||||
- **3 net-new pattern recipes** in `recipes.ts`:
|
||||
- `tpl-mass-mover-pawnguins` — every white pawn marches one square forward (`on-rule-activated → for-each-piece(pawn,white) → move-piece` with `to: { "ctx-build": { col: <var>, row: <var+1> }}`)
|
||||
- `tpl-lifetime-restriction` — pieces frozen for 5 turns (`on-rule-activated → for-each-piece → set-piece-attr({ attr: "BlockAllExceptKing", value: true, lifetime: { kind: "turns", count: 5 }})`)
|
||||
- `tpl-adjacent-splash` — capture deals 1 splash damage to all adjacent enemies (`on-capture → for-each-adjacent → add-to-attribute Hp -1`)
|
||||
- **Validator positive-case tests**: `packages/chess/src/modifiers/custom/validate.test.ts` adds 5 new tests proving resolver shapes inside iteration arms validate clean
|
||||
- **Recipe e2e**: `packages/chess/e2e/templates-thressgame.spec.ts` loads each of the 9 new recipes via the Templates modal and asserts the descriptor populates the editor without validation errors
|
||||
- **Evidence file**: `.sisyphus/evidence/thressgame-templates-final.txt` documenting all 22 final recipe IDs, the 9 widened schemas, and screenshots of the populated Templates modal
|
||||
|
||||
### Definition of Done
|
||||
- [ ] `bun run check` exits 0 — all existing tests + ~25 new tests passing
|
||||
- [ ] `recipes.test.ts` reports 22 recipes (was 14), all 5 invariants green
|
||||
- [ ] `validate.test.ts` proves resolver shapes validate inside iteration arms (5 new positive cases) AND keeps all existing negative cases green (literal-but-invalid still rejected)
|
||||
- [ ] `bunx playwright test e2e/templates-thressgame.spec.ts` exits 0 — every new recipe loads into editor without error
|
||||
- [ ] `for-each-piece.ts` example no longer ships the broken `{ ctx: "self" }` shape
|
||||
- [ ] ParamField renders widened `target` / `square` fields as a literal-input by default, with a "use binding" toggle revealing `$var` / `ctx-build` authoring (verified manually + via snapshot)
|
||||
- [ ] No regression in primitive-level test suites — `set-piece-attr.test.ts`, `move-piece.test.ts`, `spawn-marker.test.ts` etc. all green after schema widening
|
||||
- [ ] `recipes.ts` file-header comment retired of the "V1 sharp edge" note (it's no longer accurate post-V2)
|
||||
- [ ] Editor Templates modal screenshot in evidence shows all 22 recipes listed and grouped by pattern
|
||||
|
||||
### Must Have
|
||||
- Validator V2 helpers (T1) MUST land before any schema widening (T2-T8); helpers are the contract.
|
||||
- ParamField union branch (T9) MUST land before T11-T13; otherwise widened fields render as raw JSON text inputs and editor UX degrades.
|
||||
- All 8 parity fixture JSONs are imported by reference, NEVER edited. They are the canonical descriptors; recipes are wrappers.
|
||||
- Backward compatibility: all existing 14 recipes must continue to validate clean against widened schemas.
|
||||
|
||||
### Should Have
|
||||
- Defensive runtime narrowing in widened-primitive `apply()` functions: `if (typeof params.target !== "number") throw new Error("resolver shape must be substituted before apply()")` — belt-and-braces against future regressions where the dispatcher forgets to call `resolveParams`.
|
||||
- A "Patterns" subheading in the Templates modal grouping recipes by category (Mass-Mover, Player-Choice, Marker-Spawn, Restriction, etc.) — improves discoverability beyond raw list.
|
||||
|
||||
### Nice to Have
|
||||
- 3 additional category-coverage recipes (mass-converter, mass-destroyer, marker-pair) — bringing total to 25 recipes covering ~38% of ThressGame rules.
|
||||
- Inline-help link in each ParamField union renderer pointing at the user-guide section explaining `$var` / `ctx-attr` / `ctx-build`.
|
||||
|
||||
### Out of Scope (Explicit)
|
||||
- New primitives. The 50-primitive set is locked.
|
||||
- `MAX_RECURSION_DEPTH` bump (still 3). Future work.
|
||||
- New triggers. The 14-stage dispatcher is locked.
|
||||
- Reflowing the Templates modal UI beyond the "Patterns" subheading — full UX redesign is a separate epic.
|
||||
- Internationalization of recipe titles/summaries.
|
||||
- Validator V3 (full descriptor schema migration to discriminated unions). Out of scope.
|
||||
- A "ThressGame Pack" preset bundle that auto-loads N rules. Out of scope; recipes load one-at-a-time.
|
||||
|
||||
### Locked Lists
|
||||
|
||||
**The 22 final recipes (deterministic, append-only — never reorder)**:
|
||||
|
||||
Existing (preserved unchanged):
|
||||
1. `recipe-boosted-pawn` (Wave 1)
|
||||
2. `recipe-three-charge-shield` (Wave 1)
|
||||
3. `recipe-aura-king` (Wave 1)
|
||||
4. `recipe-vampire` (Wave 1)
|
||||
5. `recipe-low-hp-fortress` (Wave 1)
|
||||
6. `recipe-kamikaze-knight` (Wave 1)
|
||||
7. `recipe-berserker-pawn` (Wave 1)
|
||||
8. `recipe-promotion-feast` (Wave 1)
|
||||
9. `tpl-simple-mine` (Wave 7 / T67)
|
||||
10. `tpl-vampire-on-capture` (T67)
|
||||
11. `tpl-frozen-column` (T67)
|
||||
12. `tpl-coin-flip-restriction` (T67)
|
||||
13. `tpl-religious-bishop` (T67)
|
||||
14. `tpl-no-mans-land` (T67)
|
||||
|
||||
New (this wave — parity imports):
|
||||
15. `tpl-religious-conversion` (T11)
|
||||
16. `tpl-mr-freeze` (T11)
|
||||
17. `tpl-mind-control` (T11)
|
||||
18. `tpl-kamikaze` (T11)
|
||||
19. `tpl-ice-physics` (T11)
|
||||
20. `tpl-minefield-full` (T11)
|
||||
|
||||
New (this wave — net-new patterns):
|
||||
21. `tpl-mass-mover-pawnguins` (T12)
|
||||
22. `tpl-lifetime-restriction` (T12)
|
||||
23. `tpl-adjacent-splash` (T13)
|
||||
|
||||
(Note: count = 22 unique IDs; the 6 parity imports + 3 net-new = 9 added; existing 14 preserved → 23 total. The locked list is 23 once shipped.)
|
||||
|
||||
**The 9 widened primitive schemas (locked at T0)**:
|
||||
1. `move-piece` — `target` and `to`
|
||||
2. `set-piece-attr` — `target`
|
||||
3. `destroy-piece` — `target`
|
||||
4. `destroy-marker` — `target`
|
||||
5. `swap-pieces` — `a` and `b`
|
||||
6. `convert-piece-type` — `target` (`pieceType` stays strict enum, no resolver)
|
||||
7. `place-piece` — `square` (`pieceType`/`color` stay strict enum)
|
||||
8. `spawn-marker` — `square` and `owner` (`markerKind` stays strict enum)
|
||||
9. `spawn-marker-pair` — `square1`, `square2`, and `owner` (markerKind stays strict)
|
||||
|
||||
`cancel-capture` has no positional field — verified excluded.
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
### Wave 0 — Schema helpers + doc-bug fix (sequential, blocks all)
|
||||
|
||||
- [ ] **T1**: Create `packages/chess/src/modifiers/primitives/param-resolver-schema.ts` exporting:
|
||||
- `numberOrResolver()` → `z.union([z.number().int().nonnegative(), VarShape, CtxAttrShape, CtxBuildShape])`
|
||||
- `enumOrResolverFor<T extends readonly [string, ...string[]]>(values: T)` → preserves `_def.entries` so ParamField enum-detection works
|
||||
- `stringOrResolver()` → `z.union([z.string(), VarShape, CtxAttrShape, CtxBuildShape])`
|
||||
- TS narrowing guards `isResolverShape(v): v is ResolverShape`, `isLiteralNumber(v): v is number`
|
||||
- Inline JSDoc with examples mirroring the format in `move-piece.ts`
|
||||
- Co-located test `param-resolver-schema.test.ts` proving each helper accepts literal + each resolver shape, rejects bare-string-where-number-expected
|
||||
|
||||
**Acceptance**: New file exists, exports 3 helpers + 2 guards, co-located test 8+ cases passing.
|
||||
**Parallelizable with**: nothing (blocks T2-T8)
|
||||
**Notepad write**: append to `.sisyphus/notepads/thressgame-templates/decisions.md` the locked union-shape order (literal first; resolver shapes second).
|
||||
|
||||
- [ ] **T1.5**: Fix the `for-each-piece.ts:132` doc bug — replace the example's `value: { ctx: "self" }` with `value: { "ctx-attr": { entity: "self", attr: "Color" } }`. Verify against `param-resolver.ts:139-216` for the canonical resolver-shape syntax.
|
||||
|
||||
**Acceptance**: `for-each-piece.ts:132` no longer contains the string `"ctx": "self"`. `bun run check` clean.
|
||||
**Parallelizable with**: T1 (one-line edit, no schema dependency)
|
||||
|
||||
### Wave 1 — Widen 9 imperative-primitive schemas (parallel after T1)
|
||||
|
||||
Each task: read the file, locate `paramsSchema = z.object({...})`, replace literal-typed positional fields with `numberOrResolver()` / `enumOrResolverFor(...)` calls, update co-located `.test.ts` to add positive cases for resolver shapes AND keep negative cases for genuinely invalid shapes (string-where-number, etc.). Run `bun run check` after each.
|
||||
|
||||
- [ ] **T2**: Widen `packages/chess/src/modifiers/primitives/move-piece.ts` schema fields `target` and `to` to `numberOrResolver()`. Update `move-piece.test.ts` accordingly.
|
||||
|
||||
**Acceptance**: Schema parses `{target: 28, to: 35}` AND `{target: {$var: "p"}, to: {"ctx-build": {col: 4, row: 3}}}`. Rejects `{target: "string"}`. Test file 5+ cases.
|
||||
**Parallelizable with**: T3, T4, T5, T6, T7, T8
|
||||
|
||||
- [ ] **T3**: Widen `packages/chess/src/modifiers/primitives/set-piece-attr.ts` schema field `target` to `numberOrResolver()`. Leave `attr: z.string()` and `value: z.unknown()` (already permissive). Update `set-piece-attr.test.ts`.
|
||||
|
||||
**Acceptance**: `religious_conversion.json` now passes `validateCustomDescriptor` (verify with a one-shot test in `recipes.test.ts` import).
|
||||
**Parallelizable with**: T2, T4, T5, T6, T7, T8
|
||||
|
||||
- [ ] **T4**: Widen `packages/chess/src/modifiers/primitives/destroy-piece.ts` AND `destroy-marker.ts` schema field `target` to `numberOrResolver()`. Update both co-located test files.
|
||||
|
||||
**Acceptance**: `kamikaze.json` now passes `validateCustomDescriptor`.
|
||||
**Parallelizable with**: T2, T3, T5, T6, T7, T8
|
||||
|
||||
- [ ] **T5**: Widen `packages/chess/src/modifiers/primitives/swap-pieces.ts` schema fields `a` and `b` to `numberOrResolver()`. Update test.
|
||||
|
||||
**Acceptance**: New test: descriptor with `{a: {$var: "p1"}, b: {$var: "p2"}}` inside `for-each-piece` validates clean.
|
||||
**Parallelizable with**: T2, T3, T4, T6, T7, T8
|
||||
|
||||
- [ ] **T6**: Widen `packages/chess/src/modifiers/primitives/convert-piece-type.ts` AND `place-piece.ts` schemas. For convert-piece-type: widen `target`. For place-piece: widen `square`. **Keep `pieceType` and `color` enums strict** — those are intentional design constraints, not gaps. Update both tests.
|
||||
|
||||
**Acceptance**: `convert-piece-type` accepts `{target: {$var: "p"}, pieceType: "knight"}`. Rejects `{target: 5, pieceType: {$var: "x"}}` (enum stays strict).
|
||||
**Parallelizable with**: T2, T3, T4, T5, T7, T8
|
||||
|
||||
- [ ] **T7**: Widen `packages/chess/src/modifiers/primitives/spawn-marker.ts` AND `spawn-marker-pair.ts`. For spawn-marker: widen `square` to `numberOrResolver()`, widen `owner` to `enumOrResolverFor(PIECE_COLORS)`. For spawn-marker-pair: widen `square1`, `square2`, and `owner` similarly. **Keep `markerKind` strict**. Update both tests.
|
||||
|
||||
**Acceptance**: `mr_freeze.json`'s `square: {"ctx-build": {...}}` shape AND `owner: {"ctx-attr": {...}}` shape now validate clean. Critical verification: `_def.entries` still introspectable on widened `owner` schema (ParamField enum-detection at `ParamField.tsx:255-263` still works post-widening).
|
||||
**Parallelizable with**: T2, T3, T4, T5, T6, T8
|
||||
|
||||
- [ ] **T8**: Audit `packages/chess/src/modifiers/primitives/cancel-capture.ts` schema. **Confirm there is no positional field that needs widening** (cancel-capture operates on the implicit `ctx.event` from the trigger frame, not an explicit target). Document the audit result in the file's JSDoc. NO code change expected.
|
||||
|
||||
**Acceptance**: JSDoc updated noting "no positional resolver-shape needed — operates via trigger event context."
|
||||
**Parallelizable with**: T2-T7
|
||||
|
||||
### Wave 2 — UI handles the widened types (after T1-T8)
|
||||
|
||||
- [ ] **T9**: Update `packages/chess/src/ui/ParamField.tsx` introspection block (lines 243-263). Add `z.ZodUnion` branch: detect literal-type members (`ZodNumber`, `ZodEnum`, `ZodString`); render the literal-type input by default; expose a "use binding" toggle that swaps the input for a JSON resolver-shape authoring mode (textarea pre-populated with `{"$var": ""}` template). When user types a binding name, validate it against in-scope binders via the `BINDING_INTRODUCING_KINDS` map. Co-locate snapshot test updates in `ParamField.snapshot.test.tsx` — likely 4-6 new snapshots for widened-field rendering.
|
||||
|
||||
**Acceptance**: A descriptor with `{move-piece: {target: {$var: "p"}, to: 28}}` renders in the editor without dropping to text-input fallback. Snapshot tests cover: literal-mode default, "use binding" mode, $var picker dropdown.
|
||||
**Parallelizable with**: T10 (independent file)
|
||||
|
||||
- [ ] **T10**: Update `packages/chess/src/modifiers/custom/validate.test.ts` — add 5 positive-case tests proving resolver shapes inside iteration arms validate clean:
|
||||
1. `for-each-piece(bind: "p") → set-piece-attr({target: {$var: "p"}, ...})` validates ok
|
||||
2. `for-each-adjacent(bind: "adj") → spawn-marker({square: {$var: "adj"}, ...})` validates ok
|
||||
3. `for-row(bind: "r") → spawn-marker({square: {"ctx-build": {col: 3, row: {$var: "r"}}}, ...})` validates ok
|
||||
4. `request-choice(bind: "sq", kind: "square") → spawn-marker({square: {$var: "sq"}, ...})` validates ok
|
||||
5. `for-each-piece(bind: "p") → set-piece-attr({target: {"ctx-attr": {entity: {$var: "p"}, attr: "Color"}}, attr: "Color", ...})` validates ok
|
||||
|
||||
AND keep all existing negative cases green:
|
||||
- `{target: "string-not-number"}` still rejected (resolver-shape OBJECT or literal NUMBER only)
|
||||
- `{target: -1}` still rejected (negative numbers fail `nonnegative()`)
|
||||
- `{$var: ""}` rejected (empty bind name)
|
||||
|
||||
**Acceptance**: 5 new positive cases + all existing negative cases green.
|
||||
**Parallelizable with**: T9
|
||||
|
||||
### Wave 3 — Recipes (sequential after T1-T10)
|
||||
|
||||
- [ ] **T11**: Add 6 parity-faithful recipes to `packages/chess/src/modifiers/custom/recipes.ts` by importing the existing parity JSONs. Use Vite's JSON import (`import religiousConversionDescriptor from "../../__fixtures__/parity/religious_conversion.json" assert { type: "json" }`). For each recipe:
|
||||
- Stable id (`tpl-religious-conversion`, `tpl-mr-freeze`, `tpl-mind-control`, `tpl-kamikaze`, `tpl-ice-physics`, `tpl-minefield-full`)
|
||||
- Title in the format `<Name> (full ThressGame fidelity)` so users see this as the "real" version
|
||||
- Summary explaining what the rule does in plain English (3-4 sentences max; references the simplified-recipe sibling where one exists, e.g. "Full version of the Frozen Column simplification — player picks the column at activation time.")
|
||||
- Descriptor: shallow-cast the imported JSON to `CustomModifierDescriptor` (the JSON is by definition the same shape; types align)
|
||||
- **Do NOT edit the parity JSON files**. The recipe is a wrapper.
|
||||
|
||||
**Acceptance**: 6 new entries in `CUSTOM_MODIFIER_RECIPES`, all 5 invariants in `recipes.test.ts` green.
|
||||
**Parallelizable with**: T12, T13 (different lines in same file — coordinate to avoid merge conflicts; recommend single agent for all three)
|
||||
|
||||
- [ ] **T12**: Add 2 net-new pattern recipes to `recipes.ts`:
|
||||
1. `tpl-mass-mover-pawnguins` — every white pawn marches one square forward on activation. Uses `on-rule-activated → for-each-piece(filter: {pieceType: "pawn", color: "white"}, bind: "p") → move-piece({target: {$var: "p"}, to: {"ctx-build": {col: <??>, row: <??>}}})`. **Open question**: how does `move-piece.to` reference "the source's col/row + 1"? Likely needs `{"ctx-attr": {entity: {$var: "p"}, attr: "Position"}}` plus an arithmetic helper, OR needs `for-each-piece` to bind `col` and `row` separately. **Resolution path**: read `param-resolver.ts:139-216` and pick the simplest existing shape; if none exists cleanly, drop this recipe to `tpl-mass-destroyer-they-deserved-it` instead (`for-each-piece → with-probability(p: 1/N) → destroy-piece({target: {$var: "p"}})`) which has no positional-arithmetic problem.
|
||||
2. `tpl-lifetime-restriction` — pieces frozen for 5 turns at activation. Uses `on-rule-activated → for-each-piece(bind: "p") → set-piece-attr({target: {$var: "p"}, attr: "BlockAllExceptKing", value: true, lifetime: {kind: "turns", count: 5}})`.
|
||||
|
||||
**Acceptance**: Both recipes added, validation green, manual smoke-load via Templates modal works.
|
||||
**Parallelizable with**: T11, T13
|
||||
|
||||
- [ ] **T13**: Add `tpl-adjacent-splash` to `recipes.ts`. Uses `on-capture → for-each-adjacent(target: "self", bind: "adj", filter: {occupied: true, excludeKing: true}) → add-to-attribute({attr: "Hp", delta: -1})`. The `add-to-attribute` here is targeted at the bound `adj` — but `add-to-attribute` operates on `ctx.pieceId`, not an explicit target. **Resolution path**: this is exactly the pattern `religious_conversion.json` uses with `set-piece-attr`. Either (a) use `set-piece-attr` with `{target: {$var: "adj"}, attr: "Hp", value: {"ctx-attr": {entity: {$var: "adj"}, attr: "Hp"}}}` and arithmetic-via-`ctx-attr-build` if available, OR (b) drop to `for-each-adjacent` with `target: {$var: "adj"}` so the for-each-adjacent's own redirection feeds inner `add-to-attribute`. Read `for-each-adjacent.ts` apply() to confirm which mechanism redirects pieceId.
|
||||
|
||||
**Acceptance**: Recipe validates and a Playwright e2e test (T14) proves a capture deals splash damage to all adjacent enemies.
|
||||
**Parallelizable with**: T11, T12
|
||||
|
||||
### Wave 4 — Verification (after T11-T13)
|
||||
|
||||
- [ ] **T14**: Create `packages/chess/e2e/templates-thressgame.spec.ts`. For each of the 9 new recipes (T11 + T12 + T13 = 6 + 2 + 1 = 9), write a Playwright spec that:
|
||||
1. Opens the Custom Modifier Editor
|
||||
2. Clicks the Templates button
|
||||
3. Clicks the recipe's `[data-testid="custom-template-recipe-{id}"]` button
|
||||
4. Asserts the editor populates without an error toast
|
||||
5. Asserts the descriptor's name field matches the expected name
|
||||
6. For tpl-religious-conversion + tpl-kamikaze + tpl-mind-control: also start a game with the loaded descriptor attached and verify a single observable behavior (bishop converts adjacent enemy / kamikaze kills self+adjacent / both players see piece-picker prompt). Use the existing `__test__.activate-descriptor` test-only WS frame for fast attachment.
|
||||
|
||||
**Acceptance**: `bunx playwright test e2e/templates-thressgame.spec.ts` exits 0 against `docker-compose.dev.yml`. Use `.sisyphus/scripts/run-pw.sh` (NEVER set CI=true).
|
||||
**Parallelizable with**: T15
|
||||
|
||||
- [ ] **T15**: Update `recipes.ts` file-header comment (lines 1-14) to retire the "V1 sharp edge" + "validator-clean stand-in" rhetoric — those notes are no longer accurate post-V2. Replace with a current statement of the recipe contract.
|
||||
|
||||
**Acceptance**: File-header comment accurate as of post-wave state. No mention of "T67-followup" or "V1 sharp edge."
|
||||
**Parallelizable with**: T14
|
||||
|
||||
- [ ] **T16**: Write evidence file `.sisyphus/evidence/thressgame-templates-final.txt` documenting:
|
||||
- Final list of 22 recipe IDs with title + descriptor count
|
||||
- The 9 widened primitive schemas with line refs in their files
|
||||
- ParamField union-renderer change with snapshot-diff summary
|
||||
- Test counts (before vs after)
|
||||
- Three editor screenshots (PNG attached): Templates modal showing all 22 recipes; ParamField rendering a widened `target` field in literal mode; same field in "use binding" mode showing the in-scope binder dropdown
|
||||
- Cross-reference table mapping the 6 parity-import recipes to their fixture file paths
|
||||
|
||||
**Acceptance**: Evidence file in `.sisyphus/evidence/` with full audit trail.
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave
|
||||
|
||||
Each reviewer produces a VERDICT (APPROVE / REJECT). All 4 must APPROVE before the wave is closed.
|
||||
|
||||
- [ ] **F1** (oracle review): Read all 9 widened schemas, the ParamField change, all 9 new recipes, and the validate.test.ts changes. Verify (a) widening is symmetric across all positional fields needing it, (b) enum schemas preserve `_def.entries` survivability, (c) ParamField doesn't degrade to text-input on any widened field, (d) backward compatibility with all 14 existing recipes, (e) recipes.ts file-header is accurate. **VERDICT: APPROVE / REJECT** with detailed reasons.
|
||||
|
||||
- [ ] **F2** (manual QA): Run the editor locally against `docker-compose.dev.yml`. For each of the 9 new recipes, click Templates → Load → verify the descriptor populates, save it as a custom modifier, attach it to a fresh game, and verify the rule actually triggers as expected. Record screen recording. **VERDICT: APPROVE / REJECT**.
|
||||
|
||||
- [ ] **F3** (test-suite quality): Run `bun run check` AND `bunx playwright test`. Verify zero new fixmes, zero new skips, zero flaky retries. Inspect coverage delta — recipes.test.ts, validate.test.ts, ParamField.snapshot.test.tsx, all 9 widened-primitive .test.ts files, and templates-thressgame.spec.ts. **VERDICT: APPROVE / REJECT**.
|
||||
|
||||
- [ ] **F4** (scope fidelity): Re-read this plan against the user's original ask ("Are there examples and templates that cover how to make all of the stuff from [ruleHooks.js]?"). Verify (a) we did not silently de-scope any locked recipe, (b) the parity JSONs are genuinely imported by reference and not duplicated, (c) no out-of-scope work crept in (no new primitives, no recursion-depth bump, no preset-pack work). **VERDICT: APPROVE / REJECT**.
|
||||
|
||||
---
|
||||
|
||||
## Notes for the Orchestrator (Atlas)
|
||||
|
||||
- **Anti-duplication**: Do not re-explore territory the oracle already mapped (paramfield introspection, validator constraints, parity fixture validation status). Reference the oracle session for grounding when needed.
|
||||
- **Session continuity**: T2-T8 are perfect for parallel `category="quick"` delegation since each is a one-file, two-test-update task. Fire all 7 in one message.
|
||||
- **Notepad pattern**: Mirror the thressgame-coverage notepad layout — `decisions.md` (locked union-shape ordering), `learnings.md` (Zod union introspection gotchas), `issues.md` (any blocked schemas), `problems.md` (T12 / T13 open-question resolution paths).
|
||||
- **Critical helper**: All Playwright runs (T14) MUST use `.sisyphus/scripts/run-pw.sh` and poll the `.done` marker. Do NOT run `bunx playwright test` directly; it times out the agent runtime.
|
||||
- **Docker stack**: `docker compose -f docker-compose.dev.yml ps` should show web (:5173) + server (:7357) up before T14. If not, user must `docker compose up` first.
|
||||
- **The hard-stop on `recipes.test.ts:30-54`**: After T11 lands, run `bun test packages/chess/src/modifiers/custom/recipes.test.ts` IMMEDIATELY. The test walks `primitive.childPrimitives()` against every recipe — if Zod widening broke childPrimitives's internal parse, this test fails. Fix at the schema layer (T1 helpers), never by removing the test invariant.
|
||||
|
||||
---
|
||||
|
||||
## Resolution Path for T12 / T13 Open Questions
|
||||
|
||||
The plan flags T12 (mass-mover-pawnguins) and T13 (adjacent-splash) as potentially needing arithmetic on `ctx-build` shapes that may not exist. Before starting either task:
|
||||
|
||||
1. Read `packages/chess/src/modifiers/primitives/param-resolver.ts:139-216` to enumerate the EXACT resolver shapes the runtime knows about.
|
||||
2. If `ctx-build` accepts arithmetic on a bound `$var` (e.g. `{"ctx-build": {col: {$var: "col"}, row: {add: [{$var: "row"}, 1]}}}` or similar), use it directly.
|
||||
3. If `ctx-build` is purely literal `{col, row}`, the simplest path is to author the recipe as `for-each-piece(bind: "p") → for-row(rows: [<list>], bind: "r") → ...` so the row arithmetic happens at recipe-author time, not run time. This is a "dumber" recipe but ships clean.
|
||||
4. If neither path works for `tpl-mass-mover-pawnguins`, replace it with `tpl-mass-destroyer-they-deserved-it` (no arithmetic needed): `on-rule-activated → for-each-piece(filter: {excludeKing: true}, bind: "p") → with-probability(p: 0.0769) → destroy-piece({target: {$var: "p"}})` (1/13 ≈ 7.7% chance to destroy each non-king, on average 1 destroyed per activation — matches `they_deserved_it` semantics).
|
||||
5. If `tpl-adjacent-splash` hits the same wall (add-to-attribute can't redirect target), replace with `tpl-adjacent-debuff` which uses `set-piece-attr({target: {$var: "adj"}, attr: "HpBonus", value: -1, lifetime: {kind: "turns", count: 1}})` — a 1-turn HP debuff. Mechanically different from "deal damage" but covers the adjacent-iteration pattern category.
|
||||
|
||||
The orchestrator records the final choice in `.sisyphus/notepads/thressgame-templates/decisions.md` so future tasks (T14 e2e) test the actual chosen recipe.
|
||||
Loading…
Add table
Add a link
Reference in a new issue