Create Minesweeper using Python From the Basic to Advanced

Minesweeper Featured Image

In this article, we will be going through the steps of creating our own terminal-based Minesweeper using Python Language.

About the game

Minesweeper is a single-player game in which the player has to clear a square grid containing mines and numbers. The player has to prevent himself from landing on a mine with the help of numbers in the neighbouring tiles.


Gameplay Demo

Aftermath of few hours of creating a game of Minesweeper.

Minesweeper Demo

Designing Minesweeper Using Python

Before creating the game logic, we need to design the basic layout of the game. A square grid is rather easy to create using Python by:

# Printing the Minesweeper Layout
def print_mines_layout():
	global mine_values
	global n

	print()
	print("\t\t\tMINESWEEPER\n")

	st = "   "
	for i in range(n):
		st = st + "     " + str(i + 1)
	print(st)	

	for r in range(n):
		st = "     "
		if r == 0:
			for col in range(n):
				st = st + "______"	
			print(st)

		st = "     "
		for col in range(n):
			st = st + "|     "
		print(st + "|")
		
		st = "  " + str(r + 1) + "  "
		for col in range(n):
			st = st + "|  " + str(mine_values[r][col]) + "  "
		print(st + "|")	

		st = "     "
		for col in range(n):
			st = st + "|_____"
		print(st + '|')

	print()

The grid displayed in each iteration resembles the following figure:

Minesweeper Layout
Minesweeper Layout

The 'M' symbol denotes the presence of a ‘mine’ in that cell. As we can see clearly, any number on the grid denotes the number of mines present in the neighbouring ‘eight’ cells.

The use of variables like, mine_values will be explained further in the tutorial.


Input system

One of the most important parts of any game is sustaining the input method. In our version of Minesweeper, we will be using the row and column numbers for our input technique.

Before starting the game, the script must provide a set of instructions for the player. Our game prints the following.

Minesweeper Instructions
Minesweeper Instructions

The row and column numbers displayed along with the grid are helpful for our input system. As we know, keeping track of mines without any indicator can be difficult. Therefore, Minesweeper has a provision of using ‘flag’ to mark the cells, which we know contains a mine.


Data Storage

For a single game of Minesweeper, we need to keep track of the following information:

  • The size of the grid.
  • The number of mines.
  • The ‘actual’ grid values – At the start of the game, we need a container for storing the real values for the game, unknown to the player. For instance, the location of mines.
  • The ‘apparent’ grid values – After each move, we need to update all the values that must be shown to the player.
  • The flagged positions – The cells which have been flagged.

These values are stored using the following data structures

if __name__ == "__main__":

	# Size of grid
	n = 8
	# Number of mines
	mines_no = 8

	# The actual values of the grid
	numbers = [[0 for y in range(n)] for x in range(n)] 
	# The apparent values of the grid
	mine_values = [[' ' for y in range(n)] for x in range(n)]
	# The positions that have been flagged
	flags = []

There is not much in the game-logic of Minesweeper. All the effort is to be done in setting up the Minesweeper layout.


Setting up the Mines

We need to set up the positions of the mines randomly, so that the player might not predict their positions. This can be done by:

# Function for setting up Mines
def set_mines():

	global numbers
	global mines_no
	global n

	# Track of number of mines already set up
	count = 0
	while count < mines_no:

		# Random number from all possible grid positions 
		val = random.randint(0, n*n-1)

		# Generating row and column from the number
		r = val // n
		col = val % n

		# Place the mine, if it doesn't already have one
		if numbers[r][col] != -1:
			count = count + 1
			numbers[r][col] = -1

In the code, we choose a random number from all possible cells in the grid. We keep doing this until we get the said number of mines.

Note: The actual value for a mine is stored as -1, whereas the values stored for display, denote the mine as 'M'.

Note: The ‘randint’ function can only be used after importing the random library. It is done by writing 'import random' at the start of the program.


Setting up the grid numbers

For each cell in the grid, we have to check all adjacent neighbours whether there is a mine present or not. This is done by:

