Master Mind auf Konsolenbasis

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Chiller Royal
User
Beiträge: 8
Registriert: Freitag 7. März 2014, 16:49

Hi, ich bin neu hier im Forum und beginne gerade damit Python zu lernen.
Als kleinen Anreiz habe ich mal eine Java-Programmieraufgabe aus meinem Studium genommen in der ich das Spiel Master Mind programmieren sollte und habe es in Python umgeschrieben.

Ich wäre euch dankbar wenn ihr mal rüberschauen und mir eure Kritik mitteilen könntet.
Was würdet ihr vielleicht anders machen, findet ihr Fehler die ausgebessert werden müssten oder gibt es Konventionen die ich nicht eingehalten habe?

An sonsten habt ihr ja vielleicht auch einfach etwas Spaß mit dem Spiel :D
Alles weitere sollte im Code erklärt sein.

LG

Code: Alles auswählen


import random

def frage_parameter():
    '''Fragt nach Wertebereich und Länge.

    Der Wertebereich und die Länge werden durch input() erfragt und anschließend als Liste zurück gegeben.
    '''
    print("Gib eine Zahl als Wertebereich ein. Der Code wird dann aus Zahlen zwischen 1 und deinem Wert generiert.")
    while True:
        wb = int(input("> "))
        if wb > 0:
            break
        else:
            print("Ungültiger Wertebereich!")
    print("Gib an wie viele Stellen dein Code haben soll.")
    while True:
        länge = int(input("> "))
        if länge > 0:
            break
        else:
            print("Die länge muss eine Positive Zahl sein!")
    return [wb, länge]


def kreiere_code(wb, länge):
    '''Gibt den Code als Liste zurück.

    Der Code besteht aus 'länge' Ziffern die jeweils zwischen 1 und 'wb' liegen.
    '''
    code = []
    for wert in range(länge):
        code.append(random.randint(1, wb))
    return code


def frage_lösung(code, wb):
    '''Fragt nach einer Lösung für den Code.

    Durch input() wird nach einer Lösung gefragt und dann als Liste zurück gegeben.
    Es wird auch die korrekte Länge und der Wertebereich überprüft.
    Ist die Länge nicht korrekt wird die Funktion erneut aufgerufen.
    '''
    eingabe = input("> ")
    lösung = []
    for i in eingabe.split():
        if 1 <= int(i) <= wb:
            lösung.append(int(i))
        else:
            print("Die Zahl", i, "liegt nicht im Wertebereich!")
    if len(lösung) != len(code):
        print("Deine Lösung hat nicht die richtige Länge!")
        return frage_lösung(code, wb)
    return lösung


def vergleiche_lösung(code, lösung):
    '''Bewertet die Lösung des Codes.

    Der Code und die Lösung werden verglichen und die Treffer (exakte und korrekte) als Liste zurück gegeben.
    '''
    treffer = exakteTreffer = 0
    # exakte Treffer sind alle Zahlen die genau an der richtigen stelle in der Lösung sind
    # Normale treffer sind alle Zahlen die keine exakten Treffer sind aber in der Lösung auftauchen.
    # Jeder Treffer wird nur einmal gezählt und nur mit einer Zahl im Code in Verbindung gebracht. (Keine Doppelzuordnung)

    # Die Listen dienen zur Markierung der gefundenen Treffer.
    zählerCode = []
    zählerLösung = []

    # Alle exakten Treffer werden gesucht und in den Listen zählerCode und zählerLösung vermerkt.
    for i in range(len(code)):
        if code[i] == lösung[i]:
            zählerCode.append(True)
            zählerLösung.append(True)
            exakteTreffer = exakteTreffer + 1
        else:
            zählerCode.append(False)
            zählerLösung.append(False)

    # Alle weiteren Treffer werden gesucht und ebenfalls vermerkt.
    for i in range(len(code)):
        if not zählerCode[i]:
            for j in range(len(code)):
                if not zählerLösung[j]:
                    if code[i] == lösung[j]:
                        zählerLösung[j] = True
                        treffer = treffer + 1
                        break
    return [exakteTreffer, treffer]


print("In diesem Spiel sollst du versuchen einen vom Computer generierten Code zu erraten. \n"
      "Du kannst wählen wie lang, und aus wie vielen verschiedenen Zahlen der Code bestehen soll. \n"
      "So kannst du einstellen, wie schwer das Rätsel sein soll.")
[wb, länge] = frage_parameter()
code = kreiere_code(wb, länge)
print("Gib einen Code aus", länge, "Zahlen, getrennt durch Leerzeichen ein. \n"
      "Danach wird dir gesagt, wie viele deiner Zahlen genau an der richtigen Stelle sind (Exakte Treffer) \n"
      "und wie viele Zahlen zwar im Code vorkommen, aber an der falschen stelle sind. Danach kannst du weiter raten \n"
      "bis du den Code geknackt hast.")
