EINFÜHRUNG |
Bei einer bestimmten Klasse von Computergames können sich die Spielfiguren nur in Zellen einer Gitterstruktur aufhalten, wobei die Zellen meist gleiche Grösse haben und matrixartig angeordnet sind. Die Berücksichtigung der Ortsbeschränkung auf eine Gitterstruktur vereinfacht die Implementierung des Spiels ganz wesentlich. Wie schon der Name sagt, ist die Gamelibrary JGameGrid für gitterartige Games besonders optimiert. In diesem Kapitel entwickelst du schrittweise das Brett-Solitaire mit dem englischen Brettlayout. Dabei lernst du wichtige Lösungsverfahren kennen, die du auf alle Gitterspiele anwenden kannst. |
BRETTINITIALISIERUNG, MAUSSTEUERUNG |
Das Ziel besteht darin, alle Murmeln bis auf eine letzte vom Brett "abzuräumen". Das Spiel gilt als besonders gut gelöst, falls sich die letzte Murmel im Zentrum befindet. In der Implementierung als Computergame sollst du eine bestimmte Murmel durch Drücken der Maustaste "packen" und bei gedrückter Maustaste verschieben können. Beim Loslassen der Maustaste wird geprüft, ob der Zug den Spielregeln entspricht. Falls er regelwidrig ist, soll die Murmel wieder an den Anfangsort zurückspringen; ist er legal, so wird die Murmel am neuen Ort angezeigt und die übersprungene Murmel vom Brett entfernt. Damit ist das Pflichtenheft klar und du kannst hinter die Implementierung gehen. Diese erfolgt wie immer schrittweise, wobei bei jedem Schritt ein lauffähiges Programm vorliegen muss. Es liegt auf der Hand, eine GameGrid mit 7x7 Zellen zu verwenden, wobei die Eckzellen nicht verwendet werden. Zuerst zeichnest du in der Funktion initBoard() mit dem Hintergrundbild solitaire_board.png, die sich in der Distribution von TigerJython befindet, das Brett und realisierst die Maussteuerung mit den Maus-Callbacks mousePressed, mouseDragged und mouseReleased. Beim Press-Event merkst du dir die aktuelle Location und die aktuelle Murmel in dieser Zelle. Diese wird dir mit getOneActorAt() zurückgeben, wobei du None erhältst, wenn die Zelle leer ist. Der Entwicklungsprozess ist leichter zu beherrschen und Fehler besser aufzufinden, wenn du in einer Statusbar (oder in der Konsole) wichtige Ergebnisse ausschreibst.
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 |
Statt beim Draggen den Actor selbst zu verschieben, kannst du auch nur sein Spritebild bewegen. Dazu verwendest du setLocationOffset(x, y), wobei x und y relative Koordinaten in Bezug auf den aktuellen Mittelpunkt des Actors sind. Im Zusammenhang mit Mausbewegungen musst du zwischen den Koordinaten der Maus und Zellenkoordinaten sorgfältig unterscheiden. Dabei sind die Konversionsfunktionen toLocationInGrid(pixel_coord) bzw. toPoint(location_coord) wichtig. Gehst du von einer leeren Zelle aus, so führen der Drag- und Release-Event zu einem berüchtigten Programm-Absturz, da movingMarble den Wert None hat und du damit eine Methode aufrufst. Um den Fehler zu vermeiden, verlässt du die Callbacks gerade zu Beginn mit einem sofortigen return. |
IMPLEMENTIERUNG DER SPIELREGELN |
Wie würdest du beim wirklichen Spiel die Spielregeln überprüfen? Du müsstest wissen, mit welcher Murmel du gestartet bist, also deren Startlocation start kennen. Dann müsstest du wissen, wohin du die Murmel verschieben möchtest, also deren Ziellocation dest kennen. Für einen legalen Zug müssen folgenden Bedingungen zutreffen: 1. Bei start gibt es eine Murmel2. Bei dest gibt es keine Murmel 3. dest ist eine Zelle, die zum Board gehört 4. start und dest sind entweder horizontal oder vertikal zwei Zellen auseinander 5. An der Zwischenzelle befindet sich eine Murmel Es ist elegant, diese Bedingungen in einer Funktion getRemoveMarble(start, dest) zu implementieren, welche bei einem legalen Zug die zu entfernende Murmel und bei einem illegalen Zug None zurückgibt. Es ist klar, dass du diese Funktion im Release-Event aufrufst und bei einem legalen Zug den zurückgegebenen Actor mit removeActor() vom Board nimmst. 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 |
Statt mit mehreren vorzeitigen return die Funktion getRemoveMarble() zu verlassen, könnte man die Bedingungen auch mit booleschen Operation verknüpfen. Es ist Ansichtssache, welche Programmiertechnik man als geeigneter betrachtet. |
PRÜFUNG AUF GAME-OVER |
Es bleibt dir jetzt nur noch die Aufgabe, bei jedem legalen Zug zu prüfen, ob das Spiel beendet ist. Dies ist sicher dann der Fall, falls sich nur noch eine einzige Murmel auf dem Spielfeld befindet und du damit das Spielziel erreicht hast.
Um diese Situation in den Griff zu bekommen, kannst du in der Funktion isMovePossible() alle noch vorhandenen Murmeln einzeln darauf zu testen, ob man mit ihnen einen legalen Zug machen kann. Dazu prüfst du für jede Murmel, ob es mit irgendeinem Loch eine zu entfernende Zwischenmurmel gibt [mehr... Du müsstest zwar nur für Löcher in der näheren Umgebung danach suchen]. 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 |
Nach jedem Zug testest du mit checkGameOver(), ob das Spiel beendet ist. Ist dies der Fall, befindet sich das Spiel in einem ganz speziellen Zustand, den du mit der booleschen Variable (ein Flag) isGameOver = True kennzeichnest. Insbesondere musst du bei Game-Over auch alle Maus-Aktionen unterbinden. Du erreichst dies mit einem sofortigen return aus den Maus-Callbacks. |
AUFGABEN |
|