Einteilung von GUI und Spiellogik durch Klassen

Fragen zu Tkinter.
Antworten
Sergeant
User
Beiträge: 1
Registriert: Donnerstag 10. Oktober 2019, 13:28

Donnerstag 10. Oktober 2019, 13:38

Code: Alles auswählen

from random import randint
from tkinter import *


#Definitionen

def Button_Einsatz_Click():
    # Verwaltung der Daten
    ButtonEinsatz.config(state = DISABLED)
    radiobutton1.config(state = NORMAL)
    radiobutton2.config(state = NORMAL)
    radiobutton3.config(state = NORMAL)
    radiobutton4.config(state = NORMAL)
    radiobutton5.config(state = NORMAL)
    radiobutton6.config(state = NORMAL)
    global konto
    konto = int(labelKonto.cget('text'))
    # Verarbeitung der Daten
    konto = konto - 1
    # Anzeige der Daten
    labelKonto.config(text=str(konto))
    
    
def Button_Würfel_Click():
    # Verwaltung der Daten
    ButtonGewinn.config(state = NORMAL)
    ButtonWürfel.config(state = DISABLED)
    global Würfel1
    global Würfel2
    global Würfel3
    
    Würfel1 = int(label1Würfel.cget('text'))
    Würfel2 = int(label2Würfel.cget('text'))
    Würfel3 = int(label3Würfel.cget('text'))
    # Verarbeitung der Daten
    Würfel1 = randint(1, 6)
    Würfel2 = randint(1, 6)
    Würfel3 = randint(1, 6)
    # Anzeige der Daten
    label1Würfel.config(text=str(Würfel1))
    label2Würfel.config(text=str(Würfel2))
    label3Würfel.config(text=str(Würfel3))

def Button_Gewinn_Click():
    #Verwaltung der Daten
    ButtonEinsatz.config(state = NORMAL)
    ButtonGewinn.config(state = DISABLED)
    global labelAusgewaehlteZahl1
    labelAusgewaehlteZahl1 = labelAusgewaehlteZahl.cget("text")
    #Verarbeitung der Daten
    global konto
    global Würfel1
    global Würfel2
    global Würfel3
    
    if int(labelAusgewaehlteZahl1) == int(Würfel1) and int(labelAusgewaehlteZahl1) == int(Würfel2) and int(labelAusgewaehlteZahl1) == int(Würfel3):
        konto = konto + 3
        print(konto, "der Gewinn ist:3")
    elif int(labelAusgewaehlteZahl1) == int(Würfel1) and int(labelAusgewaehlteZahl1) == int(Würfel2) or int(labelAusgewaehlteZahl1) == int(Würfel1) and int(labelAusgewaehlteZahl1) == int(Würfel3) or int(labelAusgewaehlteZahl1) == int(Würfel2) and int(labelAusgewaehlteZahl1) == int(Würfel3):
        konto = konto + 2
        print(konto, "der Gewinn ist:2")
    elif int(labelAusgewaehlteZahl1) == int(Würfel1) or int(labelAusgewaehlteZahl1) == int(Würfel2) or int(labelAusgewaehlteZahl1) == int(Würfel3):
        konto = konto + 1
        print(konto, "der Gewinn ist:1")
    #Anzeige der Daten
    labelKonto.config(text=str(konto))

    
def Radiobutton_Click():
    # Verwaltung der Daten
    ButtonWürfel.config(state = NORMAL)
    radiobutton1.config(state = DISABLED)
    radiobutton2.config(state = DISABLED)
    radiobutton3.config(state = DISABLED)
    radiobutton4.config(state = DISABLED)
    radiobutton5.config(state = DISABLED)
    radiobutton6.config(state = DISABLED)
    zahl = spielzahl.get()
    # Anzeige der Daten
    labelAusgewaehlteZahl.config(text=str(zahl))

#------------------------------------------------------------------

#Die Grafik


