4Gewinnt - Mittels ModelViewController

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Hi @ all,

Tjoar, ich habe ein kleines Konsolen-4Gewinnt-Spiel programmiert. Das Besondere: Ich habe versucht, mich dabei an das ModelViewController-Muster zu halten, gefühlt überhaupt das erste Mal, dass ich sowas mache^^. Das Programm läuft vollständig in der Konsole; ich habe es erfolgreich in Python 2.7 und 3.3 unter Windows und Linux ohne Fehler getestet.

Da ich mit MVC allgemein noch garnichts am Hut hatte, wollte ich das Progrämmchen einfach mal vorstellen und eure Meinung bzgl. des Quellcodes hören ;) . Es gibt ein paar Stellen, die nicht wirklich "schön" sind (je eine Methode in zwei Klassen, die man sofort erkennt^^), aber sonst bin ich einigermaßen zufrieden mit dem Ergebnis^^.

Für alle, die nicht wissen, was das MVC-Muster ist: Grob gesagt beschreibt es, dass die einzelnen Programmteile (Steuerung, Programmlogik, Ausgabe) unabhängig voneinander existieren. Zwischen diesen Schichten wird über "Events" kommuniziert. Ein Programmteil sollte auf einen anderen nur maximal lesend zugreifen dürfen (z.B. der Ausgabe-Programmteil, damit er ggf. ergänzende Angaben von den Modellen erhält). Nunja, so hab ich das Spiel auch aufgebaut...

Tjoar... Hier das Programm :) :
Direktdownload als ZIP, ca. 6 KB

Link bei gist.github.com mit allen Quellcodes

Das Programm läuft, grob beschrieben, so ab: Ein EventManager sorgt dafür, dass jederzeit eine "notify"-Methode samt Parameter (Events, die sich ebenfalls in derselben Quellcode-Datei befinden wie der Event-Manager) an jene Objekte losgeschickt werden können, die sich bei ihm registrieren. Und das tun alle auch: controller, viewer usw... In jeder Klasse wird in der "notify"-Methode dann die entsprechenden Events ausgewertet und entsprechend reagiert. Das ist eigentlich auch schon alles^^.

Ich vermute, dass man da noch einiges besser machen kann (u.a. gibt es wahrscheinlich elegantere Lösungen, als immer wieder "isinstance" zu verwenden^^)

Würde mich über Kritik und Verbesserungsvorschläge freuen :) (Und ich hab Lust, noch je einen weiteren Controller und Viewer für Pygame zu benutzen^^...)


Kurzer Hintergrund meinerseits (ich hab grade das Bedürfnis, als immer-noch-OOP-Anfänger darüber zu reden. Vorsicht, enthält Geschwafel, muss nicht unbedingt gelesen werden ;) ): Ich programmiere schon ziemlich lange (vor ca. 13 Jahren mit QBasic angefangen), allerdings fast ausschließlich hobbymäßig. Seit ich mit OOP in Berührung gekommen bin, hab ich mich schon die ganze Zeit gefragt, wo denn eigentlich dieses "Killer-Feature" in OOP steckt. Ich meine: Ich habe schon relativ viele und auch größere Programme geschrieben und mir einen Stil angeeignet, der - das maße ich mir einfach mal an - auch bei ebenjenen großen Programmen "trotz" prozeduraler Programmiersprache leicht lesbar und verständlich ist. OOP war für mich bisher sowas wie ein Objekt-Speicher mit Attributen, das die Zusammenarbeit mit anderen Funktionen jedoch unnötig beeinträchtigt (wie gesagt: war ;) ). Im Internet findet man auch nur dutzende Auto-Reifen-Türen-Beispiele, aber nie einen Hinweis darauf, wo denn nun der "eigentliche" Vorteil in OOP liegt - gerade bei Programmierern, die keinerlei Probleme mit dem prozeduralen Programmier-Paradigma haben.

