# Minesweeper - Python 3 (beginner)

I’ve just created Minesweeper game, which work perfectly fine (for me). Any suggestions on how to improve this code would be greatly appreciate, in terms of:

• Object-oriented
• Array
• Error handling
• Function
• More efficient approach
• Others: Style(PEP8), comments, clean code, recursion, etc

Explanation of game:

• Minesweeper is an array(table) of hidden mines and non-mine cells.
• You can reveal any cells, one by one. Once you found the mines, you lose.
• If you can revealed all non-mine cells, you win.
• Each non-mine cell show you number of neighbor cell that contain mines.
• Read more Wikipedia, or you can try it on your computer.

My approach:

Clean version


import random

class Minesweeper:

def __init__(self,width=9,height=10,mine_numbers=12):

self.width = width
self.height = height
self.mine_numbers = mine_numbers
self.table = [None]*self.width*self.height

self.user_cell = False
self.user_row = False
self.user_column = False
self.user_reveal = []

def game_create(self):
print(f'Default size is {self.width}*{self.height}, {self.mine_numbers} mines')
default_size = input('Play default size?(Y/N): ')

if default_size.lower() == 'n':
correct_input = False
while not correct_input:
try:
self.width = int(input('Enter width: '))
self.height = int(input('Enter height: '))
self.mine_numbers = int(input('Enter number of mines: '))
if self.mine_numbers >= self.width*self.height or self.mine_numbers == 0:
print('ERROR: Number of mines can not be 0 or equal/exceed table size')
elif self.width > 99 or self.height > 99:
print('ERROR: Maximum table size is 99*99')
else:
self.table = [None]*self.width*self.height
self.user_reveal = []
correct_input = True
return self.width,self.height,self.mine_numbers,self.table,self.user_reveal
except ValueError:
print('ERROR: Try again, number only')
else:
self.table = [None]*self.width*self.height
self.user_reveal = []
return self.width,self.height,self.mine_numbers,self.table,self.user_reveal

def user_input(self):

correct_input = False

while not correct_input:
try:
self.user_cell = input('Enter {[column][row]} in 4 digits eg. 0105: ')
int(self.user_cell)
if len(self.user_cell) != 4:
print('ERROR: Only 4 digits allowed')
elif int(self.user_cell[2:]) > self.height or self.user_cell[2:] == '00':
print('ERROR: Row out of range')
elif int(self.user_cell[:2]) > self.width or self.user_cell[:2] == '00':
print('ERROR: Column of range')
elif self.user_cell in self.user_reveal:
else:
correct_input = True
except ValueError:
print('ERROR: Try again, number only')

if self.user_cell:
self.user_row = int(self.user_cell[2:])
self.user_column = int(self.user_cell[:2])
self.user_reveal.append(self.user_cell)

return self.user_cell,self.user_row,self.user_column

def mines_generator(self):

user_location = ((self.user_row-1)*self.width)+self.user_column-1
possible_location = [i for i in range(self.width*self.height) if i != user_location]
mines_location = random.sample(possible_location,self.mine_numbers)

for i in mines_location:
self.table[i] = 9

return self.table

def two_dimension_array(self):
for i in range(self.height):
self.table[i] = self.table[0+(self.width*i):self.width+(self.width*i)]

del self.table[self.height:]

return self.table

def complete_table(self):

temporary_table = [[None for _ in range(self.width)] for _ in range(self.height)]

for i in range(self.height):
for j in range(self.width):
if self.table[i][j] == 9:
temporary_table[i][j] = 9
continue
else:
counter = 0
for k in range(i-1,i+2):
if 0 <= k <= self.height-1:
for l in range(j-1,j+2):
if 0 <= l <= self.width-1:
if self.table[k][l] == 9:
counter += 1
continue
temporary_table[i][j] = counter
self.table = temporary_table

return self.table

