Advertisement
Guest User

minesweeper.py

a guest
Apr 22nd, 2015
21
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.26 KB | None | 0 0
  1.  
  2. __all__ = [
  3.     "Array2D",
  4.     "LostGameError",
  5.     "Minefield",
  6.     "testSolver",
  7.     "playGame",
  8. ]
  9.  
  10. import math
  11. import random
  12.  
  13.  
  14. class Array2D(object):
  15.     """
  16.     Represents a 2D rectangular array of objects.  In addition to methods
  17.     to access elements, Array2Ds also have a copy() method which creates
  18.     a shallow copy of the array.
  19.    
  20.     Example:
  21.         arr = Array2D(3,5, ".")
  22.         arr[0,0] = "x"
  23.         arr[1,3] = "o"
  24.         print "\n".join(
  25.             "".join(
  26.                     arr[r,c] for c in xrange(arr.ncols))
  27.             for r in xrange(arr.nrows))
  28.            
  29.     Output:
  30.         x....
  31.         ...o.
  32.         .....
  33.     """
  34.  
  35.     def __init__(self, nrows, ncols, value=None):
  36.         self.nrows, self.ncols = nrows, ncols
  37.         self.__data = [value]*(nrows*ncols)
  38.        
  39.     def __getitem__(self, key):
  40.         if isinstance(key, int):
  41.             return self.__data[key]
  42.         else:
  43.             assert len(key) == 2
  44.             index = self.__rc2ind(key[0], key[1])
  45.             return self.__data[index]
  46.        
  47.     def __setitem__(self, key, value):
  48.         if isinstance(key, int):
  49.             self.__data[key] = value
  50.         else:
  51.             assert len(key) == 2
  52.             index = self.__rc2ind(key[0], key[1])
  53.             self.__data[index] = value
  54.         return self
  55.        
  56.     def copy(self):
  57.         arr = Array2D(self.nrows, self.ncols)
  58.         arr.__data = self.__data[:]
  59.         return arr
  60.  
  61.     def __rc2ind(self, r, c):
  62.         assert r >= 0 and r < self.nrows
  63.         assert c >= 0 and c < self.ncols
  64.         return r + c*self.nrows
  65.  
  66.  
  67. class LostGameError(RuntimeError):
  68.     """
  69.     This is the type of exception thrown when a mine is triggered.
  70.     """
  71.     pass
  72.  
  73.  
  74. class Minefield(object):
  75.     """
  76.     Represents the state of a game of Minesweeper.  The game takes place on a
  77.     rectangular grid of squares with some number of mines randomly distributed
  78.     at unknown locations.  The size of the grid and the total number of mines
  79.     can be retrieved with the methods nRows(), nCols() and nMines().
  80.    
  81.     Each square may be visible or hidden.  Visible squares are squares where
  82.     the user has made a guess and confirmed not to contain a mine.  Hidden
  83.     squares may or may not contain a mine.  To switch a square from hidden
  84.     to vissible, call the method guess(r,c) with the row and column of the
  85.     square.  If the square contains a mine a LostGameError will be thrown,
  86.     otherwise it will be set to visible.  It is guaranteed that the very
  87.     first guess of the game will not be on a mine, and the all of the
  88.     squares orthogonally or diagonally adjacent to this first guess will also
  89.     not contain mines.  Other than this, the mines are distributed randomly.
  90.    
  91.     If a square is visible, the total number of mines in the squares
  92.     orthogonally or diagonally adjacent to it can be retreived with the
  93.     method nMinesAround(r,c), which takes as input the row and column
  94.     of a visible square and returns an int indicating the total number
  95.     of mines surrounding it.
  96.    
  97.     There are a number of utility methods provided to help you in writing
  98.     an AI to play the game.  Info about these methods can be found in
  99.     the a comment within each such method.
  100.     """
  101.  
  102.     def __init__(self, nrows, ncols, nmines, rand=None):
  103.         assert nrows > 1 and ncols > 1
  104.         self.__nrows, self.__ncols = nrows, ncols
  105.         self.__nmines = nmines
  106.         self.__rand = rand
  107.         self.__mines = None
  108.         self.__visible = Array2D(nrows, ncols, False)
  109.  
  110.     def nRows(self):
  111.         """
  112.         Returns the number of rows in the grid of mines for this game.
  113.         """
  114.         return self.__nrows
  115.        
  116.     def nCols(self):
  117.         """
  118.         Returns the number of columns in the grid of mines for this game.
  119.         """
  120.         return self.__ncols
  121.        
  122.     def nMines(self):
  123.         """
  124.         Returns the total number of mines in this game.
  125.         """
  126.         return self.__nmines
  127.  
  128.     def guess(self, r, c):
  129.         """
  130.         Call this to make a guess that the square at r,c does not contain a
  131.         mine.  If the guess is correct and there is no mine in the square,
  132.         the square is set to visible.  If there is a mine there then a
  133.         LostGameError is thrown.
  134.         """
  135.         if (self.__mines is not None) and self.__mines[r,c]:
  136.             raise LostGameError, "you exploded!!!"
  137.         else:
  138.             self.__visible[r,c] = True
  139.             if self.__mines is None:
  140.                 self.__initMines()
  141.  
  142.     def isVisible(self, r, c):
  143.         """
  144.         Returns True if the given square in visible, and false if it is
  145.         hidden.
  146.         """
  147.         return self.__visible[r,c]
  148.  
  149.     def nSquares(self):
  150.         """
  151.         Returns the total number of squares in the grid for this game.
  152.         """
  153.         return self.nRows() * self.nCols()
  154.        
  155.     def nVisible(self):
  156.         """
  157.         Returns the total number of currently visible squares in the grid
  158.         for this game.
  159.         """
  160.         return sum(1 for r,c in self.iterVisible())
  161.        
  162.     def nHidden(self):
  163.         """
  164.         Returns the total number of currently hidden squares in the grid for
  165.         this game.
  166.         """
  167.         return sum(1 for r,c in self.iterHidden())
  168.        
  169.     def isSolved(self):
  170.         """
  171.         Returns True if the game has been successfully solved, and False
  172.         otherwise.
  173.         """
  174.         assert self.nHidden() >= self.nMines()
  175.         return self.nHidden() == self.nMines()
  176.  
  177.     def nMinesAround(self, r, c):
  178.         """
  179.         Returns the total number of mines in all the squares orthogonally or
  180.         diagonally adjacent to the square at (r,c).  The square at (r,c) must
  181.         be visible, and an exception is thrown if it's not.
  182.         """
  183.         if self.__visible[r,c]:
  184.             return sum(1 for r2,c2 in self.iterNeighbors(r,c) if self.__mines[r2,c2])
  185.         else:
  186.             raise RuntimeError, "you can't see how many mines are around a non-visible square"
  187.    
  188.     def nSquaresAround(self, r, c):
  189.         """
  190.         Returns the total number of squares orthogonally or diagonally
  191.         adjacent to the square at (r,c).  This will be 8 for an interior
  192.         square, 5 for an edge square, and 3 for a corner square.
  193.         """
  194.         return sum(1 for r2,c2 in self.iterNeighbors(r,c))
  195.        
  196.     def nVisibleAround(self, r, c):
  197.         """
  198.         Returns the total number of currently visible squares orthogonally or
  199.         diagonally adjacent to the square at (r,c).
  200.         """
  201.         return sum(1 for r2,c2 in self.iterNeighbors(r,c) if self.isVisible(r2,c2))
  202.        
  203.     def nHiddenAround(self, r, c):
  204.         """
  205.         Returns the total number of currently hidden squares orthogonally or
  206.         diagonally adjacent to the square at (r,c).
  207.         """
  208.         return sum(1 for r2,c2 in self.iterNeighbors(r,c) if not self.isVisible(r2,c2))
  209.  
  210.     def iterNeighbors(self, r, c):
  211.         """
  212.         Iterates over (row,column) tuples of all the squares orthogonally or
  213.         diagonally adjacent to the square at (r,c).
  214.         """
  215.         if c > 0:
  216.             if r > 0: yield (r-1, c-1)
  217.             yield (r, c-1)
  218.             if r+1 < self.nRows(): yield (r+1, c-1)
  219.  
  220.         if r > 0: yield (r-1, c)
  221.         if r+1 < self.nRows(): yield (r+1, c)
  222.        
  223.         if c+1 < self.nCols():
  224.             if r > 0: yield (r-1, c+1)
  225.             yield (r, c+1)
  226.             if r+1 < self.nRows(): yield (r+1, c+1)
  227.    
  228.     def iterSquares(self):
  229.         """
  230.         Iterates over the (row,column) tuples of all the squares in the grid.
  231.         """
  232.         for r in xrange(self.nRows()):
  233.             for c in xrange(self.nCols()):
  234.                 yield (r,c)
  235.  
  236.     def iterVisible(self):
  237.         """
  238.         Iterates over the (row,column) tuples of all the currently visible
  239.         squares in the grid.
  240.         """
  241.         for r,c in self.iterSquares():
  242.             if self.isVisible(r,c):
  243.                 yield (r,c)
  244.                
  245.     def iterHidden(self):
  246.         """
  247.         Iterates over the (row,column) tuples of all the currently hidden
  248.         squares in the grid.
  249.         """
  250.         for r,c in self.iterSquares():
  251.             if not self.isVisible(r,c):
  252.                 yield (r,c)
  253.  
  254.     def isInterior(self, r, c):
  255.         """
  256.         Iterates over the (row,column) tuples of all the squares in the grid
  257.         which are both currently hidden, and for which all the orthogonally
  258.         and diagonally adjacent neighbors are currently hidden.
  259.         """
  260.         if self.isVisible(r,c): return False
  261.         for r2,c2 in self.iterNeighbors(r,c):
  262.             if self.isVisible(r2,c2): return False
  263.         return True
  264.    
  265.     def iterInterior(self):
  266.         """
  267.         Iterates over the (row,column) tuples for all the squares in the grid
  268.         for which isInterior(row,column) returns True.
  269.         """
  270.         for r,c in self.iterSquares():
  271.             if self.isInterior(r, c):
  272.                 yield (r,c)
  273.  
  274.     def isInBounds(self, r, c):
  275.         """
  276.         Returns True if the given square is within the game grid, and False
  277.         otherwise.
  278.         """
  279.         return (r >= 0) and (c >= 0) and (r < self.nRows()) and (c < self.nCols())
  280.  
  281.     def copy(self):
  282.         """
  283.         This is provided purely as a convienence function to help you in
  284.         writing an AI to play the game.  This method returns a copy of the
  285.         complete game state, so any modifications made to the copy with
  286.         leave the original unchanged.
  287.         """
  288.         grid = Minefield(self.nRows(), self.nCols(), self.nMines())
  289.         if self.__mines is not None:
  290.             grid.__mines = self.__mines.copy()
  291.         grid.__visible = self.__visible.copy()
  292.         return grid
  293.        
  294.     def __str__(self):
  295.         """
  296.         Returns a string showing the state of the Minesweeper game in crude
  297.         ASCII art.  Row and column labels are also included.
  298.         """
  299.  
  300.         def cellStr(r, c):
  301.             if self.__mines is None:
  302.                 return "."
  303.             elif self.isVisible(r,c):
  304.                 n = self.nMinesAround(r,c)
  305.                 if n == 0:
  306.                     return " "
  307.                 else:
  308.                     return str(n)
  309.             else:
  310.                 return "."
  311.                
  312.         nRowDigits = int(math.ceil(math.log(self.nRows(),10)))
  313.         nColDigits = int(math.ceil(math.log(self.nCols(),10)))
  314.        
  315.         colStrs = [str(c+1).rjust(nColDigits) for c in xrange(self.nCols())]
  316.         colNumbersStr = "\n".join(
  317.             " "*nRowDigits + "  " + " ".join(
  318.                 colStrs[c][i] for c in xrange(self.nCols()))
  319.             for i in xrange(nColDigits))
  320.  
  321.         return "\n".join(
  322.             str(r+1).rjust(nRowDigits) + "  " + " ".join(
  323.                 cellStr(r,c)
  324.                 for c in xrange(self.nCols()))
  325.             for r in xrange(self.nRows())) + "\n\n" + colNumbersStr
  326.  
  327.     def __initMines(self):
  328.         """
  329.         This function is for internal use only, so don't call it.
  330.         """
  331.         self.__mines = Array2D(self.nRows(), self.nCols(), False)
  332.         openCells = list(self.iterInterior())
  333.         if self.__rand is None:
  334.             random.shuffle(openCells)
  335.         else:
  336.             self.__rand.shuffle(openCells)
  337.         for i in xrange(self.nMines()):
  338.             r,c = openCells[i]
  339.             self.__mines[r,c] = True
  340.  
  341.  
  342. def testSolver(solverFcn, nsamples=100, isControlTest=False):
  343.     """
  344.     Tests a function for solving minesweeper games and reports its win
  345.     percentage on four different difficulties:
  346.    
  347.     debugging:     9 x 9  grid with  1 mine
  348.     beginner:      9 x 9  grid with 10 mines
  349.     intermediate: 16 x 16 grid with 40 mines
  350.     expert:       16 x 30 grid with 99 mines
  351.    
  352.     The testSolver function takes a single parameter, which is itself a
  353.     function. This function should take as input a Minefield, and either
  354.     return if successfully solves the game, or throws a LostGameError if it
  355.     hits a mine.
  356.     """
  357.  
  358.     testCases = [
  359.         ("debugging"    , (9 , 9,  1)),
  360.         ("beginner"     , (9 , 9, 10)),
  361.         ("intermediate" , (16,16, 40)),
  362.         ("expert"       , (16,30, 99)),
  363.     ]
  364.  
  365.     if isControlTest:
  366.         rand = random.Random(34559)
  367.     else:
  368.         rand = None
  369.  
  370.     testResults = []
  371.     for testName, (nrows, ncols, nmines) in testCases:
  372.         print "testing '{:s}' difficulty".format(testName)
  373.  
  374.         nwins = 0
  375.         for i in xrange(nsamples):
  376.             grid = Minefield(nrows, ncols, nmines, rand=rand)
  377.             isWin = False
  378.             try:
  379.                 if isControlTest:
  380.                     random.seed(7669)
  381.                 solverFcn(grid)
  382.                 if grid.isSolved():
  383.                     isWin = True
  384.                     nwins += 1
  385.                 else:
  386.                     print "error: the game was not actually solved by the solver function"
  387.                     print "this indicates a bug in the solution method"
  388.                     return
  389.  
  390.             except LostGameError as e:
  391.                 pass
  392.             print "    {:d} / {:d}  ({:s})".format(i+1, nsamples, "win" if isWin else "loss")
  393.         winProbability = nwins / float(nsamples)
  394.         testResults.append((testName, winProbability))
  395.  
  396.     for testName, winProbability in testResults:
  397.         winPercent = int(100*winProbability)
  398.         print "{:s}: {:d}%".format(testName, winPercent)
  399.  
  400.  
  401. def playGame(nrows, ncols, nmines):
  402.     """
  403.     Plays a game of Minesweeper using a basic text interface.  This is
  404.     intended to illustrate how to use the Minefield class, so it's not a
  405.     particularly user friendly interface.
  406.     """
  407.  
  408.     def intInput(minVal, maxVal):
  409.         while True:
  410.             s = raw_input("> ")
  411.             try:
  412.                 i = int(s)
  413.                 if minVal <= i <= maxVal:
  414.                     return i
  415.             except:
  416.                 pass
  417.             print "please input an integer between {:d} and {:d}".format(minVal, maxVal)
  418.  
  419.     grid = Minefield(nrows, ncols, nmines)
  420.    
  421.     while True:
  422.         print "number of mines: {:d}".format(grid.nMines())
  423.         print grid
  424.         print
  425.  
  426.         print "enter row of square to select:"
  427.         row = intInput(1, nrows)-1
  428.         print "enter column of square to select:"
  429.         col = intInput(1, ncols)-1
  430.  
  431.         try:
  432.             grid.guess(row,col)
  433.             if grid.isSolved():
  434.                 print "you a winner"
  435.                 return True
  436.         except LostGameError:
  437.             print "BOOM.  you ded"
  438.             return False
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement