AI et 2048. Partie 2: écrêtage Minimax + alpha beta



Nous avons examiné la méthode de Monte Carlo , nous verrons aujourd'hui comment l'esprit de l'ordinateur joue en 2048 en utilisant le bon vieux minimax avec écrêtage alpha-bêta.

Logiciel EDISON - développement web
L'article a été écrit avec le soutien d'EDISON, une entreprise qui développe des applications mobiles et fournit des services de test de logiciels .

Solution espionnée par le stackoverflow de l'utilisateur ovolve , qui a noté dans la discussion comment enseigner l'IA au jeu 2048 .

Traduction des commentaires de ovolve
Je suis l'auteur du programme mentionné dans ce fil. Vous pouvez voir l'IA en action ou voir le code .

Actuellement, le programme gagne dans environ 90% des cas en exécutant des scripts java dans un navigateur sur mon ordinateur portable, dépensant 100 millisecondes pour réfléchir au cours, fonctionnant, bien que pas parfaitement, mais assez bien.

Comme le jeu est un espace d'état discret avec des informations complètes, étant en fait un jeu au tour par tour comme les échecs et les dames, j'ai utilisé les mêmes méthodes qui ont montré leurs performances dans ces jeux, à savoir la recherche minimax avec écrêtage alpha-bêta . Étant donné que les liens fournissent de nombreuses informations sur cet algorithme, je vais simplement parler des deux principales heuristiques que j'ai utilisées dans la fonction d'estimation statique et formaliser de nombreuses hypothèses intuitives faites par d'autres personnes ici.


Monotonie


Cette heuristique tente de garantir que toutes les valeurs de tuiles augmentent ou diminuent à la fois à gauche / à droite et en haut / en bas. Cette heuristique à elle seule reflète la conjecture que beaucoup d'autres ont mentionné que des tuiles plus précieuses devraient être regroupées dans un coin. En règle générale, cela empêche l'accumulation de tuiles de moindre valeur et maintient le plateau organisé, car les petites tuiles se transforment en plus grandes.

Voici une capture d'écran d'une grille complètement monotone. J'ai eu cette situation en exécutant un algorithme avec la fonction eval installée afin d'ignorer les autres heuristiques et de ne prendre en compte que la monotonie.


Douceur (douceur, régularité)


L'heuristique ci-dessus en soi a tendance à créer des structures dans lesquelles les cellules voisines sont réduites en valeur, cependant, bien sûr, les voisins devraient avoir la même signification à combiner. Par conséquent, l'heuristique de lissage mesure simplement la différence de valeurs entre les tuiles adjacentes, en essayant de minimiser leur nombre.

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

Traduction de formalisation avec Hacker News
Hier, j'ai montré ce jeu à un collègue, un amoureux de la théorie des graphes, et nous avons également décidé de réfléchir à la façon de résoudre ce jeu en utilisant l'IA.

La solution la plus simple est minimax, qui, à mon avis, est assez bien implémentée. Si quelqu'un ici ne connaît pas minimax, OP a écrit un code très élégant et bien commenté qui serait un excellent tutoriel.

L'approche la moins intensive en calcul que nous avons proposée était de modéliser l'état du jeu sous la forme d'un graphique G (V, E) , où V est un ensemble de tuiles actives et E est un ensemble d'arêtes reliant des tuiles adjacentes pondérées par la fonction c (v1, v2) , qui renvoie la valeur absolue de la différence entre les deux tuiles. Pour chaque solution, l'IA choisit un coup qui minimise la somme des poids de tous les bords dans le nouvel état de jeu.

La raison en est que la seule façon de progresser dans le jeu est d'avoir des tuiles avec les mêmes valeurs côte à côte, pour lesquelles le poids en G sera 0. Ainsi, l'IA devrait essayer de minimiser le poids total. À la fin, il y aura un grand nombre sur les planches avec un grand poids de bords aux tuiles adjacentes, donc l'IA essaiera de garder ces tuiles à côté d'autres grandes tuiles pour minimiser la différence.

Étant donné que le jeu est stochastique, l'approche que j'ai décrite peut ne pas fonctionner dans le pire des cas, mais elle peut également être appliquée à la solution minimax existante en tant que fonction de pondération pour chaque nœud de l'arbre.


Voici une capture d'écran d'un maillage parfaitement lisse, gracieusement fourni par cette excellente fausse fourche . (lien vers l'archive Web, tandis que les scripts Java sur la page fonctionnent et que vous pouvez utiliser le clavier pour vous déplacer dans n'importe quelle direction - note du traducteur).

Tuiles en vrac


Et enfin, il y a une pénalité pour avoir trop peu de tuiles gratuites, car les options peuvent rapidement se terminer lorsque le terrain de jeu devient trop étroit.

