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.