Key responsibilities
This page owns three outcomes for every engineer reading it:Why layer ownership matters
Without clear ownership, coupling accumulates in ways that are easy to introduce and expensive to remove. A MonoBehaviour that reaches directly intoNakamaMultiplayerService works fine until you need to test that MonoBehaviour — at which point you discover you can’t do it without a running Nakama server. A Domain class that references UnityEngine.Debug works fine until you try to port the validation logic to a Nakama server runtime — at which point it won’t compile.
Layer boundaries are what make AI, local, and online multiplayer modes run the same code path. They are not bureaucratic overhead; they are the mechanism by which the project’s “one core, three modes” goal is achievable.
Domain layer — SET.Domain
The innermost layer. Contains everything needed to represent and validate a game of SET with zero external dependencies. If a type can be expressed purely in C# without importing any third-party package, it belongs here.
What Domain owns
ISetValidator — the core domain service (Planned)
Validate checks whether exactly three cards form a valid Set. FindAllSets returns every valid Set on the current board — used by the hint system and AI scanner. AnySetExists is a fast early-exit check used before board expansion decisions.
Rules
- Zero external dependencies — no
using UnityEngine;, nousing Nakama;, nousing R3;. The.asmdeffile forSET.Domainlists no references. - All value objects are immutable —
CardAttributes,GameRules, andSetResultarereadonly structtypes that implementIEquatable<T>. They cannot be mutated after construction. Matchis the aggregate root — code outside the Domain layer interacts with game state exclusively throughMatch’s public API. No external class reaches intoBoardorDeckdirectly by bypassingMatch.SetValidatoris a stateless domain service — it is deterministic, has no side effects, and is safe to call in parallel. The same implementation is used for client-side hint display, local validation in single-player, and server-side authoritative validation in multiplayer.
Card identity
Each of the 81 cards has a uniqueCardId in the range 0–80, computed from its attributes:
What Domain is forbidden from doing
- Referencing
UnityEngine,Nakama,R3, or any third-party package - Holding mutable state in value objects
- Throwing exceptions to represent invalid game inputs (use
SetResult.IsValid = falseinstead) - Knowing that
GameSessionor any application-layer orchestrator exists
Application layer — SET.Application
The orchestration layer. GameSession lives here — it is the central state machine for a match. Application code coordinates Domain objects in response to commands and emits the results as observable streams. It depends only on SET.Domain.
What Application owns
GameSession — the central state machine
GameSession is the heart of a match. It receives IGameCommand objects, transitions the MatchState enum, coordinates Board, Deck, and ISetValidator, and emits the updated state and events as R3 observables.
Constructor injection signature — GameSession never constructs its dependencies itself:
IMatchOrchestrator — the command entry point
HandleCommand is the single entry point for all game input — card selections, claim attempts, forfeit requests. The IGameCommand type hierarchy models each action as a distinct, serialisable value object.
IGameStateProvider — the observable output
StateStream emits a full GameStateSnapshot every time any game state changes. EventStream emits discrete semantic events — SetClaimed, BoardRefilled, PenaltyApplied, MatchEnded — that Presentation uses to trigger animations and audio cues.
Online multiplayer separation
GameSession does not call IMultiplayerService directly. In online multiplayer (Planned), a separate OnlineMatchController (which lives in Infrastructure) acts as the bridge: it translates incoming IObservable<ServerMessage> messages from Nakama into IGameCommand objects and forwards them to GameSession via HandleCommand. This separation ensures GameSession is identical whether running locally or in network-mirror mode.
What Application is forbidden from doing
- Referencing
UnityEngine,Nakama, orR3concretely (R3’sIObservable<T>interface fromSystem.Reactiveor R3’s own namespace is acceptable for stream type declarations) - Instantiating
NakamaMultiplayerServiceor any Infrastructure class - Knowing how commands originate (touch input vs. network message vs. AI — all arrive identically as
IGameCommand) - Calling UI code or modifying visual state directly
Infrastructure layer — SET.Infrastructure
Implements every interface defined in Application that requires an external SDK, file system access, or platform API. Infrastructure is allowed to reference both SET.Domain and SET.Application (to know the interfaces it must implement), as well as Unity Engine APIs and the Nakama SDK.
What Infrastructure owns
Key adapter interfaces
The Nakama boundary rule
All Nakama SDK types (IMatch, IApiUser, IMatchData, etc.) must be converted into plain domain DTOs before they are passed to Application or Domain. Infrastructure classes are the only ones that may hold references to Nakama types. This ensures that if the backend changes from Nakama to another service, Application and Domain are untouched.
What Infrastructure is forbidden from doing
- Being referenced at compile time by
SET.Presentation(Presentation knows only the interfaces) - Passing raw Nakama types to Application or Domain
- Containing game logic — Infrastructure is plumbing, not rules
Presentation layer — SET.Presentation
Everything Unity-dependent: MonoBehaviours, R3 ViewModels, scene prefabs, VFX controllers, and the DI composition root. Presentation depends on Domain and Application but has no compile-time reference to Infrastructure — it receives Infrastructure implementations via VContainer at runtime.
What Presentation owns
MonoBehaviours are thin views
Every MonoBehaviour in Presentation must be a pure view. It binds UI elements to ViewModel reactive properties inStart() or Awake(), and disposes subscriptions in OnDestroy(). Zero game logic.
Example ViewModel binding pattern:
TouchInputHandler — input as commands
TouchInputHandler translates Unity touch events into IGameCommand objects and makes them available as an IObservable<IGameCommand> stream that GameSession subscribes to via IInputHandler. Input is pushed — the handler fires when the user acts, not when the game polls.
Bootstrap — the composition root
TheBootstrap scene contains one LifetimeScope MonoBehaviour that registers all DI bindings. It is the only place in the entire codebase where concrete Infrastructure types are named — everywhere else, code depends on interfaces.
What Presentation is forbidden from doing
- Referencing
SET.Infrastructureat compile time - Containing game rules, scoring logic, or state transition decisions
- Calling
ISetValidator.Validate()directly (validation results arrive viaStateStream) - Using
FindObjectOfType,GameObject.Find, or static singletons
Cross-layer communication summary
All runtime communication between layers follows these four patterns:Implementation checklist
Use this checklist when adding a new cross-layer feature:- New domain type? Add it to
_Project/Domain/with no external references - New application interface? Define it in
_Project/Application/Services/— Infrastructure implements it, Presentation consumes it - New Infrastructure implementation? Implement the Application interface in
_Project/Infrastructure/and register it in Bootstrap - New Presentation view? Create a thin MonoBehaviour in
_Project/Presentation/Views/and a ViewModel in_Project/Presentation/ViewModels/; bind with R3 and dispose inOnDestroy() - No
using UnityEngine;in Domain or Application - No
using Nakama;in Domain, Application, or Presentation - Unit tests for any new Domain or Application logic in
_Tests/EditMode/
Common mistakes
Related pages
Architecture Overview
The four-layer model, dependency graph, and client-server mode switching.
Repo Tour
Physical folder structure and assembly definition map.
DI with VContainer
How Bootstrap wires all layer interfaces to their concrete implementations.
Reactive UI Pipeline
R3 observable streams, ViewModel patterns, and subscription lifecycle.