Skip to content

feat(cardshed): bootstrap deterministic core rules engine (#139)#140

Merged
w7-mgfcode merged 2 commits into
masterfrom
feat/cardshed-core
May 21, 2026
Merged

feat(cardshed): bootstrap deterministic core rules engine (#139)#140
w7-mgfcode merged 2 commits into
masterfrom
feat/cardshed-core

Conversation

@w7-mgfcode
Copy link
Copy Markdown
Owner

@w7-mgfcode w7-mgfcode commented May 21, 2026

Summary

  • Executes 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).
  • Adds cardshed to .claude/rules/commit-format.md allow-list (mirrors how reliquary was added in feat(reliquary): bootstrap @lab/ll-RELIQUARY browser collectable-artifact game #133).
  • Force-adds PRPs/cardshed-02-core-prp.md with all 5 cross-PRP contradictions marked Resolved: — PRP 3 (cardshed-03-experience-prp.md) MUST consume these resolutions verbatim.

Cross-PRP resolutions

  1. beatenPairs live in pendingAttack; stopDefending flushes to discard on both branches (not just FULL DEFENCE)
  2. pendingAttackCardCount(pa) helper exported and used by invariant assertions
  3. checkWin runs after refill in both stopDefending branches — defender can win on round-trailing turn; PRP 3 must not assume "winner = current attacker"
  4. deck[0] = bottom (trump face), deck.pop() = top (drawn next)
  5. Card.id = "{letter}-{rank}-{slotHex}[-{saltB36}]"; startNewRound salts ids with the round seed → deterministic but per-round disjoint id sets

Validation gates (all pass on this branch)

  • npx tsc --noEmit — 0 errors
  • npx eslint src — 0 errors; determinism bans active (Math.random / Date.now / new Date / performance.now / crypto.getRandomValues forbidden in src/core/**)
  • npx vitest run82/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% reach RoundEnded / 3.7s on dev host (mean 92.2 turns/round, p95 125)

Test plan

  • CI: typecheck + lint + vitest + sim-smoke all green
  • Reviewer reads the 5 Resolved: clauses at the top of PRPs/cardshed-02-core-prp.md and 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 execution
  • Confirm cardshed scope addition matches the reliquary precedent (one row in the allow-list table)
  • Manual: cd @lab/ll-CARDSHED/apps/core && npm install && npm run typecheck && npm run lint && npm test && npm run sim-smoke

Out of scope

  • UI / networking / AI / analytics (PRP 3 — cardshed-03-experience-prp.md)
  • Rust port (deferred per PRP 1 blueprint)
  • Stack scaffold (Compose, db, cache) — core is pure logic, no runtime services

Summary by CodeRabbit

  • Chores
    • Established core game engine and rules system with comprehensive test coverage for validating game mechanics and state invariants.
    • Configured development environment with TypeScript, ESLint, and Vitest infrastructure.

Review Change Stack

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).
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @w7-mgfcode, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c65ea403-1eae-4d43-85c2-adb88b11db4e

📥 Commits

Reviewing files that changed from the base of the PR and between 074c41a and 481aa73.

⛔ Files ignored due to path filters (2)
  • @lab/ll-CARDSHED/apps/core/package-lock.json is excluded by !**/package-lock.json
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/__snapshots__/shuffle.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (28)
  • .claude/rules/commit-format.md
  • @lab/ll-CARDSHED/apps/core/.gitignore
  • @lab/ll-CARDSHED/apps/core/eslint.config.js
  • @lab/ll-CARDSHED/apps/core/package.json
  • @lab/ll-CARDSHED/apps/core/scripts/sim-smoke.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/_bot.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/_helpers.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/attack-validation.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/can-beat.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/check-win.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/conservation.property.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/deck.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/draw-to-minimum.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/integration.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/legal-actions.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/shuffle.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/stop-defending.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/submit-attack.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/submit-beat.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/turn-flow.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/__tests__/views.test.ts
  • @lab/ll-CARDSHED/apps/core/src/core/index.ts
  • @lab/ll-CARDSHED/apps/core/src/core/prng.ts
  • @lab/ll-CARDSHED/apps/core/src/core/rules.ts
  • @lab/ll-CARDSHED/apps/core/src/core/types.ts
  • @lab/ll-CARDSHED/apps/core/tsconfig.json
  • @lab/ll-CARDSHED/apps/core/vitest.config.ts
  • PRPs/cardshed-02-core-prp.md

📝 Walkthrough

Walkthrough

This 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.

Changes

CARDSHED Deterministic Core Rules Engine

Layer / File(s) Summary
Domain contracts and documentation
src/core/types.ts, PRPs/cardshed-02-core-prp.md, .claude/rules/commit-format.md
Frozen domain model defining card primitives, game state structures (Player, RoundState, MatchState), action/event unions, and view-model projections; comprehensive PRP specification resolving contradictions and detailing implementation pseudocode and acceptance criteria.
Deterministic PRNG and seeded shuffle
src/core/prng.ts
32-bit Mulberry32 seeded PRNG returning uniform [0,1) floats and immutable Fisher–Yates shuffle on copied arrays for reproducible randomization.
Game rules engine and state transitions
src/core/rules.ts
Complete 672-line rules module: deck creation/shuffling with salted IDs, initial dealing, trump selection, round initialization with dealer/attacker rotation, attack/beat/stop validation and execution, win conditions, draw-to-minimum refill, legal action enumeration (1/3/5-card attacks and all beat options), and view creation with hidden-information redaction.
Public API barrel and build configuration
src/core/index.ts, package.json, tsconfig.json, eslint.config.js, vitest.config.ts, .gitignore
Public-facing barrel curating types, rules, and PRNG utilities; TypeScript strict config (ES2022, bundler resolution), ESLint flat config with determinism bans (no Math.random, Date.now, crypto.getRandomValues), Vitest test runner with 20s timeout, and standard dependency/build artifact ignores.
Test infrastructure and bot helpers
src/core/__tests__/_helpers.ts, src/core/__tests__/_bot.ts
setupMatch initializes players and rounds; buildState constructs controlled test scenarios; card factory c generates deterministic IDs; totalCards and allCardIds implement conservation checkers; runRandomLegalRound executes deterministic bot-driven rounds with state history and terminal status.
Unit test coverage for core rules
src/core/__tests__/deck.test.ts, src/core/__tests__/shuffle.test.ts, src/core/__tests__/attack-validation.test.ts, src/core/__tests__/can-beat.test.ts, src/core/__tests__/check-win.test.ts, src/core/__tests__/draw-to-minimum.test.ts
Vitest suite validating deck creation (52 unique cards, 4 suits × 13 ranks, salted ID format), shuffle determinism and immutability, attack size/pair constraints (1/3/5 cards with pair rules), card-beating logic (same-suit, trump, non-trump), win condition (empty deck and hand), and refill behavior without array mutation.
State transition and turn-flow tests
src/core/__tests__/submit-attack.test.ts, src/core/__tests__/submit-beat.test.ts, src/core/__tests__/stop-defending.test.ts, src/core/__tests__/turn-flow.test.ts
Vitest suite covering submitAttack success (phase/defender/pending) and errors (wrong phase/caller/shape), submitBeat beat legality and phase transitions, stopDefending full/partial defense with refill and win triggers, and turn-flow dealer/attacker rotation with state immutability.
Legal action enumeration and view creation tests
src/core/__tests__/legal-actions.test.ts, src/core/__tests__/views.test.ts
Tests for getLegalActions enumerating unique attacks (1/3/5-card) and all valid beats plus stop; createPublicView redacting opponent hands while exposing counts/trump/phase; createPrivateView revealing viewer's own hand and redacting others; immutability of returned projections.
Integration and property-based test validation
src/core/__tests__/integration.test.ts, src/core/__tests__/conservation.property.test.ts
End-to-end 3/4-player round runs with winner detection and per-state conservation checks; fast-check property tests (200 runs) validating card conservation (52 total, unique IDs), public view redaction, round-reference validity, and round termination under random legal actions.
Large-scale simulation smoke test
scripts/sim-smoke.ts
1000-round randomized simulation with alternating player counts validating card conservation, round termination, and unique card IDs across all states; computes and reports mean/p50/p95 turn counts, elapsed time, and exits non-zero on invariant violations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


Possibly related issues


Poem

🎰 A shuffle here, a deal just right,
With types so pure and logic tight,
Fifty-two cards, no random fright—
The core rules engine shines so bright! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cardshed-core

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@w7-mgfcode w7-mgfcode merged commit 926ba71 into master May 21, 2026
13 of 14 checks passed
@w7-mgfcode w7-mgfcode deleted the feat/cardshed-core branch May 21, 2026 18:22
w7-mgfcode added a commit that referenced this pull request May 21, 2026
* 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant