7.4 GRID GAMES, SOLITAIRE BOARD GAME

 

 

INTRODUCTION

 

In a certain class of computer games, tokens are restricted to be located on cells in a grid structure whereby the cells often have the same size and are arranged in a matrix. The consideration of this location restriction on a grid structure substantially simplifies the implementation of the game. As the name implies, the game library JGameGrid is particularly optimized for grid-like games.

In this chapter you will gradually develop the peg solitaire with the English board layout. You will get to know important solution methods that you can apply to all grid games.

PROGRAMMING CONCEPTS: Game board, game rules, specifications, game over

 

 

BOARD INITIALIZATION, MOUSE CONTROL

 

There is a regular arrangement of holes or recesses in a board into which you can either plug pegs or put marbles. The best-known Solitaire board uses a board with a cross-like arrangement of 33 holes and is called the English board. At the start of the game, all the holes except for the center hole are filled with marbles. As the name Solitaire implies, the game is usually played by a single person.

The following rules apply: a turn consists of moving a marble onto a free hole by skipping exactly one marble either horizontally or vertically. The skipped marble is removed from the game board.
 

An English Solitaire board from India, 1830
© 2003 puzzlemuseum.com

The goal is to "clear up" all the marbles from the board, except for the last marble. If the last marble ends up in the center, the game is considered to be solved especially well. When Solitaire is implemented as a computer game, you should be able to "grab" a certain marble by pressing the mouse button and move it by holding down and dragging. When you release the mouse button, the game checks if the turn followed the rules of the game. If you make an illegal move the marble will jump back to its previous location, and if you make a legal move the marble will appear at the new location and the skipped marble will be removed from the board.

With this, the specification is clear and you can start with the implementation. As always, this is done step by step, and you should make sure that your program is running at each of these steps. It is perfectly obvious to use a game grid with 7x7 cells, without using the corner cells. First you draw the board in the function initBoard() using the background image solitaire_board.png which is included in the distribution of TigerJython.You implement the mouse controls with the mouse callbacks mousePressed, mouseDragged, and mouseReleased.

At a Press event you keep track of the current cell location and the marble currently in it. You can obtain the marble with getOneActorAt() and you will receive None if the cell is empty. If you write out important results in a status bar (or in the console), the development process will be easier to control and mistakes easier to find.

During the Drag event you move the visible image of the marble to the current cursor position using setLocationOffset(). You also move it to any mouse position away from the middle of the cells, so that a continuous motion arises. It is important that this does not move the marble actor itself, but only its sprite image (hence the term offset). With this, you can avoid any difficulties with superimposed actors.

In this first version, the marble should simply jump back to its original position upon a Release event. You can do this by calling setLocationOffset(0, 0).
 


from gamegrid import *

def isMarbleLocation(loc):
    if loc.x < 0 or loc.x > 6 or loc.y < 0 or loc.y > 6:
        return False
    if loc.x in [0, 1, 5, 6] and loc.y in [0, 1, 5, 6]:
        return False
    return True

def initBoard():
    for x in range(7):
        for y in range(7):
            loc = Location(x, y)
            if isMarbleLocation(loc):
                marble = Actor("sprites/marble.png")
                addActor(marble, loc)
    removeActorsAt(Location(3, 3)) # Remove marble in center

def pressEvent(e):
    global startLoc, movingMarble
    startLoc = toLocationInGrid(e.getX(), e.getY())
    movingMarble = getOneActorAt(startLoc)
    if movingMarble == None:
       setStatusText("Pressed at " + str(startLoc) + ". No marble found")
    else:
       setStatusText("Pressed at " + str(startLoc) + ". Marble found")

def dragEvent(e):
    if movingMarble == None:
        return
    startPoint = toPoint(startLoc)
    movingMarble.setLocationOffset(e.getX() - startPoint.x, 
                                   e.getY() - startPoint.y) 

def releaseEvent(e):
    if movingMarble == None:
        return
    movingMarble.setLocationOffset(0, 0)

makeGameGrid(7, 7, 70, None, "sprites/solitaire_board.png", False,
    mousePressed = pressEvent, mouseDragged = dragEvent, 
    mouseReleased = releaseEvent)
setBgColor(Color(255, 166, 0))
setSimulationPeriod(20)
addStatusBar(30)
setStatusText("Press-drag-release to make a move.")
initBoard()
show()
doRun()
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

Instead of moving the actual actor while dragging, you can just move its sprite image. For this, use setLocationOffset(x, y) where x and y are coordinates relative to the current center point of the actor.

