Build a Match-3 Game using Python and Kivy

Lesson 5 of 8

Selecting and Swapping Gems

With gems visible on screen, the next step is making them tappable: select one gem, tap an adjacent one, and swap them.

Tracking the current selection

class Match3App(App):
    def build(self):
        self.board = make_board()
        self.selected = None  # will hold a (row, col) tuple, or None
        self.grid = GridLayout(cols=BOARD_SIZE)
        self.buttons = {}

        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                gem = Button(text='', background_normal='')
                gem.bind(on_press=self.make_gem_handler(row, col))
                self.buttons[(row, col)] = gem
                self.grid.add_widget(gem)

        self.render_board()
        return self.grid

    def make_gem_handler(self, row, col):
        def handler(instance):
            self.on_gem_tapped(row, col)
        return handler

bind(on_press=...) only passes Kivy the widget instance that was pressed, not which row/col it represents. make_gem_handler(row, col) solves this with a closure: it returns a fresh handler function that remembers its own row and col from when it was created, one such closure is made per button, in the same loop that creates the button itself.

Handling a tap

    def on_gem_tapped(self, row, col):
        if self.selected is None:
            self.selected = (row, col)
            return

        first = self.selected
        second = (row, col)
        self.selected = None

        if first == second:
            return  # tapped the same gem twice, treat as a deselect

        if not adjacent(first, second):
            self.selected = second  # start a fresh selection from here instead
            return

        self.swap(first, second)
        self.render_board()

The logic has three outcomes for a second tap: the same gem (cancel), a non-adjacent gem (start over from that gem instead of swapping), or a valid adjacent gem (swap). Storing self.selected = None at the very top of the "second tap" branch, before any of those checks, means every path correctly clears the selection, there's no way to get stuck with a stale self.selected.

Swapping two positions

    def swap(self, pos1, pos2):
        r1, c1 = pos1
        r2, c2 = pos2
        self.board[r1][c1], self.board[r2][c2] = self.board[r2][c2], self.board[r1][c1]

Python's tuple-assignment swap (a, b = b, a) works just as well on two list-of-list cells as it does on two plain variables, no temporary variable needed.

WARNING

This lesson's version swaps unconditionally, even if the swap doesn't create any match. A real Match-3 game only allows swaps that produce at least one match, swapping back otherwise, that's exactly what the next lesson's match-detection makes possible.

📝 Selecting and Swapping Quiz

Passing score: 70%
  1. 1.Why does make_gem_handler(row, col) return a new function instead of binding one shared handler to every button?

  2. 2.a, b = b, a swaps two variables (or list cells) in Python without a temporary variable.

  3. 3.A function defined inside another function that remembers variables from the enclosing scope is called a ____.