中国象棋AI实现——alpha-beta剪枝
一、简介
这是基于alpha-bata剪枝算法实现的一个中国象棋博弈程序,可以实现人机交互,AI具有初级的智能,可以应对一般的象棋新手。
界面用最基本的html+css+js实现,参考自中国象棋界面素材 AI逻辑同样用js实现
二、算法实现
在让AI自动做出落子决定前,必须提供一个方法让其判断哪种走法较优,也即需要一个局面评估函数,能够返回一个表示局面好坏的分数,对于AI方,这个分数越大越好。
//有一点需要强调:alpha-beta剪枝过程中走棋的双方可能会变化,但估分的标准一直是以黑方即AI方为准,黑方希望估值最大,红方希望估值最小。
function evaluate() {
var score = 0;
score += SingleChessScore();
score += ChouJu();
score += ShuangPao();
return score;
}
目前的局面评估只包括一个主要部分和两个分支部分,分别为单棋子棋力判断、抽车将军判断和双炮将军判断,后续可能会在github仓库上更新更多的局面评估方法。
主要部分SingleChessScore依次考虑棋面上每个单独的子,由它们本身的属性和所在位置决定它们的分数,黑方取正值,红方取负值,并累加起来。
棋力表部分参考自eleeye。
以马的分数表为例,马的分数表如下:
var Ma = [
[90, 90, 90, 96, 90, 96, 90, 90, 90],
[90, 96,103, 97, 94, 97,103, 96, 90],
[92, 98, 99,103, 99,103, 99, 98, 92],
[93,108,100,107,100,107,100,108, 93],
[90,100, 99,103,104,103, 99,100, 90],
[90, 98,101,102,103,102,101, 98, 90],
[92, 94, 98, 95, 98, 95, 98, 94, 92],
[93, 92, 94, 95, 92, 95, 94, 92, 93],
[85, 90, 92, 93, 78, 93, 92, 90, 85],
[88, 50, 90, 88, 90, 88, 90, 50, 88]//马的两个初始位置权值设小一点,防止AI的炮“盲目攻击马”
];
有了棋面评估函数,AI现在可以遍历每一个己方棋子的每一种走法,并判断该走法的分数,基于这个,已经可以实现一个贪心算法,该算法只考虑当前的一步,并选择对自己最有利的走法。
V1.0 贪心算法,AI是黑方
function AImove() {
var max_score = -100000000;
var from = [];
var to = [];
var can_eat = false;
for (var j = 0; j < 10; ++j) {
for (var i = 0; i < 9; ++i) {
if (map[j][i] < 0) {
var t = WhatSpace(j, i);//返回棋子的属性
var tmap = WhereCan(j,i,t);//返回能走的位置,分为能吃和不能吃(即目的棋子或空)
if(tmap!=null && tmap.length>0) {
for(var q=0;q<tmap.length;q++){
var dest = tmap[q];
var tmp = map[dest[0]][dest[1]];
map[dest[0]][dest[1]] = map[j][i];
map[j][i] = 0;
var score = evaluate(map);
if (score > max_score) {
from[0] = j;
from[1] = i;
to[0] = dest[0];
to[1] = dest[1];
max_score = score;
can_eat = tmp == 0 ? false : true;
}
map[j][i] = map[dest[0]][dest[1]];
map[dest[0]][dest[1]] = tmp;
}
}
}
}
}
console.log("now best: " + max_score);
if (can_eat) {
eat(from[0], from[1], to[0], to[1]);
}
else {
move(from[0], from[1], to[0], to[1]);
}
}
显然,这样的AI智能还不够,所以下一步我们用alpha-beta对其进行优化,使其能考虑接下来几步内的较优走法。
算法框架如下:
function alpha-beta(depth, alpha, beta) {
对于每一个棋子的每一种走法
修改局面为该走法落子后的局面
ret = alpha-beta(depth + 1, alpha, beta)
修改局面回落子前的局面
if 在MAX层, alpha = max(alpha, ret)
else if 在MIN层, beta = min(beta, ret)
if beta <= alpha return (在MAX层 ? alpha : beta)
return (在MAX层 ? alpha : beta)
}
具体代码如下:
//V2.0 alpha-beta算法,仍然假设AI是黑方
function AImove() {
console.log("best: " + alpha_beta(1, -1e9, 1e9));
if (AIcan_eat) {
eat(AIfrom[0], AIfrom[1], AIto[0], AIto[1]);
}
else {
move(AIfrom[0], AIfrom[1], AIto[0], AIto[1]);
}
}
var AIfrom = [];
var AIto = [];
var AIcan_eat = false;
function alpha_beta(depth, alpha, beta) {
if (depth >= 5) return evaluate(map);
for (var j = 0; j < 10; ++j) {
for (var i = 0; i < 9; ++i) {
if ((depth & 1) == 1 && map[j][i] < 0 || (depth & 1) == 0 && map[j][i] > 0) {//哪些棋子能走
var t = WhatSpace(j, i);//返回棋子的属性
var tmap = WhereCan(j,i,t);//返回能走的位置,分为能吃和不能吃(即目的棋子或空)
if(tmap!=null && tmap.length>0) {
for(var q=0;q<tmap.length;q++){
var dest = tmap[q];
var tmp = map[dest[0]][dest[1]];
map[dest[0]][dest[1]] = map[j][i];
map[j][i] = 0;
ret = alpha_beta(depth + 1, alpha, beta);
if (depth & 1 == 1) {
if (ret > alpha) {
alpha = ret;
if (depth == 1) {
AIfrom[0] = j;
AIfrom[1] = i;
AIto[0] = dest[0];
AIto[1] = dest[1];
AIcan_eat = !(tmp == 0);
}
}
}
else {
beta = Math.min(beta, ret);
}
map[j][i] = map[dest[0]][dest[1]];
map[dest[0]][dest[1]] = tmp;
if (beta <= alpha) return (depth & 1 == 1 ? alpha : beta);
}
}
}
}
}
return (depth & 1 == 1 ? alpha : beta);
}
可能每次都走同一种走法会使AI显得过于死板,所以在返回到第一层的所有结果中加入一点随机性
if (depth & 1 == 1) {
if (ret > alpha) {
if (depth == 1) {
console.log(j + '-' + i + getCText(dest[0],dest[1])[0] + "移动到 " + dest[0] + '-' + dest[1])
//如果新的最好结果比原最好结果只大了5分以内,以某种概率保持原最好结果,以提高随机性
if (ret - alpha < 5 && Math.random() > 0.8) {
console.log("跨过最优解法");
map[j][i] = map[dest[0]][dest[1]];
map[dest[0]][dest[1]] = tmp;
continue;
}
AIfrom[0] = j;
AIfrom[1] = i;
AIto[0] = dest[0];
AIto[1] = dest[1];
AIcan_eat = !(tmp == 0);
}
alpha = ret;
}
//console.log(j + " " + i + " " + "alpha: " + alpha);
}
为了提高玩家体验,增加了悔棋功能。
用一个栈保存之前走过的所有步数,每次悔棋从栈中弹出并恢复。
三、结果分析
alpha-beta剪枝层数大于4时,AI表现出较好的智能,能够与普通人类进行对战且有较大的胜算。由于象棋游戏本身有较大的不确定性,因此以下结果仅供参考。
对战局数 | AI获胜局数 | 胜率 |
10 | 8 | 80% |
搜索深度 | 每步用时 |
4 | <0.1s |
5 | <0.5s |
6 | 1~3s |
AI程序与成熟的象棋AI程序相比仍然有很多不足,主要体现在以下方面:
- 搜索层数过低
成熟的象棋程序中的AI算法一般能搜索到数十层的深度,这是我的算法和硬件设备所远远达不到的。 - 套路不足
尽管我已经为重炮将军和将军抽车这些套路加到棋力评估当中,仍然有很多危险的套路是AI所不能识别和避免的。 - 多棋子联动性差
棋面评估目前只限制在单子棋力评估,所以不能很好地联动多个棋子,造成较大的杀伤。但由于alpha-beta剪枝本身的特性,即使专门写两个或两个以上棋子的联动也不能有很大的改进,所以我放弃了优化这个方向。
一些可能起到优化作用的改进:
- 完善棋力表
在目前的程序中自始至终每个棋子都只有一个表,但一个表只能提供最粗略的估计,显然适应不了变化莫测的局面。为了提供更详细可靠的评估标准,可以将棋局分为三个阶段:开局、中局和残局,每个局面对应一个棋力评估表。 - 增加搜索深度
- 历史记录
很多同学都有提到这个点,不过我很好奇他们是怎么实现的。记住一个好的走法是需要记住整个局面吗,这样的代价似乎有点大,一局下来也不一定能遇到几次重复的局面,而你还要从出现过这个局面的历史记录里找出分数最高的走法,听起来很好,但我认为很难实现。如果不记住整个局面而记下局部区域的局面,那开销可能小一点,重复出现该局面的可能性也比较高,但算法就更复杂了。