feat(cardshed): bootstrap deterministic core rules engine (#139)#140
Conversation
Executes PRPs/cardshed-02-core-prp.md. Adds @lab/ll-CARDSHED/apps/core/
— a pure, side-effect-free TypeScript module encoding CARD SHED v2.0.
Engine surface (src/core/):
- types.ts Shared Contract + domain entities + view projections
- prng.ts mulberry32 + seeded Fisher-Yates (Math.random forbidden in core/)
- rules.ts full Rules Engine API (createDeck, shuffleDeck, dealInitialHands,
startNewRound, validateAttack, submitAttack, canBeat, submitBeat,
stopDefending, drawToMinimum, checkWin, advanceTurn*, getLegalActions,
createPublicView, createPrivateView, pendingAttackCardCount)
- index.ts public re-exports
Tests (78+ vitest tests across 14 files):
- deck, shuffle, attack-validation, can-beat, submit-{attack,beat},
stop-defending, draw-to-minimum, check-win, turn-flow, views, legal-actions
- conservation.property.test.ts: 4 fast-check properties × 200 iters each
(conservation, hidden-info, player-ref integrity, termination)
- integration: scripted 3- and 4-player rounds to RoundEnded
- scripts/sim-smoke.ts: 1000 random-legal-bot games (0 violations, 100%
terminate, <30s)
Tooling:
- tsconfig (ES2022, strict, bundler resolution, no DOM)
- vitest 2.1, fast-check 3.23, tsx 4
- eslint 9 flat config with no-restricted-syntax bans for Math.random /
Date.now / new Date / performance.now / crypto.getRandomValues in src/core/**
Cross-PRP resolutions (PRPs/cardshed-02-core-prp.md updated):
1. beatenPairs live in pendingAttack; flushed to discard on BOTH stopDefending
branches (not just FULL DEFENCE)
2. pendingAttackCardCount(pa) helper exported and used by invariants
3. checkWin runs after refill in BOTH stopDefending branches — defender can
win on round-trailing turn; PRP 3 must not assume "winner = attacker"
4. deck[0] = bottom (trump face), deck.pop() = top (drawn next)
5. Card.id = "{letter}-{rank}-{slotHex}[-{saltB36}]"; startNewRound salts ids
with round seed → deterministic but per-round disjoint id sets
Validation gates: tsc 0 errors · eslint 0 errors · vitest 82/82 · sim-smoke
1000/1000 reaches RoundEnded with 0 conservation violations in 3.7s.
PRPs/cardshed-02-core-prp.md force-added (PRPs/ is blanket-gitignored;
mirrors the precedent set by cardshed-01-blueprint.md in #137).
There was a problem hiding this comment.
Sorry @w7-mgfcode, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (28)
📝 WalkthroughWalkthroughThis PR implements the CARDSHED deterministic core rules engine in TypeScript, including immutable domain contracts, seeded randomness, pure state transitions, legal action enumeration, hidden-information view redaction, and comprehensive unit/property/integration test coverage plus a 1000-round smoke test validating invariants. ChangesCARDSHED Deterministic Core Rules Engine
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
* chore(repo): sync commit-format hook scope allow-list (#144) The hook regex in .claude/hooks/check-commit-format.sh was missing 'reliquary' and 'cardshed' — both added to .claude/rules/commit-format.md in #135 and #139 respectively, but the corresponding PRs landed via GitHub squash-merge, which bypasses local hooks. Drift went undetected until a local commit on either scope was attempted. One-line fix: extend SCOPE_ALLOW with both scopes. Smoke-tested locally with a stub JSON payload — the hook now ALLOWs 'chore(cardshed): ...' and 'feat(reliquary): ...' messages. * chore(cardshed): bootstrap @lab/ll-CARDSHED stack contract (#143) The CARD SHED deterministic core (PRP 2) landed in #139/#140 at apps/core/, but the stack lacked a W7-Base contract root: no .w7-meta, compose.yml, README, CLAUDE.md, AGENTS.md, BLUEPRINT.md, .claude/rules/, or .stitch/DESIGN.md placeholder. This commit scaffolds the stack root modelled on @lab/ll-RELIQUARY/: - .w7-meta + compose.yml (alpine stub, replaced at PRP 3 M1) + .env.example - README.md / CLAUDE.md / AGENTS.md / BLUEPRINT.md - .claude/rules/ui-design-pipeline.md (mandatory Stitch + agent-browser) - .claude/rules/core-determinism.md (encodes apps/core/ purity guarantees) - .stitch/DESIGN.md placeholder (real generation deferred to PRP 3 M1) - data/, dogfood-output/, docs/SCREENS/, docs/DECISIONS/ with .gitkeep Force-added .w7-meta / .claude/ / data/ / dogfood-output/ since root .gitignore blanket-ignores those paths (same convention as ll-RELIQUARY). Compose config validates; all three policy scripts (prod-privileged, prod-no-root-mount, zone-ingress-naming) exit 0. apps/core/ tests remain 82/82 green (no changes to PRP 2 surface).
Summary
PRPs/cardshed-02-core-prp.md— adds@lab/ll-CARDSHED/apps/core/, a pure, side-effect-free TypeScript rules engine encoding CARD SHED v2.0 (~2000 LoC engine + tests, 0 runtime deps).cardshedto.claude/rules/commit-format.mdallow-list (mirrors howreliquarywas added in feat(reliquary): bootstrap @lab/ll-RELIQUARY browser collectable-artifact game #133).PRPs/cardshed-02-core-prp.mdwith all 5 cross-PRP contradictions markedResolved:— PRP 3 (cardshed-03-experience-prp.md) MUST consume these resolutions verbatim.Cross-PRP resolutions
beatenPairslive inpendingAttack;stopDefendingflushes todiscardon both branches (not just FULL DEFENCE)pendingAttackCardCount(pa)helper exported and used by invariant assertionscheckWinruns after refill in bothstopDefendingbranches — defender can win on round-trailing turn; PRP 3 must not assume "winner = current attacker"deck[0]= bottom (trump face),deck.pop()= top (drawn next)Card.id = "{letter}-{rank}-{slotHex}[-{saltB36}]";startNewRoundsalts ids with the round seed → deterministic but per-round disjoint id setsValidation gates (all pass on this branch)
npx tsc --noEmit— 0 errorsnpx eslint src— 0 errors; determinism bans active (Math.random / Date.now / new Date / performance.now / crypto.getRandomValues forbidden insrc/core/**)npx vitest run— 82/82 tests across 14 files, including 4 fast-check property tests at 200 iters each (conservation, hidden-info, player-ref integrity, termination)npx tsx scripts/sim-smoke.ts— 1000 random-legal-bot games / 0 conservation violations / 100% reachRoundEnded/ 3.7s on dev host (mean 92.2 turns/round, p95 125)Test plan
Resolved:clauses at the top ofPRPs/cardshed-02-core-prp.mdand acks each — especially T3 — compose.yml finalization #3 (defender-can-win) and T5 — Storage layer port (PostgREST → Gitea API) #5 (salted ids), which are operator decisions made during executioncardshedscope addition matches thereliquaryprecedent (one row in the allow-list table)cd @lab/ll-CARDSHED/apps/core && npm install && npm run typecheck && npm run lint && npm test && npm run sim-smokeOut of scope
cardshed-03-experience-prp.md)Summary by CodeRabbit