it-swarm.com.de

Was ist der optimale Algorithmus für das Spiel 2048?

Ich bin kürzlich auf das Spiel gestoßen 2048 . Sie fügen ähnliche Kacheln zusammen, indem Sie sie in eine der vier Richtungen bewegen, um "größere" Kacheln zu erhalten. Nach jedem Zug erscheint ein neues Plättchen an einer zufälligen leeren Position mit einem Wert von entweder 2 oder 4. Das Spiel wird beendet, wenn alle Felder gefüllt sind und keine Züge vorhanden sind, die Kacheln zusammenführen können, oder Sie erstellen ein Kacheln mit dem Wert 2048.

Erstens muss ich eine klar definierte Strategie verfolgen, um das Ziel zu erreichen. Also habe ich darüber nachgedacht, ein Programm dafür zu schreiben.

Mein aktueller Algorithmus:

while (!game_over) {
    for each possible move:
        count_no_of_merges_for_2-tiles and 4-tiles
    choose the move with a large number of merges
}

Was ich tue, ist zu jedem Zeitpunkt, ich werde versuchen, die Kacheln mit den Werten 2 und 4 zusammenzuführen, das heißt, ich versuche, 2 und 4 Kacheln zu haben, so gering wie möglich. Wenn ich es so versuche, werden alle anderen Kacheln automatisch zusammengeführt und die Strategie scheint gut zu sein.

Aber wenn ich diesen Algorithmus verwende, erhalte ich nur ungefähr 4000 Punkte, bevor das Spiel endet. Maximale AFAIK-Punkte sind etwas mehr als 20.000 Punkte, was viel mehr ist als meine aktuelle Punktzahl. Gibt es einen besseren Algorithmus als den oben genannten?

1877
nitish712

Ich entwickelte eine 2048 AI unter Verwendung der Expectimax -Optimierung anstelle der Minimax-Suche, die der @ ovolve-Algorithmus verwendet. Die KI führt einfach eine Maximierung über alle möglichen Züge durch, gefolgt von einer Erwartung über alle möglichen Kachelbrüche (gewichtet mit der Wahrscheinlichkeit der Kacheln, d. H. 10% für eine 4 und 90% für eine 2). Soweit mir bekannt ist, ist es nicht möglich, die Expectimax-Optimierung zu bereinigen (mit Ausnahme des Entfernens von Zweigen, die äußerst unwahrscheinlich sind). Daher wird als Algorithmus eine sorgfältig optimierte Brute-Force-Suche verwendet.

Performance

Die KI benötigt in ihrer Standardkonfiguration (maximale Suchtiefe 8) 10 ms bis 200 ms, um einen Zug auszuführen, abhängig von der Komplexität der Kartenposition. Beim Testen erreicht die KI eine durchschnittliche Bewegungsrate von 5-10 Bewegungen pro Sekunde im Verlauf eines gesamten Spiels. Wenn die Suchtiefe auf 6 Züge begrenzt ist, kann die KI problemlos mehr als 20 Züge pro Sekunde ausführen, was zu einigen interessanten Beobachtungen führt.

Um die Punktzahlleistung der KI zu beurteilen, habe ich die KI 100 Mal ausgeführt (über die Fernbedienung mit dem Browsergame verbunden). Für jedes Plättchen sind hier die Anteile der Spiele aufgeführt, in denen dieses Plättchen mindestens einmal erreicht wurde:

2048: 100%
4096: 100%
8192: 100%
16384: 94%
32768: 36%

Die Mindestpunktzahl über alle Läufe betrug 124024; Die maximale Punktzahl lag bei 794076. Der Medianwert liegt bei 387222. Die KI konnte das 2048-Plättchen nie verfehlen (daher hat sie das Spiel auch nur einmal in 100 Spielen verloren). Tatsächlich erreichte es das 8192 Plättchen mindestens einmal in jedem Durchgang!

Hier ist der Screenshot des besten Laufs:

32768 tile, score 794076

Dieses Spiel dauerte 27830 Züge über 96 Minuten oder durchschnittlich 4,8 Züge pro Sekunde.

Implementierung

Mein Ansatz codiert das gesamte Board (16 Einträge) als eine einzelne 64-Bit-Ganzzahl (wobei Kacheln die Nybbles sind, d. H. 4-Bit-Chunks). Auf einer 64-Bit-Maschine kann auf diese Weise die gesamte Karte in einem einzigen Maschinenregister weitergegeben werden.

Bitverschiebungsoperationen werden verwendet, um einzelne Zeilen und Spalten zu extrahieren. Eine einzelne Zeile oder Spalte ist eine 16-Bit-Größe, sodass eine Tabelle der Größe 65536 Transformationen codieren kann, die für eine einzelne Zeile oder Spalte ausgeführt werden. Beispielsweise werden Verschiebungen als 4 Lookups in eine vorberechnete "Verschiebungseffekttabelle" implementiert, die beschreibt, wie sich jede Verschiebung auf eine einzelne Zeile oder Spalte auswirkt (zum Beispiel enthält die Tabelle "Nach rechts verschieben" den Eintrag "1122 -> 0023", der beschreibt, wie die Zeile [2,2,4,4] wird zur Zeile [0,0,4,8], wenn sie nach rechts verschoben wird.

Das Scoring erfolgt auch über die Tabellensuche. Die Tabellen enthalten heuristische Bewertungen, die für alle möglichen Zeilen/Spalten berechnet wurden, und die resultierende Bewertung für eine Tafel ist einfach die Summe der Tabellenwerte für jede Zeile und Spalte.

Diese Board-Darstellung ermöglicht es der KI zusammen mit dem Table-Lookup-Ansatz für Bewegung und Wertung, eine große Anzahl von Spielzuständen in kurzer Zeit zu durchsuchen (über 10.000.000 Spielzustände pro Sekunde auf einem Kern meines Laptops von Mitte 2011).

Die Expectimax-Suche selbst ist als rekursive Suche codiert, die zwischen "Erwartungs" -Schritten (Testen aller möglichen Kachel-Spawn-Positionen und -Werte und Gewichten ihrer optimierten Punktzahlen mit der Wahrscheinlichkeit jeder Möglichkeit) und "Maximierungs" -Schritten (Testen aller möglichen Züge) wechselt und das mit der besten Punktzahl auswählen). Die Baumsuche wird beendet, wenn sie eine zuvor gesehene Position sieht (unter Verwendung einer Transpositionstabelle ), wenn sie eine vordefinierte Tiefenbegrenzung erreicht oder wenn sie einen Brettstatus erreicht, der höchst unwahrscheinlich ist (z. B. wenn er erreicht wurde) durch 6 "4" Steine ​​in einer Reihe von der Startposition bekommen). Die typische Suchtiefe beträgt 4-8 Züge.

