23
\$\begingroup\$

I'm new to Python (and generally to programming), and I have recently written this Minesweeper game:

import random
import string
import os
class Square(object):
 """Represent a square in the cell.
 mine -- if the square has a mine, it's True. Otherwise, False.
 location -- a tuple that represents the (x, y) of the square in the grid.
 grid -- the gird that has the square in it
 """
 signs = ('*', ' ') + tuple([str(n) for n in range(1,9)])
 # reserved signs - the player can't mark a square with those signs
 # (signs of visible squares - it doesn't contain '.' because when the
 # player mark the square with '.', he cancels the previous sign)
 def __init__(self, mine, location, grid):
 self.mine = mine
 self.location = tuple(location)
 self.grid = grid
 self.sign = '.' # sign - how is the square represented to the user.
 self.visible = False # not visible yet
 assert self.legal_square()
 def __str__(self):
 return self.sign
 def expose(self):
 '''Make the square visible to the player (when he exposes it, for
 example).'''
 self.visible = True
 if self.has_mine():
 self.sign = '*' # The sign that means that the square has a mine
 # and the player exposed it (he lost).
 elif self.num() == 0: # There are no mines near the square
 self.sign = ' ' # The sign that means that the square is clean,
 # and there are no mines near it.
 x, y = self.location[0], self.location[1]
 # Expose all of the squares near the current square, and all of
 # the squares near them (recursively), until we reach squares
 # with numbers.
 for near_x in range(x-1, x+2):
 for near_y in range(y-1, y+2):
 if not (near_x == x and near_y == y): # not the same square
 self.grid.expose_square((near_x, near_y))
 # If there's no square with these coordinates, this
 # method returns so there's no any error.
 else: # If the square has no any mine, but there are mines near it.
 self.sign = str(self.num())
 def is_visible(self):
 '''Return True if the square is visible, False otherwise.'''
 return self.visible
 def has_mine(self):
 '''Return True if the square has a mine, False otherwise.'''
 return self.mine
 def num(self):
 '''Return the number of mines near the square.'''
 mines = 0
 x, y = self.location[0], self.location[1] # square's coordinates
 # every square near the current square
 for near_x in range(x-1, x+2):
 for near_y in range(y-1, y+2):
 if not (near_x == x and near_y == y): # not the same square
 if self.grid.has_mine((near_x, near_y)): mines += 1
 # If that square has a mine 
 return mines
 def mark(self, sign):
 '''Mark the square with the given sign.
 sign -- string
 '''
 if not self.is_visible() and sign not in Square.signs:
 # The player can only mark invisible squares, with unreserved marks
 self.sign = sign
 def legal_square(self):
 '''Return True if the square is legal, False otherwise.'''
 if not isinstance(self.location, tuple) or len(self.location) != 2:
 return False
 if not isinstance(self.location[0], int)\
 or not isinstance(self.location[1], int):
 return False
 if not isinstance(self.has_mine(), bool):
 return False
 if self.location[0] < 0 or self.location[1] < 0:
 return False
 if not isinstance(self.grid, Grid):
 return False
 if self.visible and (self.sign not in Square.signs):
 return False
 return True
