我最近偶然发现了2048游戏。通过在四个方向中的任何一个方向上移动类似的平铺,可以合并它们,以生成“更大”的平铺。每次移动后,一个新的平铺显示在随机的空位置,值为2或4。当所有的方块都被填满并且没有可以合并平铺的移动,或者你创建了一个值为2048的平铺时,游戏终止。

首先,我需要遵循一个明确的战略来实现目标。所以,我想为它写一个程序。

我当前的算法:

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
}

我所做的是,在任何时候,我都会尝试将值为2和4的平铺合并,也就是说,我尝试尽可能少地使用2和4个平铺。如果我这样做,所有其他平铺都会自动合并,策略似乎很好。

但是,当我实际使用这个算法时,在游戏结束之前,我只得到了大约4000分。AFAIK的最高分数略高于20000分,这比我目前的分数要大得多。有比上述更好的算法吗?


当前回答

许多其他答案使用人工智能,对可能的未来、启发式、学习等进行计算成本高昂的搜索。这些令人印象深刻,可能是正确的前进方向,但我想提出另一个想法。

模拟游戏中优秀玩家使用的策略。

例如:

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

按照上面显示的顺序读取正方形,直到下一个正方形值大于当前值。这就带来了试图将另一个具有相同值的平铺合并到此方形中的问题。

为了解决这个问题,他们有两种移动方式,没有留下或更糟,检查这两种可能性可能会立即发现更多问题,这形成了一个依赖关系列表,每个问题都需要先解决另一个问题。我认为我在决定下一步行动时,特别是在被卡住的时候,会有一条链条,或者在某些情况下,是内部的依赖树。


瓷砖需要与邻居合并,但太小:将另一个邻居与此邻居合并。

较大的平铺:增加较小的周围平铺的值。


整个方法可能比这更复杂,但并不复杂。这可能是一种机械的感觉,缺乏分数、体重、神经和对可能性的深入探索。可能性之树甚至需要足够大,完全需要分支。

其他回答

这不是对OP问题的直接回答,这是我迄今为止试图解决同一问题的更多东西(实验),并获得了一些结果和一些我想分享的观察结果,我很好奇我们能否从中获得一些进一步的见解。

我刚刚尝试了使用alpha beta修剪的minimax实现,搜索树深度截止值为3和5。我试图解决4x4网格的相同问题,作为edX课程ColumbiaX:CSMM101x人工智能(AI)的项目作业。

我应用了两个启发式评估函数的凸组合(尝试了不同的启发式权重),主要来自直觉和上面讨论的函数:

单调性可用的可用空间

在我的情况下,电脑玩家是完全随机的,但我仍然假设了对抗性设置,并将AI玩家代理实现为最大玩家。

我有4x4网格来玩游戏。

观察结果:

如果我给第一个启发式函数或第二个启发式函数分配了太多权重,那么AI玩家获得的分数都很低。我对启发式函数进行了许多可能的权重分配,并采用了凸组合,但很少有AI玩家能够得分2048。大多数时候,它要么停在1024或512。

我也尝试过拐角启发式,但出于某种原因,它会使结果更糟,凭直觉为什么?

此外,我尝试将搜索深度截止值从3增加到5(我不能再增加了,因为即使在修剪的情况下,搜索该空间也超过了允许的时间),并添加了一个启发式方法,它查看相邻平铺的值,如果它们可以合并,则会给出更多的点,但我仍然无法获得2048。

我认为使用Expectimax而不是minimax会更好,但我仍然希望只使用minimax来解决这个问题,并获得2048或4096等高分。我不确定我是否遗漏了什么。

以下动画显示了AI代理与计算机玩家玩游戏的最后几个步骤:

