Mastermind programmieren

Du hast eine Idee für ein Projekt?
Antworten
Riebers
User
Beiträge: 9
Registriert: Sonntag 18. Juni 2017, 18:52

Guten Tag,
ich bin Python Anfänger und versuche das Spiel Mastermind mit dazugehöriger GUI zu programmieren. Hier ein Link falls jemand das Spiel nicht kennen sollte. https://de.wikipedia.org/wiki/Mastermind_(Spiel)

Um die GUI zu programmieren sind Kenntnisse in Objektorientierter Programmierung Voraussetzung oder sehe ich das falsch? Ich habe das grundlegende Spiel schon programmiert, jedoch bin ich mir nicht sicher ob das so richtig ist oder wie man es besser machen sollte. Wie gesagt bin Anfänger :D . Wäre cool wenn einige Experten sich mal meine Code anschauen und mir Tipps geben, auch gerne zur späteren Umsetzung der GUI! Habe vor die GUI mit PyQt5 zu machen.

Hier der Code:

Code: Alles auswählen

import random

class Spiel:
    def __init__(self):
        random.seed()                  # Bei jedem Start der Berechnung mit gleichem Startwert
        
                                       
        self.farben = []
        for i in range(0,4):
            self.farben.append(random.randint(1,6))
        
        
    def spielen(self):
        x = self.farben
        for i in range(1,8):
            c = Master(i)               
            
            y = c.kombination()         # Vier Zahlen vom Spieler werden in eine Liste geschrieben um diese vergleichen zu können 
            
            self.erg = c.raten(x,y)     # Auswertung der gesteckten Kombination
            
            ## Ergebnis
            print("\nHinweis: "+str(self.erg)+"\n")
            
            if self.erg == ['+','+','+','+']:
                print("=== Richtig===\nDu hast die Lösung in "+ str(i) +" Versuchen gefunden!")
            elif self.erg != ['+','+','+','+'] and i ==7:
                print("===Game Over===\nDu hast es nicht geschafft die richtige Lösung zu finden.")
            else:
                print("===Falsch===\nDu hast noch "+ str(7-i)+" Versuch(e)\n")
            
        print("Die richtige Lösung lautet: "+ str(x))



class Master:
    def __init__(self,i):
        self.versuche = i
        
        
    def kombination(self):              
        print("Bitte Kombination stecken!")
        b = []
        self.counter = 0
        while self.counter <4:
            try:
                b.append(int(input("Bitte Platz " +str(self.counter+1) +  " stecken:")))
                if b[self.counter] > 6 or b[self.counter] < 1:
                    del b[self.counter]
                    continue
                else:
                    self.counter += 1
            except ValueError:
                print("Keine gültige Eingabe")
        print("\nEingesteckte Kombination: " + str(b))
        return b
        
    def raten(self,a,b):                 # Die Kombinationen werden verglichen und zwei Listen erstellt (z_s & z_w).
                                         	 # In z_s werden die Volltreffer eingetragen, in z_w die Halbtreffer.
        z_w = []				 # Ein + ist dabei ein Volltreffer ein o ein Halbtreffer
        z_s = []
        
        for k in range(1,7):
            zaehler_a = 0
            zaehler_b = 0
            zaehler_schwarz = 0
            for i in range(0,4):
                if a[i] == k and b[i] == k:
                    z_s.append('+')
                    zaehler_schwarz += 1
                elif a[i] == k:
                    zaehler_a += 1
                elif b[i] == k:
                    zaehler_b += 1
            if zaehler_a>0 and zaehler_b == zaehler_a and (zaehler_a-zaehler_schwarz ==1 or zaehler_a-zaehler_schwarz ==2):
                z_w.append('o')
            elif zaehler_a>0 and zaehler_b > zaehler_a and zaehler_a-zaehler_schwarz ==1:
                z_w.append('o')
            elif zaehler_a>0 and zaehler_a > zaehler_b and zaehler_b-zaehler_schwarz ==1:
                z_w.append('o')                 
        t = z_s + z_w
        return t