# Erzeugung des Fensters
tkFenster = Tk()
tkFenster.title("Test")
tkFenster.geometry('800x500')
# Label mit Gesamtüberschrift
labelGesamtüberschrift = Label(master=tkFenster,
                                text="chuck a luck",
                                background="#E6E6FA")
labelGesamtüberschrift.place(x=0, y=0, width=800, height=30)
# Label mit Überschrift für das Konto
labelUeberschriftKonto = Label(master=tkFenster,
                                text="Konto",
                                background="#FFCFC9")
labelUeberschriftKonto.place(x=10, y=40, width=100, height=50)
# Label für den Kontostand
labelKonto = Label(master=tkFenster,
                    text="100",
                    background="#FFCFC9")
labelKonto.place(x=28, y=110, width=60, height=60)
# Label mit der Überschrift für die Würfel
labelUeberschriftWürfel = Label(master=tkFenster,
                                text="Würfel",
                                background="#FBD975")
labelUeberschriftWürfel.place(x=550, y=40, width=200, height=50)
# Label für den 1Würfel
label1Würfel = Label(master=tkFenster,
                    text="1",
                    background="#FBD975")
label1Würfel.place(x=550, y=110, width=60, height=60)
# Label für den 2Würfel
label2Würfel = Label(master=tkFenster,
                    text="2",
                    background="#FBD975")
label2Würfel.place(x=620, y=110, width=60, height=60)
# Label für den 3Würfel
label3Würfel = Label(master=tkFenster,
                    text="3",
                    background="#FBD975")
label3Würfel.place(x=690, y=110, width=60, height=60)

# Button zum Auswerten
ButtonEinsatz = Button(master=tkFenster,
                        text="Einsatz zahlen",
                        command=Button_Einsatz_Click)
ButtonEinsatz.place(x=10, y=180, width=100, height=20)
ButtonEinsatz.config(state = NORMAL)
#Button zum Würfel werfen
ButtonWürfel = Button(master=tkFenster,
                       text="Würfel werfen",
                       command=Button_Würfel_Click)
ButtonWürfel.config(state = DISABLED)
ButtonWürfel.place(x=600, y=180, width=100, height=20)

#Label mit Überschrift für die Zahl
labelUeberschriftZahl = Label(master=tkFenster,
                                text="Zahl",
                                background="#D5E88F")
labelUeberschriftZahl.place(x=200, y=40, width=300, height=50)

# Label mit ausgewählter Zahl
labelAusgewaehlteZahl = Label(master=tkFenster,
                              text="",
                              background="white")
labelAusgewaehlteZahl.place(x=335, y=145, width=30, height=18)

# Radiobutton für die Zahl

spielzahl = IntVar()
radiobutton1 = Radiobutton(master=tkFenster,
                           text='1',
                           value=1,
                           variable=spielzahl,
                           command=Radiobutton_Click)
radiobutton1.config(state = DISABLED)
radiobutton1.place(x=200, y=100, width=30, height=18)
radiobutton2 = Radiobutton(master=tkFenster,
                           text='2',
                           value=2,
                           variable=spielzahl,
                           command=Radiobutton_Click)
radiobutton2.config(state = DISABLED)
radiobutton2.place(x=250, y=100, width=30, height=18)
radiobutton3 = Radiobutton(master=tkFenster,
                           text='3',
                           value=3,
                           variable=spielzahl,
                           command=Radiobutton_Click)
radiobutton3.config(state = DISABLED)
radiobutton3.place(x=300, y=100, width=30, height=18)
radiobutton4 = Radiobutton(master=tkFenster,
                           text='4',
                           value=4,
                           variable=spielzahl,
                           command=Radiobutton_Click)
radiobutton4.config(state = DISABLED)
radiobutton4.place(x=350, y=100, width=30, height=18)
radiobutton5 = Radiobutton(master=tkFenster,
                           text='5',
                           value=5,
                           variable=spielzahl,
                           command=Radiobutton_Click)
