Detecting Matches
Swapping gems is only useful once the game can tell whether that swap actually created a match, three or more identical gems in a row, either horizontally or vertically.
Scanning rows
def find_matches(board):
matched = set()
# horizontal runs
for row in range(BOARD_SIZE):
run_start = 0
for col in range(1, BOARD_SIZE + 1):
end_of_row = col == BOARD_SIZE
broke_run = end_of_row or board[row][col] != board[row][run_start]
if broke_run:
run_length = col - run_start
if run_length >= 3:
for c in range(run_start, col):
matched.add((row, c))
run_start = col
return matched
This walks each row left to right, tracking where the current run of identical gems started (run_start). Every time the gem changes (or the row ends), it checks whether the run that just ended was long enough (>= 3), and if so, adds every position in that run to matched. Using a loop bound of BOARD_SIZE + 1 (not just BOARD_SIZE) is what lets the last run in the row get checked too, without it, a run reaching all the way to the last column would never trigger the "broke run" check.
Adding vertical runs
Columns need the exact same logic, just swapped: outer loop over columns, inner loop over rows.
def find_matches(board):
matched = set()
for row in range(BOARD_SIZE):
run_start = 0
for col in range(1, BOARD_SIZE + 1):
end_of_row = col == BOARD_SIZE
broke_run = end_of_row or board[row][col] != board[row][run_start]
if broke_run:
if col - run_start >= 3:
for c in range(run_start, col):
matched.add((row, c))
run_start = col
for col in range(BOARD_SIZE):
run_start = 0
for row in range(1, BOARD_SIZE + 1):
end_of_col = row == BOARD_SIZE
broke_run = end_of_col or board[row][col] != board[run_start][col]
if broke_run:
if row - run_start >= 3:
for r in range(run_start, row):
matched.add((r, col))
run_start = row
return matched
matched is a set of (row, col) tuples, not a list, deliberately: a gem at the intersection of a horizontal and vertical run (an L or T shape) would otherwise get added twice, a set automatically collapses duplicates.
Using it after a swap
def on_gem_tapped(self, row, col):
# ... same selection logic as the previous lesson ...
self.swap(first, second)
if find_matches(self.board):
self.render_board()
else:
self.swap(first, second) # no match, swap back
Calling self.swap(first, second) a second time with the same two positions undoes the first swap, exactly the "swap back if it didn't help" rule real Match-3 games use to stop players from making pointless moves.
TIP
find_matches only reports which cells matched, it doesn't remove anything from the board itself, keeping "detect" and "remove" as separate steps (next lesson) makes each one easy to test on its own.