2048游戏的最佳算法是什么?


1919

我最近偶然发现了2048游戏。您可以通过在四个方向上任意移动相似的图块来合并它们,以制作“更大”的图块。每次移动后,新的图块都会出现在随机的空白位置,其值为24。当所有方框都已填满且没有可合并磁贴的移动,或者您创建的值为时,游戏终止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
}

我做的是在任何时候,我会尽量与值合并瓷砖24,就是我努力24瓷砖,尽可能最小。如果以这种方式尝试,所有其他磁贴将自动合并,并且该策略看起来不错。

但是,当我实际使用此算法时,在游戏终止前我只能得到4000点。最高分数AFAIK略高于20,000点,这比我目前的分数还大。是否有比以上更好的算法?


84
这可能有帮助!ov3y.github.io/2048-AI
cegprakash 2014年

5
顺便说一句@ nitish712,您的算法很贪婪,因为您choose the move with large number of merges很快就会导致局部最优
Khaled.K 2014年

21
@ 500-InternalServerError:如果要使用alpha-beta游戏树修剪来实现AI,那么将假定新的块已被敌对放置。这是最坏的假设,但可能有用。
2014年

6
当您没有时间争取高分时,这会很有趣,分散您的注意力:尝试获得最低分。从理论上讲,它是交替的2s和4s。
马克·赫德2014年

7
讨论这个问题的合法性可以在元找到:meta.stackexchange.com/questions/227266/...
吉荣Vannevel

Answers:


1266

我使用Expectimax优化开发了2048 AI ,而不是@ovolve算法使用的minimax搜索。AI会简单地对所有可能的移动执行最大化,然后对所有可能的图块生成进行期望(通过图块的概率加权,即4的概率为10%,2的概率为90%)。据我所知,不可能修剪Expectimax优化(除去删除极不可能的分支),因此使用的算法是经过仔细优化的蛮力搜索。

性能

AI的默认配置(最大搜索深度为8)从10ms到200ms的任何时间执行一次移动,具体取决于电路板位置的复杂程度。在测试中,AI在整个游戏过程中的平均移动速率为每秒5-10次移动。如果搜索深度限制为6个动作,则AI可以轻松地每秒执行20个以上的动作,这使您可以进行一些有趣的观看

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

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

最低分数为124024;AI的最高得分是794076。中位数是387222。实际上,它在每次运行中至少获得了8192个磁贴!

这是最佳运行的屏幕截图:

32768瓷砖,得分794076

该游戏在96分钟内进行了27830次移动,或平均每秒4.8次移动。

实作

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

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

还可以使用表查找来进行计分。这些表包含在所有可能的行/列上计算的启发式分数,并且一块木板的结果分数就是每个行和列中表值的总和。

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

Expectimax搜索本身被编码为递归搜索,它在“期望”步骤(测试所有可能的瓦片生成位置和值,并通过每种可能性的概率加权其优化得分)和“最大化”步骤(测试所有可能的移动)之间交替并选择得分最高的那个)。当树搜索看到先前看到的位置(使用转置表),达到预定义的深度限制或达到极不可能的板状态(例如,通过获取6个“ 4”图块达到该状态)时,树搜索终止从起始位置连续)。典型的搜索深度是4-8步。

启发式

几种启发式方法用于将优化算法引向有利位置。启发式算法的精确选择对算法的性能有很大的影响。权衡各种启发式方法并将其组合到位置分数中,该分数确定给定板位置的“好”程度。然后,优化搜索将旨在使所有可能的董事会职位的平均分数最大化。如游戏所示,实际得分用于计算棋盘得分,因为它过于偏重而无法合并板块(延迟合并可能产生巨大收益)。

最初,我使用了两种非常简单的启发式方法,分别为开放正方形和在边缘具有较大值的对象授予“奖励”。这些启发式方法的效果非常好,经常达到16384,但从未达到32768。

PetrMorávek(@xificurk)采用了我的AI,并添加了两个新的启发式方法。第一种启发式方法是对具有非单调的行和列的行列进行惩罚,该行和列随等级的增加而增加,以确保数量较少的非单调行不会严重影响得分,但数量较大的非单调行将严重损害分数。第二种启发式方法计算了除开放空间之外的潜在合并数(相邻的相等值)。这两种启发式算法将算法推向单调板(更易于合并),并推向具有大量合并的板位置(鼓励它在可能的情况下对齐合并以产生更大的效果)。

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

这些变化的影响极为显着。该算法从大约13%的时间达到16384瓦片变为90%的时间达到了该算法,并且算法在1/3的时间中开始达到32768(而旧的启发式算法从未产生过32768瓦片) 。

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


人工智能在超过三分之一的游戏中获得了32768瓦,这是一个巨大的里程碑。听到有人在正式游戏中获得32768的成绩(即未使用savestates或undo等工具),我会感到惊讶。我认为65536磁贴触手可及!

您可以自己尝试AI。该代码可从https://github.com/nneonneo/2048-ai获得


12
@RobL:2的出现率90%;4的出现时间为10%。在源代码中var value = Math.random() < 0.9 ? 2 : 4;
nneonneo 2014年

35
目前正在移植到Cuda,因此GPU可以更快地工作!
nimsson 2014年

25
@nneonneo我将带有emscripten的代码移植到了javascript,现在在浏览器中效果很好!观看时很酷,不需要编译等等……在Firefox中,性能相当不错……
reverse_engineer

6
4x4网格中的理论极限实际上是131072,而不是65536。但是,这需要在正确的时刻获得4(即,整个电路板每次填充4 .. 65536一次-占用15个场),并且必须在该位置设置电路板时刻,以便您实际上可以合并。
博多·蒂森

5
@nneonneo您可能想检查一下我们的AI,这似乎更好,在60%的游戏中达到32k:github.com/aszczepanski/2048
cauchy

1253

我是该线程中其他人提到的AI程序的作者。您可以在中查看AI行动或阅读

目前,考虑到每次移动大约100毫秒的思考时间,该程序在我的笔记本电脑上的浏览器中的javascript中运行时,可以达到大约90%的获胜率,因此虽然效果不理想(但!),但它的表现还不错。

由于该游戏是离散的状态空间,完美的信息,基于回合的游戏(如国际象棋和西洋跳棋),因此我使用了已被证明可用于这些游戏的相同方法,即带有alpha-beta修剪的minimax 搜索。由于已经有很多关于该算法的信息,因此,我将仅讨论在静态评估函数中使用的两种主要启发式方法,这些启发式方法将其他人在这里表达的许多直觉形式化。

单调性

这种试探法试图确保图块的值都沿着左/右和上/下方向都增加或减少。仅凭这种启发式方法就可以捕捉许多其他人提到的直觉,即更高价值的瓷砖应聚集成一角。通常,它将防止较小价值的瓷砖变得孤立,并使板块井井有条,较小的瓷砖会层叠并填充到较大的瓷砖中。

这是一个完美单调网格的屏幕截图。我是通过运行带有eval函数集的算法来忽略其他启发式算法而仅考虑单调性而获得的。

完美单调的2048板

光滑度

仅上述启发式方法趋向于创建其中相邻区块的值减小的结构,但是当然为了合并,相邻区块需要具有相同的值。因此,平滑度启发式方法只是测量相邻图块之间的值差,以尽量减少此计数。

关于Hacker News的评论者从图论的角度对该想法进行了有趣的形式化

这是一个完美平滑的网格的屏幕截图,这要归功于出色的模仿叉

完美光滑的2048板

免费瓷砖

最后,免费磁贴太少会受到惩罚,因为当游戏板太狭窄时,选项会很快用完。

就是这样!在优化这些条件的同时搜索游戏空间会产生非常好的性能。使用这样的通用方法而不是显式编码的移动策略的一个优点是该算法通常可以找到有趣且出乎意料的解决方案。如果您观察它的运行,它通常会做出令人惊讶但有效的举动,例如突然切换它要面对的墙或角。

编辑:

这是这种方法的强大功能的演示。我取消了图块值的上限(因此在达到2048后继续保持不变),这是八次试验后的最佳结果。

4096

是的,这是4096和2048的乘积。=)这意味着它在同一块板上完成了3倍难以捉摸的2048瓦片。


89
您可以将放置“ 2”和“ 4”图块的计算机视为“对手”。
魏延

29
@WeiYen当然可以,但是将其视为minmax问题并不忠实于游戏逻辑,因为计算机会以一定概率随机放置图块,而不是有意地将得分降低到最小。
koo 2014年

57
即使AI随机放置磁贴,目标也不会丢失。运气不佳与对手选择最差的举动是一样的。“最小”部分表示您要尝试保守地进行游戏,以免出现可能会倒霉的可怕举动。
FryGuy 2014年

196
我有一个想法,创建一个2048的派生叉,在该计算机中,计算机不是随机放置2s和4s,而是使用您的AI来确定将值放置在何处。结果是:绝对不可能。可以在这里尝试:sztupy.github.io/2048-Hard
SztupY 2014年

30
@SztupY哇,这很邪恶。让我想起了qntm.org/hatetris Hatetris,它也尝试放置可以最大程度改善您的状况的作品。
Patashu 2014年

145

我对此游戏的AI构想产生了兴趣,其中包含硬编码的智能(即,没有启发式,计分功能等)。AI应该只“知道”游戏规则,然后“确定”游戏玩法。这与大多数AI(如该线程中的AI)形成鲜明对比,在这些AI中,游戏玩法本质上是由代表人类对游戏理解的计分功能控制的。

人工智能算法

我发现了一个简单但出乎意料的出色玩法:要确定给定棋盘的下一招,AI会使用随机招数在内存中玩游戏,直到游戏结束。在跟踪最终比赛分数的同时,完成了几次。然后是每个起跑动作的平均终点得分计算。选择平均终点得分最高的起点作为下一步。

每步仅进行100次运行(即在记忆游戏中),AI可以达到80%的2048瓦片和50%的4096瓦片。使用10000次运行可获得2048个图块的2048个图块,4096个图块的70%和8192个图块的约1%。

实际观看

最佳成绩如下所示:

最佳得分

关于此算法的一个有趣的事实是,尽管毫无疑问,随机游戏非常糟糕,但选择最佳(或最差)的举动却可以带来非常好的游戏玩法:典型的AI游戏可以达到70000点,最后可以移动3000次,但是在死之前,来自任何给定位置的内存中随机播放游戏平均会在大约40个额外动作中平均产生340个额外点。(您可以通过运行AI并打开调试控制台来亲自查看。)

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

得分图

我感到非常惊讶的是,该算法实际上不需要预测良好的游戏玩法来选择产生它的动作。

稍后搜索,我发现此算法可能被归类为“ 纯蒙特卡洛树搜索”算法。

实施和链接

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

后来,为了玩得更多,我使用了@nneonneo高度优化的基础结构,并使用C ++实现了我的版本。如果您有足够的耐心,此版本最多可允许单次运行100000次,甚至100万次。提供了建筑说明。它在控制台中运行,还具有播放网络版本的遥控器。(来源

结果

令人惊讶的是,增加运行次数并不能大大改善游戏玩法。对于4096个图块和所有较小的图块,此策略似乎在80000点附近存在限制,非常接近实现8192个图块。将运行次数从100增加到100000会增加赔率达到此分数限制(从5%到40%)但没有突破。

奔跑10000次,在关键位置附近暂时增加到1000000次,成功突破这一障碍的时间不到1%,达到了最高分129892和8192磁贴。

改进措施

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

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

但是,与简单的第一个想法相比,这些想法都没有显示出任何真正的优势。我将这些想法留在代码中,以在C ++代码中注释掉。

我确实添加了一个“深度搜索”机制,当任何运行设法意外到达下一个最高磁贴时,运行次数会暂时增加到1000000。这提供了时间上的改进。

我很想听听是否有人还有其他可以维持AI领域独立性的改进想法。

2048个变体和克隆

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

由于AI的域独立性质,这是可能的。一些变体非常不同,例如六角形克隆。


7
+1。作为一名AI学生,我发现这真的很有趣。在空闲时间会对此有更好的了解。
以撒

4
这真太了不起了!我只花了几个小时来优化权重,以实现预期最大的启发式功能,并且在3分钟内实现了这一点,这完全粉碎了它。
布伦丹·安纳布尔

8
很好地使用了蒙特卡洛模拟。
nneonneo 2014年

5
观看这场比赛需要启发。这打击了所有的启发式方法,但是仍然有效。恭喜你!
斯特凡纳·古里科

4
到目前为止,这里是最有趣的解决方案。
shebaw

126

编辑:这是一种幼稚的算法,可对人类意识的思维过程进行建模,与AI相比,它搜索所有可能性的结果非常微弱,因为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)
  }

缺少步骤的一些提示。这里:型号变更

由于更接近预期模型,所以模型已更改。AI试图实现的模型是

 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

execute move with best score您如何从可能的下一个状态中评估最佳分数?
Khaled.K 2014年

启发式是在evaluateResult您定义的,基本上是尝试尽可能接近最佳方案。
Daren 2014年

@Daren我正在等待您的详细说明
ashu 2014年

@ashu我正在研究它,突发情况使我没有时间完成它。同时,我对算法进行了改进,现在可以解决75%的时间。
Daren

13
我真正喜欢这种策略的地方是,我可以在手动玩游戏时使用它,它使我获得了37,000分。
Cephalopod 2014年

94

我在这里复制博客文章的内容


我提出的解决方案非常简单且易于实现。虽然,它已经达到131040的分数。提出了算法性能的几个基准。

得分了

算法

启发式评分算法

我的算法所基于的假设非常简单:如果您想获得更高的分数,则必须尽可能保持董事会整洁。特别地,最佳设置是由瓦片值的线性和单调递减顺序给出的。这种直觉还将为您提供图块值的上限:s其中n是板上的图块数。

(如果在需要时随机生成4区块而不是2区块,则有可能到达131072磁贴)

下图显示了两种组织董事会的可能方式:

在此处输入图片说明

为了以单调递减的顺序执行瓦片的排序,将分数si计算为板上线性化值的总和乘以公共比率r <1的几何序列的值。

s

s

可以一次评估几个线性路径,最终分数将是任何路径的最大分数。

决策规则

实现的决策规则不是很聪明,Python的代码如下所示:

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

minmax或Expectiminimax的实现肯定会改进算法。显然,更复杂的决策规则会使算法变慢,并且需要一些时间来实现。我将在不久的将来尝试minimax实现。(敬请关注)

基准测试

  • T1-121测试-8条不同的路径-r = 0.125
  • T2-122个测试-8条不同的路径-r = 0.25
  • T3-132个测试-8条不同的路径-r = 0.5
  • T4-211测试-2条不同的路径-r = 0.125
  • T5-274个测试-2条不同的路径-r = 0.25
  • T6-211测试-2条不同的路径-r = 0.5

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明

在T2的情况下,十分之四的测试会生成4096个图块,平均得分为 s 42000

可以在以下链接的GiHub上找到该代码:https : //github.com/Nicola17/term2048-AI 它基于term2048并用Python编写。我将尽快在C ++中实现一个更有效的版本。


不错,您的插图使我有了一个想法,可以对合并向量进行评估
Khaled.K 2014年

你好。您确定github页面中提供的说明适用于您的项目吗?我想尝试一下,但这些似乎只是原始可玩游戏的说明,而不是AI自动运行的说明。你能更新那些吗?谢谢。
JD Gamboa

41

我的尝试像上面的其他解决方案一样使用Expectimax,但是没有位板。Nneonneo的解决方案可以检查1000万次移动,大约剩下4个图块,深度为4,可以进行4个移动(2 * 6 * 4) 4。就我而言,这个深度需要花费太长时间来探索,我会根据剩余的可用磁贴数量来调整Expectimax搜索的深度:

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

木板的分数是通过以下方法计算的:免费瓷砖数的平方和2D网格的点积的加权和:

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

迫使从左上角的瓷砖以某种蛇形的降序排列瓷砖。

下面或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>


3
不知道为什么没有更多的投票。它的简单性非常有效。
David Greydanus

谢谢,迟来的回答,它的效果不是很好(几乎总是在[
1024,8192

您如何加权空白空间?
David Greydanus 2015年

1
很简单cost=1x(number of empty tiles)²+1xdotproduct(snakeWeights,grid),我们尝试使这一成本最大化
caub 2015年

谢谢@Robusto,我有一天应该改进代码,可以简化
一下

38

我是2048控制器的作者,该控制器的得分比该线程中提到的任何其他程序都要好。可以在github上找到控制器的有效实现。在单独的仓库中,还存在用于训练控制器状态评估功能的代码。本文介绍了这种训练方法。

控制器使用带有状态评估功能的Expectimax搜索,该状态评估功能是通过时间差异学习(一种强化学习技术)的变种从零开始学习的(没有2048人的专业知识)。状态值函数使用n元组网络,基本上是板上观察到的图案的加权线性函数。它涉及超过十亿重量总共。

性能

每秒移动1次:609104(平均100场比赛)

10次​​移动/秒:589355(平均300场比赛)

3层时(约1500次/ s):511759(平均每1000场比赛)

10个动作/秒的磁贴统计信息如下:

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

(最后一行表示在板上同时具有给定的图块)。

对于三层:

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

但是,我从未观察到它能获得65536瓦。


4
令人印象深刻的结果。但是,您是否可以更新答案以进行解释(大概,用简单的术语……我确定完整的细节将太长,无法在此处发布)您的程序是如何实现这一目标的?如粗略解释学习算法如何工作?
Cedric Mamo

27

我认为我找到了一种效果很好的算法,因为我经常达到10000以上的得分,我个人的最高得分约为16000。我的解决方案并不是要把最大的数字放在角落,而是要保持在第一行。

请参见下面的代码:

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

5
我运行了100,000个游戏,分别测试了这种微不足道的循环策略“上,右,上,左,...”(如果需要,则下调)。循环策略得出的“平均平铺得分”为770.6,而这个平均得分为396.7。您猜为什么会这样吗?我想这会造成太多麻烦,即使左右合并更多。
Thomas Ahle 2014年

1
如果磁贴没有在多个方向上移动,它们往往会以不兼容的方式堆叠。通常,使用循环策略会导致中心区域变大,从而使操作更加局促。
bcdan

25

已经有一个AI实现这个游戏在这里。自述摘录:

该算法是迭代加深深度优先的alpha-beta搜索。评估功能尝试使行和列保持单调(全部减少或增加),同时最小化网格上的图块数量。

Hacker News上也有关于此算法的讨论,您可能会发现它很有用。


4
这应该是最多的回答,但是这将是很好添加有关实施的详细信息:如游戏板建模方式(如图表),优化使用(瓦之间最小-最大的差异)等
Alceu科斯塔

1
对于将来的读者:这是由其作者(ovolve)在此处第二高的答案中解释的同一程序。这个答案以及讨论中对ovolve程序的其他提及,促使ovolve出现并写下了他的算法如何工作。这个问题的答案现在的得分为1200
MultiplyByZer0

23

算法

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

    choose the maximum evaluation
}

评价

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)

评估细节

128 (Constant)

这是一个常数,用作基线以及用于测试等其他用途。

+ (Number of Spaces x 128)

更多的空间使状态更灵活,我们将其乘以128(这是中位数),因为填充有128个面的网格是最佳不可能状态。

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

在这里,我们通过向后评估来评估可能合并的面,图块2的值为2048,而图块2048的值为2。

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

在这里,我们仍然需要检查堆叠的值,但是以一种不影响灵活性参数的较小方式,因此我们得到了[4,44]中x的总和。

+ (Number of possible next moves x 256)

如果一个状态具有更多可能的过渡自由度,则它会更加灵活。

+ (Number of aligned values x 2)

这是对在该状态内进行合并的可能性的简化检查,而无需提前进行。

注意:可以调整常量。


2
我将在稍后进行编辑,以添加实时代码@ nitish712
Khaled.K,2014年

9
该算法的胜率是多少?
cegprakash 2014年

为什么需要一个constant?如果您正在做的只是比较分数,那将如何影响这些比较的结果?
bcdan

@bcdan启发式(也称为比较分数)取决于对未来状态的期望值的比较,类似于国际象棋启发式的工作方式,只是这是线性启发式,因为我们没有建立树来知道接下来的N次最佳移动
Khaled.K 2015年

12

这不是对OP问题的直接答案,这是我到目前为止为解决同一问题而尝试过的更多东西(实验),并获得了一些结果并想分享一些看法,我很好奇我们是否可以提出一些建议。进一步的见解。

我刚刚尝试了在3和5时使用带有搜索树深度截止的alpha-beta修剪的minimax实现。我试图解决与edX课程ColumbiaX的项目分配相同的4x4网格问题:CSMM.101x人工智能( AI)

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

  1. 单调性
  2. 可用空间

就我而言,计算机播放器是完全随机的,但我仍然采用对抗设置并将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代理探索的游戏树,假设计算机只是一个步骤,它就是对手:

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明


9

我在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


尝试用实际规则扩展它。了解Haskell的随机生成器是一个很好的挑战!
Thomas Ahle 2014年

我对Haskell尝试这样做感到非常沮丧,但我可能会再尝试一次!我确实发现,没有随机性,游戏变得容易得多。
wvdz 2014年

没有随机性,我很确定您可以找到始终获得16k或32k的方法。然而,Haskell中的随机化并不是那么糟糕,您只需要一种绕过“种子”的方法即可。要么显式地执行,要么使用Random单子。
Thomas Ahle 2014年

改进算法,使其对于非随机游戏始终达到16k / 32k可能是另一个有趣的挑战……
wvdz 2014年

你是对的,这比我想的要难。我设法找到了这个顺序:[UP,LEFT,LEFT,UP,LEFT,DOWN,LEFT]总是会赢得比赛,但不会超过2048。(在没有合法举动的情况下,循环算法只会选择下一个按顺时针顺序排列)
Thomas Ahle,2014年

6

该算法并不是赢得游戏的最佳方法,但在性能和所需代码量方面却是最佳方法:

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

10
如果您说random from (right, right, right, down, down, up) 并非所有举动都具有相同的可能性,那么效果会更好。:)
Daren 2014年

3
实际上,如果您是游戏的新手,那么仅使用3个键(基本上是该算法的作用)确实有帮助。因此,并不像乍看起来那样糟糕。
Digits 2014年

5
是的,这是基于我对游戏的观察。在您必须使用第四方向之前,该游戏几乎无需任何观察即可解决自身问题。无需检查任何块的确切值,该“ AI”应该能够达到512/1024。
API-Beast

3
适当的AI会尽量避免陷入不惜一切代价只能向一个方向移动的状态。
API-Beast

3
实际上只使用3个方向是一个非常不错的策略!这让我几乎接近2048手动玩游戏。如果将此与其他策略结合起来以决定其余3个动作,那么它可能会非常强大。更不用说将选择减少到3对性能有很大的影响。
wvdz 2014年

4

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

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

例如:

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

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

为了解决这个问题,他们有两种不容忽视的向上移动方式,检查这两种可能性可能会立即发现更多问题,这形成了依赖性列表,每个问题都需要首先解决另一个问题。我认为在决定下一步行动时,特别是在遇到困难时,我内部就具有这种链条或某些情况下的依赖关系树。


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

较大的瓦片的方式:增加较小的周围瓦片的值。

等等...


整个方法可能会比这更复杂,但不会更复杂。缺乏分数,体重,神经元和对可能性的深入搜索可能是这种机械感觉。可能性之树甚至需要足够大,以至于根本不需要任何分支。


5
您正在用启发式方法描述本地搜索。这会让您陷入困境,因此您需要为下一步的行动做好准备。反过来,您也可以对解决方案进行搜索和评分(以便决定)。因此,这实际上与其他任何提出的解决方案没有什么不同。
runDOSrun
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.