MinMax-极小极大算法——2048
Github上找到的是JS的代码,个人用Python重写了,代码之后会开源在github上。
算法介绍
MinMax
大家在编程的时候应该或多或少都接触到过这样的写法:
min(max(xxx,yyy))
,MinMax算法的表达形式就是如此,不过其中的Min和Max都是具有对应含义的。
一般解决博弈类问题的自然想法是将格局组织成一棵),树的每一个节点表示一种格局,而父子关系表示由父格局经过一步可以到达子格局。Minimax也不例外,它通过对以当前格局为根的格局树搜索来确定下一步的选择。而一切格局树搜索算法的核心都是对每个格局价值的评价。Minimax算法基于以下朴素思想确定格局价值:
- Minimax是一种悲观算法,即假设对手每一步都会将我方引入从当前看理论上价值最小的格局方向,即对手具有完美决策能力。因此我方的策略应该是选择那些对方所能达到的让我方最差情况中最好的,也就是让<u>对方在完美决策</u>下所<u>对我造成的损失最小</u>。
- Minimax不找理论最优解,因为理论最优解往往依赖于对手是否足够愚蠢,Minimax中我方完全掌握主动,如果对方每一步决策都是完美的,则我方可以达到预计的最小损失格局,如果对方没有走出完美决策,则我方可能达到比预计的最悲观情况更好的结局。总之我方就是要在<u>最坏情况中选择最好的</u>。
举例For Example:
现在考虑这样一个游戏:有三个盘子A、B和C,每个盘子分别放有三张纸币。A放的是1、20、50;B放的是5、10、100;C放的是1、5、20。单位均为“元”。有甲、乙两人,两人均对三个盘子和上面放置的纸币有可以任意查看。游戏分三步:
- 甲从三个盘子中选取一个。
- 乙从甲选取的盘子中拿出两张纸币交给甲。
- 甲从乙所给的两张纸币中选取一张,拿走。
其中甲的目标是最后拿到的纸币面值尽量大,乙的目标是让甲最后拿到的纸币面值尽量小。
分析过程可看 MinMax和Alpha-beta剪枝分析[转]
Alpha-beta剪枝
是在Minmax的基础上通过对每个结点下界alpha和上界beta值的维护进行了剪枝
偶数层为Max层(己方),奇数层为Min层(对手),其中root为当前形势。
执行过程
在root层,
在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实现核心代码:
附录
极大极小算法有些不明白 ?
极小极大算法主要应用于什么样的游戏:
- 零和游戏(Zero-sum Game)
- 完全信息(Perfect Information)——Max代表你自己,Min代表你的对手
对弈类游戏的人工智能(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个,导致我的分数挺低的。