# Hauptprogramm
s = None
s = Spiel()
s.spielen()
Zuletzt geändert von Anonymous am Dienstag 20. Juni 2017, 09:57, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Riebers: Kommentare sollten dem Leser einen Mehrwert bieten. Dabei hilft es, Methoden so zu benennen, dass man keinen Kommentar braucht "Auswertung der gesteckten Kombination" hinter einer Methode die raten heißt, widerspricht sich, wer hat nun recht? Hieße die Methode "auswerten" wäre das geklärt und man könnte den Kommentar ersatzlos streichen. Kommentare, die falsch sind, sind noch schädlicher: "Bei jedem Start der Berechnung mit gleichem Startwert" steht als Kommentar, random.seed initialisiert aber mit der aktuellen Systemzeit, also eben nicht der gleiche Startwert. Also random.seed mit Zahl für Testzwecke, die Zeile komplett weg, beim richtigen Spiel.
Gute Variablennamen sind wichtig. Warum bindest Du self.farben an x, Master() an c oder die Eingabe an y? Warum wird das Ergebnis als Attribut self.erg gespeichert? Wobei wir gleich beim nächsten Thema wären: die Klasse Spiel ist keine Klasse, weil sie nur aus einer Funktion besteht, die man auch ohne Klasse schreiben könnte. Warum wird für jede Runde ein neuer Master erzeugt, dem man die Anzahl der Versuche übergibt? Wenn der Master als Klasse eine Daseinsberechtigung haben soll, dann wenigstens, dass er die Versuche selbst zählt. Ansonsten ist Master ein Container für zwei Funktionen die nichts miteinander zu tun haben. Zusammengefasst, beide Klassen sind unnötig und man könnte das ganze mit einfachen Funktionen schreiben.
Strings stückelt man nicht mit + und str zusammen, sondern benutz .format. Die Bedingung in Zeile 27 ist zur Hälfte das Gegenteil von der in Zeile 25 und schon durch das elif eine Nichterfüllung ausgeschlossen. "self.erg == ['+','+','+','+']" würde ich auch als "self.erg.count('+') == len(self.farben)" schreiben.
Funktion "kombination": Funktionen sollten nach Tätigkeiten benannt werden. self.counter sollte kein Attribut sein. Die Prüfung auf gültige Eingaben ist unendlich kompliziert und kaum zu verstehen.
Funktion "raten": sollte eigentlich "prüfen" oder "vergleichen" heißen, weil geraten wird hier ja nichts. Die if-Abfragen in Zeile 75ff sind mir zu kompliziert. Das versteh ich gar nicht. Glaub auch nicht, dass das richtig ist. Ich hätte das wahrscheinlich so geschrieben:

Code: Alles auswählen

def vergleichen(vorgegeben, geraten):
    weiss = sum(min(vorgegeben.count(x), geraten.count(x)) for x in set(vorgegeben))
    schwarz = sum(x==y for x,y in zip(vorgegeben, geraten)) - weiss
    return ['+'] * schwarz + ['o'] * weiss
Benutzeravatar
Kebap
User
Beiträge: 686
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Guten Tag und willkommen bei Python! :)
Riebers hat geschrieben:Um die GUI zu programmieren sind Kenntnisse in Objektorientierter Programmierung Voraussetzung oder sehe ich das falsch
Ja, etwas fortgeschrittene Kenntnisse sind hilfreich, da man sich sonst sehr leicht verwirren kann oder zuviel gleichzeitig lernen will.
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.
BlackJack

@Riebers: Okay, Tipps:

Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst, dann braucht man da auch keinen Kommentar dran zu schreiben das es das Hauptrogpramm ist. :-)

Einen Namen an `None` zu binden macht keinen Sinn wenn man den Namen gleich in der nächsten Zeile an einen anderen Wert bindet, ohne vorher etwas mit dem bisherigen Wert gemacht zu haben. Das `Spiel`-Exemplar überhaupt an einen Namen zu binden ist hier nicht nötig, denn das einzige was damit gemacht wird, ist die `spielen()`-Methode darauf aufzurufen. Was in der Regel ein Warnzeichen ist. Wenn die Klasse dann neben der `__init__()` nur *eine* Methode hat, dann ist es fast immer eine Funktion die unnötigerweise und kompliziert als Klasse geschrieben wurde.

Wenn man `Spiel` zu einer Funktion macht, dann besteht das Hauptprogramm nur noch aus dem Aufruf dieser Funktion, also ist `Spiel` als Funktion eigentlich schon das Hauptprogramm.

Der Kommentar beim `random.seed()`-Aufruf ist falsch. Man will ja gerade *nicht* immer die gleichen Werte haben. Der Aufruf an sich ist aber schon überflüssig, und bei nicht so guten Implementierungen sogar schädlich. Lass den Aufruf einfach weg.

Sehr viele Namen sind sehr schlecht gewählt. Namen sollen dem Leser vermitteln was der Wert dahinter im Programm bedeutet. Einbuchstabige Namen und kryptische Abkürzungen tun das sehr selten. Warum Du etwas erst `farben` nennst, nur um es dann ohne Not in `x` umzubennenen, ist mir ein Rätsel.

Wobei es sich ja gar nicht um Farben handelt, sondern um Zahlen. Ein passenderer Name wäre hier vielleicht `geheimnis`. Ob das dem Benutzer am Ende als Zahlen, Farben, oder Symbolbildchen präsentiert wird, ist an dieser Stelle ja erst einmal egal.

