25.9. Gameplay Functions Review (Optional)

When you first cloned the Minesweeper project from GitHub, the repository included several complete functions. The video tutorials in the chapter mention these functions, but they skip a detailed analysis of the code. Well, now is a good time to take a deeper look!

This page describes how the ready-made functions work. However, the code shows ONE way to solve the gameplay tasks. There are many OTHER ways to achieve the same results. Feel free to create a new branch in your Git repository and experiment with your own ideas. You can try to streamline the functions, or replace any of them with a completely new set of statements. Remember, we learn to code by coding, and this includes examining other programmers’ work.

25.9.1. The game_logic.py Module

This file manages the nitty-gritty details of running Minesweeper. It resets the conditions for each new game, initializes session variables, populates lists, hides mines, formats SQL query strings, and checks if the player makes a safe choice on the board.

The module begins by importing three other Python modules:

1
2
3
import random
import string
from crud import *

random is used to choose where to hide the mines. string helps build the row labels and cell coordinates. The functions from crud run the SQL queries.

Now let’s examine four of the functions included in game_logic.py.

25.9.1.1. The make_columns() Function

make_columns() is the shortest function in the module. It’s job is to fill a list with the column headings for the game board. It is called from the reset_board() function.

The 11 column headings on the game board. The first is blank, followed by numbers 1 - 10.

The game board has 11 columns, numbered 1 - 10. The first column doesn’t contain a heading.

20
21
22
23
24
def make_columns():
   headings = ['']
   for label in range(10):
      headings.append(label+1)
   return headings.copy()

Here’s a breakdown of the code:

  1. Line 21: Instead of an empty list, headings begins with a single entry. That string value keeps the upper left cell on the board blank. Since the first column contains row letters instead of active spaces, no heading is necessary.
  2. Line 22: This sets up a basic for loop. Each time it repeats, label is assigned a new integer (0, 1, 2, … 9).
  3. Line 23: This appends the value label + 1 to the end of the headings list.
  4. Line 24: This returns an independent copy of the column headings, which is assigned to session['columns'].

Note

Since the loop just builds a list of 10 numbers, you might wonder why we don’t hard-code the result.

