Skip to content

refactor(domain): reshape the game to pure deciders in vertical slices (#301)#307

Merged
david-acm merged 10 commits into
mainfrom
claude/issue-301-decider-slices
Jul 6, 2026
Merged

refactor(domain): reshape the game to pure deciders in vertical slices (#301)#307
david-acm merged 10 commits into
mainfrom
claude/issue-301-decider-slices

Conversation

@david-acm

@david-acm david-acm commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Closes #301. Part of epic #295 (go native on the Critter Stack).

Reshapes all seven game command groups into pure Decide(command, state) -> events functions organized as vertical slices under src/Farkle/Features/<Command>/, and locks decider purity in with architecture tests. Behaviour-preserving — the Eventuous Game aggregate delegates each command to its decider, so nothing observable changes; the decision logic becomes framework-free and mock-free to test.

Deciders (all seven slices)

Slice Notable
StartGame pattern-setter
JoinPlayer derives player id + colour from state
BeginGame composes the existing validator primitives
RollDice side effect extracted — the roll (IRandom) happens in the aggregate, the rolled dice are passed into the pure decider; dice count is now a pure GameState.DiceToRoll
KeepDice scoring + table-center as pure logic
SetDiceAside / ReturnDice transient selection
PassTurn event-history peek → pure state: replaces PlayerCanPass reading game.Current with a pure GameState.HasActedThisTurn flag (folded from roll/keep, reset on pass)

Also adds a pure GameState.Fold (reusing the existing Handle* methods) — used by the decider tests now, and by the Marten SingleStreamProjection in #302.

Guardrails (permanent)

  • KeepDecidersPureAndFrameworkFree — every *Decider references no Eventuous/Marten/Wolverine/ASP.NET/FastEndpoints/Npgsql. Purity by arch-test, not by an assembly boundary, so slices keep their locality (ADR 0001).
  • KeepSlicesOffTheApplicationAndInfrastructureLayers — slices point inward only.

ADRs

Scope note

The literal removal of the Eventuous base types (GameId : Id, GameState : State<>, Game : Aggregate<>, [EventType]) is deliberately folded into #302, not done here. Mapping the coupling showed it requires a throwaway adapter that previews — then deletes — much of the #302 cutover. The decider-purity guardrail already proves the decision logic is framework-free today; the base types come off naturally when #302 deletes Eventuous. (Discussed and agreed in-session; #301 acceptance updated to match, #302 absorbs the base removal.)

Behaviour note

The old PassTurn could apply GameWon even on a failed pass (an unconditional threshold check after the validating apply). The pure decider validates first, so a rejected pass emits only the error event. No test relied on the old path (full suite green); intentional latent-bug fix.

Testing

  • Domain unit: 167 passing (+24 pure decider tests, zero mocks); full pre-existing suite stays green (behaviour preserved).
  • Architecture: 12 passing (+2 new guardrails).
  • CI (green on this branch): unit + integration (real Postgres + EventStore: HTTP, SignalR broadcast, Eventuous round-trip), verify-generated (no contract drift), two-player E2E happy path, storyboard.
  • Build clean (0 warnings, warnings-as-errors on).

🤖 Generated with Claude Code

claude added 8 commits July 6, 2026 20:14
First step of the decider refactor (#301): specify StartGame as a pure
(command, state) -> events function, tested with no aggregate and no mocks.
Fails to compile until StartGameDecider exists (green in the next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
… (green)

Introduces the vertical-slice + decider pattern for the Critter Stack
migration (#301): a pure StartGameDecider.Decide(command, state) -> events
(validation-as-events included) living in Features/StartGame/. The Eventuous
Game aggregate now delegates Start to it via base.Apply, so behaviour is
unchanged (existing StartShould tests stay green) while the decision logic
is framework-free and mock-free to test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
…s (red)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
…ameState.Fold (green)

Continues the decider refactor (#301). Adds a pure GameState.Fold (reusing the
existing Handle* methods) so decider tests — and the Marten projection in #302 —
can rebuild state without Eventuous. JoinPlayer and BeginGame join StartGame as
pure Decide(command, state) -> events functions in their Features/ slices, with
the aggregate delegating. Behaviour unchanged; existing tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
…turnDice (red)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
…rs (green)

Continues the decider refactor (#301). RollDice is the first slice with an
extracted side effect: the roll (IRandom) happens in the aggregate and the
already-rolled Dice are passed into the pure decider, with the dice count now a
pure GameState.DiceToRoll shared by handler and decider. SetDiceAside/ReturnDice
follow the same shape. Aggregate delegates; behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
…isTurn state (green)

Completes the decider extraction for all seven command groups (#301). KeepDice
carries the scoring + table-center logic as a pure decision. PassTurn replaces
the old event-history peek (PlayerCanPass reading game.Current) with a pure
GameState.HasActedThisTurn flag folded from roll/keep and reset on pass — so
"can I pass?" is now a pure (command, state) decision. The full existing suite
stays green, confirming behaviour is preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Storyboard screenshots ✅ passed

🖼️ View the multi-viewport storyboard — mobile, medium and large desktop frames.

All runs · Actions run

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

E2E Happy-Path ✅ passed

▶️ Watch the recordings inline — plays in your browser, no download needed.

All runs · Actions run

claude added 2 commits July 6, 2026 21:02
…301)

Two permanent guardrails for the vertical-slice model: KeepDecidersPureAnd
FrameworkFree asserts every *Decider references no Eventuous/Marten/Wolverine/
ASP.NET/FastEndpoints/Npgsql (purity by test, not by project boundary, so slices
keep their locality), and KeepSlicesOffTheApplicationAndInfrastructureLayers
asserts slices point inward only. Both pass today — the deciders are already pure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
…ns (#301)

ADR 0001 captures the vertical-slice + pure-decider model and the decision to
enforce decider purity by arch-test rather than an assembly boundary. ADR 0002
records the Marten stream-identity choice (string key "game-{code}") to be
applied at the #302 cutover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRcQ2yyaPBfYbpVpy8fWLg
@david-acm david-acm changed the title refactor(domain): reshape the game to pure deciders in vertical slices (#301, phase 1) refactor(domain): reshape the game to pure deciders in vertical slices (#301) Jul 6, 2026
@david-acm david-acm marked this pull request as ready for review July 6, 2026 21:18
@david-acm david-acm merged commit 707722e into main Jul 6, 2026
8 checks passed
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.

Critter Stack (1/6): decider domain + vertical-slice layout — pure functions, framework-free

2 participants