Python Tk-Codeknacker
Willemers Informatik-Ecke
Codeknacker Python-Kurs Tkinter

Zur Begleitung des Python-Kurses wird das Spiel Codeknacker implementiert. Zuletzt wurde es in einer Klasse zusammengefasst, die nun benutzt werden kann, um in einem Fenster gespielt zu werden.

Als grafische Oberfläche wird die Tk-Version tkinter verwendet, die quasi den Standard für Python darstellt.

Erstellung eines Fensters

Die hier vorgestellte Version ist recht einfach gehalten. Der Spieler tippt seinen vierstelligen Versuch ein, klickt auf den Button und erhält eine Bewertung der Treffer. Damit er sich das nicht merken muss, werden diese in einer Liste abgelegt, damit der Spieler aus den bisherigen Versuchen kombinieren kann, welche Geheimzahl sich der Computer wohl ausgedacht hat.

Wir benötigen ein Fenster mit mehreren Kontrollementen.

Das fertige Fenster sähe etwa so aus:

Das lässt sich durch folgendes Programm herstellen:

import tkinter

fenster = tkinter.Tk()
fenster.title("Code-Knacker")

ctrlframe = tkinter.Frame(fenster)
ctrlframe.pack(expand=True, fill=tkinter.X)
eingabe = tkinter.Entry(ctrlframe)
eingabe.pack(expand=True, side=tkinter.LEFT, fill=tkinter.BOTH)
knopf = tkinter.Button(ctrlframe, text = "Raten")
knopf.pack(expand=False, side=tkinter.LEFT)
label = tkinter.Label(ctrlframe, text="0")
label.pack(expand=False, side=tkinter.LEFT)

listframe = tkinter.Frame(fenster)
scrollbar = tkinter.Scrollbar(listframe)
scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
listbox = tkinter.Listbox(listframe, yscrollcommand=scrollbar.set)
listbox.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH)
scrollbar.config(command=listbox.yview)
listframe.pack(expand=True, fill=tkinter.X)

fenster.mainloop()

Das sieht schon gut aus, tut aber nichts. Man kann eingeben, was man will, kann den Button drücken. Es passiert nichts. Nur das Anklicken des Kreuzes rechts oben führt zum Ende des Programms. Andererseits ist das Ergebnis für ein Programm von rund 20 Zeilen ja schon recht ansehnlich.

Aktion durch den Button

Das starre Fenster muss in Bewegung geraten. Die Eingabe in das Entry kann bereits erfolgen. Das Auslesen sollte dann passieren, wenn der Button gedrückt wird.

Für die Reaktion auf den Button wird eine sogenannte Callback-Funktion definiert. Sie wird vom Fenster zurückgerufen, wenn der Button gedrückt wird.

Damit der Button auch weiß, dass er eine Callback-Funktion hat, wird diese bei Aufruf des Konstruktors als Parameter command übergeben.

def btnRead():
    txt = eingabe.get()

knopf = tkinter.Button(ctrlframe, text = "Raten", command=btnRead)

Die Callback-Funktion übernimmt die zentrale Rolle im Spielablauf.

import tkinter

import codeknacker

ratezahl = 0
listbox = None
ck = codeknacker.CodeKnacker()

def btnRead():
    global ratezahl, ck, listbox
    txt = eingabe.get()
    eingabe.delete(0, tkinter.END)
    pos, enth = ck.vergleiche(txt)
    ratezahl += 1
    listeneintrag = "{} Pos: {} In: {}".format(txt, pos, enth)
    listbox.insert(tkinter.END, listeneintrag)
    label["text"] = "{} {}".format(ck.geheim,str(ratezahl))

fenster = tkinter.Tk()
fenster.title("Code-Knacker")
ctrlframe = tkinter.Frame(fenster)
ctrlframe.pack(expand=True, fill=tkinter.X)
eingabe = tkinter.Entry(ctrlframe)
eingabe.pack(expand=True, side=tkinter.LEFT, fill=tkinter.BOTH)
knopf = tkinter.Button(ctrlframe, text = "Raten", command=btnRead)
knopf.pack(expand=False, side=tkinter.LEFT)
label = tkinter.Label(ctrlframe, text="0")
label.pack(expand=False, side=tkinter.LEFT)

listframe = tkinter.Frame(fenster)
scrollbar = tkinter.Scrollbar(listframe)
scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
listbox = tkinter.Listbox(listframe, yscrollcommand=scrollbar.set)
listbox.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH)
scrollbar.config(command=listbox.yview)
listframe.pack(expand=True, fill=tkinter.X)

fenster.mainloop()
Da einige Variablen außerhalb der Callback-Funktion in dieser verändert werden, müssen diese gleich zu Anfang global gekennzeichnet werden.

Siegerehrung

Das Spiel ist gewonnen, wenn alle vier Ziffern an der richtigen Position sind. Dazu muss in btnRead das Ergebnis von pos mit 4 verglichen werden.

    if pos==4:
        tkmsg.Message(master=fenster,message="Gewonnen in {} Versuchen".format(ratezahl)).show()
        ck = codeknacker.CodeKnacker()
        ratezahl = 0
        listbox.delete(0,tkinter.END)
        label["text"] = "{} {}".format(ck.geheim,str(ratezahl))

