Konsolen-TicTacToe in OOP

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, um mich ein bisschen mit Klassen und Objekten zu beschäftigen (wenn auch nur grob^^), wollte ich einfach mal ein konsolenbasiertes TicTacToe-Spiel entwerfen. Die Nummernblöcke auf der Tastatur dienen dabei quasi als Eingabefeld.

Der Code ist in Python2 geschrieben, wg. dem print-Statement nicht in Python3 aufrufbar. Ich habe mich dabei bemüht, Spiellogik und Ausgabe so gut es geht zu trennen und hoffe, dass ich einigermaßen nachvollziehbar programmiert habe. Vorallem, da ich mit OOP eher theoretische Erfahrung habe, weiß ich nicht, inwieweit mein Code "brauchbar" ist (im Sinne von: "Diesen Programmierstil kann ich beibehalten" oder auch "wirf den Programmierstil so weit es geht über den Haufen"; sinnvolle Verwendung von Klassen/Objekten?). Ich würde mich dementsprechend über Feedback und Verbesserungsvorschläge freuen :)

Der Quellcode:

http://pastebin.com/NgeK05zS#

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sys import exit

class Game():
    ''' Verwaltet das Spielfeld und die Spieler '''
    
    constants = {
        "EMPTY"           : " ",
        "SYMBOL_PLAYER_1" : "X",
        "SYMBOL_PLAYER_2" : "O",
    }
    
    def __init__(self):
        # Das Spielfeld generieren
        self.gamefield = [
            [
                Game.constants["EMPTY"]for i in range(3)
            ]
            for j in range(3)
        ]
        
        # Die Spieler generieren
        self.players = (Player(Game.constants["SYMBOL_PLAYER_1"], True),
                        Player(Game.constants["SYMBOL_PLAYER_2"])
        )
        
    def switchPlayersTurn(self):
        ''' wechselt, welcher Spieler gerade dran ist '''
        for player in self.players:
            player.turn = not player.turn
        
    def place(self, (ypos, xpos)):
        ''' Platziert das Spielerobjekt im angegebenen Feld '''
        for player in self.players:
            if player.turn is True:
                self.gamefield[ypos][xpos] = player.char
                break
                
    def isPlacePossible(self, (ypos, xpos)):
        ''' Gibt zurück, ob eine Platzierung möglich ist '''
        if self.gamefield[ypos][xpos] == Game.constants["EMPTY"]:
            return True
        else:
            return False
            
    def isSomeoneWinner(self):
        ''' Prüft, ob ein Spieler gewonnen hat '''
        # Waagrechte Prüfung
        for x in self.gamefield:
            for player in self.players:
                if x.count(player.char) == 3:
                    return True
        
        # Senkrechte Prüfung
        for x in range(3):
            for player in self.players:
                if self.gamefield[0][x] == player.char and \
                   self.gamefield[1][x] == player.char and \
                   self.gamefield[2][x] == player.char:
                       return True
        
        # Diagonale Prüfung
        for player in self.players:
            if self.gamefield[0][0] == player.char and \
               self.gamefield[1][1] == player.char and \
               self.gamefield[2][2] == player.char:
                   return True
            elif self.gamefield[0][2] == player.char and \
                 self.gamefield[1][1] == player.char and \
                 self.gamefield[2][0] == player.char:
                   return True
        return False
        
    def isGamefieldFull(self):
        ''' Prüft, ob das Spielfeld voll und keine Züge mehr möglich sind '''
        for x in self.gamefield:
            for y in x:
                if y == Game.constants["EMPTY"]:
                    return False
        return True
        

