Adventskalender Python Grundlagen

Gute Links und Tutorials könnt ihr hier posten.
mdietz
User
Beiträge: 5
Registriert: Freitag 9. Dezember 2022, 00:39

Hi,

ich baue gerade einen Adventskalender mit ein paar Python Aufgaben für meine Kids.
Die Aufgaben sollen nicht zu simple aber auch nicht zu schwer sein. Ein wenig Basis Wissen wird vorausgesetzt (if, for, variablen,..) .
Es soll am Ende des Tages Spass machen (z.b. weil ein kleines Spiel raus kommt).

Habe bis jetzt Aufgabenstellungen für die ersten 13 Tage welche teilweise aufeinander aufbauen. (Glücksrad, Hangman, Keyboard training)
Alles nichts besonderes, aber noch macht es den Kids spass. Ich muss natürlich öfters mal helfen weil es doch nicht immer so einfach ist.

Wer das ganze mal ausprobieren will:
http://advent.dietzm.de

Über Feedback würde ich mich freuen.

LG
Mathias
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Du schreibst in am ersten Advent, dass eine Zahl zwischen 1 und 26 ausgegeben werden soll, in der Lösung ist es dann aber eine Zahl zwischen 0 und 25. Die Buchstaben würde man einfacher als String schreiben ('abcdefghijklmnopqrstuvwxyz') oder gleich string.ascii_lowercase nehmen. Warum nennst Du die Variable, für die Eingabe `q`?

Deine Schreibweise ist inkonsistent, spieleranzahl aber aktuellerSpieler. Variabelnnamen werden in Python nach Konvention komplett kleingeschrieben.
`spielerinput` ist unnötig, weil Du spieleranzahl gleich in einer Zeile abfragen und berechnen könntest. Wenn spieleranzahl != 2 ist, dann wird angenommen, dass es nur einen Spieler gibt, später wird aber 10*spieleranzahl gespielt und jeweils zwei Spieler wechseln sich ab; diesen Fehler im Programm korrigierst Du wieder dadurch, dass Du nochmal prüfst, ob auch wirklich zwei Spieler mitspielen. Das ist doch sehr umständlich und fehleranfällig. exit wird einfach so benutzt, ohne dass es importiert wird. Das wird eigentlich gelöst, indem man eine Funktion main hat, die man per return verläßt. Wenn man etwas anderes als Q oder B eingibt, setzt man eine Runde aus. Ist das so gewollt?
Heutzutage benutzt man statt %-Strings Formatstrings.

Code: Alles auswählen

import random

def main():
    letters = "abcdefghijklmnopqrstuvwxyz"

    punkte1 = 0
    punkte2 = 0
    spieleranzahl = int(input("Wieviel Spieler (1/2)?"))

    spieler1 = input("Spieler 1 Name?")
    if spieleranzahl == 2:
        spieler2 = input("Spieler 2 Name?")
    elif spieleranzahl > 2:
        print("Es gibt nur einen Spieler!")
        spieleranzahl = 1

    while True:
        wort = input("Bitte ein Wort eingeben:").lower()

        for runde in range(10*spieleranzahl):
            aktueller_spieler = runde % spieleranzahl + 1
            if aktueller_spieler == 2:
                print(f"{spieler2} ist dran!")
            else:
                print(f"{spieler1} ist dran!")

            q = input('Zum beenden Q drücken, B drücken um ein Buchstaben zu generieren:')
            if q == 'Q':
                return
            if q == 'B':
                #Lösung mit Liste
                suchbuchstabe = random.choice(letters)
                if suchbuchstabe in wort:
                    anzahl = wort.count(suchbuchstabe)
                    if aktueller_spieler == 2:
                        punkte2 += anzahl * 10
                    else:
                        punkte1 += anzahl * 10
                    print(f"Buchstabe: {suchbuchstabe}, Punkte +{anzahl * 10}")
                else:
                    print(f"Buchstabe: {suchbuchstabe} nicht im Wort")
            else:
                print("Aussetzen")
        print("Punkte {spieler1}: {punkte1}")
        if spieleranzahl == 2:
            print("Punkte {spieler2}: {punkte2}")

if __name__ == "__main__":
    main()
Abgesehen davon, ist das eine tolle Idee.
Benutzeravatar
Dennis89
User
Beiträge: 1152
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

weil ich es gerade versucht habe und Sirius3 'string' angesprochen hat, da ich auch verwendet habe, hier der Code dazu. Von einem "Z" stand nichts in der Aufgabenstellung, deswegen ist das bei mir nicht drin.

Code: Alles auswählen

import string
from random import randint


def main():
    while True:
        user_input = input("Bitte gebe ein Zeichen ein: ")
        if user_input == "Q":
            break
        elif user_input == "B":
            random_number = randint(1, 26)
            print(random_number)
            print(string.ascii_uppercase[random_number - 1])


if __name__ == "__main__":
    main()
Ich finde die Idee auch super und vielleicht kannst du dich etwas an [url="https://adventofcode.com/"]Advent of Code[/code] orientieren oder kleine Teilaufgaben daraus nehmen oder die Aufgaben daraus stark vereinfachen oder auch deine eigene kleine Geschichte für nächstes Jahr schreiben, falls dir die Art gefällt.

Grüße
Dennis

P.S. Bin gespannt was hinter den anderen Türchen steckt :D
"When I got the music, I got a place to go" [Rancid, 1993]
mdietz
User
Beiträge: 5
Registriert: Freitag 9. Dezember 2022, 00:39

Hey,
Danke für das Feedback.

Die Lösungen sind nur als Beispiel gedacht und sollten sich auf das wesentliche konzentrieren. Sie sind natürlich nicht perfekt, es gibt immer viele Wege zum Ziel ( z.b. String.ascii_uppercase vs Liste..).
SPOILER: In einem späteren Türchen kommt auch noch die Aufgabe den Input besser zu validieren z.b. ungültige Eingaben besser abzufangen.
f-strings hatte ich anfänglich vermieden da mein Sohn noch mit python2 begonnen hatte, aber inzwischen hast du natürlich recht und ich sollte das umstellen.

Ich bin mir nicht sicher ob ich die Lösungen überhaupt drin lassen soll bzw. bei den späteren Türchen hab ich nicht immer eine Lösung drin. Was meint Ihr ?



LG
Mathias
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Viele Wege führen aber auch in die Irre. Wenn der aktuelle Spieler größer als die Spieleranzahl ist, dann ist das ein Logikfehler, der nur dadurch nicht auffällt, weil Du an vielen Stellen den Fehler abfängst. Einer der vielen richtigen Wege wäre, dass der Fehler erst gar nicht auftritt.
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Es gibt halt auch so eine Art ”weiches” Falsch. Etwas immer wieder zu machen, was man einmal an einer Stelle hätte machen können, ist aus Programmierersicht falsch. Auch wenn das natürlich funktioniert.

Ähnlich die Liste mit Buchstaben vs. `string.ascii_lowercase`. Die Buchstaben selbst hinzuschreiben ist solange richtig, solange man nicht weiss, dass es die als fertige Konstante gibt. Ab da ist das falsch, weil es unnötige Schreibarbeit und fehleranfällig ist.

Schönere Lösung als erst eine Zahl zu ”würfeln” wäre hier aber IMHO auch einfach ``random.choice(string.ascii_lowercase)``. Weil das direkter ausdrückt was passieren soll.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
mdietz
User
Beiträge: 5
Registriert: Freitag 9. Dezember 2022, 00:39

ich habe das Feedback ja schon teilweise eingearbeitet ;-)
Ob ihr würfeln vs random.choice(..) besser findet überlass ich euch. Für Kinder finde ich es persönlich wichtiger die Grundlagen von String/Listen zugriffen zu kennen statt eine random.choice() function.

Ihr könnt gerne eure Lösungen nach Github pushen und hier teilen. Dann kann man die schönste Lösung raussuchen.
Bin gespannt

LG
Mathias
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Es geht nicht darum was wir besser finden, sondern was objektiv besser ist. Und das ist die einfachere und weniger fehleranfällige Lösung, also `random.choice()`, weil man sich da keine Gedanken um typische „off by one“-Fehler machen muss. Grundlage von Sequenzzugriffen in Python per Index ist, das man die nicht macht. Jedenfalls nicht falls es eine Alternative gibt, die es fast immer gibt. Hier ist so ein bisschen die Frage ob *Python* lernen der Fokus ist, oder zufällig Python benutzt wird um zu lernen wie man früher in Sprachen wie BASIC, Pascal, oder C programmieren musste.

Noch ein paar Anmerkungen zu den Beispiellösungen:

Die `input_aus_datei()` aus der Beispiellösung zu Tag 4 geht gar nicht:

Code: Alles auswählen

def input_aus_datei():
    random_number = random.randint(1,239650)
    w = open("wortliste.txt", "r")
    for x in range(random_number):
        woerter = w.readline()
    return woerter.lower().strip()
Wo kommt denn die magische 239650 her? Was wirklich wichtig ist beim Programmieren sind auch Namen. `w` und `x` sind keine guten Namen, da wie nur jeweils ein nichtssagender Buchstabe sind. `x` geht für (gebrochene) Zahlen, insbesondere wenn es sich um eine X-Koordinate handelt. `w` geht eher gar nicht. `woerter` ist *falsch* weil es sich dabei gar nicht um mehrere Wörter handelt, sondern um *eines*. Beim Aufrufer wird der Rückgabewert dann ja auch an `wort` gebunden. Konvention für Namen die aus syntaktischen Gründen da sein müssen, wo man den Wert aber gar nicht braucht, ist `_`. Dann sieht der Leser gleich, dass der Wert nicht verwendet wird, und das absichtlich, und nicht weil das ein Fehler ist oder der Code noch nicht vollständig fertig ist.

Bei Textdateien sollte man beim öffnen immer die Kodierung angeben. Sonst wird ”geraten” und man muss hoffen, das die Kodierung der Datei passt.

Dateien die man öffnet sollte man auch wieder schliessen. Dazu bietet sich die ``with``-Anweisung an.

`readline()` macht nur sehr selten Sinn. In Python 2 hat sich das noch anders verhalten als den nächsten Wert vom Dateiobjekt abzufragen, aber mittlerweile ist beides äquivalent und ich würde hier immer den allgemeineren Weg über den Iterator gehen.

Letztlich ist das aber alles unnötig umständlich. Einfach die komplette Datei in eine Liste einlesen, eine Zeile mit `random.choice()` auswählen, und die dann in Kleinbuchstaben gewandelt zurückgeben:

Code: Alles auswählen

def input_aus_datei():
    with open("wortliste.txt", "r", encoding="utf-8") as zeilen:
        return random.choice(list(zeilen)).strip().lower()
