MinMax-极小极大算法——2048

2020-02-04  本文已影响0人  我只要喝点果粒橙

Github上找到的是JS的代码,个人用Python重写了,代码之后会开源在github上。

算法介绍

MinMax

大家在编程的时候应该或多或少都接触到过这样的写法:min(max(xxx,yyy)),MinMax算法的表达形式就是如此,不过其中的Min和Max都是具有对应含义的。

一般解决博弈类问题的自然想法是将格局组织成一棵),树的每一个节点表示一种格局,而父子关系表示由父格局经过一步可以到达子格局。Minimax也不例外,它通过对以当前格局为根的格局树搜索来确定下一步的选择。而一切格局树搜索算法的核心都是对每个格局价值的评价。Minimax算法基于以下朴素思想确定格局价值:

举例For Example:

现在考虑这样一个游戏:有三个盘子A、B和C,每个盘子分别放有三张纸币。A放的是1、20、50;B放的是5、10、100;C放的是1、5、20。单位均为“元”。有甲、乙两人,两人均对三个盘子和上面放置的纸币有可以任意查看。游戏分三步:

  1. 甲从三个盘子中选取一个。
  2. 乙从甲选取的盘子中拿出两张纸币交给甲。
  3. 甲从乙所给的两张纸币中选取一张,拿走。

其中甲的目标是最后拿到的纸币面值尽量大,乙的目标是让甲最后拿到的纸币面值尽量小。

分析过程可看 MinMax和Alpha-beta剪枝分析[转]

Alpha-beta剪枝

是在Minmax的基础上通过对每个结点下界alpha和上界beta值的维护进行了剪枝

偶数层为Max层(己方),奇数层为Min层(对手),其中root为当前形势。

执行过程

在root层,\alpha' = max(N_1.\beta, N_2.\beta,...,N_i.\beta)==(self.\beta,N_i.\beta)

在Max层

在Min层,

△其中N为子节点; self.α表示当前节点已更新的α值

伪代码

function alphabeta(node, depth, α, β, Player)
    //达到最深搜索深度或胜负已分         
    if  depth = 0 or node is a terminal node
        return the heuristic value of node
    if  Player = MaxPlayer // 极大节点
        for each child of node // 子节点是极小节点
            α := max(α, alphabeta(child, depth-1, α, β, not(Player) ))   
            if β ≤ α 
            // 该极大节点的值>=α>=β,该极大节点后面的搜索到的值肯定会大于β,因此不会被其上层的极小节点所选用了。对于根节点,β为正无穷
                 break //beta剪枝                        
        return α
    else // 极小节点
        for each child of node //子节点是极大节点
            β := min(β, alphabeta(child, depth-1, α, β, not(Player) )) // 极小节点
            if β ≤ α // 该极大节点的值<=β<=α,该极小节点后面的搜索到的值肯定会小于α,因此不会被其上层的极大节点所选用了。对于根节点,α为负无穷
                break //alpha剪枝
        return β 

2048AI介绍

游戏规则:

2048游戏共有16个格子,初始时初始数字由2或者4构成,之后每次移动生成2的概率为0.9,生成4的概率为0.1,见2048随机生成新数字源码

1、手指向一个方向滑动,所有格子会向那个方向运动。
2、相同数字的两个格子,相撞时数字会相加。
3、每次滑动时,空白处会随机刷新出一个数字的格子。
4、当界面不可运动时(当界面全部被数字填满时),游戏结束;当界面中最大数字是2048时,游戏胜利。

建模:

之前的对弈类游戏, 博弈双方的地位都是对等的. 但这边只有游戏者一人, 对手在哪里?
  让人脑洞大开的是, 2048游戏AI的设计者, 创造性把棋局环境本身做为了博弈的另一方.
  当然双方追求的胜利目标不一样:
  • 我方:游戏者(AI), 追求2048及2048以上的方块出现
  • 对方:计算机(棋局环境), 填满棋局格子, 使得4个方向皆不能移动
• 胜利条件:出现某个方块的数值为“2048”。
•失败条件:格子全满,且无法向四个方向中任何一个方向移动(均不能触发合并)。
  ▲.游戏模型就被建模成了信息完备的双人对弈问题. 而传统博弈树和技巧就自然有了用武之地.

评估函数:

评估函数是算法的核心,如何评价当前格局的价值是重中之重。依据游戏经验, 作者选用了如下评估因素:
  (1) 单调性: 指方块从左到右、从上到下均遵从递增或递减.
  (2) 平滑性: 指每个方块与其直接相邻方块数值的差,其中差越小越平滑.
  (3) 空格数: 局面的空格总数.
  (4) 最大数: 当前局面的最大数字, 该特征为积极因子.

采用线性函数, 并添加权重系数:

// static evaluation function
AI.prototype.eval = function() {
  var emptyCells = this.grid.availableCells().length;
 
  var smoothWeight = 0.1,
      //monoWeight   = 0.0,
      //islandWeight = 0.0,
      mono2Weight  = 1.0,
      emptyWeight  = 2.7,
      maxWeight    = 1.0;
 
  return this.grid.smoothness() * smoothWeight
       //+ this.grid.monotonicity() * monoWeight
       //- this.grid.islands() * islandWeight
       + this.grid.monotonicity2() * mono2Weight
       + Math.log(emptyCells) * emptyWeight
       + this.grid.maxValue() * maxWeight;
};

分析:前3项能衡量一个局面的好坏, 而最大数该项, 则让游戏AI多了一点积极和"冒险"

执行过程

游戏AI的决策过程, 是标准的maxmin search和alpha+beta pruning的实现. 所有的方向(上下左右)都会去尝试.

Alpha+beta pruning + worst consideration pruning

然而在游戏本身做决策时, 在Min节点还采用了另一种剪枝,即只考虑对方走出让格局最差的那一步(而实际2048中计算机的选择是随机的),做为搜索分支的剪枝条件,即不是每个空格都去尝试填{2, 4}。

这种假定环境生成了最坏的局面的剪枝做法,可以很好地提高搜索效率,并且获得更强的生存能力。如果全部搜索的话,对方所有可能的选择就为变成了“空格数×2”种,使得搜索效率很低,严重限制搜索深度。而这种选择性地丢掉了很多搜索分支,能够大大地提高搜索效率。

// try a 2 and 4 in each cell and measure how annoying it is
// with metrics from eval
var candidates = [];
var cells = this.grid.availableCells();
var scores = { 2: [], 4: [] };
for (var value in scores) {
  for (var i in cells) {
    scores[value].push(null);
    var cell = cells[i];
    var tile = new Tile(cell, parseInt(value, 10));
    this.grid.insertTile(tile);
    scores[value][i] = -this.grid.smoothness() + this.grid.islands();
    this.grid.removeTile(cell);
  }
}
 
// now just pick out the most annoying moves
var maxScore = Math.max(Math.max.apply(null, scores[2]), Math.max.apply(null, scores[4]));
for (var value in scores) { // 2 and 4
  for (var i=0; i<scores[value].length; i++) {
    if (scores[value][i] == maxScore) {
      candidates.push( { position: cells[i], value: parseInt(value, 10) } );
    }
  }
}

分析:对于选择性忽略搜索节点, 其实很有争议. 在某些情况下, 会失去获取最优解的机会. 不过砍掉了很多分支后, 其搜索深度大大加强. 生存能力更强大.

限时的迭代深搜

// performs iterative deepening over the alpha-beta search
AI.prototype.iterativeDeep = function() {
  var start = (new Date()).getTime();
  var depth = 0;
  var best;
  do {
    var newBest = this.search(depth, -10000, 10000, 0 ,0);
    if (newBest.move == -1) {
      break;
    } else {
      best = newBest;
    }
    depth++;
  } while ( (new Date()).getTime() - start < minSearchTime);
  return best
}

该代码没有限制搜索的深度,但是限制了每次“思考”的时间:超时判断在每个深度探索结束后进行, 这未必会精确, 甚至误差很大. 我还是推崇前文谈到过的实现方式.但不管怎样, 作者基本达到了其每100ms决策一步的要求.

Python实现核心代码:


附录

极大极小算法有些不明白 ?

极小极大算法主要应用于什么样的游戏:

对弈类游戏的人工智能(5)--2048游戏AI的解读

2048-AI程序算法分析——Java实现

一图流解释 Alpha-Beta 剪枝(Alpha-Beta Pruning)

2048高分技巧

1、最大数尽可能放在角落。
2、数字按顺序紧邻排列。
3、首先满足最大数和次大数在的那一列/行是满的。
4、时刻注意活动较大数(32以上)旁边要有相近的数。
5、以大数所在的一行为主要移动方向
6、不要急于“清理桌面”。

2048随机生成新数字源码

// Adds a tile in a random position
Grid.prototype.addRandomTile = function () {
  if (this.cellsAvailable()) {
    var value = Math.random() < 0.9 ? 2 : 4;
    //var value = Math.random() < 0.9 ? 256 : 512;
    var tile = new Tile(this.randomAvailableCell(), value);

    this.insertTile(tile);
  }
};

▲需要注意的是,2的几率是0.9,4的几率是0.1,但是除了开局会随机出现1或者2个方块,而在正常游戏中每次移动后只出现一个随机方块。——我当时自己写的是随机1-2个,导致我的分数挺低的。

上一篇下一篇

猜你喜欢

热点阅读