Playing

gaming_in_progress.png
A game in progress

The image above shows a game under way. Many squares are revealed, some are marked as bombs (the flags), and some remain a mystery.

We'll start with some support functions.

First is one that is used when the player uncovers an empty square. Starting at that square, more are uncovered, up to squares that are next to bombs. It works much like the bucket tool in most painting applications.  The uncovering spreads until it runs in to a non-empty square (which is uncovered) or a previously uncovered square

The way it works is to use a stack to store squares to be visited. This is initialized to hold the square the player uncovered. The while loop continues until the stack is empty, which is the case when we're run out of squares to examine. Each time through the loop, a square is popped from the stack. This is the square to examine. If it's already been uncovered, it gets ignored.

If the square needs to be uncovered, we check what's in the board bytearray at that location. If is adjacent to a bomb (it contains a number), it's uncovered and the next iteration starts. However, if it's an empty square (not adjacent to a bomb) it is uncovered and each of the squares around it (skipping any that are off the edge of the board) are pushed onto the stack. The next iteration then starts.

So, this causes the code to visit a square, then the squares surrounding it, until one with a number (i.e. adjacent to a bomb) is encountered (it's neighbors aren't pushed onto the stack). Once it has visited as many as it can, it's done.

Download: file
def expand_uncovered(start_x, start_y):
    number_uncovered = 1
    stack = [(start_x, start_y)]
    while len(stack) > 0:
        x, y = stack.pop()
        if tilegrid[x, y] == BLANK:
            under_the_tile = get_data(x, y)
            if under_the_tile <= OPEN8:
                tilegrid[x, y] = under_the_tile
                number_uncovered += 1
                if under_the_tile == OPEN0:
                    for dx in (-1, 0, 1):
                        if x + dx < 0 or x + dx >= 20:
                            continue              # off screen
                        for dy in (-1, 0, 1):
                            if y + dy < 0 or y + dy >= 15:
                                continue          # off screen
                            if dx == 0 and dy == 0:
                                continue          # don't process where the bomb
                            stack.append((x + dx, y + dy))
    return number_uncovered

Another helper function examines the board to determine if the game is over and whether the player has won or lost.

This is more straight forward.

First, we check every square in the tilegrid to see whether it is still covered or has been marked as questionable. If any are, the game isn't over and None is returned.

If all squares have been uncovered or flagged as a bomb, we check if any that have been marked as covering a bomb actually don't. If there are any such squares the game is lost, otherwise it is won. False or True is returned, respectively.

Download: file
def check_for_win():
    """Check for a complete, winning game. That's one with all squares uncovered
    and all bombs correctly flagged, with no non-bomb squares flaged.
    """
    # first make sure everything has been explored and decided
    for x in range(20):
        for y in range(15):
            if tilegrid[x, y] == BLANK or tilegrid[x, y] == BOMBQUESTION:
                return None               #still ignored or question squares
    # then check for mistagged bombs
    for x in range(20):
        for y in range(15):
            if tilegrid[x, y] == BOMBFLAGGED and get_data(x, y) != BOMB:
                return False               #misflagged bombs, not done
    return True               #nothing unexplored, and no misflagged bombs

Finally, we have the core game loop.

After initializing some local variables, the loop begins. While the loop runs continuously, it only processes input from the player every 200 milliseconds. This avoids too much noise on the touch input, essentially downsampling.

Actions happen on the detection of an initial touch. Subsequent touch detection has to be ignored until a release has been seen (i.e. lack of touch). The wait_for_release flag is used to accomplish this. It gets set when the first touch is detected. Subsequent touch detections are ignored if the flag is set. Once no touch is detected, the flag is cleared, allowing the next touch to be processed.

Once an initial touch is detected, its coordinates are divided by 16 (the width and height of each tile/square) to give the coordinates of the touched square.

What happens next depends on what is displayed in the tilegrid in the square the player touched.

  • If it's blank, set it to a question mark, indicating that the player thinks there might be a bomb there.
  • If it's a question mark, set it to a flag, indicating that the player thinks there is a bomb there.
  • If it's a flag there's a bit more work involved, depending on what's in that square in the bytearray:
    • If it's a bomb, the game is lost. This is indicated by placing a red square with a bomb at that location and returning False
    • If it's a tile adjacent to a bomb (i.e. it contains a number) it is simply revealed.
    • If it's empty, expand_uncovered is called to recursively uncover the space around it.
    • Anything else is invalid and causes an error to be raised.