Bei Namen wäre noch zu sagen, dass so komische Denglisch-Mischungen nicht gut sind. Am besten ist Englisch, weil die Schlüsselworte der Programmiersprache englisch sind, und die ganzen Namen (Module, Funktionen, Klassen) in der Standardbibliothek sind englisch benannt, so wie auch 99,9% der externen Bibliotheken. Und Deutsch hat den entscheidenden Nachteil das Einzahl und Mehrzahl relativ häufig gleich geschrieben werden, während das im Englischen so gut wie nie der Fall ist. Werte von Containertypen/Sequenztypen werden in der Regel an den Namen gebunden der in der Mehrzahl ein einzelnes Element beschreibt. Im Englischen kann man immer beides gleichzeitig haben, die Sequenz und ein einzelnes Element daraus an nachvollziehbare Namen gebunden: ``for player in players:``. Im Deutschen hiesse eine Sequenz von Spielern `spieler`, ein einzelner Spieler aber auch `spieler`. Während ``for spieler in spieler:`` ”nur” verwirrt, aber man immer noch weiss, das `spieler` vor der Schleife mehrere Spieler meint, in der Schleife dann aber für einen einzelnen Spieler steht, ist bei der Funktionssignatur ``mach_irgendwas(spieler)`` so gar nicht klar ob da nun ein einzelner Spieler oder mehrere gemeint ist.

Wenn man mit Funktionen anfängt, sollte man IMHO auch gleich *alles* in Funktionen stecken. Dann besteht keine Gefahr, dass irgendeine der Funktionen nicht doch aus versehen (oder aus Bequemlichkeit) auf eine Variable aus dem Hauptprogramm zugreift, also letztlich globale Variablen verwendet werden.

In den Beispielen werden Berechnungen von Punkten unnötig oft wiederholt. Die Multiplikation mit 10 steht da teilweise drei mal hintereinander mit immer dem gleichen Ergebnis. Und man muss auch nicht erst prüfen ob ein Buchstabe im Wort vorkommt und dann erst zu zählen, sondern kann gleich zählen. Wenn dabei 0 raus kommt, war der Buchstabe offensichtlich nicht vorhanden. Solche Sachen sind von Programmieranfängern ja okay, aber bei der Beispiellösung sollte man IMHO höhere Massstäbe anlegen, denn daraus soll man ja auch was lernen können, in dem man die eigene Lösung damit vergleicht was dort anders gemacht wurde, und warum das besser ist.

Beispiellösung für Tag 5:

Die gleiche schreckliche Funktion zum Lesen eine zufälligen Wortes, nur diesmal nicht den Konventionen entsprechend benannt: `inputVonDatei()`.

Man definiert keine Variablen auf Vorrat am Anfang. `gefunden` sollte dort nicht definiert werden, das wird erst viel später gebraucht. Und das hatten wir neulich schon in einem Thema: `clear()` ist eine *sehr* selten benutzte Methode. Statt eine Datenstruktur zu leeren ist es viel einfacher und sicherer einfach eine neue, leere Datenstruktur zu erstellen. Statt also irgendwo weit weg eine leere Liste zu erstellen, und die dann in der Schleife zu leeren und neu zu befüllen, würde man einfach an der Stelle wo die in der Schleife befüllt wird, neu erstellen:

Code: Alles auswählen

gefunden = []

...

while True:
    print("Erstelle Geheimwort......")
    wort = erzeuge_zufaelliges_wort()
    gefunden.clear()
    for b in wort:
        gefunden.append("_")

# => 

while True:
    print("Erstelle Geheimwort......")
    wort = erzeuge_zufaelliges_wort()
    gefunden = ["_"] * len(wort)
Namen wie `buchst` oder `cnt` gehen gar nicht. Warum hat es für das `abe` nicht mehr gereicht und wie erklärt man seinen Kindern was „cunt“ im Deutschen bedeutet? 😛

Die Punkteberechnung wird zweimal gemacht und auf unterschiedliche Arten. So etwas ist auch wieder eine Einladung für Fehler, weil man immer aufpassen muss, das auf beide Arten immer die gleiche Punktzahl heraus kommt.

Insgesamt finde ich das Programm ein bisschen zu ambitioniert, insbesondere wegen dem Zwei-Spieler-Modus, denn hier werden Sachen gemacht, *müssen* Sachen gemacht werden, die entweder sehr unschön und ”falsch” sind, nämlich dauernde Abfragen welcher Spieler dran ist und Code-Wiederholung pro Spieler, oder die Klassen, mindestens mal als Verbunddatentyp benötigen, um das zu vermeiden und es richtig zu machen.

Edit: ``continue`` ganz vergessen: Nicht zeigen. Ist unübersichtlich, weil man den unbedingten Sprung nicht an der eingerückten Struktur erkennen kann und kann Probleme beim Erweitern des Codes oder herausziehen von Teilen aus der Schleife machen. Und man braucht ``continue`` nicht, das lässt sich immer einfach anders ausdrücken.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
mdietz
User
Beiträge: 5
Registriert: Freitag 9. Dezember 2022, 00:39

ok, gute Verbesserungsvorschläge.
Aber Ihr habt mich davon überzeugt die Beispiel Lösungen rauszunehmen. Ansonsten bekommen die Aufgaben selbst wohl keine Aufmerksamkeit. (Und eigentlich geht es ja darum)

Trotzdem würde ich mich über eure Lösungen in Github freuen.

LG Mathias
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Tag 1:

Warum eigentlich die Eingabe von "Q"? Wofür steht das? "B" steht für Buchstabe nehme ich an. "Q" für Quit? Dann wäre das Denglisch auch in der Benutzerinteraktion.

Code: Alles auswählen

#!/usr/bin/env python3
import random
from string import ascii_uppercase


def main():
    while True:
        answer = input(
            "Gib Q zum beenden oder B für einen zufälligem Buchstaben ein: "
        ).lower()
        if answer == "q":
            break

        if answer == "b":
            number = random.randint(1, 26)
            print(
                f"{ascii_uppercase[number - 1]}"
                f" ({number}. Buchstabe im Alphabet)"
            )


if __name__ == "__main__":
    main()
Tag 2:

Code: Alles auswählen

#!/usr/bin/env python3
import random
from string import ascii_uppercase


def main():
    while True:
        word = input("Gib ein beliebiges Wort ein: ").upper()
        total_points = 0
        for _ in range(10):
            answer = input(
                "Gib Q zum beenden oder B für einen zufälligem Buchstaben ein: "
            ).lower()
            if answer == "q":
                return

            if answer == "b":
                letter = random.choice(ascii_uppercase)
                count = word.count(letter)
                points = count * 10
                print(
                    f"{letter!r} ist {count} mal enthalten,"
                    f" das gibt {points} Punkte."
                )
                total_points += points

        print(f"Die Gesamtpunktzahl beträgt {total_points} Punkte.")


if __name__ == "__main__":
    main()
Tag 3:

Wie schon gesagt sehe ich hier ein Problem damit, dass man für zwei Spieler anfangen muss unschönen Code zu schreiben wenn Datenklassen und Listen nicht ”erlaubt” sind. Das ganze wird durch die Zusatzbedingung, dass sich das bei einem einzelnen Spieler auch noch leicht anders verhalten soll als bei mehreren Spielern, nicht besser. Meine Lösung deshalb mit Datenklasse und Liste und allgemein für jede positive Anzahl von Spielern.

Code: Alles auswählen

#!/usr/bin/env python3
import random
from itertools import groupby
from operator import attrgetter
from string import ascii_uppercase


class Player:
    def __init__(self, name, points=0):
        self.name = name
        self.points = points


def main():
    while True:
        player_count = int(input("Gib die Anzahl der Spieler ein: "))
        if player_count >= 1:
            break

        print("Die Anzahl muss ≥1 sein!")

    if player_count == 1:
        player_names = ["Du"]
    else:
        player_names = [
            input(f"Gib Namen von Spieler {i + 1} ein: ")
            for i in range(player_count)
        ]

    while True:
        word = input("Gib ein beliebiges Wort ein: ").upper()
        players = list(map(Player, player_names))
        for _ in range(10):
            for player in players:
                print(f"Aktueller Spieler: {player.name}.")
                answer = input(
                    "Gib Q zum beenden oder B für einen"
                    "zufälligem Buchstaben ein: "
                ).lower()
                if answer == "q":
                    return

                if answer == "b":
                    letter = random.choice(ascii_uppercase)
                    count = word.count(letter)
                    points = count * 10
                    print(
                        f"{letter!r} ist {count} mal enthalten,"
                        f" das gibt {points} Punkte."
                    )
                    player.points += points

        get_key = attrgetter("points")
        points, winners = next(
            groupby(sorted(players, key=get_key, reverse=True), key=get_key)
        )
        winners_text = ", ".join(winner.name for winner in winners)
        print(f"Gewinner mit {points} Punkten: {winners_text}.")


if __name__ == "__main__":
    main()
Ja, Anfänger werden eher nicht `attrgetter()` und `groupby()` kennen, aber das alles zu Fuss zu programmieren wäre ziemlich aufwändig. Der Fall der damit abgedeckt wird, ist in der Beispiellösung übrigens unbehandelt, beziehungsweise drückt sich die Beispiellösung um den Aufgabenpunkt 5).

Und mir das auch so schon zu lang für eine Funktion. Wenn man das tatsächlich mit zwei Variablen pro Spieler und dann jeweils mit ``if``/``else`` und dann im Grunde den gleichen Code für jeden der beiden Spieler löst, wäre es noch mal länger, und wenn man noch Eingabeüberprüfungen einbaut, beispielsweise um sicherzustellen, dass jeder Name nur einmal vergeben werden kann, würde das noch mal länger.

Ich ermuntere Anfänger immer kleine Funktionen zu schreiben. Es ist einfacher später mehrere kleine Funktionen zusammenzufassen, falls die einem nicht genug tun und nur an einer Stelle im Programm aufgerufen werden, als erst einen grossen Haufen Code zu schreiben, und den dann auf Funktionen aufzuteilen, wenn er einem über den Kopf wächst.

Programmteile lassen sich auch leichter testen wenn sie in kleinen Funktionen stecken. Wenn man hier beispielsweise testen wollen würde ob das mit dem Gewinner ermitteln am Ende klappt — Anfänger werden das ja „zu Fuss“ programmieren — müsste man immer das Spiel durchspielen und hat keinen Einfluss auf die erreichten Punkte pro Spieler. Wenn das in einer Funktion stecken würde, welche die Spieler übergeben bekommt, könnte man sich zum Testen einfach ein paar Spieler mit Punkten erstellen, und die Funktion aufrufen.

Tag 4:

