Getting Started
Do I need Nakama running to work on the project?
Do I need Nakama running to work on the project?
dotnet test setup). Nakama only becomes relevant when you reach the Infrastructure layer’s multiplayer implementation.See Multiplayer: Authority Model for how the client and server are separated.Why Unity 2022 LTS and not 2023 or Unity 6?
Why Unity 2022 LTS and not 2023 or Unity 6?
- Long support window — LTS releases receive bug-fix patches for two years, which matters for a project with a multi-phase roadmap.
- Known compatibility — the Nakama .NET SDK and the R3 reactive library were both verified against Unity 2022 LTS. Newer engine versions had unresolved compatibility questions at the time the decision was made.
- Fewer surprises — LTS releases are more stable than feature releases. A card game does not benefit from bleeding-edge rendering features, so there is no upside to accepting an unstable engine.
Can I use ECS/DOTS instead of OOP for game logic?
Can I use ECS/DOTS instead of OOP for game logic?
- Scale: The game has at most 21 interactive cards on screen. ECS is designed to process tens of thousands of entities per frame. The threshold where ECS outperforms OOP is never reached here.
- Testability: OOP domain objects are straightforward to unit-test with a standard test runner. ECS requires specific Unity package versions and the ECS test framework, which significantly increases test-environment complexity.
- Architecture fit: Clean Architecture with constructor injection and interfaces is incompatible with ECS’s data-oriented component model. Mixing them would undermine both patterns.
Architecture Questions
Why can't I add a UnityEngine reference to the Domain project?
Why can't I add a UnityEngine reference to the Domain project?
Card, SetValidator, Match, GameRules, and all the rest — contains the rules of the game. These rules need to run:- In Unity on Android (the game client).
- In unit tests on a developer’s machine, with no Unity Editor open.
- Potentially server-side in the Nakama TypeScript match handler.
UnityEngine is referenced in Domain, tests break the moment they’re run outside the Unity Editor, server-side reuse becomes impossible, and the layer boundary is compromised. Unity is a deployment target for the rules engine, not a dependency of it.If you need a Unity type in logic that belongs in Domain, the fix is to abstract it — pass a value (e.g., a float for time) rather than a Unity object (e.g., Time.deltaTime read from a MonoBehaviour above). See Architecture: Layers for the full dependency diagram.Why does Presentation not reference Infrastructure?
Why does Presentation not reference Infrastructure?
IMatchOrchestrator, IGameStateProvider, IMultiplayerService, ILeaderboardService, etc. It has no knowledge of the concrete classes that implement them.VContainer, configured in the Bootstrap scene, binds each interface to its concrete Infrastructure implementation at runtime:NakamaMultiplayerService for a LocalMultiplayerStub in tests or for a future alternative backend — without changing a single line of Presentation code. The moment Presentation references a concrete Infrastructure type directly, that flexibility is lost.Why use R3 instead of C# events or ScriptableObject channels?
Why use R3 instead of C# events or ScriptableObject channels?
ThrottleFirst, DistinctUntilChanged, Buffer, and ObserveOnMainThread out of the box.ScriptableObject channels work well for simple decoupling but become awkward for typed event streams with multiple subscribers, especially across scenes.The more important reason: all three game modes (Single Player, Online Multiplayer, Pass & Play) feed the same IGameStateProvider reactive pipeline. The ViewModel and View code is identical regardless of which mode is running. There are no if (mode == Multiplayer) branches in Presentation code — the mode difference lives entirely in the Infrastructure layer below.See Architecture: Reactive UI for a walkthrough.Why VContainer and not Zenject?
Why VContainer and not Zenject?
- Less garbage: VContainer generates fewer allocations per resolve, which matters for a 60 FPS mobile target.
- Simpler registration API: VContainer’s
LifetimeScopeis a regularMonoBehaviour; the registration syntax is concise and explicit. - No magic: VContainer has no auto-scanning or attribute-based injection by default — everything is registered intentionally, making it easier to audit what the container knows about.
Game Rules Questions
What happens if the board reaches 21 cards and there's still no Set?
What happens if the board reaches 21 cards and there's still no Set?
AnySetExists() returns false at that point, the server broadcasts MatchEndedEvent and transitions MatchState to MatchEnd.This is documented as an edge case in the Game Rules document (§14): “The game ends even if the deck still has cards.”In practice this is rare — statistically, 21-card boards with no Set are extremely unlikely — but the code must handle it correctly.What is the difference between Refill and Expansion?
What is the difference between Refill and Expansion?
Refill is called → cards from the Deck fill those three slots → AnySetExists() is called. If still no Set: Expansion adds three new slots and deals into them → AnySetExists() is called again.If the Deck is empty during Refill, slots simply remain empty. If the Deck has fewer than 3 cards during a needed Expansion, the game ends — no partial deal.Can a player select the same card twice?
Can a player select the same card twice?
DeselectCardCommand. The UI must immediately update the card’s visual state to unselected. The domain enforces that _selectedCards never contains duplicate entries.This is consistent with the physical card game, where you simply pick up or put down a card. The selection mechanic is atomic per tap.Does score count go below zero with Point penalties?
Does score count go below zero with Point penalties?
Player entity: Player.ApplyPenalty() deducts 1 point in Point mode but clamps the result at 0. A player with a score of 0 who makes an invalid claim in Point mode stays at 0.This invariant is listed in the Domain Model document: “A player’s Score and Penalties cannot be negative.”Multiplayer Questions
Why doesn't the client validate Sets in multiplayer?
Why doesn't the client validate Sets in multiplayer?
- Cheating: A modified client could mark any three cards as a valid Set and claim fraudulently. There is no reliable way to detect this from the client side alone.
- Simultaneous claims: If two players claim the same Set at roughly the same time, both clients would “succeed” locally, resulting in diverged game state. The server is the only single source of truth that can resolve this deterministically.
What is the Nakama tick rate and why 20 Hz?
What is the Nakama tick rate and why 20 Hz?
- A claim submitted just after a tick starts is processed within 50 ms — fast enough to feel responsive in a card game where selecting three cards takes at least a second.
- Multiple inputs that arrive within the same tick are batched and processed together, which reduces server-side processing overhead compared to processing every message instantly.
- The tick rate is appropriate for a turn-based-feel real-time game. Contrast this with a first-person shooter where 64–128 Hz is necessary to resolve sub-frame collisions.
What happens if two players claim the same Set at the exact same millisecond?
What happens if two players claim the same Set at the exact same millisecond?
- Claims arriving in the same server tick are ordered by their message timestamp.
- If timestamps are identical (true simultaneous arrival), the player with the lower session ID wins.
- The winning claim is validated and processed. If the Set is valid, the cards are removed.
- The losing claim is then evaluated. Because the cards are already gone, the claim is invalid — but it counts as an invalid attempt only if the
PenaltyModeis notNone.
What happens when a player disconnects mid-match?
What happens when a player disconnects mid-match?
- During the window: the match continues for the remaining connected players. The disconnected player’s cards-in-selection are released back to unselected state.
- If the player reconnects within 30 seconds: they receive the current full
GameStateSnapshotand resume normally. - If the window expires: the player is considered to have forfeited. Their score is retained in the final results, but they cannot win. If all opponents disconnect, the last connected player wins by default.
Scope & v1.0 Questions
Is iOS planned for v1.0?
Is iOS planned for v1.0?
Is voice chat in scope?
Is voice chat in scope?
Can I add a new game mode that isn't in the GDD?
Can I add a new game mode that isn't in the GDD?
- Single Player vs AI (Easy / Medium / Hard / Expert + rubber-band assist)
- Online Multiplayer — Quick Match, Ranked, Private Room
- Tournament Mode (server-scheduled bracket, up to 8 players)
- Pass & Play (2–8 players, single device)
- Practice Mode
- Campaign Mode (progressive AI difficulty)
- Daily Challenge (fixed seed, global leaderboard)
Why is monetisation cosmetic-only? Can I add a gameplay booster IAP?
Why is monetisation cosmetic-only? Can I add a gameplay booster IAP?
“Only cosmetic items (table skins, card backs, symbol packs) may be sold. No temporary boosts, extra hints for purchase, or consumable advantages.”Additionally:
- No loot boxes or random-chance mechanics — all purchases are direct “see it, buy it.”
- No gambling of any kind, including tournament entry fees with prizes.
- Rewarded ads are allowed for extra hints in Single Player only; no forced interstitials during gameplay.
Troubleshooting
Unity won't compile — missing assembly reference error
Unity won't compile — missing assembly reference error
asmdef system enforces that lower layers cannot reference higher layers. If you’re seeing a missing reference error, it means one of:- You added a type from a higher layer to a lower-layer class (e.g., a
MonoBehaviourin the Domain project). - You added a dependency to an
asmdefthat is not permitted by the architecture.
My unit test fails with 'UnityEngine.dll not found'
My unit test fails with 'UnityEngine.dll not found'
UnityEngine dependency — but Unity’s Edit Mode Test Runner sets up the right environment and handles assembly resolution correctly. Tests in the wrong runner context will fail to resolve assemblies.How to check:- Open Window → General → Test Runner in the Unity Editor.
- Select the Edit Mode tab.
- Ensure your test file is in an assembly with
asmdefreferences pointing toSET.Tests.EditMode(or your project’s equivalent test assembly), not Play Mode.
dotnet test outside Unity, Domain-only tests can work if the test project references only the Domain assembly and has no Unity types. As soon as Application-layer code (which may use Unity types for coroutines or lifecycle) is included, you need the Edit Mode runner.Nakama connection fails in the Editor
Nakama connection fails in the Editor
docker-compose.yml:Assets/_Project/Data/nakama_config.json matches your local instance:localhost:7350. If you changed the port in docker-compose.yml, update the config to match. If connection still fails, check that your firewall is not blocking port 7350.The board shows no Set, but AnySetExists() returns true
The board shows no Set, but AnySetExists() returns true
GameStateSnapshot.This mismatch means the domain is ahead of what the UI is showing. Work through this checklist:-
Is
GameStateSnapshotbeing emitted after the board mutation? Set a breakpoint or log inGameSessionafter theRefillorExpansionstep completes and confirmStateStream.OnNext()is called. -
Is the View subscribed to the correct
ReactiveProperty? Check the ViewModel’s board property — it should be updated inside theStateStreamsubscription, not cached from an earlier snapshot. -
Did
RefillorExpansioncomplete beforeAnySetExistswas called? The call order inGameSessionmatters: board mutation →Refill/Expansion→ emit snapshot →AnySetExistscheck. If the snapshot is emitted beforeRefillfinishes, the UI will show the board in an intermediate state. -
Is the
CompositeDisposabledisposed too early? If the ViewModel’s disposable is collected before the match ends, the View silently stops receiving updates.