Schere-Stein-Papier mit GUI

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Wundertüte
User
Beiträge: 2
Registriert: Sonntag 22. Mai 2022, 21:03

Hallo zusammen!

Habe gerade mit Python angefangen und mich am Anfängerklassiker "Schere-Stein-Papier" versucht.
Es funktioniert zwar prima, ist aber bestimmt Murks. Ich bin für alle Anregungen dankbar. Habe mich gerade ein bisschen in OOP eingelesen und versucht, dasselbe Programm im OOP-Stil zu schreiben. Mit bemerkenswertem Misserfolg. Macht das bei so einem Projekt überhaupt Sinn?
Wie dem auch sei, anbei der Code. Sieger ist, wer drei Runden "Schere-Stein-Papier" gewinnt.

Ps: Falls ihr beim nochmal spielen, ein "PermissionError: [Errno 13]" bekommt, ich musste das Programm auf "chmod 755" setzen.

beste Grüße
Wundertüte

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random
import tkinter as tk
import os
import sys

liste = ["schere", "stein", "papier"]
Spielerpunkte = 0
Computerpunkte = 0

def spieler_gewinnt():
    if Spielerpunkte == 3:
        child = tk.Tk()
        child.title("Nochmal?")
        tk.Label(child, text="Spielende, du hast gewonnen! Noch eine Runde?").pack()

        def neustart():
            sys.stdout.flush()
            os.execv(sys.argv[0], sys.argv)

        def schließen():
            child.destroy()
            root.destroy()

        tk.Button(child, text="Ja", command=neustart).pack()
        tk.Button(child, text="Nein, beenden", command=schließen).pack()


def computer_gewinnt():
    if Computerpunkte == 3:
        child = tk.Tk()
        child.title("Nochmal?")
        tk.Label(child, text="Spielende, ich hab gewonnen! Noch eine Runde?").pack()

        def neustart():
            sys.stdout.flush()
            os.execv(sys.argv[0], sys.argv)

        def schließen():
            child.destroy()
            root.destroy()

        tk.Button(child, text="Ja", command=neustart).pack()
        tk.Button(child, text="Nein, beenden", command=schließen).pack()


def okbutton():
    computerwahl = random.choice(liste)
    spielerwahl = e1.get()

    if spielerwahl.lower() == computerwahl:
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Unentschieden!").pack()

    if spielerwahl.lower() == "schere" and computerwahl == "papier":
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Du gewinnst").pack()
        global Spielerpunkte
        Spielerpunkte = Spielerpunkte + 1
        spieler_gewinnt()

    if spielerwahl.lower() == "schere" and computerwahl == "stein":
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Ich gewinne").pack()
        global Computerpunkte
        Computerpunkte = Computerpunkte + 1
        computer_gewinnt()

    if spielerwahl.lower() == "papier" and computerwahl == "schere":
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Ich gewinne").pack()
        Computerpunkte = Computerpunkte + 1
        computer_gewinnt()

    if spielerwahl.lower() == "papier" and computerwahl == "stein":
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Du gewinnst").pack()
        Spielerpunkte = Spielerpunkte + 1
        spieler_gewinnt()

    if spielerwahl.lower() == "stein" and computerwahl == "schere":
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Du gewinnst").pack()
        Spielerpunkte = Spielerpunkte + 1
        spieler_gewinnt()

    if spielerwahl.lower() == "stein" and computerwahl == "papier":
        tk.Label(root, text="Meine Wahl: " + computerwahl).pack()
        tk.Label(root, text="Ich gewinne").pack()
        Computerpunkte = Computerpunkte + 1
        computer_gewinnt()


root = tk.Tk()
XPOS = 500
YPOS = 200
root.geometry("+%d+%d" % (XPOS, YPOS))
root.title("Schere-Stein-Papier")
tk.Label(root, text="Wähle deine Waffe! Sieger ist, wer 3 Mal gewinnt!").pack()
e1 = tk.Entry()
e1.pack()

tk.Button(root, text="OK", command=lambda: [okbutton()]).pack()
root.bind("<Return>", lambda event: [okbutton()])

root.mainloop()
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Wundertüte: OOP macht Sinn weil man da bei so ziemlich jeder nicht-trivialen GUI-Anwendung gar nicht drum herum kommt. Denn ``global`` hat in einem Programm nichts zu suchen und auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Damit kommt man bei GUIs nicht darum herum sich Zustand über Aufrufe hinweg in einem Objekt zu merken.

Man kann hier vielleicht noch um eine eigene Klasse herum kommen, weil Tk Klassen anbietet in denen man einfache Werte wie ganze Zahlen kapseln kann, die man dann überall als Argumente durchreichen kann.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

`liste` ist als Name zu generisch, insbesondere wo das ja eigentlich eine Konstante ist die modulweit sichtbar ist.

Der `geometry()`-Aufruf um die Fensterposition absolut vorzugeben ist unschön. In erster Linie sollte das Fenstersystem einen guten, sinnvollen Platz für ein neues Fenster finden, und in zweiter Instanz möchte ich als Benutzer gerne selbst bestimmen wo ein Fenster zu sehen ist. Ich vermute mal das ist als Krücke gedacht, damit das Fenster nicht bei jeder Spielrunde woanders auftaucht, weil Du für jede Runde das Programm beendest und neu startest. Was man so nicht macht, also hätte man auch dieses Problem nicht.

Bei den beiden ``lambda``-Ausdrücken ist es sinnlos bis falsch den Aufruf in eine literale Liste zu stecken.

`e1` ist kein guter Name. Davon abgesehen das man keine Namen nummeriert, ist es ja noch mal sinnloser eine 1 anzuhängen wenn es nicht einmal ein `e2` gibt.

Da Funktionen und Methoden alles was sie ausser Konstanten benötigen, als Argument(e) übergeben bekommen, muss `okbutton()` die Objekte die die Punkte kapseln, das Eingabeelement, und ein Objekt das die Ausgaben sammelt, als Argumente. Für die Ausgaben kann man eine Liste verwenden. Man muss die aufbewahren, damit man sie für die nächste Spielrunde wieder entfernen kann.

`okbutton` wäre ein passender Name für ein `Button`-Objekt, wenn man den Unterstrich zwischen den Worten noch ergänzt, aber genau aus dem Grund kein guter Name für eine Funktion. Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt die sie durchführen. Eben um sie besser von eher passiven Werten unterscheiden zu können.

Statt die `spielerwahl` bei jedem Vergleich in Kleinbuchstaben zu wandeln, kann man das auch *einmal* nach dem Abfragen des `Entry`-Objekts machen.

Die ganzen ``if``-Abfragen schliessen sich gegenseitig aus, als braucht man die nicht alle ausführen wenn man einen Treffer hat, also ``elif`` verwendet. Das bietet zudem die Möglichkeit am Ende auch noch ein ``else`` anzufügen für den Fall, dass man beim Programmieren eine Bedingung vergessen hat, oder einen Fehler gemacht hat.

In *jedem* Zweig wird als erstes die Wahl vom Computer angezeigt. Das macht man einmal *vor* dem Konstrukt und nicht in jedem Zweig.

Man muss nicht für jeden Test ein eigenes ``if`` oder ``elif`` schreiben. Und im Grunde muss man auch nur die Tests für eine Seite, Spieler oder Computer, tatsächlich machen, nachdem klar ist, dass es kein Unentschieden ist. Denn wenn es kein Unentschieden ist und keiner der Tests für den Spieler als Gewinner zutrifft, bleibt ja nur noch übrig das der Computer gewonnen hat.

Man kann die Tests auch etwas kompakter als durch ``or``-Verknüpfungen zusammenfassen in dem man per ``in`` prüft ob das Paar der Auswahlen in einer Liste aus ”Gewinnerpaarungen” enthalten ist.

`spieler_gewinnt()` und `computer_gewinnt()` sind nahezu identisch. Wenn man so ähnliche Funktionen hat, dann ist das eigentlich eine, die entsprechend parametrisiert wird.