Et c'est tout! La recherche de l'espace de jeu tout en optimisant ces critères donne des performances étonnamment bonnes. L'un des avantages de l'utilisation d'une approche générique comme celle-ci plutôt que d'une stratégie de déplacement explicitement codée est que l'algorithme peut souvent trouver des solutions intéressantes et inattendues. Si vous observez sa progression, il fait souvent des mouvements étonnants mais efficaces, comme le changement soudain de murs ou de coins, près desquels il construit son jeu.


Petit changement


La capture d'écran montre la puissance de cette approche. J'ai supprimé la limite de tuiles (afin qu'elles continuent de croître après avoir atteint 2048), et voici le meilleur résultat après huit tests.

Oui, c'est 4096 avec 2048. =) Cela signifie qu'il a atteint la tuile 2048 insaisissable sur un plateau.







Le code Java-Script pour minimax avec écrêtage alpha-bêta et fonction d'évaluation statique de l'utilisateur ovover de stackoverflow est donné ci-dessous dans l'article.

La méthode minimax est consacrée à plusieurs excellents articles habr, donc nous omettons l'explication détaillée académique de ce qu'elle consiste. Pour ceux qui ont rejoint la communauté informatique, j'ai récemment entendu les beaux termes «minimax» et «écrêtage alpha-bêta», mais je ne sais pas ce que cela signifie, essayons, littéralement en quelques paragraphes, d'expliquer la signification la plus générale.

Minimax


Dans certains jeux, le processus d'un jeu entre deux joueurs (qui se déplacent à leur tour) peut être représenté comme un soi-disant arbre d'options. Dans chaque position spécifique, chaque joueur a généralement le choix entre différentes options pour son mouvement. Et en réponse à chacune de ces options, un adversaire peut aussi ressembler à bien des égards.


Fragment d'un arbre d'options

Étant donné qu'à tout moment du jeu, il existe des informations complètes sur l'état du terrain de jeu, l'état actuel de la position peut toujours être estimé avec précision. Une telle fonction est appelée fonction d'évaluation statique ou SFO abrégé. De plus, plus cette fonction est importante lors de l'évaluation d'une position spécifique, plus la position est avantageuse pour un joueur (appelons-la le joueur maximisant ). Plus la valeur numérique de cette fonction est petite lors de l'évaluation d'une position, plus la position du deuxième joueur est avantageuse (appelons-la le joueur minimisant ).

Après chaque mouvement, la position change et donc son score change. Lors de l'examen de l'arbre des options, chaque joueur doit non seulement préférer les branches dans lesquelles le classement lui est le plus favorable. Vous devez également éviter les branches dans lesquelles l'évaluation de la position est favorable à l'adversaire.

On suppose que l'adversaire est également guidé par le rationalisme et évite également les options qui pourraient le conduire à perdre. Autrement dit, chaque joueur, lors du choix d'une option, procède de la maximisation de son propre avantage et en même temps de la minimisation du profit de l'adversaire.

C'est minimax.

Détourage alpha bêta


C'est assez évident: qui calcule un arbre d'une position donnée à une plus grande profondeur, il a plus de chances de gagner. Mais il y a une nuisance - l'arbre des options dans les jeux a la mauvaise habitude de se ramifier et de croître de façon exponentielle à chaque niveau d'imbrication. Les capacités de comptage des programmes, et d'autant plus que les gens sont limités, compter "jusqu'au tapis" est loin d'être toujours possible. Il peut facilement s'avérer que le joueur a compté jusqu'à une position où il a une bonne évaluation du terrain de jeu, mais littéralement au niveau suivant (illisible), l'adversaire a la possibilité de faire un tel mouvement qui change radicalement l'estimation de la position en sens inverse.

Qui est à blâmer et que faire? La complexité de calcul est à blâmer pour la traversée complète de l'arbre; il est proposé de lutter en coupant les branches inutiles. Si le joueur qui évalue la position voit qu'une branche de l'arborescence d'options:

ou moins rentable pour elle que d'autres branches qui ont déjà été analysées,
ou plus avantageux pour l'adversaire que d'autres branches qui ont déjà été analysées,

alors le joueur défausse cette branche, ne perd pas de temps et de ressources à considérer les sous-options de cette branche évidemment pire pour lui.

Cela vous permet d'allouer plus de ressources informatiques pour calculer des branches plus favorables à une plus grande profondeur de rendu dans l'arborescence des options. Dans le processus d'évaluation du terrain de jeu à différents niveaux de l'arborescence des options, le joueur fonctionne avec deux coefficients changeant dynamiquement - alpha (la valeur du SFD qui est rencontrée de manière minimale dans la branche - c'est-à-dire plus favorable pour le joueur minimisant) et beta (la valeur de la SFD la plus rencontrée dans la branche - c'est-à-dire plus favorable pour le joueur maximisant). À chaque niveau, la comparaison du SFD de la position actuelle avec les coefficients alpha et bêta permet de balayer (sans les calculer complètement) des branches moins favorables pour le joueur évaluant la position et / ou plus avantageuses pour son adversaire.

Il s'agit d'un découpage alpha bêta.

Fonction minimax récursive avec écrêtage alpha bêta


2048 avec AI est implémenté en tant qu'application Excel avec des macros VBA, c'est ainsi que l'algorithme minimax avec écrêtage alpha beta ressemble à un élémentaire visuel méprisable.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' '''''''''''''''''''''''( - )''''''''''''''''''''''''' ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' '       -- 'Position -  4  4     'Depth - ,    'Alpha, Beta -         'MaximisingPlayer -      ? Private Function MiniMaxAlpaBeta_Evaluation(Position As Variant, Depth As Long, _ Alpha As Double, Beta As Double, _ MaximisingPlayer As Boolean, _ Optional MainLevel As Boolean = False) As Double Dim MaxEval As Double '  Dim MinEval As Double '  Dim PositionNext As Variant '     Dim PositionTemp As Variant '     Dim Eval As Double '   Dim Way As Long '   -      Dim Row As Long '     Dim Col As Long '     Dim TileNew As Long '      '   (  ,  '     ) If GameOverPosition(Position) Then '    ? '     MiniMaxAlpaBeta_Evaluation = -1000000 + TileMax(Position) '         ElseIf Depth = 0 Then '     MiniMaxAlpaBeta_Evaluation = StaticEvaluation(Position) '  ,    '     () ElseIf MaximisingPlayer Then MaxEval = -1000000 '      For Way = 1 To 4 ' 4   - (, , , ) ChangeCount = 0 ' ,      ',       PositionNext = StepHuman(Position, Way) If ChangeCount > 0 Then '     '      , '    () Eval = MiniMaxAlpaBeta_Evaluation(PositionNext, Depth - 1, _ Alpha, Beta, False) If Eval > MaxEval Then MaxEval = Eval '  '     If Eval > Alpha Then Alpha = Eval '    ,   '   -    If Beta > Alpha Then Exit For End If Next '          MiniMaxAlpaBeta_Evaluation = MaxEval '  ,    '     () Else 'Not MaximisingPlayer MinEval = 1000000 '      For Row = 1 To 4 '     For Col = 1 To 4 '     If Position(Row, Col) = 0 Then '   For TileNew = 2 To 4 Step 2 '    2  4 ',       '    PositionNext = StepComp(Position, Row, Col, TileNew) '     , '    () Eval = MiniMaxAlpaBeta_Evaluation(PositionNext, Depth - 1, _ Alpha, Beta, True) If Eval < MinEval Then MinEval = Eval '  '     If Eval < Beta Then Beta = Eval '    ,   '   -    If Alpha < Beta Then Exit For Next End If Next Next '          MiniMaxAlpaBeta_Evaluation = MinEval End If End Function 

Ovolve code dans java-script
 function AI(grid) { this.grid = grid; } //   () AI.prototype.eval = function() { var emptyCells = this.grid.availableCells().length; var smoothWeight = 0.1, //monoWeight = 0.0, //islandWeight = 0.0, mono2Weight = 1.0, emptyWeight = 2.7, maxWeight = 1.0; return this.grid.smoothness() * smoothWeight //+ this.grid.monotonicity() * monoWeight //- this.grid.islands() * islandWeight + this.grid.monotonicity2() * mono2Weight + Math.log(emptyCells) * emptyWeight + this.grid.maxValue() * maxWeight; }; // alpha-beta depth first search AI.prototype.search = function(depth, alpha, beta, positions, cutoffs) { var bestScore; var bestMove = -1; var result; // the maxing player if (this.grid.playerTurn) { bestScore = alpha; for (var direction in [0, 1, 2, 3]) { var newGrid = this.grid.clone(); if (newGrid.move(direction).moved) { positions++; if (newGrid.isWin()) { return { move: direction, score: 10000, positions: positions, cutoffs: cutoffs }; } var newAI = new AI(newGrid); if (depth == 0) { result = { move: direction, score: newAI.eval() }; } else { result = newAI.search(depth-1, bestScore, beta, positions, cutoffs); if (result.score > 9900) { // win result.score--; // to slightly penalize higher depth from win } positions = result.positions; cutoffs = result.cutoffs; } if (result.score > bestScore) { bestScore = result.score; bestMove = direction; } if (bestScore > beta) { cutoffs++ return { move: bestMove, score: beta, positions: positions, cutoffs: cutoffs }; } } } } else { // computer's turn, we'll do heavy pruning to keep the branching factor low bestScore = beta; // try a 2 and 4 in each cell and measure how annoying it is // with metrics from eval var candidates = []; var cells = this.grid.availableCells(); var scores = { 2: [], 4: [] }; for (var value in scores) { for (var i in cells) { scores[value].push(null); var cell = cells[i]; var tile = new Tile(cell, parseInt(value, 10)); this.grid.insertTile(tile); scores[value][i] = -this.grid.smoothness() + this.grid.islands(); this.grid.removeTile(cell); } } // now just pick out the most annoying moves var maxScore = Math.max(Math.max.apply(null, scores[2]), Math.max.apply(null, scores[4])); for (var value in scores) { // 2 and 4 for (var i=0; i<scores[value].length; i++) { if (scores[value][i] == maxScore) { candidates.push( { position: cells[i], value: parseInt(value, 10) } ); } } } // search on each candidate for (var i=0; i<candidates.length; i++) { var position = candidates[i].position; var value = candidates[i].value; var newGrid = this.grid.clone(); var tile = new Tile(position, value); newGrid.insertTile(tile); newGrid.playerTurn = true; positions++; newAI = new AI(newGrid); result = newAI.search(depth, alpha, bestScore, positions, cutoffs); positions = result.positions; cutoffs = result.cutoffs; if (result.score < bestScore) { bestScore = result.score; } if (bestScore < alpha) { cutoffs++; return { move: null, score: alpha, positions: positions, cutoffs: cutoffs }; } } } return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs }; } // performs a search and returns the best move AI.prototype.getBest = function() { return this.iterativeDeep(); } // performs iterative deepening over the alpha-beta search AI.prototype.iterativeDeep = function() { var start = (new Date()).getTime(); var depth = 0; var best; do { var newBest = this.search(depth, -10000, 10000, 0 ,0); if (newBest.move == -1) { break; } else { best = newBest; } depth++; } while ( (new Date()).getTime() - start < minSearchTime); return best } AI.prototype.translate = function(move) { return { 0: 'up', 1: 'right', 2: 'down', 3: 'left' }[move]; } 

Fonction d'évaluation statique


Étant donné qu'à chaque niveau de l'arborescence des options, vous devez évaluer le terrain de jeu (afin de décider pour lequel des joueurs, la position estimée est en fait plus avantageuse), vous devez décider selon quels critères pour distinguer une bonne position d'une mauvaise position.

Nous supposons que le joueur maximisant est la personne (ou l'IA) qui décide dans laquelle des 4 directions (haut, gauche, droite, bas) déplacer toutes les tuiles. Un joueur minimisant est ce sous-programme insidieux qui génère au hasard 2 ou 4 dans les endroits les plus inappropriés.

L'OFS est compilé du point de vue d'un acteur maximisant. Plus la note SFD est élevée pour le terrain de jeu, meilleure est la position du «maximaliste». Le plus bas - plus la position sur la planche est agréable pour le "minimaliste".

Dans le cas de 2048 - quels facteurs sont considérés comme favorables à celui qui déplace les tuiles?

Monotonie


Tout d'abord, il est souhaitable que les carreaux soient disposés dans l'ordre croissant / décroissant dans certaines directions. Si cela n'est pas fait, lorsque de nouvelles tuiles sont générées, le terrain de jeu sera rapidement obstrué par des tuiles disposées au hasard de différentes tailles, qui ne peuvent pas être immédiatement connectées normalement les unes aux autres.

Dans le district fédéral de Sibérie, vous devez regarder dans les 4 directions (de haut en bas, de gauche à droite, de droite à gauche, de bas en haut) et calculer où les tuiles sont une progression décroissante ou croissante. Si en progression il y a des tuiles qui ne rentrent pas dans la série générale, cela réduit le coefficient numérique de monotonie. Ensuite, parmi les 4 coefficients pour toutes les directions, le meilleur est sélectionné, qui est pris en compte dans la valeur totale du district fédéral sibérien.

Douceur


De plus, il serait plus préférable que la progression de la position debout dans une rangée de tuiles ne soit pas seulement croissante, mais non décroissante (ou au lieu de diminuer la ligne, il est préférable de ne pas augmenter), c'est-à-dire que c'est bien lorsque les mêmes tuiles sont à proximité, ce qui leur permet de s'effondrer en une seule, de gagner des points et augmenter l'espace libre sur le terrain de jeu.

Par conséquent, le district fédéral de Sibérie recherche les mêmes tuiles adjacentes sur le terrain de jeu et prend en compte le nombre de ces paires dans un coefficient spécial.

Cellules vides


Évidemment, plus il y a d'espace libre, plus il y a de marge de manœuvre et moins il y a de chances de perdre rapidement.

SFO considère les cellules vides sur le terrain et plus celles-ci, la position est considérée comme plus rentable pour le joueur maximisant.

Tuile maximum


Étant donné que la principale chose dans ce jeu est d'obtenir une grande tuile sur le terrain, plus c'est mieux - 2048, 4096, 8192 (ou tout ce pour quoi vous avez la force et la patience), les options où la valeur maximale de la tuile est la plus élevée doivent être considérées comme le SFD le plus rentable.

District fédéral sibérien pour 2048


Implémentation du District fédéral sibérien en tant que macro VBA
 ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' '''''''''''''''''''''''  '''''''''''''''''''''''' ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' '     'Position -  4  4     Private Function StaticEvaluation(Position As Variant) As Double Dim Smoothness As Double ' Dim Monotonicity As Double ' Dim EmptyCount As Double '  Dim MaxValue As Long '  '   Const SmoothWeight = 0.1 Const MonoWeight = 1 Const EmptyWeight = 2.7 Const MaxWeight = 1 Dim k As Long '   Dim i As Long '  Dim j As Long '  Dim x As Long '  Dim y As Long '  ' Dim Value As Double '       '         Dim TargetValue As Double Smoothness = 0 '    For i = 1 To 4 '     For j = 1 To 4 '     If Position(i, j) > 0 Then '   Value = Log(Position(i, j)) / Log(2) If i < 4 Then '       For x = i + 1 To 4 '    If Position(x, j) > 0 Then '    '    TargetValue = Log(Position(x, j)) / Log(2) ',   Smoothness = Abs(Value - TargetValue) '       Exit For End If Next End If If j < 4 Then '       For y = j + 1 To 4 '    If Position(i, y) > 0 Then '    '    TargetValue = Log(Position(i, y)) / Log(2) ',   Smoothness = Abs(Value - TargetValue) '        Exit For End If Next End If End If Next Next ' Dim arrTotals(1 To 4) As Double '     Dim Current As Long '   Dim Next_ As Long '      Dim CurrentValue As Double '      Dim NextValue As Double '        Monotonicity = 0 '    '      For k = 1 To 4 arrTotals(k) = 0 Next ' -  - For x = 1 To 4 '   Current = 1 '      '   (     )  Next_ = Current + 1 Do While Next_ <= 4 '       '      '(       ) Do While Next_ <= 4 '       If Position(x, Next_) = 0 Then '     Next_ = Next_ + 1 '   Else ' -    Exit Do ' ,  ,   End If Loop '         If Next_ > 4 Then Next_ = 4 '          If Position(x, Current) > 0 Then CurrentValue = Log(Position(x, Current)) / Log(2) Else CurrentValue = 0 End If ' MsgBox "Position[" & x & ", " & Next_ & "]=" & Position(x, Next_) If Position(x, Next_) > 0 Then NextValue = Log(Position(x, Next_)) / Log(2) Else NextValue = 0 End If If CurrentValue > NextValue Then '   ? arrTotals(Up) = arrTotals(Up) + NextValue - CurrentValue ElseIf NextValue > CurrentValue Then '   ? arrTotals(Down) = arrTotals(Down) + CurrentValue - NextValue End If Current = Next_ '       Next_ = Next_ + 1 '    Loop Next '      -  - Monotonicity = IIf(arrTotals(Up) >= arrTotals(Down), _ Monotonicity + arrTotals(Up), _ Monotonicity + arrTotals(Down)) ' -  - For y = 1 To 4 '   Current = 1 '      '   (     )  Next_ = Current + 1 Do While Next_ <= 4 '       '      '(       ) Do While Next_ <= 4 '       If Position(Next_, y) = 0 Then '     Next_ = Next_ + 1 '   Else ' -    Exit Do ' ,  ,   End If Loop '         If Next_ > 4 Then Next_ = 4 '          If Position(Current, y) > 0 Then CurrentValue = Log(Position(Current, y)) / Log(2) Else CurrentValue = 0 End If If Position(Next_, y) > 0 Then NextValue = Log(Position(Next_, y)) / Log(2) Else NextValue = 0 End If If CurrentValue > NextValue Then '   ? arrTotals(Left) = arrTotals(Left) + NextValue - CurrentValue ElseIf NextValue > CurrentValue Then '   ? arrTotals(Right) = arrTotals(Right) + CurrentValue - NextValue End If Current = Next_ '       Next_ = Next_ + 1 '    Loop Next '      -  - Monotonicity = IIf(arrTotals(Left) >= arrTotals(Right), _ Monotonicity + arrTotals(Left), _ Monotonicity + arrTotals(Right)) '     EmptyCount = 0 '      MaxValue = 0 '    For i = 1 To 4 '     For j = 1 To 4 '     If Position(i, j) = 0 Then '  ... '...     EmptyCount = EmptyCount + 1 '     ... ElseIf Position(i, j) > MaxValue Then MaxValue = Position(i, j) '...    End If Next Next '   StaticEvaluation = Smoothness * SmoothWeight + _ Monotonicity * MonoWeight + _ Log_Base_Arg(EmptyCount) * EmptyWeight + _ MaxValue * MaxWeight End Function 

Ovolve code dans java-script
 function Grid(size) { this.size = size; this.startTiles = 2; this.cells = []; this.build(); this.playerTurn = true; } // pre-allocate these objects (for speed) Grid.prototype.indexes = []; for (var x=0; x<4; x++) { Grid.prototype.indexes.push([]); for (var y=0; y<4; y++) { Grid.prototype.indexes[x].push( {x:x, y:y} ); } } // Build a grid of the specified size Grid.prototype.build = function () { for (var x = 0; x < this.size; x++) { var row = this.cells[x] = []; for (var y = 0; y < this.size; y++) { row.push(null); } } }; // Find the first available random position Grid.prototype.randomAvailableCell = function () { var cells = this.availableCells(); if (cells.length) { return cells[Math.floor(Math.random() * cells.length)]; } }; Grid.prototype.availableCells = function () { var cells = []; var self = this; this.eachCell(function (x, y, tile) { if (!tile) { //cells.push(self.indexes[x][y]); cells.push( {x:x, y:y} ); } }); return cells; }; // Call callback for every cell Grid.prototype.eachCell = function (callback) { for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { callback(x, y, this.cells[x][y]); } } }; // Check if there are any cells available Grid.prototype.cellsAvailable = function () { return !!this.availableCells().length; }; // Check if the specified cell is taken Grid.prototype.cellAvailable = function (cell) { return !this.cellOccupied(cell); }; Grid.prototype.cellOccupied = function (cell) { return !!this.cellContent(cell); }; Grid.prototype.cellContent = function (cell) { if (this.withinBounds(cell)) { return this.cells[cell.x][cell.y]; } else { return null; } }; // Inserts a tile at its position Grid.prototype.insertTile = function (tile) { this.cells[tile.x][tile.y] = tile; }; Grid.prototype.removeTile = function (tile) { this.cells[tile.x][tile.y] = null; }; Grid.prototype.withinBounds = function (position) { return position.x >= 0 && position.x < this.size && position.y >= 0 && position.y < this.size; }; Grid.prototype.clone = function() { newGrid = new Grid(this.size); newGrid.playerTurn = this.playerTurn; for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { if (this.cells[x][y]) { newGrid.insertTile(this.cells[x][y].clone()); } } } return newGrid; }; // Set up the initial tiles to start the game with Grid.prototype.addStartTiles = function () { for (var i=0; i<this.startTiles; i++) { this.addRandomTile(); } }; // Adds a tile in a random position Grid.prototype.addRandomTile = function () { if (this.cellsAvailable()) { var value = Math.random() < 0.9 ? 2 : 4; //var value = Math.random() < 0.9 ? 256 : 512; var tile = new Tile(this.randomAvailableCell(), value); this.insertTile(tile); } }; // Save all tile positions and remove merger info Grid.prototype.prepareTiles = function () { this.eachCell(function (x, y, tile) { if (tile) { tile.mergedFrom = null; tile.savePosition(); } }); }; // Move a tile and its representation Grid.prototype.moveTile = function (tile, cell) { this.cells[tile.x][tile.y] = null; this.cells[cell.x][cell.y] = tile; tile.updatePosition(cell); }; Grid.prototype.vectors = { 0: { x: 0, y: -1 }, // up 1: { x: 1, y: 0 }, // right 2: { x: 0, y: 1 }, // down 3: { x: -1, y: 0 } // left } // Get the vector representing the chosen direction Grid.prototype.getVector = function (direction) { // Vectors representing tile movement return this.vectors[direction]; }; // Move tiles on the grid in the specified direction // returns true if move was successful Grid.prototype.move = function (direction) { // 0: up, 1: right, 2:down, 3: left var self = this; var cell, tile; var vector = this.getVector(direction); var traversals = this.buildTraversals(vector); var moved = false; var score = 0; var won = false; // Save the current tile positions and remove merger information this.prepareTiles(); // Traverse the grid in the right direction and move tiles traversals.x.forEach(function (x) { traversals.y.forEach(function (y) { cell = self.indexes[x][y]; tile = self.cellContent(cell); if (tile) { //if (debug) { //console.log('tile @', x, y); //} var positions = self.findFarthestPosition(cell, vector); var next = self.cellContent(positions.next); // Only one merger per row traversal? if (next && next.value === tile.value && !next.mergedFrom) { var merged = new Tile(positions.next, tile.value * 2); merged.mergedFrom = [tile, next]; self.insertTile(merged); self.removeTile(tile); // Converge the two tiles' positions tile.updatePosition(positions.next); // Update the score score += merged.value; // The mighty 2048 tile if (merged.value === 2048) { won = true; } } else { //if (debug) { //console.log(cell); //console.log(tile); //} self.moveTile(tile, positions.farthest); } if (!self.positionsEqual(cell, tile)) { self.playerTurn = false; //console.log('setting player turn to ', self.playerTurn); moved = true; // The tile moved from its original cell! } } }); }); //console.log('returning, playerturn is', self.playerTurn); //if (!moved) { //console.log('cell', cell); //console.log('tile', tile); //console.log('direction', direction); //console.log(this.toString()); //} return {moved: moved, score: score, won: won}; }; Grid.prototype.computerMove = function() { this.addRandomTile(); this.playerTurn = true; } // Build a list of positions to traverse in the right order Grid.prototype.buildTraversals = function (vector) { var traversals = { x: [], y: [] }; for (var pos = 0; pos < this.size; pos++) { traversals.x.push(pos); traversals.y.push(pos); } // Always traverse from the farthest cell in the chosen direction if (vector.x === 1) traversals.x = traversals.x.reverse(); if (vector.y === 1) traversals.y = traversals.y.reverse(); return traversals; }; Grid.prototype.findFarthestPosition = function (cell, vector) { var previous; // Progress towards the vector direction until an obstacle is found do { previous = cell; cell = { x: previous.x + vector.x, y: previous.y + vector.y }; } while (this.withinBounds(cell) && this.cellAvailable(cell)); return { farthest: previous, next: cell // Used to check if a merge is required }; }; Grid.prototype.movesAvailable = function () { return this.cellsAvailable() || this.tileMatchesAvailable(); }; // Check for available matches between tiles (more expensive check) // returns the number of matches Grid.prototype.tileMatchesAvailable = function () { var self = this; //var matches = 0; var tile; for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { tile = this.cellContent({ x: x, y: y }); if (tile) { for (var direction = 0; direction < 4; direction++) { var vector = self.getVector(direction); var cell = { x: x + vector.x, y: y + vector.y }; var other = self.cellContent(cell); if (other && other.value === tile.value) { return true; //matches++; // These two tiles can be merged } } } } } //console.log(matches); return false; //matches; }; Grid.prototype.positionsEqual = function (first, second) { return first.x === second.x && first.y === second.y; }; Grid.prototype.toString = function() { string = ''; for (var i=0; i<4; i++) { for (var j=0; j<4; j++) { if (this.cells[j][i]) { string += this.cells[j][i].value + ' '; } else { string += '_ '; } } string += '\n'; } return string; } // counts the number of isolated groups. Grid.prototype.islands = function() { var self = this; var mark = function(x, y, value) { if (x >= 0 && x <= 3 && y >= 0 && y <= 3 && self.cells[x][y] && self.cells[x][y].value == value && !self.cells[x][y].marked ) { self.cells[x][y].marked = true; for (direction in [0,1,2,3]) { var vector = self.getVector(direction); mark(x + vector.x, y + vector.y, value); } } } var islands = 0; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cells[x][y]) { this.cells[x][y].marked = false } } } for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cells[x][y] && !this.cells[x][y].marked) { islands++; mark(x, y , this.cells[x][y].value); } } } return islands; } // measures how smooth the grid is (as if the values of the pieces // were interpreted as elevations). Sums of the pairwise difference // between neighboring tiles (in log space, so it represents the // number of merges that need to happen before they can merge). // Note that the pieces can be distant Grid.prototype.smoothness = function() { var smoothness = 0; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if ( this.cellOccupied( this.indexes[x][y] )) { var value = Math.log(this.cellContent( this.indexes[x][y] ).value) / Math.log(2); for (var direction=1; direction<=2; direction++) { var vector = this.getVector(direction); var targetCell = this.findFarthestPosition(this.indexes[x][y], vector).next; if (this.cellOccupied(targetCell)) { var target = this.cellContent(targetCell); var targetValue = Math.log(target.value) / Math.log(2); smoothness -= Math.abs(value - targetValue); } } } } } return smoothness; } Grid.prototype.monotonicity = function() { var self = this; var marked = []; var queued = []; var highestValue = 0; var highestCell = {x:0, y:0}; for (var x=0; x<4; x++) { marked.push([]); queued.push([]); for (var y=0; y<4; y++) { marked[x].push(false); queued[x].push(false); if (this.cells[x][y] && this.cells[x][y].value > highestValue) { highestValue = this.cells[x][y].value; highestCell.x = x; highestCell.y = y; } } } increases = 0; cellQueue = [highestCell]; queued[highestCell.x][highestCell.y] = true; markList = [highestCell]; markAfter = 1; // only mark after all queued moves are done, as if searching in parallel var markAndScore = function(cell) { markList.push(cell); var value; if (self.cellOccupied(cell)) { value = Math.log(self.cellContent(cell).value) / Math.log(2); } else { value = 0; } for (direction in [0,1,2,3]) { var vector = self.getVector(direction); var target = { x: cell.x + vector.x, y: cell.y+vector.y } if (self.withinBounds(target) && !marked[target.x][target.y]) { if ( self.cellOccupied(target) ) { targetValue = Math.log(self.cellContent(target).value ) / Math.log(2); if ( targetValue > value ) { //console.log(cell, value, target, targetValue); increases += targetValue - value; } } if (!queued[target.x][target.y]) { cellQueue.push(target); queued[target.x][target.y] = true; } } } if (markAfter == 0) { while (markList.length > 0) { var cel = markList.pop(); marked[cel.x][cel.y] = true; } markAfter = cellQueue.length; } } while (cellQueue.length > 0) { markAfter--; markAndScore(cellQueue.shift()) } return -increases; } // measures how monotonic the grid is. This means the values of the tiles are strictly increasing // or decreasing in both the left/right and up/down directions Grid.prototype.monotonicity2 = function() { // scores for all four directions var totals = [0, 0, 0, 0]; // up/down direction for (var x=0; x<4; x++) { var current = 0; var next = current+1; while ( next<4 ) { while ( next<4 && !this.cellOccupied( this.indexes[x][next] )) { next++; } if (next>=4) { next--; } var currentValue = this.cellOccupied({x:x, y:current}) ? Math.log(this.cellContent( this.indexes[x][current] ).value) / Math.log(2) : 0; var nextValue = this.cellOccupied({x:x, y:next}) ? Math.log(this.cellContent( this.indexes[x][next] ).value) / Math.log(2) : 0; if (currentValue > nextValue) { totals[0] += nextValue - currentValue; } else if (nextValue > currentValue) { totals[1] += currentValue - nextValue; } current = next; next++; } } // left/right direction for (var y=0; y<4; y++) { var current = 0; var next = current+1; while ( next<4 ) { while ( next<4 && !this.cellOccupied( this.indexes[next][y] )) { next++; } if (next>=4) { next--; } var currentValue = this.cellOccupied({x:current, y:y}) ? Math.log(this.cellContent( this.indexes[current][y] ).value) / Math.log(2) : 0; var nextValue = this.cellOccupied({x:next, y:y}) ? Math.log(this.cellContent( this.indexes[next][y] ).value) / Math.log(2) : 0; if (currentValue > nextValue) { totals[2] += nextValue - currentValue; } else if (nextValue > currentValue) { totals[3] += currentValue - nextValue; } current = next; next++; } } return Math.max(totals[0], totals[1]) + Math.max(totals[2], totals[3]); } Grid.prototype.maxValue = function() { var max = 0; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cellOccupied(this.indexes[x][y])) { var value = this.cellContent(this.indexes[x][y]).value; if (value > max) { max = value; } } } } return Math.log(max) / Math.log(2); } // WIP. trying to favor top-heavy distributions (force consolidation of higher value tiles) /* Grid.prototype.valueSum = function() { var valueCount = []; for (var i=0; i<11; i++) { valueCount.push(0); } for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cellOccupied(this.indexes[x][y])) { valueCount[Math.log(this.cellContent(this.indexes[x][y]).value) / Math.log(2)]++; } } } var sum = 0; for (var i=1; i<11; i++) { sum += valueCount[i] * Math.pow(2, i) + i; } return sum; } */ // check for win Grid.prototype.isWin = function() { var self = this; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (self.cellOccupied(this.indexes[x][y])) { if (self.cellContent(this.indexes[x][y]).value == 2048) { return true; } } } } return false; } //Grid.prototype.zobristTable = {} //for //Grid.prototype.hash = function() { //} 

2048.xlsm


L'application Excel elle-même peut être téléchargée depuis Google .

La fonctionnalité de l'application est décrite dans un article précédent, où l'IA joue en utilisant la méthode Monte Carlo . La solution d'aujourd'hui a été ajoutée au Monte Carlo existant.

Tous les articles des séries AI et 2048


  • Monte Carlo
  • Écrêtage Minimax + alpha beta
  • En attente du maximum
  • Réseau de neurones

Source: https://habr.com/ru/post/fr474680/


All Articles