if self.table[int(zero_cell[2:])-1][int(zero_cell[:2])-1] == 0:
for i in range(int(zero_cell[2:])-1-1,int(zero_cell[2:])-1+2):
if 0 <= i < self.height:
for j in range(int(zero_cell[:2])-1-1,int(zero_cell[:2])-1+2):
if 0 <= j < self.width:
if str(j+1).zfill(2)+str(i+1).zfill(2) not in self.user_reveal:
self.user_reveal.append(str(j+1).zfill(2)+str(i+1).zfill(2))
if self.table[i][j] == 0:

return self.user_reveal

def first_turn(self):
self.user_input()
self.mines_generator()
self.two_dimension_array()
self.complete_table()

def print_table(self):
print('\n'*10)
for row in range(self.height+1):
cell = '|'
for column in range(self.width+1):
if row == 0:
cell += f'{column:2}|'
continue
elif column == 0:
cell += f'{row:2}|'
continue
elif str(column).zfill(2)+str(row).zfill(2) in self.user_reveal:
cell += f'{self.table[row-1][column-1]:2}|'
continue
else:
cell += '{:>3}'.format('|')
print(cell)

def end_game(self):

def reveal_mine():
for i,j in enumerate(self.table):
for k,l in enumerate(j):
if l == 9:
self.table[i][k] = ‘XX’
if str(k+1).zfill(2)+str(i+1).zfill(2) not in self.user_reveal:
self.table[i][k] = ‘**’
self.user_reveal.append(str(k+1).zfill(2)+str(i+1).zfill(2))

if self.user_cell:
if self.table[self.user_row-1][self.user_column-1] == 9:
end_game = True
reveal_mine()
self.print_table()
print('YOU LOSE!')
elif len(self.user_reveal) == (self.width*self.height)-self.mine_numbers:
end_game = True
reveal_mine()
self.print_table()
print('YOU WIN!')
else:
end_game = False
else:
end_game = False

return end_game

def restart_game(self):

restart = input('Restart?(Y/N): ')

if restart.lower() == 'y':
return True
else:
return False

def main():

minesweeper = Minesweeper()

while True:
minesweeper.game_create()
minesweeper.print_table()
minesweeper.first_turn()

while not minesweeper.end_game():
minesweeper.print_table()
minesweeper.user_input()

if not minesweeper.restart_game():
break

if __name__ == '__main__':
main()


Comment version


import random

class Minesweeper:

def __init__(self,width=9,height=10,mine_numbers=12):
# Table generate: change via tables_ize()
self.width = width
self.height = height
self.mine_numbers = mine_numbers
self.table = [None]*self.width*self.height
# User cell input
self.user_cell = False
self.user_row = False
self.user_column = False
self.user_reveal = []
'''
{user_reveal} is changed by
- {game_create()}: reset user_reveal
- {user_input()}: append input cell, cannot reveal all adjacent 0 here (first turn - table not yet generated)
- {end_game()}: append all mines
'''
'''
{*_user_*},{user_cell} = {[column][row]}, index is 1 more than {*_cell_*} index
{*_cell_*} = {[row][column]}, index is 1 less than {*_user_*} index
'''

def game_create(self):
print(f'Default size is {self.width}*{self.height}, {self.mine_numbers} mines')
default_size = input('Play default size?(Y/N): ')
if default_size.lower() == 'n':
correct_input = False
while not correct_input:
try:
self.width = int(input('Enter width: '))
self.height = int(input('Enter height: '))
self.mine_numbers = int(input('Enter number of mines: '))
if self.mine_numbers >= self.width*self.height or self.mine_numbers == 0:
print('ERROR: Number of mines can not be 0 or equal/exceed table size')
elif self.width > 99 or self.height > 99:
print('ERROR: Maximum table size is 99*99')
else:
self.table = [None]*self.width*self.height
self.user_reveal = []
correct_input = True
return self.width,self.height,self.mine_numbers,self.table,self.user_reveal
except ValueError:
print('ERROR: Try again, number only')
else:
self.table = [None]*self.width*self.height
self.user_reveal = []
return self.width,self.height,self.mine_numbers,self.table,self.user_reveal

