GameSession is wired, you know how every session-like class is wired.
The diagram below shows how the adopted patterns interact at runtime. Dependencies flow inward — outer layers depend on inner layers via interfaces, never the reverse.
Adopted Patterns
Pattern Detail: Clean Architecture Layering
The project is split into four concentric layers. Dependencies flow inward only — outer layers know about inner layers, never the reverse.
Why this matters in practice:
SetValidator has no using UnityEngine;. This means you can run the entire game loop in a unit test in milliseconds with no Unity process. It also means the domain logic can be ported cleanly to the Nakama server side as the authoritative reference.
Review checklist:
- No
using UnityEngine;inSET.DomainorSET.Application - No
using Nakama;inSET.DomainorSET.Application - Infrastructure classes implement interfaces declared in Application or Domain
-
SET.Presentationdoes not importSET.Infrastructuredirectly — only through interfaces
Pattern Detail: Constructor Injection (VContainer)
All dependencies are declared as constructor parameters and injected by VContainer at startup. No class creates its own dependencies withnew ConcreteClass() (except in factories, which exist precisely to own that responsibility).
- No
new ConcreteClass()inside Application or Domain methods - All injected fields are interface types, not concrete types
- All DI bindings are in the Bootstrap scene — no scattered registration
Pattern Detail: Command / Query Separation (CQS)
User input and AI decisions become commands — immutable value objects sent toGameSession.HandleCommand(). They have no return value. State is read separately as queries — GameSession exposes an IObservable<GameStateSnapshot> that pushes a new immutable snapshot after every state change.
- Commands are immutable records — no mutable properties
- No command type has a return value (use
IObservable<MatchEvent>for async feedback) - Queries return read-only data — no mutable domain objects leak through a snapshot
Pattern Detail: Finite State Machine (GameSession)
GameSession uses an explicit MatchState enum and a guarded HandleCommand switch. Any command received in the wrong state is silently discarded — no exceptions, no corrupted state.
If you find yourself adding an
else branch that does something when the state is wrong, stop. The FSM is supposed to silently discard invalid inputs. If a bad state is reached, log a warning — don’t try to recover ad hoc.- Every valid transition is explicitly listed in the switch
- Input is blocked (discarded) during animation lock states
-
AnySetExists()is called after every board mutation - End-game condition is checked after every state transition
Pattern Detail: Adapter (Wrapping External SDKs)
Nakama, Google Play Games Services, and any other third-party SDK lives entirely insideSET.Infrastructure. The Application layer declares the interface; the Infrastructure layer implements it.
- No Nakama types (
IMatch,IMatchState,ISocket) appear in Application or Domain assemblies - Adapter methods are thin wrappers; domain logic stays in
GameSession - The interface is defined in the layer that consumes it, not the layer that implements it
Banned Anti-Patterns
Every entry in this table has been considered and explicitly rejected. If you recognise one of these in existing code, open a refactor ticket — don’t extend it.Pattern Decision Matrix
When you are unsure which pattern to reach for, use this table.Related Pages
Coding Conventions
Naming, formatting, and class design rules that complement these patterns.
Testing Standards
How to test pattern-compliant code with NSubstitute mocks.
PR Checklist
The review checklist that enforces pattern compliance before merge.
Phase Breakdown
Which phases introduce which patterns first.