radiobutton5.config(state = DISABLED)
radiobutton5.place(x=400, y=100, width=30, height=18)
radiobutton6 = Radiobutton(master=tkFenster,
                           text='6',
                           value=6,
                           variable=spielzahl,
                           command=Radiobutton_Click)
radiobutton6.config(state = DISABLED)
radiobutton6.place(x=450, y=100, width=30, height=18)

#Button für Gewinn auszahlen
ButtonGewinn = Button(master=tkFenster,
                        text="Gewinn auszahlen",
                        command=Button_Gewinn_Click)
ButtonGewinn.config(state = DISABLED)
ButtonGewinn.place(x=300, y=180, width=100, height=20)


# Aktivierung des Fensters
tkFenster.mainloop()
Wir haben folgenden Arbeitsauftrag bekommen:
Bewertet werden die folgenden Aspekte:
1. Funktioniert das Spiel? (Chuck-a-luck oder eine eigene Entwicklung!)
2. Reagiert die GUI auf Klicks/Eingaben? (Verändert sich etwas?)
3. Gibt es eine saubere Trennung zwischen Spielelogik und GUI-Funktionen?
4. Wurde eine zweckmäßige, einfache Klassenstruktur verwendet?
Das Programm funktioniert soweit ganz gut doch wir bekommen die Einteilung der GUI und der Spiellogik in einer Klassenstruktur nicht hin. Wir bitten um Hilfe!
__deets__
User
Beiträge: 6614
Registriert: Mittwoch 14. Oktober 2015, 14:29

Donnerstag 10. Oktober 2019, 13:44

Ich sehe da nicht, das ihr etwas hinkriegen muesst. Da steht doch nur die Frage, ob der Code auf bestimmte Art beschaffen ist. Wurde eine zweckmaessig, einfache Klassenstruktur verwendet? Was denkst du?
Sirius3
User
Beiträge: 10763
Registriert: Sonntag 21. Oktober 2012, 17:20

Donnerstag 10. Oktober 2019, 15:53

@__deets__: ich denke, wenn nur das Spiel funktioniert (wobei funktionieren doch wohl Punkt 2 einschließt) dann gibt es halt nur eine schlechte Note, wenn man dagegen mit zweckmäßigen, einfachen Klassenstrukturen programmiert, dann gibt es eine guute Note.
Sirius3
User
Beiträge: 10763
Registriert: Sonntag 21. Oktober 2012, 17:20

Donnerstag 10. Oktober 2019, 15:59

@Sergeant: bevor man hier anfängt, Logik von GUI zu trennen, oder Klassen einzuführen, muß erst einmal der Code aufgeräumt werden, das bedeutet, den *-Import durch `import tkinter as tk` ersetzen; alle `global` zu entfernen und den Funktionen, alles was sie brauchen per Parameter übergeben; die Namenskonvention umsetzen, dass Funktionsnamen und Variablennamen klein_mit_unterstrich geschrieben werden; alles ab "Die Grafik" in eine Funktion packen, die `main` heißt und zum Schluß per `if __name__ == '__main__': main()` aufgerufen wird.

Gleichzeitig sollten die durchnummerierten Variablen durch Listen ersetzt werden, der Code dadurch per for-Schleifen von doppeltem Code entschlacken.

Weißt Du, was es bedeutet Logik von GUI zu trennen?
__deets__
User
Beiträge: 6614
Registriert: Mittwoch 14. Oktober 2015, 14:29

Donnerstag 10. Oktober 2019, 16:03

@Sirius3: ah, ich habe das irgendwie falsch interpretiert. Es klang nach "das ist der Code, das sind die Kritierien, genuegt der Code denen?". Aber wenn der selbst gebaut ist - klar. Dann ist er nicht so dolle...
Sirius3
User
Beiträge: 10763
Registriert: Sonntag 21. Oktober 2012, 17:20

Donnerstag 10. Oktober 2019, 16:21

