web-dev-qa-db-fra.com

Quel est l'algorithme optimal pour le jeu 2048?

Je suis récemment tombé sur le jeu 2048 . Vous fusionnez des tuiles similaires en les déplaçant dans l'une des quatre directions pour obtenir des tuiles "plus grandes". Après chaque déplacement, une nouvelle tuile apparaît à une position vide aléatoire avec une valeur de 2 ou 4. Le jeu se termine lorsque toutes les cases sont remplies et qu'aucun mouvement ne peut fusionner de mosaïques ni que vous ne créez une mosaïque de valeur 2048.

Premièrement, je dois suivre une stratégie bien définie pour atteindre l'objectif. Alors j'ai pensé à écrire un programme pour ça.

Mon algorithme actuel:

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
}

À tout moment, je vais essayer de fusionner les tuiles avec les valeurs 2 et 4, c’est-à-dire que j’essaie d’avoir 2 et 4 tuiles, aussi minimum que possible. Si j'essaie de cette façon, toutes les autres tuiles sont automatiquement fusionnées et la stratégie semble bonne.

Mais, lorsque j'utilise cet algorithme, je ne gagne que 4000 points avant la fin du jeu. Le nombre maximum de points AFAIK est légèrement supérieur à 20 000 points, ce qui est nettement supérieur à mon score actuel. Y a-t-il un meilleur algorithme que celui ci-dessus?

1877
nitish712

J'ai développé une IA 2048 utilisant l'optimisation expectimax au lieu de la recherche minimax utilisée par l'algorithme de @ ovolve. L’intelligence artificielle effectue simplement une maximisation de tous les mouvements possibles, suivie d’une attente sur toutes les apparitions possibles de tuiles (pondérée par la probabilité des tuiles, c’est-à-dire 10% pour un 4 et 90% pour un 2). Autant que je sache, il n'est pas possible d'élaguer l'optimisation de expectimax (sauf pour supprimer les branches extrêmement improbables). L'algorithme utilisé est donc une recherche de force brute soigneusement optimisée.

Performance

L'IA dans sa configuration par défaut (profondeur de recherche maximale de 8) prend entre 10 ms et 200 ms pour exécuter un déplacement, en fonction de la complexité de la position de la carte. Lors des tests, l'IA atteint un taux de déplacement moyen de 5 à 10 mouvements par seconde tout au long d'un match. Si la profondeur de recherche est limitée à 6 coups, l'IA peut facilement exécuter plus de 20 coups par seconde, ce qui en fait observation intéressante .

Pour évaluer les performances de score de l'IA, j'ai exécuté l'IA 100 fois (connecté au jeu par navigateur via une télécommande). Pour chaque tuile, voici les proportions de jeux dans lesquels cette tuile a été réalisée au moins une fois:

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

Le score minimum sur toutes les pistes était de 124024; Le score maximal obtenu était de 794076. Le score médian était de 387222. L'IA n'a jamais manqué d'obtenir la tuile 2048 (elle n'a donc jamais perdu le jeu, même une fois en 100 matchs); en fait, elle a obtenu la tuile 8192 au moins une fois par course!

Voici la capture d'écran de la meilleure course:

32768 tile, score 794076

Ce jeu a pris 27830 coups en 96 minutes, soit une moyenne de 4,8 coups par seconde.

La mise en oeuvre

Mon approche code la totalité du tableau (16 entrées) sous la forme d’un entier entier de 64 bits (où les tuiles sont les nybbles, c’est-à-dire des morceaux de 4 bits). Sur une machine 64 bits, cela permet de faire passer toute la carte dans un seul registre de machine.