You have to carefully distinguish between the coordinates of the mouse and the cell coordinates when dealing with mouse movements. You can use the functions toLocationInGrid(pixel_coord) and toPoint(location_coord) to convert between these coordinates.

If you start at an empty cell, the drag and release events lead to an infamous program crash. This is because you are trying to call a method with movingMarble that has the value None.

In order to avoid this error, leave the callbacks with an immediate return right at the beginning.

 

 

IMPLEMENTING THE GAME RULES

 

How would you verify the game rules with the real game? You would have to know which marble you started with, so you would need to know its starting location start. You would then need to know where you want to move the marble to, which is the cell location dest. The following conditions must be met in order to make a legal move:

1. At start there is a marble
2. At dest there is no marble
3. dest is a cell belonging to the board
4. start and dest are either horizontally or vertically two cells apart
5. There is a marble in the cell between them, too

It is a good idea to implement these conditions in a function getRemoveMarble(start, dest) that returns the marble to be removed after a legal turn and returns None after an illegal turn.

Thus, you should call this function at the release event and remove the returned Actor from the board using removeActor() if the turn was legal.

from gamegrid import *

def getRemoveMarble(start, dest):
    if getOneActorAt(start) == None:
        return None
    if getOneActorAt(dest) != None:
        return None
    if not isMarbleLocation(dest):
        return None
    if dest.x - start.x == 2 and dest.y == start.y:
        return getOneActorAt(Location(start.x + 1, start.y))
    if start.x - dest.x == 2 and dest.y == start.y:
        return getOneActorAt(Location(start.x - 1, start.y))
    if dest.y - start.y == 2 and dest.x == start.x:
        return getOneActorAt(Location(start.x, start.y + 1))
    if start.y - dest.y == 2 and dest.x == start.x:
        return getOneActorAt(Location(start.x, start.y - 1))

def isMarbleLocation(loc):
    if loc.x < 0 or loc.x > 6 or loc.y < 0 or loc.y > 6:
        return False
    if loc.x in [0, 1, 5, 6] and loc.y in [0, 1, 5, 6]:
        return False
    return True

def initBoard():
    for x in range(7):
        for y in range(7):
            loc = Location(x, y)
            if isMarbleLocation(loc):
                marble = Actor("sprites/marble.png")
                addActor(marble, loc)
    removeActorsAt(Location(3, 3)) # Remove marble in center

def pressEvent(e):
    global startLoc, movingMarble
    startLoc = toLocationInGrid(e.getX(), e.getY())
    movingMarble = getOneActorAt(startLoc)
    if movingMarble == None:
       setStatusText("Pressed at " + str(startLoc) + ". No marble found")
    else:
       setStatusText("Pressed at " + str(startLoc) + ". Marble found")

def dragEvent(e):
    if movingMarble == None:
        return
    startPoint = toPoint(startLoc)
    movingMarble.setLocationOffset(e.getX() - startPoint.x, 
                                   e.getY() - startPoint.y) 

def releaseEvent(e):
    if movingMarble == None:
        return
    destLoc = toLocationInGrid(e.getX(), e.getY())
    movingMarble.setLocationOffset(0, 0)
    removeMarble = getRemoveMarble(startLoc, destLoc)
    if removeMarble == None:
        setStatusText("Released at " + str(destLoc) + ". Not a valid move.")
    else:
        removeActor(removeMarble)
        movingMarble.setLocation(destLoc)    
        setStatusText("Released at " + str(destLoc)+ ". Marble removed.")
    

startLoc = None
movingMarble = None

makeGameGrid(7, 7, 70, None, "sprites/solitaire_board.png", False,
    mousePressed = pressEvent, mouseDragged = dragEvent, 
    mouseReleased = releaseEvent)
setBgColor(Color(255, 166, 0))
setSimulationPeriod(20)
addStatusBar(30)
setStatusText("Press-drag-release to make a move.")
initBoard()
show()
doRun()
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

Instead of using several early returns to leave the function getRemoveMarble(), you could also combine the conditions with a Boolean operation. Which programming technique is considered to be more appropriate is a matter of opinion.

 

 

CHECKING FOR GAME OVER

 

Now all that remains is checking if the game is over at the end of each turn. The game is certainly over if only a single marble is left in the game, which means you have achieved the goal of the game.

However, you may forget that there are other game constellations where the game is considered to have ended, namely when there is more than one marble on the board, but you can no longer make a legal turn. It is not certain whether you will ever run into this situation with legal moves, but you have to program defensively to make sure that you always stay on the safe side. You can expect Murphy's law to also be true for programming: "If anything can go wrong, it goes wrong".

 

