AI和2048。第2部分:Minimax + Alpha Beta剪切



我们研究了蒙特卡洛方法 ,今天我们将看到使用旧的具有alpha-beta剪切功能的minimax在2048年如何发挥计算机思维。

EDISON软件-网络开发
本文是在EDISON(一家开发移动应用程序并提供软件测试服务的公司)的支持下撰写的。

用户堆栈溢出发现的解决方案 ovolve ,他在讨论中提到了如何教AI 2048游戏

来自ovolve的评论翻译
我是该线程中提到的程序的作者。 您可以查看正在运行的AI或代码

目前,通过在笔记本电脑上的浏览器中执行Java脚本,花费100毫秒的时间来考虑该过程,该程序在大约90%的情况下都可以胜任,尽管效果并不理想,但效果很好。

由于该游戏是一个具有完整信息的离散状态空间,实际上是象棋和跳棋之类的基于回合的游戏,因此我使用了在这些游戏中显示其性能的相同方法,即带有alpha-beta剪切的 minimax搜索 。 由于这些链接提供了有关此算法的大量信息,因此,我将仅讨论在静态估计函数中使用的两种主要启发式方法,并在这里对其他人所做的许多直观假设进行形式化。


单调


这种试探法试图确保所有图块值在左/右和上/下都增加或减少。 光是这种启发式方法就反映了许多其他人已经提到的更多猜想,即应该将更多有价值的磁贴分组在一个角落。 通常,这可以防止价值较低的瓷砖积聚,并使板块井井有条,因为较小的瓷砖会层叠成较大的瓷砖。

这是一个完全单调的网格的屏幕截图。 我通过运行带有已安装的eval函数的算法来解决这种情况,以便忽略其他启发式方法,并且仅考虑单调性。


平滑度(平滑度,均匀度)


上述启发式方法本身倾向于创建其中相邻单元的值减小的结构,但是,当然,邻居应该具有相同的含义来组合。 因此,平滑的启发式方法只是测量相邻图块之间的值差,以尽量减少它们的数量。

Hacker News的评论员从图论的角度对该思想进行了有趣的形式化

黑客新闻的形式化翻译
昨天我向喜欢图形理论的同事展示了这款游戏,我们还决定考虑如何使用AI解决这款游戏。

最简单的解决方案是minimax,如我所见,它实现得很好。 如果这里的人对minimax不熟悉,OP会编写非常优雅且受好评的代码,这将是一个很好的教程。

我们提出的计算强度较小的方法是以图形G(V,E)的形式模拟游戏状态,其中V是一组活动图块, E是一组连接相邻图块的边,这些图块的权重为c (v1,v2) ,它返回两个图块之间的差的绝对值。 对于每种解决方案,AI都会选择一个使新游戏状态下所有边的权重之和最小的动作。

原因是在游戏中取得进展的唯一方法是让彼此相邻的图块具有相同的值,对于这些图块, G中的权重为0。因此,AI应该尝试使总权重最小。 最后,在板上有大量的相邻边砖,这些边的权重很大,因此AI将尝试将这些砖与其他大砖保持相邻,以最大程度地减少差异。

由于游戏是随机的,因此我描述的方法在最坏的情况下可能不起作用,但它也可以作为树中每个节点的权重函数应用于现有的minimax解决方案。


这是一个完美光滑的网格的屏幕截图,由这个出色的模拟叉提供(链接到Web存档,而页面上的Java脚本有效,您可以使用键盘向任何方向移动-译者注意)。

松散的瓷砖


最后,由于可用空间太少会受到惩罚,因为当比赛场地变得狭窄时,选项可能很快结束。

仅此而已! 在优化这些条件的同时搜索游戏空间会产生令人惊讶的良好性能。 使用这种通用方法而不是显式编码的移动策略的好处之一是该算法通常可以找到有趣且出乎意料的解决方案。 如果您观察他的进步,他通常会做出惊人而有效的动作,例如墙壁或角落的突然变化,在此附近他可以建立自己的游戏。


零钱


屏幕截图展示了这种方法的强大功能。 我取消了图块限制(以便它们在达到2048后继续增长),这是八次测试后的最佳结果。

是的,这是4096和2048。=)这意味着他已经在一块板上达到了难以捉摸的2048磁贴。







本文下面给出了带有alpha-beta剪切和来自stackoverflow用户的静态评估功能的minimax的Java脚本代码。

minimax方法专用于一些优秀的habr文章,因此我们省略了对其组成的学术详细解释。 对于那些加入IT社区的人来说,我最近听到了漂亮的术语“ minimax”和“ alpha-beta裁剪”,但不知道这意味着什么,让我们尝试在几个段落中按字面意思解释最笼统的含义。

极小值


在某些游戏中,两个玩家(依次行动)之间的游戏过程可以表示为所谓的选择树。 在每个特定位置,每个玩家通常在其移动的不同选项之间进行选择。 并且,针对这些选项中的每一个,对手也可以在许多方面像对手。


选项树的片段

由于在游戏的任何时刻,都有有关比赛场地状态的完整信息,因此始终可以准确估算位置的当前状态。 这种功能称为静态评估功能或缩写SFO 。 而且,此功能在评估特定位置时越重要,一个玩家的位置就越有利(我们称其为最大化玩家 )。 评估位置时此函数的数值越小,第二个玩家的位置就越有利(我们称其为“ 最小化玩家” )。

