Ein Objekt für die Spieldaten
Wir arbeiten ja inzwischen nur noch objektorientiert. Wie wäre es also, wenn wir für die gesamte Spielsteuerung ein Objekt erstellen? Also ein ganz eigenes, das alle Spieldaten der aktuellen TicTacToe-Runde als Eigenschaften enthält und alle Spielfunktionen als Methoden. Damit hätten wir dann ein Element gebaut, das komplett in der Lage ist, unser TicTacToe-Spiel zu steuern.
Nennen wir es Spielverwaltung. Es bekommt alle Daten des Spiels als Eigenschaften.
class Spielverwaltung(object):
spielstand = [0,0,0,0,0,0,0,0,0]
aktiver_spieler = 1
Die Klasse Spielverwaltung ist nicht von einer anderen Klasse wie gamegrid oder Actor abgeleitet, sondern es ist unsere ganz eigene Klasse. Ihre Eigenschaften sind zunächst der Spielstand (also die aktuelle Belegung der Felder) und der aktive Spieler. Wenn ein Objekt mit dieser Klasse neu erstellt wird, hat es die Anfangswerte, die in der Klassendefinition angelegt sind.
So erzeugt das Programm ein Spielverwaltungs-Objekt:
spiel = Spielverwaltung()
Auch alle Funktionen, die für das Spielen von TicTacToe gebraucht werden, können wir als Methoden der Spielverwaltung-Klasse hinzufügen.
Fangen wir an mit der einfachsten Funktion: naechsterSpieler(). Immer wenn ein Zug beendet und das Spiel noch nicht zu Ende ist, wird sie aufgerufen. Die Objektvariable aktiver_spieler muss in dieser Funktion einfach nur zwischen 1 und 2 hin- und herwechseln.
def naechsterSpieler(self):
if self.aktiver_spieler == 1:
self.aktiver_spieler = 2
else:
self.aktiver_spieler = 1
Als Nächstes wäre eine Funktion gut, die prüft, ob eine Zelle leer oder belegt ist. Denn nur wenn eine Zelle leer ist, darf dort ein Symbol (Kreis oder Kreuz) platziert werden.
Wie prüft man das?
Man muss die Zellenkoordinaten mit der Liste spielstand abgleichen. Dazu muss die Zellenposition (0,0) oder (2,1) mit der jeweiligen Stelle in der Liste verglichen werden. Nur wenn eine 0 darinsteht, kann hier etwas gesetzt werden.
Abbildung 22.4 Koordinaten der Zellen und darunter jeweils ihre Position in der Liste spielstand
Die Funktion könnte so aussehen:
def zellePruefen(self,x,y):
listenpos = y*3+x
return self.spielstand[listenpos]
Die Funktion gibt also eine 0 zurück, wenn die Zelle leer ist, eine 1, wenn sie mit dem Symbol von Spieler 1 belegt ist (Kreis) und eine 2, wenn darin ein Symbol von Spieler 2 ist (Kreuz).
Und dann brauchen wir natürlich auch noch eine Funktion, um die aktuell gesetzte Marke auf dem Spielfeld zu setzen (das Symbol in der Zelle zu erzeugen) und es auch in die Liste spielstand einzutragen.
def setzen (self,x,y,spielernr):
symbol = Actor("sprites/mark_"+str(spielernr-1)+".gif")
feld.addActor(symbol,Location(x,y))
listenpos = y*3+x
self.spielstand[listenpos] = spielernr
Hier wird also neben der Zellenposition (x,y) auch noch die Spielernummer übergeben, denn die wird in die Liste eingetragen. 1 steht für Spieler 1 bzw. den Kreis, 2 steht für Spieler 2, also das Kreuz. Vorher wird das entsprechende Symbol an der Zellenposition x,y platziert. Verstehst du, wie das funktioniert? Der Name für die richtige Grafik des aktiven Spielers wird als String zusammengesetzt.
"mark_"+str(spielernr-1)+".gif"
Dadurch wird Spieler 1 zu mark_0.gif und Spieler 2 zu mark_1.gif.
Unser Spielverwaltungs-Objekt ist fast fertig. Nur eine Funktion fehlt noch, die wichtigste: Es muss geprüft werden, ob ein Spieler eine Reihe gebildet, also gewonnen hat. Der Funktion wird die Spielernummer übergeben (dafür steht sn), und dann bleibt nichts anderes als durchzuprüfen, ob hier drei Mal die Spielernummer in einer Reihe steht. Erst waagerecht, das geht aufgrund der linearen Liste noch einfach, dann senkrecht, da müssen die Koordinaten per Schleife abgefragt werden, danach diagonal, das wird einzeln geprüft. Wenn eine komplette Reihe gefunden wird, gibt die Funktion True zurück – durch das return wird sie automatisch beendet, es muss dann ja auch nicht weiter geprüft werden. Am Schluss, falls sie also keine Reihe gefunden hat, wird automatisch False (nicht gewonnen) zurückgegeben. Alles klar?
def gewonnen (self,sn):
liste = self.spielstand
# waagerecht (Teilreihen prüfen)
if (liste[0:3] == [sn,sn,sn]) or (liste[3:6] == [sn,sn,sn]) or (liste[6:9] =
= [sn,sn,sn]):
return True
# senkrecht (per Schleife prüfen)
for x in range(0,3):
if (liste[x]==sn) and (liste[x+3] == sn) and (liste[x+6] == sn):
return True
# diagonal (Einzelfelder prüfen)
if (liste[0]==sn) and (liste[4] == sn) and (liste[8] == sn):
return True
if (liste[2]==sn) and (liste[4] == sn) and (liste[6] == sn):
return True
return False
Schau dir die Funktion genau an, bis du alles verstehst.
Nun nur noch eine Minifunktion, die prüft, ob alle Felder belegt sind – denn dann ist das Spiel nämlich beendet, weil kein Zug mehr möglich ist.
Man kann das ganz einfach prüfen, indem man checkt, ob die Zahl 0 in der Liste spielstand enthalten ist. Wenn ja, gibt es noch freie Felder, wenn nein, dann ist das Spielbrett voll.
def brettVoll(self):
return not (0 in self.spielstand)
Wenn die Funktion so formuliert ist, gibt sie True zurück, wenn keine 0 mehr in der Liste ist (das Spiel ist also beendet). Ist noch ein Feld leer (wird also mindestens eine 0 in der Liste gefunden) wird False zurückgegeben.
Die gesamte Klasse Spielverwaltung sieht jetzt so aus:
class Spielverwaltung(object):
spielstand = [0,0,0,0,0,0,0,0,0]
aktiver_spieler = 1
def naechsterSpieler(self):
if self.aktiver_spieler == 1:
self.aktiver_spieler = 2
else:
self.aktiver_spieler = 1
def zellePruefen(self,x,y):
listenpos = y*3+x
return self.spielstand[listenpos]
def setzen(self,x,y,spielernr):
listenpos = y*3+x
self.spielstand[listenpos] = spielernr
def gewonnen (self,sn):
liste = self.spielstand
# waagerecht
if (liste[0:3] == [sn,sn,sn]) or (liste[3:6] == [sn,sn,sn]) or
(liste[6:9] == [sn,sn,sn]):
return True
# senkrecht
for x in range(0,3):
if (liste[x]==sn) and (liste[x+3] == sn) and (liste[x+6] == sn):
return True
# diagonal
if (liste[0]==sn) and (liste[4] == sn) and (liste[8] == sn):
return True
if (liste[2]==sn) and (liste[4] == sn) and (liste[6] == sn):
return True
return False
def brettVoll(self):
return not (0 in self.spielstand)
Wir haben mit unserem Objekt Spielverwaltung jetzt eine kleine »Maschine« gebaut, die alle Eigenschaften und Fähigkeiten hat, die man benötigt, um damit ein komplettes TicTacToe-Spiel durchzuführen. Wir müssen die Werte des Objekts jetzt nur noch setzen und ihre Funktionen an der richtigen Stelle aufrufen. Den Rest macht das Objekt selbst.
Wo im Programm werden die Funktionen der Spielverwaltung denn nun aufgerufen?
Nun, da die Spielverwaltung immer dann aktiv werden muss, wenn ein Spieler einen Zug gemacht hat, sollte an dieser Stelle auch die Spielverwaltung aufgerufen werden – also innerhalb der mausKlick()-Funktion. Ihr Inhalt verändert sich jetzt ein wenig – aber weil ja schon alle Funktionen, die das Spiel benötigt, da sind, lässt sich der ganze Spielverlauf sehr einfach abhandeln:
def mausKlick(ereignis):
# geklickte Zelle aus Mausposition ermitteln:
zellenPos = feld.toLocationInGrid(ereignis.getX(),ereignis.getY())
zx = zellenPos.getX() # gibt die x-Position der Zelle
zy = zellenPos.getY() # gibt die y-Position der Zelle
# Wenn Zelle leer, dann weiter:
if spiel.zellePruefen(zx,zy) == 0:
# Symbol setzen und in Liste eintragen:
spiel.setzen(zx,zy,spiel.aktiver_spieler)
if spiel.gewonnen(spiel.aktiver_spieler):
msgDlg("Spieler "+str(spiel.aktiver_spieler)+" hat gewonnen!")
elif (spiel.brettVoll()):
msgDlg("Das Spiel ist beendet: Unentschieden!")
else:
spiel.naechsterSpieler()
Eigentlich ganz schön simpel jetzt, der eigentliche Spielablauf, genau wie zuvor beschrieben: Wenn auf eine Zelle geklickt wird, wird erst einmal geprüft, ob die Zelle leer ist, wenn ja, dann wird dort ein Symbol gesetzt und in die spielstand-Liste eingetragen, dann wird geprüft, ob der Spieler mit diesem Zug gewonnen hat, wenn nein, dann wird geprüft, ob das Spielbrett voll ist, wenn nein, dann kommt der nächste Spieler an die Reihe.
Das war’s!
Hier noch einmal das ganze Programm im Überblick:
from gamegrid import *
class Spielverwaltung(object):
spielstand = [0,0,0,0,0,0,0,0,0]
aktiver_spieler = 1
def naechsterSpieler(self):
if self.aktiver_spieler == 1:
self.aktiver_spieler = 2
else:
self.aktiver_spieler = 1
def zellePruefen(self,x,y):
listenpos = y*3+x
return self.spielstand[listenpos]
def setzen(self,x,y,spielernr):
symbol = Actor("sprites/mark_"+str(spielernr-1)+".gif")
feld.addActor(symbol,Location(x,y))
listenpos = y*3+x
self.spielstand[listenpos] = spielernr
def gewonnen(self,sn):
liste = self.spielstand
# waagerecht
if (liste[0:3] == [sn,sn,sn]) or (liste[3:6] == [sn,sn,sn]) or
(liste[6:9] == [sn,sn,sn]):
return True
# senkrecht
for x in range(0,3):
if (liste[x]==sn) and (liste[x+3] == sn) and (liste[x+6] == sn):
return True
# diagonal
if (liste[0]==sn) and (liste[4] == sn) and (liste[8] == sn):
return True
if (liste[2]==sn) and (liste[4] == sn) and (liste[6] == sn):
return True
return False
def brettVoll(self):
return not (0 in self.spielstand)
def mausKlick(ereignis):
# geklickte Zelle ermitteln:
zellenPos = feld.toLocationInGrid(ereignis.getX(),ereignis.getY())
zx = zellenPos.getX() # gibt die x-Position der Zelle
zy = zellenPos.getY() # gibt die y-Position der Zelle
# Wenn Zelle leer, dann weiter:
if spiel.zellePruefen(zx,zy) == 0:
spiel.setzen(zx,zy,spiel.aktiver_spieler)
if spiel.gewonnen(spiel.aktiver_spieler):
msgDlg("Spieler "+str(spiel.aktiver_spieler)+" hat gewonnen!")
elif (spiel.brettVoll()):
msgDlg("Das Spiel ist beendet: Unentschieden!")
else:
spiel.naechsterSpieler()
feld = GameGrid(3,3,100,Color.BLACK,False,mouseReleased=mausKlick)
feld.setTitle("Tic Tac Toe")
feld.setBgColor(Color.WHITE)
feld.show()
spiel = Spielverwaltung()
[+] Kein »doRun()«?
Vielleicht fragst du dich, warum das Spielfeld zwar erzeugt wird, aber niemals feld.doRun() aufgerufen wird wie in den vorherigen Programmen? Es wird also keine interne Dauerschleife gestartet, aber trotzdem läuft das Spiel immer weiter bis zum Ende?
Dabei musst du dir klarmachen, wozu doRun() gut ist: doRun() wird immer dann gebraucht, wenn Figuren sich in ständiger Bewegung befinden und spontan auf Veränderungen reagieren müssen. doRun() spielt quasi so etwas wie einen Film ab und tut nichts anderes, als alle 50 Millisekunden (oder in dem von dir gewählten Takt) die Methode act() der Figuren aufzurufen und danach das Feld neu zu zeichnen, damit diese ununterbrochen ihre Aktionen ausführen und mit anderen Figuren interagieren können. Das alles brauchen wir hier ja nicht. Die Figuren haben gar keine act()-Methode, und sie müssen sich weder bewegen noch spontan auf etwas reagieren. Hier muss immer nur auf den Mausklick auf ein Feld reagiert werden – und das geschieht bei jedem Klick durch die Ereignisbehandlung von »mouseReleased« in feld. Wenn ein neues Symbol dem Feld hinzugefügt wird, zeichnet es sich ohnehin automatisch neu. Den Rest erledigt dann die Spielverwaltung jedes Mal, sobald ein neuer Zug gemacht wurde. Fertig. Objektorientierte Programmierung in Aktion!
Abbildung 22.5 Das Spiel hat begonnen …
Abbildung 22.6 … Ende: Unentschieden!