Ich denke, ich könnte alle möglichen Zustände für einen Spieltick generieren, aber mit vier Spielern und 5 Grundaktionen (4 Züge und Bombenplatz) ergibt sich 5 ^ 4 Zustände auf der ersten Ebene des Spielbaums.
Richtig! Sie müssen alle 5 ^ 4 (oder sogar 6 ^ 4, da Sie in 4 Richtungen gehen, anhalten und "eine Bombe legen"?) Aktionen für jeden Spieltick suchen. ABER wenn sich ein Spieler bereits für einen Zug entschieden hat, dauert es einige Zeit, bis der Zug ausgeführt wird (z. B. 10 Spiel-Ticks). Während dieser Zeit verringert sich die Anzahl der Möglichkeiten.
Dieser Wert steigt mit jedem nächsten Level exponentiell an. Vermisse ich etwas Gibt es Möglichkeiten, es zu implementieren, oder sollte ich einen völlig anderen Algorithmus verwenden?
Sie können eine Hash-Tabelle verwenden, um denselben Spielstatus "Teilbaum" nur einmal zu berechnen. Stellen Sie sich vor, Spieler A geht auf und ab, während alle anderen Spieler "warten", landen Sie im selben Spielzustand. Es ist das gleiche wie für "links-rechts" oder "rechts-links". Auch das Verschieben von "nach oben, dann nach links" und "nach links, dann nach oben" führt zum gleichen Zustand. Mit einer Hash-Tabelle können Sie die berechnete Punktzahl für einen bereits ausgewerteten Spielstatus "wiederverwenden". Dies reduziert die Wachstumsgeschwindigkeit erheblich. Mathematisch reduziert es die Basis Ihrer exponentiellen Wachstumsfunktion. Um eine Vorstellung davon zu bekommen, um wie viel sich die Komplexität verringert, betrachten wir die Bewegungen, die nur für einen Spieler möglich sind, im Vergleich zu erreichbaren Positionen auf der Karte (= verschiedene Spielzustände), wenn sich der Spieler nur nach oben / unten / links / rechts / anhalten bewegt .
Tiefe 1: 5 Züge, 5 verschiedene Zustände, 5 zusätzliche Zustände für diese Rekursion
Tiefe 2: 25 Züge, 13 verschiedene Zustände, 8 zusätzliche Zustände für diese Rekursion
Tiefe 3: 6125 Züge, 25 verschiedene Zustände, 12 zusätzliche Zustände für diese Rekursion
Um dies zu visualisieren, antworten Sie sich selbst: Welche Felder auf der Karte können mit einem Zug, zwei Zügen, drei Zügen erreicht werden. Die Antwort lautet: Alle Felder mit einem maximalen Abstand = 1, 2 oder 3 von der Startposition.
Wenn Sie eine HashTable verwenden, müssen Sie jeden erreichbaren Spielstatus (in unserem Beispiel 25 in Tiefe 3) nur einmal auswerten. Ohne eine HashTable müssen Sie sie mehrmals auswerten, was 6125 Auswertungen anstelle von 25 in Tiefenstufe 3 bedeuten würde. Das Beste: Sobald Sie einen HashTable-Eintrag berechnet haben, können Sie ihn in späteren Zeitschritten wiederverwenden ...
Sie können auch inkrementelle Vertiefungs- und Alpha-Beta-Bereinigungs-Teilbäume verwenden, die es nicht wert sind, genauer untersucht zu werden. Beim Schach reduziert dies die Anzahl der gesuchten Knoten auf etwa 1%. Eine kurze Einführung in das Alpha-Beta-Bereinigen finden Sie hier als Video: http://www.teachingtree.co/cs/watch?concept_name=Alpha-beta+Pruning
Ein guter Anfang für weitere Studien ist http://chessprogramming.wikispaces.com/Search . Die Seite bezieht sich auf Schach, aber die Such- und Optimierungsalgorithmen sind ziemlich gleich.
Ein anderer (aber komplexer) KI-Algorithmus - der für das Spiel besser geeignet wäre - ist "Temporal Difference Learning".
Grüße
Stefan
PS: Wenn Sie die Anzahl der möglichen Spielzustände reduzieren (z. B. sehr kleine Kartengröße, nur eine Bombe pro Spieler, sonst nichts), besteht die Möglichkeit, eine Bewertung für alle Spielzustände vorab zu berechnen.
--bearbeiten--
Sie können auch offline berechnete Ergebnisse der Minimax-Berechnungen verwenden, um ein neuronales Netzwerk zu trainieren. Oder Sie können sie verwenden, um handimplementierte Strategien zu bewerten / vergleichen. Zum Beispiel könnten Sie einige der vorgeschlagenen "Persönlichkeiten" und einige Heuristiken implementieren, die erkennen, in welchen Situationen welche Strategie gut ist. Daher sollten Sie Situationen (z. B. Spielzustände) "klassifizieren". Dies könnte auch von einem neuronalen Netzwerk erledigt werden: Trainieren Sie ein neuronales Netzwerk, um vorherzusagen, welche der handcodierten Strategien in der aktuellen Situation am besten funktioniert, und führen Sie sie aus. Dies sollte zu extrem guten Echtzeitentscheidungen für ein echtes Spiel führen. Viel besser als eine Suche mit niedrigem Tiefenlimit, die sonst durchgeführt werden kann, da es nicht so wichtig ist, wie lange die Offline-Berechnungen dauern (sie sind vor dem Spiel).
- bearbeiten # 2 -
Wenn Sie Ihre besten Züge nur alle 1 Sekunde neu berechnen, können Sie auch versuchen, eine höhere Ebene zu planen. Was meine ich damit? Sie wissen, wie viele Züge Sie in 1 Sekunde ausführen können. Sie können also eine Liste der erreichbaren Positionen erstellen (z. B. wenn dies 3 Züge in 1 Sekunde wären, hätten Sie 25 erreichbare Positionen). Dann könnten Sie wie folgt planen: Gehen Sie zu "Position x und platzieren Sie eine Bombe". Wie einige andere vorgeschlagen haben, können Sie eine "Gefahren" -Karte erstellen, die für den Routing-Algorithmus verwendet wird (wie geht man zu Position x? Welcher Pfad sollte bevorzugt werden [in den meisten Fällen sind einige Variationen möglich]). Dies ist im Vergleich zu einer großen HashTable weniger speicherintensiv, führt jedoch zu weniger optimalen Ergebnissen. Da jedoch weniger Speicher benötigt wird, kann dies aufgrund von Caching-Effekten schneller sein (bessere Verwendung Ihrer L1 / L2-Speicher-Caches).
ZUSÄTZLICH: Sie können Vorsuchen durchführen, die nur Züge für jeweils einen Spieler enthalten, um Variationen zu sortieren, die zum Verlust führen. Nehmen Sie deshalb alle anderen Spieler aus dem Spiel ... Speichern Sie, welche Kombinationen jeder Spieler wählen kann, ohne zu verlieren. Wenn nur Züge verloren gehen, suchen Sie nach den Zugkombinationen, bei denen der Spieler am längsten am Leben bleibt. Um diese Art von Baumstrukturen zu speichern / verarbeiten, sollten Sie ein Array mit folgenden Indexzeigern verwenden:
class Gamestate {
int value;
int bestmove;
int moves[5];
};
#define MAX 1000000
Gamestate[MAX] tree;
int rootindex = 0;
int nextfree = 1;
Jeder Zustand hat einen Bewertungswert und wird beim Bewegen (0 = Stopp, 1 = Aufwärts, 2 = Rechts, 3 = Abwärts, 4 = Links) mit den nächsten Spielzuständen verknüpft, indem der Array-Index in "Baum" in Zügen [0 "gespeichert wird ] zu bewegen [4]. Um Ihren Baum rekursiv zu erstellen, könnte dies folgendermaßen aussehen:
const int dx[5] = { 0, 0, 1, 0, -1 };
const int dy[5] = { 0, -1, 0, 1, 0 };
int search(int x, int y, int current_state, int depth_left) {
// TODO: simulate bombs here...
if (died) return RESULT_DEAD;
if (depth_left == 0) {
return estimate_result();
}
int bestresult = RESULT_DEAD;
for(int m=0; m<5; ++m) {
int nx = x + dx[m];
int ny = y + dy[m];
if (m == 0 || is_map_free(nx,ny)) {
int newstateindex = nextfree;
tree[current_state].move[m] = newstateindex ;
++nextfree;
if (newstateindex >= MAX) {
// ERROR-MESSAGE!!!
}
do_move(m, &undodata);
int result = search(nx, ny, newstateindex, depth_left-1);
undo_move(undodata);
if (result == RESULT_DEAD) {
tree[current_state].move[m] = -1; // cut subtree...
}
if (result > bestresult) {
bestresult = result;
tree[current_state].bestmove = m;
}
}
}
return bestresult;
}
Diese Art der Baumstruktur ist viel schneller, da die dynamische Zuweisung von Speicher sehr langsam ist! Das Speichern des Suchbaums ist jedoch auch ziemlich langsam ... Dies ist also eher eine Inspiration.