Seminararbeit Implementierung eines Schachprogramms vorgelegt von Dipl.cand.-MedInf. Stefan Koppitz Matr.-Nr.: 3075856 Betreuer: Doz. Dr. habil. Uwe Petersohn Fakultät Informatik Institut für Künstliche Intelligenz Technische Universität Dresden Dresden, im August 2006 Seminar: Neuronale Netze und Fallbasiertes Schließen Thema: Nr VI.2 Implementierung eines Schachprogramms Implementieren Sie in Anlehnung an Russel / Norvig ein Schachprogramm. Wenden Sie verschiedene dort beschriebene Strategien zur Verbesserung des Suchraumes an und dokumentieren Sie Ihr Vorgehen. Messen Sie die Performance Ihres Schachspielers“. ” Es wird ein selbständiges Literaturstudium erwartet. Die angegebenen Literaturstellen sind nur richtungsweisend. Inhaltsverzeichnis 1 Einleitung 1 2 Such-Algorithmus 2 2.1 Minimax-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2.2 Alpha-Beta-Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.3 Vergleich Minimax- und Alpha-Beta-Suche . . . . . . . . . . . . . . . . . . 8 3 Optimierung 9 3.1 Zugsortierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.2 NegaScout-Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.3 Iterative Tiefensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.4 Aspiration Window . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.5 Killer-Heuristik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.6 Quiescent-Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.7 Null-Zug-Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.8 Hash-Tabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.9 Ruhesuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 4 Bewertungsfunktion 15 5 Zusammenfassung und Ausblick 16 Literaturverzeichnis 17 Kapitel 1 Einleitung Im Gegensatz zu Würfelspielen ist Schach nicht vom Zufall abhängig. Im Gegenteil: es ist offen und alle benötigten Informationen sind vorhanden. D. h. in jeder Spielsituation sind jedem der beiden Spieler alle Zugmöglichkeiten des Gegenspielers bekannt. Der Verlust des Gegners ist dabei der eigene Gewinn. In solchen Fällen lässt sich die optimale Strategie mit dem Minimax-Algorithmus ermitteln. Die optimale Strategie ist dann gefunden, wenn sie zum bestmöglichen Ergebnis eines Spielers führt. Dabei geht man von optimaler Spielweise des Gegenspielers aus. Diese Arbeit soll nun den Minimax-Algorithmus als Suchalgorithmus am Beispiel eines Schachprogramms vorstellen und durch Optimierung verbessern. Suchalgorithmen spielen in der künstlichen Intelligenz eine wesentliche Rolle, um effizient optimale Strategien zu finden. Um den Algorithmus zu visualisieren, wird ein Programm implementiert. Dazu wird es die Performance des Programms messen und die Gedanken des Computers“ während ” des Spielens aufzeigen. Es ist weiterhin möglich bestimmte Optimierungen zu- bzw. abzuschalten. Nachfolgend sollen der Minimax-Algorithmus und seine Verbesserungen, dann die Optimierungsverfahren für den Algorithmus und schließlich die Bewertungsfunktion vorgestellt werden. Kapitel 2 Such-Algorithmus 2.1 Minimax-Algorithmus Schach kann vereinfacht als Baum dargestellt werden. Dabei kennzeichnen alle Knoten und Blätter dieses sogenannten Suchbaums eine Stellung der Figuren auf dem Schachbrett. Jede Verzweigung dieses Baumes stellt dann ein Übergang von einer Spielstellung zu einer anderen, also einen Schachzug, dar. Der Baum hat eine Tiefe d und beginnt an der Wurzel mit d = 0. Man kann sich nun denken, dass es nicht möglich ist, den Baum nach allen Spielkombinationen abzusuchen. Es müssten dann nämlich alle 35100 Knoten des Suchbaums untersucht werden. Davon sind aber bloß 1040 zulässige Positionen. Der Minimax-Algorithmus nutzt daher eine Vereinfachung. Um den Suchaufwand zu minimieren, kann man sich überlegen, immer die beste Stellung erreichen zu wollen. Andererseits wird der Gegenspieler das gleiche versuchen. Also ordnet man einem Spieler eine maximale Punktzahl und dem anderen eine minimale Punktzahl zu. Der Spieler, welcher die maximalen Punkte erhofft, wird auch Maximizer“ und der Ge” genspieler Minimizer“ genannt. ” Da die Spieler abwechselnd eine Spielfigur ziehen, ist immer ein Spieler auf einer bestimmten Baumebene am Zug. Daher können wir diese Ebenen auch Maximizing Level“ und ” Minimizing Level“ nennen. ” Ein Spiel wird dann formal als ein Suchproblem über die Menge aller möglichen Züge mit folgenden Komponenten definiert: • Der Zustand, der die Position der Figuren auf dem Brett enthält und anzeigt, 2 Such-Algorithmus 3 welcher Spieler als nächstes am Zug ist. • Eine Menge von Operatoren, welche die zulässigen Züge definieren, die ein Spieler ausführen darf. • Einen Abbruchkriterium, der das Ende des Spieles anzeigt. Zustände, die diesen Test erfüllen, werden Endzustände genannt. • Eine Bewertungsfunktion, die dem Ausgang des Spiels einen numerischen Wert zuweist. Der formale Algorithmus läuft dann wie folgt ab: 1. Generiere den Suchbaum bis zu den Endzuständen 2. Wende die Bewertungsfunktion auf alle Endzustände bzw. Blätter an 3. Falls die aktuelle Ebene ein Maximizing/Minimizing Level ist, dann erhalten die Elternknoten den jeweils höchsten/niedrigsten Wert ihrer Kindknoten 4. Propagiere mit Schritt 3 bis zur Wurzel Daraus lässt sich dieser Pseudocode konstruieren. Der rekursive Algorithmus arbeitet nach dem Prinzip der Tiefensuche. D. h. es wird vom Startknoten aus durch Expansion des jeweils ersten auftretenden Nachfolgeknoten im Suchbaum nach und nach weiter in die Tiefe gesucht. function MINIMAX_WERT(zustand) { if END_TEST(zustand) then return BEWERTEN(zustand) else init MIN_WERT maximal; init MAX_WERT minimal; for each op in OPERATOREN do if MAX ist am Zug then MAX_WERT = MAX(MAX_WERT,MINIMAX_WERT(zustand-1)) else MIN_WERT = MIN(MIN_WERT,MINIMAX_WERT(zustand-1)) end if MAX ist am Zug then 2 Such-Algorithmus 4 return MAX_WERT else return MIN_WERT end } Der Algorithmus kann weiterhin vereinfacht werden, indem jeder Knoten des Suchbaums gleich behandeln wird. Dabei wird definiert, dass jeder Spieler versucht, ein für sich selbst maximales Ergebnis zu erzielen. Dazu muss die Bewertung der Folgestellungen mit −1 multipliziert werden. Daher auch der Name NegaMax-Algorithmus. Damit muss nicht mehr unterschieden werden, ob Schwarz oder Weiß am Zug ist. Es wird in jeder Stellung immer nur das Maximum der negierten Bewertungen der Folgestellungen berechnet. Der Pseudocode sieht dann wie folgt aus: function NEGAMAX_WERT(zustand) { if END_TEST(zustand) then return BEWERTEN(zustand) else init WERT minimal; for each op in OPERATOREN do WERT = MAX(WERT,-NEGAMAX_WERT(zustand-1)) end return WERT end } Es sei gesagt, dass der Minimax-Algorithmus vollständig bei einem endlichen Suchbaum ist. Durch die definierten Schachregeln wird der Determinismus garantiert. Weiterhin ist der Algorithmus optimal unter der Voraussetzung, dass auch gegen ein optimal spielenden Gegner gespielt wird. Betrachten wir nun die Komplexität des Algorihtmus. Der Zeitbedarf wächst durch das iterieren über den Suchbaum exponentiell und beträgt O(nd ). Der Platzbedarf ist durch Speichern aller Knoten O(n · d) (Fierz 2005). Dabei sind n die Anzahl der möglichen Züge und d die Suchbaum-Tiefe. Die Konplexität ist somit nicht praktikabel für Schach mit n ≈ 35 und d ≈ 100. Die naheliegende Veränderungen des Minimax-Algorithmus ist folglich die Einführung einer Abbruchtiefe, die den Spielend-Test ersetzt. 2 Such-Algorithmus 5 Nehmen wir an, es sind 100 s pro Zug erlaubt. Dann können 104 Knoten pro s durchsucht werden. Da die Ressourcen auf dem Computer beschränkt sind, muss die Ungleichung nd ≤ 104 erfüllt sein. Mit n = 35 folgt daraus d = 2 Züge Vorausschau für die Schach-KI. Wir werden im folgenden Absatz den Minimax-Algorithmus direkt auf das Schachspiel anwenden und detailiert die einzelnen Phasen durchgehen: Ausgehend von der gerade am Brett befindlichen Stellung ermittelt das Programm mittels des Zuggenerators alle regelkonformen Züge und führt im Geiste“ den ersten möglichen davon aus; in der neu ent” standenen Stellung werden nun wiederum alle Züge bestimmt, der erste ausgeführt usw., bis zu einer bestimmten Tiefe d. Ist diese erreicht, schätzt man die entstandene Stellung mittels der Bewertungsfunktion ab; anschließend wird der letzte Zug zurückgenommen und nacheinander alle Alternativmöglichkeiten durchprobiert, wobei hier noch jeweils unmittelbar im Anschluss die Bewertungsfunktion aufgerufen wird. Sind alle Möglichkeiten untersucht, wird auch der vorletzte Zug zurückgenommen und nun dessen Alternativen getestet, worauf genau das gleiche Verfahren einsetzt (d. h. Ausprobieren aller möglichen Züge und Aufruf der Bewertungsfunktion). Macht man so weiter, hat man am Ende alle Stellungen besichtigt, die sich nach d Zügen ergeben könnten. Da für den optimalen Zug dessen Bewertung und der Pfad“ (d. h. die Züge, die zur Stellung führten) gespeichert ” sind, kann man diese Zug nun ausführen. function NEGAMAX_SCHACH(zustand) { if END_TEST(zustand) then return BEWERTEN(zustand) else init WERT minimal; for each op in OPERATOREN do AUSFUEHREN(op) WERT = MAX(WERT,-NEGAMAX_SCHACH(zustand-1)) ZURUECKNEHMEN(op) end return WERT end } 2 Such-Algorithmus 2.2 6 Alpha-Beta-Suche Der Minimax-Algorithmus bewertet den vollständigen Suchbaum. Dabei werden aber auch Knoten betrachtet, die in das Ergebnis (die Wahl des Zweiges an der Wurzel) nicht einfließen. Die Alpha-Beta-Suche versucht, möglichst viele dieser Knoten zu ignorieren. Denn wir wollen nur wissen, welche Zugfolge am besten ist und nicht wie viel besser sie ist (Lorenz 2006). Was beim ersten Lesen vielleicht lediglich spitzfindig klingt, birgt gewaltiges Einsparungspotential in sich: Nehmen wir an, das Programm hat in einem beliebigen Knoten K0 des Baumes bereits einige Züge untersucht und für den bisher besten den Wert w0 aus Sicht der am Zug befindlichen Partei A ermittelt. Gemäß des Minimax-Algorithmus untersucht es nun die nächste Alternative; sollte dann bei der Suche in diesem Teilbaum an irgendeiner Stelle der Gegner B die Möglichkeit haben, einen Zug z mit einer Bewertung wz , die aus Sicht von A nicht besser als w0 ist, zu spielen, so weiß man, dass einer der vorangegangenen Züge von A (ab dem Knoten K0 ) bereits nicht optimal war, denn den Wert w0 kann er ja dadurch erreichen, dass er in K0 den zu w0 gehörigen Zug wählt. Es ist dann an dieser Stelle unnötig, noch weitere Gegenzüge von B auszuprobieren; das vorhergehende Spiel von A kann bereits durch z widerlegt werden, ob es noch stärkere Züge gibt, ist uninteressant (Heuner 2002). Folglich würde an dieser Stelle der Wert wz als obere Schranke für den Wert des letzten Zuges von A zurückgegeben und die nächste Alternative zu diesem getestet. Der Wert w0 kann also bei allen Folgeknoten von K0 als sicherer Wert für die Partei A angesehen werden. Bei der Untersuchung eines Knotens bezeichnet bezeichnet man den zu diesem Zeitpunkt sicheren Wert für die Partei am Zug als Alpha-, den für den Gegner als Beta-Wert, wobei Alpha stets kleiner Beta ist. Die Idee ist also, dass diese zwei Werte weitergereicht werden, die das Worst-Case- Szenario der Spieler beschreiben (Wikipedia.org 2006). Der Alpha-Wert ist das Ergebnis, das Spieler A mindestens erreichen wird, der Beta-Wert ist das Ergebnis, das Spieler B mindestens erreichen wird. Besitzt ein maximierender Knoten (von Spieler A) einen Zug, dessen Rückgabe den Beta-Wert überschreitet, wird die Suche in diesem Knoten abgebrochen (Beta-Cutoff, denn Spieler B würde erst gar nicht diese Variante wählen, weil sie ein zu gutes Ergebnis für Spieler A liefern würde). Liefert der Zug stattdessen ein Ergebnis, das den Alpha-Wert übersteigt, wird dieser entsprechend nach oben angehoben. Analoges gilt für die minimierenden Knoten, wobei bei Werten kleiner als Alpha abgebrochen wird (Alpha-Cutoff) und der Beta Wert nach unten angepasst wird. 2 Such-Algorithmus 7 Der Pseudocode der Alpha-Beta-Such sieht dann wie folgt aus: function ALPHA_BETA(zustand,alpha,beta) { if END_TEST(zustand) then return BEWERTEN(zustand) else init WERT minimal; for each op in OPERATOREN do AUSFUEHREN(op) WERT = -ALPHA_BETA(zustand-1,-alpha,-beta) ZURUECKNEHMEN(op) if WERT >= beta then return beta if WERT > alpha then alpha = WERT end return alpha end } MAX -∞ 12 ∞ 10 12 ∞ -∞ 10 ∞ 12 3 ∞ MIN MAX -∞ 10 ∞ -∞ 12 10 10 12 ∞ 10 13 12 12 3 ∞ 10 -5 -6 12 ? 10 12 3 13 ? 3 2 3 ? -4 ? ? ? Bild 2.1: Obige Abbildung zeigt einen Beispielbaum mit 18 Blättern, von denen nur 12 von der Alpha-Beta-Suche ausgewertet werden. Die drei umrandeten Werte eines inneren Knotens beschreiben den Alpha-Wert, den Rückgabewert und den Beta-Wert. rot: 12,13 Beta-Cut; rot: 3 - Alpha-Cut Es sei an dieser Stelle noch einmal betont, dass die Alpha-Beta-Suche erlaubt, risikolos 2 Such-Algorithmus 8 bestimmte Äste des Baumes wegzulassen, d. h. es wird genau der gleiche Zug mit dem gleichen Wert als bester ermittelt wie bei der vollständigen Minimax- Suche. Die Einsparung dagegen ist ganz enorm. Nur um ein Gefühl für die Einsparung durch die Alpha-BetaSuche zu vermitteln, hier die Zahl zu bewertender Endknoten n beim Idealfall für einen Baum der Hohe d und (als konstant angenommenem) Verzweigungsfaktor z: für d ungerade: n=z d+1 2 +z d−1 2 −1 für d gerade: d n = 2 · z2 − 1 Wie man den Formeln entnimmt, steigt die Zahl zu bewertender Endknoten bei Erhöhung √ der Suchtiefe um einen Zug im Mittel um den Faktor z; zur Erinnerung: mit dem einfachen Minimax-Algorithmus ist dieser gleich z! Grob gesprochen kann man also mit der Alpha-Beta-Suche bei gleicher Endknotenzahl etwa doppelt so tief rechnen wie mit dem Minimax-Algorithmus (Zwanzger 2004). 2.3 Vergleich Minimax- und Alpha-Beta-Suche Nachfolgende Tabelle zeigt eine Beispielberechnung einer Schachstellung bei konstanter Suchtiefe von vier Zügen. Es wurde der normale Minimax-Algorithmus angewendet und Alpha-Beta ohne Zugsortierung. Die Prozentangabe bei den Cutoffs beziehen sich auf den gesamten Suchbaum und beschreibt, wie viel des gesamten Suchbaumes nicht ausgewertet wurde. Es handelt sich dabei um Schätzungen, denen zugrunde liegt, dass die Teilbäume in etwa gleich groß sind (bei Cutoffs ist nicht bekannt, wie groß der weggeschnittene Teilbaum wirklich wäre). Algorithmus Bewertungen Cutoffs Anteil der Cutoffs in % Rechenzeit in s Minimax 28018531 0 0,00 134.87 Alpha-Beta 2005246 136478 91,50 9.88 Tabelle 2.1: Vergleich der Algorithmen Minimax und Alpha-Beta mit einer Beispielberechnung. Es ist deutlich zu erkennen, dass die Alpha-Beta-Suche eine erhebliche Geschwindigkeitssteigerung gegenüber Minimax bedeutet. Kapitel 3 Optimierung 3.1 Zugsortierung Anders als beim Minimax-Algorithmus spielt bei der Alpha-Beta-Suche die Reihenfolge, in der Kindknoten (Züge) bearbeitet werden, eine wesentliche Rolle. Je schneller das Alpha-Beta-Fenster verkleinert wird, desto mehr Cutoffs werden erreicht. Deshalb ist es wichtig, zuerst die Züge zu betrachten, die das beste Ergebnis versprechen. In der Praxis werden verschiedene Heuristiken verwendet. Bei Schach z. B. kann man die Züge danach sortieren, ob bzw. welche Figur geschlagen wird, oder auch welche Figur schlägt. Turm ” schlägt Dame“ wird danach vor Turm schlägt Turm“ einsortiert und Bauer schlägt ” ” Turm“ wird zwischen beiden einsortiert. 3.2 NegaScout-Suche Zunächst sei erwähnt, dass der NegaScout-Algorithmus (oft auch Principal-VariationSuche genannt) nicht zwangsläufig besser ist als die Alpha-Beta-Suche. Unter welchen Umständen er für mehr Schnitte sorgt und somit insgesamt zu einer PerformanceSteigerung führt, sei im Folgenden erläutert. Die Idee des NegaScout-Algorithmus besteht darin, nach der Bewertung des ersten Unterbaumes, die analog zum Alpha-Beta-Algorithmus erfolgt, alle weiteren a-priori als unterlegen zu betrachten und eine Nullfenstersuche (d. h. setze Beta = Alpha ± 1) durchzuführen. Dadurch werden mehr Schnitte im Suchbaum vorgenommen. Kann die Unterlegenheit nicht bewiesen werden, d. h. es gab doch noch einen besseren Zug, so muss eine Wiederholungssuche mit offenem Fenster durchgeführt werden. 3 Optimierung 10 Diese Vorgehensweise ist vor allem dann effektiv, wenn mit geeigneten Heuristiken sichergestellt wird, dass auch tatsächlich aussichtsreiche Züge zuerst untersucht werden. Die Ersparnis der Suche mit Nullfenster ist dann in der Regel so groß, dass der Mehraufwand gelegentlicher Wiederholungssuchen bei weitem aufgewogen wird. Um nun die Alpha-Beta-Suche durch die NegaScout-Suche zu optimieren, brauchen wir noch Knotenarten. Ein Knoten bei der Alpha-Beta-Suche gehört einer von drei Kategorien an (bezogen auf die NegaMax-Variante): • Alpha-Knoten: Jeder Folgezug liefert einen Wert kleiner oder gleich Alpha, was bedeutet, dass hier kein guter Zug möglich ist. • Beta-Knoten: Mindestens ein Folgezug liefert einen Wert größer oder gleich Beta, was einen Cutoff bedeutet. • Principal-Variation-Knoten: Mindestens ein Folgezug liefert einen Wert größer als Alpha, aber alle liefern einen Wert kleiner oder gleich Beta. Manchmal kann man frühzeitig erkennen, um welchen Knoten es sich handelt. Liefert der erste getestete Folgezug einen Wert größer gleich Beta, dann ist es ein Beta-Knoten. Liefert er einen Wert kleiner gleich Alpha, dann ist es möglicherweise ein Alpha-Knoten (vorausgesetzt, die Züge sind gut vorsortiert). Liefert er aber einen Wert zwischen Alpha und Beta, so handelt sich möglicherweise um einen Principal-Variation-Knoten. Die NegaScout-Suche nimmt nun an, dass ein Folgezug, der einen Wert zwischen Alpha und Beta liefert, sich als bester möglicher Zug herausstellen wird. Deshalb wird das Alpha-Beta-Fenster im folgenden minimal verkleinert (Nullfenstersuche), um eine maximale Anzahl an Cutoffs zu erreichen, aber dennoch die verbleibenden Züge als schlechter zu beweisen. Im folgenden der Pseudocode für die mit NegaScout verbesserte Alpha-Beta-Suche: function ALPHA_BETA_NEGASCOUT(zustand,alpha,beta) { PV_GEFUNDEN = false if END_TEST(zustand) then return BEWERTEN(zustand) else init WERT minimal; for each op in OPERATOREN do AUSFUEHREN(op) 3 Optimierung 11 if PV_GEFUNDEN then WERT = -ALPHA_BETA_NEGASCOUT(zustand-1,-alpha-1,-alpha) if WERT > alpha und WERT < beta then WERT = -ALPHA_BETA_NEGASCOUT(zustand-1,-beta,-alpha) else WERT = -ALPHA_BETA_NEGASCOUT(zustand-1,-beta,-alpha) end ZURUECKNEHMEN(op) if WERT >= beta then return beta if WERT > alpha then alpha = WERT PV_GEFUNDEN = true end return alpha end } MAX -∞ 12 ∞ -∞ 10 ∞ 10 12 ∞ 12 3 ∞ MIN MAX -∞ 10 ∞ -∞ 12 10 10 12 ∞ 10 13 12 12 3 ∞ 10 -5 -6 12 ? 10 12 ? 13 ? 3 2 3 ? -4 ? ? ? Bild 3.1: Obige Abbildung zeigt einen Beispielbaum mit 18 Blättern, von denen nur 11 durch den NegaScout-Algorithmus ausgewertet werden. Die drei umrandeten Werte eines inneren Knotens beschreiben den Alpha-Wert, den Rückgabewert und den Beta-Wert. rot: 12,13 - Beta-Cut; rot: 3 - Alpha-Cut; grün: 12 - NegaScout-Cut 3 Optimierung 3.3 12 Iterative Tiefensuche Die iterative Tiefensuche ist die schrittweise Erhöhung der Tiefe des Suchbaumes. Da die Alpha-Beta-Suche eine Tiefensuche ist, kann man meist vorher nicht bestimmen, wie lange die Berechnung dauern wird. Deshalb beginnt man mit einer geringen Suchtiefe und erhöht diese nach und nach. Das Ergebnis einer Berechnung kann benutzt werden, um bei der nächsten Berechnung die Züge besser vorzusortieren (s. Zugvorsortierung, NegaScout-Suche). 3.4 Aspiration Window Aspiration windows werden zusammen mit der iterativen Tiefensuche verwendet. Grundsätzlich beginnt die Alpha-Beta-Suche an der Wurzel mit einem maximalen Fenster. Bei der iterativen Tiefensuche kann aber angenommen werden, dass eine neue Berechnung mit höherer Tiefe einen ähnlichen Ergebniswert liefert wird wie die vorangegangene. Deshalb kann das Suchfenster initial auf einen (relativ) kleinen Bereich um den Ergebniswert der vorherigen Berechnung gesetzt werden. Stellt sich heraus, dass dieses Fenster zu klein war, muss man (ähnlich wie bei der Principal-Variation-Suche) die Suche mit maximalem Fenster wiederholen. 3.5 Killer-Heuristik Die Killer-Heuristik ist eine spezielle Art der Zugvorsortierung. Man nimmt hierbei an, dass Züge, die einen Cutoff verursacht haben, auch in anderen Teilen des Suchbaumes (bei gleicher Tiefe) einen Cutoff verursachen werden. Deshalb werden sie künftig immer zuerst betrachtet, sofern sie in der gerade betrachteten Spielposition gültige Züge darstellen. Diese Heuristik kann nicht bei allen Spielen sinnvoll angewendet werden, da die Aussicht darauf, dass Killerzüge auch in anderen Teilen des Suchbaumes noch gültige Züge sind, gering ist. Diese Heuristik wird deswegen nicht in dem Schachprogramm implementiert sein. 3.6 Quiescent-Suche Die Alpha-Beta-Suche bzw. der Minimax-Algorithmus allgemein haben das Problem, dass bei einer gewissen Tiefe die Suche strikt abgebrochen wird, obwohl der Ergebniswert an 3 Optimierung 13 dieser Stelle die Brettstellung nicht besonders gut widerspiegelt. Anstatt die Alpha-BetaSuche bei der erreichten Höchsttiefe abzubrechen, wird eine spezielle Quiescent-Suche fortgeführt, die nur noch wenige wichtige der möglichen Züge betrachtet. Dadurch wird vermieden, dass kritische Spielpositionen an den Blättern allein durch die Bewertungsfunktion evaluiert werden. 3.7 Null-Zug-Suche Speziell in Schachprogrammen hat sich die Null-Zug-Suche (oft auch das NullmovePruning genannt) bewährt. Diese Technik wird benötigt, um die Ermittlung der Spielstärke möglicher Züge bzw. Spielverläufe zu beschleunigen: Es werden Züge, welche durch unten beschriebenes Verfahren als zu schwach ermittelt werden, von einer weiteren Berechnung ausgeschlossen. Ausgehend von der Annahme, dass das Zugrecht einen Vorteil darstellt, wird beim Nullmove-Pruning in der Baumsuche einer Seite ermöglicht zwei Züge auszuführen. Ist der dadurch erzielte Vorteil nicht groß genug, so war wahrscheinlich schon der erste der beiden Züge minderwertig, und der daraus resultierende Ast des Spielbaums braucht nicht weiter untersucht zu werden. Er wird einfach abgeschnitten. Hierdurch können minderwertige Varianten gut und schnell erkannt werden und die zur Verfügung stehende Zeit für die Analyse wichtigerer Varianten genutzt werden. 3.8 Hash-Tabellen Bisher wurde immer so getan, als ob verschiedene Zugfolgen auch immer zwangsläufig zu verschiedenen Stellungen führen würden. Das ist allerdings im Schach nicht der Fall. Zum Beispiel führen, wenn keine Schlagfälle o. ä. dabei sind, Zugfolgen vom Typ A-B-C und CB-A zur gleichen Stellung. Es wäre unvorteilhaft, die komplette Suche noch einmal in der Stellung nach C-B-A durchzuführen, wenn die Stellung A-B-C bereits untersucht wurde. Oftmals kann nämlich das Ergebnis unverändert übernommen werden (die Einschränkung ergibt sich durch evtl. andere Alpha- und Beta-Werte beim Aufruf). Es wäre also sinnvoll, wenn das Programm sich merken würde, welche Stellungen es bereits untersucht hat und zu welchem Ergebnis es ggf. gekommen ist. Das macht man, indem diese Ergebnisse in einer sogenannten Hashtabelle“ gespeichert werden. ” Es kann allerdings durchaus vorkommen, dass die gleiche Stellung während einer Suche mehrmals bei einer unterschiedlichen Zahl vorangegangener Züge und entsprechend höher- 3 Optimierung 14 er bzw. niedrigerer Resttiefe besichtigt wird. Deshalb muss das Programm sich auch noch merken, welche Tiefe die Suche hatte, auf der der Eintrag basiert. 3.9 Ruhesuche Nun soll noch eine Lösung zu einem (schachspezifischen) Problem vorgestellt werden, das der bisher vorgestellte Algorithmus mit sich bringt. Würde man tatsächlich immer nur fix bis zu einer festen Tiefe rechnen, so käme es am Ende der Varianten, die das Programm ausgibt, meist zu einem wahren Schlagabtausch. Der Grund: da nach Erreichen der vorgesehen Suchtiefe nicht mehr weitergerechnet wird, werden auch einstehende Figuren nicht mehr erkannt. Die Partei, die den letzten Zug ausführen darf, könnte also ungestraft gedeckte Steine schlagen und würde dafür auch noch mit einem hohen Wert belohnt, weil das mögliche Wiederschlagen nicht mehr erfasst wird. Dieser Effekt kann bei längeren Schlagfolgen auch schon ein paar Halbzüge vor Erreichen der Endknoten eintreten. Die auf diese Weise ermittelten besten Züge wären dann oft zufälliger Natur. Grundsätzlich wäre es also besser, wenn die Bewertungsfunktion erst dann aufgerufen wird, wenn Ruhe“ am Brett herrscht, d. h. fürs erste keine Schlagzüge ” mehr möglich sind. Da solche Stellungen jedoch oft viel zu spät eintreten, behilft man sich folgendermaßen: ist die entsprechende Tiefe erreicht, so wird der Wert, den die Bewertungsfunktion zurückliefert, als sicherer Wert für die Partei am Zug angesehen. Weiterhin werden nun aber auch noch alle Schlagzüge untersucht. Nach Ausführung eines solchen wird wieder der Wert der entstandenen Stellung bestimmt. Diesen hat nun die gegnerische Partei sicher, und jetzt werden für diese alle Schlagzüge untersucht und so weiter. Jede Partei kann also, wenn sie am Zug ist, noch eine Schlagserie beginnen oder aussteigen“. Auf diese Weise wird das ” oben geschilderte Problem vermieden. Denn es ist sichergestellt, dass auf jedes Schlagen das evtl. mögliche Wiederschlagen erkannt wird. Da die Zahl der Schlagzüge in aller Regel sehr gering ist und sich Schlagfolgen auch noch selbst begrenzen (im Schach ist hier nach spätestens dreißig Zügen mangels Material Schluss), ist der Zusatzaufwand nicht allzu extrem. Das Spiel des Programms wird jedoch merklich verbessert. Kapitel 4 Bewertungsfunktion Die Bewertungsfunktion ist das, was ein Schachprogramm erst richtig intelligent macht. Zwar werden durch den Suchalgorithmus alle Züge schnell gefunden und ausgeführt, aber welcher Zug das ist, wird eben genau durch diese Bewertungsfunktion bestimmt. Die Bewertungsfunktion liefert die Bewertung einer Stellung zurück, ohne die Nachfolgezüge zu bestimmen. Sie setzt sich aus einer materiellen und einer positionellen Komponente zusammen. Die positionelle Komponente ergänzt die materielle, da die Stärke der Spielfiguren auch von ihren Positionen untereinander abhängen. Das Schachprogramm zeigt die Bewertung einer Spielsituation numerisch, in sogenannten Bauerneinheiten, an. Wobei positive Werte Vorteile und negative Werte Nachteile für einen bestimmten Spieler bedeuten. Für die materielle Wertung werden für die auf dem Brett befindlichen Spielfiguren Werte addiert. Dabei entsprechen Bauer = 100, Springer = 275, Läufer = 325, Turm = 465 und Dame = 900. Für Schwarz gelten die entsprechenden negativen Werte. Der König braucht nicht mitgezählt zu werden, da beide Parteien während des Spiels nur einen König haben. Die positionelle Wertung zu bestimmen, ist eine Aufgabe von größerer Komplexität, in der sich die verschiedenen Schachprogramme deutlich voneinander unterscheiden. Dabei wird versucht, Stellungen aufgrund von schachrelevanten Parametern zu bewerten. Schachrelevante Parameter lassen sich grob klassifizieren in Königssicherheit, Bauernstruktur, beherrschte und bedrohte Felder sowie Figurenentwicklung. So wird zum Beispiel eine Stellung bei der die Türme noch eingeengt zwischen Springern und Bauern stehen schlechter bewertet als eine, bei der die Türme schon auf offenen Linien stehen. Man kann also sagen, dass sich die positionelle Bewertung auf Heuristiken stützt. (Lüscher 2000) Kapitel 5 Zusammenfassung und Ausblick Im Rahmen der vorliegenden Arbeit wurde der Minimax-Algorithmus für ein Schachprogramm adaptiert und durch die Alpha-Beta-Suche und später durch die NegaScout-Suche optimiert. Der vorgestellte und verbesserte Algorithmus wurde in einem Schachprogramm implementiert. An nächster Stelle wurden auch weitere Optimierungsverfahren vorgestellt, welche jedoch nicht im Schachprogramm implementiert sind. Die Wichtigkeit der Bewertungsfunktion ist in dieser Arbeit erwähnt, wird aber nicht weiter vertieft. Eine sicherlich interessanter Ausblick wäre die Diskussion über das Lernen bzw. automatische Generieren solch einer Bewertungsfunktion (z. B. mit Neuronalen Netzen). Literaturverzeichnis Fierz 2005 Fierz, Martin: Strategy Game Programming. 2005 Heuner 2002 Heuner: Schachprogrammierung. 2002 Lorenz 2006 Lorenz, Ulf: Der Alphabeta-Algorithmus für Spielbaumsuche. 2006 Lüscher 2000 Lüscher, Matthias: Automatisierte Generierung einer Bewertungs- funktion für Schachendspiele, ETH Zürich, Diplomarbeit, 2000 Wikipedia.org 2006 Zwanzger 2004 Wikipedia.org: Alpha-Beta-Suche. 2006 Zwanzger, Johannes: Das Geheimnis der Schachprogramme. In: Schachprogramme (2004), S. 48–55