Das ``sys.stdout.flush()`` macht keinen Sinn bei einem Programm das überhaupt gar keine Konsolenausgaben macht.

Es darf nur ein `Tk`-Objekt geben. Wenn man davon mehr erstellt kann alles Mögliche passieren. Zusätzliche Fenster erstellt man mit `tk.Toplevel`. Man könnte aber auch `askyesno()` aus `tkinter.messagebox` verwenden. Dann wird das auch so simpel, das man dafür nicht unbedingt noch eine weitere Funktion braucht.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import random
import tkinter as tk
from functools import partial
from tkinter.messagebox import askyesno

MOEGLICHE_ZUEGE = ["schere", "stein", "papier"]


def spiel_initialisieren(spielerpunkte, computerpunkte, ausgaben):
    spielerpunkte.set(0)
    computerpunkte.set(0)
    for widget in ausgaben:
        widget.destroy()
    ausgaben.clear()


def ausgeben(ausgabe_widget, ausgaben, text):
    label = tk.Label(ausgabe_widget, text=text)
    label.pack()
    ausgaben.append(label)


def eingabe_auswerten(
    eingabe,
    ausgabe_widget,
    ausgaben,
    spielerpunkte,
    computerpunkte,
    _event=None,
):
    computerwahl = random.choice(MOEGLICHE_ZUEGE)
    spielerwahl = eingabe.get().lower()

    ausgeben(ausgabe_widget, ausgaben, f"Meine Wahl: {computerwahl}")

    if spielerwahl == computerwahl:
        ausgeben(ausgabe_widget, ausgaben, "Unentschieden!")

    elif (spielerwahl, computerwahl) in [
        ("schere", "papier"),
        ("papier", "stein"),
        ("stein", "schere"),
    ]:
        ausgeben(ausgabe_widget, ausgaben, "Du gewinnst.")
        spielerpunkte.set(spielerpunkte.get() + 1)

    else:
        ausgeben(ausgabe_widget, ausgaben, "Ich gewinne.")
        computerpunkte.set(computerpunkte.get() + 1)

    if 3 in [spielerpunkte.get(), computerpunkte.get()]:
        text = "Du hast" if spielerpunkte.get() == 3 else "ich habe"
        if askyesno(
            "Nochmal?", f"Spielende, {text} gewonnen. Noch eine Runde?"
        ):
            spiel_initialisieren(spielerpunkte, computerpunkte, ausgaben)
        else:
            eingabe.quit()


def main():
    root = tk.Tk()
    root.title("Schere-Stein-Papier")

    spielerpunkte = tk.IntVar()
    computerpunkte = tk.IntVar()
    ausgaben = []
    spiel_initialisieren(spielerpunkte, computerpunkte, ausgaben)

    tk.Label(
        root, text="Wähle deine Waffe! Sieger ist, wer 3 Mal gewinnt!"
    ).pack()
    eingabe = tk.Entry()
    eingabe.pack()

    action = partial(
        eingabe_auswerten,
        eingabe,
        root,
        ausgaben,
        spielerpunkte,
        computerpunkte,
    )
    tk.Button(root, text="OK", command=action).pack()
    root.bind("<Return>", action)

    root.mainloop()


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Beim Programmieren versucht man Wiederholungen zu vermeiden. `spieler_gewinnt` und `computer_gewinnt` sind quasi identisch und in `okbutton` hast Du fast sieben identische if-Blöcke.
Variablen schreibt man generell klein, Konstanten dagegen KOMPLETT_GROSS.
`liste` ist ein sehr genereller Name, sowas wie `AUSWAHL` wäre besser.
Was soll die 1 bei `e1`? Warum nichts sprechendes wie `eingabe`?
Warum erzeugst Du mit einem Lambda-Ausdruck eine Liste, deren einziges Element immer None ist, die Du aber gar nirgends verwendest? command kann man auch die Funktion `okbutton` direkt übergeben.

Code: Alles auswählen

import random
import tkinter as tk
import os
import sys

AUSWAHL = ["schere", "stein", "papier"]
XPOS = 500
YPOS = 200

