Board is the physical state of the game — it is everything the player sees on screen. Understanding how it initialises, how empty slots are refilled after a valid Set, and how it expands when no Set exists is essential before implementing any visual layout or game-flow code.
Pre-production notice. SET: 3D Edition is in pre-production. All class signatures, slot-index conventions, and flow decisions described here are based on the current design specification and may change before the first playable build.
Responsibilities and Boundaries
TheBoard entity is deliberately narrow in scope. Knowing what it does not own is as important as knowing what it does.
Board IS responsible for:
- Maintaining the ordered list of
CardSlotvalues (positions and their occupants). - Placing, removing, and querying cards by slot index.
- Tracking which slots are empty, so
GameSessionknows where to place new cards. - Enforcing the maximum slot count (21) and throwing when that invariant would be violated.
- Drawing cards from the
Deck— that isGameSession’s job. - Validating Sets — that is
SetValidator’s job. - Deciding when to expand — that is
GameSession’s job (triggered byAnySetExistsreturningfalse). - Locking input during animations — that is
GameSession’s job.
Board a simple, testable data structure and puts all orchestration logic in one place: GameSession.
Core Types
CardSlot is a readonly struct — it is allocated on the stack and copied by value. With 21 slots at roughly 8–12 bytes each, the entire slot list fits in ~252 bytes. There is no heap pressure here.Grid Layout and Slot Indices
The board always has exactly 4 columns. The initial deal produces a 4 × 3 grid (12 slots, indices 0–11):n, the grid position is:
The
BoardView (Presentation layer) maps slot indices to 3D world positions using the same row = n / 4 formula. No other coordinate system should be used.
Initial Deal
GameSession performs the initial deal at match start:
- Call
Deck.Draw(12)to draw the first 12 cards. - For each card at index
i(0–11), callBoard.PlaceCard(i, card). - After all 12 cards are placed, call
SetValidator.AnySetExists(boardCards). - If no Set exists on the opening board (rare but legal), begin the expansion flow immediately.
Board.Expand() is not called during the initial deal — the constructor creates 12 empty slots, and GameSession fills them.
Refill After a Valid Set
When a player claims a valid Set, the three claimed cards are removed from the board and replaced:GameSessioncallsBoard.RemoveCard(slotIndex)for each of the three claimed slots.GameSessionchecksDeck.CardsRemaining. If the deck has ≥ 3 cards:- Call
Deck.Draw(3)to get three new cards. - Call
Board.PlaceCard(slotIndex, newCard)for each of the same three slot indices, preserving the positions the claimed cards occupied.
- Call
- If the deck has 0 cards, the three slots stay empty. The board may end up with fewer than 12 occupied positions (which is legal).
- After refill (or leaving slots empty), call
SetValidator.AnySetExists()on the current board cards.
Official SET rules state that refill cards go back into the same positions as the removed cards. Do not move cards to fill gaps.
Board.PlaceCard is called with the original slot indices, not with fresh indices at the end of the list.Expansion: No Set on the Board
Expansion is triggered whenAnySetExists returns false after any board mutation.
Expansion preconditions:
Expansion steps (when preconditions pass):
GameSessioncallsBoard.Expand(3)— appends three new empty slots to the end ofSlots.GameSessioncallsDeck.Draw(3)and places each card into the newly appended slots viaBoard.PlaceCard.GameSessionemitsBoardExpandedEventfor the Presentation layer to show the “No Set — dealing 3 more cards” toast and play the deal animation (~150 ms).- Input remains locked (
MatchState.ExpandBoardAnim) during the animation. - After the animation completes,
GameSessioncallsAnySetExistsagain on the new board. - If still no Set and conditions allow, repeat from step 1.
Board Invariants
Board must enforce the following invariants and throw named exceptions when they are violated. These are programming errors, not expected game events, so exceptions are the correct signal.
RemoveCard on an empty slot is a no-op (returns silently), not an exception — GameSession may call it during cleanup without knowing slot occupancy in advance.
Implementation Checklist
1
Board initialises with exactly 12 empty slots
The
Board constructor must create exactly 12 CardSlot values with Card = null. No card placement happens in the constructor — that is the responsibility of GameSession during the initial deal.2
PlaceCard and RemoveCard enforce invariants
PlaceCard must throw InvalidOperationException if the target slot is already occupied. Both methods must throw ArgumentOutOfRangeException for invalid indices. Write unit tests for every exception path.3
GetEmptySlots returns the correct indices
After removing three cards,
GetEmptySlots() must return those exact three indices. GameSession relies on this list to know where to call PlaceCard for refill cards.4
Expand appends exactly 3 slots, capped at 21
Expand(3) must append exactly three new empty CardSlot values. Any call that would push Slots.Count above 21 must throw ArgumentOutOfRangeException rather than silently truncating.5
GameSession owns Deck draw and AnySetExists calls
Board must not reference Deck or ISetValidator. If you find yourself calling Deck.Draw() from inside Board, that code belongs in GameSession. This separation is what allows Board to be tested without a real deck or validator.6
All operations are O(1) or O(n) for small n
GetCard, PlaceCard, and RemoveCard must all be O(1) (direct index access into the backing array). GetEmptySlots is O(n) where n ≤ 21 — acceptable.Common Mistakes
Related Pages
Card Model
The Card and CardSlot types that Board stores and manages.
Set Validation
AnySetExists and FindAllSets — called by GameSession after every Board mutation.
Session Lifecycle
How GameSession orchestrates Board.Expand, Deck.Draw, and AnySetExists into the full match state machine.
AI Opponents
How AIScanner reads the Board’s card list to find and claim Sets.