Und dann traf ich zufällig auf dieses Projekt: Ein einfaches Pong-Spiel, aber mit einem Quellcode, der mich wach werden lies. Die Programmteile (Viewer, Controller etc.) sind völlig unabhängig voneinander und können problemlos entfernt, ersetzt oder erweitert werden, ohne dass die anderen Quelltexte auch groß bearbeitet werden müssten - zwei Zeilen (import, Registrierung) reichen völlig! Kurz: Ich war beeindruckt vom Quellcode, v.a. von dessen Verständlichkeit und Modularität. Und da hab ich mir in einem Anflug von Motivation gedacht: Machste mal ein kleines Projekt genauso wie der Macher des Pong-Spiels... Ich denke, ich hab nun einen Teil vom "Killer-Feature" für OOP endlich gefunden ;) . Und ich verstehe nicht, weshalb kaum ein OOP-Tutorial, gleich welcher Programmiersprache, das MVC-Muster erwähnt. Es wird zwar maximal erwähnt, dass es mit OOP leichter möglich ist, verschiedene Quelltexte auszutauschen, aber wie man sowas genau macht, kann man ohne Hilfe eigentlich nur erraten :K .
BlackJack

@Astorek: Erster Eindruck: Es sieht ziemlich „over engineered” aus, beinahe „javaesque”. Die schreibweise der Methodennamen und doppelte führende Unterstriche tragen da auch zu bei. Man kann es mit dem entkoppeln auch übertreiben und alles komplizierter machen als nötig.

Diese Eventgeschichte mit den vielen Klassen und den Kaskaden von ``if``/``elif``\s die dann mit `isinstance()` prüfen welchen Typ man denn nun genau vor sich hat, gefällt mir überhaupt nicht. Das ist nicht wirklich OOP, sondern eher die guten (oder eben auch nicht guten) alten ``switch``-Statements aus C-Programmen. Die `Event`-Basisklasse wird nicht verwendet, die kann weg.

Die Spalten `rows` zu nennen ist ein wenig verwirrend. ;-)
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

BlackJack hat geschrieben:@Astorek: Erster Eindruck: Es sieht ziemlich „over engineered” aus, beinahe „javaesque”. Die schreibweise der Methodennamen und doppelte führende Unterstriche tragen da auch zu bei. Man kann es mit dem entkoppeln auch übertreiben und alles komplizierter machen als nötig.
Okay, Danke für deine ehrliche Meinung :) . Hat mir grade ein wenig den Höhenflug mit dem Projekt geraubt (aber auch vollkommen zurecht^^).
Diese Eventgeschichte mit den vielen Klassen und den Kaskaden von ``if``/``elif``\s die dann mit `isinstance()` prüfen welchen Typ man denn nun genau vor sich hat, gefällt mir überhaupt nicht. Das ist nicht wirklich OOP, sondern eher die guten (oder eben auch nicht guten) alten ``switch``-Statements aus C-Programmen.
Eine Frage dazu: Welcher Ansatz wäre hier passender? Mangels Wissen und Erfahrung komme ich nicht wirklich drauf, wie man sowas besser lösen könnte... Mir fällt dazu als Alternative nur ein, dass die Events direkt die Tätigkeiten als Methode schon implementiert hätten - aber das wäre IMHO noch unübersichtlicher...
Die `Event`-Basisklasse wird nicht verwendet, die kann weg.
Stimmt. War ein Relikt aus dem erwähnten Pong-Spiel, das ich 1:1 übernommen habe...
Die Spalten `rows` zu nennen ist ein wenig verwirrend. ;-)
Da hab ich auch ein paarmal den Hund vergraben, stimmt^^.