In order to get the situation under control, you can test for each remaining marble individually whether it can be used in a legal move, by implementing a function isMovePossible(). There, you check for each marble whether there is a removable intermediary marble in combination with any empty spot [more...You have only look for holes in the surrounding area].

from gamegrid import *

def checkGameOver():
    global isGameOver
    marbles = getActors() # get remaining marbles
    if len(marbles) == 1:
        setStatusText("Game over. You won.")
        isGameOver = True
    else:
        # check if there are any valid moves left
        if not isMovePossible():
           setStatusText("Game over. You lost. (No valid moves available)")
           isGameOver = True

def isMovePossible():
   for a in getActors():  # run over all remaining marbles
        for x in range(7): # run over all holes
            for y in range(7):
                loc = Location(x, y)
                if getOneActorAt(loc) == None and \
                  getRemoveMarble(a.getLocation(), Location(x, y)) != None:
                    return True
   return False
    
def getRemoveMarble(start, dest):
    if getOneActorAt(start) == None:
        return None
    if getOneActorAt(dest) != None:
        return None
    if not isMarbleLocation(dest):
        return None
    if dest.x - start.x == 2 and dest.y == start.y:
        return getOneActorAt(Location(start.x + 1, start.y))
    if start.x - dest.x == 2 and dest.y == start.y:
        return getOneActorAt(Location(start.x - 1, start.y))
    if dest.y - start.y == 2 and dest.x == start.x:
        return getOneActorAt(Location(start.x, start.y + 1))
    if start.y - dest.y == 2 and dest.x == start.x:
        return getOneActorAt(Location(start.x, start.y - 1))
    return None

def isMarbleLocation(loc):
    if loc.x < 0 or loc.x > 6 or loc.y < 0 or loc.y > 6:
        return False
    if loc.x in [0, 1, 5, 6] and loc.y in [0, 1, 5, 6]:
        return False
    return True

def initBoard():
    for x in range(7):
        for y in range(7):
            loc = Location(x, y)
            if isMarbleLocation(loc):
                marble = Actor("sprites/marble.png")
                addActor(marble, loc)
    removeActorsAt(Location(3, 3)) # Remove marble in center

def pressEvent(e):
    global startLoc, movingMarble
    if isGameOver:
        return
    startLoc = toLocationInGrid(e.getX(), e.getY())
    movingMarble = getOneActorAt(startLoc)
    if movingMarble == None:
       setStatusText("Pressed at " + str(startLoc) + ".No marble found")
    else:
       setStatusText("Pressed at " + str(startLoc) + ".Marble found")

def dragEvent(e):
    if isGameOver:
        return
    if movingMarble == None:
        return
    startPoint = toPoint(startLoc)
    movingMarble.setLocationOffset(e.getX() - startPoint.x, 
                                   e.getY() - startPoint.y) 

def releaseEvent(e):
    if isGameOver:
        return
    if movingMarble == None:
        return
    destLoc = toLocationInGrid(e.getX(), e.getY())
    movingMarble.setLocationOffset(0, 0)
    removeMarble = getRemoveMarble(startLoc, destLoc)
    if removeMarble == None:
        setStatusText("Released at " + str(destLoc) 
                       + ". Not a valid move.")
    else:
        removeActor(removeMarble)
        movingMarble.setLocation(destLoc)    
        setStatusText("Released at " + str(destLoc)+
                      ". Valid move - Marble removed.")
        checkGameOver()


startLoc = None
movingMarble = None
isGameOver = False

makeGameGrid(7, 7, 70, None, "sprites/solitaire_board.png", False,
   mousePressed = pressEvent, mouseDragged = dragEvent, 
   mouseReleased = releaseEvent)
setBgColor(Color(255, 166, 0))
setSimulationPeriod(20)
addStatusBar(30)
setStatusText("Press-drag-release to make a move.")
initBoard()
show()
doRun()
Highlight program code (Ctrl+C to copy, Ctrl+V to paste)

 

 

MEMO

 

After each turn, you check if the game is over using checkGameOver(), If it is over, the game is in a very specific state that you can distinguish with the Boolean variable (a flag) isGameOver = True .

In particular, you must remember to stop all the mouse actions in Game Over. You can do this with an immediate return from the mouse callbacks.

 

 

EXERCISES

 

1.

Create a French Solitaire board.

 

 

2.

Expand the Solitaire board with a score that counts and writes out the number of turns. The game should also be able to restart by pressing the space bar.


3.

Familiarize yourself with solution strategies of Peg solitaire with the help of a teacher or the Internet [more... Book recommendation: John Beasley, The Ins and Outs of Peg Solitaire].

 

4.

Create a Solitaire board from your own imagination.