Build a Match-3 Game using Python and Kivy

Lesson 7 of 8

Clearing, Dropping, and Refilling

A detected match needs to actually disappear, let the gems above it fall down to fill the gap, and have brand-new gems spawn at the top, then check whether that created new matches, and repeat until the board settles. This is the "cascade" that makes Match-3 games feel alive.

Clearing matched gems

An empty cell is represented as None, distinct from any real gem color:

def clear_matches(board, matched):
    for (row, col) in matched:
        board[row][col] = None

Dropping gems down, column by column

def drop_gems(board):
    for col in range(BOARD_SIZE):
        # collect this column's surviving (non-None) gems, top to bottom
        remaining = [board[row][col] for row in range(BOARD_SIZE) if board[row][col] is not None]
        missing = BOARD_SIZE - len(remaining)

        # empty space at the top, then the survivors settle to the bottom
        new_column = [None] * missing + remaining

        for row in range(BOARD_SIZE):
            board[row][col] = new_column[row]

Building a brand-new new_column list (rather than trying to shift entries in place) avoids a whole class of off-by-one bugs: remaining already has the survivors in the right relative order, padding missing Nones in front of them is all it takes to push them down, since row 0 is the top of the board.

Spawning new gems in the empty top cells

def refill_board(board):
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] is None:
                board[row][col] = random.choice(GEM_COLORS)

After drop_gems, every remaining None is guaranteed to be at the top of its column (that's exactly what the padding in drop_gems guaranteed), so refill_board can simply fill in any None it finds, no need to track which rows were affected.

Repeating until the board is stable

    def resolve_matches(self):
        matched = find_matches(self.board)
        while matched:
            clear_matches(self.board, matched)
            drop_gems(self.board)
            refill_board(self.board)
            matched = find_matches(self.board)  # newly-fallen gems might match too

This while matched: loop is the "repeat until no matches remain" step: clearing and refilling can easily create new runs of 3 (a cascade), so the function keeps re-checking find_matches after every refill, only stopping once a full pass finds nothing left to clear.

Wiring it into a swap

    def on_gem_tapped(self, row, col):
        # ... selection logic ...
        self.swap(first, second)

        if find_matches(self.board):
            self.resolve_matches()
        else:
            self.swap(first, second)

        self.render_board()

WARNING

resolve_matches only touches self.board, the plain Python data, it never calls self.render_board() itself. Forgetting to call render_board() after resolve_matches() (as the snippet above does, on the very last line) is a common bug, the board would be fully correct internally while the screen still shows the old, un-cleared gems.

📝 Clearing, Dropping & Refilling Quiz

Passing score: 70%
  1. 1.Why does the game keep calling find_matches() again inside a while loop after refilling?

  2. 2.In this board representation, None represents an empty cell, distinct from any real gem color.

  3. 3.random.____(GEM_COLORS) picks one random gem color to fill an empty cell.