def user_input(self):
correct_input = False
while not correct_input:
try:
self.user_cell = input('Enter {[column][row]} in 4 digits eg. 0105: ')
int(self.user_cell)
if len(self.user_cell) != 4:
print('ERROR: Only 4 digits allowed')
elif int(self.user_cell[2:]) > self.height or self.user_cell[2:] == '00':
print('ERROR: Row out of range')
elif int(self.user_cell[:2]) > self.width or self.user_cell[:2] == '00':
print('ERROR: Column of range')
elif self.user_cell in self.user_reveal:
else:
correct_input = True
except ValueError:
print('ERROR: Try again, number only')

self.user_row = int(self.user_cell[2:])
self.user_column = int(self.user_cell[:2])
if self.user_cell:
self.user_reveal.append(self.user_cell)
return self.user_cell,self.user_row,self.user_column

def mines_generator(self):
# Exclude first cell from mines generator
user_location = ((self.user_row-1)*self.width)+self.user_column-1
possible_location = [i for i in range(self.width*self.height) if i != user_location]
mines_location = random.sample(possible_location,self.mine_numbers)

# Assign 'Location with mine' with 9
for i in mines_location:
self.table[i] = 9
return self.table

def two_dimension_array(self):
# Save table into 2D array
for i in range(self.height):
self.table[i] = self.table[0+(self.width*i):self.width+(self.width*i)]

# Remove unnessessary elements
del self.table[self.height:]
return self.table

def complete_table(self):
# Create temporary 2D array
temporary_table = [[None for _ in range(self.width)] for _ in range(self.height)]
# For every table[i][j]
for i in range(self.height):
for j in range(self.width):
# If table[i][j] is bomb, continue
if self.table[i][j] == 9:
temporary_table[i][j] = 9
continue
else:
counter = 0
# For every adjacent neighbor arrays
for k in range(i-1,i+2):
# Error handling: list index out of range
if 0 <= k <= self.height-1:
for l in range(j-1,j+2):
# Error handling: list index out of range
if 0 <= l <= self.width-1:
if self.table[k][l] == 9:
counter += 1
continue
temporary_table[i][j] = counter
self.table = temporary_table
return self.table

# If value is 0
if self.table[int(zero_cell[2:])-1][int(zero_cell[:2])-1] == 0:
# For all neighbor elements
for i in range(int(zero_cell[2:])-1-1,int(zero_cell[2:])-1+2):
# Error handling: index out of range
if 0 <= i < self.height:
for j in range(int(zero_cell[:2])-1-1,int(zero_cell[:2])-1+2):
if 0 <= j < self.width:
# If neighbor element of 0 is not yet append, append all adjacent element
if str(j+1).zfill(2)+str(i+1).zfill(2) not in self.user_reveal:
self.user_reveal.append(str(j+1).zfill(2)+str(i+1).zfill(2))
# If neighbor is also 0, do a recursion
if self.table[i][j] == 0:

def first_turn(self):
self.user_input()
self.mines_generator()
self.two_dimension_array()
self.complete_table()

def print_table(self):
# Clear UI
print('\n'*10)
for row in range(self.height+1):
cell = '|'
for column in range(self.width+1):
# Top-row label
if row == 0:
cell += f'{column:2}|' # (Note: try 02 instead of 2)
continue
# First column label
elif column == 0:
cell += f'{row:2}|'
continue
# Revealed cell
elif str(column).zfill(2)+str(row).zfill(2) in self.user_reveal:
cell += f'{self.table[row-1][column-1]:2}|'
continue
# Not yet revealed cell
else:
cell += '{:>3}'.format('|')
print(cell)

def end_game(self):

