Der Python-Kurs: Codeknacker
Willemers Informatik-Ecke
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:

Mit diesen Hilfen an der Hand versucht der Spieler, sich dem richtigen Geheimcode zu nähern. Hat er die richtige Kombination gefunden, endet das Spiel.

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.

Bevor Sie die Lösung eintippen, sollten Sie erst einmal selbst versuchen. Man lernt Programmieren nur dadurch, dass man es tut.
#!/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.

Vielleicht sollten Sie nun schon einmal knobeln, ob Ihnen eine Lösung einfällt.

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.

Weiter zu der GUI-Version von Code-Knacker