中学时看过一本关于围棋的漫画《棋魂》,奈何天赋有限,围棋至今也不会……好吧,退而求其次,五子棋相对简单一点。对着网上的教程实现了一个简单的五子棋:
其实ui的实现并不难,主要记录下ai的思路吧。
// 绘制棋盘
for (var i = 0; i < 15; i++) {
context.beginPath();
context.moveTo(15 + i * 30, 15);
context.lineTo(15 + i * 30, 435);
context.stroke();
context.beginPath();
context.moveTo(15, 15 + i * 30);
context.lineTo(435, 15 + i * 30);
context.stroke();
};
五子棋的棋盘为15*15,落子黑先白后,落子的过程其实就是在绘制旗子。
// 绘制棋子
function oneStep(x, y, color) {
// x,y为棋子在棋盘的坐标索引,color为黑棋或白棋
context.beginPath();
context.arc(15 + x * 30, 15 + y * 30, 12, 0, 2 * Math.PI);
context.closePath();
var gradient = context.createRadialGradient(15 + x * 30 + 2, 15 + y * 30 - 2, 12, 15 + x * 30 + 2, 15 + y * 30 - 2, 0);
if (color) {
gradient.addColorStop(0, '#0a0a0a');
gradient.addColorStop(1, '#636766');
} else {
gradient.addColorStop(0, '#d1d1d1');
gradient.addColorStop(1, '#f9f9f9');
};
context.fillStyle = gradient;
context.fill();
}
需要一个二维数组记录当前棋盘的落子情况,每次落子需要判断胜负以及是否结束。
var me = true;
var chessBoard = []; //创建一个二维数组用于记录当前棋盘的落子情况
var wins = []; //三维数组记录五子棋所有的赢法
var count = 0; //记录五子棋所有赢法的索引
var over = false;
//落子事件
chess.onclick = function(e) {
var x = e.offsetX;
var y = e.offsetY;
var i = Math.floor(x / 30);
var j = Math.floor(y / 30);
if (chessBoard[i][j] == 0 && !over) { //没有落子的位置才能落子,黑子为1,白子为2
oneStep(i, j, me);
if (me) {
chessBoard[i][j] = 1;
} else {
chessBoard[i][j] = 2;
};
console.log(chessBoard);
winner(i, j, me);
me = !me;
if (!over) {
computerAI(me);
};
};
}
先说判断胜负,其实无论哪方,五子连珠作为胜利的条件,在15*15的棋盘上胜利的所有情况是可以枚举出来的。
for (var i = 0; i < 15; i++) { //横向统计所有的赢法
for (var j = 0; j < 11; j++) {
for (var k = 0; k < 5; k++) {
wins[i][j + k][count] = true;
};
count++;
};
};
for (var i = 0; i < 15; i++) { //纵向统计所有的赢法
for (var j = 0; j < 11; j++) {
for (var k = 0; k < 5; k++) {
wins[j + k][i][count] = true;
};
count++;
};
};
for (var i = 0; i < 11; i++) { //斜向统计所有的赢法
for (var j = 0; j < 11; j++) {
for (var k = 0; k < 5; k++) {
wins[i + k][j + k][count] = true;
};
count++;
};
};
for (var i = 0; i < 11; i++) { //斜向统计所有的赢法
for (var j = 14; j > 3; j--) {
for (var k = 0; k < 5; k++) {
wins[i + k][j - k][count] = true;
};
count++;
};
};
console.log(count);
关键在于wins这个三维数组,有点难理解,举个例子:假如五子棋只有一种赢法:
var black = [];
var white = [];
//分别记录五子棋黑白的赢法数组
这两个数组结合wins数组来判断胜负,五子棋共有572种赢法,默认黑子与白子的赢法都为0。
for (var i = 0; i < count; i++) { //开局默认黑白所有的赢法都是0
black[i] = 0;
white[i] = 0;
};
还是用上面的例子,如果黑方在第一种赢法处落下一子,那么黑子的第一种赢法+1,同时白子此种赢法作废。
//判断输赢
function winner(i, j, color) {
for (var k = 0; k < count; k++) {
if (wins[i][j][k]) {
if (color) {
black[k]++;
white[k] = 6;
//如果某种赢法黑子已经落子,白子此种赢法就作废
} else {
white[k]++;
black[k] = 6;
};
if (black[k] == 5) {
alert('黑子获胜');
over = true;
};
if (white[k] == 5) {
alert('白子获胜');
over = true;
};
};
};
}
代码到这里,已经能实现五子棋的规则逻辑了,接下来实现ai。
var blackScore = [];
var whiteScore = [];
//分别记录五子棋黑白的二维得分数组
这个ai其实挺简单的,实现思路就是通过遍历每一个能落子的空坐标,然后结合算法找出分数最高的一个位置落子。
//AI
function computerAI(color) {
var max = 0;
var u = 0;
var v = 0;
// 保存最大的分数和相应坐标
for (var i = 0; i < 15; i++) {
blackScore[i] = [];
whiteScore[i] = [];
for (var j = 0; j < 15; j++) {
blackScore[i][j] = 0;
whiteScore[i][j] = 0;
};
};
//每个坐标的分数为零
//遍历每个空坐标,如果某种赢法已经落子的数量越大则该坐标加分越多
//同理拦截对方的落子
//加分的数值很重要
for (var i = 0; i < 15; i++) {
for (var j = 0; j < 15; j++) {
if (chessBoard[i][j] == 0) {
for (var k = 0; k < count; k++) {
if (wins[i][j][k]) {
switch (black[k]) {
case 1:
blackScore[i][j] += 2;
break;
case 2:
blackScore[i][j] += 5;
break;
case 3:
blackScore[i][j] += 20;
break;
case 4:
blackScore[i][j] += 50;
break;
}
switch (white[k]) {
case 1:
whiteScore[i][j] += 2;
break;
case 2:
whiteScore[i][j] += 5;
break;
case 3:
whiteScore[i][j] += 20;
break;
case 4:
whiteScore[i][j] += 50;
break;
}
//找出得分最高的坐标点
if (blackScore[i][j] > max) {
max = blackScore[i][j];
u = i;
v = j;
} else if (blackScore[i][j] == max) {
if (whiteScore[i][j] > whiteScore[u][v]) {
u = i;
v = j;
}
}
if (whiteScore[i][j] > max) {
max = whiteScore[i][j];
u = i;
v = j;
} else if (whiteScore[i][j] == max) {
if (blackScore[i][j] > blackScore[u][v]) {
u = i;
v = j;
}
}
};
};
};
};
};
oneStep(u, v, color);
color ? chessBoard[u][v] = 1 : chessBoard[u][v] = 2;
winner(u, v, color);
me = !color;
}
五子棋的棋盘上,每一个位置都存在多种赢法,此算法的逻辑就是假如一个空坐标还未落子,那么遍历所有的赢法,如果黑方在此种赢法已经落下一子,那么这个坐标对黑方有利,加分;如果黑方落下二子,那么分数更高;白方也是同理……找出最有价值的坐标落子。
至于如何加分,可以借鉴网上的评分表:
代码只是实现了思路,没有优化;而且此类“”民间规则“”五子棋都是先手必胜,
在五子棋专业规则中规定,一共有26种开局。直指开局13种,斜指开局13种。
这26种开局分别是:
寒星 溪月 残月 雨月 金星 丘月 新月
山月 游星 长星 峡月 恒星 水月 流星
浦月 岚月 银月 明星 名月 彗星 花月
松月 疏星 斜月 瑞星 云月
其中公认黑必胜的开局有:花月,浦月
黑必败开局有:彗星,游星
以上之适用于五子棋专业规则
在民间规则里,几乎全部是黑先手必胜。
专业的五子棋比赛还有禁手、三手交换、五手两打等限制,以后有机会再研究吧……