@Sergeant: anderer Ansatz wäre, alles zu löschen und mit der Logik als Klasse neu anzufangen, ohne GUI. Dazu die Fragen beantworten:
Wie wird der Zustand des Spiels komplett beschrieben? Das sind die Attribute, die in __init__ angelegt werden müssen.
Wie kann der Zustand des Spiels geändert werden? Das sind die Methoden der Klasse.
Benutzeravatar
__blackjack__
User
Beiträge: 4467
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Donnerstag 24. Oktober 2019, 12:48

@Sergeant: Ergänzend zu dem was Sirius3 schon schrieb:

Der Code sieht verdächtig nach dem Stil von https://www.inf-schule.de aus. Der ist nicht gut, das sollte man sich nicht als Vorbild nehmen. Die mögen dort Sternchenimporte, Namen die sich nicht an die Python-Konventionen halten, ``global``, und `place()`. Auf der anderen Seite wird das dann auch gerne mal mit Klassenwahnsinn wie in Java vermischt. Also nicht entweder oder, sondern beides im gleichen Programm!

Im Grunde würde ich empfehlen das Programm als Prototypen zu betrachten und noch mal komplett neu anzufangen. Und zwar mit der Programmlogik, ohne GUI. Und erst wenn die Programmlogik als Klasse steht und funktioniert, da dann eine GUI drauf setzen. So kommt man gar nicht erst in die Versuchung GUI und Programmlogik zu vermischen.

Und in der GUI dann auch nicht `place()` verwenden. Das gezeigte Programm sieht bei dem Rechner an dem ich gerade sitze beispielsweise so aus:
Bild

Die Zahlen bei den Radio-Buttons sind halb abgeschnitten und „Gewinn auszahlen“ ist nicht komplett lesbar. Es nützt auch nichts da mehr absoluten Platz zu lassen — bei der Vielfalt von Anzeigegrössen und -auflösungen wird es immer irgendwo Probleme damit geben. Mit `pack()` oder `grid()`, gerne auch noch mal in `Frame`\s untergliedert, bekommt jedes Widget den Platz den es braucht, und man muss/sollte auch keine Fenstergesamtgrösse mehr vorgeben und hat dann auch nicht so viel überflüssigen Platz unten im Fenster.

Von der Bedienung her finde ich es negativ überraschend das man bei den Radiobuttons durch die erste Auswahl einer Zahl die Radiobuttons deaktiviert. Das ist so ganz und gar nicht das normale Verhalten das man von anderen Anwendung her kennt. Ich sehe auch nicht so ganz warum die überhaupt jemals deaktiviert sein müssen. Das grenzt an Gängelung des Benutzers. Das einzige was man per GUI erzwingen muss, ist das der Benutzer vor dem Würfeln einen Einsatz gemacht hat.

Der „Gewinn auszahlen“-Button ist auch nicht sinnvoll. Das ist ja das einzige was man an der Stelle machen kann. Dann kann das auch ohne extra Klick einfach automatisch passieren.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Benutzeravatar
__blackjack__
User
Beiträge: 4467
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Freitag 8. November 2019, 11:40

@Sergeant: Mir ist noch aufgefallen das das Konto nur einen, ich sag mal informierenden Character hat. Ich hätte da ja erwartet das man nicht mehr spielen kann wenn das Konto leer ist, aber das geht dann ja einfach beliebig weit ins negative weiter.

Hier mal ein Ansatz:

Code: Alles auswählen

#!/usr/bin/env python3
from functools import partial
from random import randint
import tkinter as tk
from tkinter.font import Font

DIE_FACE_COUNT = 6
FIRST_DIE_FACE_CODEPOINT = ord("⚀")


roll_die = partial(randint, 1, DIE_FACE_COUNT)


def roll_dice(count):
    return [roll_die() for _ in range(count)]


