| Listen | Python-Kurs | Code-Knacker mit GUI |
Bevor Sie hier weiterarbeiten, sollten Sie die Grundkenntnisse in Python verstanden haben und zumindest bis zu den Listen gekommen sein. Falls dem nicht so ist, sollten Sie den Python-Kurs durcharbeiten.
Spielregeln
Der Computer denkt sich eine zufällige vierstellige Zahl aus, deren Ziffern zwischen 1 und 6 liegen. Der Spieler versucht die Zahl zu raten, indem er immer wieder eine vierstellige Zahl eintippt.Damit der Spieler nicht alle 10.000 denkbaren Kombinationen durchprobieren muss, erhält er Tipps vom Computer in Form von zwei Zahlen:
- Die erste Zahl zeigt ihm, wie oft er eine Ziffer an der korrekten Stelle getroffen hat.
- die zweite Zahl zeigt an, wie oft er eine Ziffer getroffen hat, die zwar in der Kombination enthalten ist, aber nicht an der von ihm vermuteten Stelle.
Geheimzahlen erstellen und einmal Raten
Im ersten Anlauf wird eine Liste geheim mit zufälligen Werten zwischen 1 und 6 erstellt.Anschließend lassen wir den Spieler eine vierstellige Zahl eintippen. Wir testen weder, ob es wirklich vier Stellen sind, noch ob es auch alles Ziffern sind.
Nun soll das Programm prüfen, ob ein Volltreffer da ist. Dazu benötigt man eine Schleife, die durch alle vier Positionen geht und zählt, wenn es Übereinstimmungen gibt.
#!/usr/bin/python3
# codeknacker1.py (C) Arnold Willemer
import random
geheim = []
for i in range(4):
geheim.append(random.randint(1,6))
print(geheim) # nur für die Testphase
rate = input("Gebe vierstelligen Code ein (je 1-6) ")
treffer = 0
for i in range(4):
if int(rate[i])==geheim[i]:
treffer = treffer + 1
print(treffer)
Die Schleife prüft, ob die Ziffern von Geheimzahl
und Rateversuch exakt an einer Stelle übereinstimmen. Dann wird die
Variable treffer hochgezählt.
Nach Ende der Schleife wird die Zahl der Treffer ausgegeben.
Ziffern finden, die nicht an der gleichen Position sind
Wir versuchen nun, die Ziffern zu finden, die zwar im Geheimcode enthalten sind, aber nicht an der richtigen Stelle.Nutzung des Befehls in
Dazu nutzen wir die Möglichkeit des Befehls in, der prüft, ob sich die jeweilige Zahl in einer Liste befindet.
# Dieser Teil funktioniert nicht ganz richtig bei Doppelten
drin = 0
for z in rate:
if int(z) in geheim:
drin = drin + 1
Natürlich zählen wir dabei auch die Ziffern mit, die wir schon als Treffer
gezählt haben. Das ist leicht zu regeln, indem man einfach die Treffer
abzieht.
drin = drin - treffer print(treffer, drin)Aber ganz richtig ist das Ergebnis nicht. Wenn man im Rateversuch vier gleiche Ziffern eintippt und diese nur einmal im Geheimcode vorkommt, werden für alle vier je eine Übereinstimmung gezählt.
Korrektes Zählen der Übereinstimmungen
So verlockend die Verwendung von in ist, so gefährlich ist sie. Betrachten wir das folgende Szenario:
| Geheimcode | 3 | 1 | 4 | 2 |
| Rateversuch | 1 | 5 | 1 | 6 |
An der ersten und in der dritten Stelle des Rateversuchs würde die Überprüfung eine Übereinstimmung mit der zweiten Stelle in der Geheimzahl auftreten.
Es gibt aber noch eine Problemstellung, die das folgende Szenario aufzeigt.
| Geheimcode | 1 | 5 | 1 | 6 |
| Rateversuch | 3 | 1 | 4 | 2 |
Hier würde der umgekehrte Fall eintreten, dass der zweite Rateversuch die erste und dritte Ziffer des Geheimcodes findet und diese dann doppelt zählt.
Der Lösungsansatz wäre, dass bei einer Übereinstimmung der gefundenen Wert im Geheimcode gelöscht wird, damit er nicht in einer weiteren Runde noch einmal gezählt wird, beispielsweise mit einer negativen Zahl. Dasselbe sollte im Rateversuch passieren, sinnvollerweise mit einer anderen negativen Zahl, damit nicht hinterher doch noch eine Übereinstimmung gefunden wird, wo keine ist.
drin = 0
for r in range(4):
for g in range(4):
if rate[r]==geheim[g]:
drin = drin + 1
rate[r] = -1
geheim[g] = -2
Hier ist nun rate nicht mehr ein String, sondern eine Liste, die
wir aus dem Eingabestring generieren, weil man im String keine Elemente
verändern kann.
Wiederholen bis zum geknackten Code
Dem Spieler sollen mehrere Runden gegönnt werden, bis er den Code findet. Dazu muss eine große Schleife über die Eingabe und die anschließende Prüfung gelegt werden.Dabei stellen wir fest, dass wir beim zweiten Raten nicht den durch die Markierungen veränderten Geheimcode zum Raten anbieten können. Wir müssen also in jeder Runde den Originalcode einmal kopieren, damit jede Runde mit der gleichen Umgebung beginnt.
#!/usr/bin/python3
# codeknacker3.py (C) Arnold Willemer
import random
geheimX = []
for i in range(4):
geheimX.append(random.randint(1,6))
print(geheimX) # nur für die Testphase
treffer = 0
while treffer < 4:
# Wir benötigen eine Kopie von geheimX zum Ausmerzen der Doppelten
geheim = []
for z in geheimX:
geheim.append(z)
rateX = input("Gebe vierstelligen Code ein (je 1-6) ")
# Eine Kopie vom Rateversuch
rate = []
for z in rateX:
rate.append(int(z))
treffer = 0
for i in range(4):
if rate[i]==geheim[i]:
treffer = treffer + 1
rate[i] = -1 # markiere die gefundene Zahl
geheim[i] = -2 # markiere mit anderer Ziffer
drin = 0
for r in range(4):
for g in range(4):
if rate[r]==geheim[g]:
drin = drin + 1
rate[r] = -1
geheim[g] = -2
print(treffer, drin)
print("Gewonnen")
Realisierung mit Funktionen
Funktionen ermöglichen uns die Zusammenfassung von Anweisungen. Dadurch kann zunächst geschrieben werden, welche Funktionalitäten das Programm hat. Diesen Ansatz nennt man Top-Down-Programmierung.Das Spiel läuft in einer Schleife, bis alle vier Ziffern geraten wurden. In dieser Schleife gibt der Spieler einen Versuch ein. Der Versuch wird auf Übereinstimmungen mit der Geheimzahl verglichen. In jeder Runde wird dem Spieler der Erfolg seiner Bemühungen angezeigt.
geheimZahlZiehen()
treffer = 0
while treffer < 4:
input("Dein Versuch: ")
vergleiche()
zeigeErgebnis()
print("Gewonnen")
Es fehlen die Daten, die von den Funktionen durchgereicht werden.
Wir ergänzen die gezogene Geheimzahl und den vom Spieler angegebene
Versuch.
geheim = geheimZahlZiehen()
print(geheim)
treffer = 0
while treffer < 4:
ratestr = input("Dein Versuch: ")
treffer, drin = vergleiche(geheim, ratestr)
print(treffer, drin)
print("Gewonnen")
Anschließend müssen die Funktionen definiert werden.
Das Ziehen der Geheimzahl erzeugt eine Liste von vier Zahlen zwischen
1 und 6. Wir benötigen den import von random.
Anschließend wird an die Liste immer wieder eine neue Zahl angehängt,
bis es 4 sind.
import random
def geheimZahlZiehen():
geheim = []
for i in range(4):
geheim.append(random.randint(1,6))
return geheim
Die Funktion geheimZahlZiehen ist übersichtlich kompliziert.
Die Auswertung des Spielversuchs erfolgt in der Funktion vergleiche
und das ist schon komplizierter. Im ersten Schritt zerlegen wir die
Aufgabe in den exakten Vergleich an der aktuellen Position und den
schwierigeren Part, zu prüfen, ob Übereinstimmungen an anderer Stelle sind.
def vergleiche(geheim, ratestr):
treffer = vergleicheExakt(geheimX, rateListe)
drin = vergleicheDrin(geheimX, rateListe)
return treffer, drin
Wir erhalten zwei Variablen als Ergebnisse, treffer und drin.
Beide müssen an den Aufrufer gemeldet werden. C- und Java-Programmierer
dürften staunen, dass in Python zwei Werte zurückgegeben werden können.
Das Geheimnis ist, dass auch Python eigentlich nur einen Wert zurückgibt.
Dieser ist allerdings ein Tupel. Damit geht es.
Beim ermitteln der Übereinstimmung an anderer Position kommt es zu dem Problem, dass Zahlen eventuell doppelt gezählt werden. In der vorigen Version haben wir das Problem gelöst, indem wir gefundene Elemente markiert haben. Das werden wir auch hier anwenden. Allerdings müssen wir dazu eine Kopie der Geheimzahl verwenden, damit wir sie in der nächsten Runde noch zur Verfügung haben. Auch die geratene Zahl muss markiert werden. Hier ist die Ausgangslage anders, weil es sich um einen String handelt, der nicht so leicht verändert werden kann. Also erzeugen wir aus dem String eine Liste. Es kommen also zwei weitere Funktionsaufrufe in die Funktion vergleiche.
def vergleiche(geheim, ratestr):
geheimX = kopiereGeheimzahl(geheim)
rateListe = stringToListe(ratestr)
treffer = vergleicheExakt(geheimX, rateListe)
drin = vergleicheDrin(geheimX, rateListe)
return treffer, drin
Das Kopieren der Gemeinzahl kann nicht durch eine einfache Zuweisung geschehen,
weil sonst nur ein zusätzlicher Verweis entstehen würde. Es muss explizit
eine neue Liste entstehen.
def kopiereGeheimzahl(geheim):
kopie = []
for i in geheim:
kopie.append(i)
return kopie
Um aus dem String des Rateversuchs eine Liste zu machen, durchläuft
stringToListe den String und erzeugt jeweils ein neues Listenelement.
Dabei konvertiert es den Buchstaben zu der entsprechenden Zahl.
def stringToListe(ratestr):
rate = []
for z in ratestr:
rate.append(int(z))
return rate
Vergleiche
Der Vergleich auf einen echten Treffer ist noch recht einfach. Die Funktion durchläuft die beiden Listen und zählt, wie oft eine Übereinstimmung vorliegt. Das besondere Problem ist, dass die gefundenen Treffer bei der Überprüfung des Enthaltenseins nicht ein weiteres Mal gezählt werden dürfen. Darum ersetzt man sowohl den Rateversuch als auch die Geheimzahl so, dass der Wert nicht wieder passt. Darum wird beim Raten -1 und bei der Geheimzahl -2 verwendet.Da Listen per Referenz an Funktion übergeben werden, sind die Änderungen der Liste beim Aufrufer auch gültig. Das ist in diesem Fall sehr praktisch, weil die Listen ja später genau so an vergleicheDrin übergeben werden.
def vergleicheExakt(geheim, rate):
treffer = 0
for i in range(4):
if rate[i]==geheim[i]:
treffer = treffer + 1
rate[i] = -1 # markiere die gefundene Zahl
geheim[i] = -2 # markiere mit anderer Ziffer
return treffer
Beim Vergleich, ob eine geratene Ziffer an irgendeine anderen Stelle der
Geheimzahl auftaucht, benötigt man zwei ineinander geschachtelte Schleifen.
So wird jede mit jeder verglichen.
Tritt eine Übereinstimmung auf, wird diese gezählt. Ein direkter Treffer
kann es nicht sein, weil sie bereits markiert wurden.
Damit die gefundene Übereinstimmung nicht noch einmal gezählt wird, wird
sie auch hier markiert.
def vergleicheDrin(geheim, rate):
drin = 0
for r in range(4):
for g in range(4):
if rate[r]==geheim[g]:
drin = drin + 1
rate[r] = -1
geheim[g] = -2
return drin
Zusammenfassung
Zu guter Letzt soll das Programm noch einmal als Ganzes gezeigt werden. Jede der Funktionen ist nur wenige Zeilen lang und dadurch ist die Komplexität gesunken. Wenn sprechende Namen verwendet werden, ist das Zusammenspiel recht übersichtlich. Wenn Ihnen an der einen oder anderen Stelle etwas kompliziert vorkam, ist dies genau die Stelle, wo Sie unbedingt einen Kommentar einfügen sollten, damit Sie das Listing beim nächsten Mal flüssig lesen können.
#!/usr/bin/python3
# codeknacker3.py (C) Arnold Willemer
import random
def geheimZahlZiehen():
geheim = []
for i in range(4):
geheim.append(random.randint(1,6))
return geheim
def stringToListe(ratestr):
rate = []
for z in ratestr:
rate.append(int(z))
return rate
def vergleicheExakt(geheim, rate):
treffer = 0
for i in range(4):
if rate[i]==geheim[i]:
treffer = treffer + 1
rate[i] = -1 # markiere die gefundene Zahl
geheim[i] = -2 # markiere mit anderer Ziffer
return treffer
def vergleicheDrin(geheim, rate):
drin = 0
for r in range(4):
for g in range(4):
if rate[r]==geheim[g]:
drin = drin + 1
rate[r] = -1
geheim[g] = -2
return drin
def kopiereGeheimzahl(geheim):
kopie = []
for i in geheim:
kopie.append(i)
return kopie
def vergleiche(geheim, ratestr):
geheimX = kopiereGeheimzahl(geheim)
rateListe = stringToListe(ratestr)
treffer = vergleicheExakt(geheimX, rateListe)
drin = vergleicheDrin(geheimX, rateListe)
return treffer, drin
geheim = geheimZahlZiehen()
print(geheim)
treffer = 0
while treffer < 4:
ratestr = input("Dein Versuch: ")
treffer, drin = vergleiche(geheim, ratestr)
print(treffer, drin)
print("Gewonnen")
Eine Codeknacker-Klasse
Nun soll eine Klasse CodeKnacher entstehen, die dann als Modul in Code eingebunden werden kann. Auf diese Weise kann die Spiellogik sowohl in eine Konsolenapplikation als auch in eine GUI eingebunden werden.Zum Verständnis dieses Teils sollten auf jeden Fall die Themen Klasse und Methode verstanden sein.
Alle Methodennamen der Klasse, die mit einem Unterstrich beginnen, sind interne Methoden, die von außen nicht erreicht werden können.
#!/usr/bin/python3
# codeknacker_klasse.py (C) Arnold Willemer
import random
class CodeKnacker():
"""
Der Konstruktor zieht eine neue Geheimzahl
"""
def __init__(self):
self.geheim = []
for i in range(4):
self.geheim.append(random.randint(1,6))
def _stringToListe(self, ratestr):
self._rate = []
for z in ratestr:
self._rate.append(int(z))
def _vergleicheExakt(self):
treffer = 0
for i in range(4):
if self._rate[i]==self._geheim[i]:
treffer = treffer + 1
self._rate[i] = -1 # markiere die gefundene Zahl
self._geheim[i] = -2 # markiere mit anderer Ziffer
return treffer
def _vergleicheDrin(self):
drin = 0
for r in range(4):
for g in range(4):
if self._rate[r]==self._geheim[g]:
drin = drin + 1
self._rate[r] = -1
self._geheim[g] = -2
return drin
def _kopiereGeheimzahl(self):
self._geheim = []
for i in self.geheim:
self._geheim.append(i)
def vergleiche(self, ratestr):
self._kopiereGeheimzahl()
self._stringToListe(ratestr)
treffer = self._vergleicheExakt()
drin = self._vergleicheDrin()
return treffer, drin
# Testspiel lokal
if __name__ == "__main__":
spiel = CodeKnacker()
print(spiel.geheim)
treffer = 0
while treffer < 4:
ratestr = input("Dein Versuch: ")
treffer, drin = spiel.vergleiche(ratestr)
print(treffer, drin)
print("Gewonnen")
Nach außen stellt die Klasse den Konstruktor und die Methode
vergleiche zur Verfügung.
Damit kann die Geheimzahl als Attribut gehalten werden.
Wie man die Klasse nutzt, sieht man unten im Hauptprogramm, das
zunächst abfragt, ob das Modul als Programm aufgerufen wurde oder
ob es als externes Modul eines Programms dient.