Les opérations de décalage de bits sont utilisées pour extraire des lignes et des colonnes individuelles. Une ligne ou une colonne unique représente une quantité de 16 bits. Ainsi, un tableau de taille 65536 peut coder des transformations qui fonctionnent sur une seule ligne ou une seule colonne. Par exemple, les déplacements sont implémentés sous la forme de 4 recherches dans une "table d’effets de déplacement" précalculée qui décrit comment chaque déplacement affecte une seule ligne ou une seule colonne (par exemple, la table "déplacer à droite" contient l’entrée "1122 -> 0023" décrivant comment rangée [2,2,4,4] devient la rangée [0,0,4,8] lorsqu'elle est déplacée à droite).

La notation est également effectuée à l'aide de la consultation de table. Les tableaux contiennent des scores heuristiques calculés sur toutes les lignes/colonnes possibles et le score résultant d'un tableau est simplement la somme des valeurs du tableau sur chaque ligne et chaque colonne.

Cette représentation du tableau, combinée à l’approche de table pour le mouvement et le score, permet à l’intelligence artificielle de rechercher un grand nombre d’états de jeu en un court laps de temps (plus de 10 000 000 états de jeu par seconde sur un noyau de mon ordinateur portable mi-2011).

La recherche expectimax elle-même est codée comme une recherche récursive alternant des étapes "d’attente" (test de tous les emplacements et valeurs de ponte possibles, et une pondération de leurs scores optimisés par la probabilité de chaque possibilité) et des étapes de "maximisation" (test de tous les mouvements possibles). et en sélectionnant celui avec le meilleur score). La recherche dans l’arbre se termine quand elle voit une position déjà vue (en utilisant un table de transposition ), quand elle atteint une limite de profondeur prédéfinie ou quand elle atteint un état de carte hautement improbable (par exemple si elle a été atteinte) en obtenant 6 "4" tuiles dans une rangée à partir de la position de départ). La profondeur de recherche typique est de 4 à 8 mouvements.

Les heuristiques

Plusieurs heuristiques sont utilisées pour diriger l'algorithme d'optimisation vers des positions favorables. Le choix précis de l'heuristique a un effet considérable sur les performances de l'algorithme. Les différentes heuristiques sont pondérées et combinées dans un score de position, qui détermine la "bonne" position d'un conseil. La recherche d'optimisation visera alors à maximiser le score moyen de tous les postes possibles au conseil d'administration. Le score réel, tel qu’indiqué par le jeu, est non utilisé pour calculer le score du tableau, car il est trop fortement pondéré en faveur de la fusion de tuiles (lorsque la fusion retardée peut produire un avantage).

Au départ, j'ai utilisé deux heuristiques très simples, accordant des "bonus" pour les carrés ouverts et pour avoir de grandes valeurs sur le bord. Ces heuristiques ont plutôt bien fonctionné, atteignant souvent 16384 mais n’atteignant jamais 32768.

