What is the optimal algorithm for the game 2048?
我最近偶然发现了2048年的比赛。通过将相似的瓷砖向四个方向中的任意一个方向移动,可以合并成"较大"的瓷砖。每次移动后,一个新的图块随机出现在空位置,值为
第一,我需要遵循一个明确的战略来实现目标。所以,我想为它写一个程序。
我当前的算法:
1 2 3 4 5 | 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 } |
我所做的是在任何时候,我都会尝试将这些图块与值
但是,当我实际使用这个算法时,在游戏结束前我只得到大约4000分。阿法克的最高分数略高于20000分,这比我目前的分数要高得多。有没有比上述更好的算法?
我是其他人在本文中提到的人工智能程序的作者。您可以查看活动中的人工智能或读取源代码。
目前,该程序在我的笔记本电脑浏览器中运行的javascript中获得了大约90%的成功率,因为每次移动大约需要100毫秒的思考时间,所以虽然还不完美(还没有!)它的性能相当好。
由于游戏是一个离散的状态空间,完美的信息,像国际象棋和西洋跳棋那样基于回合的游戏,我使用的方法已经被证明对这些游戏有效,即使用α-β修剪的极大极小搜索。因为已经有很多关于这个算法的信息了,我将只讨论我在静态评估函数中使用的两个主要的启发式方法,它将其他人在这里表达的许多直觉形式化。
单调性这种启发式方法试图确保瓷砖的值都沿左/右和上/下方向递增或递减。这种启发式方法本身就捕获了许多其他人提到的直觉,即更高值的图块应该聚集在一个角落。它通常会防止较小值的瓷砖成为孤立的,并将保持董事会非常有条理,较小的瓷砖层叠和填充到较大的瓷砖。
这是一个非常单调的网格的截图。我通过运行eval函数集的算法来获得这个结果,它忽略了其他的启发式,只考虑单调性。
平滑度上面的启发式本身倾向于创建邻接图块值正在减少的结构,但当然为了合并,邻接图块需要具有相同的值。因此,平滑度启发式算法只测量相邻瓷砖之间的值差,试图最小化这一计数。
一位评论黑客新闻的人用图论的方式对这个想法进行了有趣的形式化。
这是一个完美平滑网格的截图,这是一个出色的模仿叉子。
自由瓦片最后,由于游戏板太窄,选项会很快用完,所以拥有太少的免费互动程序会受到惩罚。
就这样!通过搜索游戏空间,同时优化这些标准,可以获得非常好的性能。使用像这样的通用方法而不是显式编码的移动策略的一个优点是,该算法通常可以找到有趣和意外的解决方案。如果你看着它跑起来,它通常会做出令人惊讶但有效的动作,比如突然切换它所建的墙或拐角。
编辑:下面是这种方法的威力的演示。我打开了瓷砖值的上限(在达到2048年之后它一直在运行),这是八次试验后的最佳结果。
是的,那是4096和2048。=)这意味着它在同一块板上实现了三次难以捉摸的2048瓷砖。
我使用expectimax优化开发了2048a i,而不是@ovolve算法使用的minimax搜索。人工智能只在所有可能的移动上执行最大化,然后在所有可能的瓷砖生成上执行预期(由瓷砖的概率加权,即10%用于4,90%用于2)。据我所知,不可能对expectimax优化进行删减(除去极不可能删除的分支),因此使用的算法是一种经过仔细优化的蛮力搜索。好的。性能
默认配置中的人工智能(最大搜索深度为8)需要从10毫秒到200毫秒的任何时间执行移动,这取决于板位置的复杂性。在测试中,人工智能在整个游戏过程中平均每秒移动5-10次。如果搜索深度限制为6步,人工智能可以很容易地每秒执行20+步,这使得一些有趣的观看。好的。
为了评估人工智能的得分性能,我运行了100次人工智能(通过遥控器连接到浏览器游戏)。对于每个图块,下面是至少实现一次图块的游戏比例:好的。
1 2 3 4 5 | 2048: 100% 4096: 100% 8192: 100% 16384: 94% 32768: 36% |
所有跑步的最低分为124024分,达到的最高分为794076分。中位数是387222。人工智能从来没有失败过获得2048瓦片(所以它从来没有在100场游戏中输过一次);事实上,它在每一次运行中至少获得了8192瓦片!好的。
以下是最佳跑步记录的截图:好的。
好的。
这场比赛在96分钟内完成了27830次移动,平均每秒移动4.8次。好的。实施
我的方法将整个板(16个条目)编码为一个64位整数(其中tiles是nybols,即4位块)。在64位机器上,这使得整个电路板可以在一个机器寄存器中传递。好的。
位移位操作用于提取单独的行和列。一行或一列是16位的数量,所以一个大小为65536的表可以对在一行或一列上操作的转换进行编码。例如,移动被实现为对预先计算的"移动效果表"的4个查找,该表描述了每个移动如何影响一行或一列(例如,"向右移动"表包含条目"1122->0023",描述了行[2,2,4,4]在向右移动时如何成为行[0,0,4,8])。好的。
评分也可以使用表查找完成。这些表包含对所有可能的行/列计算的启发式得分,一个板的结果得分只是每一行和每一列的表值之和。好的。
这种棋盘表示,加上移动和得分的查表方法,允许人工智能在短时间内搜索大量的游戏状态(在2011年年中笔记本电脑的一个核心上,每秒超过10000000个游戏状态)。好的。
expectimax搜索本身被编码为递归搜索,在"预期"步骤(测试所有可能的图块生成位置和值,并根据每个可能性的概率加权其优化分数)和"最大化"步骤(测试所有可能的移动并选择得分最高的一个)之间交替进行。当树搜索看到以前看到的位置(使用换位表)、达到预先定义的深度限制或达到极不可能达到的板状态(例如,通过从起始位置开始一行中获取6"4"图块)时,树搜索终止。典型的搜索深度是4-8步。好的。试探法
利用几种启发式方法将优化算法引向有利位置。启发式算法的精确选择对算法的性能有很大影响。不同的启发式方法被加权并组合成一个位置分数,这决定了给定的板位置有多"好"。然后,优化搜索的目标是最大化所有可能董事会职位的平均得分。如游戏所示,实际得分不用于计算棋盘得分,因为它太重了,倾向于合并分块(如果延迟合并可以产生很大的好处)。好的。
最初,我使用了两个非常简单的启发式方法,为开放的正方形和边缘具有大值授予"奖金"。这些启发式方法表现很好,经常达到16384,但从未达到32768。好的。
Petr Mor_vek(@xificurk)接受了我的人工智能,并添加了两个新的启发式方法。第一个启发式是对具有非单调行和列的惩罚,这些行和列随着列的增加而增加,确保小数字的非单调行不会对分数产生强烈影响,但大数字的非单调行会对分数造成重大伤害。第二个启发式算法除了计算开放空间外,还计算了潜在合并(相邻的等值)的数量。这两种启发式方法有助于将算法推向单调的电路板(更容易合并),以及向具有大量合并的电路板位置(鼓励它在可能的情况下对齐合并以获得更大的效果)。好的。
此外,Petr还使用"元优化"策略(使用一种称为CMA-ES的算法)优化启发式权重,在该策略中,对权重本身进行调整以获得可能的最高平均分数。好的。
这些变化的影响非常显著。算法从大约13%的时间内实现16384图块到90%的时间内实现它,并且算法开始在1/3的时间内实现32768图块(而旧的启发式算法从未产生过32768图块)。好的。
我相信启发式方法还有改进的余地。这个算法显然还不是"最优"的,但我觉得它已经接近了。好的。
人工智能在超过三分之一的游戏中实现了32768块,这是一个巨大的里程碑;如果有任何人类玩家在官方游戏中实现了32768块,我会感到惊讶(即,不使用savestates或undo等工具)。我想65536块瓷砖已经够得着了!好的。
你可以自己试试人工智能。该代码可在https://github.com/nneonneo/2048-ai上找到。好的。好啊。
我开始对这个游戏的人工智能感兴趣,因为它不包含硬编码的智能(即没有启发式、计分函数等)。人工智能应该只"知道"游戏规则,并且"找出"游戏规则。这与大多数的人工智能(如这条线中的人工智能)形成了鲜明的对比,在人工智能中,游戏本质上是由一个表示人类对游戏理解的得分函数控制的蛮力。好的。人工智能算法
我发现了一个简单却出奇地好的游戏算法:为了确定给定棋盘的下一个移动,人工智能使用随机移动在内存中玩游戏,直到游戏结束。这是在跟踪最后一场比赛的得分时做的几次。然后计算每次开始移动的平均结束分数。以平均得分最高的开始动作作为下一个动作。好的。
每次移动只有100次(即内存游戏),人工智能达到2048分牌的80%,4096分牌的50%。使用10000次运行将获得2048块瓷砖100%,4096块瓷砖70%,8192块瓷砖约1%。好的。
行动起来看看好的。
最佳成绩如下:好的。
好的。
关于这个算法的一个有趣的事实是,虽然随机游戏不出所料地相当糟糕,但选择最好(或最差)的移动会带来非常好的游戏体验:一个典型的人工智能游戏可以达到70000点和最后3000个移动,但是任何给定位置的内存随机游戏在大约4个移动中平均能获得340个额外的点。临死前0个额外动作。(通过运行ai并打开调试控制台,您可以自己看到这一点。)好的。
这张图说明了这一点:蓝线表示每次移动后的棋盘得分。红线显示了算法在该位置的最佳随机运行结束游戏分数。本质上,红色值是向上拉蓝色值,因为它们是算法的最佳猜测。有趣的是,每一点的红线都比蓝线略高一点,然而蓝线却在不断增加。好的。
好的。
我觉得很奇怪的是,算法不需要预测好的游戏效果,就可以选择产生它的动作。好的。
后来我发现这个算法可能被归类为纯蒙特卡罗树搜索算法。好的。实现和链接
首先,我创建了一个javascript版本,可以在这里看到它的实际操作。这个版本可以在适当的时间运行100次。打开控制台获取更多信息。(源)好的。
后来,为了发挥更多的作用,我使用了@ NeNeNeX高度优化的基础设施,并在C++中实现了我的版本。这个版本允许每次移动最多100000次,如果你有耐心,甚至1000000次。提供了建筑说明。它在控制台中运行,并且有一个遥控器来播放Web版本。(源)好的。结果
令人惊讶的是,增加跑步次数并不能显著提高游戏效果。在4096块瓷砖和所有较小的瓷砖上,这个策略似乎有一个80000点的限制,非常接近于实现8192块瓷砖。将跑步次数从100次增加到100000次会增加达到这一分数限制(从5%增加到40%)的几率,但不会突破这一限制。好的。
运行10000次,临时增加到1000000次,接近关键位置,设法打破这一障碍,不到1%的次数,达到129892和8192瓷砖的最高分数。好的。改进
在实现了这个算法之后,我尝试了很多改进,包括使用最小或最大得分,或者最小、最大和平均的组合。我也尝试了使用深度:我没有尝试每次移动k次,而是尝试了给定长度的每次移动k次列表(例如"向上、向上、向左"),并选择了最佳得分移动列表的第一个移动。好的。
后来,我实现了一个计分树,它考虑了在给定的移动列表之后能够进行移动的条件概率。好的。
然而,这些想法都没有显示出任何真正的优势比简单的第一个想法。我在C++代码中留下了注释这些代码的代码。好的。
我添加了一个"深度搜索"机制,当运行中的任何一个意外达到下一个最高的图块时,该机制会将运行数临时增加到1000000。这提供了一个时间改进。好的。
我很想知道是否有人有其他的改进想法来保持人工智能的领域独立性。好的。2048个变种和克隆
为了好玩,我还实现了人工智能作为书签,钩住游戏的控制。这允许人工智能与原始游戏及其许多变体一起工作。好的。
这是可能的,因为人工智能的领域独立性。有些变体非常独特,例如六边形克隆体。好的。好啊。
编辑:this is a naive algorithm,人的思想意识的建模过程和结果变得非常弱,对搜索到的compared恩,因为一只北瓦前进。它是在提交的早期反应的时间。P></
我beaten refined the algorithm and the game!它可能失败的两个简单的坏运气to close to the end to Move Down(You are强制You should never do,which appears和文件,在你的最高should be。只是试着让我茶行填充的顶部,移动左does not,but the)打破模式基本上你有在固定端部分和移动部分to play with。这是你的目标。P></
P></
This is the chose模型的默认模式。P></
1 2 3 4 | 1024 512 256 128 8 16 32 64 4 2 x x x x x x |
任意角is the chosen one,你基本上不会出版社(move the key禁止的),如果你做了,你的Press the相反和试图修复它。未来总是tiles for the模型随机文件expects the next to be a 2和opposite appear on the Side to the current模型(while the第一行是不完整的,在底部右角,一旦第一行is completed on the left,自下而上的角落)。P></
这里的GOES the algorithm。在80 %的尝试(EN is possible to似乎总是赢得更多的"专业技术"与AM about this not sure,虽然)。P></
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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) } |
一些步骤分on the missing。在这里:P></
两个模型has changed to the the expected to the of =运气?模型。is the模型的努力是is theP></
1 2 3 4 | 512 256 128 x X X x x X X x x x x x x |
and the chain to get there has成为:P></
1 2 3 4 | 512 256 64 O 8 16 32 O 4 x x x x x x x |
the
我知道它会那么好了,出版社的权利,那么(right or where the Top 4在线depending has created to the proceed)然后将完整的链,直到它:P></
P></
我知道现在是back to the chain模型:布尔P></
1 2 3 4 | 512 256 128 64 4 8 16 32 X X x x x x x x |
第二指针,它有坏运气和its main has been taken有现货。EN,EN是likely会失败,但它仍然可以是:P></
P></
模型和链:这里is theP></
1 2 3 4 | O 1024 512 256 O O O 128 8 16 32 64 4 x x x |
当它达到它manages to the Whole 128行的海外收益:在IS。P></
1 2 3 4 | O 1024 512 256 x x 128 128 x x x x x x x x |
我把博客上一篇文章的内容复制到这里
我提出的解决方案非常简单,易于实现。尽管如此,它已经达到了131040分。给出了算法性能的几个基准点。
算法启发式评分算法
我的算法基于的假设相当简单:如果你想获得更高的分数,董事会必须保持尽可能整洁。特别地,最优设置由瓷砖值的线性和单调递减顺序给出。这种直觉也会给你一个瓦片值的上限:,其中n是板上瓦片的数量。
(如果在需要时随机生成4个图块而不是2个图块,则有可能到达131072图块)
两种可能的董事会组织方式如下图所示:
为了以单调递减顺序执行瓷砖的排序,分数si计算为板上线性化值的和乘以具有公共比率r<1的几何序列的值。
多条直线路径可以同时评估,最终得分将是任何路径的最大得分。
决策规则
实现的决策规则不是很智能,这里给出了python中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @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或expectminimax肯定会改进算法。显然是更多复杂的决策规则会降低算法的速度,需要一定的时间来实现,我会在不久的将来尝试一个极大极小值的实现。(敬请关注)
基准- 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,每10个测试中有4个生成4096个瓷砖,平均得分为42000。
代码代码可以在gihub上的以下链接找到:https://github.com/nicola17/term2048-ai它基于term2048,并用python编写。我将在C++中实现一个更高效的版本。
我的尝试使用expectimax,和上面的其他解决方案一样,但没有位板。Nneonneo的解决方案可以检查1000万次移动,大约4个深度,剩余6个瓷砖,可能4个移动(2*6*4)4。在我的例子中,这个深度需要很长的时间来探索,我根据剩余的空闲瓷砖数量调整expectimax搜索的深度:
1 | depth = free > 7 ? 1 : (free > 4 ? 2 : 3) |
用自由瓷砖数量的平方和二维网格的点积的加权和计算板的分数:
1 2 3 4 | [[10,8,7,6.5], [.5,.7,1,3], [-.5,-1.5,-1.8,-2], [-3.8,-3.7,-3.5,-3]] |
从左上角的瓦片上以蛇的形式向下组织瓦片。
以下或Github上的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | 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) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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; } |
1 2 3 4 5 | <table title="press arrow keys"></table> <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> |
我是一个2048控制器的作者,它的得分比这个线程中提到的任何其他程序都要高。在GitHub上可以有效地实现控制器。在单独的回购中,还存在用于培训控制器状态评估功能的代码。本文介绍了训练方法。
控制器使用expectimax搜索,通过一种不同的时间差分学习(一种强化学习技术)从零开始(没有人类2048专业知识)学习状态评估函数。状态值函数使用一个n元组网络,它基本上是板上观察到的模式的加权线性函数。它总共涉及超过10亿个重量。
性能1步/秒:609104(平均100场)
10步/秒:589355(平均300场)
三层(约1500步/秒):511759(平均1000场)
10次移动/秒的磁贴统计如下:
1 2 3 4 5 6 | 2048: 100% 4096: 100% 8192: 100% 16384: 97% 32768: 64% 32768,16384,8192,4096: 10% |
(最后一行表示在板上同时具有给定的瓷砖)。
对于3层:
1 2 3 4 5 6 | 2048: 100% 4096: 100% 8192: 100% 16384: 96% 32768: 54% 32768,16384,8192,4096: 8% |
然而,我从来没有观察到它获得了65536块瓷砖。
我想我发现安algorithm which那么好的作品,当我过经常达到10000分,最佳是在16000个人。does not ALM解决方案在我的最大的数字在角落中保鲜,but to keep in the顶行。P></
Please see below the队列:P></
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); } |
在我的例子中,计算机播放器是完全随机的,但我仍然假设对手的设置,并实现了作为max播放器的ai播放器代理。
我有4x4网格可以玩这个游戏。
观察:如果我给第一个启发式函数或第二个启发式函数分配太多权重,那么人工智能玩家得到的两个结果都很低。我对启发式函数进行了许多可能的权重分配,并采用了凸组合,但很少有人工智能玩家能够得分2048。大多数时候它要么停在1024或512。
我也尝试过角启发式,但出于某种原因,它使结果更糟,有什么直觉为什么?
此外,我尝试将搜索深度截止值从3增加到5(因为搜索空间超过了允许的时间,即使使用修剪也不能增加),并添加了一个启发式的,它可以查看相邻瓷砖的值,如果它们可以合并,可以提供更多的点,但我仍然无法获得2048。
我认为用expectimax代替minimax会更好,但我还是想用minimax来解决这个问题,并获得2048或4096等高分。我不确定我是否遗漏了什么。
下面的动画显示了人工智能代理与计算机播放器玩游戏的最后几个步骤:
任何见解都会非常有帮助,提前谢谢。(这是我的博客文章链接:https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-prunning-and-heuristic-evaluation-to-solve-2048-game-with-computer/和YouTube视频:https://www.youtube.com/watch?V= VNVFLFZ0R4
下面的动画显示了游戏的最后几个步骤,人工智能玩家代理可以获得2048分,这次也添加了绝对值启发式:
下图显示了玩家人工智能代理所探索的游戏树,假设计算机只是一步之遥的对手:
我在哈斯克尔写了一篇2048年的解决方案,主要是因为我现在正在学习这门语言。
我对游戏的实现与实际的游戏略有不同,因为新的图块总是"2"(而不是90%2和10%4)。而且新的瓷砖不是随机的,而是从左上角第一个可用的。这种变体也被称为DET 2048。
因此,该解算器具有确定性。
我使用了一个详尽的算法,它喜欢空的瓷砖。对于深度1-4,它的执行速度相当快,但对于深度5,它的执行速度相当慢,每次移动大约1秒钟。
下面是实现求解算法的代码。网格表示为16长度的整数数组。得分只需计算空方块的数量。
1 2 3 4 5 6 7 | 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处求解时,得到的结果是:
1 2 3 4 5 6 7 | 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
此算法不是赢得游戏的最佳算法,但在性能和所需代码数量方面相当理想:
1 2 3 4 5 6 7 8 9 10 | if(can move neither right, up or down) direction = left else { do { direction = random from (right, down, up) } while(can not move in"direction") } |
许多其他的答案使用人工智能来计算昂贵的搜索可能的未来,启发式,学习等。这些都令人印象深刻,也许是正确的前进道路,但我希望贡献出另一个想法。
为优秀的游戏玩家使用的策略建模。
例如:
1 2 3 4 | 13 14 15 16 12 11 10 9 5 6 7 8 4 3 2 1 |
按照上面显示的顺序读取方块,直到下一个方块值大于当前方块值。这就出现了一个问题,即试图将另一个具有相同值的图块合并到这个正方形中。
为了解决这个问题,他们有两种方法可以解决这个问题,而这两种方法都没有留下或更糟,检查这两种可能性可以立即发现更多的问题,这形成了一个依赖关系列表,每个问题都需要先解决另一个问题。我认为我在决定下一步行动时,特别是在陷入困境时,我有这条链条,或者在某些情况下,我有内在的依赖树。
图块需要与邻居合并,但太小:将另一个邻居与此邻居合并。
以较大的方式:增加较小的周围瓷砖的值。
等。。。
整个方法可能会比这更复杂,但不会更复杂。可能是这种感觉缺乏分数、体重、神经和对可能性的深入探索的机械作用。可能性之树甚至需要足够大,完全需要任何分支。