TicTacToe - Varianten der NegaAlphaBeta Zugsuche und notwendige Klassen
Die Regeln
TicTacToe wird auf einem 3X3 Feld gespielt. Es wird abwechselnd gezogen. Der Spieler, der zuerst 3 Spielsteine
in einer diagonalen, vertikalen oder horizontalen Reihe anordnet, hat gewonnen und beendet damit unmittelbar das Spiel.
Ansonsten endet das Spiel, wenn kein weiterer Zug möglich ist, unentschieden.
Wie groß ist das Problem?
Jedes Feld kann drei Zustände annehmen: leer, MaxStein, MinStein. Das Feld kann also 39 = 19683 verschiedene
Zustände einnehmen. Wie kann es gefüllt werden? Für die Position des ersten Steins gibt es 9
Möglichkeiten, für den Zweiten 8, für den Dritten 7 usw.. Es kommen also 9! = 362880 verschiedene
Zugmöglichkeiten in Betracht. Weil diese Züge in unterschiedlicher Reihenfolge ausgeführt werden
können, muss ein Baum mit (laut Programm) 549945 Stellungen untersucht werden. Für heutige Rechner
ist es kein Problem diese Varianten vollständig durchzurechnen. Außerdem ist da noch ordentlich Platz für
Optimierungen: Es gibt z.B. für den ersten Zug nur 3 prinzipielle Positionen, alle anderen
lassen sich durch Rotation oder Spiegelung des Spielfeldes erreichen.
Wie löst man das Problem?
Man muss eine Möglichkeit schaffen, alle Züge auszuführen und zu bewerten.
Üblicher Weise wird wie folgt bewertet: Gewinnt Spieler Max, erhält die Stellung
den Wert +1, Gewinnt Spieler Min, erhält die Stellung den Wert -1, bei Unentschieden erhält
die Stellung den Wert 0. Der eine, der Max Spieler, hat die Aufgabe den Stellungswert zu maximieren.
Sein Gegner, der Min Spieler, muss den Stellungswert minimieren.
Da auf jeden Zug in der Regel mehrere Antwortzüge möglich sind, ergibt sich ein
verzweigter Baum, bei dem die Kanten Züge und die Knoten Stellungen entsprechen.
Im folgenden Bild ist ein Ausschnitt eines auf diese Art erzeugeten Baumes zu sehen.
Min (Rot) hat gesetzt, Max (Blau) hat das Zugrecht. Eine bestimmte Stellungsfolge ergibt sich,
von der Startstellung (Wurzel) entlang der Kanten bis zum jeweiligen Blatt.
-
- Ausschnitt des TicTacToe Suchbaumes
Schwarze Ziffern stellen Ergebnisse der Stellungsbewertung des Blattes dar, rote Ziffern sind im Baum von unten nach
oben weitergereichte Stellungswerte.
Max wählt den Zug mit der höchsten Bewertung in der tieferen Ebene, Min den mit der niedrigsten.
Bei gleich guten wird der Erste gewählt. Das Spiel wird unentschieden enden.
Es gibt zwei Möglichketen den Baum zu untersuchen. Die Breitensuche, die jeweils die Stellungen
in einer Ebene untersucht (unter Umständen sehr hoher Speicherbedarf) oder die Tiefensuche,
die jeweils einen Pfad von der Wurzel bis zum Blatt untersucht.
Hier wird die Tiefensuche verwendet. Der MiniMax bzw. AlphaBeta Algorithmus und deren
Erweiterungen bewerkstelligen genau das. Schöne Artikel finden sich in der Wikipedia zur
Minimax-
und AlphaBetasuche.
Was wird benutzt?
Das Programm wird mit dem MS Visual Studio 12, WPF und C# entwickelt.
Bäume werden rekursiv untersucht. Das Spielfeld wird wegen der enormen
Rechenzeitvorteile als Bitboard realisiert. Daher kommen logische Verknüpfungen auf
Bit Ebene vor. Arrays, Listen, Maps und Hashtables sind wichtig.
Delegates werden hauptsächlich wegen der thread übergreifenden Aufrufe vom Spiel
zur Oberfläche vorkommen.
Was wird alles implementiert?
Eine Übersicht über alle implementierte Klassen, ihre Abhängigkeiten, Variablen, Funktion usw. bietet das Klassendiagramm.
Wie anfangen?
Ich mag die Top-Down Entwicklungsstrategie und beginne mit der Frage: Was soll das Programm
können und welche Oberflächenelemente werden dazu benötig? Es soll:
- Die Oberfläche
- die Oberflächenelemente (Ribbon, Canvas, Listview) anordnen. (Grid)
- die Auswahl von 2 Spielertypen (Max und Min) ermöglichen.
(2 RibbonComboboxen weil für Max- und Minspieler mehrere unterschiedliche
Suchverfahren wählbar sein sollen.)
- Informationen über den aktuellen Spielzustand anzeigen können. (Label im Ribbon)
- das Spiel starten und beenden können. (Button im Ribbon)
- das Spielfeld darstellen. (Canvas)
- ein Protokoll mit Information zu den Zügen anzeigen. (ListView)
- Eingaben des menschlichen Spielers annehmen. (MausClick Handler am Spielfeld Canvas)
- jederzeit bedienbar sein. (GameLoop im Thread)
- die eigentliche Spiel Klasse enthalten.
Mit WPF im MS Visual Studio bekommt die Oberfläche dieses Aussehen. Der Code wird vom
Visual Studio erzeugt, wenn man die Elemente im Editor setzt. Einzig die Zeilen 6 - 20
(Window.Resources) sind händisch erzeugt und dienen dazu, die Zeilen im Listview
abwechseln zu färben. Der zugehörige XAML Code sieht so aus:
Im zugehörigem Code behind werden
- die globale Game und Board Klasse gehalten,
- die Kommunikation der Game- und der SearchHumanKlasse mit den Oberflächenelementen
(Comboboxen, Buttons, Listview, Canvas usw.) hergestellt und
- auf Useranforderungen zu reagiert.
Diese Aufgaben werden über die On...(object sender, ...EventArgs e) Eventhandler wahrgenammen.
Sie gestaltet sich so:
using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Ribbon;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace PlasticineTicTacToe
{
public partial class MainWindow : RibbonWindow
{
delegate void DelegateGameFinished(GameState gamesState);
DelegateGameFinished delegateGameFinished;
delegate void DelegateWriteInfo(bool playerMaxHasToMove);
DelegateWriteInfo delegateWriteInfo;
const int BOARDCOLUMNS = 3;
const int BOARDROWS = 3;
Board board;
Game game;
public MainWindow()
{
InitializeComponent();
}
private void OnNewGame(object sender, RoutedEventArgs e)
{
EnableUI(true);
Player PlayerMax = (Player)((ComboBoxItem)galPlayerMax.SelectedItem).DataContext;
Player PlayerMin = (Player)((ComboBoxItem)galPlayerMin.SelectedItem).DataContext;
if (PlayerMax.behaviorSearch is SearchHuman)
{
((SearchHuman)PlayerMax.behaviorSearch).SendCursor += new SearchHuman.OnSetCursor(OnSetCursor);
canvasBoard.MouseLeftButtonUp += ((SearchHuman)PlayerMax.behaviorSearch).OnMouseLeftButtonUp;
}
if (PlayerMin.behaviorSearch is SearchHuman)
{
((SearchHuman)PlayerMin.behaviorSearch).SendCursor += new SearchHuman.OnSetCursor(OnSetCursor);
canvasBoard.MouseLeftButtonUp += ((SearchHuman)PlayerMin.behaviorSearch).OnMouseLeftButtonUp;
}
RemovePieces();
game.Start(PlayerMax, PlayerMin);
}
private void OnQuitGame(object sender, RoutedEventArgs e)
{
game.Abort();
canvasBoard.Cursor = Cursors.No;
EnableUI(false);
}
private void OnHelp(object sender, RoutedEventArgs e)
{
}
private void OnInitialized(object sender, EventArgs e)
{
Thread.Sleep(1000);
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
board = new Board(BOARDCOLUMNS, BOARDROWS);
FillUIElements(board);
game = new Game(board);
game.SendWriteProtokoll += new Game.OnWriteProtokoll(OnWriteProtokoll);
game.SendWriteInfo += new Game.OnWriteInfo(OnWriteInfo);
game.SendGameFinished += new Game.OnGameFinished(OnGameFinished);
game.SendSetPiece += new Game.OnSetPiece(OnSetPiece);
delegateWriteInfo = WriteInfo;
delegateGameFinished = GameFinished;
}
private void OnClosed(object sender, EventArgs e)
{
game.Abort();
}
private void FillUIElements(Board board)
{
FillPlayerComboBox(rgcPlayerMax, board);
FillPlayerComboBox(rgcPlayerMin, board);
galPlayerMax.SelectedItem = rgcPlayerMax.Items[0];
galPlayerMin.SelectedItem = rgcPlayerMin.Items[1];
DrawCellGridLines();
DrawCellIndex();
}
private void FillPlayerComboBox(RibbonGalleryCategory rgc, Board board)
{
EvaluateBase bahaviorMinMax = new EvaluateMinMax(board);
EvaluateBase bahaviorNega = new EvaluateNega(board);
ComboBoxItem cbi = new ComboBoxItem();
cbi.Content = "Mensch";
cbi.DataContext = new Player(new SearchHuman(board, bahaviorMinMax, new MoveGenerator(board, bahaviorMinMax)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "Computer Zufall";
cbi.DataContext = new Player(new SearchRandom(board, bahaviorMinMax, new MoveGenerator(board, bahaviorMinMax)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "MiniMax";
cbi.DataContext = new Player(new SearchMiniMax(board, bahaviorMinMax, new MoveGenerator(board, bahaviorMinMax)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "Nega MiniMax";
cbi.DataContext = new Player(new SearchNegaMax(board, bahaviorNega, new MoveGenerator(board, bahaviorNega)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "AlphaBeta";
cbi.DataContext = new Player(new SearchAlphaBeta(board, bahaviorMinMax, new MoveGenerator(board, bahaviorMinMax)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "NegaAlphaBeta";
cbi.DataContext = new Player(new SearchNegaAlphaBeta(board, bahaviorNega, new MoveGenerator(board, bahaviorNega)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "NegaAlphaBetaSort";
cbi.DataContext = new Player(new SearchNegaAlphaBetaSort(board, bahaviorNega, new MoveGenerator(board, bahaviorNega)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
cbi = new ComboBoxItem();
cbi.Content = "NegaAlphaBetaSortTrans";
cbi.DataContext = new Player(new SearchNegaAlphaBetaSortTrans(board, bahaviorNega, new MoveGenerator(board, bahaviorNega)));
cbi.IsHitTestVisible = false;
rgc.Items.Add(cbi);
}
private void EnableUI(bool runningGame)
{
btnNewGame.IsEnabled = !runningGame;
btnQuitGame.IsEnabled = runningGame;
rcbPlayerMax.IsEnabled = !runningGame;
rcbPlayerMin.IsEnabled = !runningGame;
if (!runningGame)
{
// Workaround für RibbonCombobox Problem
// Items bleiben disabled, wenn die RibbonCombobox
// wieder enabled wird. Durch die Commandzuweisung
// und Entfernung wird ein internes reinit erzwungen
// und der enabled Status korekt gesetzt.
galPlayerMax.Command = ApplicationCommands.Print;
galPlayerMax.Command = null;
galPlayerMin.Command = ApplicationCommands.Print;
galPlayerMin.Command = null;
}
}
private void OnWriteProtokoll(Object obj, ProtokollEventArgs e)
{
string strSec = String.Format("{0:0.000}", e.seconds);
lvProtokoll.Dispatcher.Invoke(
DispatcherPriority.Normal,
new Action(delegate()
{
lvProtokoll.Items.Insert(0, new
{
MoveNr = e.moveNumber,
Move = e.idxCell,
Nodes = e.nodes,
Sec = strSec,
NPerSec = e.nodesPerSec,
Leaf = e.evaluateLeaf
});
})
);
}
private void OnWriteInfo(Object obj, InfoEventArgs e)
{
this.Dispatcher.Invoke(DispatcherPriority.Normal,
delegateWriteInfo, e.playerMaxHasToMove);
}
private void WriteInfo(bool playerMaxHasToMove)
{
imgToDo.Source = playerMaxHasToMove ? rcbPlayerMax.SmallImageSource : rcbPlayerMin.SmallImageSource;
lblToDo.Content = "Ist am Zug.";
}
private void OnGameFinished(Object obj, GameFinishedEventArgs e)
{
this.Dispatcher.Invoke(DispatcherPriority.Normal,
delegateGameFinished, e.gameState);
}
private void GameFinished(GameState gameState)
{
lblToDo.Content = "hat gewonnen.";
switch (gameState)
{
case GameState.playerMaxWins: imgToDo.Source = rcbPlayerMax.SmallImageSource; break;
case GameState.playerMinWins: imgToDo.Source = rcbPlayerMin.SmallImageSource; break;
case GameState.drawGame:
{
lblToDo.Content = "unentschieden.";
imgToDo.Source = new BitmapImage(new Uri(@"pack://application:,,/pics/Both.png", UriKind.RelativeOrAbsolute));
}
break;
}
lvProtokoll.Items.Insert(0, new
{
Move = new String('\u2550', 8),
Cell = new String('\u2550', 8),
Nodes = new String('\u2550', 8),
Sec = new String('\u2550', 8),
kNPerSec = new String('\u2550', 8),
Leaf = new String('\u2550', 8)
});
EnableUI(false);
canvasBoard.Cursor = Cursors.No;
}
private void OnSetCursor(Object obj, CursorEventArgs e)
{
canvasBoard.Dispatcher.Invoke(
DispatcherPriority.Normal,
new Action(delegate()
{
canvasBoard.Cursor = e.cursor;
})
);
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (board == null) return;
double cellWidth = canvasBoard.ActualWidth / board.MaxCols;
double cellHeight = canvasBoard.ActualHeight / board.MaxRows;
for (int idx = 0; idx < canvasBoard.Children.Count; idx++)
{
int cellIdx;
var obj = canvasBoard.Children[idx];
if (obj.GetType() == typeof(Line))
{
Line child = obj as Line;
int.TryParse(child.Name.Remove(0, 4), out cellIdx);
if (child.X1 == child.X2)
{
child.X1 = child.X2 = cellIdx * cellWidth;
child.Y1 = 0;
child.Y2 = canvasBoard.ActualHeight;
}
else
{
child.Y1 = child.Y2 = cellIdx * cellHeight;
child.X1 = 0;
child.X2 = canvasBoard.ActualWidth;
}
}
else if (obj.GetType() == typeof(TextBlock))
{
TextBlock child = obj as TextBlock;
int.TryParse(child.Name.Remove(0, 4), out cellIdx);
int colNumber = board.MaxCols - 1 - cellIdx % board.MaxCols;
int rowNumber = board.MaxRows - 1 - cellIdx / board.MaxCols;
Canvas.SetLeft(child, colNumber * cellWidth + cellWidth / 10);
Canvas.SetTop(child, rowNumber * cellHeight + cellHeight / 10);
}
else if (obj.GetType() == typeof(Ellipse))
{
double vertSymBorderDist = cellHeight / 5.0;
double horiSymBorderDist = cellWidth / 5.0;
Ellipse child = obj as Ellipse;
int.TryParse(child.Name.Remove(0, 9), out cellIdx);
child.Width = cellWidth - 2 * horiSymBorderDist;
child.Height = cellHeight - 2 * vertSymBorderDist;
Canvas.SetTop(child, (board.MaxRows - 1 - cellIdx / board.MaxCols) * cellHeight + vertSymBorderDist);
Canvas.SetLeft(child, (board.MaxCols - 1 - cellIdx % board.MaxCols) * cellWidth + horiSymBorderDist);
}
}
}
private void DrawCellIndex()
{
TextBlock tbCellIndex;
int colNumber, rowNumber;
double cellWidth = canvasBoard.ActualWidth / board.MaxCols;
double cellHeight = canvasBoard.ActualHeight / board.MaxRows;
for (int cellIndex = board.CellCount - 1; cellIndex >= 0; cellIndex--)
{
colNumber = board.MaxCols - 1 - cellIndex % board.MaxCols;
rowNumber = board.MaxRows - 1 - cellIndex / board.MaxCols;
tbCellIndex = new TextBlock();
tbCellIndex.Name = "Text" + cellIndex.ToString();
tbCellIndex.FontSize = 10;
tbCellIndex.Foreground = Brushes.Black;
tbCellIndex.Text = (cellIndex).ToString();
canvasBoard.Children.Add(tbCellIndex);
Canvas.SetTop(tbCellIndex, rowNumber * cellHeight + cellHeight / 10);
Canvas.SetLeft(tbCellIndex, colNumber * cellWidth + cellWidth / 10);
}
}
private void DrawCellGridLines()
{
int colNumber, rowNumber;
double cellWidth = canvasBoard.ActualWidth / board.MaxCols;
double cellHeight = canvasBoard.ActualHeight / board.MaxRows;
Line line;
double cellStrokeThickness = 2.0;
for (colNumber = 0; colNumber <= board.MaxCols; colNumber++)
{
line = new Line();
line.StrokeThickness = cellStrokeThickness;
line.Stroke = Brushes.Black;
line.Name = "Hori" + colNumber.ToString();
line.X1 = line.X2 = colNumber * cellWidth;
line.Y1 = 0;
line.Y2 = canvasBoard.ActualHeight;
canvasBoard.Children.Add(line);
}
for (rowNumber = 0; rowNumber <= board.MaxRows; rowNumber++)
{
line = new Line();
line.StrokeThickness = cellStrokeThickness;
line.Stroke = Brushes.Black;
line.Name = "Vert" + rowNumber.ToString();
line.Y1 = line.Y2 = rowNumber * cellHeight;
line.X1 = 0;
line.X2 = canvasBoard.ActualWidth;
canvasBoard.Children.Add(line);
}
}
private void RemovePieces()
{
for (int idx = canvasBoard.Children.Count - 1; idx > 0; idx--)
{
if (canvasBoard.Children[idx].GetType() == typeof(Ellipse))
{
canvasBoard.Children.RemoveAt(idx);
}
}
}
private void OnSetPiece(object sender, SetPieceEventArgs e)
{
double cellWidth = canvasBoard.ActualWidth / board.MaxCols;
double cellHeight = canvasBoard.ActualHeight / board.MaxRows;
canvasBoard.Dispatcher.Invoke(
DispatcherPriority.Normal,
new Action(delegate()
{
double symbolStrokeThickness = 4.0;
double vertSymBorderDist = cellHeight / 5.0;
double horiSymBorderDist = cellWidth / 5.0;
Ellipse ellipse = new Ellipse();
ellipse.StrokeThickness = symbolStrokeThickness;
ellipse.Stroke = e.playerMaxHasToMove ?
new SolidColorBrush(Colors.Orange) : new SolidColorBrush(Colors.DarkRed);
ellipse.Fill = e.playerMaxHasToMove ?
new SolidColorBrush(Colors.Yellow) : new SolidColorBrush(Colors.Red);
ellipse.Width = cellWidth - 2 * horiSymBorderDist;
ellipse.Height = cellHeight - 2 * vertSymBorderDist;
ellipse.Name = "CellIndex" + e.cellIdx.ToString();
canvasBoard.Children.Add(ellipse);
Canvas.SetTop(ellipse, (board.MaxRows - 1 - e.cellIdx / board.MaxCols) * cellHeight + vertSymBorderDist);
Canvas.SetLeft(ellipse, (board.MaxCols - 1 - e.cellIdx % board.MaxCols) * cellWidth + horiSymBorderDist);
})
);
}
}
}
Um überhaupt irgendwie spielen zu können, müssen mindestens die Game, Board, MoveGenerator, Move, SearchHuman, EvaluateMinMax und Player
Klassen vollständig implementiert werden.
Was muss die Game Klasse können?
Sie muss:
- auf Spielstart und -ende Button der Oberfläche reagieren. (Start, Abort)
- die gewählten Spielertypen handeln. (Start Parameterübergabe)
- das Board auf Startzustand setzen.
- den Spielablauf in der Gameloop regeln (Zugrecht, Zugsuche starten, via Nachrichten Oberfläche aktualisieren)
und dabei NICHT die Bedienung des Programms behindern. Die Gameloop soll daher einen Thread laufen.
Für diese Aufgaben kennt die Game Klasse:
- den aktuelle Gamestatus,
- die Spieler und
- das Spielfeld
Wegen der Zugriffe auf die Anwendungsoberfäche aus der Gameloop
sind diverse Delegates und Events nötig. Das geht so:
Wie ist das Board organisiert?
Die wesentliche Aufgabe des Boards besteht darin, die zur Zugsuche notwendigen Daten zu speichern.
Das sind:
- das Zugrecht (maxPlayerHasToMove)
- die Boardbelegung als 3 Bitboards (playerMaxPieces, playerMinPieces, usedCells)
- Bitmasken für ein leeres und gefülltes Spielfeld, sowie eine Liste für Masken von
Gewinnpositionen.(EmptyBoard, FilledBoard, winningPositions)
- Funktionen, die einen Zug auf dem internen Brett ausführt(Do) und zurücknimmt(Undo). Wegen der
schönen XOR Eigenschaften kann man mit der Do Funktion den Zug auch rückgängig machen.
Der besseren Lesbarkeit wegen wird ein delegate mit dem Namen 'UnDo' erzeugt und diesem die Do Funktion
zugewiesen.(DelegateUnDo)
- einen (Zobrist)Schlüssel, der das ganze Board repräseniert und später fürs
hashing verwendet wird. (boardKey)
- 2 Listen mit Zufallswerten für die Spielsteine, mit denen der Schlüssel erzeugt wird.
JEDER Zufallswert repräsentiert EINEN Spielsteintyp EINER Partei auf EINER bestimmten Position. Es sind
also 2 Parteien * 1 Steintyp * 9 Positionen = 18 Zufallswerte nötig.(keyPlayerMinPiecesPos,
keyPlayerMaxPiecesPos)
Bitboards sind bei TicTacToe nicht unbedingt notwendig. Sie werden hier aber zum Training benutzt.
Was sind die Vorteile von Bitboards?
- Sie sind klein im Vergleich mit Arrays.
- Sie sind enorm schnell beim Zugriff im Vergleich mit Arrays, weil keine Adressberechnung nötig ist.
- Mit Bitmasken kann man mit EINEM Vergleich mehrere Steinpositionen abfragen. (z.B. die Gewinnpositionen)
- Züge werden mittels logischer Operationen ausgefüht. Bei Arrays müssten
Feldinhalte geändert werden, was Zuweisungen und Adressberechnungen notwendig macht.
Leider sind Bitboards auch gewöhnungsbedürftig und im Gegensatz zum Array nicht so intuitiv.
Aus diesem Grund haben die Spielfelder auch Indexnummern, die mit dem jeweiligen Bitindex im Bitboard
übereinstimmen. Die Zuordnung von Feld zu Bitindex ist beliebig und richtet sich danach, was
fürs Programm (Züge setzten) oder fürs eigene Denken am praktischsten ist.
Wie alle Spieler grundsätzlich sind.
Die Player Base Klasse
Die PlayerBase Klasse ist, wie der Name schon sagt, Basis aller Player Klassen. Für die
PlayerHuman und PlayerRandom Klasse ist lediglich die Fähigkeit, sich den letzten Zellenindex
und das Board zu merken, von Bedeutung. Alle anderen Klassen nutzen zusätzlich den Zuggenerator, der alle (in der
aktuellen Spielsituation) möglichen Züge in eine Liste scheibt, die während der Zugsuche
abgearbeitet wird. Wichtig: der jeweilige Zug wird ohne Vertiefung (statisch) bewertet, um später eine
Sortierung der Züge zu ermöglichen. Die Qualität dieser Bewertung ist von zentraler
Bedeutung für die Beschleunigung der Suchverfahren, die von einer Zugsortierung Gebrauch machen.
Es kann von erheblichem Vorteil sein, zwei unterschiedliche Bewertungsverfahren für die statische
(hier) und die dynamische Bewertung (am Blatt) zu nutzen. Im Idealfall liefern beide den selben (korrekten)
Wert. Leider wird das kaum gelingen. Wäre das möglich, könnte man sich den Aufwand mit der
Suche klemmen. Grobe Abweichungen in der Bewertung sind aber dringend zu vermeiden. TicTacToe nutzt nur ein
Bewertungsverfahren.
SearchMove und Evaluate sind abstract damit jedes Suchverfahren seine eigene Variante nutzen kann.
Der menschliche Faktor.
Der human Player ist einfach gestrickt. Im Wesentlichen wartet er nur auf eine Eingabe,
überprüft ihre Gültigkeit und führt gegebenenfalls den Zug auf dem
Board aus.
Der Gegner NegaAlphaBeta
Die Klasse PlayerNegaAlphaBeta besteht aus drei Funktionen. SeachMove ruft den eigenlichen NegaAlphaBeta
auf und führt den gefundenen Zug aus. Evaluate bewertet die Blätter des von NegaAlphaBeta erzeugten
Baumes. Ein Blatt ist erreicht, wenn die maximale Suchtiefe erreicht oder kein weiterer Zug möglich ist.
Zentral ist die Funktion NegaAlphaBeta. Ihr Arbeitspferd ist die foreach Schliefe, die nacheinander alle Züge
des Zuggenerators abarbeitet. Zu diesem Zweck führt sie den Zug aus und ruft sich selbst wieder auf.
Jedes Mal, wenn sie aus der Rekursion zurückkommt nimmt sie den vorher ausgeführten Zug zurück, so daß
bei vollständiger Rückkehr das Board wieder im ursprüngichen Zustand ist. Sie merkt sich den Zug als
besten Zug, der auf der obersten Rekusionsebene zur besten Bewertung führt.
Der Gegner NegaAlphaBeta mit Zugsortierung und Transpositions Hashtable
Man ahnt es bereits: diese Klasse unterscheidet sich kaum von der vorherigen. SearchMove unterscheidet
sich nur in einer Zeile wegen der Transpositionstabelle. Evaluate ist identisch. Die Funktion
hätte man auch in PlayerBase ausformulieren
können. Ist aber nicht passiert, um die Möglichkeit zu haben, in späteren Projekten gleiche Suchalgorithmen mit
unterschiedlichen Bewertungsfunktionen zu testen. Hauptunterschiede sind in der
Funktion 'NegaAlphaBetaSortTrans'. Sie betreffen Zugriffe auf die Transpositionstabelle und
die Sortierung der vom Zuggenerator gelieferten Züge.