class GUI():
    ''' Stellt Funktionalität für eine grafische Ausgabe bereit '''
    def __init__(self, game):
        self.game = game
        
    def showPlayersTurn(self):
        ''' Zeigt, welcher Spieler an der Reihe ist '''
        if self.game.players[0].turn is True:
            print "Spieler 1 ist an der Reihe!"
        elif self.game.players[1].turn is True:
            print "Spieler 2 ist an der Reihe!"
        
    def showGamefield(self):
        ''' Stellt das Spielfeld in ASCII-Zeichen dar '''
        # TODO: Komplizierter gings nicht, oder?
        j = 0
        for x in reversed(self.game.gamefield):
            i = 0
            for y in x:
                print y,
                if i != 2:
                    print "|",
                i += 1
            if j < 2:
                print "\n--+---+--"
                j += 1
        print

    def showPlaceNotPossible(self):
        print "An der Position kannst du nicht setzen!"
        
    def showOptions(self):
        print "Tasten auf dem Nummernblock druecken, um die Position",
        print "zu bestimmen:"
        
    def showGamefieldIsFull(self):
        print "Das Spielfeld ist voll, und niemand hat gewonnen!"
        
    def showWinner(self):
        print "Herzlichen Glueckwunsch, Spieler",
        if self.game.players[0].turn is True:
            print "1",
        else:
            print "2",
        print "hat das Spiel gewonnen!"
              
    def waitForInput(self):
        # TODO: Fehlerprüfung
        return int(raw_input())
            


class Player():
    ''' Stellt Spieler bereit '''
    def __init__(self, char=" ", turn=False):
        self.char = char  # "Symbol" des Spielers, entweder O oder X
        self.turn = turn  # ob der Spieler gerade dran ist

class Program():
    ''' Das Programm vermengt alles zu einem schönen Brei^^ '''
    def convertInput(self, input):
        '''
        Diese Methode wandelt die Eingabe des Nummernblocks um.
        
        Da die Klasse Game() nur mit Y- und X-Koordinaten etwas an-
        fangen kann, der Benutzer aber nur ein Zeichen beim
        Nummernblock eintragen muss, muss eine Methode her, die dieses
        Zeichen in Y- und X-Koordinaten umwandelt. Die Game()-Klasse
        kann (soll?) ja nicht wissen, WIE der Input vonstatten kommt,
        es soll nur mit den richtigen Daten gefüttert werden.
        '''
        input -= 1
        xpos = input % 3
        ypos = input // 3
        return ypos, xpos
        
    def run(self):
        game = Game()
        gui = GUI(game)

        gui.showGamefield()
        running = True

        while running:
            gui.showPlayersTurn()
            gui.showOptions()
            key = gui.waitForInput()
            y, x = self.convertInput(key)
            if game.isPlacePossible((y, x)) is False:
                gui.showPlaceNotPossible()
            else:
                game.place((y, x))
                if game.isSomeoneWinner():
                    gui.showWinner()
                    running = False
                elif game.isGamefieldFull():
                    gui.showGamefieldIsFull()
                    running = False
                else:
                    game.switchPlayersTurn()
            gui.showGamefield()
    
if __name__ == "__main__":
    Program().run()
BlackJack

@Astorek: Okay, Achtung, Kritik. Aber Du hast ja gefragt. :-)

Es fängt mit einem unbenutzen Import an. `exit()` wird nirgends verwendet.

In Python 2 sollte man von `object` erben, denn nur so bekommt man „new style”-Klassen bei denen auch alles funktioniert was Python so an Eigenschaften bietet. Zum Beispiel `property()` funktioniert nur dann richtig.

DocStrings werden üblicherweise in """ statt in ''' gesetzt. Obwohl das *eigentlich* egal sein sollte hatte selbst das `pydoc`-Modul aus der Standardbibliothek irgendwann mal Probleme mit ''' weil es einfach nur von """ ausgegangen ist.

`Game.constants` ist unnötig kompliziert, man kann die Schlüssel direkt als Attribute auf der Klasse setzen ohne den Umweg über das Wörterbuch.

Die Kommentare in `Game.__init__()` sind überflüssig weil sie nur das sehr offensichtliche beschreiben.

Ein weniger generischer Name für `Game` wäre `TicTacToe`.

`Player` ist in der Tat eine fragwürdige Klasse in dieser Form. Sie hat kein Verhalten ausser ein Objekt zu sein, aber auch bei den Daten kann man die Frage stellen ob sie dort hingehören. Denn wer an der Reihe ist sollte eigentlich in die Verantwortung des Spiels fallen. Der Spieler selber braucht das nicht wissen solange es auf der Klasse kein Verhalten gibt was die Information nutzt. Zudem wird das Programm durch dieses Attribut komplizierter und fehleranfälliger. `Game` muss immer alle Spieler durchgehen um den einzigen Aktiven zu finden, statt sich direkt den aktiven Spieler zu merken. Die Information ist auch redundant: Immer wenn `turn` bei einem Spieler wahr ist, *muss* es beim anderen falsch sein. Da wird *eine* Information, an *mehreren* Stellen mit *unterschiedlichen* Werten gespeichert, die man immer synchron halten muss. So etwas sind immer Fehlerquellen.

Der Default-Wert von `char` in `Player.__init__()` macht keinen Sinn, weil es hoffentlich nie ein Spielerobjekt mit diesem Wert geben wird. Wenn im Kommentar zu dem Attribut `char` steht es wäre ein "Symbol" welches die Werte X und O annehmen kann, sollte man vielleicht das Attribut auch `symbol` nennen, die Konstanten für die Werte von `Game` auf `Player` verschieben und in der `__init__` auch überprüfen ob der übergebene Wert einer der beiden konstanten Werte entspricht. Dann kann man den Kommentar weg lassen.

Auf der anderen Seite fehlt Information im Spieler Objekt, nämlich um welchen Spieler es sich handelt. Statt zum Beispiel in der GUI-Klasse in die Spielklasse zu greifen auf das Tupel mit den beiden Spielern zuzugreifen und aufgrund der Position dort zu entscheiden wie der Spieler als Zeichenkette angezeigt werden soll ist nicht so schön. Die GUI-Klasse sollte die Spielklasse nach dem aktuellen Spieler fragen und den Spieler dann wie er gerne als Zeichenkette aussehen möchte. Also so etwas wie: ``print '{0} ist an der Reihe!'.format(game.active_player)``. Wobei `active_player` durchaus eine „Methode” in Form eines `property()` sein kann. Hauptsache die GUI-Klasse weiss nicht so viel darüber in was für einer Datenstruktur die Spieler gespeichert sind.

Die schreibweise der Methodennamen entspricht nicht der Empfehlung aus dem Style Guide.

Und nun zu einem Programmfehler: ``is`` ist zum Vergleichen von Objektidentitäten und *nicht* zum Vergleichen von Werten. Wenn Dein Programm so funktioniert dann ist das nur Zufall denn es ist in Python 2 nicht garantiert das jeder Wahrheitswert tatsächlich das selbe Objekt ist.

Speziell bei den Wahrheitswerten ist ein Vergleich aber sowieso unsinnig denn bei ``a_bool == True`` kommt grundsätzlich wieder der gleiche Wert heraus den `a_bool` sowieso schon hatte. Das ist als wenn man überall wo man eine ganze Zahl hat einfach noch mal ``an_int + 0`` schreibt. Das addieren von 0 ist von weitem betrachtet nicht falsch, aber hochgradig unnsinnig. :-)

Ähnlich verhält es sich mit Quelltext der ein ``if``/``else`` mit einer Bedingung enthält und je nach dem entweder ``return True`` oder ``return False`` in den beiden Zweigen zu stehen hat. Die Bedingung beim ``if`` wird doch schon zu einem Wahrheitswert ausgewertet, den kann man *direkt* ohne ein zusätzliches, unnötiges ``if``/``else``-Konstrukt zurück geben.

Da das `TicTacToe`-Spielerobjekt hauptsächlich aus einem Spielfeld besteht, könnte man es als Container für die Kästchen auffassen und einen entsprechenden Zugriff über den Indexoperator zur Verfügung stellen. Und es iterierbar über die Zeilen des Spielfelds machen.

Der Name `x` für eine Zeile des Spielfelds ist unschön. Die meistem Programmierer würden bei dem Namen eine Zahl erwarten. Genau wie bei `y`.

Die Anzeige des Spielfelds ist in der Tat kompliziert geschrieben. Warum speicherst Du die Zeilen ”verkehrt” herum? Statt einen Laufindex parallel zum iterieren über ein Objekt manuell zu verwalten sollte man die `enumerate()`-Funktion verwenden. Das wäre hier alles nicht nötig wenn Du erst eine komplette Zeichenkette erstellst und Gebrauch von der `join()`-Methode machen würdest.

Die Trennung von `GUI` und `Program` ist IMHO nicht sinnvoll. GUI ist voll von trivialen Methoden die eigentlich nur ``print``-Ausgaben sind und `Program` enthält den Spielablauf als lineares Programm. Also kann man GUI sowieso nicht durch etwas anderes ersetzen, wie zum Beispiel eine grafische GUI die ereignisbasiert arbeitet ohne dass man auch ein neues `Program` dazu schreiben müsste. Die Trennung macht das vorhandene Programm nur komplizierter ohne einen wirklichen Nutzen zu haben.

Die `convertInput()`-”Methode” würde auch eher zur GUI gehören, denn die Klasse sollte ja die Benutzerinteraktion kapseln. Es ist semantisch auch gar keine Methode genau so wenig wie `run()` eine Methode ist. Das sind einfach nur Funktionen, die Du ohne Not in eine Klasse gesteckt hast. Es gibt keinen gemeinsamen Zustand auf dem diese ”Methoden” operieren.

Ein Warnzeichen für so etwas ist wenn man ein Exemplar so einer ”Klasse” nur erstellt um eine einzige ”Methode” darauf aufzurufen und sich nie für das Exemplar selber interessiert, weil das ein zweckfreies Objekt wäre. Es gibt Fälle wo bei so einem Verwendungsmuster eine Klasse Sinn machen kann, aber das ist eher selten.

Quasi rekursiv kann man in Frage stellen ob `GUI` eine echte Klasse ist, denn der einzige Zustand ist `game` und benutzt wird das ganze nur von einer einzigen Funktion (`run()`) aus.

Die Umwandlung der Eingabe kann man mit der `divmod()`-Funktion deutlich vereinfachen. Das ist ein Einzeiler: ``x, y = divmod(digit - 1, 3)``.

`GUI.showGamefield()` wird in `run()` einmal vor der Schleife und am Ende jeden Schleifendurchlaufs aufgerufen. Das kann man *einmal* am *Anfang* jeden Schleifendurchlaufs machen. Dorthin kann man auch den Test auf Gewinner oder Unentschieden verschieben und erst *danach* Benutzereingaben zulassen. Denn *dann* kann man die Variable `running` weg lassen denn nun kann man die Schleife bei dem Test einfach verlassen ohne das sich an der Semantik etwas ändert. Ausserdem könnte man das Programm nun um das laden eines Spielstandes erweitern ohne sich das Problem einzuhandeln das der Benutzer nach einem Zug gefragt wird bevor überhaupt überprüft wird ob nicht schon jemand in dem Spielstand gewonnen hat, oder ob bereits alle Felder belegt sind.

Insgesamt kann man durch entfernen von `GUI` und `Program` zu einer viel einfacheren Funktion kommen. Deutlich weniger Komplexität bei gleicher Funktionalität:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class Player(object):
    """ Stellt Spieler bereit """

    SYMBOL_PLAYER_1 = 'X'
    SYMBOL_PLAYER_2 = 'O'

    def __init__(self, name, symbol, turn=False):
        if symbol not in [self.SYMBOL_PLAYER_1, self.SYMBOL_PLAYER_2]:
            raise ValueError('symbol must be a SYMBOL_PLAYER_... constant')
        self.name = name
        self.symbol = symbol
        self.turn = turn  #: ob der Spieler gerade dran ist
        #
        # TODO Attribut `turn` entfernen und die Information in die Spielklasse
        #   verschieben.
        #
    
    def __str__(self):
        return '{0.name} ({0.symbol})'.format(self)


class TicTacToe(object):
    """ Verwaltet das Spielfeld und die Spieler """

    EMPTY = ' '

    def __init__(self):
        self.gamefield = [[self.EMPTY] * 3 for _ in range(3)]
        self.players = (
            Player('Spieler 1', Player.SYMBOL_PLAYER_1, True),
            Player('Spieler 2', Player.SYMBOL_PLAYER_2)
        )

    def __iter__(self):
        return iter(self.gamefield)

    def __str__(self):
        return '\n--+--+--\n'.join(
            '|'.join(cell + ' ' for cell in row) for row in self
        )

    @property
    def active_player(self):
        for player in self.players:
            if player.turn:
                return player
        assert False, 'No active player found'

    def switch_players(self):
        """ wechselt, welcher Spieler gerade dran ist """
        # 
        # TODO Von dem `turn`-Attribut weg kommen.
        # 
        for player in self.players:
            player.turn = not player.turn

    def place(self, (ypos, xpos)):
        """ Platziert das aktuelle Spielerobjekt im angegebenen Feld """
        self.gamefield[ypos][xpos] = self.active_player.symbol

    def is_place_possible(self, (ypos, xpos)):
        """ Gibt zurück, ob eine Platzierung möglich ist """
        return self.gamefield[ypos][xpos] == self.EMPTY

    def is_someone_winner(self):
        """ Prüft, ob ein Spieler gewonnen hat """
        # 
        # Waagrechte Prüfung
        # 
        for row in self:
            for player in self.players:
                if row.count(player.symbol) == 3:
                    return True
        # 
        # Senkrechte Prüfung
        # 
        for i in range(3):
            for player in self.players:
                if self.gamefield[0][i] == player.symbol and \
                   self.gamefield[1][i] == player.symbol and \
                   self.gamefield[2][i] == player.symbol:
                    return True
        # 
        # Diagonale Prüfung
        # 
        for player in self.players:
            if self.gamefield[0][0] == player.symbol and \
               self.gamefield[1][1] == player.symbol and \
               self.gamefield[2][2] == player.symbol:
                return True
            elif self.gamefield[0][2] == player.symbol and \
                 self.gamefield[1][1] == player.symbol and \
                 self.gamefield[2][0] == player.symbol:
                return True

        return False

    def is_gamefield_full(self):
        """ Prüft, ob das Spielfeld voll und keine Züge mehr möglich sind """
        return all(all(cell != self.EMPTY for cell in row) for row in self)


def input_cell_coordinates():
    # TODO: Fehlerprüfung
    return divmod(int(raw_input()) - 1, 3)


def main():
    game = TicTacToe()
    while True:
        print game
        print
        if game.is_someone_winner():
            # 
            # Zurück zum Spieler der den letzten Zug gemacht hat und damit
            # gewonnen hat.
            # 
            game.switch_players()
            print 'Herzlichen Glueckwunsch, {0} hat das Spiel gewonnen!'.format(
                game.active_player
            )
            return
        if game.is_gamefield_full():
            print 'Das Spielfeld ist voll, und niemand hat gewonnen!'
            return
        print '{0} ist an der Reihe!'.format(game.active_player)
        print 'Tasten auf dem Nummernblock druecken, um die Position',
        print 'zu bestimmen:'
        coordinates = input_cell_coordinates()
        if game.is_place_possible((coordinates)):
            game.place((coordinates))
            game.switch_players()
        else:
            print 'An der Position kannst du nicht setzen!'


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

@BlackJack: Vielen Dank für deine Kritik :) und generell vielen Dank, dass du dich immer wieder meiner annimmst, wenn ich hier alle Jubeljahre mal was schreibe^^. (Ernsthaft: Danke dafür! :) )

Alles in allem hast du - neben diversen "Formalien" - genau die Punkte angesprochen, bei der ich ein mulmiges Gefühl beim Programmieren hatte, z.B. die erwähnte, in dieser Form eher unsinnige "Player"-Klasse. Auf die Idee mit einer "get_active_Player"-Methode hätte ich auch selbst kommen können *flache Hand auf die Stirn hau*.
Die Anzeige des Spielfelds ist in der Tat kompliziert geschrieben. Warum speicherst Du die Zeilen ”verkehrt” herum?
Ehrliche Antwort: Weil die Ausgabe sonst fehlerhaft gewesen wäre und z.B. die Eingabe von "3" das Feld oben rechts (statt unten rechts) gefüllt hätte. Zugegeben ein ziemlich hässlicher Workaround, weil ich es mit der "convertInput"-Methode nicht besser hinbekam und keinesfalls etwas, das man anwenden sollte^^.
Statt einen Laufindex parallel zum iterieren über ein Objekt manuell zu verwalten sollte man die `enumerate()`-Funktion verwenden. Das wäre hier alles nicht nötig wenn Du erst eine komplette Zeichenkette erstellst und Gebrauch von der `join()`-Methode machen würdest.
Danke für den Hinweis, "enumerate" ist auf meiner ToDo-Liste.
Die Trennung von `GUI` und `Program` ist IMHO nicht sinnvoll.
[...]
Ein Warnzeichen für so etwas ist wenn man ein Exemplar so einer ”Klasse” nur erstellt um eine einzige ”Methode” darauf aufzurufen und sich nie für das Exemplar selber interessiert, weil das ein zweckfreies Objekt wäre. Es gibt Fälle wo bei so einem Verwendungsmuster eine Klasse Sinn machen kann, aber das ist eher selten.
Da hab ich wohl noch ein paar Altlasten von Java mitgenommen^^. Danke für den Hinweis. Und bei näherer Überlegung ist die "run()"-Methode in der "Program"-Klasse wirklich besser als eigenständige Methode in "GUI" aufgehoben^^...
Die GUI-Klasse sollte die Spielklasse nach dem aktuellen Spieler fragen und den Spieler dann wie er gerne als Zeichenkette aussehen möchte.
Und noch ein Punkt, dass ich in meinem Denken "Prozedurales Programmieren ungleich Objektorientiertes Programmieren" hinzufügen kann^^. (Notiz an mich selbst: Wenn sich die GUI aus sämtlichen Objekten die Datenstrukturen herausfriemeln muss, was habe ich dann mit Klassen und Objekten verloren, wenn ich die Methoden genausogut 1:1 in klassenlose Funktionen auslagern kann?)

Nochmals Danke fürs Feedback! :)
Benutzeravatar
HarteWare
User
Beiträge: 69
Registriert: Samstag 23. Februar 2013, 21:16
Wohnort: localhost

Wow... ich bin wirklich beeindruckt, was man hier für konstruktive, nützliche Kritik bekommt.
Das sich jemand die Mühe macht, das anzuschauen, zu verbessern, und etliche Paragraphen darüber zu schreiben, einfach mal so, für ne fremde Person, das ist stark. Ganz ehrlich.
Wenn die Lehrer sowas nach einem missglückten Aufsatz bieten würden, hätte ich glaub nicht so miese Noten in Deutsch... :D
Dami123
User
Beiträge: 225
Registriert: Samstag 23. Februar 2013, 13:01

Das kannst du auch von Lehrern auf öffentlichen Schulen, die gerade so ihre Prüfung bestanden haben, nicht erwarten.
Die Schulqualität von öffentlichen Schulen schwankt auch stark von Bundesland und Stadt.

Deswegen sind Privatschulen immer besser, da diese nur Lehrer, System, ... verwenden, die angepasst und auf einem viel höheren Level sind.

Soviel zum Offtopic. :D

Finde die Qualität dieses Forums, im Vergleich mit vielen anderen im Netz existierenden, erste Klasse! :)
BlackJack

@Dami123: Privatschulen sind immer besser weil die angepasste Lehrer haben? System kam in dem Satz auch vor…

Sorry, aber ganz so pauschal kann man das nicht sagen. Es gibt Privatschulen wo ich auf keinen Fall meine nicht vorhandenen Kinder hinschicken würde.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dami123: Welche Privatschulen kennst Du? Der größte Teil der Privatschulen hat zwar ein anders System, angepasst an bestimmte Ideologien, ob das dadurch besser ist, mag jeder selbst bewerten. Auf öffentlichen Schulen haben nur die besten Absolventen jeden Jahrgangs überhaupt eine Chance. Und viele Lehrer auf Privatschulen sind froh, dass sie überhaupt noch einen schlecht bezahlten Job gefunden haben.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Sirius3 hat geschrieben:Und viele Lehrer auf Privatschulen sind froh, dass sie überhaupt noch einen schlecht bezahlten Job gefunden haben.
Je, nach Privatschule wird das ganze noch interessanter. Ich kenne jemanden der sein Staatsexamen nicht bestanden hat und dann auf einer Privatschule als Lehrer gelandet ist. Ich weiß ja nicht wie üblich das ist (und die Stichprobe ist wirklich extrem klein), aber für die angepriesene höhere Qualität spricht das sicher nicht.
Das Leben ist wie ein Tennisball.
Antworten