# Function for setting up the other grid values
def set_values():

	global numbers
	global n

	# Loop for counting each cell value
	for r in range(n):
		for col in range(n):

			# Skip, if it contains a mine
			if numbers[r][col] == -1:
				continue

			# Check up	
			if r > 0 and numbers[r-1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check down	
			if r < n-1  and numbers[r+1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check left
			if col > 0 and numbers[r][col-1] == -1:
				numbers[r][c] = numbers[r][c] + 1
			# Check right
			if col < n-1 and numbers[r][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-left	
			if r > 0 and col > 0 and numbers[r-1][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-right
			if r > 0 and col < n-1 and numbers[r-1][col+1]== -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-left	
			if r < n-1 and col > 0 and numbers[r+1][col-1]== -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-right
			if r < n-1 and col< n-1 and numbers[r+1][col+1]==-1:
				numbers[r][col] = numbers[r][col] + 1

These values are to be hidden from the player, therefore they are stored in numbers variable.


Game Loop

Game Loop is a very crucial part of the game. It is needed to update every move of the player as well as the conclusion of the game.

# Set the mines
set_mines()

# Set the values
set_values()

# Display the instructions
instructions()

# Variable for maintaining Game Loop
over = False
		
# The GAME LOOP	
while not over:
	print_mines_layout()

In each iteration of the loop, the Minesweeper grid must be displayed as well as the player’s move must be handled.


Handle the player input

As we mentioned before, there are two kinds of player input :

# Input from the user
inp = input("Enter row number followed by space and column number = ").split()

Standard input

In a normal kind of move, the row and column number are mentioned. The player’s motive behind this move is to unlock a cell that does not contain a mine.

# Standard Move
if len(inp) == 2:

	# Try block to handle errant input
	try: 
		val = list(map(int, inp))
	except ValueError:
		clear()
		print("Wrong input!")
		instructions()
		continue

Flag input

In a flagging move, three values are sent in by the gamer. The first two values denote cell location, while the last one denotes flagging.

# Flag Input
elif len(inp) == 3:
	if inp[2] != 'F' and inp[2] != 'f':
		clear()
		print("Wrong Input!")
		instructions()
		continue

	# Try block to handle errant input	
	try:
		val = list(map(int, inp[:2]))
	except ValueError:
		clear()
		print("Wrong input!")
		instructions()
		continue

Sanitize the input

After storing the input, we have to do some sanity checks, for the smooth functioning of the game.

# Sanity checks
if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
	clear()
	print("Wrong Input!")
	instructions()
	continue

# Get row and column numbers
r = val[0]-1
col = val[1]-1

On the completion of input process, the row and column numbers are to be extracted and stored in 'r' and 'c'.


Handle the flag input

Managing the flag input is not a big issue. It requires checking for some pre-requisites before flagging the cell for a mine.

The following checks must be made:

  • The cell has already been flagged or not.
  • Whether the cell to be flagged is already displayed to the player.
  • The number of flags does not exceed the number of mines.

After taking care of these issues, the cell is flagged for a mine.

# If cell already been flagged
if [r, col] in flags:
	clear()
	print("Flag already set")
	continue

# If cell already been displayed
if mine_values[r][col] != ' ':
	clear()
	print("Value already known")
	continue

# Check the number for flags 	
if len(flags) < mines_no:
	clear()
	print("Flag set")

	# Adding flag to the list
	flags.append([r, col])
	
	# Set the flag for display
	mine_values[r][col] = 'F'
	continue
else:
	clear()
	print("Flags finished")
	continue	 

Handle the standard input

The standard input involves the overall functioning of the game. There are three different scenarios:

Anchoring on a mine

The game is finished as soon as the player selects a cell having a mine. It can happen out of bad luck or poor judgment.

# If landing on a mine --- GAME OVER	
if numbers[r][col] == -1:
	mine_values[r][col] = 'M'
	show_mines()
	print_mines_layout()
	print("Landed on a mine. GAME OVER!!!!!")
	over = True
	continue

After we land on a cell with mine, we need to display all the mines in the game and alter the variable behind the game loop.

The function 'show_mines()' is responsible for it.

def show_mines():
	global mine_values
	global numbers
	global n

	for r in range(n):
		for col in range(n):
			if numbers[r][col] == -1:
				mine_values[r][col] = 'M'

Visiting a ‘0’-valued cell.

The trickiest part of creating the game is managing this scenario. Whenever a gamer, visits a ‘0’-valued cell, all the neighboring elements must be displayed until a non-zero-valued cell is reached.

# If landing on a cell with 0 mines in neighboring cells
elif numbers[r][n] == 0:
	vis = []
	mine_values[r][n] = '0'
	neighbours(r, col)	

This objective is achieved using Recursion. Recursion is a programming tool in which the function calls itself until the base case is satisfied. The neighbours function is a recursive one, solving our problem.

def neighbours(r, col):
	
	global mine_values
	global numbers
	global vis

	# If the cell already not visited
	if [r,col] not in vis:

		# Mark the cell visited
		vis.append([r,col])

		# If the cell is zero-valued
		if numbers[r][col] == 0:

			# Display it to the user
			mine_values[r][col] = numbers[r][col]

			# Recursive calls for the neighbouring cells
			if r > 0:
				neighbours(r-1, col)
			if r < n-1:
				neighbours(r+1, col)
			if col > 0:
				neighbours(r, col-1)
			if col < n-1:
				neighbours(r, col+1)	
			if r > 0 and col > 0:
				neighbours(r-1, col-1)
			if r > 0 and col < n-1:
				neighbours(r-1, col+1)
			if r < n-1 and col > 0:
				neighbours(r+1, col-1)
			if r < n-1 and col < n-1:
				neighbours(r+1, col+1)	
				
		# If the cell is not zero-valued 			
		if numbers[r][col] != 0:
				mine_values[r][col] = numbers[r][col]

For this particular concept of the game, a new data structure is used, namely, vis. The role of vis to keep track of already visited cells during recursion. Without this information, the recursion will continue perpetually.

After all the cells with zero value and their neighbours are displayed, we can move on to the last scenario.

Choosing a non zero-valued cell

No effort is needed to handle this case, as all we need to do is alter the displaying value.

# If selecting a cell with atleast 1 mine in neighboring cells	
else:	
	mine_values[r][col] = numbers[r][col]

End game

There is a requirement to check for completion of the game, each time a move is made. This is done by:

# Check for game completion	
if(check_over()):
	show_mines()
	print_mines_layout()
	print("Congratulations!!! YOU WIN")
	over = True
	continue

The function check_over(), is responsible for checking the completion of the game.

# Function to check for completion of the game
def check_over():
	global mine_values
	global n
	global mines_no

	# Count of all numbered values
	count = 0

	# Loop for checking each cell in the grid
	for r in range(n):
		for col in range(n):

			# If cell not empty or flagged
			if mine_values[r][col] != ' ' and mine_values[r][col] != 'F':
				count = count + 1
	
	# Count comparison 			
	if count == n * n - mines_no:
		return True
	else:
		return False

We count the number of cells, that are not empty or flagged. When this count is equal to the total cells, except those containing mines, then the game is regarded as over.


Clearing output after each move

The terminal becomes crowded as we keep on printing stuff on it. Therefore, there must be provision for clearing it constantly. This can be done by:

# Function for clearing the terminal
def clear():
	os.system("clear")

Note: There is a need to import the os library, before using this feature. It can be done by 'import os' at the start of the program.


The complete code

Below is the complete code of the Minesweeper game:

# Importing packages
import random
import os

# Printing the Minesweeper Layout
def print_mines_layout():

	global mine_values
	global n

	print()
	print("\t\t\tMINESWEEPER\n")

	st = "   "
	for i in range(n):
		st = st + "     " + str(i + 1)
	print(st)	

	for r in range(n):
		st = "     "
		if r == 0:
			for col in range(n):
				st = st + "______"	
			print(st)

		st = "     "
		for col in range(n):
			st = st + "|     "
		print(st + "|")
		
		st = "  " + str(r + 1) + "  "
		for col in range(n):
			st = st + "|  " + str(mine_values[r][col]) + "  "
		print(st + "|")	

		st = "     "
		for col in range(n):
			st = st + "|_____"
		print(st + '|')

	print()
 
# Function for setting up Mines
def set_mines():

	global numbers
	global mines_no
	global n

	# Track of number of mines already set up
	count = 0
	while count < mines_no:

		# Random number from all possible grid positions 
		val = random.randint(0, n*n-1)

		# Generating row and column from the number
		r = val // n
		col = val % n

		# Place the mine, if it doesn't already have one
		if numbers[r][col] != -1:
			count = count + 1
			numbers[r][col] = -1

# Function for setting up the other grid values
def set_values():

	global numbers
	global n

	# Loop for counting each cell value
	for r in range(n):
		for col in range(n):

			# Skip, if it contains a mine
			if numbers[r][col] == -1:
				continue

			# Check up	
			if r > 0 and numbers[r-1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check down	
			if r < n-1  and numbers[r+1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check left
			if col > 0 and numbers[r][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check right
			if col < n-1 and numbers[r][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-left	
			if r > 0 and col > 0 and numbers[r-1][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-right
			if r > 0 and col < n-1 and numbers[r-1][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-left	
			if r < n-1 and col > 0 and numbers[r+1][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-right
			if r < n-1 and col < n-1 and numbers[r+1][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1

# Recursive function to display all zero-valued neighbours	
def neighbours(r, col):
	
	global mine_values
	global numbers
	global vis

	# If the cell already not visited
	if [r,col] not in vis:

		# Mark the cell visited
		vis.append([r,col])

		# If the cell is zero-valued
		if numbers[r][col] == 0:

			# Display it to the user
			mine_values[r][col] = numbers[r][col]

			# Recursive calls for the neighbouring cells
			if r > 0:
				neighbours(r-1, col)
			if r < n-1:
				neighbours(r+1, col)
			if col > 0:
				neighbours(r, col-1)
			if col < n-1:
				neighbours(r, col+1)	
			if r > 0 and col > 0:
				neighbours(r-1, col-1)
			if r > 0 and col < n-1:
				neighbours(r-1, col+1)
			if r < n-1 and col > 0:
				neighbours(r+1, col-1)
			if r < n-1 and col < n-1:
				neighbours(r+1, col+1)	

		# If the cell is not zero-valued 			
		if numbers[r][col] != 0:
				mine_values[r][col] = numbers[r][col]

# Function for clearing the terminal
def clear():
	os.system("clear")		

# Function to display the instructions
def instructions():
	print("Instructions:")
	print("1. Enter row and column number to select a cell, Example \"2 3\"")
	print("2. In order to flag a mine, enter F after row and column numbers, Example \"2 3 F\"")

# Function to check for completion of the game
def check_over():
	global mine_values
	global n
	global mines_no

	# Count of all numbered values
	count = 0

	# Loop for checking each cell in the grid
	for r in range(n):
		for col in range(n):

			# If cell not empty or flagged
			if mine_values[r][col] != ' ' and mine_values[r][col] != 'F':
				count = count + 1
	
	# Count comparison 			
	if count == n * n - mines_no:
		return True
	else:
		return False

# Display all the mine locations					
def show_mines():
	global mine_values
	global numbers
	global n

	for r in range(n):
		for col in range(n):
			if numbers[r][col] == -1:
				mine_values[r][col] = 'M'


if __name__ == "__main__":

	# Size of grid
	n = 8
	# Number of mines
	mines_no = 8

	# The actual values of the grid
	numbers = [[0 for y in range(n)] for x in range(n)] 
	# The apparent values of the grid
	mine_values = [[' ' for y in range(n)] for x in range(n)]
	# The positions that have been flagged
	flags = []

	# Set the mines
	set_mines()

	# Set the values
	set_values()

	# Display the instructions
	instructions()

	# Variable for maintaining Game Loop
	over = False
		
	# The GAME LOOP	
	while not over:
		print_mines_layout()

		# Input from the user
		inp = input("Enter row number followed by space and column number = ").split()
		
		# Standard input
		if len(inp) == 2:

			# Try block to handle errant input
			try: 
				val = list(map(int, inp))
			except ValueError:
				clear()
				print("Wrong input!")
				instructions()
				continue

		# Flag input
		elif len(inp) == 3:
			if inp[2] != 'F' and inp[2] != 'f':
				clear()
				print("Wrong Input!")
				instructions()
				continue

			# Try block to handle errant input	
			try:
				val = list(map(int, inp[:2]))
			except ValueError:
				clear()
				print("Wrong input!")
				instructions()
				continue

			# Sanity checks	
			if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
				clear()
				print("Wrong input!")
				instructions()
				continue

			# Get row and column numbers
			r = val[0]-1
			col = val[1]-1	

			# If cell already been flagged
			if [r, col] in flags:
				clear()
				print("Flag already set")
				continue

			# If cell already been displayed
			if mine_values[r][col] != ' ':
				clear()
				print("Value already known")
				continue

			# Check the number for flags 	
			if len(flags) < mines_no:
				clear()
				print("Flag set")

				# Adding flag to the list
				flags.append([r, col])
				
				# Set the flag for display
				mine_values[r][col] = 'F'
				continue
			else:
				clear()
				print("Flags finished")
				continue	 

		else: 
			clear()
			print("Wrong input!")	
			instructions()
			continue
			

		# Sanity checks
		if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
			clear()
			print("Wrong Input!")
			instructions()
			continue
			
		# Get row and column number
		r = val[0]-1
		col = val[1]-1

		# Unflag the cell if already flagged
		if [r, col] in flags:
			flags.remove([r, col])

		# If landing on a mine --- GAME OVER	
		if numbers[r][col] == -1:
			mine_values[r][col] = 'M'
			show_mines()
			print_mines_layout()
			print("Landed on a mine. GAME OVER!!!!!")
			over = True
			continue

		# If landing on a cell with 0 mines in neighboring cells
		elif numbers[r][col] == 0:
			vis = []
			mine_values[r][col] = '0'
			neighbours(r, col)

		# If selecting a cell with atleast 1 mine in neighboring cells	
		else:	
			mine_values[r][col] = numbers[r][col]

		# Check for game completion	
		if(check_over()):
			show_mines()
			print_mines_layout()
			print("Congratulations!!! YOU WIN")
			over = True
			continue
		clear()	

Conclusion

We hope that this tutorial on creating our own Minesweeper game was understandable as well as fun. For any queries, feel free to comment below. The complete code is also available on my Github account.