Danke für deine Kritik. :)
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@Astorek: hier mal von oben nach unten, was mir so an Deinem Code aufgefallen ist.
In „4gewinnt.py“: »exit« wird nicht verwendet (auch besser so), warum bekommen »viewer« und »controler« »game« nicht über __init__? Warum gibt es einen CPUSpinner, statt dass der Eventmanager einen Eventloop besitzt?
Warum sendet »check_input« im Fehlerfall ein Event aber im Erfolgsfall gibt es einen Rückgabewert? Wie kann ein »SyntaxError« oder »NameError« oder »EOFError« entstehen? Das binden von »keyb_key« an None ist unnötig. Warum hat der Eventmanager einen Status? Das hat doch nichts mit Events zu tun. Warum ist »keyb_key« in »CmdController« ein Attribut?
»EventManager.unregister« ist fehlerhaft: self.listener ist kein Dictionary sondern eine Liste.
Die Event-Klassen sind die meisten eine seltsame Art Aufzählungskonstante zu definieren. Die gemeinsame Oberklasse »Event« hat keinen Mehrwert.
In Col.__init__ sollten alle Attribute gesetzt werden. Ist ist nicht gleich ersichtlich, ob »self.height« oder »self.emptyvalue« überhaupt jemals gesetzt werden. Für emptyvalue gibt es in Python einen bestimmten Wert »None«. height ist überflüssig, da schon durch die Länge von col gegeben.
»is_full« prüft das Gegenteil. Wenn man Col eine __nonzero__-Methode gönnt, kann man die Schleife einfach durch any(self.col) ersetzen.
Da fällt mir gerade auf, warum haben Gamefield und Controller überhaupt ein game-Attribut? Die Kreuz-und-Querverkettung macht doch das Programm undurchschaubar. Der Controller senden ein RequestDrop mit dem Spieler eines ihm zugeordenten Spiels an alle zuhörenden Spiele. Die Anzahl der Spalten ist im Controller fest verdrahtet, wird im Game aber nicht geprüft und wirft im GameField gegebenenfalls einen IndexError. Warum wird dem Spielfeld eigentlich nicht der RequestDrop-Event gesendet? Oder gleich der Spalte? Nein, das Spiel bekommt die Anfrage, schickt sich selbst dann das Event, tatsächlich einen Stein einzuwerfen, wenn es geht. Aber bevor der Stein überhaupt gefallen ist, fragt es schon nach dem Gewinner?
Auf mich macht die ganz Event-Geschichte den Eindruck, dass alles sehr hart miteinander gekoppelt ist, durch das Zwischenschalten der Events aber die Abhäningkeiten, verwischt werden und damit alles undurchschaubar wird. Die Events sorgen eben gerade nicht dafür, dass die einzelnen Objekte voneinander unabhängig werden. Selbst das View reagiert nicht auf Events, sondern wird durch Events ferngesteuert. Du denkst, oh ich muß etwas auf dem Bildschirm ausgeben, deshalb sende ich ein passendes Event.
Dabei muß es derjenige, der das Event sendet, eben gerade nicht daran denken, was er damit bewirken will, sondern er teilt nur mit, dass sich für ihn etwas geändert hat.