Juhuu, wir können Funktionen benutzen. Gleich mal das Hauptprogramm ein bisschen schlanker machen. Und ich habe da das Q und B eingeben raus genommen, weil das echt besch…eiden zu bedienen ist immer noch mal B eingeben zu müssen. Abbruch geht ja immer mit Strg+C.

Code: Alles auswählen

#!/usr/bin/env python3
import random
from itertools import groupby
from operator import attrgetter

WORDS_FILENAME = "wortliste.txt"


class Player:
    def __init__(self, name, points=0):
        self.name = name
        self.points = points


def read_random_word(filename):
    with open(filename, encoding="utf-8") as lines:
        return random.choice(list(lines)).strip().upper()


def ask_player_count():
    while True:
        player_count = int(input("Gib die Anzahl der Spieler ein: "))
        if player_count >= 1:
            return player_count

        print("Die Anzahl muss ≥1 sein!")


def ask_player_names(player_count):
    if player_count == 1:
        return ["Du"]

    return [
        input(f"Gib Namen von Spieler {i + 1} ein: ")
        for i in range(player_count)
    ]


def ask_letter():
    while True:
        letter = input("Gib einen Buchstaben ein: ").upper()
        if len(letter) == 1:
            return letter

        print("Bitte genau einen Buchstaben eingeben!")


def play_round(players, word):
    for player in players:
        print(f"Aktueller Spieler: {player.name}.")
        letter = ask_letter()
        count = word.count(letter)
        points = count * 10
        print(
            f"{letter!r} ist {count} mal enthalten, das gibt {points} Punkte."
        )
        player.points += points


def get_winners(players):
    get_key = attrgetter("points")
    return next(
        groupby(sorted(players, key=get_key, reverse=True), key=get_key)
    )


def main():
    player_names = ask_player_names(ask_player_count())
    while True:
        word = read_random_word(WORDS_FILENAME)
        print("Es wurde ein zufälliges Wort ermittelt.")
        players = list(map(Player, player_names))
        for _ in range(10):
            play_round(players, word)

        print(f"Das Wort lautet {word!r}.")

        points, winners = get_winners(players)
        winners_text = ", ".join(winner.name for winner in winners)
        print(f"Gewinner mit {points} Punkten: {winners_text}.")


if __name__ == "__main__":
    main()
Tag 5:

Nix besonderes.

Code: Alles auswählen

#!/usr/bin/env python3
import random
from itertools import groupby
from operator import attrgetter

WORDS_FILENAME = "wortliste.txt"


class Player:
    def __init__(self, name, points=0):
        self.name = name
        self.points = points


def read_random_word(filename):
    with open(filename, encoding="utf-8") as lines:
        return random.choice(list(lines)).strip().upper()


def ask_player_count():
    while True:
        player_count = int(input("Gib die Anzahl der Spieler ein: "))
        if player_count >= 1:
            return player_count

        print("Die Anzahl muss ≥1 sein!")


def ask_player_names(player_count):
    if player_count == 1:
        return ["Du"]

    return [
        input(f"Gib Namen von Spieler {i + 1} ein: ")
        for i in range(player_count)
    ]


def ask_letter():
    while True:
        letter = input("Gib einen Buchstaben ein: ").upper()
        if len(letter) == 1:
            return letter

        print("Bitte genau einen Buchstaben eingeben!")


def play_round(players, word, word_display):
    for player in players:
        print(f"Aktueller Spieler: {player.name}.")
        print("Suche: ", " ".join(word_display))
        letter = ask_letter()
        if letter not in word_display:
            count = word.count(letter)
            points = count * 10
            print(
                f"{letter!r} ist {count} mal enthalten,"
                f" das gibt {points} Punkte."
            )
            player.points += points
            word_display = [
                letter if letter == word_letter else display_letter
                for word_letter, display_letter in zip(
                    word, word_display
                )
            ]
        else:
            print(
                f"Der Buchstabe {letter!r} wurde bereits aufgedeckt."
            )
    
    return word_display


def get_winners(players):
    get_key = attrgetter("points")
    return next(
        groupby(sorted(players, key=get_key, reverse=True), key=get_key)
    )


def main():
    player_names = ask_player_names(ask_player_count())
    while True:
        word = read_random_word(WORDS_FILENAME)
        print("Es wurde ein zufälliges Wort ermittelt.")
        word_display = ["_"] * len(word)
        players = list(map(Player, player_names))
        for _ in range(10):
            word_display = play_round(players, word, word_display)

        print(f"Das Wort lautet {word!r}.")
        
        points, winners = get_winners(players)
        winners_text = ", ".join(winner.name for winner in winners)
        print(f"Gewinner mit {points} Punkten: {winners_text}.")


if __name__ == "__main__":
    main()
Tag 6:

`split()` ist möglich, aber `partition()` wäre passender, aber letztlich sollte man so etwas nicht selber neu erfinden, sondern ein robustes Standardformat wie JSON verwenden. Die Beispiellösung kommt zum Beispiel nicht damit klar wenn jemand ein "=" im Spielernamen hat. Ich tippe da aber gerne mal "-=> BlackJack <=-" ein. 😇

Nur die Teile die neu hinzugekommen sind:

Code: Alles auswählen

#!/usr/bin/env python3
import json

...

HIGHSCORE_FILENAME = "highscore.json"


def load_highscore(filename):
    with open(filename, encoding="utf-8") as file:
        return json.load(file)


def save_highscore(filename, name_to_points):
    with open(filename, "w", encoding="utf-8") as file:
        json.dump(name_to_points, file)


