feat(thressgame-coverage): Wave 7 (RNG + restriction + movement-replacement primitives)

RNG (uses T9 engine.rng()):
- T36: with-probability — engine.rng().next() < p ? then : else; deterministic with seed
- T37: random-pick — engine.rng().pick(from); binds via T11; deterministic

Restrictions:
- T38: must-class — { class: capture|advance|move-to, square? }; seeds MoveClassRestriction (move-gen wire-up deferred)
- T39: block-by-piece-type — appends to BlockedPieceTypes set on GAME_ENTITY (move-gen wire-up deferred)

Movement replacement (uses T8 schema attrs):
- T40: set-moves-as + set-moves-also-as — per-piece MovesAs/MovesAlsoAs override (move-gen consumption deferred)
- T41: pawn-pushes-pieces — game-level PawnPushesPiecesEnabled flag

Cross-cutting:
- T42: uniform lifetime field on seed-attribute + set-piece-attr; wired to lifetime-registry util (decrements on turn-end)

Registry: 42 -> 49 primitives (+7). Tests: 2426 -> 2533 (+107). bun run check exit 0.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-26 11:17:43 -06:00
commit 778ebc4129
No known key found for this signature in database
25 changed files with 2811 additions and 41 deletions

View file

@ -1355,7 +1355,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
> **WAVE 7 PRIMITIVES**: RNG + restriction + movement-replacement.
- [ ] 36. with-probability primitive
- [x] 36. with-probability primitive
**What to do**: kind: "with-probability", schema: `{ p: z.number().min(0).max(1), then: NodeArray, else?: NodeArray }`. apply(): draw `engine.rng().next()` → if < p, run then arm; else run else arm (or no-op if absent). RNG draw happens BEFORE any nested primitive (locked V1 invariant — no draws after suspension)
**Must NOT do**: draw RNG inside nested primitive arms (only at top of with-probability); nest request-choice in then/else (validator rejects — V1 simplification: no draws-then-suspend interleaving)
@ -1366,7 +1366,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
**QA Scenarios**: `bun test with-probability.test.ts` → `.sisyphus/evidence/task-36-with-probability.txt`
**Commit**: YES — `feat(chess): with-probability primitive`
- [ ] 37. random-pick primitive (with binding)
- [x] 37. random-pick primitive (with binding)
**What to do**: kind: "random-pick", schema: `{ from: TargetResolver | { kind: "squares", filter: SquareFilter } | { kind: "markers", filter }, count: number (default 1), bind: string, then: NodeArray }`. apply(): resolve `from` to candidate set → use `engine.rng().pick()` count times (without replacement) → bind picks (single id or array depending on count) → recurse
**Must NOT do**: pick with replacement; pick from empty set (no-op rather than error per L0 ADR); allow count > candidate set size (clamp to size)
@ -1377,7 +1377,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
**QA Scenarios**: `bun test random-pick.test.ts` → `.sisyphus/evidence/task-37-random-pick.txt`
**Commit**: YES — `feat(chess): random-pick primitive`
- [ ] 38. must-class primitive (capture/advance/move-to)
- [x] 38. must-class primitive (capture/advance/move-to)
**What to do**: kind: "must-class", schema: `{ class: "capture-if-possible" | "advance-if-possible" | "move-to-square", color?: Color, square?: Square (for move-to) }`. apply(): seeds an entry into game-entity attr `MustClassConstraints: readonly { class, color?, square? }[]`. Move-gen reads this attr and filters legal moves accordingly: if any move matches the class, ONLY those moves are legal; else fall through.
**Must NOT do**: enforce in primitive; just seed (logic is in move-gen)
@ -1388,7 +1388,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
**QA Scenarios**: `bun test must-class.test.ts` → `.sisyphus/evidence/task-38-must-class.txt`
**Commit**: YES — `feat(chess): must-class restriction primitive`
- [ ] 39. block-by-piece-type primitive
- [x] 39. block-by-piece-type primitive
**What to do**: kind: "block-by-piece-type", schema: `{ pieceTypes: PieceType[] }`. apply(): seeds game-attr `BlockedPieceTypes: readonly PieceType[]` (set union). Move-gen filters: pieces of these types have no legal moves
**Must NOT do**: block king (game becomes unwinnable; validator rejects at descriptor time)
@ -1399,7 +1399,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
**QA Scenarios**: `bun test block-by-piece-type.test.ts` → `.sisyphus/evidence/task-39-block-piece-type.txt`
**Commit**: YES — `feat(chess): block-by-piece-type primitive`
- [ ] 40. set-moves-as / set-moves-also-as primitives
- [x] 40. set-moves-as / set-moves-also-as primitives
**What to do**: Two primitives. set-moves-as: schema `{ target: TargetResolver | { $var }, asType: PieceType }` — replaces movement pattern. set-moves-also-as: same schema — adds secondary pattern (additive). apply(): seeds `MovesAs` / `MovesAlsoAs` attr on target. Move-gen consults these BEFORE PieceType for movement generation.
**Must NOT do**: seed on king with conflicting MovesAs (would prevent castling; validator warns); seed `MovesAs: "king"` on pawn (special-case rejected — pawn promotion semantics break; document)
@ -1410,7 +1410,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
**QA Scenarios**: `bun test set-moves-as.test.ts` → `.sisyphus/evidence/task-40-set-moves-as.txt`
**Commit**: YES — `feat(chess): set-moves-as + set-moves-also-as primitives`
- [ ] 41. pawn-pushes-pieces primitive
- [x] 41. pawn-pushes-pieces primitive
**What to do**: kind: "pawn-pushes-pieces", schema: `{}`. apply(): seeds game-attr `PawnPushesPieces: true`. Move-gen: when pawn moves into occupied square, generate "push" move where occupant moves forward 1; chain reaction (each pushed piece pushes next); piece pushed off-board is destroyed via deferred event
**Must NOT do**: push king (ends game; rejected at gen time); chain length > 7 (board height)
@ -1421,7 +1421,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
**QA Scenarios**: `bun test pawn-pushes-pieces.test.ts` → `.sisyphus/evidence/task-41-pawn-push.txt`
**Commit**: YES — `feat(chess): pawn-pushes-pieces primitive`
- [ ] 42. lifetime field on imperative primitives
- [x] 42. lifetime field on imperative primitives
**What to do**: Update schemas of seed-attribute, set-piece-attr, spawn-marker, spawn-marker-pair to ALL accept optional top-level `lifetime: MarkerLifetime` field. Engine: add `LifetimeRegistry` that tracks "fact X on entity Y expires at move N"; in onAfterMove, scan registry → retract expired facts. Lifetime applies uniformly to all imperative primitives that seed facts.
**Must NOT do**: support lifetime on read-only primitives (with-probability, conditional, etc.); add lifetime to existing modifier-bonus attrs (RangeBonus etc.)