任何见解都将非常有用,提前感谢。(这是我博客文章的链接:https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve-2048-game-with-computer/以及youtube视频:https://www.youtube.com/watch?v=VnVFilfZ0r4)

以下动画显示了游戏的最后几个步骤,其中AI玩家代理可以获得2048分,这一次还添加了绝对值启发式:

下图显示了玩家AI代理探索的游戏树,假设计算机是对手,只需一步:

我使用expectimax优化开发了2048 AI,而不是@ovolve算法使用的最小值搜索。AI简单地对所有可能的移动执行最大化,然后对所有可能瓦片产生进行期望(由瓦片的概率加权,即,4个瓦片为10%,2个瓦片为90%)。据我所知,不可能删减expectimax优化(除了删除极不可能的分支),因此所使用的算法是经过仔细优化的暴力搜索。

表演

AI在其默认配置(最大搜索深度为8)中执行移动需要10毫秒到200毫秒,具体取决于板位置的复杂性。在测试中,AI在整个游戏过程中实现了每秒5-10次的平均移动速度。如果搜索深度被限制在6次移动,AI可以轻松地每秒执行20次以上的移动,这使得观看更加有趣。

为了评估AI的得分表现,我运行了100次AI(通过远程控制连接到浏览器游戏)。对于每个平铺,以下是该平铺至少实现一次的游戏比例:

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

所有跑步的最低得分为124024分;最高得分为794076分。平均得分为387222。AI从未未能获得2048个区块(因此它从未在100场游戏中输掉过一次游戏);事实上,它在每次运行中至少实现一次8192平铺!

以下是最佳跑步记录的截图:

这场比赛在96分钟内进行了27830次移动,即平均每秒4.8次移动。

实施

我的方法将整个电路板(16个条目)编码为单个64位整数(其中瓦片是nybbles,即4位块)。在64位机器上,这使得整个电路板可以在单个机器寄存器中传递。

位移位操作用于提取单独的行和列。单个行或列是16位的量,因此大小为65536的表可以对在单个行或行上操作的转换进行编码。例如,移动被实现为预计算的“移动效果表”中的4个查找,该表描述了每次移动如何影响单个行或列(例如,“向右移动”表包含条目“1122->0023”,描述了当向右移动时,行[2,2,4,4]如何变为行[0,0,4,8])。

评分也使用表格查找来完成。这些表包含对所有可能的行/列计算的启发式得分,一个板的最终得分只是每行和每列的表值之和。

这种棋盘表示,以及移动和得分的表格查找方法,允许AI在短时间内搜索大量游戏状态(在我2011年中期笔记本电脑的一个核心上,每秒超过10000000个游戏状态)。

expectimax搜索本身被编码为递归搜索,它在“期望”步骤(测试所有可能的平铺生成位置和值,并根据每个可能性的概率加权其优化分数)和“最大化”步骤(检测所有可能的移动并选择具有最佳分数的移动)之间交替。当树搜索看到之前看到的位置(使用换位表)、达到预定义的深度限制或达到极不可能达到的板状态时(例如,通过从起始位置开始一行获得6“4”块而达到),树搜索终止。典型的搜索深度为4-8次移动。

启发式

使用几种启发式方法将优化算法引向有利位置。启发式算法的精确选择对算法的性能有着巨大的影响。各种启发式算法被加权并组合成一个位置得分,这决定了给定的董事会位置有多“好”。然后,优化搜索将旨在最大化所有可能董事会位置的平均得分。如游戏所示,实际得分不用于计算棋盘得分,因为它的权重太大,有利于合并瓦片(当延迟合并可能产生很大的好处时)。

最初,我使用了两种非常简单的启发式方法,即为开放正方形和边缘值较大的正方形授予“奖金”。这些启发式算法表现得很好,经常达到16384,但从未达到32768。

Petr Morávek(@xivicurk)使用了我的AI,并添加了两种新的启发式方法。第一个启发式是对非单调行和列的惩罚,这些行和列随着排名的增加而增加,从而确保小数字的非单调行不会强烈影响分数,但大数字的非非单调行会严重影响分数。第二个启发式算法除了计算开放空间之外,还计算了潜在合并(相邻的相等值)的数量。这两种启发式方法用于将算法推向单调板(更容易合并),以及大量合并的板位置(鼓励其在可能的情况下对齐合并以获得更大的效果)。

此外,Petr还使用“元优化”策略(使用称为CMA-ES的算法)优化了启发式权重,其中权重本身被调整以获得可能的最高平均分数。

这些变化的影响极其显著。该算法在大约13%的时间内实现了16384个瓦片,在90%的时间内完成了它,并且该算法在1/3的时间内开始实现32768个瓦片(而旧的启发式算法从未产生过32768个)。

我相信启发式方法还有改进的空间。这个算法肯定还不是“最佳”的,但我觉得它已经接近了。


人工智能在超过三分之一的游戏中获得32768分,这是一个巨大的里程碑;我会很惊讶地听到是否有人类玩家在官方游戏中达到了32768(即不使用保存状态或撤销等工具)。我认为65536瓷砖触手可及!

你可以自己尝试人工智能。该代码位于https://github.com/nneonneo/2048-ai.

编辑:这是一个天真的算法,模拟人类有意识的思维过程,与搜索所有可能性的人工智能相比,它的结果非常微弱,因为它只向前看一块砖。它是在答复时间表的早期提交的。

我改进了算法,打败了游戏!它可能会因为临近结束时的简单厄运而失败(你被迫向下移动,这是你永远不应该做的,并且在你最高的位置会出现一个瓦片。只需保持最上面的一行填满,这样向左移动不会打破模式),但基本上你最终有一个固定的部分和一个移动的部分可以玩。这是您的目标:

这是我默认选择的模型。

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

所选的角是任意的,你基本上不会按一个键(禁止的移动),如果按了,你会再次按相反的键并尝试修复它。对于未来的平铺,模型总是希望下一个随机平铺为2,并出现在当前模型的相反侧(当第一行不完整时,在右下角,第一行完成后,在左下角)。

算法来了。大约80%的人获胜(似乎总是可以用更“专业”的人工智能技术获胜,但我对此并不确定。)

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

关于缺失步骤的几点提示。在这里:

由于运气更接近预期模型,模型发生了变化。人工智能试图实现的模型是

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

实现这一目标的链条变成了:

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

O代表禁区。。。

因此,它将向右,然后再向右,然后(向右或向右,取决于4创建的位置),然后继续完成链,直到它得到:

因此,现在模型和链又回到了:

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

第二个指针,它运气不好,它的主要位置已经被占据。它很可能会失败,但仍能实现:

这里的模型和链是:

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

当它设法达到128时,它将再次获得一整行:

  O 1024 512 256
  x   x  128 128
  x   x   x   x
  x   x   x   x

我用Haskell编写了一个2048解算器,主要是因为我现在正在学习这种语言。

我的游戏实现与实际游戏略有不同,因为新的平铺始终是“2”(而不是90%2和10%4)。而且,新的平铺不是随机的,而是始终是左上角第一个可用的平铺。该变体也称为Det 2048。

因此,此解算器是确定性的。

我使用了一种支持空瓷砖的穷举算法。它在深度1-4时表现得很快,但在深度5时,每次移动大约1秒就会变得很慢。

下面是实现求解算法的代码。网格表示为16长度的整数数组。得分是通过计算空方块的数量来完成的。

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

我认为它很简单,很成功。当从空网格开始并在深度5处求解时,其结果为:

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

Game Over

源代码可在此处找到:https://github.com/popovitsj/2048-haskell

我对这款游戏的人工智能的想法产生了兴趣,它不包含硬编码的智能(即没有启发式、评分功能等)。人工智能应该只“知道”游戏规则,并“弄清楚”游戏玩法。这与大多数AI(如本线程中的AI)形成对比,在这些AI中,游戏玩法基本上是由代表人类对游戏理解的评分函数控制的暴力。

AI算法

我发现了一个简单但令人惊讶的好游戏算法:为了确定给定棋盘的下一步,AI使用随机移动在内存中玩游戏,直到游戏结束。这是在跟踪最终比赛分数的同时进行的几次。然后计算每次开始移动的平均结束得分。平均结束得分最高的起始动作被选为下一个动作。

每次移动仅运行100次(即内存游戏),AI可实现2048次平铺80%的次数和4096次平铺50%的次数。使用10000次运行可获得2048个平铺100%,4096个平铺70%,8192个平铺约1%。

在行动中看到它

最佳成绩如下:

关于这个算法的一个有趣的事实是,尽管随机游戏毫无疑问非常糟糕,但选择最佳(或最不糟糕)的招式会带来非常好的游戏效果:一个典型的人工智能游戏可以达到70000点,并持续3000步,但任何给定位置的记忆中随机游戏在死亡前都会在大约40次额外的招式中平均增加340点。(您可以通过运行AI并打开调试控制台自行查看。)

这张图说明了这一点:蓝线显示了每次移动后的棋盘得分。红线显示了该位置的算法的最佳随机运行结束游戏分数。本质上,红色值是将蓝色值向上拉向它们,因为它们是算法的最佳猜测。有趣的是,在每一点上,红线都比蓝线略高一点,但蓝线仍在不断增加。

我觉得很奇怪的是,算法不需要实际预测好的游戏玩法,就可以选择产生它的动作。

后来搜索发现,这个算法可能被归类为纯蒙特卡罗树搜索算法。

实施和链接

首先,我创建了一个JavaScript版本,可以在这里看到。这个版本可以在适当的时间内运行100次。打开控制台获取更多信息。(来源)

后来,为了玩更多,我使用了@nneonneo高度优化的基础设施,并用C++实现了我的版本。这个版本允许每次移动最多100000次,如果你有耐心的话,甚至可以达到1000000次。提供建筑说明。它在控制台中运行,也有一个遥控器来播放网络版本。(来源)

后果

令人惊讶的是,增加跑步次数并不能显著改善比赛。这一策略似乎有一个限制,即4096个区块和所有较小的区块在80000点左右,非常接近8192个区块。将跑步次数从100次增加到100000次会增加达到这一分数限制(从5%增加到40%)但无法突破的几率。

在关键位置临时增加到1000000次的10000次跑步打破了这一障碍,达到129892分的最高得分和8192分的次数不到1%。

改进

在实现这个算法后,我尝试了许多改进,包括使用最小或最大分数,或最小、最大和平均值的组合。我还尝试了使用深度:我没有尝试每次移动K次,而是尝试了给定长度的每次移动列表(例如“向上、向上、向左”)的K次移动,并选择最佳得分移动列表的第一个移动。

后来我实现了一个得分树,它考虑了在给定的移动列表之后能够进行移动的条件概率。

然而,这些想法都没有比简单的第一个想法显示出任何真正的优势。我将这些想法的代码注释在C++代码中。

我确实添加了一个“深度搜索”机制,当任何一次运行意外达到下一个最高的平铺时,该机制将运行次数临时增加到1000000次。这提供了时间上的改进。

我很想知道是否有人有其他改进想法来保持人工智能的领域独立性。

2048个变体和克隆

为了好玩,我还将AI实现为书签,与游戏的控件挂钩。这使得AI可以与原始游戏及其许多变体一起工作。

这是可能的,因为AI的领域独立性。一些变体非常独特,例如六边形克隆。