headings = ['', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Using a loop keeps our code flexible. If we add a parameter to make_columns(), we can adapt the for loop to generate a different number of headings. This opens up the possibility for other board layouts.

def make_columns(num_headings = 10):
   headings = ['']
   for label in range(num_headings):
      headings.append(label+1)
   return headings.copy()

25.9.1.2. The make_rows() Function

make_rows() is also called from the reset_board() function. Its job is to build a list with all the string values needed for the row labels and button text.

There are 10 rows on the game board (A - J), and each one contains 11 columns. To fit this structure, make_rows() returns a two-dimensional list of lists.

rows = [ [row_A_entries], [row_B_entries], ... [row_J_entries] ]

Each entry in rows is a list with 11 elements. Each element is the string value for a row label (A - J) or button text (like B7).

To generate the 2-dimensional list, make_rows() uses a pair of nested loops.

26
27
28
29
30
31
32
33
34
35
36
37
def make_rows():
   rows = []
   for row in range(10):
      letter = string.ascii_uppercase[row]
      cells = []
      for column in range(11):
         if column == 0:
            cells.append(letter)
         else:
            cells.append(letter + str(column))
      rows.append(cells)
   return rows

Here’s a breakdown of the code:

  1. Line 27: Assigns an empty list to rows.

  2. Lines 28 - 30: Line 28 starts the outer loop. Each time it repeats, row is assigned a new value in the range 0 - 9. Line 29 uses this integer to assign an uppercase character (A - J) to letter. Line 30 assigns an empty list to the accumulator variable cells.

  3. Lines 31 - 35: Line 31 begins the inner loop. Each time it repeats, column is assigned an integer in the range 0 - 10. When column == 0, we are dealing with the first cell in the row. Line 33 appends letter to cells.

    When column is not 0, the space on the board will contain a button. Line 35 appends a letter/number combination to cells. This string will be used as the text inside the button.

    After the inner loop finishes, the cells list contains 11 entries.

  4. Line 36: This statement is part of the outer loop. It appends the completed cells list to rows.

  5. Line 37: This returns the completed rows list, which is assigned to session['rows'].

25.9.1.3. The place_mines() Function

place_mines() is called from the index() function in main.py. Its job is to randomly assign mines to locations on the game board. It accepts a single parameter, which is the number of mines to hide.

39
40
41
42
43
44
45
46
47
48
49
50
def place_mines(amount):
   mines = []
   while len(mines) < amount:
      row = random.choice(string.ascii_uppercase[0:10])
      column = random.randint(1, 10)
      location = row + str(column)
      if location not in mines:
         mines.append(location)
   mines.sort()
   record_mines(mines)
   count_mines()
   return mines.copy()

Here’s a breakdown of the code:

  1. Line 40: Assigns an empty list to the mines variable. This begins yet another example of the accumulator pattern!

  2. Line 41: The condition len(mines) < amount keeps the while loop running until the number of entries in mines matches the number assigned to amount.

  3. Line 42: string.ascii_uppercase[0:10] returns a slice from the string of uppercase letters. In this case, the index values [0:10] return the letters 'ABCDEFGHIJ'.

    random.choice then selects one letter from the slice.

  4. Line 43: This selects a random integer from 1 - 10, including both end points.

  5. Line 44: This combines the row letter with the column number and assigns the string to location.

  6. Lines 45 & 46: The conditional prevents duplicate choices for the mine locations. If the newly chosen cell is NOT currently in the mines list, it is added. Otherwise, the choice is ignored.

  7. Lines 47 - 50: These statements alphabetize the mines list, call two of the crud.py functions, and return an independent copy of the list.

Note

We coded the record_mines() and count_mines() functions on the Database Functions page.

25.9.1.4. The check_guess() Function

check_guess() is called from the play() function in main.py. It returns True each time the player chooses a safe cell on the game board. This happens when the cell does NOT contain a mine, or if the user selects the Flag Mine option before clicking on the space. check_guess() returns False when the player hits a mine.

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def check_guess(guess, flag):
   safe_guess = True
   if flag:
      session['flags'].append(guess)
      session['num_mines'] -= 1
      if guess in session['mines']:
         session['mines'].remove(guess)
   else:
      sql_query = f"SELECT * FROM board WHERE coordinates = '{guess}' AND mine_id IS NULL"
      no_mine = execute_query(sql_query)
      if no_mine:
         session['guesses'].append(guess)
         if guess in session['flags']:
            session['flags'].remove(guess)
            session['num_mines'] += 1
      else:
         safe_guess = False
   session.modified = True
   sql_query = f"UPDATE board SET guessed = True WHERE coordinates = '{guess}'"
   execute_query(sql_query)
   return safe_guess

Given the size of the function, it’s easier to review it with a video!

25.9.2. The crud.py Module

This file manages the nitty-gritty details of interacting with the game’s database. We reviewed or coded all but one of these functions earlier in this chapter. Now it’s time to complete that work.

25.9.2.1. The check_surroundings() Function

check_surroundings() is called at the start of each new round of Minesweeper. When the player submits a number of mines to hide:

  1. The index() function calls place_mines().
  2. The place_mines() function picks random locations on the board to hide the new set of mines. Then it calls the count_mines() function.
  3. count_mines() iterates through all of the cells in the board table, and it passes each location to check_surroundings().

For a given cell on the table, check_surroundings() counts the number of mines hidden in the surrounding spaces. This total is stored in both the session cookie and the database.

The code for check_surroundings() is probably the most involved. To help break up the discussion, we’ve split the explanation into two parts. The first describes how to perform a 2-dimensional search, which checks the 8 cells above, below, and to each side of the selected location. The second video examines the Python code used to perform that search.

Showing the difference between a 1-dimensional and 2-dimensional search.

A 2-dimensional search involves changing the row AND column values.

25.9.2.2. How to Check Surrounding Cells

25.9.2.3. The check_surroundings() Code

25.9.2.4. Video Summary

  1. To check all of the spaces surrounding a given cell, we can use a pair of nested for loops. The outer loop sets a new row value. The inner loop iterates over the possible columns in each row.
  2. To simplify our search algorithm, add extra cells to the board!
    1. Place the cells along the outer edge of the game board.
    2. The cells remain hidden from the player, and they will never contain a mine.
    3. The cost of storing data for the hidden cells is well worth it. We gain a lot more by decreasing the complexity of the search.