每次移动后,位置都会改变,因此其得分也会改变。 考虑选项树时,每个玩家不仅需要选择评分最适合他的那些分支。 您还应该避免那些位置评价对对手有利的分支。

假定对手也受到理性主义的引导,并且避免了可能导致他失败的选择。 就是说,每个玩家在选择一个选项时,都会从最大化自己的利益而同时最小化对手的利润中获得收益。

这是极小值。

Alpha Beta裁剪


显而易见:从给定位置计算树到更大深度的人,他有更多获胜的机会。 但是有一个麻烦:游戏中的选择树有一个令人讨厌的习惯,即在每个嵌套级别上分支并呈指数增长。 程序的计数能力,甚至更多的人受到限制,计数“恰到好处”远非总是可能。 可以很容易地证明,玩家已经计数到一个可以对比赛场地进行良好评估的位置,但是从字面上看,在下一级别(不可读)上,对手有机会做出这样的举动,从而将位置估计值从根本上改变为相反方向。

谁该怪谁该怎么办? 复杂的树遍历应归咎于计算复杂性;建议通过切断不必要的分支来解决。 如果评估位置的玩家看到选项树的某个分支:

或比已经分析过的其他分支机构少赚钱,
或比已经分析过的其他分支机构更有利于对手,

玩家就放弃了这个分支,不浪费时间和资源去考虑这个明显更差的分支的子选项。

这使您可以分配更多计算资源,以便在选项树中分配更大的渲染深度来计算更有利的分支。 在评估选项树不同级别上的公平竞争环境的过程中,玩家使用两个动态变化的系数进行操作-alpha (分支中最少遇到的SFD值-即更有利于最小化玩家)和beta (分支中最多遇到的SFO值-更有利于玩家最大化)。 在每个级别上,将当前位置的SFD与alphabeta系数进行比较,就可以扫描(而不是完全计算出它们) 不利于评估位置和/或对手更有利的玩家的分支。

这是alpha beta裁剪。

具有alpha beta裁剪的递归minimax函数


带有AI的2048是作为带有VBA宏的Excel应用程序实现的,这就是带有alpha beta剪切的minimax算法如何看起来像卑鄙的视觉基础。
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' '''''''''''''''''''''''( - )''''''''''''''''''''''''' ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' '       -- '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 

用Java脚本编写Ovolve代码
 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]; } 

静态评估功能


由于在选项树的每个级别上您都必须评估比赛环境(为了确定对哪个球员而言,估计的位置实际上更有利),因此您需要确定哪种标准将好位置与坏位置区分开。

我们假设最大化玩家是决定要移动所有图块的4个方向(上,左,右,下)中的哪个(或AI)的人。 一个最小化的参与者是在最不合适的地方随机生成2或4的那个阴险的子例程。

SFO是从最大化参与者的角度进行编译的。 比赛场地的SFD评分越高,“极简主义者”的位置就越好。 越低-“极简主义者”在董事会上的职位越愉快。

在2048的情况下-移动瓷砖的人认为哪些因素有利?

单调


首先,期望在一些方向上以升/降顺序布置瓦片。 如果不这样做,那么当生成新的图块时,运动场将很快被不同大小的随机排列的图块阻塞,这些图块无法立即正常地相互连接。

在西伯利亚联邦区,您需要在所有四个方向上(从上至下,从左至右,从右至左,自下而上)进行查看,并计算图块在哪里递减或递增。 如果在进行中存在不适合一般序列的图块,则这会降低单调的数值系数。 然后,从所有方向的4个系数中,选择最佳的一个,并考虑到西伯利亚联邦区的总价值。

光滑度


此外,更理想的是,从排成一排的瓷砖开始的进度不仅增加,而且不减少(或者最好减少而不是减少排的数量,而不是不增加),也就是说,当同一块瓷砖在附近时,允许它们塌陷成一个块,获得点数并增加增加运动场上的可用空间。

因此,西伯利亚联邦区正在运动场上寻找相同的相邻砖块,并以特殊系数考虑了此类对的数量。

空单元格


显然,自由空间越多,机动性就越大,快速失去的可能性就越小。

SFO会考虑场地上的空单元,而其中的空单元越多,该位置对于最大化玩家来说就越有利可图。

最大瓦数


由于此游戏的主要目的是在场地上获得较大的磁贴,所以越好-2048、4096、8192(或您有实力和耐心的任何东西),因此应将最大磁贴值越大的选项视为最赚钱的SFD。

西伯利亚联邦区2048


将西伯利亚联邦区作为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 

用Java脚本编写Ovolve代码
 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


Excel应用程序本身可以从Google下载

在上一篇文章中描述了该应用程序的功能,其中AI使用Monte Carlo方法播放 。 今天的解决方案已添加到现有的蒙特卡洛中。

AI和2048系列的所有文章


  • 蒙特卡洛
  • Minimax + Alpha Beta裁剪
  • 等待最大
  • 神经网络

Source: https://habr.com/ru/post/zh-CN474680/


All Articles