Advertisement
SafariMonkey

engine.py

Apr 21st, 2015
69
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.89 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. import globals
  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 range(arr.ncols))
  27.            for r in range(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):
  103.         assert nrows > 1 and ncols > 1
  104.         self.__nrows, self.__ncols = nrows, ncols
  105.         self.__nmines = nmines
  106.         self.__mines = None
  107.         self.__visible = Array2D(nrows, ncols, False)
  108.  
  109.     def nRows(self):
  110.         """
  111.        Returns the number of rows in the grid of mines for this game.
  112.        """
  113.         return self.__nrows
  114.  
  115.     def nCols(self):
  116.         """
  117.        Returns the number of columns in the grid of mines for this game.
  118.        """
  119.         return self.__ncols
  120.  
  121.     def nMines(self):
  122.         """
  123.        Returns the total number of mines in this game.
  124.        """
  125.         return self.__nmines
  126.  
  127.     def guess(self, r, c):
  128.         """
  129.        Call this to make a guess that the square at r,c does not contain a
  130.        mine.  If the guess is correct and there is no mine in the square,
  131.        the square is set to visible.  If there is a mine there then a
  132.        LostGameError is thrown.
  133.        """
  134.         if (self.__mines is not None) and self.__mines[r,c]:
  135.             print(self.__str__() + " lost at " + str(r+1) + " " + str(c+1))
  136.             raise LostGameError("you exploded!!!")
  137.         else:
  138.             self.__visible[r,c] = True
  139.             if self.__mines is None:
  140.                 self.__initMines(r, c)
  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 range(self.nRows()):
  233.             for c in range(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 iterExterior(self):
  275.         """
  276.        Iterates over the (row,column) tuples for all the squares in the grid
  277.        for which isInterior(row,column) returns True.
  278.        """
  279.         for r,c in self.iterHidden():
  280.             if not self.isInterior(r, c):
  281.                 yield (r,c)
  282.  
  283.     def isInBounds(self, r, c):
  284.         """
  285.        Returns True if the given square is within the game grid, and False
  286.        otherwise.
  287.        """
  288.         return (r >= 0) and (c >= 0) and (r < self.nRows()) and (c < self.nCols())
  289.  
  290.     def copy(self):
  291.         """
  292.        This is provided purely as a convienence function to help you in
  293.        writing an AI to play the game.  This method returns a copy of the
  294.        complete game state, so any modifications made to the copy with
  295.        leave the original unchanged.
  296.        """
  297.         grid = Minefield(self.nRows(), self.nCols(), self.nMines())
  298.         if self.__mines is not None:
  299.             grid.__mines = self.__mines.copy()
  300.         grid.__visible = self.__visible.copy()
  301.         return grid
  302.  
  303.     def __str__(self):
  304.         """
  305.        Returns a string showing the state of the Minesweeper game in crude
  306.        ASCII art.  Row and column labels are also included.
  307.        """
  308.  
  309.         def cellStr(r, c):
  310.             if self.__mines is None:
  311.                 return "."
  312.             elif self.isVisible(r,c):
  313.                 n = self.nMinesAround(r,c)
  314.                 if n == 0:
  315.                     return ","
  316.                 else:
  317.                     return str(n)
  318.             elif self.__mines[r,c] == True:
  319.                 if globals.flagsdict[r,c]:
  320.                     return "P"
  321.                 else:
  322.                     return ":"
  323.             elif globals.flagsdict[r,c]:
  324.                 return "?"
  325.             else:
  326.                 return "."
  327.  
  328.         nRowDigits = int(math.ceil(math.log(self.nRows(),10)))
  329.         nColDigits = int(math.ceil(math.log(self.nCols(),10)))
  330.  
  331.         colStrs = [str(c+1).rjust(nColDigits) for c in range(self.nCols())]
  332.         colNumbersStr = "\n".join(
  333.             " "*nRowDigits + "  " + " ".join(
  334.                 colStrs[c][i] for c in range(self.nCols()))
  335.             for i in range(nColDigits))
  336.         '''
  337.        return "\n".join(
  338.            str(r+1).rjust(nRowDigits) + "  " + " ".join(
  339.                cellStr(r,c)
  340.                for c in range(self.nCols()))
  341.            for r in range(self.nRows())) + "\n\n" + colNumbersStr
  342.        '''
  343.         return "\n".join(" ".join(cellStr(r,c)for c in range(self.nCols()))for r in range(self.nRows())) + "\n\n"
  344.  
  345.     def __initMines(self, r, c):
  346.         """
  347.        This function is for internal use only, so don't call it.
  348.        """
  349.         self.__mines = Array2D(self.nRows(), self.nCols(), False)
  350.         openCells = list(self.iterInterior())
  351.         random.shuffle(openCells)
  352.         for i in range(self.nMines()):
  353.             r,c = openCells[i]
  354.             self.__mines[r,c] = True
  355.  
  356.  
  357. def testSolver(solverFcn):
  358.     """
  359.    Tests a function for solving minesweeper games and reports its win
  360.    percentage on four different difficulties:
  361.  
  362.    debugging:     9 x 9  grid with  1 mine
  363.    beginner:      9 x 9  grid with 10 mines
  364.    intermediate: 16 x 16 grid with 40 mines
  365.    expert:       16 x 30 grid with 99 mines
  366.  
  367.    The testSolver function takes a single parameter, which is itself a
  368.    function. This function should take as input a Minefield, and either
  369.    return if successfully solves the game, or throws a LostGameError if it
  370.    hits a mine.
  371.    """
  372.  
  373.     nsamples = 1000
  374.     testCases = [
  375.         ("debugging"    , (9 , 9,  1)),
  376.         ("beginner"     , (9 , 9, 10)),
  377.         ("intermediate" , (16,16, 40)),
  378.         ("expert"       , (16,30, 99)),
  379.     ]
  380.  
  381.     testResults = []
  382.     for testName, (nrows, ncols, nmines) in testCases:
  383.         print("testing '{:s}' difficulty".format(testName))
  384.  
  385.         nwins = 0
  386.         for i in range(nsamples):
  387.             grid = Minefield(nrows, ncols, nmines)
  388.             isWin = False
  389.             try:
  390.                 solverFcn(grid)
  391.                 if grid.isSolved():
  392.                     isWin = True
  393.                     nwins += 1
  394.                 else:
  395.                     print("error: the game was not actually solved by the solver function")
  396.                     print("this indicates a bug in the solution method")
  397.                     return
  398.  
  399.             except LostGameError as e:
  400.                 pass
  401.             print("    {:d} / {:d}  ({:s})".format(i+1, nsamples, "win" if isWin else "loss"))
  402.         winProbability = nwins / float(nsamples)
  403.         testResults.append((testName, winProbability))
  404.  
  405.     for testName, winProbability in testResults:
  406.         winPercent = int(100*winProbability)
  407.         print("{:s}: {:d}%".format(testName, winPercent))
  408.  
  409.  
  410. def playGame(nrows, ncols, nmines):
  411.     """
  412.    Plays a game of Minesweeper using a basic text interface.  This is
  413.    intended to illustrate how to use the Minefield class, so it's not a
  414.    particularly user friendly interface.
  415.    """
  416.  
  417.     def intInput(minVal, maxVal):
  418.         while True:
  419.             s = input("> ")
  420.             try:
  421.                 i = int(s)
  422.                 if minVal <= i <= maxVal:
  423.                     return i
  424.             except:
  425.                 pass
  426.             print("please input an integer between {:d} and {:d}".format(minVal, maxVal))
  427.  
  428.     grid = Minefield(nrows, ncols, nmines)
  429.  
  430.     while True:
  431.         print("number of mines: {:d}".format(grid.nMines()))
  432.         print(grid)
  433.         print()
  434.  
  435.         print("enter row of square to select:")
  436.         row = intInput(1, nrows)-1
  437.         print("enter column of square to select:")
  438.         col = intInput(1, ncols)-1
  439.  
  440.         try:
  441.             grid.guess(row,col)
  442.             if grid.isSolved():
  443.                 print("you a winner")
  444.                 return True
  445.         except LostGameError:
  446.             print("BOOM.  you ded")
  447.             return False
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement