Leave without saving?

Your editor changes will be lost.

Match-3-4 turns the matches detected in the previous lesson into a full board update. A matched tile is removed, tiles above it fall into the holes, new tiles enter from above the board, and the game checks again for cascades.

Resolve a Cascade

After an animated swap, PlayState.swapTiles() sets isResolvingMatches and resolves the board inside try/finally. The flag prevents new selection input while the asynchronous removal and falling work is active, even if a promise rejects.

this.isResolvingMatches = true;
try {
await this.calculateMatches();
} finally {
this.isResolvingMatches = false;
}

calculateMatches() follows the same sequence for every cascade:

  1. Detect the current matches.
  2. Stop when there are none.
  3. Remove the matched tiles.
  4. Fall existing tiles and add replacements.
  5. Call itself again because the replacements may make new matches.
board.calculateMatches();
if (board.matches.length === 0) {
return;
}
await board.removeMatches();
await this.placeNewTiles();
await this.calculateMatches();

Remove Matched Tiles

Board.removeMatches() walks every matched tile, waits briefly between tiles after the first, replaces its grid slot with null, and plays a random match sound. The null values are intentional: they are the holes that the falling algorithm fills next.

this.tiles[tile.boardY][tile.boardX] = null;
this.playRandomMatchSound();

At the end of removal, this.matches is reset. The red detection outlines disappear because there are no remaining matches to render.

Compact Each Column

Board.getFallingTiles() asks compactColumn() to process every column. compactColumn() scans upward from the bottom while tracking destinationRow, the lowest position that should receive a tile.

When it finds a tile above an empty destination, it updates the grid immediately, updates the tile’s boardY, and records the visual destination for a tween.

this.tiles[destinationRow][column] = tile;
this.tiles[sourceRow][column] = null;
tile.boardY = destinationRow;
tweens.push({
tile,
endValues: { y: tile.boardY * Tile.SIZE },
});

Fall then Replace

placeNewTiles() runs all existing-tile fall tweens together with Promise.all(). Once those have reached their new rows, it creates replacement tiles and drops them in one at a time.

await Promise.all(
board
.getFallingTiles()
.map((tile) => timer.tweenAsync(tile.tile, tile.endValues, 0.25))
);

Board.getNewTiles() fills each remaining null slot immediately, but starts each new tile above the board:

const tile = this.generateTile(x, y);
tile.y = -Tile.SIZE * 3;
this.tiles[y][x] = tile;

The replacement tween moves each tile to tile.boardY * Tile.SIZE and plays a fall sound. The recursive detection call then decides whether the new board is stable or begins another cascade.

Powered by WebContainers
Files
Preparing Environment