def update_highscore(name_to_points, players):
    for player in players:
        name_to_points[player.name] = max(
            name_to_points.get(player.name, 0), player.points
        )


def print_highscore(name_to_points):
    if name_to_points:
        print("Highscore")
        print("---------")
        for number, (points, name) in enumerate(
            sorted(
                ((points, name) for name, points in name_to_points.items()),
                reverse=True,
            ),
            1,
        ):
            print(f"{number:3d}. {points:4d} {name}")

...

def main():
    name_to_points = load_highscore(HIGHSCORE_FILENAME)
    print_highscore(name_to_points)

    player_names = ask_player_names(ask_player_count())
    while True:
        ...
        
        update_highscore(name_to_points, players)
        print_highscore(name_to_points)
        save_highscore(HIGHSCORE_FILENAME, name_to_points)


if __name__ == "__main__":
    main()
Tag 7:

Hier ändert sich mehr als man vielleicht auf den ersten Blick vermuten würde, denn wenn nicht mehr in 10 Runden jeder Spieler einmal dran kommt, sondern es ”unendlich” Runden gibt, und jeder Spieler solange dran kommt wie er richtig rät (oder löst), kann man die Schleife, die jeden Spieler durchgeht aus der `play_round()`-Funktion holen und die Funktion auf *einen* Spieler beschränken.

Da ich diesen nervigen Q/B-Zwischenschritt rausgenommen habe, geht R auch nicht — das unterscheide ich einfach danach ob der Benutzer einen einzelnen Buchstaben oder mehr eingegeben hat.

Die `play_round()` ist für meinen Geschmack auch zu gross geworden durch diese neue Möglichkeit, also gibt es eine `try_letter()`- und eine `try_word()`-Funktion.

Und ich habe endlich diese komische Sonderbehandlung rausgeworfen, dass man bei einem einzelnen Spieler keinen Namen eingeben muss. Das hätte eigentlich schon mit Einführung der Bestenliste passieren müssen.

Code: Alles auswählen

#!/usr/bin/env python3
import json
import random
from itertools import cycle, groupby, repeat
from operator import attrgetter

WORDS_FILENAME = "wortliste.txt"
HIGHSCORE_FILENAME = "highscore.json"
BLANK = "_"


class Player:
    def __init__(self, name, points=0):
        self.name = name
        self.points = points


def load_highscore(filename):
    with open(filename, encoding="utf-8") as file:
        return json.load(file)


def save_highscore(filename, name_to_points):
    with open(filename, "w", encoding="utf-8") as file:
        json.dump(name_to_points, file)


def update_highscore(name_to_points, players):
    for player in players:
        name_to_points[player.name] = max(
            name_to_points.get(player.name, 0), player.points
        )


def print_highscore(name_to_points):
    if name_to_points:
        print("Highscore")
        print("---------")
        for number, (points, name) in enumerate(
            sorted(
                ((points, name) for name, points in name_to_points.items()),
                reverse=True,
            ),
            1,
        ):
            print(f"{number:3d}. {points:4d} {name}")


def read_random_word(filename):
    with open(filename, encoding="utf-8") as lines:
        return random.choice(list(lines)).strip().upper()


def ask_player_count():
    while True:
        player_count = int(input("Gib die Anzahl der Spieler ein: "))
        if player_count >= 1:
            return player_count

        print("Die Anzahl muss ≥1 sein!")


def ask_player_names(player_count):
    return [
        input(f"Gib Namen von Spieler {i + 1} ein: ")
        for i in range(player_count)
    ]


def ask_non_empty_text():
    while True:
        answer = (
            input("Gib einen Buchstaben oder die Lösung ein: ").strip().upper()
        )
        if answer:
            return answer

        print("Bitte keine Leereingabe!")


def try_letter(player, word, word_display, letter):
    if letter not in word_display:
        letter_count = word.count(letter)
        points = letter_count * 10
        print(
            f"{letter!r} ist {letter_count} mal enthalten,"
            f" das gibt {points} Punkte."
        )
        player.points += points
        success = points > 0
        word_display = [
            letter if letter == word_letter else display_letter
            for word_letter, display_letter in zip(word, word_display)
        ]
    else:
        print(f"Der Buchstabe {letter!r} wurde bereits aufgedeckt.")
        success = False

    return success, word_display


def try_word(player, word, word_display, text):
    success = text == word
    if success:
        points = 200
        print(f"Korrekt, das gibt {points} Punkte.")
        player.points += points
        word_display = list(word)
    else:
        print("Leider falsch! Alle Punkte sind weg.")
        player.points = 0

    return success, word_display


def play_round(player, word, word_display):
    print(f"Aktueller Spieler: {player.name}.")
    while BLANK in word_display:
        print("Suche: ", " ".join(word_display))
        text = ask_non_empty_text()
        success, word_display = (
            try_letter(player, word, word_display, text)
            if len(text) == 1
            else try_word(player, word, word_display, text)
        )
        if not success:
            break

    return word_display


def get_winners(players):
    get_key = attrgetter("points")
    return next(
        groupby(sorted(players, key=get_key, reverse=True), key=get_key)
    )