Beim Erstellen des Geheimnisses bietet sich eine „list comprehension“ an. Und ich würde mit den Zahlen bei 0 anfangen, denn Informatiker fangen beim Zählen mit 0 an. Wie man diese internen Werte dann dem Benutzer präsentiert, ist eine andere Frage. Denn bei einer GUI wirst Du ja wahrscheinlich Farben verwenden wollen und wenn man die in einer Liste speichert, dann ist die erste Farbe am Index 0 und nicht am Index 1.

Warum `Master` so heisst, verstehe ich nicht. Klassen repräsentieren Dinge im weitesten Sinn. Was soll denn ein `Master` sein?

In der `Master.__init__()` wird das `versuche`-Attribut gesetzt was nirgends verwendet wird. Damit ist die Klasse die `__init__()`-Methode los.

In der `kombination()`-Methode wird das Attribut `counter` neu eingeführt. Nach dem abarbeiten der `__init__()` sollte ein Objekt alle Attribute besitzen. Das Objekt sollte in einem sinnvollen, vollständigen Zustand sein.

Jetzt könnte man natürlich wieder eine `__init__()` einführen und dort `self.counter` beispielsweise an `None` binden, *aaaber* dieses Attribut wird sowieso nur in der `kombination()`-Methode verwendet, sollte also eigentlich gar kein Attribut sein, sondern nur ein lokaler Name.

*Dann* ist `kombination()` allerdings semantisch keine Methode mehr, denn man kann einfach das `self` weg lassen und es als Funktion aus der Klasse heraus verschieben.

`kombination` ist kein guter Name für eine Funktion oder Methode. Dafür wählt man in der Regel Tätigkeiten weil Funktionen und Methoden etwas tun. Also beispielsweise `erfrage_kombination()`, weil die Funktion eine Kombination vom Benutzer erfragt. `kombination` wäre ein passender Name für das Ergebnis der Funktion. Auf jeden Fall besser als `b`, was so überhaupt nichts aussagt. Beim Aufrufer könnte man den Rückgabewert dann zum Beispiel `tipp` nennen, statt `y`.

Der Wert von `counter` ist redundant, weil das immer gleich der Länge der Liste mit den Benutzereingaben ist.

In dem ``try``-Block wird viel zu viel gemacht. Das sollte möglichst nur das stehen, was auch die Ausnahme auslösen kann. Also in diesem Fall die Umwandlung in eine Zahl und das binden an einen Namen. Vielleicht noch die Eingabe, damit man nicht einen zusätzlichen Namen für das Zwischenergebnis benötigt. Zu ``try``-Blöcken kann man auch einen ``else``-Block schreiben, wo dann das rein kommt was nur passieren soll wenn die Ausnahme *nicht* aufgetreten ist.

Einen Wert erst an eine Liste anhängen, um ihn dann auf den Wertebereich zu prüfen und gegebenenfalls wieder aus der Liste zu löschen ist umständlich. Einfacher und leichter verständlich wäre es erst zu prüfen, und den Wert nur an die Liste anhängen, wenn er den Kriterien entspricht.

Kommen wir zur `Master.raten()`-Methode: Das ist im vorgefundenen Zustand schon keine Methode, sondern einfach nur eine Funktion die in eine Klasse geschrieben wurde. Verschieben wir sie heraus, dann ist `Master` nun leer und kann entfernt werden.

`raten()` trifft es nicht so ganz, denn das tut ja der Benutzer. Diese Funktion wertet eine geratene Kombination aus.

`a` und `b` sind wieder schlechte Namen. Hier könnte man auch wieder `geheimnis` und `tipp` verwenden. Statt eines Kommentars das `z_w` die Voll- und `z_s` die Halbtreffer enthält, könnte man die Variablen gleich `volltreffer` und `halbtreffer` nennen.

Indexwerte als Laufvariablen in Schleifen, die dann nur als Index in Sequenzen verwendet werden, sind in Python ein „anti pattern“. Man kann *direkt* über die Elemente von Listen & Co iterieren, ohne den Umweg über einen Index. Wenn man über zwei iterierbare Objekte ”parallel” iterieren möchte, gibt es die `zip()`-Funktion.

Kommen wir zur Auswertung innerhalb der Schleife mit ``if``/``elif``/``elif``. Was als erstes auffällt, ist das alle Bedingungen gleich anfangen. Diese erste Teilbedingung braucht man also nicht drei mal prüfen, sondern kann sie als eigenes ``if`` um das ganze setzen.

