2048 游戏以下图所示,它由一个 4*4 共 16 个方块组成。玩家能够经过「上下左右」四个方向操纵方块滑动,滑动时两个相邻且数值相同的方块会合并,新的方块,数值为二者之和。当游戏里任意方块的数值达到 2048,即为胜利。git
咱们将使用「蒙特卡洛方法」来打造 2048 AI。github
有不少问题,数学公式很复杂,甚至短期内找不到数学公式。好比下面的不规则形状的面积。算法
咱们能够经过一种「统计模拟」手段,在实践上获得上述不规则形状面积的近似值。作法就是:1)在正方形里生成许多位置随机的点;2)统计在不规则图形内的点的数量;3)计算步骤2获得的数量跟总数的比值;4)用正方形的面积乘以步骤三获得的比值,就是不规则形状面积的近似值。编程
上述作法,就是一个典型的蒙特卡洛方法。当咱们生成的随机点数量足够大时,咱们获得的近似值跟理论计算值就愈加接近,偏差愈加小。以下图所示,求正方形里的扇形面积的蒙特卡洛方法的模拟过程:数组
上面两幅图,只是蒙特卡洛方法的两个应用而已。事实上,蒙特卡洛方法的适用范围很广,任何可模拟和统计的比例分布,均可以使用蒙特卡洛方法来模拟。好比检测硬币构造上是否足够均衡。微信
理论上,抛硬币的正反面几率是同样的,各50%。然而,实际工艺上,作不到绝对均匀,总有误差。要想知道这个误差,是偏向正面,仍是偏向反面,可使用蒙特卡洛方法。不断地抛硬币,而后统计正反面所占的比例,当抛硬币的次数是无限大时,这个比例就反映了硬币的均匀性。现实中,咱们作不到无限次抛硬币,因此只能在某个偏差范围内,获得硬币的均匀性评估。性能
总而言之,蒙特卡洛方法,在实践上给予咱们这种便利:咱们能够用模拟和统计,代替数学公式的运算过程,获得跟理论值相近的解。优化
咱们能够把蒙特卡洛方法,应用在 2048 游戏上。ui
对于 2048 游戏的任意状态,都有「上下左右」四个方向能够选择;虽然有时往某个方向走了之后,不会改变盘面的状态,但也是游戏支持的走法,并不会被判输,因此也是一个可选项。3d
这「上下左右」,哪一个方向好,哪一个方向坏,它们各自的胜率是多少?咱们都不知道,但咱们知道,客观上它们是有一种分布存在的。把它们四个的胜率加起来,一定等于 100%。
能够把这个「上下左右」想象成一个四面骰子,并且是不均匀的四面骰子;或者把它们想象成一个正方向被分红四块,并且是不均等的四块。咱们有「2048 公式」能够套用吗?咱们能直接计算出每个方向的胜率面积占比吗?我不是数学家,我没有找到,但我知道蒙特卡洛方法,能够估测出近似解。因此来试试吧。
蒙特卡洛方法的极端情形,等价于暴力穷举,把四个方向,以及四个方向以后的四个方向,以及四个方向以后的四个方向的四个方向,每个排列组合都走一遍,知道输或者赢;而后统计一下走「上下左右」时每一个的胜利次数,跟总次数相除,就获得胜率了。
暴力穷举太粗暴?不要紧。模拟 400 次,可能准确率就达到 90% 呢,剩下的无限次,或许只是把 90% 的准确率提到到 100% 罢了。
按照蒙特卡洛方法的描述。
第一步,先写一个类,有 run 方法,run 方法接受一个参数 iterations,表示模拟多少次,simulate 方法就是模拟。
模拟完毕以后,getBestAction 获取分数最高的那个 action 动做。
simulate 方法怎么写呢?就是不断地随机选一个方向,走到死。board.getActions 方法要在胜利或者失败时,返回空数组,表示玩家在游戏里没有任何有效动做能够作了。这样 while 死循环就能够获得释放。
board.doAction 应该是让游戏进入下一个状态。若是游戏步骤是无限的,那么咱们须要控制一下一次模拟的时间长短,或者 doAction 的次数,对于 2048 等非无限步骤游戏来讲,这一步倒能够省略。
模拟时,须要 board.clone 复制一个,避免影响到当前游戏的状态。若是咱们拿不到游戏模拟器,蒙特卡洛方法就没有那么方便地派上用场。
path 数组变量,记录了咱们此次模拟的 action 序列。
当咱们一次模拟走到死以后,就把当前第一个 action 和本次模拟的结果(胜负01或者得分 score),存到统计表里累计。为何是第一个action?由于咱们的目的就是找到当前游戏的下一步动做,因此模拟的第一步动做,对应的就是咱们实际上要作的下一部动做。
最后一个方法 updateStatistic,就是咱们更新统计表了。它的实现也很简单,就是判断一下这个动做是否已经存在,存在就累计,不存在就建立。
不知道你是否注意到,咱们的代码里,并无 2048 限定的内容,而是在操做一个 board,以及 clone, getActions, doAction, getResult 等高度抽象的方法?
没错,咱们刚才实现的蒙特卡洛方法,不是为 2048 定制的,它可使用在不一样的棋盘游戏、视频游戏或者跟步骤序列相关的游戏里。只要写一个适配器,把游戏状态和动做导出到 clone, getActions, doAction, getResult 等接口便可。
只须要很简短的几行代码,就能够提供让 2048 board 实例的方法,适配咱们所实现的「蒙特卡洛方法类」。
在 getActions 里,判断 2048 board 当前是否胜利(hasWon)或者失败(hasLost),若是是,就返回空数组,若是不是,就返回 [0, 1, 2, 3] 数组表示「上下左右」。
getResult 返回结果就是,先记录模拟前的分数 board.score 为 startScore,在模拟后,getResult 时,把当前的 board.score - startScore,就获得本次模拟的挣到的实际分数。
doAction 方法里简单地调用 board.move 移动方向。为何要抽象成 doAction,而非 doMove 呢?由于有些游戏的动做,不局限于移动啊,因此 move 太具体了,action 更抽象,能够表示更多可能的动做。
写完适配器以后,就能够输出一个方法 getBestAction,只要把当前 2048 board 输入进来,就用蒙特卡洛方法模拟 400 次,而后返回统计上得分最高的那个 action,做为下一个 action。
每走一步都跑一下蒙特卡洛方法,虽然重复走了不少次,但不要紧,只要性能跟得上,重复就重复吧,重复带来更多的模拟次数,也意味着更准确拟合了理论上的面积分布。
若是 400 次模拟,准确度不够,能够增长到 800 次, 2000 次,总有一个数量级,能够达到满意的结果。
下图是在我机器上模拟后,成功抵达 2048 的截图。你也能够在本身机器上看一下这个过程。固然,最好你能够动手实现一下蒙特卡洛方法的算法,加固印象。
请关注个人微信公众号。有机会,咱们再介绍基于蒙特卡洛方法的「蒙特卡洛树搜索(MCTS)」,它实际上是蒙特卡洛方法在编程上的结构优化,本质仍是蒙特卡洛方法。