Petr Morávek (@xificurk) a pris mon IA et a ajouté deux nouvelles heuristiques. La première heuristique était une pénalité pour avoir des rangées et des colonnes non monotones qui augmentait à mesure que les rangs augmentaient, garantissant que les rangées non monotones de petits nombres n'affecteraient pas fortement le score, mais que les rangées non monotones de grands nombres le pénaliseraient considérablement. La deuxième heuristique comptait le nombre de fusions potentielles (valeurs égales adjacentes) en plus des espaces ouverts. Ces deux heuristiques ont servi à pousser l'algorithme vers les cartes monotoniques (qui sont plus faciles à fusionner) et vers les positions de cartes avec beaucoup de fusions (en l'encourageant à aligner les fusions si possible pour obtenir un effet plus important).

De plus, Petr a également optimisé les poids heuristiques en utilisant une stratégie de "méta-optimisation" (en utilisant un algorithme appelé CMA-ES ), où les poids eux-mêmes ont été ajustés pour obtenir le score moyen le plus élevé possible.

Les effets de ces changements sont extrêmement importants. L'algorithme est passé d'environ 13% du temps de réalisation de la tuile 16384 à plus de 90%, et il a commencé à atteindre 32768 en 1/3 du temps (alors que les anciennes heuristiques ne produisaient jamais une tuile 32768) .

Je crois qu'il reste encore des améliorations à apporter à l'heuristique. Cet algorithme n'est certainement pas encore "optimal", mais j'ai l'impression qu'il se rapproche beaucoup.


Le fait que l'IA réalise la tuile 32 768 dans plus d'un tiers de ses matchs est une étape importante. Je serais surpris d’apprendre si des joueurs humains ont réussi 32768 au jeu officiel (c’est-à-dire sans utiliser d’outils comme les sauvegardes ou les annuler). Je pense que la tuile 65536 est à portée de main!

Vous pouvez essayer l'IA pour vous-même. Le code est disponible sur https://github.com/nneonneo/2048-ai .

1228
nneonneo

Je suis l'auteur du programme d'IA que d'autres ont mentionné dans ce fil. Vous pouvez voir l'IA dans action ou lire le source .

Actuellement, le programme affiche un taux de gain de 90% en javascript dans le navigateur de mon ordinateur portable, avec environ 100 millisecondes de temps de réflexion par coup. Par conséquent, même s'il n'est pas parfait (encore!), Il fonctionne assez bien.

Comme le jeu est un espace d’états discret, une information parfaite, un jeu tour par tour comme les échecs et les dames, j’ai utilisé les mêmes méthodes qui ont fait leurs preuves sur ces jeux, à savoir minimaxrecherche avec élagage alpha-bêta . Puisqu'il y a déjà beaucoup d'informations sur cet algorithme, je parlerai simplement des deux heuristiques principales que j'utilise dans fonction d'évaluation statique et qui formalise bon nombre des intuitions que d'autres personnes ont. exprimé ici.

La monotonie

Cette heuristique essaie de s'assurer que les valeurs des tuiles augmentent ou diminuent dans les directions gauche/droite et haut/bas. Cette heuristique à elle seule traduit l’intuition que beaucoup d’autres ont mentionnée, selon laquelle les tuiles de valeur supérieure devraient être regroupées dans un coin. Cela empêchera généralement les plus petites tuiles de devenir orphelines et maintiendra le tableau très organisé, les plus petites tuiles entrant en cascade et se remplissant dans les plus grandes.

Voici une capture d'écran d'une grille parfaitement monotone. J'ai obtenu cela en exécutant l'algorithme avec la fonction eval configurée pour ignorer les autres heuristiques et ne considérer que la monotonie.

A perfectly monotonic 2048 board

Douceur

La seule heuristique ci-dessus tend à créer des structures dans lesquelles les tuiles adjacentes ont une valeur décroissante, mais bien sûr, pour fusionner, les tuiles adjacentes doivent avoir la même valeur. Par conséquent, l'heuristique de lissage mesure simplement la différence de valeur entre les mosaïques voisines, en essayant de réduire ce nombre.

Un commentateur sur Hacker News a donné ne formalisation intéressante de cette idée en termes de théorie des graphes.

Voici une capture d'écran d'une grille parfaitement lisse, gracieuseté de cet excellent fork de parodie .

A perfectly smooth 2048 board

Tuiles gratuites

Enfin, trop peu de tuiles gratuites sont pénalisées, car les options peuvent être épuisées rapidement lorsque le plateau de jeu devient trop étroit.

Et c'est tout! La recherche dans l'espace de jeu tout en optimisant ces critères permet d'obtenir des performances remarquablement bonnes. Un avantage à utiliser une telle approche généralisée plutôt qu'une stratégie de déplacement explicitement codée est que l'algorithme peut souvent trouver des solutions intéressantes et inattendues. Si vous le regardez courir, il effectuera souvent des mouvements surprenants mais efficaces, comme changer subitement de mur ou de coin contre lequel il se construit.

Modifier:

Voici une démonstration de la puissance de cette approche. J'ai décapsulé les valeurs des tuiles (donc elles ont continué après 2048) et voici le meilleur résultat après huit essais.

4096

Oui, c'est un 4096 aux côtés d'un 2048. =) Cela signifie qu'il a réalisé l'insaisissable 2048 à trois reprises sur le même tableau.

1244
ovolve

Je me suis intéressé à l’idée d’une IA pour ce jeu ne contenant aucune intelligence codée en dur (c’est-à-dire sans heuristique, fonctions de scoring, etc.). L'IA devrait "connaître" uniquement les règles du jeu et "comprendre" le jeu. Cela contraste avec la plupart des IA (comme celles de ce fil) où le jeu est essentiellement une force brute dirigée par une fonction de score représentant la compréhension humaine du jeu.