Der Teilausdruck `zaehler_a - zaehler_schwarz` kommt dort insgesamt vier mal vor, den könnte man vorher *einmal* berechnen.

In jedem Zweig wird am Ende das gleiche gemacht, also reicht ein ``if`` mit den ganzen Bedingungen entsprechend verknüpft. Falls ich mich nicht ”verrechnet” habe, müsste die Bedingung so aussehen:

Code: Alles auswählen

        tmp = zaehler_geheimnis - zaehler_schwarz
        if (
            zaehler_geheimnis > 0
            and (
                (zaehler_tipp == zaehler_geheimnis and (tmp in [1, 2]))
                or (zaehler_tipp > zaehler_geheimnis and tmp == 1)
                or zaehler_tipp - zaehler_schwarz == 1
            )
        ):
            halbtreffer.append('o')
Was aber immer noch zu kompliziert ist. Ich verstehe es ehrlich gesagt auch nicht und ich möchte nicht wissen wie das aussehen muss wenn man statt vier beispielsweise zehn Steckplätze hat. Das ist vom Ansatz her also schon falsch dort mit festen Zahlen zu operieren, die von der Anzahl der Steckplätze abhängig sind.

Was auch auffällt ist das die beiden Listen jeweils gleiche Elemente enthalten, es also eigentlich *Zähler* sind. Die '+'- und 'o'-Elemente darin gehören schon zur Anzeige. Spiellogik und Benutzerinteraktion sollte man aber sauber trennen, damit man die Spiellogik nicht neu schreiben muss wenn man eine GUI drauf setzt.

Bei der Auswertung würde ich zwei Zahlen zurück geben, die Anzahl der richtigen Elemente und die Anzahl wie viele davon am richtigen Platz stecken. Das kann man getrennt berechnen. Und IMHO auch effizienter als Du das machst. Für die Anzahl der richtigen Elemente zählt man in Geheimnis und Tipp die ”Farben” und addiert dann jeweils den kleineren Wert zu einer Summe auf. Das wäre, eine `histogramm()`-Funktion vorausgesetzt, beides auch ziemlich kurz:

Code: Alles auswählen

      anzahl_richtige = sum(
            min(a, b) for a, b in zip(histogramm(geheimnis), histogramm(tipp))
      )
      anzahl_volltreffer = sum(a == b for a, b in zip(geheimnis, tipp))
Bei Namen die auf *einen* Ausdruck beschränkt sind, kann man auch einbuchstabige Namen verwenden, denn da sieht man gleich im Ausdruck wo die her kommen.

Du gibst zwar aus wenn man richtig geraten hat, aber das ganze geht dann trotzdem weiter‽ An der Stelle sollte man die Schleife mit ``break`` verlassen. Und dann kann man ”hinter” der Schleife auch mittels ``else`` zur Schleife das machen, was gemacht werden soll wenn der Spieler das Geheimnis *nicht* erraten hat. Damit wird der Code einfacher.

Die ganzen `str()`-Aufrufe und die ``+`` zum Verbinden von Zeichenketten und Werten sind eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten.

Zwischenstang (komplett ungetestet):

Code: Alles auswählen

import random


def erfrage_kombination():
    print('Bitte Kombination stecken!')
    kombination = list()
    while len(kombination) < 4:
        try:
            item = int(
                input('Bitte Platz {} stecken:'.format(len(kombination) + 1))
            )
        except ValueError:
            print('Keine gültige Eingabe')
        else:
            if 0 <= item <= 5:
                kombination.append(item)
            else:
                print('Bitte nur Zahlen von 0 bis 5 eingeben')

    print('\nEingesteckte Kombination:', kombination)
    return kombination


def auswerten(geheimnis, tipp):
    anzahl_volltreffer = anzahl_halbtreffer = 0

    for wert in range(6):
        zaehler_geheimnis = zaehler_tipp = zaehler_schwarz = 0
        for geheimnis_wert, tipp_wert in zip(geheimnis, tipp):
            if geheimnis_wert == tipp_wert == wert:
                anzahl_volltreffer += 1
                zaehler_schwarz += 1
            elif geheimnis_wert == wert:
                zaehler_geheimnis += 1
            elif tipp_wert == wert:
                zaehler_tipp += 1

        tmp = zaehler_geheimnis - zaehler_schwarz
        if (
            zaehler_geheimnis > 0
            and (
                (zaehler_tipp == zaehler_geheimnis and (tmp in [1, 2]))
                or (zaehler_tipp > zaehler_geheimnis and tmp == 1)
                or zaehler_tipp - zaehler_schwarz == 1
            )
        ):
            anzahl_halbtreffer += 1

    return (anzahl_volltreffer, anzahl_halbtreffer)