versuche = 0
while True:
    lösung = frage_lösung(code, wb)
    versuche = versuche + 1
    check = vergleiche_lösung(code, lösung)
    if check[0] == len(code):
        print("Herzlichen Glückwunsch. Du hast den Code mit", versuche, "Versuchen gelöst.")
        break
    else:
        print("Versuch", versuche)
        print("Exakte Treffer:", check[0], "\nAndere Treffer:", check[1])
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo und willkommen im Forum!

Ich gehe einfach mal der Reihe nach durch.

- Warum nennst du den Wertebereich "wb"? Wenn du es Wertebereich nennst, dann weißt du auch noch in mehreren Wochen, was du dir bei dem Code gedach hast. Ja, das passiert dir auch bei deinem eigenem Code ;-) Ich musste erstmal lesen um zu versten, was "wb" überhaut ist.
- deine "frage_parameter"-Funktion besteht aus zwei fast identischen Teilen. Abstrahiere das mal und lagere es in eine eigene Funktion aus.
- ``return [wb, länge]`` sollte besser ``return wb, länge`` heißen.
- Die Funktion zum erzeugendes Codes geht kürzer:

Code: Alles auswählen

[random.randint(1, wb) for _ in range(länge)]
- Wenn du einen Werte nicht brauchst, so wie "wert", dann kennzeichne das einfach mit einem "_" als Namen.
- Docstrings sollten beschreiben was eine Funktion macht und nicht wie sie es macht. Den Benutzer interessiert es zum Beispiel nicht, dass Werte mit input abgefragt werden.
- Du solltest nur ``for i in ...`` schreiben, wenn es sich bei i um einen Integer handelt, alles andere is unerwartet.
- Statt "i" doppelt mit int in einen Integer zu verwandelt, solltest du das am besten einfach einmal machen.
- Du solltest "frage_lösung" nicht rekursiv in "frage_lösung" aufrufen. Zum einen hat Python ein Rekursionslimit (was hier eher nicht relevant ist) und zum anderen vermischt du hier zu vieles. "frage_lösung" sollte nur nach der Lösung fragen. Die Entscheidung, ob das eine korrekte Lösung ist und was als nächstes zu tun ist, sollte an einer anderen Stelle fallen. Jede Funktion sollte genau eine Aufgabe haben. Nicht mehr.
- "zählerCode" und "zählerLösung" entsprechen nicht PEP 8.
- ``for i in range(len(code)):`` ist in Python ein Anti-Pattern. Wenn du das machst, dann ist fast immer etwas falsch. Brauchst du den Index zu einem Element, dann verwende die enumerate-Funktion. Wilst du Listen parallel durchlaufen, dann verwende die zip-Funktion:

Code: Alles auswählen

for c, l in zip(code, lösung)
- Das ganze Ding geht aber auch kürzer:

Code: Alles auswählen

for c, l in zip(code, lösung):
    equal = c == l

    zähler_code.append(equal)
    zähler_lösunge.append(equal)
    exakte_treffer += equal
Lass dir noch bessere Namen einfallen.
- Die for-Schleifen in den Zeilen 82 und 84 solltest du ebenfalls wie oben ändern und direkt über die Listen iterieren.
- Für mich sehen die Zeilen 82 bis 89 so aus, als möchtest du unbedingt Mengen verwenden.
- Zeile 90 sollte einfach wieder ``return exakte_treffer, treffer`` sein.
- Code gehört nicht auf Modulebene, nur Importe, Konstanten und Definitionen. Alles ab Zeile 93 sollte in eine main-Funktion gepackt werden.
- Verwende das folgende Idiom, damit ist sichergestellt, dass dein Code nur beim Ausführen des Moduls gestartet wird. Somit kannst du dein Modul importieren und wiederverwenden:

Code: Alles auswählen

if __name__ == "__main__":
    main()
- Python unterstüzt Strings über mehrere Zeilen mittels dreifachen Anführungszeichen: """...""" und '''...'''
- ``[wb, länge] = frage_parameter()`` sollte besser ``wb, länge = frage_parameter()`` heißen.
- Du verwendest zu viele "Magic Numbers". In einer Woche weißt du nicht mehr, was in check[0] oder check[1] steckt.
- Statt die Versuche per Hand zu zählen, kannst du auch itertools.count verwenden:

Code: Alles auswählen

for versuch in itertools.count(1):
    lösung = ...
Das Leben ist wie ein Tennisball.
Chiller Royal
User
Beiträge: 8
Registriert: Freitag 7. März 2014, 16:49

Hi,
erstmal vielen dank für die ausführliche Kritik.

Ein par Fragen bleiben mir allerdings noch:

In wie fern ist range(len(code)) ein Antipattern? Woran erkennt man das?

In frage_lösung() hab ich deinen Vorschlag übernommen und die zweite Prüfung nach den weiteren Treffern so gelöst:

Code: Alles auswählen

for c, zc in zip(code, zähler_code):
        for l, zl in zip(lösung, zähler_lösung):
            equal = c == l
            if equal and not zc and not zl:
                zc = True
                zl = True
                treffer += equal
Hast du das so gemeint?

Das mit der main klingt plausiebel, ich gehe gerade das Python-Tutorial durch und da war das noch nicht dran.

Nochmal vielen dank für deine Anmerkungen.

LG
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Chiller Royal hat geschrieben:In wie fern ist range(len(code)) ein Antipattern? Woran erkennt man das?
Da kannst du nicht erkennen, das ist einfach schlechter Stil. Eine Schleife

Code: Alles auswählen

for i in range(len(code)):
    item = code[i]
Ist einfach viel zu umständlich, da man dies auch direkt ausdrücken kann:

Code: Alles auswählen

for item in code:
    ...
Die letzte Variante ist kürzer, einfacher zu lesen und sagt genau das aus, was gemacht wird.
Chiller Royal hat geschrieben:Hast du das so gemeint?
Nein, ich meinte das als Ersatz für deine Zeilen 72 bis 79.

Über die Zeilen 72 bis 89 solltest du aber noch einmal genau nachdeken. Ich habe jetzt keine Lust mich durch den Code zu quälen, aber das sieht doch alles sehr umständlich aus.
Das Leben ist wie ein Tennisball.
Chiller Royal
User
Beiträge: 8
Registriert: Freitag 7. März 2014, 16:49

Wie kann ich denn dann über die Indizes einer Liste itterieren wenn nicht über " for i in range(len(liste)) "?

Ich bin noch etwas zu sehr an Java gewöhnt, wo man in einer for-Schleife ja immer Startwert, Endwert und die Sprünge die man dazwischen macht angiebt. Da hat man ja immer das i als Index.
EyDu hat geschrieben:Nein, ich meinte das als Ersatz für deine Zeilen 72 bis 79.
Ja ich habe die Zeilen 72 bis 79 durch das ersetzt was du geschrieben hattest und die Zeilen 82 bis 89 durch das, was ich geschrieben habe.

Funktioniert auch so :D
BlackJack

@Chiller Royal: Wenn Du *wirklich* *nur* über die Indizes iterieren möchtest, dann ist das schon richtig, nur das man das eigentlich nie machen will. Eigentlich will man ja über die Elemente iterieren. Oder über Index und Element, dann ist `enumerate()` die Funktion der Wahl.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Auch in Java benutzt man keine for-Schleife, wenn man nur über ein Array iterieren möchte. Dann nimmt man eine for-each-Schleife, um direkt über die Elemente zu iterieren. Also genauso wie bei Python auch. (es gibt viel Java-Code "da draußen", wo for-Schleifen zum einfachen Iterieren über Arrays benutzt wird, aber das ist da genauso ein Antipattern wie in Python)
BlackJack

Mal eine Lösung in Python ohne so komische verschachtelte Schleifen mit Indizes :-)

Code: Alles auswählen

from collections import Counter


def evaluate_guess(expected, guessed):
    exact_count = sum(a == b for a, b in zip(guessed, expected))
    expected_histogram = Counter(expected)
    correct_count = sum(
        min(count, expected_histogram[value])
        for value, count in Counter(guessed).items()
    )
    return exact_count, correct_count
Chiller Royal
User
Beiträge: 8
Registriert: Freitag 7. März 2014, 16:49

@ BlackJack
Deine Variante blicke ich nicht komplett aber wenn ich sie ausprobiere habe ich auch einen kleinen Fehler drinnen:
Für einen Code [1, 2, 2, 4] und die Lösung [2, 1, 3, 4] bekomme ich 1 exakten Treffer und 3 korrekte.
Das Ergebnis sollte aber 2 korrekte Treffer bringen da die "2" in der Lösung nur mit einer "2" im Code in verbindung gesetzt wird und nicht mit beiden.

Habe ich da was falsch übernommen oder hast du vielleicht einen kleienn Denkfehler? Wie gesagt ich steig da nicht ganz durch.
BlackJack

@Chiller Royal: Wieso nur zwei korrekte? Hier sind 1, 2, und 4 korrekt und das sind soweit ich das sehe drei korrekte‽ Die 2 wird dabei nur einmal gezählt weil immer das Minimum von beiden Anzahlen aufaddiert wird.
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Ich glaube, was Chiller Royal möchte, ist dass exakte Treffer nicht zusätzlich auch als korrekte gezählt werden. Dann müsste es so aussehen:

Code: Alles auswählen

def evaluate_guess(expected, guessed):
    ...
    return exact_count, correct_count - exact_count
In specifications, Murphy's Law supersedes Ohm's.
Chiller Royal
User
Beiträge: 8
Registriert: Freitag 7. März 2014, 16:49

Ja so hatte ich das gemeint :)
Antworten