Algorithme d'IA

J'ai trouvé un algorithme de jeu simple mais étonnamment bon: pour déterminer le prochain mouvement d'un tableau, l'IA joue le jeu en mémoire en utilisant des mouvements aléatoires jusqu'à la le jeu est fini. Ceci est fait plusieurs fois tout en gardant une trace du score final. Ensuite, le score final moyen par coup de départ est calculé. Le coup de départ avec le score final moyen le plus élevé est choisi comme prochain coup.

Avec seulement 100 courses (dans les jeux à mémoire) par coup, l'IA réalise 20% des tuiles 80% des fois et les 4096 tuiles de 50%. L'utilisation de 10000 exécutions donne la tuile 2048 à 100%, 70% pour la tuile 4096 et environ 1% pour la tuile 8192.

Voir en action

Le meilleur score est affiché ici:

best score

Un fait intéressant à propos de cet algorithme est que, bien que les jeux aléatoires soient assez mauvais, le choix du meilleur (ou du moins mauvais) conduit à un très bon jeu: un jeu typique de l'IA peut atteindre 70000 points et 3000 derniers coups, mais le En mémoire, les jeux aléatoires, quelle que soit leur position, rapportent en moyenne 340 points supplémentaires, soit environ 40 coups supplémentaires, avant de mourir. (Vous pouvez le constater vous-même en exécutant l'IA et en ouvrant la console de débogage.)

Ce graphique illustre ce point: la ligne bleue indique le score du tableau après chaque coup. La ligne rouge indique le meilleur score de l'algorithme à la fin du jeu en fin de partie depuis cette position. En substance, les valeurs rouges "tirent" les valeurs bleues vers le haut, car elles constituent la meilleure estimation de l'algorithme. Il est intéressant de voir que la ligne rouge est juste un petit peu au-dessus de la ligne bleue à chaque point, mais la ligne bleue continue à augmenter de plus en plus.

scoring graph

Je trouve assez surprenant que l’algorithme n’ait pas besoin de prévoir un bon jeu pour choisir les mouvements qui le produisent.

En recherchant plus tard, je trouvai que cet algorithme pouvait être classé en tant qu'algorithme Pure Monte Tree Tree Search .

Mise en oeuvre et liens

J'ai d'abord créé une version JavaScript qui peut être vue en action ici . Cette version peut exécuter des centaines d'exécutions dans un temps décent. Ouvrez la console pour plus d'informations. ( source )

Plus tard, pour en avoir plus, j’ai utilisé l’infrastructure hautement optimisée de @nneonneo et mis en œuvre ma version en C++. Cette version permet jusqu'à 100 000 courses par coup et même 1 000 000 si vous avez la patience. Instructions de construction fournies. Il s'exécute dans la console et dispose également d'une télécommande pour lire la version Web. ( source )

Résultats

De manière surprenante, l'augmentation du nombre de pistes n'améliore pas considérablement le jeu. Il semble y avoir une limite à cette stratégie autour de 80000 points avec la tuile 4096 et toutes les plus petites, très proches de la réalisation de la tuile 8192. En augmentant le nombre de passages de 100 à 100 000, on augmente les chances d'atteindre cette limite de score (de 5% à 40%) sans toutefois la dépasser.

Courir 10 000 points avec une augmentation temporaire à 1 000 000 près des postes critiques a réussi à franchir cette barrière moins de 1% des fois, atteignant un score maximal de 129892 et de 8192.

Améliorations

Après avoir implémenté cet algorithme, j'ai essayé de nombreuses améliorations, y compris l'utilisation des scores min ou max, ou une combinaison de min, max et avg. J'ai aussi essayé d'utiliser la profondeur: au lieu d'essayer K courses par coup, j'ai essayé K coups par coup , liste d'une longueur donnée ("haut, haut, gauche" par exemple) et en sélectionnant le premier coup de la liste des meilleurs coups.

