Thornsweeper

Information about the Thornsweeper game can be found at http://en.wikipedia.org/wiki/Minesweeper_(computer_game)

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

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.

>>> createGrid(10)
grid

Figure 1: Grid

Placing thorns when user makes the first move

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.

>>> 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'
...
thorns

Figure 2: Randomly placed thorns

Assign number of adjacent thorns to each cell

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.

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.

>>> gridSize = 10
>>> grid = createGrid(gridSize)
>>> clickedCell = grid.getCell(0, 0)
>>> placeThorns(grid, clickedCell, 15, 10)
>>> assignThornNumbers(grid)
>>> revealAll(grid)
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 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.

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

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

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.

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.

>>> 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
...
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).

displayedZeroes

Figure 5: Connected zeroes’ neighbors

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.

>>> 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)
displayedZeroes2

Figure 6: Displaying connected zeroes and their neighbors

Marking the thorns with right click

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

def newGame(event, source):
    for cell in grid.cells:
        cell.label = ''
        cell.fillColor = color.grey

    firstClick = True

rectangle()
rectangle2.bind(LEFTDOWNON, newGame)

Final listing

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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?