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.
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.
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.
- 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
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.
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.
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)
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.
def play_sound(file_name): board.DISPLAY.wait_for_frame() wavfile = open(file_name, "rb") wavedata = audiocore.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:
def win(): wait_for_sound_and_cleanup(play_sound('win.wav'))
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.
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.
while True: reset_board() if play_a_game(): win() else: reveal() lose() time.sleep(5.0)
Page last edited March 08, 2024
Text editor powered by tinymce.