class Grid(object):
 """Represent the grid of the Squares.
 width, height -- grid's dimensions
 mines -- number of mines in the grid
 """
 def __init__(self, width=8, height=8, mines=10):
 self.width, self.height = width, height
 self.mines = mines
 self.create_grid(dummy=True) # Create a dummy grid (before the first
 # turn)
 assert self.legal_grid()
 def __str__(self):
 return_s = ''
 width_col_header = len(to_base26(self.height))
 width_col = len(str(self.width))
 # Header row of the table
 return_s += (width_col_header+1) * ' '
 for col in range(len(self.grid[0])):
 return_s += ' {0:^{1}}'.format(col+1, width_col)
 return_s += '\n'
 # Border between header row and the rest of the table
 return_s += (width_col_header+1) * ' '
 for col in range(len(self.grid[0])):
 return_s += ' {0:^{1}}'.format('-', width_col)
 return_s += '\n'
 # The columns with the squares
 for row in range(len(self.grid)):
 return_s += '{0:>{1}}|'.format(to_base26(row+1), width_col_header)
 for square in self.grid[row]:
 return_s += ' {0:^{1}}'.format(square, width_col)
 return_s += '\n'
 return return_s
 def create_grid(self, dummy, safe_square=None):
 '''Create a grid (that consists of Squares).
 dummy -- In the first turn, we don't want that the player will lose.
 If there are still no mines, we will call the grid "dummy".
 Set dummy to True if now is before the first turn, and False
 otherwise.
 safe_square -- If dummy == True, don't assign a value to it. This is
 a tuple that represents the x and y of the square that
 the player chose. This square (and its adjacent squares)
 should be clean of mines, because we don't want the
 player will lose right after the first turn, but if now
 is before the first turn, don't assign a value to it.
 It's None as a default.
 '''
 safe_squares = [] # squares around the safe_square (including
 # safe_square)
 if not dummy:
 # Initialize safe_squares
 for x in range(safe_square[0]-1, safe_square[0]+2):
 for y in range(safe_square[1]-1, safe_square[1]+2):
 safe_squares.append((x, y))
 mines = [] # this list will represent the random locations of mines.
 # Initialize mines
 if not dummy:
 self.is_dummy = False
 # Set the random locations of mines.
 for mine in range(self.mines): # Until we reach the wanted number
 # of mines
 while True:
 x, y = random.randint(0, self.width-1),\
 random.randint(0, self.height-1)
 # Random coordinates on the grid
 if (x, y) not in mines and (x, y) not in safe_squares:
 # If this is a new location, and not on the safe_square
 break
 mines.append((x, y))
 else:
 self.is_dummy = True
 grid = [] # a list of rows of Squares
 for y in range(self.height):
 row = []
 for x in range(self.width):
 square = Square(((x, y) in mines), (x, y), self)
 # (x, y) in mines -- if (x, y) was chosen as a mine, it's True
 row.append(square)
 grid.append(row[:])
 self.grid = grid
 def has_mine(self, location):
 '''Return True if the square in the given location has a mine, False
 otherwise.
 location -- a tuple (x, y)
 '''
 y, x = location[1], location[0] # coordinates of the square
 if x < 0 or x >= self.width or y < 0 or y >= self.height:
 # If the square doesn't exist, just return False
 return False
 return self.grid[y][x].has_mine()
 def parse_input(self):
 '''Get a location from the player and a mark optionally, and return
 them as a tuple. If the player didn't insert a mark, its value in the
 tuple will be None.'''
 while True: # The only exit is from an exit statement - be careful
 s = raw_input('Type the location of the square you want to '
 'treat. If you want to mark the square, type '
 'after the location a space, and then the sign '
 'you want to use. If you want to cancel a mark, '
 '"mark" the square with a dot.\n')
 location = s.split()[0]
 try:
 sign = s.split()[1]
 except IndexError:
 sign = None
 letters = ''.join(c for c in location if c in string.letters)
 digits = ''.join((str(c) for c in location if c in string.digits))
 if (letters + digits != location and digits + letters != location)\
 or letters == '' or digits == '':
 # If the input is something like "A2B3" or "AA" or "34"
 print 'Please type an invalid location, like "AA12" or "12AA"'
 continue
 location = (int(digits)-1, base26to_num(letters)-1)
 try:
 x, y = location[0], location[1]
 self.grid[y][x]
 except IndexError:
 print 'Ahhh... The square should be IN the grid.\n'
 continue
 break
 return (location, sign)
 def turn(self):
 '''Let the player play a turn. Return 1 if the player won, -1 if he
 lost, and 0 if he can keep playing.'''
 os.system('cls' if os.name == 'nt' else 'clear') # clear screen
 print self
 location, sign = self.parse_input()
 x, y = location
 square = self.grid[y][x] # The square that the player wanted to treat
 if sign == None: # If the player didn't want to mark the square
 if self.is_dummy:
 self.create_grid(False, (x, y))
 self.expose_square((x, y)) # then he wanted to expose it
 if square.has_mine(): # If the square has a mine, the player lost
 self.lose()
 return (-1) # lose
 # Check if the player won (if he exposed all of the clean squares)
 for row in self.grid:
 for square in row:
 if not square.has_mine() and not square.is_visible():
 # If there are clean squares that are not exposed
 return 0 # not win, not lose
 else: # all of the clean squares are exposed
 self.win()
 return 1 # win
 else: # If the player wanted to mark a square
 square.mark(sign[0])
 return 0
 def play(self):
 '''Let the player play, until he finishes. Return True if he won, and
 False if he lost.'''
 result = 0
 while result == 0:
 result = self.turn()
 return result == 1
 def expose_square(self, location):
 '''Choose the square in the given location and expose it.'''
 y, x = location[1], location[0]
 if x < 0 or x >= self.width or y < 0 or y >= self.height:
 # if the square doesn't exist, do nothing
 return
 if self.grid[y][x].is_visible():
 # if the square is already exposed, do nothing
 return
 if str(self.grid[y][x]) != '.': # the player marked the square
 return
 if self.is_dummy: # if it's the first turn, and there still aren't real
 # mines,
 self.create_grid((x, y), False) # create a real grid
 square = self.grid[y][x] # the square of the given location
 square.expose()
 def lose(self):
 '''Expose all of the squares that have mines, and print 'Game over'.'''
 os.system('cls' if os.name == 'nt' else 'clear') # clear screen
 # Expose all of the squares that have mines
 for x in range(self.width):
 for y in range(self.height):
 self.expose_square((x, y))
 print self # Print the grid with the mines
 print 'Game over.'
 def win(self):
 '''Print the grid and 'You won!'.'''
 os.system('cls' if os.name == 'nt' else 'clear') # clear screen
 print self
 print 'You won!'
 def legal_grid(self):
 if not isinstance(self.width, int) or not isinstance(self.height, int):
 return False
 if self.width <= 0 or self.height <= 0 or self.mines < 0:
 return False
 if not isinstance(self.mines, int):
 return False
 if self.mines > self.width*self.height-9:
 return False
 return True