Heuristik

Mit verschiedenen Heuristiken wird der Optimierungsalgorithmus auf günstige Positionen ausgerichtet. Die genaue Wahl der Heuristik hat einen großen Einfluss auf die Leistung des Algorithmus. Die verschiedenen Heuristiken werden gewichtet und zu einer Positionsbewertung kombiniert, die bestimmt, wie "gut" eine bestimmte Brettposition ist. Die Optimierungssuche zielt dann darauf ab, die durchschnittliche Punktzahl aller möglichen Brettpositionen zu maximieren. Die tatsächliche Punktzahl, wie sie im Spiel angezeigt wird, wird nicht zur Berechnung der Board-Punktzahl verwendet, da sie zugunsten des Zusammenführens von Kacheln zu stark gewichtet ist (wenn ein verzögertes Zusammenführen zu einer großen Punktzahl führen kann) Vorteil).

Anfangs verwendete ich zwei sehr einfache Heuristiken, die "Boni" für offene Felder und für große Werte am Rand gewährten. Diese Heuristiken zeigten eine recht gute Leistung und erreichten häufig 16384, erreichten jedoch nie 32768.

Petr Morávek (@xificurk) nahm meine KI und fügte zwei neue Heuristiken hinzu. Die erste Heuristik war eine Strafe für nicht-monotone Zeilen und Spalten, die mit zunehmendem Rang anstiegen, um sicherzustellen, dass nicht-monotone Zeilen mit kleinen Zahlen die Punktzahl nicht stark beeinflussen, nicht-monotone Zeilen mit großen Zahlen jedoch die Punktzahl erheblich beeinträchtigen. Die zweite Heuristik zählte die Anzahl möglicher Zusammenführungen (benachbarte gleiche Werte) zusätzlich zu offenen Räumen. Diese beiden Heuristiken dienten dazu, den Algorithmus auf monotone Karten (die einfacher zusammenzuführen sind) und auf Kartenpositionen mit vielen Zusammenführungen auszurichten (was ihn ermutigte, Zusammenführungen, wo immer möglich, auszurichten, um einen größeren Effekt zu erzielen).

Darüber hinaus optimierte Petr die heuristischen Gewichte mithilfe einer "Meta-Optimierungs" -Strategie (unter Verwendung eines Algorithmus namens CMA-ES ), wobei die Gewichte selbst angepasst wurden, um die höchstmögliche Durchschnittsbewertung zu erhalten.

Die Auswirkungen dieser Änderungen sind äußerst bedeutend. Der Algorithmus erreichte die 16384-Kachel in etwa 13% der Fälle und erreichte sie in etwa 90% der Fälle. In 1/3 der Fälle erreichte der Algorithmus 32768 (während die alten Heuristiken niemals eine 32768-Kachel hervorbrachten). .

Ich glaube, die Heuristik ist noch verbesserungswürdig. Dieser Algorithmus ist definitiv noch nicht "optimal", aber ich habe das Gefühl, dass er ziemlich nahe kommt.


Dass die KI in über einem Drittel ihrer Spiele das 32768-Plättchen erreicht, ist ein großer Meilenstein. Es wird mich überraschen zu hören, ob ein menschlicher Spieler im offiziellen Spiel 32768 erreicht hat (d. H. Ohne Tools wie Savestates oder Undo). Ich denke, die Fliese 65536 ist in Reichweite!

Sie können die KI selbst ausprobieren. Der Code ist verfügbar unter https://github.com/nneonneo/2048-ai .

1228
nneonneo

Ich bin der Autor des AI-Programms, das andere in diesem Thread erwähnt haben. Sie können die KI in Aktion anzeigen oder die Quelle lesen .

Derzeit erreicht das Programm auf meinem Laptop eine Gewinnrate von 90%, die in Javascript im Browser ausgeführt wird, wenn man etwa 100 Millisekunden Bedenkzeit pro Bewegung ansetzt. Obwohl es (noch) nicht perfekt ist, funktioniert es ziemlich gut.

Da das Spiel ein diskreter Zustandsraum, perfekte Informationen, rundenbasiertes Spiel wie Schach und Dame ist, habe ich die gleichen Methoden verwendet, die für diese Spiele bewiesen wurden, nämlich minimaxSuche mit Alpha-Beta-Bereinigung . Da es bereits viele Informationen zu diesem Algorithmus gibt, werde ich nur auf die beiden Hauptheuristiken eingehen, die ich in der statischen Auswertungsfunktion verwende und die viele der Algorithmen formalisieren Intuitionen, die andere Leute hier ausgedrückt haben.

Monotonie

Diese Heuristik versucht sicherzustellen, dass die Werte der Kacheln sowohl nach links/rechts als auch nach oben/unten entweder alle zunehmen oder abnehmen. Diese Heuristik fängt die Intuition ein, die viele andere erwähnt haben, dass höherwertige Kacheln in einer Ecke gruppiert werden sollten. Dies verhindert in der Regel, dass Kacheln mit geringerem Wert verwaist werden, und sorgt dafür, dass das Spielbrett gut organisiert bleibt. Kleinere Kacheln fließen in die größeren Kacheln und füllen diese auf.

Hier ist ein Screenshot eines perfekt monotonen Gitters. Ich erhielt dies, indem ich den Algorithmus mit der eingestellten Eval-Funktion ausführte, um die anderen Heuristiken zu ignorieren und nur die Monotonie zu berücksichtigen.

A perfectly monotonic 2048 board

Glätte

Die obige Heuristik alleine neigt dazu, Strukturen zu erzeugen, bei denen benachbarte Kacheln an Wert verlieren, aber um zusammenzufassen, müssen benachbarte Kacheln natürlich den gleichen Wert haben. Daher misst die Glättungsheuristik nur den Wertunterschied zwischen benachbarten Kacheln und versucht, diese Anzahl zu minimieren.

Ein Kommentar zu Hacker News gab eine interessante Formalisierung dieser Idee in Bezug auf die Graphentheorie.

Hier ist ein Screenshot eines perfekt glatten Gitters mit freundlicher Genehmigung von dieser exzellenten Parodiegabel .

A perfectly smooth 2048 board

Freie Fliesen

Und schließlich gibt es eine Strafe für zu wenig freie Steine, da die Optionen schnell ausgehen können, wenn der Spielplan zu eng wird.