def main():
    geheimnis = [random.randint(0, 5) for _ in range(4)]

    for versuch_nummer in range(1, 8):
        tipp = erfrage_kombination()
        anzahl_volltreffer, anzahl_halbtreffer = auswerten(geheimnis, tipp)

        hinweis = '+' * anzahl_volltreffer + 'o' * anzahl_halbtreffer
        print('\nHinweis: {}\n'.format(hinweis))

        if anzahl_volltreffer == 4:
            print(
                '=== Richtig===\n'
                'Du hast die Lösung in {} Versuchen gefunden!'
                    .format(versuch_nummer)
            )
            break

        print(
            '===Falsch===\n'
            'Du hast noch {} Versuch(e)\n'.format(7 - versuch_nummer)
        )
    else:
        print(
            '===Game Over===\n'
            'Du hast es nicht geschafft die richtige Lösung zu finden.'
        )

    print('Die richtige Lösung lautet:', geheimnis)


if __name__ == '__main__':
    main()
Für die GUI muss man in der Tat objektorientierte Programmierung können, und wie oben schon angedeutet sollte man die Spiellogik und die Benutzerinteraktion, also `print()` und `input()` beziehungsweise den Code für die Anzeige komplett trennen.

Was Du auch angehen solltest sind die vielen ”magischen” Zahlen im Quelltext. Also alles was mit der Anzahl der Versuche, der Anzahl der Farben, und der Anzahl der Elemente einer Kombination zu tun hat, sollte genau *einmal* als literale Zahl im Quelltext stehen, und zwar in der Regel als Definition einer Konstanten. Dann kann man das nämlich an einer Stelle ganz einfach ändern und muss nicht im ganzen Programm die gleiche Zahl anpassen und jedes mal überlegen welche Bedeutung eine Zahl hat. Und man muss ja nicht nur nach der Zahl selbst schauen, sondern eventuell auch nach der Zahl plus/minus 1 weil beispielsweise die Anzahl der Farben im `range()` als Anzahl + 1 angeben wird.
Riebers
User
Beiträge: 9
Registriert: Sonntag 18. Juni 2017, 18:52

@Kebap Ich muss scheinbar noch viel lernen^^

@Sirius3@BlackJack danke für die zahlreichen Verbesserungen. Ich muss das erstmal genau analysieren und umsetzten :D . Wie gesagt bin noch ein Anfänger und einige eurer Befehle kenne ich noch nicht :oops: . Offensichtlich macht es keinen Sinn das ganze Objektorientiert zu programmieren...
Werde dann wohl einfach Funktionen verwenden. Habt ihr einen Tipp, an welchem Beispiel man sich OOP am besten beibringen kann?
Sobald ich alles verbessert habe werde ich mich nochmal bei der Elite melden :wink: .
Benutzeravatar
Kebap
User
Beiträge: 686
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Ja, die Elite gibt dir hier gute und viele Tipps zum konkreten Code.

Ich würde Anfängern empfehlen, OOP zunächst beiseite zu lassen, und erstmal Python besser kennenzulernen. Dann OOP. Dann GUI.

Wenn es Mastermind bleiben soll, kannst du es ja auch mit reiner Texteingabe / Ausgabe realisieren. Wenn du hier schon darauf achtest, die Programmlogik von der Ein/Ausgabe zu trennen, kannst du das später für die GUI wiederverwenden.
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.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Riebers hat geschrieben:Offensichtlich macht es keinen Sinn das ganze Objektorientiert zu programmieren...
Werde dann wohl einfach Funktionen verwenden. Habt ihr einen Tipp, an welchem Beispiel man sich OOP am besten beibringen kann?
Lasse Dich inspirieren, ohne Dich dabei von Deinem Weg oder von Deinen Vorhaben abbringen zu lassen. Ich lese oft die Hinweise, um selbst etwas daraus zu lernen, doch niemand kann von Dir erwarten, dass Du alles gleich perfekt umsetzt, zumal sich einige Hinweise oftmals nicht aufs "Programmieren mit Python", sondern auf ein "Besseres Programmieren mit Python" beziehen. Beispiel:
BlackJack hat geschrieben:Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst,
In der Realität sieht es so aus, dass in drei deutschsprachigen Python-Büchern nicht einmal eine selbstdefinierte main()-Methode auftaucht und somit in Büchern, mit denen vermutlich viele das Programmieren mit Python erlernten. Was ich meine, wenn Du OOP erlernen möchtest, sollten Funktionen sitzen, doch anschließend solltest Du auch mit Klassen programmieren, um es erst einmal zu erlernen und vor allem eine gewisse Sicherheit im Umgang mit Klassen zu erhalten. Anschließend wirst Du auch diese Tipps besser verstehen und kannst Dich verbessern.
Und Tipps, Bücher mögen nicht perfekt sein, doch ohne wenigstens ein dickeres Buch für den Anfang wird es nicht ablaufen. Dann hast Du etwas zum Nachschlagen und dann wirst Du auch die gegebenen Tipps besser verstehen.
BlackJack