def to_base26(num):
 '''Convert a number to a string, that represents the number in base-26,
 without a zero.
 to_base26(1) => 'A'
 to_base26(2) => 'B'
 to_base26(28) => 'AB'
 num: number
 Return: str
 '''
 s = ''
 while num > 0:
 num -= 1
 s = chr(ord('A')+num%26) + s
 num //= 26
 return s
def base26to_num(s):
 '''Convert a string that represents a number in base-26 (1 = 'A',
 28 = 'AB', etc.) to a number.
 base26to_num('A') => 1
 base26to_num('b') => 2
 base26to_num('AB') => 28
 s: str
 Return: number
 '''
 s = ''.join(c for c in s if c in string.letters).upper()
 num = 0
 for letter in s:
 num = num*26 + (ord(letter)-ord('A')) + 1
 return num
grid = Grid(9, 9, 10)
grid.play()
raw_input()

The code works well as far as I know, but I'm not sure if it follows good practices, or how readable and efficient is it.

Simon Forsberg
59.7k9 gold badges157 silver badges311 bronze badges
asked Jul 25, 2014 at 12:20
\$\endgroup\$
1
  • \$\begingroup\$ Wheres the part that creates the x and allows the user to input the position if the x? \$\endgroup\$ Commented Nov 10, 2016 at 11:20

1 Answer 1

8
\$\begingroup\$

Purely from an OOP standpoint, I think that your classes don't make much sense.

Square should just be a square, it shouldn't modify any other square (as you do so in your expose function). In fact, a square doesn't even need to know it's own location (as its container should handle that) or its container. I would have a bare-bones square class with only its contents and an expose method that exposes itself and nothing else.

Grid is doing too much. It is simultaneously doing the job of a grid (working with squares) while also incorporating most of the game logic. I would move most of the game logic to another class (described below) and move the expose square logic to here. Also I think adding a tuple of tuples of Squares for the grid data structure would make sense.

Add a MineSweeper class that contains most of the actual game logic (input, redraw and etc) that is currently in Grid.

In general, your code should try to follow the single responsibility principle. That is, make each of your classes/functions responsible for one thing and one thing only. Note: this means creating functions for things like iterating over the neighbors and etc.


On the programming side:

There is no reason to generate a dummy grid and then generate a real grid. Create the real grid, and then if the first click is on a mine, move the mine somewhere else.

Your code may break if the number of mines is especially high. This is because you keep trying to generate random locations that haven't been used up in a while true: loop, which will keep your code running until the end of time if there are no spots left. I would take a look at some of the other mine generation algorithms that have been used over the years. Alternatively, you can chop your grid up into mines - 10 equal pieces, choose a random position within those sub-grids to add a mine, and finally distributing the last 10 randomly amongst the sub-grids. Take care to make sure that you aren't adding too many mines.

answered Jul 25, 2014 at 18:44
\$\endgroup\$
2
  • \$\begingroup\$ Thanks a lot, that will make more sense. (Pay attention that the number of mines can't be greater than the number of squares because of the method legal_grid) \$\endgroup\$ Commented Jul 26, 2014 at 8:45
  • \$\begingroup\$ Ah, didn't catch that, edited my answer to change that. \$\endgroup\$ Commented Jul 26, 2014 at 17:10

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.