Graphenalgorithmen

Spielbäume

Problem

Wir betrachten Zwei-Personen-Spiele wie zum Beispiel Schach, Dame oder TicTacToe. Die beiden Spieler A und B führen abwechselnd Spielzüge aus. Nach jedem Zug befindet sich das Spiel in einer bestimmten Spiel­stellung. Ziel von Spieler A ist es, eine Spiel­stellung zu erreichen, in der er das Spiel gewonnen hat. Das Problem besteht darin, den jeweils nächsten Zug zu bestimmen, der ihn dieser Gewinn­stellung näher bringt.

Spielbaum

Als Beispiel betrachten wir eine mögliche Spiel­stellung des Spiels TicTacToe; Spieler A ist am Zug. Diese Spiel­stellung bildet die Wurzel eines Baumes von Folge-Spiel­stellungen (Bild 1). Je nach dem, wo Spieler A sein Kreuz macht, ergeben sich zunächst drei verschiedene Folge-Spiel­stellungen. Indem daraufhin Spieler B seinen Kreis macht, ergeben sich davon ausgehend jeweils zwei weitere Folge-Spiel­stellungen. In zwei dieser Spiel­stellungen ist das Spiel bereits zu Ende, weil Spieler B drei Kreise in einer Reihe hat und damit gewonnen hat. Ausgehend von den anderen Spiel­stellungen ergibt sich, indem Spieler A wieder ein Kreuz macht, jeweils noch eine Folge-Spiel­stellung; danach ist das Spiel zu Ende.

 

Bild 1: Spielbaum ausgehend von einer Spielstellung des Spiels TicTacToe 

Bild 1: Spielbaum ausgehend von einer Spielstellung des Spiels TicTacToe

 

Bewertungsfunktion für Spielstellungen

Wir führen eine Bewertungs­funktion ein, die jeder Spiel­stellung einen Zahlenwert zuweist, der umso höher ist, je günstiger die Spiel­stellung für Spieler A ist, und umso niedriger, je günstiger die Spiel­stellung für Spieler B ist. Dann ist es möglich, durch Analyse des Spielbaums den optimalen Zug für Spieler A zu finden.

Im Allgemeinen ist es schwierig, Spiel­stellungen sinnvoll zu bewerten, beispiels­weise eine Spiel­stellung im Schachspiel. Eine sehr grobe Bewertung würde die Anzahl und Qualität der eigenen Spielfiguren minus die der gegnerischen Spielfiguren heranziehen.

Einfach ist eine Spiel­stellung dagegen dann zu bewerten, wenn sie eine Gewinn­stellung darstellt, in der ein Spieler das Spiel gewonnen hat. Wenn Spieler A gewonnen hat, dann wird diese Spiel­stellung mit einem sehr großen Wert bewertet und wenn Spieler B gewonnen hat, mit einem sehr kleinen Wert.

Die Bewertungs­funktion wird immer auf die Blätter des Spielbaums angewendet, der gerade betrachtet wird. In Bild 1 ist dies geschehen: Die Spiel­stellungen, in denen Spieler A gewonnen hat, sind mit 1 bewertet, die Spiel­stellungen, in denen Spieler B gewonnen hat, mit -1 und die Unentschieden-Spiel­stellungen mit 0.

Auswertung des Spielbaums: Minimax-Strategie

Aus den Bewertungen der Blätter des Spielbaums werden die Bewertungen der inneren Knoten abgeleitet. Die Vorgehens­weise ver­anschaulicht Bild 2.

 

Bild 2: Spielbaum mit Bewertung der Spielstellungen nach zwei Zügen 

Bild 2: Spielbaum mit Bewertung der Spielstellungen nach zwei Zügen

 

Ausgehend von Spiel­stellung S zieht erst Spieler A und dann Spieler B. Nach diesen zwei Zügen ergeben sich an den Blättern des Spielbaums vier mögliche Spiel­stellungen, die mit 6, 5, 2 bzw. 9 bewertet seien.

Die Bewertungen der inneren Knoten des Spielbaums werden bottom-up berechnet: in Spiel­stellungen, in denen Spieler B am Zug ist, als das Minimum der Bewertungen der Nachfolger­knoten und in Spiel­stellungen, in denen Spieler A am Zug ist, als deren Maximum.

Dieser Vorgang wird als Auswertung des Spielbaums nach der Minimax-Strategie bezeichnet.

Die Begründung für diese Art der Berechnung ist folgende: Zieht A ausgehend von Spiel­stellung S nach rechts, um die mit 9 bewertete Spiel­stellung zu erreichen, so wird B dies vereiteln, indem er nach links zieht, um die für ihn bessere und für A schlechtere, mit 2 bewertete Spiel­stellung zu erreichen. Die Strategie von B ist also, stets die minimal bewertete Spiel­stellung anzusteuern. Hieraus ergibt sich die Bewertung der Knoten, in denen B am Zug ist, als jeweils das Minimum der Bewertungen der Nachfolger­knoten.

Spieler A dagegen versucht stets, die maximal bewertete Spiel­stellung anzusteuern. Hieraus ergibt sich die Bewertung der Knoten, in denen A am Zug ist, als jeweils das Maximum der Bewertungen der Nachfolger­knoten.

In Bild 2 zieht A ausgehend von Spiel­stellung S somit nach links, denn so kann er mindestens, egal wie B dann zieht, den Wert 5 erreichen.

Auch in Bild 1 zieht Spieler A nach links, macht also sein Kreuz oben links in die Ecke. Denn die drei Folge-Spiel­stellungen sind nach Auswertung des Spielbaums mit 0, -1, -1 bewertet.

Bild 3 zeigt einen Spielbaum für vier Züge. Die 16 erreichbaren Spiel­stellungen sind mit ihren Bewertungen markiert. Die Auswertung des Spielbaums zeigt, dass A eine Spiel­stellung mit mindestens dem Wert 5 erreichen kann. Dieser Wert ergibt sich, indem bottom-up jeder Knoten mit dem Minimum (wenn Spieler B am Zug ist) bzw. mit dem Maximum (wenn Spieler A am Zug ist) der Werte seiner direkten Nachfolger­knoten markiert wird.

 

Bild 3: Bewertung der Spielstellungen nach vier Zügen (Blätter des Baumes) und Auswertung des Spielbaums (innere Knoten) 

Bild 3: Bewertung der Spielstellungen nach vier Zügen (Blätter des Baumes) und Auswertung des Spielbaums (innere Knoten)

 

Rekursive Auswertung des Spielbaums

Die Auswertung des Spielbaums vereinfacht sich, wenn die Bewertung der Spiel­stellungen stets aus Sicht desjenigen Spielers vorgenommen wird, der gerade am Zug ist. Dieser Spieler berechnet stets das Maximum der aus seiner Sicht bewerteten Folge­stellungen. Diese Werte sind aber gerade die mit -1 multi­plizierten Werte, die sich aus Sicht des Gegners für diese Spiel­stellungen ergeben.

Statt also abwechselnd das Minimum und das Maximum zu bilden, wird stets das Maximum der mit -1 multi­plizierten Werte gebildet; dies führt zum selben Ergebnis. Die Auswertung eval(S) des an einer Spiel­stellung S beginnenden Spielbaums lässt sich rekursiv wie folgt darstellen:

eval(S) = geschweifte Klammer
rate(S)    S ist ein Blatt und rate(S) ist die Bewertung von S
max{–eval(S')}    S' durchläuft alle durch einen Spielzug erreichbaren Folgestellungen von S

Die Funktion rate(S) bewertet hierbei die Spiel­stellung S aus Sicht des Spielers, der am Zug ist.

Implementierung

Die folgende rekursive Funktion eval wertet einen Spielbaum beginnend an einer Spiel­stellung s bis zur Tiefe d in Form einer Tiefensuche aus.

Ist d = 0, d.h. besteht der Spielbaum nur aus einem Blatt, so wird als Ergebnis die Bewertung rate(s) der entsprechenden Spiel­stellung s zurück­gegeben (bewertet aus Sicht des Spielers, der am Zug ist).

Ansonsten werden mit dem Iterator s.nextMoveIterator() alle in der Spiel­stellung s möglichen Spielzüge durchlaufen und die sich daraus ergebenden Folge-Spiel­stellungen t erzeugt. Durch rekursiven Aufruf der Funktion eval werden die an diesen Spiel­stellungen t beginnenden Spielbäume der Tiefe d-1 ausgewertet. Von den mit -1 multi­plizierten Ergebnissen wird das Maximum gebildet; hierzu wird ein Maximizer verwendet.

    private double eval(Configuration s, int d)
    {
        if (d==0 || s.gameOver())
            return rate(s);

        Iterator<Move> it=s.nextMoveIterator();
        Maximizer<Move> max=new Maximizer<Move>();
        Move move;
        Configuration t;
        while (it.hasNext())
        {
            move=it.next();
            t=s.applyMove(move);
            max.add(-eval(t, d-1));
        }
        return max.getMaxVal();
    }

Bestimmen des Spielzugs

Um den optimalen Spielzug zu bestimmen, wird der erste Rekursions­schritt ausgelagert in folgende Funktion findMove, die sich zusätzlich denjenigen Zug move merkt, der den Maximalwert liefert.

    public Move findMove(Configuration s, int d)
    {
        Iterator<Move> it=s.nextMoveIterator();
        Maximizer:(Move> max=new Maximizer<Move>();
        Move move;
        Configuration t;
        while (it.hasNext())
        {
            move=it.next();
            t=s.applyMove(move);
            max.add(-eval(t, d-1), move);
        }
        return max.getMaxObj();
    }

Partielle Auswertung des Spielbaums

Tatsächlich brauchen in vielen Fällen nicht alle Knoten des Spielbaums berechnet zu werden. Bild 4 zeigt dies zunächst anhand des Spielbaums von Bild 2.

 

Bild 4: Partielle Auswertung des Spielbaums für zwei Züge 

Bild 4: Partielle Auswertung des Spielbaums für zwei Züge

 

Egal welchen Wert x der untere rechte Knoten hat, er ändert nichts am Ergebnis 5 der Auswertung des Spielbaums. Der Wert des Knotens kann das darüber befindliche Minimum höchstens kleiner machen. Das wiederum darüber befindliche Maximum ist aber bereits größer, bleibt also erhalten.

Bild 5 zeigt anhand des Spielbaums von Bild 3, dass ganze Teilbäume nicht berechnet zu werden brauchen, weil sie zum Ergebnis nicht beitragen. Beispiels­weise kann der Wert 4 unterhalb der Wurzel des Spielbaums höchstens kleiner werden, wenn der untere rechte Teilbaum ausgewertet wird. Dadurch ändert sich jedoch am Maximum 5 an der Wurzel nichts. Die Auswertung des Teilbaums ist also nicht nötig.

 

Bild 5: Partielle Auswertung des Spielbaums für vier Züge 

Bild 5: Partielle Auswertung des Spielbaums für vier Züge

 

Implementierung

Die entsprechende Implementierung entsteht durch eine sehr kleine Abänderung der ursprüng­lichen Funktion eval.

    private double eval(Configuration s, int ddouble w)
    {
        if (d==0 || s.gameOver())
            return rate(s);

        Iterator<Move> it=s.nextMoveIterator();
        Maximizer<Move> max=new Maximizer<Move>();
        Move m;
        Configuration t;
        while (it.hasNext() && max.getMaxVal()<w)
        {
            m=it.next();
            t=s.applyMove(m);
            max.add(-eval(t, d-1, -max.getMaxVal()), m);
        }
        return max.getMaxVal();
    }

Beim ersten Aufruf der abgeänderten Funktion eval in der Funktion findMove wird für den Parameter w der Wert Double.POSITIVE_INFINITY eingesetzt.

In der Darstellung von Bild 5 sieht es so aus, als ob die Einsparung an aus­zuwertenden Spiel­stellungen nicht allzu groß ist. Dies täuscht allerdings, denn in Wirklichkeit verzweigt ein Spielbaum sehr viel breiter, weil es normaler­weise mehr als zwei Zug­möglich­keiten gibt. Entsprechend viele Teilbäume brauchen gegebenen­falls nicht ausgewertet zu werden. Und je tiefer der Spielbaum berechnet wird, umso mehr Knoten fallen weg, wenn ein Teilbaum nicht ausgewertet wird. Je nach tat­sächlichem Spiel ergibt sich in der Praxis eine Beschleunigung der Berechnung um Faktoren zwischen 10 und 50.

Literatur

[HS 81]   E. Horowitz, S. Sahni: Algorithmen. Springer (1981)

 

Weiter mit:   [Literatur]   oder   [up]

 


H.W. Lang   mail@hwlang.de   Impressum   Datenschutz
Created: 25.10.2006   Updated: 20.02.2023
Diese Webseiten sind während meiner Lehrtätigkeit an der Hochschule Flensburg entstanden