@Riebers: Es könnte schon Sinn machen da OOP mit rein zu bringen, allerdings ist es nicht automatisch OOP wenn man Klassen definiert. Die müssen aus OOP-Sicht auch Sinn machen. Wenn die Klasse(n) keinen Mehrwert über Funktionen bieten, dann braucht man es nicht unnötig komplex zu schreiben.

Klassen machen in Python teilweise auch ohne OOP Sinn, weil die auch das Sprachmittel für Verbundtypen sind. Also Datentypen die kein Container für eine Sammlung von Daten sind, wie Listen oder Wörterbücher, sondern die *einen* zusammengesetzten Wert beschreiben. So etwas kann man zwar auch mit Tupeln oder Wörterbüchern machen, aber (gute) Namen sind ein wichtiger Bestandteil von Programmen und eine Klasse mit einem Namen hilft beim Verständnis oft besser als anonyme Tupel oder Wörterbücher. Für Werttypen, also solche die nicht verändert werden sollen/dürfen, bietet sich zum erstellen `collections.namedtupel()` an.

Ich hatte hier vor ein paar Jahren mal ein ”umgekehrtes” Mastermind geschrieben, also eines wo sich der Spieler eine Kombination ausdenkt und der Computer die errät. Dort ist eine Kombination als Klasse modelliert die neben den Ziffern auch das Histogramm enthält, damit man das nur einmal berechnen muss. Das Pastebin von Pocoo gibt es ja leider nicht mehr, darum hier mal eine (leicht überarbeitete) Fassung:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf-8
from __future__ import absolute_import, division, print_function
import sys
from functools import partial
from itertools import product

if sys.version_info.major == 2:
    input = raw_input
    from itertools import ifilter as filter, imap as map, izip as zip


ITEM_COUNT = 4
CHOICE_COUNT_PER_ITEM = 10


class Combination(object):

    def __init__(self, numbers):
        self.numbers = numbers
        assert len(self.numbers) <= ITEM_COUNT
        self.histogram = [0] * CHOICE_COUNT_PER_ITEM
        for number in self.numbers:
            self.histogram[number] += 1
        assert sum(self.histogram) == ITEM_COUNT
    
    def __str__(self):
        return ' '.join(map(str, self.numbers))
    
    def compare(self, other):
        correct_numbers = sum(
            min(a, b) for a, b in zip(self.histogram, other.histogram)
        )
        correct_places = sum(
            a == b for a, b in zip(self.numbers, other.numbers)
        )
        assert correct_numbers >= correct_places
        return (correct_numbers, correct_places)


def predicate(guess, answer, combination):
    return guess.compare(combination) == answer


def main():
    combinations = map(
        Combination, product(range(CHOICE_COUNT_PER_ITEM), repeat=ITEM_COUNT)
    )
    while True:
        try:
            guess = next(combinations)
        except StopIteration:
            print('Me stupid or you liar.')
            break
        print(guess)
        answer = tuple(map(int, input().split()))
        if answer == (ITEM_COUNT, ITEM_COUNT):
            print('Heureka!')
            break
        combinations = filter(partial(predicate, guess, answer), combinations)


if __name__ == '__main__':
    main()
Riebers
User
Beiträge: 9
Registriert: Sonntag 18. Juni 2017, 18:52

@Melewo Dein Ansatz scheint Sinn zu ergeben. Ich werde erstmal alles auf Funktionen beschränken und dann später evtl. mit Klassen arbeiten. Ich nutze zum Lernen tatsächlich ein Buch in dem leider auch nicht darauf verwiesen wird, dass das Hauptprogramm üblicherweise in einer Funktion die `main()` heißt steht :D .

@BlackJack Hört sich sehr interessant an! Kann mir gar nicht vorstellen, wie man sowas programmiert :oops: . Verstehe deinen Code auch nicht so ganz^^. Das mit der OOP habe ich auch noch nicht so ganz durchdrungen. Wird wahrscheinlich einige Zeit dauern bis ich das besser verstanden habe, aber danke für die vielen Tipps und Hilfen 8) .
Riebers
User
Beiträge: 9
Registriert: Sonntag 18. Juni 2017, 18:52

