refactor(domain): reshape the game to pure deciders in vertical slices (#301)#307
Merged
Conversation
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
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
Contributor
Storyboard screenshots ✅ passed🖼️ View the multi-viewport storyboard — mobile, medium and large desktop frames. |
Contributor
E2E Happy-Path ✅ passed |
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #301. Part of epic #295 (go native on the Critter Stack).
Reshapes all seven game command groups into pure
Decide(command, state) -> eventsfunctions organized as vertical slices undersrc/Farkle/Features/<Command>/, and locks decider purity in with architecture tests. Behaviour-preserving — the EventuousGameaggregate 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)
IRandom) happens in the aggregate, the rolled dice are passed into the pure decider; dice count is now a pureGameState.DiceToRollPlayerCanPassreadinggame.Currentwith a pureGameState.HasActedThisTurnflag (folded from roll/keep, reset on pass)Also adds a pure
GameState.Fold(reusing the existingHandle*methods) — used by the decider tests now, and by the MartenSingleStreamProjectionin #302.Guardrails (permanent)
KeepDecidersPureAndFrameworkFree— every*Deciderreferences 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
0001vertical slices + pure deciders + purity-by-arch-test.0002Marten stream identity (string key"game-{code}", applied at the Critter Stack (2/6): cut over to native Marten + Wolverine and delete Eventuous/ESDB #302 cutover).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
PassTurncould applyGameWoneven 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
verify-generated(no contract drift), two-player E2E happy path, storyboard.🤖 Generated with Claude Code