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.
|
BOARD INITIALIZATION, MOUSE CONTROL |
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.
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()
|
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 marble2. 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()
|
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.
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()
|
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 |
|