Hier mal eine Beispieleventkette: Eine Taste wird gedrückt, der Controller sendet "Taste gedrückt". Das Hauptmenü prüft ist es "q" für Beenden oder "n" für Neu? Nein, also ignoriere ich das Event. Das Spielfeld prüft, ist es eine Zahl, dann ist die Spalte voll, falls ja sende Spalte-voll-Event, nein, sende Drop-Event. Das View fängt das Drop-Event ab und malt das Spielfeld neu. Das Game fängt das Drop-Event ab und prüft, hat jemand gewonnen? usw.
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Sirius3, ebenfalls Danke für die Rückmeldung. :)
Sirius3 hat geschrieben:warum bekommen »viewer« und »controler« »game« nicht über __init__?
Ich hielt es der Übersicht wegen für logischer, das in die main() zu packen. Sonst kann man beim Drüberfliegen vom Quellcode nicht sofort erkennen, woher »viewer« und »controller« ihre games-Werte herbekommen. Zumal, zumindest theoretisch, nicht jeder Viewer und nicht jeder Controller Zugriff auf die game-Klasse benötigt (im Gegensatz zur "gamefield"-Klasse, die aber selbst ein Modell ist). So hab ichs mir zumindest gedacht^^.
Warum gibt es einen CPUSpinner, statt dass der Eventmanager einen Eventloop besitzt?
Nach längerem Überlegen gebe ich dir recht: Es wär sinniger, wenn der EventManager wenigstens die Grundlegendsten Events, die in jedem Programm vorkommen, abarbeiten könnte. Danke dafür^^.
Warum sendet »check_input« im Fehlerfall ein Event aber im Erfolgsfall gibt es einen Rückgabewert?
Die Funktion kann man nur verlassen, wenn man eine geeignete Eingabe macht - beim Verlassen der Funktion gibts also immer einen Rückgabewert. Dass ein Event bei einer fehlerhaften Eingabe gesendet wird, wollte ich, damit im Viewer eben ein Fehler angezeigt wird (und ich merke gerade selbst, dass das irgendwie eine ziemlich dumme Idee war^^).
Wie kann ein »SyntaxError« oder »NameError« oder »EOFError« entstehen?
EOFError: Unter Windows Strg+Z drücken. Irgendwie hab ichs unter Linux auch zusammengekriegt, NameError und - ernsthaft - sogar einen SyntaxError zu provozieren (ich habe selbst keine Ahnung, warum ausgerechnet ein Syntaxerror bei einer fehlerhaften Eingabe erscheint). Ich werd mich heute Nachmittag nochmal vor einem Linux-PC hinsetzen und versuchen, den Fehler zu provozieren.
»EventManager.unregister« ist fehlerhaft: self.listener ist kein Dictionary sondern eine Liste.
*Hält die Hand vors Gesicht* Ich Schlaumeier hab das ausgerechnet mit einer Liste aus Zahlen getestet, die genau dieselben Werte enthielten wie deren jeweilige Indexposition. Danke für die Berichtigung, es wär mir sonst nicht aufgefallen^^.
Für emptyvalue gibt es in Python einen bestimmten Wert »None«.
Stimmt - leider ist die Ausgabe im Viewer (noch) durch die Werte in den cols gekoppelt, aber wesentlich sauberer wärs, wenn es überhaupt keinen Wert kriegen würde.
height ist überflüssig, da schon durch die Länge von col gegeben.
Tatsächlich? :shock: Die Länge von col hängt doch von height (bzw. von self.height) ab - oder verstehe ich das gerade falsch?
»is_full« prüft das Gegenteil. Wenn man Col eine __nonzero__-Methode gönnt, kann man die Schleife einfach durch any(self.col) ersetzen.
Wieder ein Punkt in meiner Checkliste "Nützliches für Python, was ich nicht kenne"^^. Danke für den Tipp, genau sowas hab ich gesucht.
Da fällt mir gerade auf, warum haben Gamefield und Controller überhaupt ein game-Attribut? Die Kreuz-und-Querverkettung macht doch das Programm undurchschaubar.
So wie ich das verstanden habe, soll das beim MVC-Muster ja gerade der Trick sein, dass die einzelnen Module (eben Controller, aber auch Modelle untereinander) maximal lesend auf die Attribute anderer Objekte zugreifen dürfen. Controller braucht das an einer Stelle, um per Event auf den Spielernamen zugreifen zu können... Und bei Gamefield ist mir wohl ein Fehler passiert, da hab ich grad selber keine Ahnung, weshalb es ein game-Attribut bräuchte^^.
Auf mich macht die ganz Event-Geschichte den Eindruck, dass alles sehr hart miteinander gekoppelt ist, durch das Zwischenschalten der Events aber die Abhäningkeiten, verwischt werden und damit alles undurchschaubar wird. Die Events sorgen eben gerade nicht dafür, dass die einzelnen Objekte voneinander unabhängig werden. Selbst das View reagiert nicht auf Events, sondern wird durch Events ferngesteuert. Du denkst, oh ich muß etwas auf dem Bildschirm ausgeben, deshalb sende ich ein passendes Event.
Dabei muß es derjenige, der das Event sendet, eben gerade nicht daran denken, was er damit bewirken will, sondern er teilt nur mit, dass sich für ihn etwas geändert hat.
Den letzten Satz sollte ich mir hinter die Ohren schreiben^^. Das war meine Befürchtung, weil ich mich beim Coden auch dauernd dabei ertappt habe, zu fragen, ob ich denn die Zuständigkeiten richtig ausgewürfelt habe.

Auf alle Fälle vielen Dank für deine ausführlichen Verbesserungsvorschläge (und gerade wegen Beiträgen wie von deiner oder von BlackJack schaue ich gerne ins Forum rein - ehrliche Antworten sind immer noch die Besten. :) ) . Da habe ich doch noch mehr Arbeit vor mir als ich wahrhaben wollte...
BlackJack

@Astorek: `NameError` und `SyntaxError` können passieren wenn man unter Python 2 die `input()`-Funktion verwendet. Weswegen man das auch nicht tun sollte. Und Du tust das ja auch nicht (mehr?).