Und das ist es! Das Durchsuchen des Spielraums unter Optimierung dieser Kriterien führt zu einer bemerkenswert guten Leistung. Ein Vorteil der Verwendung eines solchen verallgemeinerten Ansatzes anstelle einer explizit codierten Bewegungsstrategie besteht darin, dass der Algorithmus häufig interessante und unerwartete Lösungen finden kann. Wenn Sie zuschauen, wie es läuft, werden oft überraschende, aber effektive Bewegungen ausgeführt, z.

Bearbeiten:

Hier ist eine Demonstration der Kraft dieses Ansatzes. Ich habe die Kachelwerte aufgehoben (so ging es nach Erreichen von 2048 weiter) und hier ist das beste Ergebnis nach acht Versuchen.

4096

Ja, das ist eine 4096 neben einer 2048. =) Das bedeutet, dass das schwer fassbare 2048-Plättchen dreimal auf demselben Brett erreicht wurde.

1244
ovolve

Ich interessierte mich für die Idee einer KI für dieses Spiel, diekeine fest codierte Intelligenzenthält (d. H. Keine Heuristiken, Bewertungsfunktionen usw.). Die KI sollte nur die Spielregeln "kennen" und das Spiel "herausfinden" . Dies steht im Gegensatz zu den meisten AIs (wie die in diesem Thread), bei denen das Spiel im Wesentlichen brachiale Kraft ist, die durch eine Bewertungsfunktion gesteuert wird, die das menschliche Verständnis des Spiels repräsentiert.

AI-Algorithmus

Ich fand einen einfachen, aber überraschend guten Spielalgorithmus: Um den nächsten Zug für ein bestimmtes Brett zu bestimmen, spielt die KI das Spiel im Speicher mitzufälligen Zügen, bis das Spiel vorbei ist. Dies wird mehrmals durchgeführt, während der Endstand des Spiels verfolgt wird. Dann wird die durchschnittliche Endpunktzahl pro Startzug berechnet. Der Startzug mit der höchsten durchschnittlichen Endpunktzahl wird als nächster Zug ausgewählt.

Mit nur 100 Läufen (d. H. In Gedächtnisspielen) pro Zug erreicht die KI in 80% der Fälle das 2048-Plättchen und in 50% der Fälle das 4096-Plättchen. Bei Verwendung von 10000 Durchläufen werden die 2048-Kacheln zu 100%, die 4096-Kacheln zu 70% und die 8192-Kacheln zu 1% berechnet.

In Aktion sehen

Die beste Punktzahl wird hier angezeigt:

best score

Eine interessante Tatsache bei diesem Algorithmus ist, dass, obwohl die Zufallsspiele nicht überraschend schlecht sind, die Wahl des besten (oder des am wenigsten schlechten) Zuges zu einem sehr guten Spiel führt: Ein typisches KI-Spiel kann 70000 Punkte und die letzten 3000 Züge erreichen, aber die In-Memory-Zufallsspiele von einer bestimmten Position bringen durchschnittlich 340 zusätzliche Punkte in etwa 40 zusätzlichen Zügen, bevor sie sterben. (Sie können sich davon überzeugen, indem Sie die KI ausführen und die Debug-Konsole öffnen.)

Diese Grafik zeigt diesen Punkt: Die blaue Linie zeigt die Punktzahl nach jedem Zug. Die rote Linie zeigt diebestezufällige Punktzahl des Endspiels von dieser Position aus. Im Wesentlichen ziehen die roten Werte die blauen Werte nach oben, da dies die beste Schätzung des Algorithmus ist. Es ist interessant zu sehen, dass die rote Linie an jedem Punkt nur ein kleines Stück über der blauen Linie liegt, während die blaue Linie immer weiter zunimmt.

scoring graph

Ich finde es ziemlich überraschend, dass der Algorithmus kein gutes Spiel voraussagen muss, um die Moves auszuwählen, die ihn erzeugen.

Bei einer späteren Suche stellte ich fest, dass dieser Algorithmus möglicherweise als Pure Monte Carlo Tree Search -Algorithmus klassifiziert ist.

Implementierung und Links

Zuerst habe ich eine JavaScript-Version erstellt, die hier in Aktion gesehen . Diese Version kann Hunderte von Läufen in angemessener Zeit ausführen. Öffnen Sie die Konsole für zusätzliche Informationen. ( source )

Später nutzte ich die hochoptimierte Infrastruktur von @nneonneo und implementierte meine Version in C++, um ein bisschen mehr herumzuspielen. Diese Version erlaubt bis zu 100000 Läufe pro Zug und sogar 1000000, wenn Sie die Geduld haben. Bauanleitung zur Verfügung gestellt. Es läuft in der Konsole und hat auch eine Fernbedienung, um die Webversion abzuspielen. ( source )

Ergebnisse

Überraschenderweise verbessert das Erhöhen der Anzahl der Läufe das Spiel nicht drastisch. Diese Strategie scheint bei rund 80000 Punkten mit dem 4096-Plättchen und allen kleineren Punkten eine Grenze zu haben, die dem Erreichen des 8192-Plättchens sehr nahe kommt. Wenn Sie die Anzahl der Läufe von 100 auf 100000 erhöhen, erhöht sich die Wahrscheinlichkeit, dassoddsan dieses Punktelimit (von 5% auf 40%) gelangt, dieses jedoch nicht durchbricht.

Das Ausführen von 10000 Läufen mit einem vorübergehenden Anstieg auf 1000000 in der Nähe von kritischen Positionen hat es geschafft, diese Barriere in weniger als 1% der Fälle zu durchbrechen und eine maximale Punktzahl von 129892 und 8192 zu erreichen.

Verbesserungen

Nach der Implementierung dieses Algorithmus habe ich viele Verbesserungen versucht, einschließlich der Verwendung der Min- oder Max-Scores oder einer Kombination aus Min-, Max- und Avg. Ich habe auch versucht, die Tiefe zu verwenden: Anstatt K Läufe pro Zug zu versuchen, habe ich K Züge pro Zug mit einer Liste einer bestimmten Länge (z. B. "hoch, hoch, links") versucht und ausgewählt der erste Zug der Bestenliste.

Später implementierte ich einen Bewertungsbaum, der die bedingte Wahrscheinlichkeit berücksichtigte, einen Zug nach einer bestimmten Zugliste spielen zu können.

Keine dieser Ideen zeigte jedoch einen wirklichen Vorteil gegenüber der einfachen ersten Idee. Ich habe den Code für diese Ideen im C++ - Code auskommentiert.