# If end: reveal all mines, nested function
def reveal_mine():
for i,j in enumerate(self.table):
for k,l in enumerate(j):
if l == 9:
self.table[i][k] = ‘XX’
if str(k+1).zfill(2)+str(i+1).zfill(2) not in self.user_reveal:
self.table[i][k] = ‘**’
self.user_reveal.append(str(k+1).zfill(2)+str(i+1).zfill(2))

# If user choose cell: check if end
if self.user_cell:
if self.table[self.user_row-1][self.user_column-1] == 9:
end_game = True
reveal_mine()
self.print_table()
print('YOU LOSE!')
elif len(self.user_reveal) == (self.width*self.height)-self.mine_numbers:
end_game = True
reveal_mine()
self.print_table()
print('YOU WIN!')
else:
end_game = False
# If no cell selected: end = False
else:
end_game = False

return end_game

def restart_game(self):
restart = input('Restart?(Y/N): ')
if restart.lower() == 'y':
return True
else:
return False

def main():

minesweeper = Minesweeper()

while True:
minesweeper.game_create()
minesweeper.print_table()
minesweeper.first_turn()

while not minesweeper.end_game():
minesweeper.print_table()
minesweeper.user_input()

if not minesweeper.restart_game():
break

if __name__ == '__main__':
main()


• I don't have time for a full review, but one thing I notice is that you're using strings as comments. If you have a comment (something to help explain why the code is what it is) use a # character to start it. The weird """ sections in Python are intended for docstrings, which should only exist once at the start of the function and tell the function what it is for and how to use it. They are not comments and importantly, are not ignored by python. – Josiah Dec 28 '19 at 10:03
• @Josiah Thank you. I will leave it like this to remind me. And since no one reply to my post, it would be great if you could give me a full review (whenever is ok) :) – uchaosis Dec 29 '19 at 10:45

I think implementing a game is a good way to learn a new programming language. If you are up for it, you could try coding a graphical version of Minesweeper using a game library such as Pygame or Arcade to learn even more about the language.

Below are my suggestions for improvements, in no particular order:

When I review code, I read the code and the comments in tandem. The comments should help me understand what the code does. But for this code I don't think they do, because they are on a quite low level. Comments that tells me essentially the same thing as the code itself aren't very interesting. What the comments instead should communicate is the intent of the code. The why not the how.

I suggest instead of putting comments on individual lines, delete them all and replace them with a big comment on top of the module where you describe the architecture of the Minesweeper class:

import random

# Minesweeper
# -----------
# Minesweeper is a solitaire game in which the goal is to reveal all
# mines on a board. The Minesweeper class implements the game. It has
# the following attributes:
#
# * width: width of board
# ...
# * table: board representation
#
# This example shows how the class should be used:
#
#     ms = Minesweeper()
#     while True:
#         ms.game_create()
#         ...


It gives the reader an overview of what the code is all about.

### Yes/no prompts

A convention expressed in this Stackoverflow answer is to let the default choice in a yes/no prompt be capitalized. So if the prompt is

Play default size? [Y/n]:


the user knows that by just pressing return, the default size is choosen. Yes, I know it is a "small detail" but to make great software you have to consider these little things.

### Integer prompt function

If the user does not want to use the default size, he or she is prompted for the width, height and the number of mines. Consider making a function encapsulating the prompting code:

def prompt_int(name, lo, hi):
while True:
s = input(f'Enter {name} [{lo}..{hi}]: ')
try:
v = int(s)
except ValueError:
print('ERROR: Try again, number only')
continue
if not (lo <= v <= hi):
print('ERROR: Allowed range %d..%d' % (lo, hi))
continue
return v


### Unused return values

The return value of the game_create, user_input and mines_generator methods are unused.

### Only initialize attributes in one place

The attributes table, width, height and mine_numbers are initialized both in the constructor and in the game_create method. Better to put the game_create code in the constructor so that the initialization only happens once. After all, it is a constructor so it makes sense that the game is created in it.

