[Anfänger] Mein erstes Projekt: Galgenmännchen

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Hart mit Bart
User
Beiträge: 25
Registriert: Dienstag 30. Oktober 2018, 12:05

Servus zusammen.

Ich habe vor ein paar Tagen einen vierstündigen Python-Videokurs für Anfänger durchgearbeitet und dachte mir dann, dass der beste Weg, um tiefer in die Materie einzusteigen, ein eigenes Projekt wäre. Da ich nicht auf ein bestimmtes Ziel hinarbeite, lerne ich also die Dinge, die sich im Laufe meines Projektes als notwendig herauskristallisieren.

Ein kleines Spiel schien mir ein guter erster Schritt. Irgendwie gibt es immer noch ein Feature, was sich sinnvoll einbauen lässt.

Was ich euch hier vorstelle, ist die lauffähige Version 1.0.0.0. Ob es bei so einem kleinen Projekt sinnvoll ist, da Versionsnummern einzuführen, sei mal dahingestellt. Schaden kann es aber auch nicht und falls das Projekt doch wider aller Erwartungen größer wird, ist es als Übersicht über die einzelnen Entwicklungsschritte sicherlich auch ganz hilfreich.

Anfangs hatte ich noch einige Probleme, die sich vor allem in den Feinheiten zeigten. Ich benutzte zu viele Datentypen (Strings, Listen, Sets), die ich hin und her konvertiert habe, um die gewünschte Funktionalität zu gewährleisten. Später konnte ich das deutlich vereinfachen, obwohl bestimmte Abschnitte bestimmt noch immer unnötig kompliziert sind.
Ich habe versucht das Ganze so sauber wie möglich zu programmieren. Dies betrifft Variablen- und Funktionsnamen, Kommentare und das, was man gemeinhin als "guten Stil" bezeichnet. Zumindest das, was ich für guten Stil halte. Mangels Erfahrung mag das stellenweise gründlich schiefgegangen sein.

Was ich mir von euch wünsche sind Kommentare zu praktisch allen Aspekten meines Scriptes. Ungünstige Variablennamen, verkomplizierte Prozesse, schlechter Kommentarstil oder schlechter Stil allgemein und quasi allem anderen. Wenn ihr mit Blick auf die Tatsache, dass ich erst seit ein paar Tagen dabei bin, auch etwas Nettes anzumerken habt, nehme ich das natürlich auch gerne an. Sowas hält nämlich die Motivation aufrecht. Wenn der Ton stimmt, und ich habe keinen Grund, etwas anderes anzunehmen, kann ich Kritik aber gut aushalten und setze das gerne um. Ich will ja letztendlich besser werden.
Fragen, warum ich etwas so und nicht anders gemacht habe, werde ich so gut es geht beantworten.
Auch Anregungen für künstige Features sind gerne gesehen.

Kurze Projektbeschreibung:
Zu Galgenmännchen gibt es nicht viel zu sagen. Das Script wählt einen zufälligen Begriff aus einer Datei und gibt ihn mit "_" anstelle von Buchstaben aus. Man rät so lange Buchstaben, bis man entweder 3 Fehlversuche oder das Wort erraten hat. Man kann sich auch Hinweise holen. Hinweise verraten einen zufälligen Buchstaben. Die Anzahl der Hinweise ist derzeit nicht begrenzt.

Mögliche künftige Features:
- verschiedene Kategorien
- ein GUI
- die Möglichkeit, selbst Kategorien hinzuzufügen

Das Script:

Code: Alles auswählen

import random, sys, codecs, re, string

version = "1.0.0.0"

def new_game():
    global word_to_guess, letters_guessed, i, fails, play, clean_word_to_guess
    # Liest eine zufällige Zeile aus der Datei "tiere.txt" und entfernt Zeilenumbrüche
    word_to_guess = random.choice(codecs.open("tiere.txt", "r", "utf-8").readlines()).strip("\n\r")  # -> raw_word_to_guess
    clean_word_to_guess = re.sub('[- ]', '', word_to_guess).lower()  # Bindestriche und Leerzeichen entfernen
    letters_guessed = ""  # Liste aller geratener Buchstaben
    i = 1  # Schleifenzähler
    fails = 0  # Anzahl der Fehlversuche
    play = True  # Spiel läuft
    print_word(letters_guessed)  # Erste Ausgabe