Eingabe mit Drops

Problematisch ist die Eingabe. Der Benutzer kann statt eines vierstelligen Zifferncode zwischen 1 und 6 seinen Namen oder zuwenige Ziffern eintippen. In jedem dieser Fälle stürzt das Programm ab. Natürlich kann man versuchen die Fehler abzufangen und den Benutzer ermahnen, seine Rolle doch bitte ernst zu nehmen.

Mit der Hilfe von Drop-Down-Elementen oder Comboboxen kann man erreichen, dass der Benutzer gar nichts anderes eingeben kann. Man stellt vier Comboboxen auf, füllt sie mit den Ziffern von 1 bis 6 und sorgt für eine Vorauswahl. Und schon gibt es keine Eingabefehler mehr.

Ein schöner Nebeneffekt: Das Spiel wird allein durch die Maus bedient oder - bei einem Touch-Screen nur durch den Finger.

Einbau der Comboboxen

Für die Verwendung einer Combobox muss ttk von tkinter importiert werden. Dann kann eine Combobox erstellt werden, genau wie ein anderes Kontrollelement. Wir brauchen vier davon, also bauen wir die Erzeugung in eine Schleife ein.

Als Parameter wird state auf readonly geändert, so dass der Benutzer nichts eintippen, sondern nur aus den Vorgaben auswählen kan. Die Breite width wird etwas verknappt, damit sie nicht so viel Platz verbrauchen und schließlich wird als values eine Liste der sechs möglichen Ziffern übergeben.

Der Aufruf von current sorgt dafür, dass die erste Auswahl schon selektiert ist. Danach kann mit der Combobox nur noch ein gültiges Element ausgewählt werden.

Und natürlich müssen die vier Elemente zusammengepackt werden.

ziffern = ["1", "2", "3", "4", "5", "6"]
combo = []
for i in range(4):
    combo.append(ttk.Combobox(ctrlframe, width=2,
             state="readonly", values=ziffern))
    combo[i].current(0)
    combo[i].pack(expand=False, side=tkinter.LEFT)

Im Callback

In der Callback-Funktion des Buttons müssen die ausgewählten Combobox-Inhalte zu einem String zusammengesetzt werden. Danach kann man genauso fortfahren wie bei der Eingabe per Entry.

def btnRead():
    global ratezahl, ck, listbox
    ratestr = ""
    for i in range(4):
        ratestr += combo[i].get()
Im Gegensatz zu der Methode current liefert die Methode get den String, der in der Selektion steht. Wenn man alle vier aneinander hängt, erhält man den Ratestring, so wie man ihn an die vergleich-Methode der Klasse CodeKnacker senden kann.

Und nun alles zusammen

Hier setzen wir alles zusammen. Das Programm ist gar nicht erheblich länger geworden.
import tkinter
import tkinter.messagebox as tkmsg
from tkinter import ttk
import codeknacker

ratezahl = 0
listbox = None
ck = codeknacker.CodeKnacker()

def btnRead():
    global ratezahl, ck, listbox
    ratestr = ""
    for i in range(4):
        ratestr += combo[i].get()
    pos, enth = ck.vergleiche(ratestr)
    ratezahl += 1
    listeneintrag = "{} Pos: {} In: {}".format(ratestr, pos, enth)
    listbox.insert(tkinter.END, listeneintrag)
    label["text"] = "{} {}".format(ck.geheim,str(ratezahl))
    # Für die entgülte Spielversion ändern in
    #label["text"] = str(ratezahl)
    if pos==4:
        tkmsg.Message(master=fenster,
             message="Gewonnen in {} Versuchen".format(ratezahl)).show()
        ck = codeknacker.CodeKnacker()
        ratezahl = 0
        listbox.delete(0,tkinter.END)
        label["text"] = "{} {}".format(ck.geheim,str(ratezahl))

fenster = tkinter.Tk()
fenster.title("Code-Knacker")
ctrlframe = tkinter.Frame(fenster)
ctrlframe.pack(expand=True, fill=tkinter.X)
ziffern = ["1", "2", "3", "4", "5", "6"]
combo = []
for i in range(4):
    combo.append(ttk.Combobox(ctrlframe, width=2,
             state="readonly", values=ziffern))
    combo[i].current(0)
    combo[i].pack(expand=False, side=tkinter.LEFT)
knopf = tkinter.Button(ctrlframe, text = "Raten", command=btnRead)
knopf.pack(expand=False, side=tkinter.LEFT)
label = tkinter.Label(ctrlframe, text="0")
label.pack(expand=False, side=tkinter.LEFT)

listframe = tkinter.Frame(fenster)
scrollbar = tkinter.Scrollbar(listframe)
scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
listbox = tkinter.Listbox(listframe, yscrollcommand=scrollbar.set)
listbox.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH)
scrollbar.config(command=listbox.yview)
listframe.pack(expand=True, fill=tkinter.X)

fenster.mainloop()

Und so sieht das Spiel auf dem Bildschirm aus: