蒙特卡洛树搜索
全称 Monte Carlo Tree Search(MCTS),是一种人工智能问题中做出最优决策的方法,一般是在组合博弈中的行动(move)规划形式。它结合了随机模拟的一般性和树搜索的准确性。MCTS 受到快速关注主要是由计算机围棋程序的成功以及其潜在的在众多难题上的应用所致。超越博弈游戏本身,MCTS 理论上可以被用在以 {状态 state,行动 action} 对定义和用模拟进行预测输出结果的任何领域。
一、程序设计
MCTS通过很多次模拟真实对局,当模拟的次数足够多,(利用大数定律)模拟后获得的最佳收益的节点,就接近于理论上真实的最佳收益节点。那么这个节点所包含的Action(行动)就是当前state(状态)下最佳的选择。以斗地主来为例,MCTS就是在当前各家手牌的情况下,在符合斗地主规则和出牌常规的情况下框定可选的行动,进行n次模拟对局,每次对局都执行不完全相同的行动,记录和更新每种行动带来的收益,最终选择收益最佳的行动(出牌、跟牌或不出)。
一般情况下MCTS适用于二人零和完全信息博弈游戏。然而斗地主是3人游戏,并且是非完全信息博弈游戏,MCTS能适用吗?
我们先来尝试简化这个问题。首先,如果斗地主是明牌的,在规则允许的情况下,每个玩家的可选行动对其他玩家都是透明的,那就变成了3人完全信息博弈游戏。其次,斗地主中3个人分为两派,可以将2个农民和1地主之间的博弈,看成2人零和游戏。这样来考虑之后,斗地主就变成了MCTS擅长的“二人零和完全信息博弈游戏”了,理论上应该是可以实现的。
MCTS循环
每一次MCTS树搜索分为4个步骤:
1.选择(selection):从非叶子节点中选择未完全扩展的节点进行扩展;如果叶子节点都已扩展,选择UCT最高的节点。
2.扩展(extension):
选择第一个未尝试的行动。
用这个行动创建一个新的MCTS节点。其中父节点为当前节点,游戏状态为执行行动之后的游戏状态,此行动作为节点引发的行动。
将扩展出来的新节点加入MCTS树上。
返回新节点。
3.模拟(simulation):推演游戏过程,最终返回游戏结果信息(一般包括游戏得分,胜方等)。
4.反馈(backpropagation):反向传播游戏结果,更新各节点的q和n。
重复进行以上4个步骤足够多的次数,就可以通过选择UCT最高的节点作为下一步行动。
二、效果实现
AI 对局
其他效果省略
三、核心算法
本次毕设系统在设计中,主要采用Nodejs html javascript css结合的方式实现前端扑克牌对局,其中Nodejs主要实现对局算法的实现以及接收界面端用户操作的数据,实现用户线上对局的总体功能点。
核心算法实现
class AI{
constructor(param) {
param = param || {};
this.player = param.player;
this.game = param.game;
}
classify(pokerList){
pokerList.sort(this.sortFunction);
if(pokerList.length===0){
return {1: [],2: [],3: [],4: []};
}
let lastPoker = pokerList[0];
let curList = [lastPoker];
let lists = [];
for(let i=1; i<pokerList.length; i++){
if(pokerList[i].number !== lastPoker.number){
lists.push(curList);
curList = [pokerList[i]];
}else{
curList.push(pokerList[i]);
}
lastPoker = pokerList[i];
}
lists.push(curList);
let Count1List = [];let Count2List = [];
let Count3List = [];let Count4List = [];
for(let i=0; i<lists.length; i++){
if(lists[i].length === 3){
Count3List.push(lists[i]);
}else if(lists[i].length === 2){
Count2List.push(lists[i]);
}else if(lists[i].length === 1){
Count1List.push(lists[i]);
}else if(lists[i].length === 4){
Count4List.push(lists[i]);
}
}
return {
1: Count1List,
2: Count2List,
3: Count3List,
4: Count4List,
};
}
getClassifyObj(pokerList0){
let poker15 = [];
let poker16 = [];
let poker17 = [];
let pokerList = pokerList0.slice(0);
for(let i=0; i<pokerList.length; i++){
if(pokerList[i].number === 15){
let poker = pokerList.splice(i,1);
i--;
poker15.push(poker[0]);
}else if(pokerList[i].number === 16){
let poker = pokerList.splice(i,1);
i--;
poker16.push(poker[0]);
}else if(pokerList[i].number === 17){
let poker = pokerList.splice(i,1);
i--;
poker17.push(poker[0]);
}
}
let obj = this.classify(pokerList);
let Count1List = obj[1];
let Count2List = obj[2];
let Count3List = obj[3];
let Count4List = obj[4];
let four = Count4List;
let three = [];
let threeList = [];
let two = [];
let twoList = [];
let one = [];
let oneList = [];
//接牌1 最小接 不拆 炸
getByObj1(lastObj){
let obj;
obj = this.getSmallestObjByObj(lastObj);
if(!obj){
obj = this.getByBoom(lastObj);
}
return obj;
}
//接牌2 最小接 炸 拆
getByObj2(lastObj){
let obj;
obj = this.getByObj1(lastObj);
if(!obj){
obj = this.getBySplit(lastObj);
}
return obj;
}
//接牌3 最小接 不拆 不炸 不出王、2、AAA
getByObj3(lastObj){
let obj;
obj = this.getSmallestObjNoBig(lastObj);
return obj;
}
//接牌4
getByObj4(lastObj){
let obj;
if(lastObj.type==='one'){
obj = this.getByObj5(lastObj);
}else{
obj = this.getByObj2(lastObj);
}
return obj;
}
}