Skip to content

t1k:unity:dots-core:puzzle

FieldValue
Moduledots-core
Version2.3.2
Effortmedium
Tools
/t1k:unity:dots-core:puzzle

DOTS Puzzle Core — Board, Match, Cascade, Scoring

Section titled “DOTS Puzzle Core — Board, Match, Cascade, Scoring”

Package: com.the1studio.dots-puzzle — namespace DOTSPuzzle.* Related skills: dots-core-ecs-core (API) · dots-combat-puzzle (puzzle→combat bridge)

  • Implementing grid-based puzzle boards (match-3, polyomino, block-blast, slide, merge)
  • Writing match detection — 3-in-a-row, L/T shapes, combo chains
  • Building cascade / gravity systems after cell removal
  • Implementing swap, rotation, fill with seeded random
  • Connecting puzzle scoring to combat (see dots-combat-puzzle)
ComponentTypePurpose
BoardConfigIComponentData (singleton)int Width, int Height, int GemTypeCount, uint Seed
BoardCellIBufferElementData (cap 0)int GemType, bool IsMatched, bool IsEmpty, Entity CellEntity
MatchGroupIBufferElementDataint GroupId, int CellIndex, int Size
CascadeStepIComponentDataint Step, bool IsComplete
SwapRequestIComponentDataint FromIndex, int ToIndex, bool IsValid
PuzzleScoreIComponentData (singleton)int TotalScore, int ChainMultiplier, int MoveCount
SpecialGemIComponentDataSpecialGemType Type, int PowerValue; added to cell entities with special behavior
UndoStateIBufferElementDataSnapshot of BoardCell buffer for undo support
public enum SpecialGemType { Bomb, LineBlast, ColorBurst, Multiplier }
SystemGroupRole
BoardInitSystemPuzzleSystemGroup (OrderFirst)Creates BoardCell buffer sized Width * Height; seeds gems via Random.CreateFromIndex(Seed)
SwapSystemPuzzleSystemGroupValidates SwapRequest; swaps two cells; marks for match scan
MatchDetectionSystemPuzzleSystemGroup (after Swap)Scans rows + columns for 3+ contiguous same-type; marks IsMatched; populates MatchGroup
SpecialGemActivationSystemPuzzleSystemGroup (after Match)Resolves special gem effects (bombs, line blasts) before cascade
CascadeSystemPuzzleSystemGroup (after Match)Shifts non-empty cells downward per column; fills vacancies with new random gems
ScoringSystemPuzzleSystemGroup (after Cascade)Awards points per MatchGroup.Size; applies chain multiplier; resets after no-match cascade
UndoSystemPuzzleSystemGroupRestores BoardCell buffer from UndoState snapshot on UndoRequestTag
// Row-major index: (row, col) → col * Height + row OR row * Width + col
// DOTS-puzzle uses column-major for gravity (columns are contiguous):
int cellIndex = column * config.Height + row;
var cells = SystemAPI.GetBuffer<BoardCell>(boardEntity);
ref var cell = ref cells.ElementAt(cellIndex);
// MatchDetectionSystem — scan each row:
for (int row = 0; row < height; row++)
{
int runStart = 0, runLen = 1;
for (int col = 1; col < width; col++)
{
int prev = (col - 1) * height + row;
int curr = col * height + row;
if (cells[curr].GemType == cells[prev].GemType && cells[curr].GemType >= 0)
runLen++;
else
{
if (runLen >= 3) MarkMatched(cells, runStart, col - 1, row, height);
runStart = col; runLen = 1;
}
}
if (runLen >= 3) MarkMatched(cells, runStart, width - 1, row, height);
}
// CascadeSystem — compact non-empty cells downward per column:
for (int col = 0; col < width; col++)
{
int writeRow = 0;
for (int row = 0; row < height; row++)
{
int idx = col * height + row;
if (!cells[idx].IsEmpty)
{
cells.ElementAt(col * height + writeRow) = cells[idx];
if (writeRow != row) cells.ElementAt(idx) = default; // empty slot
writeRow++;
}
}
// Fill remaining rows with new random gems
}
// Deterministic fill — ensure same seed + move count → same board state:
var rng = Random.CreateFromIndex(config.Seed + (uint)score.MoveCount * 100u + (uint)column);
cells.ElementAt(idx) = new BoardCell { GemType = rng.NextInt(0, config.GemTypeCount) };
DirectionPackageDetail
Depends onDOTSCore baseECB, SystemAPI, Random
Used byDOTSPuzzleCombatMatchGroup.Size → elemental damage pipeline
Used byMatch3RPGDemoBoard drives combat; each gem color maps to element
Used byColorFitDemoQueue-to-grid dispatch uses BoardCell + SwapSystem
TypeKey variation
Match-3MatchDetectionSystem with shape variants (L/T via MatchGroup.Size >= 4)
PolyominoSwapRequest replaced by placement system; BoardCell.CellEntity tracks piece origin
Block-blastRow/column clear on match; SpecialGemType.LineBlast encodes this
Slide-mergeDirectional CascadeSystem variant (4 directions); GemType doubles on collision
MergeCascadeSystem runs after drop, not gravity; merge on adjacency
Tube sortBoardConfig encodes tube columns; match = full monochrome tube
Queue dispatchInput queue IBufferElementData; dispatch via SwapSystem to target cells
  • Column-major vs row-majorDOTSPuzzle uses column-major indexing (col * Height + row) so gravity sweeps are cache-coherent (all cells of one column are contiguous). Using row-major in new code creates silent correctness bugs (wrong neighbor checks).
  • SpecialGemActivationSystem must run BEFORE CascadeSystem — bombs and line blasts mark additional cells IsMatched; cascade must process all marked cells in one pass. Reversing order causes orphaned matched cells.
  • Act-before-set: phase-gated cascade systems MUST be explicitly ordered, or single-Update phase tests silently break — The puzzle cascade pipeline is a phase state machine (PhaseMatchCheck → PhaseDestroy → PhaseGravity → …). Each system is gated to one phase and the EditMode tests run a SINGLE World.Update() then assert the intermediate phase. The invariant: the system that ACTS on a phase must run BEFORE the system that SETS (transitions into) that phase, so a freshly-set phase is NOT consumed in the same frame (which would skip ahead one or more phases and fail the assertion). Concretely:
    • SpecialPieceCreationSystem is [UpdateBefore(typeof(MatchDestroySystem))] so special pieces (blast radius, line width) are configured before MatchDestroySystem advances PhaseDestroy → PhaseGravity.
    • MatchDetectionSystem is [UpdateAfter(typeof(MatchDestroySystem))] + [UpdateBefore(typeof(BoardShuffleSystem))]: AFTER destroy so a MatchCheck-frame detection that sets PhaseDestroy is not consumed same-frame (would clear MatchGroup and advance to Gravity before observers read it); BEFORE shuffle because shuffle SETS PhaseMatchCheck and detection ACTS on it.
    • Why it’s a trap: an UNCONSTRAINED match/cascade system (no [UpdateBefore]/[UpdateAfter]) compiles, passes today, then DRIFTS in the PuzzleSystemGroup topological sort the instant another ordering edge is added elsewhere — silently re-ordering a same-frame cascade and breaking the intermediate-phase test with no code change to the affected system. Always pin cascade/match/special systems with explicit ordering attributes; never rely on the implicit sort. Document the act-before-set reasoning inline (see the comment blocks on MatchDetectionSystem and SpecialPieceCreationSystem in com.the1studio.dots-puzzle).
  • BoardInitSystem re-runs if BoardConfig is destroyed and re-added — the system has no OnCreate guard. Guard with state.RequireForUpdate<BoardConfig>() AND check cells.IsCreated && cells.Length > 0 before re-init.
  • Seeded fill must include move count in the seed — using config.Seed alone produces the same fill after every cascade; including MoveCount ensures variety across the game.
  • Chain multiplier resets on no-match cascadeScoringSystem resets ChainMultiplier = 1 when MatchGroup buffer is empty after a cascade step. Consumer must read the multiplier BEFORE cascade resolves to display the correct chain count in UI.
  • Undo only stores 1 snapshotUndoState is a single buffer overwritten each move. Multi-level undo requires a stack pattern (multiple UndoState buffers or a circular buffer entity).