def gewinnt(text):
    child = tk.Tk()
    child.title("Nochmal?")
    tk.Label(child, text=f"Spielende, {text}! Noch eine Runde?").pack()

    def neustart():
        sys.stdout.flush()
        os.execv(sys.argv[0], sys.argv)

    def schließen():
        child.destroy()
        root.destroy()

    tk.Button(child, text="Ja", command=neustart).pack()
    tk.Button(child, text="Nein, beenden", command=schließen).pack()

def okbutton(event=None):
    computerwahl = random.choice(AUSWAHL)
    spielerwahl = eingabe.get().lower()
    tk.Label(root, text=f"Meine Wahl:{computerwahl}").pack()

    if spielerwahl == computerwahl:
        tk.Label(root, text="Unentschieden!").pack()
    elif (spielerwahl, computerwahl) in [
            ("schere", "stein"),
            ("papier", "schere"),
            ("stein", "papier"),
        ]:
        tk.Label(root, text="Ich gewinne").pack()
        global computerpunkte
        computerpunkte += 1
        if computerpunkte == 3:
            gewinnt("ich hab gewonnen")
    else:
        tk.Label(root, text="Du gewinnst").pack()
        global spielerpunkte
        spielerpunkte += 1
        if spielerpunkte == 3:
            gewinnt("du hast gewonnen")


spielerpunkte = 0
computerpunkte = 0
root = tk.Tk()
root.geometry("+%d+%d" % (XPOS, YPOS))
root.title("Schere-Stein-Papier")
tk.Label(root, text="Wähle deine Waffe! Sieger ist, wer 3 Mal gewinnt!").pack()
eingabe = tk.Entry()
eingabe.pack()

tk.Button(root, text="OK", command=okbutton).pack()
root.bind("<Return>", okbutton)
root.mainloop()
`os.execv` ist etwas sehr low-level. Wenn man ein Spiel von vorne beginnen will, dann setzt man einfach den Zustand des Spiels zurück. Hier ja sehr einfach, indem man die Punkte wieder auf 0 setzt. Es darf nur ein Instanz von Tk im gesamten Programm geben, weitere Fenster macht man mit Toplevel, hier möchte man aber eine Modalen Dialog haben:

Code: Alles auswählen

def gewinnt(text):
    result = messagebox.askquestion("Nochmal?", f"Spielende, {text}! Noch eine Runde?")
    if result == 'yes':
        global computerpunkte, spielerpunkte
        computerpunkte = spielerpunkte = 0
    else:
        root.destroy()
Globale Variablen darf es nicht geben, alles was Funktionen brauchen, müssen sie über ihre Argumente bekommen. Da Du Dir aber den Spielstand über das Ende der Funktion hinaus merken mußt, braucht es Klassen:

Code: Alles auswählen

import random
import tkinter as tk
from tkinter import messagebox

AUSWAHL = ["schere", "stein", "papier"]