def main():
    name_to_points = load_highscore(HIGHSCORE_FILENAME)
    print_highscore(name_to_points)

    player_names = ask_player_names(ask_player_count())
    while True:
        word = read_random_word(WORDS_FILENAME)
        print("Es wurde ein zufälliges Wort ermittelt.")
        word_display = [BLANK] * len(word)
        players = list(map(Player, player_names))
        for player in (
            repeat(players[0], 10) if len(players) == 1 else cycle(players)
        ):
            if BLANK not in word_display:
                break
            word_display = play_round(player, word, word_display)

        print(f"Das Wort lautet {word!r}.")

        points, winners = get_winners(players)
        winners_text = ", ".join(winner.name for winner in winners)
        print(f"Gewinner mit {points} Punkten: {winners_text}.")
        update_highscore(name_to_points, players)
        print_highscore(name_to_points)
        save_highscore(HIGHSCORE_FILENAME, name_to_points)


if __name__ == "__main__":
    main()
Tag 8:

Och nöööö, schon wieder Regeländerungen einbauen. Wer weiss wie das mittlerweile bei tatsächlichen Anfängern aussieht, die keine Erfahrung haben wie man Code sinnvoll auf Funktionen aufteilt, und die vieles umständlich bis fragil zu Fuss programmieren, statt die passenden Syntaxkonstrukte und eingebauten Funktionen sowie Standardbibliotheksfunktionen zu verwenden. Ich sehe da einen riesenklumpen Code vor dem inneren Auge, der bereits einen Haufen fragwürdige Entwurfsentscheidungen enthält, auf den jetzt *noch mal* eine Schippe Regeländerungen und Erweiterungen drauf kommt, ohne das da irgend etwas sinnvoll refaktorisiert wird. Was bei Anfängern ja auch kein Vorwurf ist.

Das ist der Punkt wo ich persönlich den bisherigen Code wegwerfen und sauber neu anfangen würde. Und das auf jeden Fall mit Klassen, denn Das Wort und was davon aufgedeckt ist, gehört ja *sowas* von zusammen und hat auch Funktionen die darauf operieren, mindestens da macht eine Klasse einfach Sinn.

Ich weiss natürlich nicht wie Du an die Erstellung der Aufgaben herangegangen bist, aber es machte den Eindruck als wenn Du das selbst von ”vorne” entwickelt hast. Ich würde das anders herum machen. Erst das Ziel programmieren, und von dort aus dann schrittweise abspecken. Dann hat man IMHO besser im Griff welche Randbedingungen am Ende zu was führen. Das sich der Einspielermodus anders verhalten soll als der Mehrspielermodus, hätte man sich beispielsweise sparen sollen. IMHO. Das bringt da nur unnötige Komplexität rein wo man entscheiden muss auf welcher Ebene man die implementiert und wie. Und bei jeder Regeländerung/-erweiterung kann sich diese Entscheidung als ungünstig erweisen und man muss das alles noch mal überarbeiten, oder Anfänger fangen dann gerne mal an, an jeder Stelle zusätzlichen Code dazu zu basteln, um vorherige Entwurfsentscheidungen wieder auszubügeln.

Die Beispiellösungen enthalten mir persönlich auch schon fast zu viel Code in einer “Funktion“. Also erst einmal sollte sowieso alles in einer Funktion stecken was Programm ist. Und Funktionen kann man gerade als Anfänger eher nicht genug haben. Faustregel: Länge einer Funktion nicht länger als 50 Zeilen, lieber nur 25. Und in der Regel so 5 bis 10 lokale Namen. Und so maximal 5 bis 6 Argumente. Und da wird es bei `spieler_1`, `spieler_2`, `punkte_1`, `punkte_2`, `wort`, und `gefunden` schon knapp wenn man eine Funktion schreibt, die den gesamten Spielzustand benötigt. Dabei würde man normalerweise den Spielernamen und dessen Punkte jeweils zu einem Objekt zusammenfassen, und die beiden dann in einen Container stecken, und `wort` und `gefunden` zu einem Objekt zusammenfassen. Und schon ist dieser gesamte Spielzustand nur noch in zwei Werten, wobei die Anzahl der Spieler daran nichts ändert. Das bleiben auch bei 10 Spielern immer noch nur zwei Werte die übergeben werden müssen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1152
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

@__blackjack__ du hast ja bereits ein paar Dinge aufgezählt, die Anfänger vermutlich nicht machen würden. Mir ist das '!r' im f-String aufgefallen (du benutzt das in sehr vielen Code-Beispielen die ich von dir schon gesehen habe), wenn ich das richtig verstanden habe, dann wird damit 'repr()' auf das Objekt aufgerufen und das gibt ein nicht ausführbares Objekt zur Ausgabe zurück? Wird damit verhindert, dass der Code kein Sicherheitsproblem darstellt und das nicht versehentlich ein Wort ausgegeben wird, dass möglicherweise ein ausführbares Kommando ist?

Die Infos habe ich aus dem folgenden Links:
https://docs.python.org/3/library/strin ... ing-syntax
https://peps.python.org/pep-0498/#s-r-a ... -redundant
https://docs.python.org/3/library/functions.html#repr

Bin mir nr nicht sicher, ob ich das richtig verstanden habe.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das sorgt hier einfach nur dafür, dass die Ausgabe von den Zeichenketten mit " oder ' eingefasst werden. Sonst mache ich das in der Regel bei Texten für Ausnahmen/Fehlermeldungen/Debug-Ausgaben und in `__repr__()`-Implementierungen, weil man da die Repräsentation sehen will, die zur Fehlersuche gedacht ist, und nicht die, die für Endnutzer da ist (`__str__()`). Mit Sicherheitsproblemen hat das nichts zu tun.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1152
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, Danke für die Erklräung.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Die letzten Tage wird dann ja wieder ein Schritt zurück gegangen und es wird statt Glücksrad, Hängemännchen implementiert. Irgendwie immer noch das gleiche. ☹️

