我最近偶然发现了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分,这比我目前的分数要大得多。有比上述更好的算法吗?


当前回答

我的尝试与上面的其他解决方案一样使用expectimax,但没有比特板。Nneonneo的解决方案可以检查1000万次移动,大约深度为4,剩余6个平铺,可能移动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上的代码:

变量n=4,M=新矩阵变换(n);var ai={weights:[1,1],深度:1};//默认情况下,depth=1,但我们会根据空闲平铺的数量对每个预测进行调整var蛇=[[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(函数(a){return a.map(Math.exp)})初始化(ai)函数运行(ai){变量p;而((p=预测(ai))!=空){移动(p,ai);}//console.log(ai.grid,maxValue(ai.ggrid))ai.maxValue=最大值(ai.grid)控制台日志(ai)}函数初始化(ai){ai.grid=[];对于(变量i=0;i<n;i++){ai网格[i]=[]对于(变量j=0;j<n;j++){ai.grid[i][j]=0;}}兰特(ai网格)兰特(ai网格)ai.steps=0;}函数move(p,ai){//0:向上,1:向右,2:向下,3:向左var newgrid=mv(p,ai.grid);if(!equal(newgrid,ai.grid)){//console.log(stats(newgrid,ai.grid))ai.grid=新网格;尝试{兰特(ai网格)ai.步骤++;}捕获(e){控制台日志(“无房间”,e)}}}函数预测(ai){var free=freeCells(ai.grid);ai.depth=自由>7?1:(自由>4?2:3);var-root={path:[],prob:1,grid:ai.grid,childs:[]};var x=expandMove(根,ai)//console.log(“叶数”,x)//console.log(“叶数2”,countLeaves(根))if(!root.childres.length)返回nullvar values=root.childres.map(expectimax);var mx=最大值;return root.childrens[mx[1]].path[0]}函数countLeaves(节点){变量x=0;if(!node.childres.length)返回1;for(node.childs的var n)x+=叶数(n);返回x;}函数预期值(节点){if(!node.childres.length){返回node.score}其他{var values=node.childres.map(expectimax);如果(node.prob){//我们处于最大节点return Math.max.apply(空,值)}否则{//我们处于随机节点var平均值=0;对于(var i=0;i<values.length;i++)avg+=节点.子项[i].prob*值[i]返回平均值/(values.length/2)}}}函数expandRandom(节点,ai){变量x=0;对于(var i=0;i<node.grid.length;i++)对于(var j=0;j<node.grid.length;j++)if(!node.grid[i][j]){var grid2=M.copy(node.grid),grid4=M.copy(node.grid);网格2[i][j]=2;网格4[i][j]=4;var child2={grid:grid2,prob:.9,path:node.path,childs:[]};var child4={grid:grid4,prob:.1,path:node.path,childs:[]}node.childres.push(child2)node.childres.push(child4)x+=expandMove(child2,ai)x+=expandMove(child4,ai)}返回x;}函数expandMove(node,ai){//node={grid,path,score}var isLeaf=真,x=0;如果(节点路径长度小于ai深度){for(变量移动[0,1,2,3]){var grid=mv(移动,node.grid);if(!equal(grid,node.grid)){isLeaf=false;var child={grid:grid,path:node.path.contat([move]),childs:[]}node.childres.push(child)x+=expandRandom(子级,ai)}}}if(isLeaf)node.score=dot(ai.weights,stats(node.grid))return是Leaf?1:x;}var单元格=[]var table=document.querySelector(“table”);对于(变量i=0;i<n;i++){var tr=document.createElement(“tr”);单元格[i]=[];对于(变量j=0;j<n;j++){cells[i][j]=document.createElement(“td”);tr.appendChild(单元格[i][j])}table.appendChild(tr);}函数更新UI(ai){cells.forEach(函数(a,i){a.forEach(函数(el,j){el.innerHTML=ai.grid[i][j]| |“”})});}更新UI(ai);updateHint(预测(ai));函数runAI(){var p=预测(ai);如果(p!=null&&ai.running){移动(p,ai);更新UI(ai);updateHint(p);requestAnimationFrame(runAI);}}runai.onclick=函数(){if(!ai.running){this.innerHTML=“停止AI”;ai.running=真;runAI();}其他{this.innerHTML=“运行AI”;ai.running=false;updateHint(预测(ai));}}函数updateHint(dir){hintvalue.innerHTML=['↑', '→', '↓', '←'][目录]| |“”;}document.addEventListener(“keydown”,函数(事件){if(!event.target.matches('.r*'))返回;event.prpreventDefault();//避免滚动if(地图中的event.which){移动(map[event.with],ai)console.log(stats(ai.grid))更新UI(ai);updateHint(预测(ai));}})变量映射={38:0,//以上39:1,//右40:2,//向下37:3,//左};init.onclick=函数(){初始化(ai);更新UI(ai);updateHint(预测(ai));}函数统计信息(网格,previousGrid){var free=freeCells(网格);var c=dot2(网格,蛇);返回[c,free*free];}函数dist2(a,b){//2D距离的平方返回Math.pow(a[0]-b[0],2)+Math.pow(a[1]-b[1],2)}功能点(a,b){变量r=0;对于(var i=0;i<a.length;i++)r+=a[i]*b[i];返回r}函数dot2(a,b){变量r=0;对于(var i=0;i<a.length;i++)对于(变量j=0;j<a[0].length;j++)r+=a[i][j]*b[i][j]返回r;}函数积(a){return a.reduce(函数(v,x){返回v*x}, 1)}函数maxValue(网格){return Math.max.apply(null,grid.map(函数(a){return Math.max.apply(null,a)}));}无功能单元格(网格){返回grid.reduce(函数(v,a){返回v+a.reduce(函数(t,x){返回t+(x==0)}, 0)}, 0)}函数max(arr){//返回max的[value,index]var m=[-无限,空];对于(变量i=0;i<arr.length;i++){如果(arr[i]>m[0])m=[arr[i],i];}返回m}函数min(arr){//返回min的[value,index]var m=[无限,空];对于(变量i=0;i<arr.length;i++){如果(arr[i]<m[0])m=[arr[i],i];}返回m}函数maxScore(节点){变量最小值={分数:-无限,路径:[]};for(节点的var节点){如果(node.score>min.score)min=节点;}

其他回答

我对这款游戏的人工智能的想法产生了兴趣,它不包含硬编码的智能(即没有启发式、评分功能等)。人工智能应该只“知道”游戏规则,并“弄清楚”游戏玩法。这与大多数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的领域独立性。一些变体非常独特,例如六边形克隆。

算法

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)

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

注意:常数可以调整。。

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


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

算法

启发式评分算法

我的算法所基于的假设相当简单:如果你想获得更高的分数,那么棋盘必须尽可能保持整洁。特别地,最优设置由瓦片值的线性和单调递减顺序给出。这种直觉也会给你一个平铺值的上限:其中n是板上平铺的数量。

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

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

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

可以同时评估多个线性路径,最终得分将是任何路径的最大得分。

决策规则

实现的决策规则不太聪明,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或Expectimimax的实现肯定会改进算法。显然更多复杂的决策规则会降低算法的速度,并且需要一些时间来实现。我将在不久的将来尝试一个最小值实现。(敬请关注)

基准

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

在T2的情况下,十次测试中有四次生成平均分数为42000的4096分图块

Code

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

这不是对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代理探索的游戏树,假设计算机是对手,只需一步:

这里已经有了这款游戏的AI实现。自述节选:

该算法是迭代加深深度优先alpha beta搜索。求值函数试图保持行和列的单调性(要么减少,要么增加),同时最小化网格上的平铺数量。

黑客新闻上也有关于这个算法的讨论,你可能会发现它很有用。