GameSession is the central orchestrator of a match in SET: 3D Edition. Every rule, every state transition, every score change, and every end-game condition flows through it. Nothing in the domain layer modifies game state except through GameSession. Understanding its structure — the commands it accepts, the states it moves through, and the events it emits — is the foundation for building or debugging any game-mode feature.
Pre-production notice. SET: 3D Edition is in pre-production. All state machine transitions, command signatures, and event types described here reflect the current design specification. Online multiplayer’s
WaitingForServer sub-state and the Nakama integration path are planned and not yet implemented.Two Interfaces, One Class
GameSession implements two interfaces that cleanly separate the direction of data flow:
StateStream and EventStream via R3 observables. They never call methods on GameSession directly — they only push IGameCommand objects through IMatchOrchestrator.HandleCommand. This strict separation is what makes GameSession unit-testable without any Unity dependency.
The MatchState Enum
GameSession is a finite state machine (FSM). At any moment, it is in exactly one of these states:
ValidSetAnim, InvalidSetAnim, RefillAnim, and ExpandBoardAnim. While the session is in any of these states, HandleCommand silently discards incoming commands (logs a warning, does not throw).
WaitingForServer (online play, planned). In the online multiplayer design, Validating encompasses an inner sub-state called WaitingForServer. When the player selects a third card, the session enters Validating and the claim is dispatched to the Nakama server. The session stays in Validating / WaitingForServer — cards highlighted, input locked — until the server broadcasts its verdict. The session then transitions to ValidSetAnim or InvalidSetAnim based on the server response. There is no separate top-level enum value for this sub-state; it is tracked internally by OnlineMatchController.State Machine Transitions
Every arrow in this diagram corresponds to a method or event thatGameSession processes inside HandleCommand or its internal helpers. There are no transitions that bypass GameSession.
Commands (Input Into GameSession)
All input is modelled as command objects that implementIGameCommand. This makes the input pipeline testable: inject any command in a unit test and assert the resulting state.
AnimationCompleteCommand is generated by the Presentation layer (e.g., a Unity Coroutine or DOTween callback) and fed back into HandleCommand. GameSession does not use Time.deltaTime or WaitForSeconds internally — it is entirely event-driven.Events Emitted (Output From GameSession)
EventStream carries discrete, named domain events that describe what happened. Presentation-layer code subscribes to specific event types for animations, toasts, and audio cues.
StateStream carries a GameStateSnapshot — a complete, immutable picture of the game at the moment of every state change. ViewModels use this to re-render the entire HUD without needing to subscribe to individual events.
GameStateSnapshot
BoardSnapshot and PlayerSnapshot are also immutable DTOs. GameSession constructs a fresh snapshot after every state change and pushes it — it never reuses or mutates the previous snapshot.
GameRules Value Object
GameRules is an immutable configuration object created before StartMatch is called. It never changes during a match.
MaxBoardSize is a constant 21 — it is exposed as a property so that code consuming GameRules does not need to hard-code the magic number, but its value is always 21 and cannot be overridden. InitialBoardSize can be 12, 15, or 18 (matching the preset configurations in the data layer).
Scoring and Penalties
Scoring logic lives insideGameSession (or a private ScoringHandler helper). The rules:
Score never goes below 0. Timer reaching zero from a penalty triggers an immediate
MatchEnd transition, the same as the timer expiring naturally.
Input Locking During Animations
Animation states (ValidSetAnim, InvalidSetAnim, RefillAnim, ExpandBoardAnim) all require locked input. The implementation is simple: HandleCommand checks the current state first.
Online Multiplayer (Planned)
In online play,Validating state has an inner sub-state: WaitingForServer. The flow:
- Player selects three cards →
GameSessionentersValidating. OnlineMatchController(Infrastructure layer) sends a claim message to the Nakama server.GameSessionwaits — cards remain visually highlighted but input is locked.- The Nakama server validates the claim authoritatively and broadcasts a result to all clients.
OnlineMatchControllerreceives the result and injects anApplyServerStateCommandintoGameSession.HandleCommand.GameSessiontransitions toValidSetAnimorInvalidSetAnimbased on the server’s verdict.
GameSession itself never imports Nakama or touches a WebSocket. The entire networking path is isolated in OnlineMatchController (Infrastructure), which only communicates with GameSession via IGameCommand.
Full Match Lifecycle Walkthrough
The following table traces a complete single-player match from start to finish:Implementation Checklist
1
HandleCommand is O(1) — no blocking
Every branch of
HandleCommand must complete in constant time. Never call Thread.Sleep, await, or any synchronous I/O inside HandleCommand. The session must remain responsive at all times.2
Input discarded during locked states
Commands received during
ValidSetAnim, InvalidSetAnim, RefillAnim, ExpandBoardAnim, or MatchEnd must be silently discarded. Log a warning. Do not throw, do not queue.3
AnySetExists called after every board mutation
After every call to
Board.PlaceCard, Board.RemoveCard, or Board.Expand, GameSession must call ISetValidator.AnySetExists(). Missing even one call allows the game to get stuck.4
End-game condition checked after every state transition
After every state change, verify the end-game conditions: deck empty + no Set, board at 21 + no Set, or timer ≤ 0. Any one of these must immediately transition to
MatchEnd and emit MatchEndedEvent.5
StateStream emits on every state change
Every state transition — including selecting individual cards — must push a new
GameStateSnapshot to StateStream. ViewModels depend on every push to re-render correctly.6
EventStream emits distinct domain events
EventStream must emit SetClaimedEvent, BoardRefilledEvent, ScoreChangedEvent, etc. as discrete events — not bundled with the snapshot. Animation and audio systems subscribe to specific event types.7
No UnityEngine or Nakama references
The
GameSession class must compile in a pure .NET context. Add an assembly definition with a reference whitelist that excludes UnityEngine and Nakama assemblies.8
Fully unit-testable with mocked dependencies
GameSession’s constructor accepts ISetValidator and IAIScanner. In tests, inject mock implementations. Write tests that send a sequence of SelectCardCommand objects and assert the resulting state and emitted events.Common Mistakes
Related Pages
Card Model
The Card and CardAttributes types that GameSession manipulates through Board and Deck.
Set Validation
ISetValidator — the domain service GameSession calls on every claim and board mutation.
Board & Dealing
How Board manages card slots, and how GameSession drives refill and expansion.
AI Opponents
How IAIScanner integrates with GameSession’s Update loop and command pipeline.