Der hier auch schon erwähnte Advent of Code hatte *ein* Jahr mal eine durchgehende Aufgabe, die jeden zweiten Tag erweitert wurde. Ich fand die toll, aber alle die irgendwo stecken geblieben sind, oder frustriert waren und auf die Aufgabe keinen Bock mehr hatten, waren dann bis Weihnachten jeden zweiten Tag raus aus der Geschichte. Das spricht entweder für Musterlösungen, damit man auch immer noch weiter machen kann, wenn man etwas nicht hinbekommt, oder für unabhängige Aufgaben.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Kebap
User
Beiträge: 687
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

... enthalten mir persönlich auch schon fast zu viel Code in einer “Funktion“. Also erst einmal sollte sowieso alles in einer Funktion stecken was Programm ist. Und Funktionen kann man gerade als Anfänger eher nicht genug haben. Faustregel: Länge einer Funktion nicht länger als 50 Zeilen, lieber nur 25. Und in der Regel so 5 bis 10 lokale Namen. Und so maximal 5 bis 6 Argumente.
Ich höre das hier öfter. Gibt es dafür einen Namen? Ist das eher Python-spezifischer Ratschlag, oder allgemeingültig oder nur bei manchen Sprachen? Ich sehe anderswo C++ Quellen mit 1000-Zeilen Funktionen. Da juckt es mich ja schon, mal bisschen die Axt anzusetzen.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Kebap: Das ist nicht wirklich Python-spezifisch. Mir fiele jetzt spontan dafür kein Name ein, das ergibt sich aber auch aus einigen anderen Ratschlägen. Das eine Funktion/Methode genau eine Sache machen sollte. Damit hängen gute Namen zusammen, die schwieriger zu finden sind, wenn die Funktion zu viel macht, insbesondere falls nicht so wirklich zusammengehört. Testbarkeit ist ein Thema. Einmal wegen zyklomatischer Komplexität/McCabe-Metrik, also wie viele Pfade durch eine Funktion gibt es die man alle mit Tests abdecken muss. Aber auch bei eher linearen Funktionen wird es oft immer schwieriger Teile am Ende zu testen, weil man die Testdaten bis dahin erst mal alle heile durch den vorderen Teil der Funktion schleusen muss. Und wie stellt man das sicher/überprüft das? Dann gibt es Studien wie viele ”Dinge” sich der durchschnittliche Mensch auf einmal im Kopf jonglieren kann (5 - 7) und das das Verständnis für eine Funktion deutlich schlechter wird, wenn man sie nicht komplett sehen kann, sondern anfangen muss zu blättern oder zu scrollen. Daher traditionell 25 Zeilen. Oder auch so 50-60 Zeilen inklusive Kommentaren, weil das in Zeiten von so ”kleinen” Terminals auch oft ausgedruckt wurde und Programmierer tatsächlich „form feed“-Steuerzeichen in den Quelltext eingefügt haben, damit Funktionen auf eigenen Seiten gedruckt werden. Auch ein Faktor ist die Faustregel das Kommentare nicht beschreiben sollten *was* der Code macht, sondern *warum*. Wenn man also bei langen Funktionen Kommentare vor einzelnen Blöcken hat, die beschreiben was der folgende Block tut, ist das oft ein Zeichen, dass der in eine Funktion ausgelagert werden könnte, und der Kommentar durch einen sprechenden Funktionsnamen ersetzt werden kann.

Historisch gibt es gegen viele kleine Funktionen öfter Bedenken wegen der Performance. Jeder Funktionsaufruf und viele Parameterübergaben sind ”langsam”. In vielen klassischen Programmiersprachen gibt es Grenzen was der Compiler ”sehen” kann, und damit auch worauf die Optimierungen wirken können.

Das mit dem „was man gleichzeitig sehen kann“ habe ich ja gerade beim Advent of Code mit dem VIC-20 ganz gut vor Augen. Obwohl es genau die gleiche BASIC-Implementierung ist wie auf dem C64, finde ich es schwerer, weil man statt 40×25 Zeichen auf dem C64, auf dem VIC-20 nur ein 22×23 Zeichen grosses ”Fenster” in den Quelltext hat. An mindestens einer Stelle ist mir eine eigentlich recht offensichtliche Optimierung erst aufgefallen, nach dem ich das auf den PC portiert hatte und mehr von dem Programm auf einem Bildschirm sehen konnte.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Manul
User
Beiträge: 52
Registriert: Samstag 13. Februar 2021, 16:00

__blackjack__ hat geschrieben: Sonntag 11. Dezember 2022, 16:04 Der hier auch schon erwähnte Advent of Code hatte *ein* Jahr mal eine durchgehende Aufgabe, die jeden zweiten Tag erweitert wurde.
Weisst Du zufällig noch, in welchem Jahr das war? Klingt reizvoll. Ich habe mal die 2. Tage der vergangenen Jahre überflogen aber auf Anhieb nicht den gefunden, der nach Fortsetzung vom Vortag klang.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Denke, 2019 ist gemeint ("Advent of (Int)code" ;) )
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Genau den meinte ich.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Manul
User
Beiträge: 52
Registriert: Samstag 13. Februar 2021, 16:00

Danke Euch beiden, ich hatte das "jeden zweiten Tag" irgendwie komplett ausgeblendet.
Antworten