.. _minesweeper: ================= Thornsweeper ================= Information about the `Thornsweeper` game can be found at http://en.wikipedia.org/wiki/Minesweeper_(computer_game) .. figure:: thornsweeper.png :alt: thornsweeper Figure 1: Thornsweeper Outline ========= * Create the cells * After the first click place thorns in random locations * Assign number of adjacent thorns to each cell * React to user clicks * Thorn * Game over you lost * Show the placement of other thorns * Mark this cell so that user can see the mistake * No thorn * Adjacent thorns * Display the number of adjacent thorns * No adjacent thorns * Display the connected zeros * Game over you won * Let the user mark the thorns by right clicking on them * Create a button to restart the game Creating the cells ================== .. code-block:: python def createGrid(gridSize): rectangle(x=8.24776481523, y=4.94865888914, width=2.29910592609, height=2.29910592609, name='rectangle1', alias='r1', fillColor=color.grey) return Grid(obj=rectangle1, rows=gridSize, cols=gridSize, pos=rectangle1.pos) To test the `createGrid` use the Python interpreter as shown below. .. code-block:: python >>> createGrid(10) .. figure:: grid.png :alt: grid Figure 1: Grid Placing thorns when user makes the first move ================================================== .. code-block:: python def placeThorns(grid, clickedCell, nThorns, gridSize): for cell in grid.cells: cell.thorn = False choices = range(gridSize) thorns = 0 while thorns < nThorns: cell = grid.getCell(random.choice(choices), random.choice(choices)) if cell == clickedCell: continue if not cell.thorn: cell.thorn = True thorns += 1 To test the `placeThorns` use the Python interpreter as shown below. .. code-block:: python >>> gridSize = 10 >>> nThorns = 15 >>> grid = createGrid(gridSize) >>> placeThorns(grid, grid.getCell(0, 0), nThorns, gridSize) >>> for cell in grid.cells: ... if cell.thorn: ... cell.label = 'X' ... .. figure:: thorns.png :alt: thorns Figure 2: Randomly placed thorns Assign number of adjacent thorns to each cell ================================================ .. code-block:: python def assignThornNumbers(grid): for cell in grid.cells: cell.nThorns = len([x for x in cell.allNeighbors if x.thorn]) To test the `assignThornNumbers` function we will create a function called `revealAll` to display the locations of the thorns as well as the number of thorns in the neighboring cells. .. code-block:: python def revealAll(grid): for cell in grid.cells: if cell.thorn: cell.label = 'X' elif cell.nThorns: cell.label = str(cell.nThorns) Now we can test the assignThornNumbers function from the interpreter. .. code-block:: python >>> gridSize = 10 >>> grid = createGrid(gridSize) >>> clickedCell = grid.getCell(0, 0) >>> placeThorns(grid, clickedCell, 15, 10) >>> assignThornNumbers(grid) >>> revealAll(grid) .. figure:: thornNumbers.png :alt: thornNumbers Figure 3: Number of neighboring thorns Reacting to user clicks ======================= We are going to follow our outline (with minor adjustments) here. A user click is either the first click or not. In these kind of binary situations we can use a :term:`flag` to retain the desired state information. We are going to name our flag as `firstClik` and initialize its value to `True`. Then in the `onClick` function change its value appropriately as shown below. .. code-block:: python firstClick = True def onClick(event, source): global firstClick if firstClick: placeThorns(grid, source, nThorns, gridSize) assignThornNumbers(grid) firstClick = False reveal(cell) for cell in grid.cells: cell.bind(LEFTDOWNON, onClick) .. Note:: Mekanimo's event binding mechanism requires the bound function's first two arguments to be `event` and `source`. Before we can test this, we need to create the `reveal` function and keep track of `firstClick` variable. Reveal the clicked cell --------------------- .. code-block:: python def reveal(cell): if cell.thorn: revealAll() cell.fillColor = color.red msgBox('Ouch') else: cell.fillColor = color.yellow if cell.nThorns: cell.label = str(cell.nThorns) else: displayConnectedZeros(cell) if won(): msgBox('Congratulations!') Check if the user won ---------------------- .. code-block:: python def won(): for cell in grid.cells: if not cell.thorn: if cell.fillColor != color.yellow: return False return True Finding connected zeros ------------------------ This is the most interesting part of the whole program and may be worth paying some extra attention especially for beginners. .. code-block:: python def findConnectedZeros(cell): queue = [cell] zeroes = [ ] while queue: curCell = queue.pop() zeroes.append(curCell) for zero in [x for x in curCell.allNeighbors if x.nThorns == 0]: if zero not in zeroes and zero not in queue: queue.append(zero) return zeroes To test the `findConnectedZeros` function use the Python interpreter as shown below. The results of this test is shown in Figure 4. .. code-block:: python >>> gridSize = 10 >>> grid = createGrid(gridSize) >>> clickedCell = grid.getCell(0, 0) >>> placeThorns(grid, clickedCell, 15, 10) >>> assignThornNumbers(grid) >>> revealAll(grid) >>> zeroes = findConnectedZeros(grid.getCell(5, 0)) >>> for cell in zeroes: ... cell.fillColor = color.yellow ... .. figure:: connectedZeros.png :alt: connectedZeros Figure 4: Connected zeroes Revealing connected zeroes -------------------------- Now we know how to get the connected zeroes the next action is to display these cells as well as their neighbors (colored orange in Figure 5 for clarity). .. figure:: displayedZeros.png :alt: displayedZeroes Figure 5: Connected zeroes' neighbors .. code-block:: python def displayConnectedZeroes(cell): zeroes = findConnectedZeroes(cell) processed = [ ] for zero in zeroes: for neighbor in zero.allNeighbors: if neighbor not in processed: neighbor.fillColor = color.yellow if neighbor.nThorns: neighbor.label = str(neighbor.nThorns) processed.append(neighbor) To test the `displayConnectedZeroes` function use the Python interpreter as shown below. The results of this test is shown in Figure 6. .. code-block:: python >>> def testGrid(gridSize): ... grid = createGrid(gridSize) ... placeThorns(grid, clickedCell) ... assignThornNumbers(grid) ... revealAll(grid) ... return grid ... >>> grid = testGrid(10) >>> clickedCell = grid.getCell(9, 0) >>> displayConnectedZeroes(clickedCell) .. figure:: displayedZeros2.png :alt: displayedZeroes2 Figure 6: Displaying connected zeroes and their neighbors Marking the thorns with right click ==================================== .. code-block:: python def onRightClick(event, source): if source.fillColor == color.grey: source.label = '' if source.label == 'X' else 'X' for cell in grid.cells: cell.bind(RIGHTDOWNON, onRightClick) New game button ================= .. code-block:: python def newGame(event, source): for cell in grid.cells: cell.label = '' cell.fillColor = color.grey firstClick = True rectangle() rectangle2.bind(LEFTDOWNON, newGame) Final listing ========================= .. code-block:: python :linenos: cellColor = color.grey revealedColor = color.yellow thornLabel = 'X' gridSize = 10 nThorns = 15 def createGrid(gridSize): rectangle(x=8, y=5, width=2.5, height=2.5, name='rectangle1', alias='r1', fillColor=cellColor) return Grid(rectangle1, gridSize, gridSize, pos=rectangle1.pos) def placeThorns(grid, clickedCell, nThorns, gridSize): for cell in grid.cells: cell.thorn = False choices = range(gridSize) thorns = 0 while thorns < nThorns: cell = grid.getCell(random.choice(choices), random.choice(choices)) if cell == clickedCell: continue if not cell.thorn: cell.thorn = True thorns += 1 def assignThornNumbers(grid): for cell in grid.cells: cell.nThorns = len([x for x in cell.allNeighbors if x.thorn]) def findConnectedZeroes(cell): queue = [cell] zeroes = [ ] while queue: curCell = queue.pop() zeroes.append(curCell) for zero in [x for x in curCell.allNeighbors if x.nThorns == 0]: if zero not in zeroes and zero not in queue: queue.append(zero) return zeroes def displayConnectedZeroes(cell): zeroes = findConnectedZeroes(cell) processed = [ ] for zero in zeroes: for neighbor in zero.allNeighbors: if neighbor not in processed: neighbor.fillColor = revealedColor neighbor.label = str(neighbor.nThorns) if neighbor.nThorns else "" processed.append(neighbor) def revealAll(): for cell in grid.cells: if cell.thorn: cell.label = thornLabel else: cell.fillColor = revealedColor cell.label = str(cell.nThorns) if cell.nThorns else "" def onClick(event, source): global firstClick if firstClick: placeThorns(grid, source, nThorns, gridSize) assignThornNumbers(grid) firstClick = False if source.label != thornLabel: reveal(source) def onRightClick(event, source): if source.fillColor == cellColor: source.label = '' if source.label == thornLabel else thornLabel def reveal(cell): if cell.thorn: revealAll() cell.fillColor = color.red msgBox('Ouch') else: cell.fillColor = revealedColor if cell.nThorns: cell.label = str(cell.nThorns) if cell.nThorns else '' else: displayConnectedZeroes(cell) if won(): msgBox('Congratulations!') def won(): for cell in grid.cells: if not cell.thorn: if cell.fillColor != revealedColor: return False return True def newGame(event, source): global firstClick for cell in grid.cells: cell.label = '' cell.fillColor = cellColor firstClick = True system.zeroGravity = True rectangle(x=42.3128547401, y=4.70541933018, width=7.47120015784, height=2.22699235474, name='restartButton', alias='r101', fillColor=(119, 187, 238, 150)) restartButton.label = 'New Game' restartButton.bind(LEFTDOWNON, newGame) grid = createGrid(gridSize) for cell in grid.cells: cell.bind(LEFTDOWNON, onClick) cell.bind(RIGHTDOWNON, onRightClick) firstClick = True Exercises =========== * Can you modify the program so that not only square but also rectangular grids can be created? * Can you modify the program so that right clicking on a marked cell would display a question mark in that cell and cells with question marks would behave like regular cells? * Can you add a timer so that users can see the elapsed time?