def print_word(letters):
    global clean_word_to_guess  # Vorher: "referenced before assignment"
    for letter in word_to_guess:  # Prüfen: Es wird ein Buchstabe zu viel ausgegeben
        # Wenn der Buchstabe ein Bidnestrich oder Leerzeichen ist ...
        if letter == "-" or letter == " ":
            sys.stdout.write(letter + " ")
        # Wenn der geratene Buchstabe im gesuchten Wort enthalten ist ...
        elif set(letters) & set(word_to_guess.lower()) and letter.lower() in letters_guessed:
            sys.stdout.write(letter + " ")
            # Eratenen Buchstaben aus dem Set löschen
            check_word = clean_word_to_guess.replace(letter.lower(), "")
            clean_word_to_guess = check_word
        else:
            # Anderenfalls Unterstrich ausgeben
            sys.stdout.write("_ ")


def is_valid(user_input):
    valid_chars = "abcdefghijklmnopqrstuvwxyz1"  # String mit allen validen Zeichen
    if len(user_input) > 1 or user_input.lower() not in valid_chars:  # Wenn die Eingabe mehr als ein Zeichen hat oder nicht valide ist
        return False
    else:
        return True


print("Viel Spaß bei Galgenmännchen v" + version +"\n")  # Beim ersten Start Willkommensnachricht und Versionsnummer ausgeben
new_game()  # Initialisiert die Variablen für ein neues Spiel
while True:
    print("\nFehlversuche: " + str(fails))
    print("(1) eingeben, um einen Hinweis zu bekommen")
    # Buchstaben eingeben und zur Liste "letters_guessed" hinzufügen
    user_letter = input("Geben sie einen Buchstaben ein (" + str(i) +". Versuch): ").lower()
    print()
    if is_valid(user_letter):  # Wenn der Buchstabe valide ist ...
        if user_letter in clean_word_to_guess:
            letters_guessed = letters_guessed + user_letter
            i += 1
        else:
            if user_letter == "1":
                hint = random.choice(clean_word_to_guess)
                letters_guessed = letters_guessed + hint
                check_word = clean_word_to_guess.replace(hint.lower(), "")
                clean_word_to_guess = check_word
            else:
                fails += 1
        i += 1
    else:
        print("Fehler: Das ist keine valide Eingabe!\n")
    print_word(letters_guessed)  # Liste aller geratener Buchstaben an print_word übergeben
    if fails == 3 or len(clean_word_to_guess) == 0:
        print()
        if fails == 3:
            print("Das war dein dritter Fehlversuch. Du hast verloren.")
            print("Das gesuchte Wort war " + word_to_guess)
        else:
            print("Herzlichen Glückwunsch! Du hast das Wort erraten.")
        keep_playing = input("Möchtest du nochmal spielen (j/n)? ")
        # Wenn eines neues Spiel gestartet werden soll ...
        if keep_playing == "j":
            print()
            new_game()
        else:
            break
Die Datei "tiere.txt" könnt ihr entweder selbst anlegen oder ich kann, bei Bedarf, meine zur Verfügung stellen. Diese enthält aktuell 535 Einträge.

Mit leicht nervösen Grüßen,

Hart mit Bart
Sirius3
User
Beiträge: 18054
Registriert: Sonntag 21. Oktober 2012, 17:20

@Hart mit Bart: vergiss gleich mal wieder, dass es soetwas wie »global« gibt. Alles was eine Funktion braucht, bekommt sie über ihre Argumente, und alle Ergebnisse liefert sie per »return« zurück. Deine „Funktionen” sind nur benannte Sprungmarken, das führt aber nur zu unlesbarem Code. Das bedeutet aber leider, dass Du das gesamte Programm neu schreiben mußt.

Der Aufbau sollte so aussehen: ein Hauptprogramm startet ein Spiel (Funktion play_game) und wenn das Spiel zu Ende ist, kommt erst die Abfrage, ob man nochmal spielen will. »new_game« das eigentlich nur dazu da sein sollte, ein neues Wort zu ermitteln, darf also nur einmal im gesamten Code aufgerufen werden. Ebenso sollte es nur eine Stelle geben, wo »print_word« aufgerufen wird, und das sollte auch nur das Wort ausgeben, und nicht auch noch einen Teil der Prüfung auf das korrekte Wort machen.

Wenn Du das verbessert hast, kann man sich noch die Feinheiten anschauen. »string« wird importiert aber nicht verwendet, »codecs« ist in Python3 überflüssig, Dateien, die man öffnet sollte man auch wieder schlißen, statt sys.stdout baut man sich erst den String zusammen und gibt ihn einfach mit »print« aus. Diese set-Abfrage ist unnötig, statt Strings mit + zusammenzustückeln nimmt man Stringformatierung.

Die Variablenbenennung ist vorbildlich. Die Kommentare sind zu viel. Ein guter Variablenname sagt doch schon, für was er da ist, das muß man nicht noch kommentieren. Eigentlich ist kein Kommentar nötig, weil er nur beschreibt, was in dieser Zeile steht. Kommentare sollten das wie oder warum beschreiben.
Hart mit Bart
User
Beiträge: 25
Registriert: Dienstag 30. Oktober 2018, 12:05