Ich habe einen "Deep Search" -Mechanismus hinzugefügt, der die Laufnummer vorübergehend auf 1000000 erhöhte, wenn einer der Läufe versehentlich das nächsthöhere Feld erreichte. Dies bot eine zeitliche Verbesserung.

Es würde mich interessieren zu hören, ob jemand andere Verbesserungsideen hat, die die Domänenunabhängigkeit der KI aufrechterhalten.

2048 Varianten und Klone

Nur zum Spaß habe ich auch die KI als Lesezeichen implementiert ​​und mich in die Steuerung des Spiels eingebunden. Dadurch kann die KI mit dem ursprünglichen Spiel undvielen seiner Variantenarbeiten.

Dies ist aufgrund der Domänenunabhängigkeit der KI möglich. Einige der Varianten sind sehr unterschiedlich, wie z. B. der hexagonale Klon.

132
Ronenz

EDIT: Dies ist ein naiver Algorithmus, der den menschlichen bewussten Denkprozess modelliert und im Vergleich zu KI, die alle Möglichkeiten durchsucht, sehr schwache Ergebnisse erzielt, da nur eine Kachel nach vorne schaut. Es wurde früh in der Antwortzeitleiste eingereicht.

Ich habe den Algorithmus verfeinert und das Spiel geschlagen! Möglicherweise scheitert es an einfachem Pech gegen Ende (Sie sind gezwungen, nach unten zu gehen, was Sie niemals tun sollten, und ein Kärtchen erscheint an der Stelle, an der Ihre höchste sein sollte. Versuchen Sie einfach, die obere Reihe gefüllt zu halten, damit Sie sich nicht nach links bewegen brechen das Muster), aber im Grunde haben Sie am Ende eine feste Rolle und eine mobile Rolle zum Spielen. Dies ist Ihr Ziel:

Ready to finish

Dies ist das Modell, das ich standardmäßig gewählt habe.

1024 512 256 128
  8   16  32  64
  4   2   x   x
  x   x   x   x

Die gewählte Ecke ist willkürlich, Sie drücken im Grunde nie eine Taste (die verbotene Bewegung), und wenn Sie dies tun, drücken Sie erneut das Gegenteil und versuchen, es zu beheben. Für zukünftige Kacheln erwartet das Modell immer, dass die nächste zufällige Kachel eine 2 ist und auf der gegenüberliegenden Seite des aktuellen Modells angezeigt wird (während die erste Reihe unvollständig ist, in der unteren rechten Ecke, sobald die erste Reihe abgeschlossen ist, unten links Ecke).

Hier geht der Algorithmus. Etwa 80% gewinnen (es scheint immer möglich zu sein, mit "professionelleren" KI-Techniken zu gewinnen, da bin ich mir allerdings nicht sicher.)

initiateModel();

while(!game_over)
{    
    checkCornerChosen(); // Unimplemented, but it might be an improvement to change the reference point

    for each 3 possible move:
        evaluateResult()
    execute move with best score
    if no move is available, execute forbidden move and undo, recalculateModel()
 }

 evaluateResult() {
     calculatesBestCurrentModel()
     calculates distance to chosen model
     stores result
 }

 calculateBestCurrentModel() {
      (according to the current highest tile acheived and their distribution)
  }

Ein paar Hinweise auf die fehlenden Stufen. Hier: model change

Das Modell hat sich aufgrund des Glücks geändert, näher am erwarteten Modell zu sein. Das Modell, das die KI zu erreichen versucht, ist

 512 256 128  x
  X   X   x   x
  X   X   x   x
  x   x   x   x

Und die Kette, um dahin zu gelangen, ist geworden:

 512 256  64  O
  8   16  32  O
  4   x   x   x
  x   x   x   x

Die O repräsentieren verbotene Räume ...

Also drückt es rechts, dann wieder rechts, dann (rechts oder oben, je nachdem, wo die 4 erstellt wurde) und fährt dann fort, die Kette zu vervollständigen, bis es Folgendes bekommt:

Chain completed

Das Modell und die Kette sind zurück zu:

 512 256 128  64
  4   8  16   32
  X   X   x   x
  x   x   x   x

Zweiter Zeiger, es hat Pech gehabt und sein Hauptpunkt wurde genommen. Es ist wahrscheinlich, dass es fehlschlägt, aber es kann es dennoch erreichen:

Enter image description here

Hier ist das Modell und die Kette:

  O 1024 512 256
  O   O   O  128
  8  16   32  64
  4   x   x   x

Wenn es schafft, die 128 zu erreichen, gewinnt es eine ganze Reihe wieder:

  O 1024 512 256
  x   x  128 128
  x   x   x   x
  x   x   x   x
123
Daren

Ich kopiere hier den Inhalt eines Post in meinem Blog


Die von mir vorgeschlagene Lösung ist sehr einfach und leicht zu implementieren. Es hat zwar die Punktzahl von 131040 erreicht. Mehrere Benchmarks der Algorithmusleistung werden vorgestellt.

Score

Algorithmus

Heuristischer Bewertungsalgorithmus

Die Annahme, auf der mein Algorithmus basiert, ist recht einfach: Wenn Sie eine höhere Punktzahl erzielen möchten, muss das Board so ordentlich wie möglich gehalten werden. Der optimale Aufbau ergibt sich insbesondere aus einer linearen und monoton abnehmenden Reihenfolge der Kachelwerte. Diese Intuition gibt Ihnen auch die Obergrenze für einen Kachelwert: s Dabei ist n die Anzahl der Kacheln auf der Tafel.

(Es besteht die Möglichkeit, das 131072-Plättchen zu erreichen, wenn das 4-Plättchen bei Bedarf statt des 2-Plättchens zufällig generiert wird.)

In den folgenden Abbildungen sind zwei Möglichkeiten zum Organisieren des Boards dargestellt:

enter image description here

Um die Ordination der Kacheln in einer monoton abnehmenden Reihenfolge zu erzwingen, wird die Punktzahl als die Summe der linearisierten Werte auf der Tafel multipliziert mit den Werten einer geometrischen Sequenz mit dem gemeinsamen Verhältnis r <1 berechnet.

s

s

Es können mehrere lineare Pfade gleichzeitig ausgewertet werden. Die endgültige Punktzahl ist die maximale Punktzahl eines Pfades.

Entscheidungsregel

Die implementierte Entscheidungsregel ist nicht ganz schlau, der Code in Python ist hier dargestellt:

@staticmethod
def nextMove(board,recursion_depth=3):
    m,s = AI.nextMoveRecur(board,recursion_depth,recursion_depth)
    return m