Plus tard, j'ai implémenté un arbre de scoring prenant en compte la probabilité conditionnelle de pouvoir jouer un coup après une liste de coups donnée.

Cependant, aucune de ces idées ne présentait d'avantage réel par rapport à la simple idée initiale. J'ai laissé le code pour ces idées commentées dans le code C++.

J'ai ajouté un mécanisme de "recherche approfondie" qui augmentait temporairement le nombre d'exécutions à 1 000 000 lorsqu'une des analyses réussissait à atteindre accidentellement la mosaïque immédiatement supérieure. Cela a offert une amélioration de temps.

Je serais intéressé de savoir si quelqu'un a d'autres idées d'amélioration qui maintiennent l'indépendance de domaine de l'IA.

2048 variantes et clones

Juste pour m'amuser, j'ai aussi implémenté l'IA sous forme de bookmarklet , accrochée aux commandes du jeu. Cela permet à l'IA de travailler avec le jeu d'origine et plusieurs de ses variantes .

Cela est possible en raison de la nature indépendante du domaine de l'IA. Certaines des variantes sont assez distinctes, comme le clone hexagonal.

132
Ronenz

EDIT: Ceci est un algorithme naïf, modélisant un processus de pensée consciente, et obtient des résultats très faibles comparés à une intelligence artificielle qui recherche toutes les possibilités car il ne regarde que d'une tuile devant. Il a été soumis au début de la chronologie de la réponse.

J'ai affiné l'algorithme et battu le jeu! Cela peut échouer à cause de la simple malchance vers la fin (vous êtes obligé de descendre, ce que vous ne devriez jamais faire, et une tuile apparaît à l'endroit où votre plus haut devrait être. Essayez simplement de garder la rangée du haut remplie pour que vous ne puissiez pas vous déplacer à gauche. briser le schéma), mais au fond, vous obtenez une partie fixe et une partie mobile pour jouer. Ceci est votre objectif:

Ready to finish

C'est le modèle que j'ai choisi par défaut.

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

Le coin choisi est arbitraire, vous n’appuyez pratiquement jamais sur une touche (le mouvement interdit), et si vous le faites, vous appuyez à nouveau sur le contraire et essayez de le réparer. Pour les futures mosaïques, le modèle s'attend toujours à ce que la prochaine mosaïque aléatoire soit un 2 et apparaisse du côté opposé au modèle actuel (alors que la première ligne est incomplète, dans le coin inférieur droit, une fois la première ligne terminée, en bas à gauche Coin).

Voici l'algorithme. Environ 80% des victoires (il semble toujours possible de gagner avec davantage de techniques "professionnelles" de l'IA, je n'en suis toutefois pas sûr.)

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)
  }

Quelques indications sur les marches manquantes. Ici: model change

Le modèle a changé en raison de la chance d'être plus proche du modèle attendu. Le modèle que l'IA essaie de réaliser est

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

Et la chaîne pour y arriver est devenue:

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

Les O représentent des espaces interdits ...

