Advertisement
Guest User

minesweeper.py

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