Hallo, ich habe eine verbesserte Version des Spiels erstellt. Es wurden bis jetzt nur wenige Tests durchgeführt, aber es scheint alles so zu funktionieren wie es soll :D . Ist es sinnvoll noch irgendwo Kommentare einzufügen? Weiß jemand wie genau das Modul random Zufallszahlen erzeugt? Wird dafür die Systemzeit des Rechners verwendet?!
Der neue Code:

Code: Alles auswählen

import random

def erfrage_kombination():
    print("Bitte Kombination stecken!")
    kombination = list()
    while len(kombination) <4:
        try:
            item=(int(input("Bitte Platz {} stecken:".format(len(kombination)+1))))
            
            if 0<= item <=5:
                kombination.append(item)
            else:
                print("Bitte nur Zahlen von 0 bis 5 eingeben")
        except ValueError:
            print("Keine gültige Eingabe")
    print("\nEingesteckte Kombination: ",kombination)
    return kombination

# Auswerten der Voll-/Halbtreffer
def auswerten(geheimnis, geraten):
    schwarz = sum(x==y for x,y in zip(geheimnis, geraten))
    weiss = sum(min(geheimnis.count(x), geraten.count(x)) for x in set(geheimnis)) - schwarz
    return ['+'] * schwarz + ['o'] * weiss


def main():
    geheimnis = [random.randint(0, 5) for _ in range(4)]
 
    for versuch_nummer in range(1, 8):
        tipp = erfrage_kombination()
        hinweis = auswerten(geheimnis, tipp)
 
        print('\nHinweis: {}\n'.format(hinweis))
 
        if hinweis.count('+') == len(geheimnis):
            print('=== Richtig===\n'
                'Du hast die Lösung in {} Versuchen gefunden!'.format(versuch_nummer))
            break
 
        else: print('===Falsch===\n'
            'Du hast noch {} Versuch(e)\n'.format(7 - versuch_nummer))
    
    print('===Game Over===\n'
            'Du hast es nicht geschafft die richtige Lösung zu finden.')
    print('Die richtige Lösung lautet:', geheimnis)
 
 
if __name__ == '__main__':
    main()
@BlackJack@Sirius3 Danke nochmal für die vielen Hilfestellungen!
Wie gehe ich am besten bei der Programmierung der GUI vor? Ich würde erstmal alle wichtigen Elemente mit dem Qt designer erstellen und danach versuchen den Code einzubinden... :K
Zuletzt geändert von Anonymous am Freitag 23. Juni 2017, 14:02, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Riebers hat geschrieben:Ist es sinnvoll noch irgendwo Kommentare einzufügen?
Ob und was für Dich sinnvoll ist, kann ich Dir nicht sagen. Ich kann Dir nur sagen, wie es für mich sinnvoll ist. Bei meinen ersten Scripts hatte ich zu wenig Kommentare und später bei Überarbeitungen zu kämpfen, erst einmal alles erneut nachzuvollziehen, warum ich was wie schrieb. Seither notiere ich alles Wesentliche in Kommentaren. Auszug aus einem Script, welches noch nicht fertig ist, darunter erfolgt dann erst die Berechnung.

Code: Alles auswählen

        # Formel jeweils für x und y und für alle Punkte einer Bézierkurve:
        # q1 = p0 + (p1 - p0) * t
        # q2 = p1 + (p2 - p1) * t
        # xy = q1 + (q2 - q1) * t
        #            
        # Wobei:
        # p0 = x_start 
        # p1 = x_ecke
        # p2 = x_ende
        # t  = n / einzelbilder
        
        x_start = 102      # p0 für x
        x_ecke  = 0        # p1 für x
        x_ende  = 500      # p2 für x
        
        y_start = 400      # p0 für y
        y_ecke  = 0        # p1 für y
        y_ende  = 302      # p2 für y
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Melewo: was ist der Sinn von Kommentaren, die nur aus einer Formel bestehen, und die Formel wahrscheinlich exakt so auch im Programm auftaucht? Auch Kommentar, die kryptische Abkürzungen erklären sind nicht sinnvoll, da man ja gleich statt der Abkürzung den sprechenden Namen verwenden kann.

@Riebers: Kommentare sollen beschreiben, warum etwas gemacht wird, das "wie" steht schon im Code, der genauso lesbar sein soll, weil er sprechende Namen enthält und mit Funktionen gut strukturiert ist.
Die Dokumentation, was eine Funktion macht kommt in einen Doc-String. Der Kommentar vor "auswerten" ist fast überflüssig, die Beschreibung was Voll und was Halbtreffer sind, sollte in den Doc-String. Ansonsten ist der Code so gut lesbar, dass weitere Kommentare eigentlich unnötig sind.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Deshalb schrieb ich, dass ich Riebers nicht sagen könnte, was für ihn an Kommentaren wichtig ist, sondern nur von meiner Warte aus für mich. Und für mich ist nun einmal wichtig - wie ging es und wo steht es.
Riebers
User
Beiträge: 9
Registriert: Sonntag 18. Juni 2017, 18:52