@staticmethod
def nextMoveRecur(board,depth,maxDepth,base=0.9):
    bestScore = -1.
    bestMove = 0
    for m in range(1,5):
        if(board.validMove(m)):
            newBoard = copy.deepcopy(board)
            newBoard.move(m,add_tile=True)

            score = AI.evaluate(newBoard)
            if depth != 0:
                my_m,my_s = AI.nextMoveRecur(newBoard,depth-1,maxDepth)
                score += my_s*pow(base,maxDepth-depth+1)

            if(score > bestScore):
                bestMove = m
                bestScore = score
    return (bestMove,bestScore);

Eine Implementierung des minmax oder des Expectiminimax wird den Algorithmus sicherlich verbessern. Offensichtlich wird eine ausgefeiltere Entscheidungsregel den Algorithmus verlangsamen und die Implementierung wird einige Zeit in Anspruch nehmen. Ich werde in naher Zukunft eine Minimax-Implementierung versuchen. (Bleib dran)

Benchmark

  • T1 - 121 Tests - 8 verschiedene Pfade - r = 0,125
  • T2 - 122 Tests - 8 verschiedene Pfade - r = 0,25
  • T3 - 132 Tests - 8 verschiedene Pfade - r = 0,5
  • T4 - 211 Tests - 2 verschiedene Pfade - r = 0,125
  • T5 - 274 Tests - 2 verschiedene Pfade - r = 0,25
  • T6 - 211 Tests - 2 verschiedene Pfade - r = 0,5

enter image description hereenter image description hereenter image description hereenter image description here

Im Fall von T2 erzeugen vier von zehn Tests die Kachel 4096 mit einer durchschnittlichen Punktzahl von s 42000

Code

Der Code ist auf GiHub unter folgendem Link zu finden: https://github.com/Nicola17/term2048-AI Er basiert auf term2048 und ist in Python geschrieben. Ich werde so schnell wie möglich eine effizientere Version in C++ implementieren.

94
Nicola Pezzotti

Mein Versuch benutzt expectimax wie andere Lösungen oben, aber ohne Bitboards. Mit der Lösung von Nneonneo können 10 Millionen Züge überprüft werden. Dies entspricht einer Tiefe von 4, wobei noch 6 Kacheln übrig sind und 4 Züge möglich sind (2 * 6 * 4).4. In meinem Fall dauert das Erkunden dieser Tiefe zu lange. Ich passe die Tiefe der Expectimax-Suche entsprechend der Anzahl der verbleibenden freien Kacheln an:

depth = free > 7 ? 1 : (free > 4 ? 2 : 3)

Die Punktzahlen der Bretter werden mit der gewichteten Summe des Quadrats der Anzahl der freien Kacheln und dem Skalarprodukt des 2D-Gitters wie folgt berechnet:

[[10,8,7,6.5],
 [.5,.7,1,3],
 [-.5,-1.5,-1.8,-2],
 [-3.8,-3.7,-3.5,-3]]

das zwingt, Kacheln absteigend in einer Art Schlange von der oberen linken Kachel zu organisieren.

code unten oder am Github :

var n = 4,
        M = new MatrixTransform(n);

var ai = {weights: [1, 1], depth: 1}; // depth=1 by default, but we adjust it on every prediction according to the number of free tiles

var snake= [[10,8,7,6.5],
            [.5,.7,1,3],
            [-.5,-1.5,-1.8,-2],
            [-3.8,-3.7,-3.5,-3]]
snake=snake.map(function(a){return a.map(Math.exp)})

initialize(ai)

function run(ai) {
        var p;
        while ((p = predict(ai)) != null) {
                move(p, ai);
        }
        //console.log(ai.grid , maxValue(ai.grid))
        ai.maxValue = maxValue(ai.grid)
        console.log(ai)
}

function initialize(ai) {
        ai.grid = [];
        for (var i = 0; i < n; i++) {
                ai.grid[i] = []
                for (var j = 0; j < n; j++) {
                        ai.grid[i][j] = 0;
                }
        }
        Rand(ai.grid)
        Rand(ai.grid)
        ai.steps = 0;
}

function move(p, ai) { //0:up, 1:right, 2:down, 3:left
        var newgrid = mv(p, ai.grid);
        if (!equal(newgrid, ai.grid)) {
                //console.log(stats(newgrid, ai.grid))
                ai.grid = newgrid;
                try {
                        Rand(ai.grid)
                        ai.steps++;
                } catch (e) {
                        console.log('no room', e)
                }
        }
}

function predict(ai) {
        var free = freeCells(ai.grid);
        ai.depth = free > 7 ? 1 : (free > 4 ? 2 : 3);
        var root = {path: [],prob: 1,grid: ai.grid,children: []};
        var x = expandMove(root, ai)
        //console.log("number of leaves", x)
        //console.log("number of leaves2", countLeaves(root))
        if (!root.children.length) return null
        var values = root.children.map(expectimax);
        var mx = max(values);
        return root.children[mx[1]].path[0]

}

function countLeaves(node) {
        var x = 0;
        if (!node.children.length) return 1;
        for (var n of node.children)
                x += countLeaves(n);
        return x;
}

function expectimax(node) {
        if (!node.children.length) {
                return node.score
        } else {
                var values = node.children.map(expectimax);
                if (node.prob) { //we are at a max node
                        return Math.max.apply(null, values)
                } else { // we are at a random node
                        var avg = 0;
                        for (var i = 0; i < values.length; i++)
                                avg += node.children[i].prob * values[i]
                        return avg / (values.length / 2)
                }
        }
}

function expandRandom(node, ai) {
        var x = 0;
        for (var i = 0; i < node.grid.length; i++)
                for (var j = 0; j < node.grid.length; j++)
                        if (!node.grid[i][j]) {
                                var grid2 = M.copy(node.grid),
                                        grid4 = M.copy(node.grid);
                                grid2[i][j] = 2;
                                grid4[i][j] = 4;
                                var child2 = {grid: grid2,prob: .9,path: node.path,children: []};
                                var child4 = {grid: grid4,prob: .1,path: node.path,children: []}
                                node.children.Push(child2)
                                node.children.Push(child4)
                                x += expandMove(child2, ai)
                                x += expandMove(child4, ai)
                        }
        return x;
}

function expandMove(node, ai) { // node={grid,path,score}
        var isLeaf = true,
                x = 0;
        if (node.path.length < ai.depth) {
                for (var move of[0, 1, 2, 3]) {
                        var grid = mv(move, node.grid);
                        if (!equal(grid, node.grid)) {
                                isLeaf = false;
                                var child = {grid: grid,path: node.path.concat([move]),children: []}
                                node.children.Push(child)
                                x += expandRandom(child, ai)
                        }
                }
        }
        if (isLeaf) node.score = dot(ai.weights, stats(node.grid))
        return isLeaf ? 1 : x;
}



