"Tempo, kleine Fische!", oder unfaires Spieldesign ist unfair.

Noam hat gestern dem Kleinmonster ein Brettspiel namens "Tempo, kleine Fische!" besorgt, da wir vermehrt das Kind mit Brettspielen etc. in Berührung bringen wollen.

Das Spiel selbst ist putzig aufgemacht, es hat kleine Fisch-Meeple, die Fischer-Meeple haben kleine Regenhüte und ein Plastikboot in dem das alles einen Platz hat ist auch dabei. Das Spielfeld besteht dickem, festen Karton, ist mit nettem Artwork bedruckt und ein sechsseitiger Farbwürfel ist auch dabei.

Thema das Spiels ist das Fischen. Konkret sind die Spielenden angehalten sich anfangs zu überlegen ob sie lieber zu den Fischen oder den Fischenden halten wollen, denn es geht beim Spiel effektiv um ein Rennen: Fisch gegen Fischerboot, den Fluss entlang zum Meer.

Der Fluss ist dabei dargestellt durch 11 schmalen Spielbrettstreifen, die aneinander gelegt werden, sowie jeweils ein großes Brettteil links (mit Fischerboot) und rechts (Das Meer, in das sich die Fische retten wollen.) davon.

Das Fischerboot ist also quasi "Brett 0", dann kommen 5 leere Brettstreifen, dann ein Streifen auf dem die Fische starten (Position 6, quasi.), dann wieder 5 leere Streifen und dann das Sicherheit versprechende Meer. (Position 12)

Das Spiel läuft so ab, daß jede Runde gewürfelt wird. Wird eine der Fischfarben gewürfelt, darf der Fisch eine Reihe weiter Richtung Meer ziehen. Wird eine der Fischerfarben gewürfelt (Die Fischermeeple sind hier Rot und Grün.), so wird der Brettstreifen der sich vor dem Boot befindet entfernt und das Boot nachgerückt. (Kommt also näher.)

War auf dem gerade entfernten Streifen ein oder mehrere Fische, so sind diese gefangen und werden ins Boot zu den Fischermeeples gelegt. Von nun an rückt das Boot auch dann weiter, wenn die Farbe eines gefangenen Fisches gewürfelt wird. (Jeder gefangene Fisch erhöht also die Chance, daß das Boot vorrücken darf.)

Soweit so einfach und für Kinder ab 3 Jahren sicher gut verständlich und nachvollziehbar.

Als ich die Regeln gelesen hab, hat sich allerdings sofort mein durch Jahrelanges Rollenspiel (Und Reste von gelernter Stochastik) geschärfter Instikt für Wahrscheinlichkeiten und Fairness im Spieldesign gemeldet und "Moment, das ist aber nicht fair, oder?".

Nachdem ich aber keine Lust (Und auch ehrlich gesagt nicht die Skillz) hatte um das ganze formal mathematisch zu analysieren habe ich das gemacht, was eins als Inf-Nerd dann halt so macht:

Ich hab das Spiel nachgecoded, es 10000 Mal durchrennen lassen und geschaut wie es ausgeht. XD

(Wen es interessiert, mein Quellcode für "Tempo, kleine Fische!" ist im Appendix. Nicht über das Denglish wundern, normalerweise code ich in Englisch, aber hier hab ich bewußt für mein deutschsprachiges Blog gecoded und das ganze hatte sich dann während dem Code komisch überschnitten und das mir auffiel wie durchtbar sich das liest hatte ich keine Lust mehr das ganze dann umzuschreiben.)

So, kommen wir also mal zur Analyse!

Grundsätzlich schaut mein Code so aus, daß das Spiel durch eine Klasse Tempo beschrieben wird. Um ein Spiel zu simulieren instanziiere ich diese Klasse und die Methode Tempo.ergebnis spielt das Spiel durch und gibt das Ergebnis als ("gefangene Fische", "gerettete Fische") zurück.

spiel = Tempo()
print(spiel.ergebnis())
(4, 0)

Hier haben also beispielsweise die Fischer alle Fische gefangen, bevor diese das Meer erreicht haben.

Jetzt können wir das ganze ein paar Mal machen und uns die Ergebnisse anschauen:

games = [Tempo().ergebnis() for x in range(10000)]

count = Counter(["Fischer" if x[0] > x[1] else "Fische" for x in games])

print("Die Fischer gewinnen etwa {}% der 10000 simulierten Spiele.".format(100 * count["Fischer"] / sum(count.values())))
Die Fischer gewinnen etwa 59.269% der 10000 simulierten Spiele.

Das Handbuch definiert einen Sieg der Fischer dadurch, daß diese es schaffen mehr als die Hälfte der Fische zu fangen. (Also 3 oder 4 Fische.) Und das passiert in ca. 60% aller Spiele.

Man sieht, mein Bauchgefühl hatte recht: Die Seite der Fischer hat einen klaren Vorteil.

Das wird umso krasser, wenn man sich die Zahlen etwas genauer ansieht:

print("Kein Fisch: \t{}%".format( 100 * len([x for x in games if x[0] == 0]) / len(games)))
print("Ein Fisch: \t{}%".format( 100 * len([x for x in games if x[0] == 1]) / len(games)))
print("Zwei Fische: \t{}%".format( 100 * len([x for x in games if x[0] == 2]) / len(games)))
print("Drei Fische: \t{}%".format( 100 * len([x for x in games if x[0] == 3]) / len(games)))
print("Vier Fische: \t{}%".format( 100 * len([x for x in games if x[0] == 4]) / len(games)))
Kein Fisch:     8.801%
Ein Fisch:      13.361%
Zwei Fische:    18.569%
Drei Fische:    24.742%
Vier Fische:    34.527%

Das bei weitem wahrscheinlichste Einzelergebnis ist, daß die Fischer einfach alle Fische fangen! Und das tatsächlich alle Fische gerettet werden passiert in weniger als 10% der Fälle!

Nachdem das ein Spiel für Kinder ist stellt mich das jetzt ein bißchen vor ein Dilemma: Ich würde das Spiel gern gut finden, weil es simpel und altersgerecht handhabbar ist, aber nachdem Kinder meiner Erfahrung nach sowohl einen sehr ausgeprägten Sinn für Fairness als auch die Tendenz zu unerwarteten Empathieschüben haben frage ich mich doch wie dieses Spiel tatsächlich von Kindern rezipiert wird.

Fiebern sie mit, weil "Ooooh, spannendes Rennen und die Fische sind in Gefahr!" oder werden sie irgendwann wütend, wenn sie merken wie wenig Chancen die Fische haben? Und was davon passiert, bevor sie gelangweilt das Spiel weglegen, weil es einfach nur stumpfsinniges Gewürfel ohne Entscheidungmöglichkeit ist?

Naja, whatever. Es hat nur ein paar Euro gekosten und mit ein bißchen Glück weckt es Kleinmonsters Interesse an Würfel und Brettspiel und so und wir können es bald mit besser designten Spiele locken.

Appendix: Quellcode

from collections import defaultdict, Counter
import random


class Boot(object):
    def __init__(self, farben, position=0):
        self.farben = farben
        self.position = position

    def __repr__(self):
        return "Fischerboot ({}, {})".format(self.farben, self.position)

class Fisch:
    def __init__(self, farbe, position=6):
        self.farbe = farbe
        self.position = position

    def __repr__(self):
        return "Fisch ({}, {})".format(self.farbe, self.position)

class Tempo:
    farben = ["Rot", "Grün", "Blau", "Rosa", "Orange", "Gelb"]

    def __init__(self):
        """Erstelle ein neues 'Tempo, kleine Fische!' Spiel."""

        self.spiel = {}

        # Boot als Shortcut zu Fischer
        self.boot = Boot(["Rot", "Grün"])


        for farbe in Tempo.farben:
            if farbe in ["Rot", "Grün"]:
                self.spiel[farbe] = self.boot
            else:
                self.spiel[farbe] = Fisch(farbe)



    def roll(self):
        """Würfle mit dem Farbenwürfel."""
        return random.choice(Tempo.farben)

    def fertig(self):
        """Überprüfe ob das Spiel vorbei ist."""
        return self._fischergewonnen() or self._fischegewonnen() or self.boot.position == 11

    def _fischergewonnen(self):
        """Fischer gewinnen, wenn alle Fische gefangen wurden."""

        return len(self.boot.farben) == 6

    def _fischegewonnen(self):
        """Fische gewinnen, wenn alle Fische im Meer landen."""
        return all([self.spiel[x].position == 12 for x in Tempo.farben[2:]])


    def zug(self):

        # Würfle Farbenwürfel
        wurf = self.roll()

        # Ziehe entsprechende Figur
        self.spiel[wurf].position += 1

        # Falls Boot gezogen wurde
        if self.spiel[wurf] == self.boot:

            # Überprüfe ob irgendwelche Fische gefangen wurden und update Spielstand
            fische = [x for x in self.spiel.values() if type(x) is not Boot]

            gefangen = [fisch.farbe for fisch in fische if fisch.position <= self.boot.position]

            for farbe in gefangen:
                self.boot.farben.append(farbe)
                self.spiel[farbe] = self.boot

    def ergebnis(self):
        """Simuliere ein ganzes Spiel."""
        while not self.fertig():
            # Mache einen Zug.
            self.zug()

        # Gib das Ergebnis des Spiels als (Gefangene Fische, Gerettete Fische) Tupel zurück.

        return len(self.boot.farben) - 2, 6 - len(self.boot.farben)