### Avoid magic numbers

It looks like if a cell contains 9 it is a mine. It is better to avoid magic numbers and to use constants instead. Declare

class Minesweeper:
CLEAR = 0
MINE = 9


and then to check if a cell contains a mine write

self.table[i][j] == Minesweeper.MINE


### Board representation

It is unclear to me why the board representation (table) is initialized as a one-dimensional array and then changed to a two-dimensional one. Why not make it two-dimensional from the start?

### Counting mines with min and max

Use min and max to count mines in the following way:

def complete_table(self):
width, height, table = self.width, self.height, self.table
for i in range(height):
for j in range(width):
if table[i][j] == Minesweeper.MINE:
continue
table[i][j] = 0
for i2 in range(max(0, i - 1), min(i + 2, height)):
for j2 in range(max(0, j -1), min(j + 2, width)):
if table[i2][j2] == Minesweeper.MINE:
table[i][j] += 1


This usage of min and max for bounds-checking is a common pattern. The first line is just to make the code shorter - so that self doesn't have to be repeated everywhere.

### Perfer storing data in a machine friendly format

If some input has to be converted in some way to become usable by the computer, then it should be stored in the converted format. That way, the conversion doesn't have to be repeated each time the data is used.

For example, user_column and user_row are inputted using 1-based indexing. Which is fine because that makes sense for humans. But for Python, 0-based indexing is more convenient so the values should be converted as soon as they are read from the user. The conversion simply means subtracting 1 from them.

The same goes for the user_reveal attribute. It stores a sequence like this:

['0101', '0201', '0404']


but it should be stored in a Python-friendly format, like this:

[(0, 0), (1, 0), (3, 3)]


### Don't store data twice

user_row and user_column are after the first turn the same as the last element of the user_reveal list. This allows us to remove the user_row and user_column attributes and instead get the same data by referring to user_reveal[-1].

### Don't delay returns

In end_game there is the following:

if ...:
end_game = True
...
elif ...:
end_game = True
...
else:
end_game = False


It is better to express it as follows

if ...:
...
return True
if ...:
...
return True
return False


because it makes the code flow "straighter."

### Avoid nested functions

Nested functions have their uses, but they also makes the code harder to follow. IMO, they should be avoided.

### Names

Last but not least, the names of many objects can be improved. For methods and functions, it is preferable to include a verb to make the name "active." Here are the renames I suggest:

• first_turn => run_first_turn
• print_table => print_minefield
• mines_generator => place_mines
• mine_numbers => mine_count
• user_input => read_location
• end_game => is_game_over
• user_reveal => revealed_locations (for collection types, you want the name to end with an S)
• adjacent_zero => reveal_safe_locations
• reveal_mine => reveal_all_mines
• complete_table => place_mine_counts
• table => minefield

### Final code

import itertools
import random

# Minesweeper
# -----------
# Minesweeper is a solitaire game in which the goal is to reveal all
# mines on a board. The Minesweeper class implements the game. It has
# the following attributes:
#
# * width: width of board
# ...
# * minefield: board representation
#
# This example shows how the class should be used:
#
#     ms = Minesweeper()
#     while True:
#         ms.game_create()
#         ...

def prompt_int(name, lo, hi):
while True:
s = input(f'Enter {name} [{lo}..{hi}]: ')
try:
v = int(s)
except ValueError:
print('ERROR: Try again, number only')
continue
if not (lo <= v <= hi):
print('ERROR: Allowed range %d..%d' % (lo, hi))
continue
return v

class Minesweeper:
CLEAR = 0
MINE = 9
def __init__(self, width = 9, height = 10, mine_count = 12):
self.revealed_locations = []
print(f'Default size is {width}*{height}, {mine_count} mines')
default_size = input('Play default size? [Y/n]: ')
if default_size.lower() == 'n':
self.width = prompt_int('width', 0, 99)
self.height = prompt_int('height', 0, 99)
self.mine_count = prompt_int('number of mines',
0, self.width * self.height - 1)
else:
self.width = width
self.height = height
self.mine_count = mine_count
self.minefield = [[Minesweeper.CLEAR] * self.width
for _ in range(self.height)]