Okay. Global gibt es nicht. Kenne ich nicht. Will ich nicht. Zugegeben, das war ein schmutziger kleiner Workaround, wie du dir vermutlich gleich gedacht hast.

Den Aufbau mit Funktionen fand ich eigentlich ganz elegant, weil es das (offenbar nur für mich) übersichtlicher gemacht hat. Das gilt wohl nur für den allerkleinsten Rahmen, bevor die Übersichtlichkeit implodiert. Im Sinne des Erfinders ist das so nicht, das kann ich nachvollziehen.

Das Programm neu zu schreiben ist kein Problem. Die Scriptgröße ist ja noch übersichtlich.
statt sys.stdout baut man sich erst den String zusammen und gibt ihn einfach mit »print« aus
Äh ... ich kann gerade nicht nachvollziehen, warum ich das nicht gemacht habe. Zu kompliziert gedacht vielleicht.

Okay, weniger Kommentare. Und andere Kommentare.

Alles in allem kann ich das alles gut nachvollziehen und stimme überein. Ich setze mich da nochmal ran.

Danke dir für deine Mühe. :)

Edit:
Diese set-Abfrage ist unnötig, statt Strings mit + zusammenzustückeln nimmt man Stringformatierung.
Die Set-Abfrage soll prüfen, ob ein beliebiger Buchstabe in aus String1 in String2 enthalten ist. Ich weiß gerade spontan nicht, wie ich das ohne Set lösen kann. Da muss ich nochmal rumsuchen.
Hart mit Bart
User
Beiträge: 25
Registriert: Dienstag 30. Oktober 2018, 12:05

Da ich meinen letzten Beitrag nicht mehr editieren kann, also ein neuer:

Wenn es keine eingebaute (oder zumindest etablierte) Methode gibt, um zu prüfen, ob ein beliebiges einzelnes(!) Zeichen eines ersten Strings Teil in einem anderen String vorkommt, könnte ich dazu auch selbst eine Funktion bauen.

Code: Alles auswählen

def compare_strings(first_string, second_string):
    for i in range(len(first_string)):
        for j in range(len(second_string)):
            if first_string[i] == second_string[j]:
                return True
Damit wären die Sets vom Tisch und die entsprechende Abfrage sauberer realisierbar. *grübel*
Benutzeravatar
snafu
User
Beiträge: 6779
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hart mit Bart hat geschrieben: Sonntag 4. November 2018, 14:30 Wenn es keine eingebaute (oder zumindest etablierte) Methode gibt, um zu prüfen, ob ein beliebiges einzelnes(!) Zeichen eines ersten Strings Teil in einem anderen String vorkommt, könnte ich dazu auch selbst eine Funktion bauen.
Warum baust du set-Funktionalität nach? Das ist viel mehr Code, es ist potenziell langsamer und fehleranfälliger. Ich hätte die Variante gelassen, die du vorher hattest. Oder hat das nun einen Mehrwert für dich?

Und wenn man es schon selber bauen will:

Code: Alles auswählen

def has_any_char(text, chars):
    return any(char in text for char in chars)
Sirius3
User
Beiträge: 18054
Registriert: Sonntag 21. Oktober 2012, 17:20

Das meinte ich nicht.
Die Zeile

Code: Alles auswählen

elif set(letters) & set(word_to_guess.lower()) and letter.lower() in letters_guessed:
wobei hier `letters`, das als Argument in `print_word` kommt immer identisch ist mit `letters_guessed`, das global ist, ist die einzige mit `set`. Der erste Teil prüft, ob letters und word_to_guess gemeinsame Buchstaben haben und danach wird nochmal explizit geprüft, ob ein konkreter Buchstabe `letter` aus word_to_guess in `letters` enthalten ist. Ist also der zweite Teil wahr, ist immer automatisch auch der erste Teil war, den ersten Teil kann man daher ohne Auswirkungen einfach weg lassen.

Zu Deinem `compare_string`: über Indize zu iterieren, macht man in Python höchst selten, weil man direkt über die Elemente iterieren kann:

Code: Alles auswählen

def compare_strings(first_string, second_string):
    for first_character in first_string:
        for second_character in second_string:
            if first_character == second_character:
                return True
Jetzt fehlt noch ein explizites »return False« statt des impliziten None, das Du zurücklieferst.
Die innere for-Schleife kann man durch den `in`-Operator ausdrücken:

Code: Alles auswählen

def compare_strings(first_string, second_string):
    for first_character in first_string:
        if first_character in second_string:
            return True
    return False
Das kann man wiederum kompakter mit any schreiben:

