PR pixi-reels
Wiki · Architecture
Algorithm

Cascade physics

Why survivors without cleared slots below them do not move.

A correct cascade drop has a simple invariant: a symbol only moves if there are cleared slots beneath its original row. Getting this wrong is the classic cascade bug — naive implementations shift every visible cell up by the winner count, then tween back down, making untouched symbols visually jump.

pixi-reels computes per-cell fall distance from the winners list. Survivors with no winners below them are never touched — no animation job is created, their view.y stays exactly where placeSymbols put it.

The algorithm, in one column

BEFORE REMOVE WINNERS GRAVITY FILL FROM TOP A B C D E A C E A fell 2 C fell 1 E no move X new Y new A C E no move
Stayed put · no winners below → no animation
Survivor fell · moved N slots where N = winners below
New symbol · fell from above viewport

Per-row fall distance formula

For a column with visible rows indexed 0..V-1 and winner rows winnerRows (sorted ascending), let nonWinnerRows be the surviving row indices in order. For each final row R:

Final row R Is it a new symbol? Starting y offset (slots above target)
R < winnerRows.length yes — falls from above the viewport R + 1
R ≥ winnerRows.length no — survivor from nonWinnerRows[R - winCount] R - originalRow (0 means: don't touch it)

Crucially: when offsetRows === 0, the code returns early and never creates an animation job for that cell. Its view.y is already correct from placeSymbols, and tweening it would be visually wrong.

Four canonical cases

Winner at the top

One new symbol falls in at row 0. Every survivor stays put.

Winner in the middle

Survivors above the winner each fall 1 slot. Survivors below don't move.

Winner at the bottom

Every survivor falls exactly 1 slot. New symbol fills row 0.

Two stacked winners at the top

Two new symbols fall in. No survivor moves.

API surface

  • tumbleToGrid(reelSet, nextGrid, winners, opts) — the primitive. Takes a Cell[] of real winners; per-reel fall distance math happens here.
  • runCascade(reelSet, stages, opts) — orchestrates multiple stages. Default winner detection is diffCells(prev, next); pass a custom winners: (prev, next) => Cell[] for match-pattern cascades.
  • diffCells(prev, next) — utility, equal cells → zero. Fine for gravity-clean sequences; wrong for patterns where survivors' new rows hold different symbol ids than before.

The trap with diffCells

Consider prevCol = [A, C, X], X at row 2 wins. The correct next col is [N, A, C] — new symbol at row 0, survivors shifted down. diffCells reports three changed cells (row 0: A→N, row 1: C→A, row 2: X→C). Treating those as "three winners" would animate three new symbols falling from above, instead of one new + two survivors sliding by one slot.

For pattern cascades, always pass your own winner list derived from the actual match, never from the diff. The remove-symbol recipe does exactly this.