var cells = []
var table = document.querySelector("table");
for (var i = 0; i < n; i++) {
        var tr = document.createElement("tr");
        cells[i] = [];
        for (var j = 0; j < n; j++) {
                cells[i][j] = document.createElement("td");
                tr.appendChild(cells[i][j])
        }
        table.appendChild(tr);
}

function updateUI(ai) {
        cells.forEach(function(a, i) {
                a.forEach(function(el, j) {
                        el.innerHTML = ai.grid[i][j] || ''
                })
        });
}


updateUI(ai);
updateHint(predict(ai));

function runAI() {
        var p = predict(ai);
        if (p != null && ai.running) {
                move(p, ai);
                updateUI(ai);
                updateHint(p);
                requestAnimationFrame(runAI);
        }
}
runai.onclick = function() {
        if (!ai.running) {
                this.innerHTML = 'stop AI';
                ai.running = true;
                runAI();
        } else {
                this.innerHTML = 'run AI';
                ai.running = false;
                updateHint(predict(ai));
        }
}


function updateHint(dir) {
        hintvalue.innerHTML = ['↑', '→', '↓', '←'][dir] || '';
}

document.addEventListener("keydown", function(event) {
        if (!event.target.matches('.r *')) return;
        event.preventDefault(); // avoid scrolling
        if (event.which in map) {
                move(map[event.which], ai)
                console.log(stats(ai.grid))
                updateUI(ai);
                updateHint(predict(ai));
        }
})
var map = {
        38: 0, // Up
        39: 1, // Right
        40: 2, // Down
        37: 3, // Left
};
init.onclick = function() {
        initialize(ai);
        updateUI(ai);
        updateHint(predict(ai));
}


function stats(grid, previousGrid) {

        var free = freeCells(grid);

        var c = dot2(grid, snake);

        return [c, free * free];
}

function dist2(a, b) { //squared 2D distance
        return Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)
}

function dot(a, b) {
        var r = 0;
        for (var i = 0; i < a.length; i++)
                r += a[i] * b[i];
        return r
}

function dot2(a, b) {
        var r = 0;
        for (var i = 0; i < a.length; i++)
                for (var j = 0; j < a[0].length; j++)
                        r += a[i][j] * b[i][j]
        return r;
}

function product(a) {
        return a.reduce(function(v, x) {
                return v * x
        }, 1)
}

function maxValue(grid) {
        return Math.max.apply(null, grid.map(function(a) {
                return Math.max.apply(null, a)
        }));
}

function freeCells(grid) {
        return grid.reduce(function(v, a) {
                return v + a.reduce(function(t, x) {
                        return t + (x == 0)
                }, 0)
        }, 0)
}

function max(arr) { // return [value, index] of the max
        var m = [-Infinity, null];
        for (var i = 0; i < arr.length; i++) {
                if (arr[i] > m[0]) m = [arr[i], i];
        }
        return m
}

function min(arr) { // return [value, index] of the min
        var m = [Infinity, null];
        for (var i = 0; i < arr.length; i++) {
                if (arr[i] < m[0]) m = [arr[i], i];
        }
        return m
}

function maxScore(nodes) {
        var min = {
                score: -Infinity,
                path: []
        };
        for (var node of nodes) {
                if (node.score > min.score) min = node;
        }
        return min;
}


function mv(k, grid) {
        var tgrid = M.itransform(k, grid);
        for (var i = 0; i < tgrid.length; i++) {
                var a = tgrid[i];
                for (var j = 0, jj = 0; j < a.length; j++)
                        if (a[j]) a[jj++] = (j < a.length - 1 && a[j] == a[j + 1]) ? 2 * a[j++] : a[j]
                for (; jj < a.length; jj++)
                        a[jj] = 0;
        }
        return M.transform(k, tgrid);
}

function Rand(grid) {
        var r = Math.floor(Math.random() * freeCells(grid)),
                _r = 0;
        for (var i = 0; i < grid.length; i++) {
                for (var j = 0; j < grid.length; j++) {
                        if (!grid[i][j]) {
                                if (_r == r) {
                                        grid[i][j] = Math.random() < .9 ? 2 : 4
                                }
                                _r++;
                        }
                }
        }
}

function equal(grid1, grid2) {
        for (var i = 0; i < grid1.length; i++)
                for (var j = 0; j < grid1.length; j++)
                        if (grid1[i][j] != grid2[i][j]) return false;
        return true;
}

function conv44valid(a, b) {
        var r = 0;
        for (var i = 0; i < 4; i++)
                for (var j = 0; j < 4; j++)
                        r += a[i][j] * b[3 - i][3 - j]
        return r
}

function MatrixTransform(n) {
        var g = [],
                ig = [];
        for (var i = 0; i < n; i++) {
                g[i] = [];
                ig[i] = [];
                for (var j = 0; j < n; j++) {
                        g[i][j] = [[j, i],[i, n-1-j],[j, n-1-i],[i, j]]; // transformation matrix in the 4 directions g[i][j] = [up, right, down, left]
                        ig[i][j] = [[j, i],[i, n-1-j],[n-1-j, i],[i, j]]; // the inverse tranformations
                }
        }
        this.transform = function(k, grid) {
                return this.transformer(k, grid, g)
        }
        this.itransform = function(k, grid) { // inverse transform
                return this.transformer(k, grid, ig)
        }
        this.transformer = function(k, grid, mat) {
                var newgrid = [];
                for (var i = 0; i < grid.length; i++) {
                        newgrid[i] = [];
                        for (var j = 0; j < grid.length; j++)
                                newgrid[i][j] = grid[mat[i][j][k][0]][mat[i][j][k][1]];
                }
                return newgrid;
        }
        this.copy = function(grid) {
                return this.transform(3, grid)
        }
}
body {
        font-family: Arial;
}
table, th, td {
        border: 1px solid black;
        margin: 0 auto;
        border-collapse: collapse;
}
td {
        width: 35px;
        height: 35px;
        text-align: center;
}
button {
        margin: 2px;
        padding: 3px 15px;
        color: rgba(0,0,0,.9);
}
.r {
        display: flex;
        align-items: center;
        justify-content: center;
        margin: .2em;
        position: relative;
}
#hintvalue {
        font-size: 1.4em;
        padding: 2px 8px;
        display: inline-flex;
        justify-content: center;
        width: 30px;
}
<table title="press arrow keys"></table>
<div class="r">
    <button id=init>init</button>
    <button id=runai>run AI</button>
    <span id="hintvalue" title="Best predicted move to do, use your arrow keys" tabindex="-1"></span>
</div>
37
caub

Ich bin der Autor eines 2048-Controllers, der besser abschneidet als jedes andere in diesem Thread erwähnte Programm. Eine effiziente Implementierung des Controllers ist ab github verfügbar. In ein separates Repo befindet sich auch der Code, mit dem die Zustandsbewertungsfunktion des Controllers trainiert wird. Die Trainingsmethode ist im Artikel beschrieben.

Die Steuerung verwendet die Expectimax-Suche mit einer von Grund auf neu erlernten Zustandsbewertungsfunktion (ohne menschliches Fachwissen 2048) durch eine Variante des Lernens mit zeitlichen Unterschieden (eine Lerntechnik zur Verstärkung). . Die Zustandswertfunktion verwendet ein n-Tupel-Netzwerk , das im Grunde eine gewichtete lineare Funktion der auf der Karte beobachteten Muster ist. Es handelte sich insgesamt um mehr als 1 Milliarde Gewichte .

Performance

Bei 1 Zügen/s: 609104 (Durchschnitt von 100 Spielen)

Bei 10 Zügen/s: 589355 (Durchschnitt von 300 Spielen)

Bei 3 Lagen (ca. 1500 Züge/s): 511759 (Durchschnitt von 1000 Spielen)

Die Kachelstatistik für 10 Züge/s lautet wie folgt:

2048: 100%
4096: 100%
8192: 100%
16384: 97%
32768: 64%
32768,16384,8192,4096: 10%

(Die letzte Zeile bedeutet, dass die angegebenen Kacheln gleichzeitig auf dem Brett liegen).

Für 3-lagig:

2048: 100%
4096: 100%
8192: 100%
16384: 96%
32768: 54%
32768,16384,8192,4096: 8%

Ich habe jedoch nie beobachtet, dass es die Fliese 65536 erhielt.

32
cauchy

Ich denke, ich habe einen Algorithmus gefunden, der recht gut funktioniert, da ich oft Werte über 10000 erreiche, wobei mein persönlicher Bestwert bei 16000 liegt. Meine Lösung zielt nicht darauf ab, die größten Zahlen in einer Ecke zu halten, sondern in der obersten Reihe.

Bitte beachten Sie den folgenden Code:

while( !game_over ) {
    move_direction=up;
    if( !move_is_possible(up) ) {
        if( move_is_possible(right) && move_is_possible(left) ){
            if( number_of_empty_cells_after_moves(left,up) > number_of_empty_cells_after_moves(right,up) ) 
                move_direction = left;
            else
                move_direction = right;
        } else if ( move_is_possible(left) ){
            move_direction = left;
        } else if ( move_is_possible(right) ){
            move_direction = right;
        } else {
            move_direction = down;
        }
    }
    do_move(move_direction);
}
27

Es gibt bereits eine AI-Implementierung für dieses Spiel hier . Auszug aus README:

Der Algorithmus ist die iterative Vertiefungstiefe der ersten Alpha-Beta-Suche. Die Auswertungsfunktion versucht, die Zeilen und Spalten monoton zu halten (entweder alle absteigend oder steigend), während die Anzahl der Kacheln auf dem Raster minimiert wird.

Es gibt auch eine Diskussion zu Hacker News über diesen Algorithmus, die Sie vielleicht nützlich finden.

25
baltazar

Algorithmus

while(!game_over)
{
    for each possible move:
        evaluate next state

    choose the maximum evaluation
}

Bewertung

Evaluation =
    128 (Constant)
    + (Number of Spaces x 128)
    + Sum of faces adjacent to a space { (1/face) x 4096 }
    + Sum of other faces { log(face) x 4 }
    + (Number of possible next moves x 256)
    + (Number of aligned values x 2)

Bewertungsdetails

128 (Constant)

Dies ist eine Konstante, die als Basislinie und für andere Zwecke wie das Testen verwendet wird.

+ (Number of Spaces x 128)

Mehr Räume machen den Zustand flexibler, wir multiplizieren mit 128 (was der Median ist), da ein mit 128 Flächen gefülltes Gitter ein optimaler unmöglicher Zustand ist.

+ Sum of faces adjacent to a space { (1/face) x 4096 }

Hier bewerten wir Gesichter, bei denen die Möglichkeit besteht, dass sie zusammengeführt werden, indem sie rückwärts bewertet werden. Kachel 2 erhält den Wert 2048, während Kachel 2048 mit 2 bewertet wird.

+ Sum of other faces { log(face) x 4 }

Hier müssen wir immer noch nach gestapelten Werten suchen, aber in einer geringeren Weise, die die Flexibilitätsparameter nicht unterbricht, haben wir die Summe von {x in [4,44]}.

+ (Number of possible next moves x 256)

Ein Staat ist flexibler, wenn er mehr Freiheit für mögliche Übergänge hat.

+ (Number of aligned values x 2)

Dies ist eine vereinfachte Überprüfung der Möglichkeit, Zusammenschlüsse innerhalb dieses Zustands vorzunehmen, ohne einen Blick nach vorn zu werfen.

Hinweis: Die Konstanten können angepasst werden.

23
Khaled.K

Dies ist keine direkte Antwort auf die Frage von OP, dies ist mehr von den Dingen (Experimenten), die ich bisher versucht habe, um das gleiche Problem zu lösen, und einige Ergebnisse erzielt habe und einige Beobachtungen habe, die ich mitteilen möchte. Ich bin gespannt, ob wir welche haben können weitere einblicke daraus.

Ich habe gerade meine Minimax-Implementierung mit Alpha-Beta-Bereinigung mit Suchbaum-Tiefenbeschränkung bei 3 und 5 versucht. Ich habe versucht, dasselbe Problem für ein 4x4-Raster als Projektzuweisung für das edX zu lösen kurs ColumbiaX: CSMM.101x Künstliche Intelligenz (KI) .

Ich habe eine konvexe Kombination (verschiedene heuristische Gewichte ausprobiert) einiger heuristischer Bewertungsfunktionen angewendet, hauptsächlich aus der Intuition und aus den oben diskutierten:

  1. Monotonie
  2. Freier Speicherplatz verfügbar

In meinem Fall ist der Computerspieler völlig zufällig, aber ich habe trotzdem gegnerische Einstellungen angenommen und den KI-Spieleragenten als maximalen Spieler implementiert.

Ich habe 4x4 Gitter zum Spielen des Spiels.

Überwachung:

Wenn ich der ersten oder der zweiten heuristischen Funktion zu viel Gewicht zugebe, sind beide Fälle, in denen der KI-Spieler Punkte erhält, niedrig. Ich habe mit vielen möglichen Gewichtszuweisungen zu den heuristischen Funktionen gespielt und eine konvexe Kombination genommen, aber sehr selten kann der KI-Spieler 2048 Punkte erzielen. Meistens stoppt er entweder bei 1024 oder 512.

Ich habe auch die Eckheuristik ausprobiert, aber aus irgendeinem Grund verschlechtert sie die Ergebnisse. Warum?

Außerdem habe ich versucht, den Grenzwert für die Suchtiefe von 3 auf 5 zu erhöhen (ich kann ihn nicht mehr erhöhen, da die Suche die zulässige Zeit auch beim Beschneiden überschreitet) und eine weitere Heuristik hinzugefügt, die die Werte benachbarter Kacheln betrachtet und angibt mehr Punkte, wenn sie zusammenführbar sind, aber ich bin immer noch nicht in der Lage, 2048 zu bekommen.

Ich denke, es ist besser, Expectimax anstelle von Minimax zu verwenden, aber ich möchte dieses Problem trotzdem nur mit Minimax lösen und hohe Punktzahlen wie 2048 oder 4096 erzielen. Ich bin nicht sicher, ob mir etwas fehlt.

Die folgende Animation zeigt die letzten Schritte des Spiels, das der KI-Agent mit dem Computerspieler spielt:

enter image description here

Alle Einblicke werden wirklich sehr hilfreich sein, danke im Voraus. (Dies ist der Link meines Blogposts zum Artikel: https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation -to-solve-2048-game-with-computer / und das Youtube-Video: https://www.youtube.com/watch?v=VnVFilfZ0r4 )

Die folgende Animation zeigt die letzten Schritte des Spiels, in denen der KI-Spieler-Agent 2048 Punkte erzielen konnte. Diesmal wird auch die Heuristik für den absoluten Wert hinzugefügt:

enter image description here

Die folgenden Abbildungen zeigen den Spielbaum , den der Spieler-KI-Agent untersucht, wenn er den Computer nur für einen einzigen Schritt als Gegner annimmt:

enter image description hereenter image description hereenter image description hereenter image description hereenter image description hereenter image description here

11
Sandipan Dey

Ich habe einen 2048-Löser in Haskell geschrieben, hauptsächlich, weil ich diese Sprache gerade lerne.

Meine Implementierung des Spiels unterscheidet sich geringfügig vom tatsächlichen Spiel, da ein neues Plättchen immer eine '2' ist (anstatt 90% 2 und 10% 4). Und dass das neue Plättchen nicht zufällig ist, sondern immer das erste von oben links. Diese Variante wird auch als Det 2048 bezeichnet.

Infolgedessen ist dieser Löser deterministisch.

Ich habe einen umfassenden Algorithmus verwendet, der leere Kacheln bevorzugt. Bei Tiefe 1 bis 4 geht es ziemlich schnell, aber bei Tiefe 5 wird es mit etwa 1 Sekunde pro Zug ziemlich langsam.

Unten finden Sie den Code, der den Lösungsalgorithmus implementiert. Das Gitter wird als 16-Längen-Array von Ganzzahlen dargestellt. Und die Wertung erfolgt einfach durch Zählen der Anzahl der leeren Felder.

bestMove :: Int -> [Int] -> Int
bestMove depth grid = maxTuple [ (gridValue depth (takeTurn x grid), x) | x <- [0..3], takeTurn x grid /= [] ]

gridValue :: Int -> [Int] -> Int
gridValue _ [] = -1
gridValue 0 grid = length $ filter (==0) grid  -- <= SCORING
gridValue depth grid = maxInList [ gridValue (depth-1) (takeTurn x grid) | x <- [0..3] ]

Ich denke, es ist ziemlich erfolgreich wegen seiner Einfachheit. Das Ergebnis, das erreicht wird, wenn mit einem leeren Gitter begonnen und in Tiefe 5 aufgelöst wird, ist:

Move 4006
[2,64,16,4]
[16,4096,128,512]
[2048,64,1024,16]
[2,4,16,2]

Game Over

Den Quellcode finden Sie hier: https://github.com/popovitsj/2048-haskell

9
wvdz

Dieser Algorithmus ist nicht optimal, um das Spiel zu gewinnen, aber in Bezug auf Leistung und benötigte Codemenge ziemlich optimal:

  if(can move neither right, up or down)
    direction = left
  else
  {
    do
    {
      direction = random from (right, down, up)
    }
    while(can not move in "direction")
  }
6
API-Beast

Viele der anderen Antworten verwenden KI mit einer rechenintensiven Suche nach möglichen Zukünften, Heuristiken, Lernen und dergleichen. Das ist beeindruckend und wahrscheinlich der richtige Weg, aber ich möchte eine andere Idee einbringen.

Modellieren Sie die Strategie, die gute Spieler des Spiels anwenden.

Zum Beispiel:

13 14 15 16
12 11 10  9
 5  6  7  8
 4  3  2  1

Lesen Sie die Quadrate in der oben gezeigten Reihenfolge ab, bis der nächste Quadratwert größer als der aktuelle ist. Dies wirft das Problem auf, zu versuchen, eine andere Kachel mit demselben Wert in dieses Quadrat einzufügen.

Um dieses Problem zu lösen, gibt es zwei Möglichkeiten, sich zu bewegen, die nicht mehr oder weniger ungünstig sind. Wenn Sie beide Möglichkeiten prüfen, können sich sofort weitere Probleme ergeben. Dies bildet eine Liste von Abhängigkeiten, bei denen jedes Problem ein anderes Problem erfordert, das zuerst gelöst werden muss. Ich glaube, ich habe diese Kette oder in einigen Fällen einen internen Abhängigkeitsbaum, wenn ich über meinen nächsten Schritt entscheide, besonders wenn ich feststecke.


Die Kachel muss mit dem Nachbarn verschmelzen, ist aber zu klein: Verschmelze einen anderen Nachbarn mit diesem.

Größere Fliese im Weg: Erhöhen Sie den Wert einer kleineren umgebenden Fliese.

usw...


Der gesamte Ansatz wird wahrscheinlich komplizierter sein, aber nicht viel komplizierter. Es könnte dieses mechanische Gefühl sein, bei dem Partituren, Gewichte, Neuronen und tiefe Suche nach Möglichkeiten fehlen. Der Baum der Möglichkeiten muss sogar groß genug sein, um überhaupt eine Verzweigung zu benötigen.

4
alan2here