class Game(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.spielerpunkte = 0
        self.computerpunkte = 0
        self.title("Schere-Stein-Papier")
        tk.Label(self, text="Wähle deine Waffe! Sieger ist, wer 3 Mal gewinnt!").pack()
        self.eingabe = tk.Entry(self)
        self.eingabe.pack()

        tk.Button(self, text="OK", command=self.okbutton).pack()
        self.bind("<Return>", self.okbutton)

    def gewinnt(self, text):
        result = messagebox.askquestion("Nochmal?", f"Spielende, {text}! Noch eine Runde?")
        if result == 'yes':
            self.computerpunkte = self.spielerpunkte = 0
        else:
            self.destroy()

    def okbutton(self, event=None):
        computerwahl = random.choice(AUSWAHL)
        spielerwahl = self.eingabe.get().lower()
        tk.Label(self, text=f"Meine Wahl:{computerwahl}").pack()

        if spielerwahl == computerwahl:
            tk.Label(self, text="Unentschieden!").pack()
        elif (spielerwahl, computerwahl) in [
                ("schere", "stein"),
                ("papier", "schere"),
                ("stein", "papier"),
            ]:
            tk.Label(self, text="Ich gewinne").pack()
            self.computerpunkte += 1
            if self.computerpunkte == 3:
                self.gewinnt("ich hab gewonnen")
        else:
            tk.Label(self, text="Du gewinnst").pack()
            self.spielerpunkte += 1
            if self.spielerpunkte == 3:
                self.gewinnt("du hast gewonnen")

def main():
    game = Game()
    game.mainloop()

if __name__ == "__main__":
    main()
Ein Fenster wird einmal mit all seinen Komponenten erzeugt. Es ist unüblich später weitere hinzuzufügen, da das die Position der Elemente verschiebt, was den Nutzer verwirrt.

Code: Alles auswählen

import random
import tkinter as tk
from tkinter import messagebox

AUSWAHL = ["schere", "stein", "papier"]

class Game(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.spielerpunkte = 0
        self.computerpunkte = 0
        self.title("Schere-Stein-Papier")
        self.eingabe = tk.StringVar(self)
        self.stand = tk.StringVar(self, '0:0')
        self.wahl = tk.StringVar(self)
        self.meldung = tk.StringVar(self)
        tk.Label(self, text="Wähle deine Waffe! Sieger ist, wer 3 Mal gewinnt!").pack()
        tk.Entry(self, textvariable=self.eingabe).pack()
        tk.Button(self, text="OK", command=self.okbutton).pack()
        self.bind("<Return>", self.okbutton)
        tk.Label(self, textvariable=self.wahl).pack()
        tk.Label(self, textvariable=self.meldung).pack()
        tk.Label(self, text="Stand").pack()
        tk.Label(self, textvariable=self.stand).pack()

    def setze_punkte(spielerpunkte, computerpunkte):
        self.spielerpunkte = spielerpunkte
        self.computerpunkte = computerpunkte
        self.stand.set(f"{self.spielerpunkte}:{self.computerpunkte}")
        if self.computerpunkte == 3:
            self.after(50, self.gewinnt, "ich hab gewonnen")
        elif self.spielerpunkte == 3:
            self.after(50, self.gewinnt, "du hast gewonnen")

    def gewinnt(self, text):
        result = messagebox.askquestion("Nochmal?", f"Spielende, {text}! Noch eine Runde?")
        if result == 'yes':
            self.setze_punkte(0,0)
        else:
            self.quit()

    def okbutton(self, event=None):
        computerwahl = random.choice(AUSWAHL)
        spielerwahl = self.eingabe.get().lower()
        self.wahl.set(f"Meine Wahl:{computerwahl}")

        if spielerwahl == computerwahl:
            self.meldung.set("Unentschieden!")
        elif (spielerwahl, computerwahl) in [
                ("schere", "stein"),
                ("papier", "schere"),
                ("stein", "papier"),
            ]:
            self.meldung.set("Ich gewinne")
            self.setze_punkte(self.spielerpunkte, self.computerpunkte + 1)
        else:
            self.meldung.set("Du gewinnst")
            self.setze_punkte(self.spielerpunkte + 1, self.computerpunkte)

def main():
    game = Game()
    game.mainloop()

if __name__ == "__main__":
    main()
Es fehlen noch ein paar Kleinigkeiten: Prüfen, ob die Eingabe in der AUSWAHL enthalten ist und eine entsprechende Meldung ausgeben.
Wundertüte
User
Beiträge: 2
Registriert: Sonntag 22. Mai 2022, 21:03

Vielen Dank euch beiden, dass ihr euch die Zeit genommen habt!
Ich habe mir eure Verbesserungen und Anmerkungen genau angesehen und kann nicht alle nachvollziehen, da gibt es noch eine Menge zu lernen!
Danke auch, dass ihr den Code so umgeschrieben habt, dass ich die Auswirkung eines OOP-Codes anhand dieses Beispiels im direkten Vergleich habe. Habt ihr vielleicht noch die ein oder andere Lektüreempfehlung für mich, die meinem derzeitigen Kenntnisstand entspricht (vor allem im Bezug auf OOP?)

Eine schöne Restwoche wünscht,
Wundertüte
Antworten