gaming_loose.bmp
Loss from uncovering a bomb

After the touched square is processed (and there hasn't been a game loss due to uncovering a bomb) the board is checked for a win or lose state. If the game isn't over yet, the loop keeps running, otherwise the win/lose state is returned.

Download: file
def play_a_game():
    number_uncovered = 0
    touch_x = -1
    touch_y = -1
    touch_time = 0
    wait_for_release = False
    while True:
        now = time.monotonic()
        if now >= touch_time:
            touch_time = now + 0.2
            # process touch
            touch_at = touchscreen.touch_point
            if touch_at is None:
                wait_for_release = False
            else:
                if wait_for_release:
                    continue
                wait_for_release = True
                touch_x = max(min([touch_at[0] // 16, 19]), 0)
                touch_y = max(min([touch_at[1] // 16, 14]), 0)
                print('Touched (%d, %d)' % (touch_x, touch_y))
                if tilegrid[touch_x, touch_y] == BLANK:
                    tilegrid[touch_x, touch_y] = BOMBQUESTION
                elif tilegrid[touch_x, touch_y] == BOMBQUESTION:
                    tilegrid[touch_x, touch_y] = BOMBFLAGGED
                elif tilegrid[touch_x, touch_y] == BOMBFLAGGED:
                    under_the_tile = get_data(touch_x, touch_y)
                    if under_the_tile == 14:
                        set_data(touch_x, touch_y, BOMBDEATH)
                        tilegrid[touch_x, touch_y] = BOMBDEATH
                        return False          #lost
                    elif under_the_tile > OPEN0 and under_the_tile <= OPEN8:
                        tilegrid[touch_x, touch_y] = under_the_tile
                    elif under_the_tile == OPEN0:
                        tilegrid[touch_x, touch_y] = BLANK
                        number_uncovered += expand_uncovered(touch_x, touch_y)
                    else:                    #something bad happened
                        raise ValueError('Unexpected value on board')
            status = check_for_win()
            if status is None:
                continue
            return status

There are a handful of support functions.

First is a function that is called when the game is lost, it reveals the board to the player, if any squares were flagged as a bomb incorrectly, they are shown as a crossed out bomb.

Download: file
def reveal():
    for x in range(20):
        for y in range(15):
            if tilegrid[x, y] == BOMBFLAGGED and get_data(x, y) != BOMB:
                tilegrid[x, y] = BOMBMISFLAGGED
            else:
                tilegrid[x, y] = get_data(x, y)
gaming_mistagged_bomb_loss.bmp
Loss from mistagged bombs

When a game is over, a sound is played. There is one function to start the sound and another to wait for it to finish playing then close it.  This allows something else to be done while the sound plays.

Download: file
def play_sound(file_name):
    board.DISPLAY.wait_for_frame()
    wavfile = open(file_name, "rb")
    wavedata = audioio.WaveFile(wavfile)
    speaker_enable.value = True
    audio.play(wavedata)
    return wavfile

def wait_for_sound_and_cleanup(wavfile):
    while audio.playing:
        pass
    wavfile.close()
    speaker_enable.value = False

If the game was won, there's nothing to do other than play a fanfare:

Download: file
def win():
    wait_for_sound_and_cleanup(play_sound('win.wav'))
gaming_win.bmp
A winning game

If the game was lost, a bomb sound is played. While it plays we do a shake effect on the screen by manipulating the location of the tilegrid slightly. Here we see the reason that the sound start/end was split between two functions.

Download: file
def lose():
    wavfile = play_sound('lose.wav')
    for _ in range(10):
        tilegrid.x = randint(-2, 2)
        tilegrid.y = randint(-2, 2)
        display.refresh_soon()
        display.wait_for_frame()
    tilegrid.x = 0
    tilegrid.y = 0
    wait_for_sound_and_cleanup(wavfile)

Finally we have the overall loop, that resets the board, plays the game, and signals whether it was won or lost. There's a 5 second delay before another game begins.

Download: file
while True:
    reset_board()
    if play_a_game():
        win()
    else:
        reveal()
        lose()
    time.sleep(5.0)
This guide was first published on Aug 09, 2019. It was last updated on Aug 09, 2019. This page (Playing) was last updated on Sep 14, 2019.