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 + 1Natü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] = -2Hier 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 geheimDie 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, drinWir 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, drinDas 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 kopieUm 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 trefferBeim 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.