Skip to main content
Your first contributions to SET: 3D Edition should build confidence in the architecture before you touch complex systems. This page gives you a concrete sequence of tasks — each one small, testable, and firmly inside the Domain layer where there are no Unity lifecycle concerns and no Nakama dependency in sight.
Pre-production — SET: 3D Edition is being built from the ground up. Early contributors are laying the foundation, not patching an existing game. What you build here will be the canonical implementation others build on top of.

Why this page exists

“Just read the code and find something to fix” is terrible onboarding advice for a pre-production project. This page gives you a defined ramp: understand the rules, prove your understanding through tests, then expand outward. Every task here is designed to be completable without running Unity at all.

Key responsibilities

Every first-contribution task on this page serves one of three outcomes:

Philosophy: start in the Domain, prove with tests

The Domain layer is pure C# — no UnityEngine, no Nakama, no R3. It compiles and runs in any .NET environment. This means you can write and run unit tests from Unity’s EditMode test runner in seconds, without entering Play mode. Starting here serves two purposes:
  1. You learn the game’s core logic by implementing it, not just reading about it.
  2. You leave behind a test suite that protects everyone who comes after you.
Once your Domain tests pass, you’ll have the vocabulary and the context to understand what GameSession in the Application layer is doing — and why the reactive streams flowing into Presentation look the way they do.

Where your first tasks sit in the architecture

Your first tasks live entirely inside SET.Domain (green). The Application layer is read-only study material for now. Infrastructure and Presentation are out of scope until Domain and Application are solid.

Starter tasks

1

Read the game rules and domain model docs

Before writing code, understand what you’re modelling. Read:
  • /game-design/rules — the complete unambiguous SET rules, including the all-same-or-all-different attribute check, board expansion logic, and win conditions.
  • /core-gameplay/card-model — the domain model document covering Card, CardAttributes, Board, Match, and the identity scheme.
You’re looking for answers to: What makes three cards a valid Set? When does the board expand? What is the aggregate root?
2

Implement SetValidator — pure C#, TDD

SetValidator is a stateless domain service that answers one question: are these three cards a valid Set? It is the most important class in the project and the natural place to start.Create Assets/_Project/Domain/Services/SetValidator.cs implementing ISetValidator:
Write your unit tests first. Create Assets/_Tests/EditMode/SetValidatorTests.cs and cover these cases before touching the implementation:
  • All four attributes all-same → valid
  • All four attributes all-different → valid
  • Three cards where one attribute has two of one value and one of another → invalid
  • FindAllSets on a 12-card board returns the expected number of Sets
  • AnySetExists returns false when no valid Set is present
The validation rule is identical for all four attributes: for any given attribute, the three cards must show either all the same value or all three different values. Two-same, one-different is always invalid.SetValidator is also used server-side as the authoritative implementation — keep it deterministic and allocation-light.
3

Write tests for Deck

Deck encapsulates the 81-card collection and its draw logic. Create tests in SetValidatorTests.cs or a separate DeckTests.cs:
  • Shuffle with a fixed seed produces a deterministic card order (run twice, compare results)
  • Draw N cards removes exactly N cards from the deck
  • Draw from an empty deck returns an empty result without throwing
  • No card appears more than once across the full 81-card deck (verify the complete set of CardId values)
The full deck contains exactly 81 cards — one for every combination of the four attributes (3 × 3 × 3 × 3 = 81). Each card has a unique CardId computed as:
This gives IDs in the range 0–80 with no collisions.
4

Write tests for Board

Board manages the active card grid. Write tests in BoardTests.cs:
  • Place a card in an empty slot — slot reports occupied
  • Remove a card from an occupied slot — slot reports empty
  • Board starts with 12 cards (4 × 3 grid) in standard configuration
  • Board expands to 15 cards when no Set is present (3 new slots added)
  • Board cannot expand beyond 21 cards (7 × 3 maximum)
  • Removing a Set from a full-expansion board contracts back to 12 if enough cards remain
Slot invariants: a CardSlot can hold exactly zero or one Card. Placing a second card in an occupied slot should return an error result, not throw.
5

Read the Interface Contracts doc

Once your Domain tests pass, read /architecture/layers and the interface contracts document (if available) to understand how GameSession in the Application layer consumes ISetValidator and IAIScanner. You’ll see that GameSession is instantiated with these interfaces injected via constructor — it never calls new SetValidator() directly.This prepares you for the next step without requiring you to change anything.
6

Explore the GameSession stub in Application

Look at Assets/_Project/Application/Sessions/GameSession.cs. You’re not modifying it yet — you’re reading it to understand:
  • How it receives ISetValidator and IAIScanner via constructor injection
  • How it manages the MatchState enum transitions
  • How it emits state and events as R3 observables through IGameStateProvider
The constructor signature establishes the dependency contract (Planned):
GameSession never calls new SetValidator() or new AIScanner() directly — VContainer supplies the concrete implementations through constructor injection at startup.When you’re ready to contribute Application-layer work, start by adding test coverage for GameSession state transitions using NSubstitute mocks for ISetValidator and IAIScanner.

Key types you’ll work with

Card attributes and enums

The four domain enums define every possible card attribute value:
CardAttributes bundles them into a single immutable value object:

ISetValidator interface

SetResult struct

SetResult is returned instead of throwing exceptions. Domain errors are values, not exceptions.

Test naming convention

All test method names follow the pattern MethodName_Scenario_ExpectedBehavior:
Keep the scenario segment specific enough to diagnose a failure without opening the test body.

Implementation checklist

Before you mark a pull request ready for review, confirm every item below:
  • All new and existing EditMode tests pass (run via Window → General → Test Runner in Unity)
  • No using UnityEngine; anywhere in SET.Domain or SET.Application
  • No using Nakama; anywhere in SET.Domain or SET.Application
  • No using R3; anywhere in SET.Domain
  • Test methods follow the MethodName_Scenario_ExpectedBehavior naming convention
  • SetResult returned for invalid input; no exceptions thrown from domain services
  • Value objects (CardAttributes, SetResult) are immutable (readonly struct)
  • SetValidator is stateless — no instance fields mutated between calls
  • Deck shuffle with a fixed seed is deterministic (tested with two identical runs)
  • Board expansion and contraction rules tested at boundary values (12, 15, 18, 21 cards)

Common mistakes

Common mistakes on first contributions:
  • Writing game logic in a MonoBehaviour — the Update() loop is for visual animation only. Set validation, score calculation, and board logic all live in the Domain and Application layers, not in views.
  • Skipping the tests — this project uses TDD for Domain logic. Writing the implementation first and the tests after is backwards; tests written after implementation tend to be shaped around what the code does rather than what it should do.
  • Using FindObjectOfType or GameObject.Find — banned everywhere. Domain and Application code has zero Unity references; Presentation code uses VContainer constructor injection.
  • Throwing exceptions for invalid SetsISetValidator.Validate returns a SetResult with IsValid = false and a reason string. Exceptions are for exceptional, unrecoverable states only.
  • Adding a using R3; import to Domain classes — R3 is a Presentation/Application concern. Domain types are plain C#; they do not depend on any reactive framework.

Repo Tour

Understand the folder hierarchy and assembly boundaries before you start editing.

Card Model

The full domain model — Card, Board, Match aggregate root, and CardId scheme.

Set Validation

Deep dive into the SetValidator algorithm and edge cases.

Engineering Standards

Naming conventions, formatting rules, and the complete PR checklist.