Auf `EOFError` (Ctrl+D unter Linux) würde ich aber so nicht reagieren, denn wenn die Eingabedatei am Ende ist, dann würde man von dort keine weiteren Daten mehr erwarten.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Mein naiver Ansatz wäre ja, überhaupt keinen Event-Manager zu benutzen und einfach direkt auf einem `Event`-Objekt zu arbeiten. In seiner einfachsten Form, die ohne übergebene Parameter auskommt, könnte das dann so aussehen:

Code: Alles auswählen

from __future__ import print_function

class Event(object):
    def __init__(self):
        self.callbacks = set()

    def connect(self, callback):
        self.callbacks.add(callback)

    def emit(self):
        for callback in self.callbacks:
            callback()

class Foo(object):
    def __init__(self):
        self.spam_received = Event()

    def write(self, text):
        if text == 'spam':
            self.spam_received.emit()

def log_spam_event():
    print('spam received')

def main():
    foo = Foo()
    foo.spam_received.connect(log_spam_event)
    for text in ('spam', 'ham', 'egg', 'foo', 'bar', 'baz', 'spam'):
        foo.write(text)

if __name__ == '__main__':
    main()
BlackJack

@snafu: So etwas verwende ich auch. Ich habe statt `emit()` die `__call__()`-Methode verwendet und reiche da auch *args und **kwargs zu den Rückruffunktionen weiter. :-)
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hier noch ein Beispiel mit Parametern (und ein *kleines* bißchen realitätsnäher):

Code: Alles auswählen

from __future__ import print_function
from datetime import datetime

class Event(object):
    def __init__(self):
        self.callbacks = set()

    def connect(self, callback):
        self.callbacks.add(callback)

    def emit(self, *args, **kwargs):
        for callback in self.callbacks:
            callback(*args, **kwargs)

class TextStream(object):
    def __init__(self):
        self.text_received = Event()
        self.content = ''

    def write(self, text):
        self.text_received.emit(text)
        self.content += text

def log_text_input(text):
    msg = '{}: Received text input: {!r}'
    print(msg.format(datetime.now(), text))

def main():
    stream = TextStream()
    stream.text_received.connect(log_text_input)
    for text in ('spam', 'ham', 'egg'):
        stream.write(text)

if __name__ == '__main__':
    main()
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Zuerst mal: Sorry, dass ich mich erst jetzt wieder melde. Gleich darauf: Vielen, VIELEN Dank für die Anregungen. Ich glaube, mir ist die Herangehensweise klar geworden^^.

Wenn ich das richtig verstehe, könnte ich - so ganz grob - folgendermaßen programmieren:

Eine "event.py" kann ich theoretisch so aufbauen (der Einfachheit halber einfach mal von "list" geerbt...):

Code: Alles auswählen

class Event(list):
    def notify(self, *args):
        for listener in self:
            listener(*args)
In einer "models.py" könnte ich dann die entsprechenden Events werfen:

Code: Alles auswählen

import event

class Gamefield(object):
    def __init__(self):
        # [... typischer Initialisierungsprozess ... ]
        self.fullEvent = event.Event()
        self.throwEvent = event.Event()
    def throw(self):
        if # [... If-Prüfung, falls Spielfeld voll etc... ]
            self.fullEvent.notify()
        if # [... If-Prüfung, falls erfolgreicher Wurf etc... ]
            self.throwEvent.notify()
Schlussendlich könnte ich dann Funktionen in den entsprechenden Abschnitten schreiben und diese dann an die Events binden (bspw. main.py):

Code: Alles auswählen

def stein_geworfen():
    print("Stein geworfen")
    # [... weiterer Code... ]

def spielfeld_voll():
    print("Spielfeld voll")
    # [... weiterer Code... ]


def main():
    gamefield = models.Gamefield()
    gamefield.fullEvent.append(spielfeld_voll)
    gamefield.throwEvent.append(stein_geworfen)
    # [... Weiterer Code um das Ding in Gang zu bringen etc... ]


if __name__ == "__main__":
    main()
Hab ich das so richtig verstanden, oder habe ich irgendwo einen Verständnisfehler drin? Ich bin mir nicht sicher, ob das so richtig ist^^...

... Unabhängig davon merke ich dann doch Schritt für Schritt, wie sich mein Verständnis für OOP immer mehr bessert. Und das habe ich euren Beiträgen zu verdanken, deshalb: Vielen, vielen Dank für eure Beiträge! :)
Antworten