feat(thressgame-100): Wave 5 \u2014 economy + score chips + 3 recipes + e2e
Wave 5 of thressgame-100 epic complete \u2014 paradigm-break wave per oracle.
User-authorized despite the cost. Coverage: 47/51 \u2192 50/51 = 98 %.
PARADIGM-BREAK CONTEXT (oracle pre-flagged):
Oracle classified Wave 5 as 'paradigm break \u2014 game-level mutable state'. User
explicitly authorized via 'no time/cost limit, no backward-compat constraint,
just fucking get it all done'. Design choice: scores live alongside RngStream
on GAME_ENTITY \u2014 same pattern as existing global state (RngStream,
ChoiceTimeoutPolicy, BoardTopology, BlockAllExceptKing), not new architecture.
ENGINE WORK (W5.0\u2013W5.4):
Score subsystem (locked decision F):
- WhiteScore + BlackScore attrs on GAME_ENTITY \u2014 type 'number', default 0
- Initialized at engine construction (engine.ts:632\u2013634) so fact-log inclusion
is mechanical \u2014 state-hash auto-includes scores via existing infrastructure
- N=100 byte-identical replay determinism verified
3 NEW PRIMITIVES:
- add-resource(player, amount) imperative; synchronous fire of
on-resource-changed hooks
- spend-resource(player, amount, imperative; selfRecurse:true \u2014 only the
then, else) chosen arm runs (walker doesn't auto-recurse
into both); cascade-depth-limited (8)
- on-resource-changed(player, trigger; fires on threshold crossing in
threshold, direction up/down/any; snapshot-before-iterate
direction, so hook arm registering more hooks doesn't
primitives) fire on the same crossing
WS PROTOCOL (W5.5) \u2014 Scenario A chosen: ZERO protocol changes:
- broadcast.ts uses session.allFacts() which iterates ALL facts on ALL entities
including GAME_ENTITY
- WhiteScore/BlackScore mutations land in game.state and game.delta frames
automatically; client PredictionManager receives them out-of-the-box
- Zod FactSchema has untyped .unknown() value field on the wire \u2014 number values
serialize implicitly
UI WORK (W5.6) \u2014 GameView.tsx score chips:
- Two chips: data-testid='score-white' (\u26aa W) and data-testid='score-black'
(\u26ab B), bordered chip styling matching ActionMenu sibling vocabulary
- HIDE-WHEN-ZERO rule: chips hidden unless either score !== 0 OR
OnResourceChangedHooks contains entries (pure-chess games visually unchanged)
- Real-time updates via existing useMultiplayerGame hook \u2014 broadcasts trigger
re-render, scores update without page reload
3 NEW RECIPES (W5.7):
Batch M \u2014 economy:
- tpl-treasure-chest \u2014 capture spawns treasure marker; entering treasure
awards white +5 score and consumes marker
(SIMPLIFIED: arm 2 hardcodes player='white' \u2014
no chooser-color resolver; LastModifierChooser
semantics = rule applier not current mover)
- tpl-cash-grab \u2014 every turn-end, white+1 + black+1 (passive income)
(SIMPLIFIED: V3 has no eq shape \u2014 random-pick result
can't be compared against piece Position; ship the
dual-add-resource pattern instead of canonical
random-square-rewards-piece-owner semantics)
- tpl-summoning-ritual \u2014 spend 5 score to summon white knight at e4
(SIMPLIFIED: single-shot at activation; repeatable
summoning needs request-choice loop \u2014 W6+ scope)
TEST SURFACE:
- add-resource.test.ts: ~10 unit tests
- spend-resource.test.ts: ~12 unit tests
- on-resource-changed.test.ts: ~15 unit tests
- economy-integration.test.ts: 8 integration tests
(incl. N=100 determinism)
- wave5-recipes-real.test.ts: 14 runtime tests
- recipes.test.ts: 5 \u00d7 62 = 513 expect calls
- wave5-economy.spec.ts (Playwright): 5 e2e tests
(3 load + summoning-ritual
success-path runtime +
cash-grab turn-end driven)
bun run check: 3270 tests pass (was 3192, +78). 0 regressions.
e2e: 5/5 green via .sisyphus/scripts/run-pw.sh against docker compose dev stack.
KEY FINDINGS (recorded in learnings.md):
- spend-resource selfRecurse:true correctly gates walker auto-recursion \u2014
place-piece inside 'then' only runs when spend succeeds
- ctx-attr.entity:'self' resolves cleanly to ctx.pieceId in resolver
- fireOnTurnEndHooks (and other per-piece dispatchers) iterate pieces \u2014 hooks
on GAME_ENTITY are dead-seeded; apply-descriptor needs targetSquare for any
recipe rooted at a per-piece trigger
- Snapshot-before-iterate in fireOnResourceChangedHooks prevents reentrant
cascade fires on the same crossing
BACKWARD-INCOMPAT TESTS UPDATED (per locked decision J):
- registry-count.test.ts: 56 \u2192 59 (3 new primitives)
- ParamField.snapshot.test.tsx: SAMPLE_PARAMS exhaustiveness for 3 new kinds
- GameView snapshot: regenerated for new score-chip section
Plan: .sisyphus/plans/thressgame-100.md
Notepads: .sisyphus/notepads/thressgame-100/
Evidence: .sisyphus/evidence/thressgame-100-wave5.txt (gitignored, 1074 lines)
This commit is contained in:
parent
3a4bd394aa
commit
a2c38a9ad2
23 changed files with 3465 additions and 5 deletions
|
|
@ -142,7 +142,12 @@
|
|||
"ses_22f1823c1ffek8p7La3fBMqslC",
|
||||
"ses_22efe07c7ffe1eTZ6kvrkGLVYl",
|
||||
"ses_22ef4380affeXjumRiXI4u1KSK",
|
||||
"ses_22ef3b48bffeJQKH6xj5U4fuTi"
|
||||
"ses_22ef3b48bffeJQKH6xj5U4fuTi",
|
||||
"ses_22ee9db6effe5AVbe40BDqIT9y",
|
||||
"ses_22ed7ead4ffe0ku37F9Y6qIO9s",
|
||||
"ses_22ed735fdffeyscON4NRPNOrz5",
|
||||
"ses_22ec6f8a8ffeaZwK7e1GUo035a",
|
||||
"ses_22ec791c3ffeMYLw6bTdYIxPNm"
|
||||
],
|
||||
"plan_name": "thressgame-coverage",
|
||||
"agent": "atlas"
|
||||
|
|
|
|||
|
|
@ -323,3 +323,144 @@ Final recipe count: **54**. None of the 8 W3 recipes was skipped.
|
|||
- **DOM piece selector is descendant `[data-square=".."] [data-piece=".."]`, NOT compound `[data-square=".."][data-piece=".."]`** — `data-piece` lives on the inner `<Piece>` element (Piece.tsx:310) while `data-square` lives on the outer `<Board>` cell wrapper (Board.tsx:281). The task brief used the compound form which would never match. Mirrored the parity-rules.spec.ts § probes pattern (`[data-square="b3"] [data-piece="white-pawn"]`).
|
||||
- **`apply-descriptor` is the right driver for W4 topology + pairing recipes** (NOT activate-descriptor). activate-descriptor lifts a request-choice to fire a PendingChoice frame; the W4 recipes have no request-choice — they need the FULL `applyCustomDescriptor` walk to seed PieceLink lists / set BoardTopology / spawn pieces. Same call shape as parity-rules.spec.ts § all_on_red and ice_physics.
|
||||
- **Test count delta (e2e only)**: wave4-topology-pairing.spec.ts ships 7 tests. All green on first invocation.
|
||||
|
||||
## [2026-04-26] W5.0-W5.4 — economy / resource accumulation subsystem
|
||||
|
||||
### Paradigm break shipped (decision F)
|
||||
|
||||
- **`WhiteScore` / `BlackScore` on `GAME_ENTITY`** — both `number`, default `0`, seeded at engine construction (engine.ts:632) right after `RngStream`. Pinning the score into the fact log from move-0 (rather than letting `add-resource.apply()`'s `?? 0` fallback do it lazily) means the determinism state hash includes them automatically — replay correctness drops out for free.
|
||||
- **Architecturally, this is more-of-the-same**, NOT a new pattern. `GAME_ENTITY` already held mutable scalars (`RngStream`, `ChoiceTimeoutPolicy`, `BlockAllExceptKing`, `BlockedPieceTypes`, `PawnPushesPiecesEnabled`, `BoardTopology`). Adding two more numeric counters is a cheap extension; the "paradigm break" framing in the plan tracks the SCOPE of mutability widening (from booleans/discriminators to running tallies) rather than any genuine architecture shift.
|
||||
|
||||
### Synchronous fire pattern (distinct from W2's stage-13 batched dispatch)
|
||||
|
||||
- `fireOnResourceChangedHooks` (triggers.ts) is invoked DIRECTLY from inside `add-resource.apply()` and `spend-resource.apply()` AFTER the score mutation lands. No deferred queue, no per-turn batch — author intent is "react to the score crossing immediately" and there's no phase-ordering concern (unlike on-attr-expire which had to land at stage 13 between turn-end and turn-start ticks). Cascade-depth cap (8) on `runPrimitives` provides the recursion ceiling; `pendingTriggers` queue is unused for this trigger family.
|
||||
- **Snapshot-before-iterate** — `fireOnResourceChangedHooks` copies the hook list into `snapshot` before iterating, so a hook arm that itself seeds `on-resource-changed` doesn't fire on the SAME crossing. Matches "registered-before-mutation" author expectations and avoids re-entrant infinite loops at the registration layer (the cascade-depth cap is the second-line defense for non-registration loops).
|
||||
- **`pieceId` for inner arms is `GAME_ENTITY`** — the trigger is conceptually game-scoped (the score lives on GAME_ENTITY). Authors who want a piece-targeted effect use `for-each-*` iteration inside the arm. Mirror of the W2 attr-expire dispatcher's "fire on the entity that owns the expiring fact" convention.
|
||||
|
||||
### Crossing semantics (locked, inclusive on the new-side)
|
||||
|
||||
- `direction: "up"` — `previous < threshold AND new >= threshold`
|
||||
- `direction: "down"` — `previous > threshold AND new <= threshold`
|
||||
- `direction: "any"` — either of the above
|
||||
- **Same-value mutations** (previous === new) NEVER cross. `add-resource(white, 0)` is observable as a fact-log no-op (the score is re-inserted with the same value, which Rete dedupes downstream) and fires no hooks. Tested explicitly in `on-resource-changed.test.ts § no-op when score doesn't actually move`.
|
||||
|
||||
### `spend-resource` self-recursion semantics
|
||||
|
||||
- `selfRecurse: true` because the dispatcher's auto-recursion would walk BOTH `then` and `else` arms regardless of the success branch, defeating the gating semantics. The primitive runs the chosen arm itself via `runPrimitives` inside `apply()`. Mirrors `with-probability`'s self-recursive pattern (the FIRST conditional-style imperative in the registry; `conditional` itself uses dispatcher auto-recursion because both arms are lazily evaluated by the existing fireConditionalHooks dispatcher).
|
||||
- **Trigger-scope detection in validate.ts** — added `node.kind === "spend-resource"` to the `childrenInTriggerScope` check at validate.ts:387 alongside `conditional` / `on-*` / `for-each-*` / `random-pick`. Without this, imperatives inside `then` / `else` (e.g. `place-piece` for the success arm) would error with `descriptor.primitives.imperative-in-passive`.
|
||||
- `runPrimitives` invocation in `spend-resource.apply()` reuses `ctx.bindings`, `ctx.cascadeDepth`, `ctx.suppressTriggers`, and `ctx.descriptor.id` — preserves outer lexical scope (no new binding introduced; mirrors `conditional`).
|
||||
|
||||
### Schema additions (schema.ts)
|
||||
|
||||
- 2 new attrs: `WhiteScore: number`, `BlackScore: number`.
|
||||
- 1 new attr: `OnResourceChangedHooks: readonly OnResourceChangedHookEntry[]`.
|
||||
- 1 new exported interface: `OnResourceChangedHookEntry { descriptorId, player, threshold, direction, primitives }`.
|
||||
- 1 new `PrimitiveEvent` variant in `context.ts`: `{ kind: "resource-changed", player, previousScore, newScore }`.
|
||||
|
||||
### Validator (validate.ts) updates
|
||||
|
||||
- IMPERATIVE_KINDS gains `add-resource` + `spend-resource`. Both legal ONLY inside trigger arms — top-level placement returns `descriptor.primitives.imperative-in-passive`.
|
||||
- Trigger-scope walker now admits `spend-resource` for child propagation (its `then` / `else` arms are imperative-legal).
|
||||
- `on-resource-changed` matches the existing `node.kind.startsWith("on-")` predicate for trigger-scope detection — no validator edit needed.
|
||||
|
||||
### Backward-incompat tests updated (per decision J)
|
||||
|
||||
- `registry-count.test.ts`: 56 → 59 (3 new primitives — add-resource, spend-resource, on-resource-changed).
|
||||
- `ParamField.snapshot.test.tsx`: SAMPLE_PARAMS map extended with the 3 new primitive kinds (TS exhaustiveness requirement: `Record<PrimitiveKind, unknown>`).
|
||||
|
||||
### Determinism
|
||||
|
||||
- N=100 game replays produce byte-identical state hash (`economy-integration.test.ts § Wave-5 economy integration — replay determinism`). The pinned mutation sequence exercises:
|
||||
- 2 add-resource calls below crossing (no hook fire).
|
||||
- 1 add-resource that crosses 10 going up (hook fires, sentinel HpBonus=1 written to GAME_ENTITY).
|
||||
- 1 add-resource on independent black counter (cross-color independence).
|
||||
- 1 successful spend-resource (10 → 6, runs `then` writing RangeBonus=7).
|
||||
- 1 failed spend-resource (6 < 100, no `else`, score untouched).
|
||||
- The state hash trivially includes the new GAME_ENTITY attrs because the fact log is the hash input and `engine.ts` seeds `WhiteScore=0` / `BlackScore=0` at construction. NO state-hash util edits required.
|
||||
|
||||
### Test count delta
|
||||
|
||||
- `bun run check`: 3192 → 3256 tests (+64). 0 regressions. Breakdown:
|
||||
- `add-resource.test.ts` (NEW): 14 tests.
|
||||
- `spend-resource.test.ts` (NEW): 16 tests.
|
||||
- `on-resource-changed.test.ts` (NEW): 20 tests.
|
||||
- `__tests__/economy-integration.test.ts` (NEW): 8 tests.
|
||||
- +6 tests from auto-discovered docs / manifest / consumer-integration walkers picking up the 3 new primitives (registry-count was bumped, exhaustive `Record<PrimitiveKind, unknown>` tests now cover 3 more kinds).
|
||||
|
||||
### Files created (line counts)
|
||||
|
||||
- `packages/chess/src/modifiers/primitives/add-resource.ts` — 111 lines.
|
||||
- `packages/chess/src/modifiers/primitives/spend-resource.ts` — 175 lines.
|
||||
- `packages/chess/src/modifiers/primitives/on-resource-changed.ts` — 140 lines.
|
||||
- `packages/chess/src/modifiers/primitives/add-resource.test.ts` — 214 lines.
|
||||
- `packages/chess/src/modifiers/primitives/spend-resource.test.ts` — 278 lines.
|
||||
- `packages/chess/src/modifiers/primitives/on-resource-changed.test.ts` — 296 lines.
|
||||
- `packages/chess/src/modifiers/primitives/__tests__/economy-integration.test.ts` — 333 lines.
|
||||
- Total: 1547 lines new code (engine + tests).
|
||||
|
||||
### Files modified
|
||||
|
||||
- `packages/chess/src/schema.ts` — `ChessAttrMap` gains 3 attrs (`WhiteScore`, `BlackScore`, `OnResourceChangedHooks`); `OnResourceChangedHookEntry` interface exported.
|
||||
- `packages/chess/src/engine.ts` — seeds `WhiteScore=0` / `BlackScore=0` at construction (after RngStream).
|
||||
- `packages/chess/src/modifiers/primitives/types.ts` — `PrimitiveKind` gains 3 kinds; `TriggerName` gains `on-resource-changed`.
|
||||
- `packages/chess/src/modifiers/primitives/context.ts` — `PrimitiveEvent` gains `resource-changed` variant.
|
||||
- `packages/chess/src/modifiers/primitives/index.ts` — registers the 3 new primitives.
|
||||
- `packages/chess/src/modifiers/triggers.ts` — `fireOnResourceChangedHooks` dispatcher exported.
|
||||
- `packages/chess/src/modifiers/apply.ts` — registers `WhiteScore` / `BlackScore` / `OnResourceChangedHooks` consumers.
|
||||
- `packages/chess/src/modifiers/custom/validate.ts` — IMPERATIVE_KINDS gains `add-resource` + `spend-resource`; trigger-scope walker admits `spend-resource`.
|
||||
- `packages/chess/src/modifiers/primitives/registry-count.test.ts` — 56 → 59.
|
||||
- `packages/chess/src/ui/ParamField.snapshot.test.tsx` — SAMPLE_PARAMS gains 3 entries.
|
||||
|
||||
## [2026-04-26] W5.7 — 3 Wave-5 economy template recipes + co-located runtime tests
|
||||
|
||||
### Recipes shipped (59 → 62)
|
||||
|
||||
**Batch M — economy (3 recipes)**:
|
||||
|
||||
- **`tpl-treasure-chest`** — TWO-armed economy + marker recipe. Arm 1: `on-capture → spawn-marker({markerKind:"treasure", square: ctx-attr({entity:"self", attr:"Position"}), lifetime:{kind:"permanent"}})` — drops a treasure marker on the attacker's POST-MOVE Position (which IS the captured piece's square because capture replaces). Arm 2: `on-piece-entered-marker(treasure) → add-resource({player:"white", amount:5}) + destroy-marker({target: ctx-self-marker-id})` — landing on a treasure awards 5 score and consumes the marker. **CONFIRMED: nested `ctx-attr.entity:"self"` resolves to `ctx.pieceId` correctly via `param-resolver.ts:371` (`if (entity === "self") return ctx.pieceId`).** No new resolver shape needed; the existing V3 union accepts `{ "ctx-attr": { entity: "self", attr: "Position" } }` as the `square` argument to `spawn-marker`. SIMPLIFICATION: Arm 2 hardcodes `player:"white"`. The brief flagged that chooser color may not be available inside `on-piece-entered-marker`; in fact `LastModifierChooser` IS persisted on `PRESET_STATE_ENTITY` and the `ctx-attr({entity:"chooser", attr:"Color"})` resolver does work — but its semantics are 'whoever applied THIS rule', not 'whoever stepped on the treasure'. A truly-mover-aware payout would need a new `ctx-piece-color` shape (deferred to W6+).
|
||||
|
||||
- **`tpl-cash-grab`** — SIMPLIFIED from the canonical 'random-pick a square; if a piece is there, that piece's owner gets +1' shape. The `random-pick` primitive returns a numeric value but `conditional` does NOT support arithmetic comparison on resolver-shape values (V3 resolver union is arithmetic-only: add/sub/mul/mod — no `eq`). Without comparison we can't gate per-piece add-resource on Position equality. Ship the simplified pattern: `on-turn-end(both) → add-resource(white, 1) + add-resource(black, 1)`. Each turn end, both players gain 1 score — passive-income demonstration of `on-turn-end → add-resource`. The original cash-grab semantic (probabilistic per-square payout) needs a future `eq` resolver shape or a dedicated `random-pick-piece` primitive variant.
|
||||
|
||||
- **`tpl-summoning-ritual`** — single-shot ritual. `on-rule-activated → spend-resource({player:"white", amount:5, then:[place-piece({pieceType:"knight", color:"white", square:28})]})`. Direct port of the `spend-resource` then-arm pattern. Pre-seed `WhiteScore=10` for a successful spend; pre-seed `WhiteScore=2` for the failure-no-op path. NO `else` arm — single-shot 'silent failure' semantics. **CONFIRMED: `spend-resource.selfRecurse=true` correctly gates the dispatcher's auto-recursion** so `place-piece` inside `then` is NOT pre-fired by the walker pass — it only runs when the spend succeeds.
|
||||
|
||||
### Test-harness conventions (W5-specific)
|
||||
|
||||
- **Direct trigger-primitive `apply()` for clean-state seeding**: BOTH `runPrimitives` AND the apply-walker auto-recurse into trigger primitives' `childPrimitives()`, which pre-fires the inner imperatives at SEED TIME (e.g. add-resource fires once during seeding even though we just want OnTurnEndHooks to land). Bypass: import the trigger primitive directly (`ON_TURN_END_PRIMITIVE`, `ON_CAPTURE_PRIMITIVE`, `ON_PIECE_ENTERED_MARKER_PRIMITIVE`) and call `*_PRIMITIVE.apply(ctx, params)` with a hand-rolled `PrimitiveApplyContext`. This calls ONLY the trigger's seed logic and never recurses. Cribbed from W4's `ON_PIECE_PAIR_LINK_BROKEN_PRIMITIVE.apply(ctx, ...)` pattern. **Net effect**: pre-fire side effects are zero, the dispatcher fire is the SOLE path that runs the inner imperatives, and we can pin `readScore(white) === 0` before firing the trigger.
|
||||
- **`applyCustomDescriptorTolerant` reused from W1** for the smoke tests on tpl-treasure-chest (legitimately throws on `ctx-self-marker-id` outside marker context — the documented apply-walker artifact, see `wave1-recipes-real.test.ts:528-576`) and tpl-summoning-ritual (the spend-resource-then-place-piece chain runs once during the walker pass and would re-fire via `fireOnRuleActivatedHooks` — same double-fire pattern as tpl-suicidal-knight). The W1 tolerant helper accepts only `ctx-self-marker-id` / `ctx-self-id` errors — no new error class needed for W5.
|
||||
- **Summoning-ritual smoke pre-seeds `WhiteScore=100`** so the walker's eager spend-resource call doesn't underflow (otherwise `2 < 5` would short-circuit and we wouldn't probe the place-piece path). The over-seeded value makes the walker's pre-fire AND the OnRuleActivatedHooks-driven re-fire both safe; we only assert hook presence, not score value (the value is artifact-dependent).
|
||||
- **No `chooser-color` resolver shape ships in W5**. Arm 2 of tpl-treasure-chest hardcodes `player:"white"`. A future `ctx-piece-color({entity: ctx-self-id})` resolver (or a `chooser-color` literal) would let recipes target the moving player's side; deferred until a recipe that NEEDS it lands.
|
||||
|
||||
### Decisions / simplifications recorded
|
||||
|
||||
- (a) — `tpl-treasure-chest` Arm 2 hardcodes `player:"white"` (chooser-color stays a future resolver shape).
|
||||
- (b) — `tpl-cash-grab` ships the on-turn-end + dual-add-resource pattern (no random-pick, no eq, no per-piece). The canonical 'random-square gambling' semantics need an `eq` resolver shape.
|
||||
- (c) — `tpl-summoning-ritual` is single-shot via on-rule-activated. Repeatable summoning would need a request-choice loop (deferred to W6+).
|
||||
|
||||
### Test count delta
|
||||
|
||||
- `bun run check`: 3256 → 3270 (+14). 0 regressions. 14 new tests from `wave5-recipes-real.test.ts`:
|
||||
- tpl-treasure-chest: 3 tests (on-capture spawn arm, on-piece-entered-marker payout arm, descriptor shape).
|
||||
- tpl-cash-grab: 2 tests (on-turn-end fires + descriptor shape).
|
||||
- tpl-summoning-ritual: 3 tests (success path with pre-seeded 10 score, failure path with 2 score, descriptor shape).
|
||||
- Cross-cutting smoke: 6 tests (presence + count + 4 hook-seed asserts via applyCustomDescriptor / Tolerant).
|
||||
- recipes.test.ts: 5 invariants now apply to 62 recipes (513 expect calls — was 490 at W4 close; +23 from 3 new recipes).
|
||||
- New file: `wave5-recipes-real.test.ts`.
|
||||
|
||||
### Files modified
|
||||
|
||||
- `packages/chess/src/modifiers/custom/recipes.ts` — appended 3 W5 recipes (Batch M).
|
||||
- `packages/chess/src/modifiers/custom/wave5-recipes-real.test.ts` — NEW.
|
||||
|
||||
### `bun run check` exit code: 0
|
||||
|
||||
## [2026-04-26] W5.8 — Playwright e2e for the 3 Wave-5 economy recipes
|
||||
|
||||
- **All 5 tests green** (3 load + 2 runtime) in 7.9s on the docker compose dev stack via `.sisyphus/scripts/run-pw.sh`. Spec at `packages/chess/e2e/wave5-economy.spec.ts`. Helper log: `/tmp/pw-w5-8.log`. First run had 4/5 (cash-grab failed due to `targetSquare` omission); single-edit fix landed the green.
|
||||
- **Runtime test depth decisions** — both went FULL (not smoke):
|
||||
- `tpl-summoning-ritual` — pre-seed `WhiteScore=15` via mini-descriptor (top-level `add-resource(white, 15)`; apply-descriptor bypasses validator, walker fires it once). Then apply real recipe → walker double-fires `spend-resource` (walker descent + `fireOnRuleActivatedHooks`), each spending 5 → final WhiteScore=5. `place-piece(knight, white, sq=28)` also fires twice; renderer dedupes to 1 visible knight at e4. Asserts: engine `WhiteScore === 5`, white knight at e4, chip visible with text containing "5".
|
||||
- `tpl-cash-grab` — applied to `targetSquare: 4` (e1 white king) so `OnTurnEndHooks` lands on a piece. Walker pre-fires the inner add-resource leaves once → post-apply white=1/black=1. Drives 2 plies (white a2-a3 + black a7-a6 via 2nd context guest); each turn-end fires the hook once (color="both") → final 3/3. Chips visible with text containing "3"/"3".
|
||||
- **CRITICAL pre-existing finding**: `fireOnTurnEndHooks` iterates `eachPiece` and reads `OnTurnEndHooks` from EACH PIECE's id (NOT GAME_ENTITY). `apply-descriptor` defaults `applyTarget = GAME_ENTITY`; for per-piece trigger families (`on-turn-end`, `on-turn-start`, `on-move`, `on-capture`, …) the caller MUST pass `targetSquare` to resolve the apply-target to a real piece-id, otherwise the hook is seeded on GAME_ENTITY and the dispatcher never finds it. Same precedent as `parity-rules.spec.ts § all_on_red` (targets sq=4) and the W1.11 `__test__.setup-board.hooks` "piece-scoped only" gotcha. The brief's pre-seed-via-mini-descriptor strategy works for `add-resource` (no trigger root, walker fires it directly) but a recipe rooted at any per-piece trigger needs `targetSquare`.
|
||||
- **Score chip selectors**: `[data-testid="score-white"]` / `[data-testid="score-black"]`. Renders `⚪ <N>` / `⚫ <N>` (emoji + space + value); `toContainText("<N>")` absorbs the emoji wrapper. `hasEconomyActive` (GameView.tsx:327) is `whiteScore !== 0 || blackScore !== 0 || OnResourceChangedHooks present` — so a freshly-loaded game shows zero chips, and `tpl-cash-grab`'s walker-pre-fire alone (1/1) is enough to make both visible. `tpl-summoning-ritual`'s WhiteScore=5 makes the white chip visible; black stays hidden (BlackScore=0, no OnResourceChangedHooks).
|
||||
- **Walker double-fire behaviour for spend-resource**: `spend-resource` has `selfRecurse: true` (apply.ts:196 — walker stops descending once spend-resource is reached). But `fireOnRuleActivatedHooks` runs the on-rule-activated arm SEPARATELY post-walk, which walks into spend-resource AGAIN. Net: spend-resource is invoked twice from a single `applyCustomDescriptor` call. The 15→5 (rather than 15→10 with a single fire) is the locked observable.
|
||||
- **`Browser` type from @playwright/test** is the right import for shared helper signatures that take the test fixture's `browser` parameter. The brittle `Parameters<Parameters<typeof test>[1]>[0]['browser']` form fails LSP's `never` inference and forces inline guest-page setup.
|
||||
- **Test count delta (e2e only)**: wave5-economy.spec.ts ships 5 tests. All green on first invocation post-targetSquare fix.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue