Der MiniMax Algorithmus

Der Minimax-Algorithmus dient ganz allgemein der Entscheidungsfindung. In Zwei-Personen-Nullsummenspielen, wie Reversi, hilft der Algorithmus herauszufinden, welches der optimale nächste Zug ist gegeben das beide Spieler optimal spielen. Optimal bedeutet dabei, dass beide gegeben der gleichen Strategie ihren Gewinn maximieren. Als "Java Pseudocode" sieht der Algorithmus beispielsweise so aus
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int max(Position pos, int depth) {
    if(depth == 0)
        return eval(pos, me);
    moves = pos.generateMoves();
    int maxWert = -INFINITY;
    foreach(m in moves) {
        pos.makeMove(m);
        int value = min(pos, depth - 1);
        pos.undoMove(m);
        if(value > maxWert) {
            maxWert = value;
        }
    }
    return maxWert;
}

int min(Position pos, int depth) {
    if(depth == 0)
        return eval(pos, foe);
    moves = pos.generateMoves();
    int minWert = INFINITY;
    foreach(m in moves) {
        pos.makeMove(m);
        int value = max(pos, depth - 1);
        pos.undoMove(m);
        if(value < minWert) {
            minWert = value;
        }
    }
    return minWert;
}
Nun sind dies zwei Funktionen, welche quasi dasgleiche machen. Da man als Programmierer gerne faul ist und zudem noch die Anzahl Fehler proportional zu den Anzahl Zeilen Code ist, möchte man dies natürlich reduzieren. Deshalb hier ohne Beweis auf Gleichheit die Alternative Variante in nur einer Funktion, NegaMax genannt:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int negaMax(Position pos, int depth, int player) {
    if(depth == 0)
        return eval(pos);
    moves = pos.generateMoves();
    int maxWert = -INFINITY;
    foreach(m in moves) {
        pos.makeMove(m);
        int value = -negaMax(pos, depth - 1, otherPlayer(player));
        pos.undoMove(m);
        if(value > maxWert) {
            value = maxWert;
        }
    }
    return maxWert;
}
Die Grundidee dabei ist, dass jeder Spieler seinen Gewinn maximieren will. Deshalb multipliziert man den Wert des anderen Spielers mit -1; daher auch der Name "NegaMax".

Alpha Beta Suche

Nun ist der Minimax Algorithmus nicht sehr effizient, da er auch Knoten auswertet die den finalen Wert an der Wurzel gar nicht mehr beeinflussen können. Deshalb führt man obere und untere Schranken des momentan besten Wertes ein um frühzeitig irrelevante Knoten aus der Suche ausschliessen zu können. Die obenstehende NegaMax-Funktion lässt sich wie folgt ergänzen:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int negaMax(Position pos, int alpha, int beta, int depth, int player) {
    if(depth == 0)
        return eval(pos);
    moves = pos.generateMoves();
    
    foreach(m in moves) {
        pos.makeMove(m);
        int value = -negaMax(pos, -beta, -alpha, depth - 1, otherPlayer(player));
        pos.undoMove(m);
        if(value >= beta)
            return beta;
        else if(value > alpha)
            alpha = value;
    }
    return alpha;
}

Alpha-Beta Optimierungen

Die Alpha-Beta-Suche findet den gleichen Wert wie der MiniMax-Algorithmus und evaluiert dabei höchstens soviele Knoten wie selbiger. Unser Ziel sollte es nun sein, möglichst wenige Knoten evaluieren zu müssen. Da der Wert sowieso derselbe ist, bringt der Mehraufwand auch nichts.
Bei der Betrachtung des Algorithmuses fällt auf, dass idealerweise der beste Zug zuerst evaluiert wird, weil dann die Anzahl Schnitte maximal wird. Wird beispielsweise das Übungsbeispiel auf der Vorlesungshomepage von rechts nach links durchgerechnet, müssen mehr Knoten evaluiert werden. Dies bedeutet, dass sich eine geschickte Zugvorsortierung lohnen kann. Hierfür kann zum Beispiel die Auswertung des Spielbaums zur Tiefe (d - 1) herbeigezogen werden. In diesem Zusammenhang kann über eine Implementierung einer "Transposition Table" nachgedacht werden. Einerseits werden so wichtige Werte einer vorherigen Suche gespeichert andererseits müssen Positionen der aktuellen Suche nicht neu evaluiert werden. Mit so einer Tabelle lässt sich der Alpha-Beta-Algorithmus mit Erinnerungsvermögen implementieren (hier gefunden).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int negaMax(Position pos, int alpha, int beta, int depth, int player) {
    if(depth == 0)
        return eval(pos);

    e = table.lookUp(pos);
    if(e is valid) {
        if(e.lowerBound >= beta)
            return e.lowerBound;
        if(e.upperBound <= alpha)
            return e.upperBound;
        alpha = Math.max(alpha, e.lowerBound);
        beta = Math.min(beta, e.upperBound);
    }

    moves = pos.generateMoves();
    sortMovesByHeuristic(moves, e);
    Move best = null;
    int bestVal = -INFINITY, a = alpha;
    foreach(m in moves) {
        pos.makeMove(m);
        int value = -negaMax(pos, -beta, -a, depth - 1, otherPlayer(player));
        pos.undoMove(m);
        if(value > bestVal) {
            best = m;
            bestVal = value;
            if(bestVal >= beta) {
                break;
            } else if(bestVal > a)
                a = bestVal;
        }
    }
    if(bestVal > alpha) {
        e.lowerBound = bestVal;
        e.move = best;
        table.store(e);
    }
    if(bestVal < beta) {
        e.upperBound = bestVal;
        table.store(e);
    }

    return bestVal;
}
Dieser Link stellt zudem noch eine Verbesserung des regulären Alpha-Beta-Algorithmuses vor. Auch hier ist das Ziel die Anzahl Knoten die pro Tiefe analysiert werden müssen zu minimieren.
Eine andere Optimierung ist der Algorithmus PVS/NegaScout.

Viele Informationen finden sich im Computer Schach-Wiki. Zumindest alle Informationen bezüglich der Suche sind auch für Reversi relevant.