Hallo Elite, ok über Kommentare kann man sich streiten. Gibt es denn keine offiziellen Richtlinien oder Normen wie Kommentare zu verfassen sind bzw. was ein Kommentar beinhalten sollte :oops: :K ? Wegen der GUI, scheint gar nicht so einfach zu sein diese zu erstellen...
Sollte ich dafür dann lieber unter der GUI Abteilung im Forum ein neues Thema aufmachen oder kann das ruhig hier diskutiert werden?
Gibt es sonst noch ratschläge bezüglich der GUI?^^
Schönes WE euch :D
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Für den Normalfall hat Sirius3 Recht, keine Frage. Da gibt es die PEP 8, in der steht auch einiges über Kommentare.

https://www.python.org/dev/peps/pep-0008/

Streiten würde ich mich deshalb nicht wirklich, denn ich weiß ja, dass es Leute gibt, die etwas innerhalb von 5 Minuten im Kopf rechnen, wofür ich 3 Tage brauchte, mit Kreisen, Ellipsen usw.. Kreise kann man nicht vergessen, was man vergessen kann, das ist bei meinem Beispiel z.B., dass der Umfang oder die Strecke nicht in Pixel berechnet wird, sondern in Einzelbildern mal Laufzeit in Sekunden mal zu wählenden Framerate und das habe ich ebenfalls notiert. Und ja, den Kommentar dazu schreibe ich und wenn es viel mehr wird, dann auch als Word-Dokument zum Ausdrucken und ab damit in einem Ordner aus Pappe oder wenn man es weitergeben möchte, dann halt in einer Readme oder Liesmich.

Wegen einer GUI, probiere es doch erst einmal wie weit Du kommst und dann könntest Du gezielter Deine Fragen bei Problemen stellen.
BlackJack

@Riebers: So ein ”umgekehrtes” Mastermind, wo der Rechner die Kombination raten muss, ist nicht wirklich schwer zu programmieren. Du siehst ja, dass das Programm nicht lang ist. :-)

Das Vorgehen ist folgendermassen:

❶ Erzeuge alle Kombinationen.

❷ Nimm die erste der noch verbleibenden Kombinationen als Tipp und frage den Spieler wie viele Zahlen richtig sind, und wie viele richtig und am richtigen Platz sind.

❸ Wenn alle Zahlen richtig geraten und am richtigen Platz sind → Fertig!

❹ Dann wird ein Filter auf die restlichen Kombinationen gesetzt der nur noch solche durchlässt bei denen ein Vergleich mit dem Tipp die gleiche Anzahl von richtigen Zahlen und die gleiche Anzahl an Zahlen am richtigen Platz ergibt. Alle anderen können als Ergebnis ja nicht in Frage kommen.

❺ Weiter bei ❷

Falls bei ❷ keine Kombinationen mehr übrig sind, dann hat der Spieler entweder eine Kombination vorher falsch bewertet, oder im Programm ist ein Fehler, weil *eine* von allen Kombinationen *muss* der Treffer sein.

Bezüglich offizieller Richtlinien zu Kommentaren: das was Sirius3 geschrieben hat und der Style Guide for Python Code. Bei Melewo ist das Beispiel vielleicht auch ungünstig gewählt, weil das Matheformeln sind und da üblicherweise deutlich mehr an Information und nötigen Domänenwissen hinter steht als zum Beispiel bei dem Mastermind. An solchen Stellen schreibe ich üblicherweise als Kommentar einen Verweis auf Literatur, also Webseite oder Buch, wo eine ausführliche Beschreibung zu beispielsweise der Formel für die Bezierkurve steht. Denn ich würde mir auch in einem Extradokument nicht zutrauen das mathematisch besser zu beschreiben als eine gute, vorhandene Quelle.

Ratschlag zur GUI wäre sich OOP anzuschauen und dann die Version ohne GUI so umzuschreiben, dass die Spiellogik und die Benutzerinteraktion komplett getrennt sind, und das in der Spiellogik keine Schleife enthalten ist die das Spiel antreibt, sondern das die im Code für die Benutzerinteraktion steckt. Denn das ist dann bei der GUI auch so.

Im Moment ist das ja noch überhaupt nicht getrennt. Alle drei Funktionen enthalten Code der etwas mit der Text-GUI zu tun hat. Entweder direkt (`print()`/`input()`) oder indirekt in dem nicht nur etwas berechnet wird, sondern auch gleich Zeichenketten für die Ausgabe erstellt werden.
Antworten