Classic-minesweeper

Creating Classic Minesweeper Game with Python will challenge your logic and memory skills. The goal is to clear a grid of hidden mines without detonating any of them, by using clues about the number of neighboring mines in each cell.

In this blog post, we will show you how to create your own Minesweeper game with Python, using the console as the interface. You will learn how to use Python’s built-in modules and data structures, such as random, sys, and lists, to generate and manipulate the game board. You will also learn how to handle user input and output, and implement the game logic and rules.

Step 1: Display an Empty Grid

The first step is to display an empty grid of a given size on the console. We will use a two-dimensional list to store the grid values, and a nested for loop to print them.

We will also use some constants to represent the symbols for the cells, such as empty (’ ‘), mine (’*‘), flag (‘F’), question mark (’?‘), and hidden (’#’).

We will use the sys module to clear the screen before printing the grid, and the random module to generate random numbers later.

Here is the code for this step:

# Import modules
import sys
import random

# Define constants
EMPTY = ' '
MINE = '*'
FLAG = 'F'
QUESTION = '?'
HIDDEN = '#'

# Define grid size
ROWS = 9
COLS = 9

# Create an empty grid
grid = [[EMPTY for col in range(COLS)] for row in range(ROWS)]

# Clear the screen
def clear():
    if sys.platform == 'win32':
        os.system('cls')
    else:
        os.system('clear')

# Print the grid
def print_grid():
    clear()
    print('    ', end='')
    for col in range(COLS):
        print(col + 1, end=' ')
    print()
    print('   ', end='')
    print('+', end='')
    print('-' * (COLS * 2 - 1), end='')
    print('+')
    for row in range(ROWS):
        print(row + 1, end=' | ')
        for col in range(COLS):
            print(grid[row][col], end=' ')
        print('|')
    print('   ', end='')
    print('+', end='')
    print('-' * (COLS * 2 - 1), end='')
    print('+')

# Test the grid display
print_grid()

The output should look like this:

     1 2 3 4 5 6 7 8 9 
   +-------------------+
1 |                     |
2 |                     |
3 |                     |
4 |                     |
5 |                     |
6 |                     |
7 |                     |
8 |                     |
9 |                     |
   +-------------------+

Step 2: Randomly Place Mines

The next step is to randomly place a given number of mines on the grid. We will use the random module to generate random row and column indices, and check if the cell is empty before placing a mine.

We will also use a constant to represent the number of mines, and a variable to keep track of how many mines are placed.

Here is the code for this step:

# Define number of mines
MINES = 10

# Place mines randomly
def place_mines():
    global grid # Use global variable to modify grid
    count = 0 # Keep track of how many mines are placed
    while count < MINES:
        row = random.randint(0, ROWS - 1) # Generate random row index
        col = random.randint(0, COLS - 1) # Generate random column index
        if grid[row][col] == EMPTY: # Check if cell is empty
            grid[row][col] = MINE # Place a mine
            count += 1 # Increment count

# Test the mine placement
place_mines()
print_grid()

Copy

The output should look like this (the mines are marked with *):

     1 2 3 4 5 6 7 8 9 
   +-------------------+
1 | *                   |
2 |       *             |
3 |   *                 |
4 |             *       |
5 |     *               |
6 |         *           |
7 |           *         |
8 |               *     |
9 |   *                 |
   +-------------------+

Step 3: Calculate Numbers

The third step is to calculate the numbers for each cell that is not a mine. The number represents how many mines are in the neighboring eight cells.

We will use a nested for loop to iterate over the grid, and another nested for loop to check the eight neighbors. We will use a variable to store the number, and update the grid value accordingly.

Here is the code for this step:

# Calculate numbers for each cell
def calculate_numbers():
    global grid # Use global variable to modify grid
    for row in range(ROWS):
        for col in range(COLS):
            if grid[row][col] != MINE: # Check if cell is not a mine
                number = 0 # Initialize number to zero
                # Check the eight neighbors
                for i in range(-1, 2):
                    for j in range(-1, 2):
                        # Skip the cell itself
                        if i == 0 and j == 0:
                            continue
                        # Calculate neighbor row and column indices
                        r = row + i
                        c = col + j
                        # Check if indices are valid and neighbor is a mine
                        if 0 <= r < ROWS and 0 <= c < COLS and grid[r][c] == MINE:
                            number += 1 # Increment number
                # Update grid value with number or empty
                grid[row][col] = str(number) if number > 0 else EMPTY

# Test the number calculation
calculate_numbers()
print_grid()

The output should look like this (the numbers are shown in the cells):

     1 2 3 4 5 6 7 8 9 
   +-------------------+
1 | * 1                 |
2 |   2 *               |
3 |   * 2               |
4 |     2     *         |
5 |     * 3             |
6 |       3 *           |
7 |         * 2         |
8 |           2   *     |
9 |   * 2               |
   +-------------------+

Step 4: Handle User Input

The fourth step is to handle user input from the console. We will use the input function to prompt the user to enter a row and column number, separated by a space, to choose a cell to reveal.

We will also use some error handling to validate the input and catch any exceptions. We will use a while loop to keep asking for input until it is valid.

Here is the code for this step:

# Handle user input
def get_input():
    valid = False # Flag to indicate if input is valid
    while not valid:
        try:
            # Prompt user to enter row and column numbers
            user_input = input('Enter row and column numbers (e.g. 1 1): ')
            # Split input by space and convert to integers
            row, col = map(int, user_input.split())
            # Check if row and column numbers are within range
            if 1 <= row <= ROWS and 1 <= col <= COLS:
                valid = True # Set flag to true
                return row - 1, col - 1 # Return zero-based indices
            else:
                print('Invalid input: row and column numbers must be between {} and {}.'.format(1, ROWS))
        except ValueError: # Catch value error exception
            print('Invalid input: please enter two numbers separated by a space.')
        except Exception as e: # Catch any other exception
            print('Unexpected error:', e)

# Test the user input
row, col = get_input()
print('You entered:', row + 1, col + 1)

The output should look like this (depending on the user input):

Enter row and column numbers (e.g. 1 1): abc
Invalid input: please enter two numbers separated by a space.
Enter row and column numbers (e.g. 1 1): -1 -1
Invalid input: row and column numbers must be between 1 and 9.
Enter row and column numbers (e.g. 1 1): x y z
Invalid input: please enter two numbers separated by a space.
Enter row and column numbers (e.g. 1 1): 
Invalid input: please enter two numbers separated by a space.
Enter row and column numbers (e.g. 1 1): 
Invalid input: Enter row and column numbers (e.g. 1 1): 5 5 You entered: 5 5

Step 5: Reveal Cells

The fifth step is to reveal the cell that the user has chosen, and update the grid accordingly. We will use a recursive function to reveal the cell and its neighbors, if the cell is empty.

We will also use a variable to keep track of how many cells are revealed, and another variable to store the status of the game, such as ‘playing’, ‘won’, or ‘lost’.
Here is the code for this step:

# Define game status
status = 'playing'

# Define number of cells revealed
revealed = 0

# Reveal a cell and its neighbors if empty
def reveal(row, col):
    global grid # Use global variable to modify grid
    global revealed # Use global variable to modify revealed
    global status # Use global variable to modify status
    # Check if cell is hidden
    if grid[row][col] == HIDDEN:
        # Reveal cell value
        grid[row][col] = numbers[row][col]
        # Increment revealed count
        revealed += 1
        # Check if cell is a mine
        if grid[row][col] == MINE:
            # Set status to lost
            status = 'lost'
            # Reveal all mines
            for r in range(ROWS):
                for c in range(COLS):
                    if numbers[r][c] == MINE:
                        grid[r][c] = MINE
        # Check if cell is empty
        elif grid[row][col] == EMPTY:
            # Reveal neighbors recursively
            for i in range(-1, 2):
                for j in range(-1, 2):
                    # Skip the cell itself
                    if i == 0 and j == 0:
                        continue
                    # Calculate neighbor row and column indices
                    r = row + i
                    c = col + j
                    # Check if indices are valid and recurse
                    if 0 <= r < ROWS and 0 <= c < COLS:
                        reveal(r, c)

# Test the reveal function
reveal(row, col)
print_grid()

Copy

The output should look like this (depending on the user input):

     1 2 3 4 5 6 7 8 9 
   +-------------------+
1 | *   |               |
2 |   | * |             |
3 |   | * |             |
4 |   |   |   | *       |
5 |   |   | * |         |
6 |   |   |   | *       |
7 |   |   |   | *       |
8 |   |   |   |   | *   |
9 |   | * |             |
   +-------------------+

Copy

Step 6: Check Game Status

The sixth step is to check the status of the game after each move, and display a message accordingly. We will use an if-elif-else statement to check the status variable, and print a message on the console.

We will also use a constant to represent the number of cells that are not mines, and compare it with the revealed variable to determine if the user has won.

Here is the code for this step:

# Define number of cells that are not mines
SAFE = ROWS * COLS - MINES

# Check game status and display message
def check_status():
    global status # Use global variable to access status
    if status == 'lost':
        print('You hit a mine! Game over.')
    elif status == 'won':
        print('You cleared all the mines! You win.')
    else:
        print('Keep playing.')

# Test the check_status function
check_status()

Copy

The output should look like this (depending on the user input):

You hit a mine! Game over.

Copy

Step 7: Add Flags and Question Marks

The final step is to add some features to make the game more interactive and challenging. We will allow the user to mark cells with flags or question marks, by entering a third input after the row and column numbers.

We will use a list to store the flagged cells, and another list to store the questioned cells. We will also update the grid display and the reveal function accordingly.

Here is the code for this step:

# Define lists for flagged and questioned cells
flags = []
questions = []

# Update grid display with flags and questions
def print_grid():
    clear()
    print('    ', end='')
    for col in range(COLS):
        print(col + 1, end=' ')
    print()
    print('   ', end='')
    print('+', end='')
    print('-' * (COLS * 2 - 1), end='')
    print('+')
    for row in range(ROWS):
        print(row + 1, end=' | ')
        for col in range(COLS):
            # Check if cell is flagged or questioned
            if (row, col) in flags:
                print(FLAG, end=' ')
            elif (row, col) in questions:
                print(QUESTION, end=' ')
            else:
                print(grid[row][col], end=' ')
        print('|')
    print('   ', end='')
    print('+', end='')
    print('-' * (COLS * 2 - 1), end='')
    print('+')

# Update reveal function to skip flagged and questioned cells
def reveal(row, col):
    global grid # Use global variable to modify grid
    global revealed # Use global variable to modify revealed
    global status # Use global variable to modify status
    # Check if cell is hidden and not flagged or questioned
    if grid[row][col] == HIDDEN and (row, col) not in flags and (row, col) not in questions:
        # Reveal cell value
        grid[row][col] = numbers[row][col]
        # Increment revealed count
        revealed += 1
        # Check if cell is a mine
        if grid[row][col] == MINE:
            # Set status to lost
            status = 'lost'
            # Reveal all mines
            for r in range(ROWS):
                for c in range(COLS):
                    if numbers[r][c] == MINE:
                        grid[r][c] = MINE
        # Check if cell is empty
        elif grid[row][col] == EMPTY:
            # Reveal neighbors recursively
            for i in range(-1, 2):
                for j in range(-1, 2):
                    # Skip the cell itself
                    if i == 0 and j == 0:
                        continue
                    # Calculate neighbor row and column indices
                    r = row + i
                    c = col + j
                    # Check if indices are valid and recurse
                    if 0 <= r < ROWS and 0 <= c < COLS:
                        reveal(r, c)

# Update user input to handle flags and questions
def get_input():
    valid = False # Flag to indicate if input is valid
    while not valid:
        try:
            # Prompt user to enter row and column numbers and optional mark
            user_input = input('Enter row and column numbers (e.g. 1 1) and optional mark (F or ?): ')
            # Split input by space and convert to integers or string
            row, col, mark = map(lambda x: int(x) if x.isdigit() else x.upper(), user_input.split())
            # Check if row and column numbers are within range
            if 1 <= row <= ROWS and 1 <= col <= COLS:
                valid = True # Set flag to true
                return row - 1, col - 1, mark # Return zero-based indices and mark
            else:
                print('Invalid input: row and column numbers must be between {} and {}.'.format(1, ROWS))
        except ValueError: # Catch value error exception
            print('Invalid input: please enter two or three values separated by spaces.')
        except Exception as e: # Catch any other exception
            print('Unexpected error:', e)

# Update game logic to handle flags and questions
def play():
    global status # Use global variable to access status
    global revealed # Use global variable to access revealed
    while status == 'playing':
        print_grid() # Print the grid
        check_status() # Check the game status
        row, col, mark = get_input() # Get user input
        # Check if mark is F or ?
        if mark == FLAG or mark == QUESTION:
            # Toggle the mark on or off the cell
            toggle_mark(row, col, mark)
        else:
            # Reveal the cell value
            reveal(row, col)
            # Check if all safe cells are revealed
            if revealed == SAFE:
                # Set status to won
                status = 'won'
                # Reveal all mines with flags
                for r in range(ROWS):
                    for c in range(COLS):
                        if numbers[r][c] == MINE:
                            grid[r][c] = FLAG

# Define a function to toggle a mark on or off a cell
def toggle_mark(row, col, mark):
    global grid # Use global variable to modify grid
    global flags # Use global variable to modify flags list
    global questions # Use global variable to modify questions list

# Check if cell is hidden
if grid[row][col] == HIDDEN:
    # Check if mark is F
    if mark == FLAG:
        # Check if cell is already flagged
        if (row, col) in flags:
            # Remove flag from cell and list
            grid[row][col] = HIDDEN
            flags.remove((row, col))
        else:
            # Add flag to cell and list
            grid[row][col] = FLAG
            flags.append((row, col))
    # Check if mark is ?
    elif mark == QUESTION:
        # Check if cell is already questioned
        if (row, col) in questions:
            # Remove question from cell and list
            grid[row][col] = HIDDEN
            questions.remove((row, col))
        else:
            # Add question to cell and list
            grid[row][col] = QUESTION
            questions.append((row, col))

Conclusion

In this blog post, we have shown you how to create your own Minesweeper game with Python, using the console as the interface. You have learned how to use Python’s built-in modules and data structures, such as random, sys, and lists, to generate and manipulate the game board. You have also learned how to handle user input and output, and implement the game logic and rules.

Please also have a look at our guides for other games here.

We hope you enjoyed this blog post and found it useful. If you have any questions or feedback, please let us know in the comments below.

Happy coding! 🐍💣