Code: Alles auswählen

def compare_strings(first_string, second_string):
    return any(c in second_string for c in first_string)
oder eben mit sets:

Code: Alles auswählen

def compare_strings(first_string, second_string):
    return not set(second_string).isdisjoint(first_string)
Benutzeravatar
snafu
User
Beiträge: 6779
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Bei Sets würde ich es als set(first) & set(second) schreiben, da ich es lesbarer finde. Bei einer Funktion evtl noch ein bool() um das Ergebnis setzen, bei direkter Verwendung im Code aber unnötig, da der Wahrheitswert implizit festgestellt wird (leeres Set vs. gefülltes Set). Außerdem kann man nummerierte Strings schnell verwechseln. Daher ist etwas wie text und letters sowieso besser.

Und wie gesagt: any() wäre die Alternative wenn man nicht drei "Wegwerf-Sets" (first, second, Ergebnis) benutzen möchte. Außerdem muss der Leser dann keine detaillierten Kenntnisse über Sets haben, die any()-Lösung liest sich ja fast wie ein normaler Satz. Der Nachteil daran ist aber die quadratische Laufzeit, was aber häufig nicht ins Gewicht fällt (z.B. speziell hier wohl eher nicht).
Zuletzt geändert von snafu am Sonntag 4. November 2018, 15:08, insgesamt 1-mal geändert.
Sirius3
User
Beiträge: 18054
Registriert: Sonntag 21. Oktober 2012, 17:20

@snafu: da die Funktion symmetrisch ist, ist es eigentlich egal, was jetzt der erste und der zweite String ist. Ein »is not disjoint« sagt eigentlich ziemlich klar, was da gemacht wird, während man bei einem & überlegen müßte, was denn das bedeutet.
Benutzeravatar
snafu
User
Beiträge: 6779
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Sirius3: Kommt wohl auf den eigenen Hintergrund an. Ein & verstehe ich hier als Binäres Und und so ist es im übertragenen Sinne ja auch gemeint. Nur dass eben keine Nullen ins Ergebnis gelangen. BTW: Man müsste disjoint() genau so nachschlagen wenn man den mathematischen Hintergrund nicht hat. Ist aber letztlich alles Geschmackssache. Man merkt hier mal wieder, dass es in Python oft leider doch nicht den einen offensichtlichen Weg gibt, etwas zu tun...
Hart mit Bart
User
Beiträge: 25
Registriert: Dienstag 30. Oktober 2018, 12:05

@snafu
Warum baust du set-Funktionalität nach?
Mir war das nicht wirklich bewusst.

Das ...

Code: Alles auswählen

def has_any_char(text, chars):
    return any(char in text for char in chars)
... ist natürlich wesentlich eleganter.

Laut Sirius ist die Set-Abfrage unnötig, was ich nicht ganz nachvollziehen kann. Es schien mir das zu tun, was ich erwartet habe.

@Sirius3
Der erste Teil prüft, ob letters und word_to_guess gemeinsame Buchstaben haben und danach wird nochmal explizit geprüft, ob ein konkreter Buchstabe `letter` aus word_to_guess in `letters`
Ohne den zweiten Teil funktioniert das Script nicht wie gewünscht. Wenn man einen einzigen Buchstaben eingibt, der sich im gesuchten Wort befindet, wird das komplette Wort als gelöst ausgegeben. Ich habe da irgendwo ziemlich rumgestümpert, wie es ausschaut.
Wenn ich das Script überarbeitet habe, tritt das hoffentlich nicht mehr auf.

Viel zu tun und viel zu lernen, ich noch habe. Aber ich stelle mich dem gerne. :mrgreen:
Sirius3
User
Beiträge: 18054
Registriert: Sonntag 21. Oktober 2012, 17:20

@snafu: ich weiß nicht, was Du mit „binärem Und” meinst, weil es sich ja hier um ein Mengen-Und handelt. Der offensichtliche Weg, wenn man die Disjunktheit zweier Mengen untersuchen will, ist isdisjoint. Weil man da beim ersten Auftreten eines gemeinsamen Elements schon fertig ist. Spart also auch noch Rechenzeit.

@Hart mit Bart: ich schrieb ja auch, man kann den ersten Teil der Bedinung weglassen, nicht den zweiten.
Hart mit Bart
User
Beiträge: 25
Registriert: Dienstag 30. Oktober 2018, 12:05

*Augen verdreh* Da stand ich wohl mächtig auf der Leitung. Jetzt, wo ich nochmal auf die Stelle geschaut habe, wird es mir auch klar. Ich frage die gleiche Sache auf 2 verschiedene Arten ab. Schön, dass du mein Script besser verstehst als ich. Das hilft! :mrgreen:
Antworten