Donc, il faudra appuyer à droite, puis à nouveau à droite, puis (à droite ou en haut, selon l'endroit où le 4 a été créé) puis procédera pour terminer la chaîne jusqu'à ce qu'elle obtienne:

Chain completed

Alors maintenant, le modèle et la chaîne sont de retour à:

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

Deuxième pointeur, il a eu la malchance et sa place principale a été prise. Il est probable que cela échouera, mais il peut quand même y parvenir:

Enter image description here

Ici le modèle et la chaîne sont:

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

Lorsqu'il parvient à atteindre les 128, il gagne une ligne entière est à nouveau gagné:

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

Je copie ici le contenu d'un poste sur mon blog


La solution que je propose est très simple et facile à mettre en œuvre. Bien qu'il ait atteint le score de 131040. Plusieurs performances des performances de l'algorithme sont présentées.

Score

Algorithme

Algorithme de notation heuristique

L'hypothèse sur laquelle repose mon algorithme est assez simple: si vous voulez obtenir un score plus élevé, le tableau doit être aussi propre que possible. En particulier, la configuration optimale est donnée par un ordre décroissant linéaire et monotone des valeurs de pavé. Cette intuition vous donnera également la limite supérieure pour une valeur de tuile: s où n est le nombre de tuiles sur le tableau.

(Il est possible d'atteindre le pavé 131072 si le pavé 4 est généré de manière aléatoire au lieu du pavé 2 si nécessaire)

Les images suivantes illustrent deux manières possibles d’organiser le tableau:

enter image description here

Pour imposer l'ordination des carreaux dans un ordre décroissant monotone, le score est calculé comme la somme des valeurs linéarisées du tableau multipliée par les valeurs d'une séquence géométrique de rapport commun r <1.

s

s

Plusieurs chemins linéaires peuvent être évalués en même temps, le score final sera le score maximum de tout chemin.

Règle de décision

La règle de décision implémentée n'est pas très intelligente, le code dans Python est présenté ici:

@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);

Une implémentation de minmax ou d'Expectiminimax améliorera sûrement l'algorithme. De toute évidence, une règle de décision plus sophistiquée ralentira l'algorithme et nécessitera un peu de temps pour être implémenté. J'essaierai une implémentation minimax dans un proche avenir. (Restez à l'écoute)

Référence

  • T1 - 121 tests - 8 chemins différents - r = 0.125
  • T2 - 122 tests - 8 chemins différents - r = 0.25
  • T3 - 132 tests - 8 chemins différents - r = 0.5
  • T4 - 211 tests - 2 chemins différents - r = 0.125
  • T5 - 274 tests - 2 chemins différents - r = 0.25
  • T6 - 211 tests - 2 chemins différents - r = 0.5

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

En cas de T2, quatre essais sur dix génèrent la tuile 4096 avec un score moyen de s 42000

Code

Le code peut être trouvé sur GiHub au lien suivant: https://github.com/Nicola17/term2048-AI Il est basé sur term2048 et est écrit en Python. Je vais implémenter une version plus efficace en C++ dès que possible.

94
Nicola Pezzotti

Ma tentative utilise expectimax comme les autres solutions ci-dessus, mais sans bitboard. La solution de Nneonneo peut contrôler 10 millions de mouvements, soit environ 4 mètres de profondeur, 6 tuiles restantes et 4 mouvements possibles (2 * 6 * 4).4. Dans mon cas, cette profondeur prend trop de temps à explorer, je règle la profondeur de la recherche expectimax en fonction du nombre de tuiles libres restantes:

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

Les scores des planches sont calculés avec la somme pondérée du carré du nombre de carreaux libres et du produit scalaire de la grille 2D avec ceci:

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

qui oblige à organiser les tuiles en descendant dans une sorte de serpent de la tuile en haut à gauche.

code ci-dessous ou sur 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

Je suis l'auteur d'un contrôleur 2048 qui marque mieux que tout autre programme mentionné dans ce fil. Une implémentation efficace du contrôleur est disponible sur github . Dans n référentiel séparé , il y a également le code utilisé pour former la fonction d'évaluation d'état du contrôleur. La méthode de formation est décrite dans le papier .

Le contrôleur utilise la recherche expectimax avec une fonction d'évaluation d'état apprise de toutes pièces (sans expertise humaine) par une variante de apprentissage par différence temporelle (technique d'apprentissage par renforcement). . La fonction état-valeur utilise un réseau n-tuple , qui est essentiellement une fonction linéaire pondérée des motifs observés sur le tableau. Cela impliquait plus de 1 milliard de poids , au total.

Performance

À 1 coups/s: 609104 (moyenne de 100 jeux)

À 10 coups/s: 589355 (moyenne de 300 jeux)

À 3 plis (environ 1500 coups/s): 511759 (moyenne de 1000 jeux)

Les statistiques de tuiles pour 10 coups/s sont les suivantes:

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

(La dernière ligne signifie que les tuiles données sont en même temps sur le tableau).

Pour 3 plis:

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

Cependant, je n'ai jamais observé qu'il obtenait la tuile 65536.

32
cauchy

Je pense avoir trouvé un algorithme qui fonctionne assez bien, mes scores étant souvent supérieurs à 10 000, mon record personnel étant de 16 000. Ma solution ne vise pas à conserver les plus gros nombres dans un virage, mais à le maintenir dans la première rangée.

Veuillez consulter le code ci-dessous:

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
Vincent Lecrubier

Il y a déjà une implémentation d'IA pour ce jeu ici . Extrait de README:

L'algorithme est une profondeur d'approfondissement itérative première recherche alpha-bêta. La fonction d'évaluation essaie de garder les lignes et les colonnes monotones (toutes diminuant ou augmentant) tout en minimisant le nombre de carreaux sur la grille.

Il existe également une discussion sur Hacker News à propos de cet algorithme qui pourrait vous être utile.

25
baltazar

algorithme

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

    choose the maximum evaluation
}

Évaluation

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)

Détails de l'évaluation

128 (Constant)

C'est une constante, utilisée comme base et pour d'autres utilisations telles que les tests.

+ (Number of Spaces x 128)

Plus les espaces sont flexibles, plus on multiplie l'état, on multiplie par 128 (ce qui est la médiane) puisqu'un quadrillage rempli de 128 faces est un état impossible optimal.

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

Ici, nous évaluons les faces qui ont la possibilité de fusionner, en les évaluant à l’arrière, la mosaïque 2 a la valeur 2048, tandis que la mosaïque 2048 est évaluée 2.

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

Ici, nous devons encore vérifier les valeurs empilées, mais d'une manière moins importante n'interrompant pas les paramètres de flexibilité, nous avons donc la somme de {x dans [4,44]}.

+ (Number of possible next moves x 256)

Un État est plus flexible s'il dispose d'une plus grande liberté de transitions possibles.

+ (Number of aligned values x 2)

Il s’agit d’une vérification simplifiée de la possibilité d’avoir des fusions au sein de cet état, sans faire de prévision.

Remarque: les constantes peuvent être modifiées ..

23
Khaled.K

Ce n’est pas une réponse directe à la question de OP, c’est plutôt ce que j’ai essayé jusqu’à présent de résoudre le même problème et j’ai obtenu des résultats et des observations que je veux partager. Je suis curieux de savoir si nous pouvons en avoir. d'autres idées à partir de cela.

Je viens d’essayer d’implémenter mon minimax avec l’élagage alpha-bêta avec une coupure de profondeur des arbres de recherche à 3 et à 5. J'essayais de résoudre le même problème pour une grille 4x4 qu'une affectation de projet pour edX course ColumbiaX: CSMM.101x Intelligence artificielle (AI) .

J'ai appliqué une combinaison convexe (essayé différents poids heuristiques) de quelques fonctions d'évaluation heuristiques, principalement à partir de l'intuition et de celles décrites ci-dessus:

  1. La monotonie
  2. Espace libre disponible

Dans mon cas, le lecteur de l’ordinateur est complètement aléatoire, mais j’ai tout de même supposé des paramètres contradictoires et mis en œuvre l’agent de lecteur AI en tant que joueur maximal.

J'ai une grille 4x4 pour jouer au jeu.

Observation:

Si j'attribue trop de poids à la première fonction heuristique ou à la seconde fonction heuristique, les deux cas où les scores obtenus par le joueur IA sont faibles. J'ai joué avec de nombreuses assignations de poids possibles aux fonctions heuristiques et pris une combinaison convexe, mais très rarement le joueur IA est capable de marquer 2048. La plupart du temps, il s'arrête à 1024 ou 512.

J'ai aussi essayé l'heuristique du coin, mais pour une raison quelconque, les résultats sont pires, vous ne savez pas pourquoi?

De plus, j'ai essayé d'augmenter la limite de profondeur de recherche de 3 à 5 (je ne peux pas l'augmenter davantage car la recherche d'espace dépasse le temps alloué, même avec l'élagage) et j'ai ajouté une heuristique supplémentaire qui examine les valeurs des mosaïques adjacentes et donne plus de points s’ils peuvent être fusionnés, mais je ne parviens toujours pas à obtenir 2048 points.

Je pense qu'il sera préférable d'utiliser Expectimax plutôt que minimax, mais je souhaite tout de même résoudre ce problème avec minimax uniquement et obtenir des scores élevés tels que 2048 ou 4096. Je ne suis pas certain de ne rien rater.

L'animation ci-dessous montre les dernières étapes du jeu joué par l'agent d'intelligence artificielle avec le lecteur de l'ordinateur:

enter image description here

Toute idée sera vraiment très utile, merci d'avance. (Ceci est le lien de mon billet de blog pour l'article: https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation -à-résoudre-2048-jeu-avec-ordinateur / et la vidéo sur youtube: https://www.youtube.com/watch?v=VnVFilfZ0r4 )

L'animation suivante montre les dernières étapes du jeu où l'agent du joueur IA pourrait obtenir 2048 scores, en ajoutant cette fois l'heuristique de valeur absolue:

enter image description here

Les figures suivantes montrent l’arborescence du jeu explorée par l’agent d’AI du joueur en supposant que l’ordinateur est l’adversaire pour une seule étape:

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

11
Sandipan Dey

J'ai écrit un solveur 2048 en Haskell, principalement parce que j'apprends cette langue actuellement.

Mon implémentation du jeu diffère légèrement du jeu réel, en ce sens qu'une nouvelle tuile est toujours un "2" (plutôt que 90% 2 et 10% 4). Et que la nouvelle tuile ne soit pas aléatoire, mais toujours la première disponible en haut à gauche. Cette variante est également appelée Det 2048 .

En conséquence, ce solveur est déterministe.

J'ai utilisé un algorithme exhaustif qui favorise les tuiles vides. Il fonctionne assez rapidement pour les profondeurs 1 à 4, mais à la profondeur 5, il devient plutôt lent à environ 1 seconde par coup.

Vous trouverez ci-dessous le code implémentant l'algorithme de résolution. La grille est représentée sous la forme d'un tableau d'entiers de 16 longueurs. Et la notation se fait simplement en comptant le nombre de carrés vides.

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] ]

Je pense que c'est assez réussi pour sa simplicité. Le résultat obtenu en partant d'une grille vide et en résolvant à la profondeur 5 est:

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

Game Over

Le code source peut être trouvé ici: https://github.com/popovitsj/2048-haskell

9
wvdz

Cet algorithme n'est pas optimal pour gagner le jeu, mais il est assez optimal en termes de performances et de quantité de code nécessaire:

  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

Beaucoup d’autres réponses utilisent l’intelligence artificielle pour effectuer une recherche coûteuse en calcul des futurs possibles, des heuristiques, de l’apprentissage, etc. Celles-ci sont impressionnantes et probablement la voie à suivre correcte, mais je souhaite apporter une autre idée.

Modélisez le type de stratégie utilisée par les bons joueurs du jeu.

Par exemple:

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

Lisez les carrés dans l'ordre indiqué ci-dessus jusqu'à ce que la valeur des carrés suivants soit supérieure à celle actuelle. Cela pose le problème d’essayer de fusionner une autre tuile de même valeur dans ce carré.

Pour résoudre ce problème, il existe deux façons de se déplacer qui ne sont pas laissées ou pire et examiner les deux possibilités peut révéler immédiatement plus de problèmes, cela forme une liste de dépendances, chaque problème nécessitant la résolution préalable d'un autre problème. Je pense avoir cette chaîne ou, dans certains cas, un arbre de dépendances en interne pour décider de mon prochain déménagement, en particulier lorsque je suis bloqué.


La mosaïque doit être fusionnée avec le voisin mais est trop petite: Fusionner un autre voisin avec celui-ci.

Plus grande tuile dans le chemin: Augmentez la valeur d'une tuile environnante plus petite.

etc...


Toute l’approche sera probablement plus compliquée que cela, mais pas beaucoup plus compliquée. Ce pourrait être cette sensation mécanique manquant de scores, de poids, de neurones et de recherches approfondies de possibilités. L'arbre des possibilités doit même être assez grand pour avoir besoin d'une ramification.

4
alan2here