class ChuckALuck:
    def __init__(self, balance=100, die_count=3):
        self.balance = balance
        self.die_count = die_count
        self.stakes = 0

    def pay_stakes(self, amount=1):
        if self.balance < amount:
            raise ValueError("insufficient funds")
        if self.stakes != 0:
            raise ValueError("stakes already paid")
        self.balance -= amount
        self.stakes += amount

    def roll_dice(self, chosen_number):
        self.stakes = 0
        result = roll_dice(self.die_count)
        points = result.count(chosen_number)
        self.balance += points
        return result


class ChuckALuckFrame(tk.Frame):
    def __init__(self, parent, game):
        tk.Frame.__init__(self, parent)
        self.game = game

        self.balance_var = tk.IntVar()
        self.face_choice_var = tk.IntVar(value=1)

        tk.Label(self, text="chuck a luck", background="#E6E6FA").pack(
            fill=tk.X
        )

        frame = tk.Frame(self)
        frame.pack()

        header_options = dict(sticky=tk.NSEW, ipady=15, padx=5, pady=5)
        color = "#FFCFC9"
        tk.Label(frame, text="Konto", background=color).grid(
            column=0, row=0, **header_options
        )
        tk.Label(frame, textvariable=self.balance_var, background=color).grid(
            column=0, row=1
        )
        self.payment_button = tk.Button(
            frame, text="Einsatz zahlen", command=self.do_pay
        )
        self.payment_button.grid(column=0, row=2)

        color = "#D5E88F"
        tk.Label(frame, text="Zahl", background=color).grid(
            column=1, row=0, **header_options
        )

        choices_frame = tk.Frame(frame)
        choices_frame.grid(column=1, row=1)

        radiobutton_frame = tk.Frame(choices_frame)
        radiobutton_frame.pack()
        for face_value in range(1, DIE_FACE_COUNT + 1):
            tk.Radiobutton(
                radiobutton_frame,
                text=face_value,
                variable=self.face_choice_var,
                value=face_value,
            ).pack(side=tk.LEFT)

        color = "#E6C460"
        tk.Label(frame, text="Würfel", background=color).grid(
            column=2, row=0, **header_options
        )

        dice_frame = tk.Frame(frame)
        dice_frame.grid(column=2, row=1)

        dice_font = Font().copy()
        dice_font["size"] = dice_font.actual("size") * 3

        self.die_labels = list()
        for i in range(self.game.die_count):
            label = tk.Label(
                dice_frame,
                text=chr(FIRST_DIE_FACE_CODEPOINT),
                font=dice_font,
                foreground=color,
                background=color,
            )
            label.grid(
                row=0, column=i, sticky=tk.NSEW, ipadx=12, ipady=12, padx=2
            )
            self.die_labels.append(label)

        self.roll_button = tk.Button(
            frame, text="Würfel werfen", command=self.do_roll
        )
        self.roll_button.grid(column=2, row=2)

        self.update_display()

    def update_display(self):
        self.balance_var.set(self.game.balance)
        if self.game.stakes != 0:
            self.payment_button["state"] = tk.DISABLED
            self.roll_button["state"] = tk.NORMAL
        else:
            self.payment_button["state"] = (
                tk.NORMAL if self.game.balance > 0 else tk.DISABLED
            )
            self.roll_button["state"] = tk.DISABLED

    def do_pay(self):
        self.game.pay_stakes()
        self.update_display()

    def do_roll(self):
        chosen_number = self.face_choice_var.get()
        die_values = self.game.roll_dice(chosen_number)
        for die_value, die_label in zip(die_values, self.die_labels):
            die_label.configure(
                text=chr(FIRST_DIE_FACE_CODEPOINT + die_value - 1),
                foreground="white" if die_value == chosen_number else "black",
            )
        self.update_display()


def main():
    chuck_a_luck = ChuckALuck()

    root = tk.Tk()
    root.title("Chuck A Luck")
    chuck_a_luck_frame = ChuckALuckFrame(root, chuck_a_luck)
    chuck_a_luck_frame.pack()
    root.mainloop()


if __name__ == "__main__":
    main()
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Antworten