Bootstrap the CARD SHED deterministic core rules engine in @lab/ll-CARDSHED/apps/core/, executing PRPs/cardshed-02-core-prp.md.
Scope
Pure-logic TypeScript rules engine encoding CARD SHED v2.0:
src/core/types.ts — Shared Contract + domain entities (Card, Player, PendingAttack, RoundState, MatchState, Action, GameEvent, view projections)
src/core/prng.ts — mulberry32 + seeded Fisher-Yates
src/core/rules.ts — full Rules Engine API surface (createDeck, shuffleDeck, dealInitialHands, startNewRound, validateAttack, submitAttack, canBeat, submitBeat, stopDefending, drawToMinimum, checkWin, advanceTurn*, getLegalActions, createPublicView, createPrivateView, pendingAttackCardCount)
src/core/index.ts — public re-exports
src/core/__tests__/ — 14 vitest files (78+ tests, including 4 property-based ≥200 iters via fast-check)
scripts/sim-smoke.ts — 1000-game random-legal-bot driver
Cross-PRP resolutions
All 5 contradictions flagged at the top of PRPs/cardshed-02-core-prp.md are now Resolved: and the PRP is updated. PRP 3 (cardshed-03-experience-prp.md) MUST consume these resolutions verbatim:
beatenPairs live in pendingAttack; stopDefending flushes to discard on both branches
pendingAttackCardCount(pa) helper exported and used by invariants
checkWin runs after refill in both stopDefending branches — a defender can win on the 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}]"; startNewRound salts ids with the round seed → per-round disjoint id sets, deterministic
Validation gates (all pass)
- ✅
npx tsc --noEmit — 0 errors
- ✅
npx eslint src — 0 errors, determinism bans (Math.random/Date.now/new Date/performance.now/crypto.getRandomValues) active
- ✅
npx vitest run — 82/82 tests across 14 files (200-iter property tests for conservation/hidden-info/termination)
- ✅
npx tsx scripts/sim-smoke.ts — 1000 random-legal-bot games / 0 conservation violations / 100% reach RoundEnded / 3.7s (mean 92.2 turns/round)
Required scope addition
Add cardshed to .claude/rules/commit-format.md allow-list (mirrors how reliquary was added in #133).
Out of scope
- UI / networking / AI / analytics — all in PRP 3 (
cardshed-03-experience-prp.md)
- Rust port — deferred per PRP 1 (
cardshed-01-blueprint.md)
- Stack scaffold (Compose, db, cache) — core is pure logic, no runtime services
Bootstrap the CARD SHED deterministic core rules engine in
@lab/ll-CARDSHED/apps/core/, executingPRPs/cardshed-02-core-prp.md.Scope
Pure-logic TypeScript rules engine encoding CARD SHED v2.0:
src/core/types.ts— Shared Contract + domain entities (Card, Player, PendingAttack, RoundState, MatchState, Action, GameEvent, view projections)src/core/prng.ts— mulberry32 + seeded Fisher-Yatessrc/core/rules.ts— full Rules Engine API surface (createDeck, shuffleDeck, dealInitialHands, startNewRound, validateAttack, submitAttack, canBeat, submitBeat, stopDefending, drawToMinimum, checkWin, advanceTurn*, getLegalActions, createPublicView, createPrivateView, pendingAttackCardCount)src/core/index.ts— public re-exportssrc/core/__tests__/— 14 vitest files (78+ tests, including 4 property-based ≥200 iters via fast-check)scripts/sim-smoke.ts— 1000-game random-legal-bot driverCross-PRP resolutions
All 5 contradictions flagged at the top of
PRPs/cardshed-02-core-prp.mdare nowResolved:and the PRP is updated. PRP 3 (cardshed-03-experience-prp.md) MUST consume these resolutions verbatim:beatenPairslive inpendingAttack;stopDefendingflushes todiscardon both branchespendingAttackCardCount(pa)helper exported and used by invariantscheckWinruns after refill in bothstopDefendingbranches — a defender can win on the 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 → per-round disjoint id sets, deterministicValidation gates (all pass)
npx tsc --noEmit— 0 errorsnpx eslint src— 0 errors, determinism bans (Math.random/Date.now/new Date/performance.now/crypto.getRandomValues) activenpx vitest run— 82/82 tests across 14 files (200-iter property tests for conservation/hidden-info/termination)npx tsx scripts/sim-smoke.ts— 1000 random-legal-bot games / 0 conservation violations / 100% reach RoundEnded / 3.7s (mean 92.2 turns/round)Required scope addition
Add
cardshedto.claude/rules/commit-format.mdallow-list (mirrors howreliquarywas added in #133).Out of scope
cardshed-03-experience-prp.md)cardshed-01-blueprint.md)