while True:
s = input('Enter {[column][row]} in 4 digits eg. 0105: ')
if len(s) != 4:
print('ERROR: Only 4 digits allowed')
continue
try:
row = int(s[2:]) - 1
column = int(s[:2]) - 1
except ValueError:
print('ERROR: Try again, number only')
continue
if not (0 <= row < self.height):
print('ERROR: Row out of range')
elif not (0 <= column < self.width):
print('ERROR: Column of range')
elif (row, column) in self.revealed_locations:
else:
break
self.revealed_locations.append((row, column))

def place_mines(self):
locs = set(itertools.product(range(self.height), range(self.width)))
locs -= {self.revealed_locations[-1]}
locs = random.sample(locs, self.mine_count)
for row, column in locs:
self.minefield[row][column] = Minesweeper.MINE

def place_mine_counts(self):
width, height, minefield = self.width, self.height, self.minefield
for i in range(height):
for j in range(width):
if minefield[i][j] == Minesweeper.MINE:
continue
minefield[i][j] = Minesweeper.CLEAR
for i2 in range(max(0, i - 1), min(i + 2, height)):
for j2 in range(max(0, j -1), min(j + 2, width)):
if minefield[i2][j2] == Minesweeper.MINE:
minefield[i][j] += 1

def reveal_safe_locations(self, row, column):
width, height, minefield = self.width, self.height, self.minefield
if minefield[row][column] == Minesweeper.CLEAR:
for i in range(max(0, row - 1), min(row + 2, height)):
for j in range(max(0, column - 1), min(column + 2, width)):
if (i, j) not in self.revealed_locations:
self.revealed_locations.append((i, j))
if minefield[i][j] == Minesweeper.CLEAR:
self.reveal_safe_locations(i, j)

def run_first_turn(self):
self.place_mines()
self.place_mine_counts()
row, column = self.revealed_locations[-1]
self.reveal_safe_locations(row, column)

def print_minefield(self):
print('\n'*10)
for row in range(self.height + 1):
cell = '|'
for column in range(self.width + 1):
if row == 0 and column == 0:
cell += ' .|'
elif row == 0:
cell += f'{column:2}|'
elif column == 0:
cell += f'{row:2}|'
elif (row - 1, column - 1) in self.revealed_locations:
cell += f'{self.minefield[row-1][column-1]:2}|'
else:
cell += '{:>3}'.format('|')
print(cell)

def reveal_all_mine(self):
for i in range(self.height):
for j in range(self.width):
if self.minefield[i][j] == Minesweeper.MINE:
self.minefield[i][j] = 'XX'
if (i, j) not in self.revealed_locations:
self.minefield[i][j] = '**'
self.revealed_locations.append((i, j))

def is_game_over(self):
row, column = self.revealed_locations[-1]
if self.minefield[row][column] == Minesweeper.MINE:
self.reveal_all_mines()
self.print_minefield()
print('YOU LOSE!')
return True
unmined_locations_count = self.width * self.height - self.mine_count
if len(self.revealed_locations) == unmined_locations_count:
self.reveal_all_mines()
self.print_minefield()
print('YOU WIN!')
return True
return False

def restart_game(self):
restart = input('Restart? [y/N]: ')
return restart.lower() == 'y'

def main():
while True:
ms = Minesweeper()
ms.print_minefield()
ms.run_first_turn()
while not ms.is_game_over():
ms.print_minefield()

• IMO, the module docstring should be above the imports and I'd also use """ Module docstring here. """ instead of multiple #s. – Grajdeanu Alex. Jan 2 at 16:21