No description
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)
|
||
|---|---|---|
| .github/workflows | ||
| .sisyphus | ||
| docs | ||
| packages | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| Dockerfile.dev | ||
| eslint.config.js | ||
| lefthook.yml | ||
| LICENSE | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
| vitest.workspace.ts | ||
@paratype
A Doorenbos-style Rete II rules engine for TypeScript games, with an authoritative WebSocket chess demo.
Packages
packages/rete— Rete II engine corepackages/chess— Browser chess demo (React + Vite)packages/server— Authoritative Bun WebSocket server
Docs
- SPEC.md — Engine specification
- PHASES.md — Development phases & perf budgets
- RULES.md — Chess rule presets
- PROTOCOL.md — WebSocket message protocol
Getting Started
bun install && bun run check