js+canvas实现AI五子棋小游戏_☆往事随風☆的博客
js实现AI五子棋小游戏
- 一、前言
- 二、开发流程
- (1)绘制棋盘
- (2)绘制棋子
- (3)玩家下棋
- (4)电脑下棋
- (5)清空棋盘
- (6)悔棋功能
- (7)撤销悔棋功能
- (8)判定输赢
- (9)判定平局
- 三、完整游戏效果图
- 四、运行结果展示
- 五、项目源码
一、前言
使用 js+canvas+less 制作一个简易的五子棋小游戏,游戏自带AI下棋,并且带有悔棋功能。这里只介绍一下js部分,其它部分请查看源码。
二、开发流程
(1)绘制棋盘
这里使用canvas绘制一个 15 x 15大小的棋盘。
// 绘制棋盘function chessBoard() {for (var i = 0; i < chessWidth; i++) {// 绘制棋盘横向线段ctx.save();ctx.beginPath();ctx.moveTo(20, 20 + i * 40);ctx.lineTo(580, 20 + i * 40);ctx.stroke();ctx.restore();// 绘制棋盘纵向线段ctx.save();ctx.beginPath();ctx.moveTo(20 + i * 40, 20);ctx.lineTo(20 + i * 40, 580);ctx.stroke();ctx.restore();}}

(2)绘制棋子
我们的棋子应该要绘制在横线和竖线的交点上,并且要实现鼠标点击捕获绘制功能,意思就是,当玩家鼠标点击以交点为中心的某个范围(如下图 红色区域)时,棋子也能落在线条交点上。

这里有两种绘制方式,第一种就是将鼠标点击棋盘后相对于棋盘的坐标进行取整(由于我们棋盘间隔是40,所以对坐标除以40取整),然后将取整后的坐标传给绘制棋子函数,以此来确定落子位置。第二种方式是通过数组遍历,计算当前位置的坐标,然后再进行棋子的绘制。不难看出第一种方式要比第二种方式的效率高。
// 绘制棋子function drawChess(eventX, eventY, flag) {ctx.fillStyle = flag ? "#000" : "#fff";ctx.beginPath();ctx.arc(20 + eventX * 40, 20 + eventY * 40, 10, 0, 360 * Math.PI / 180, true);ctx.fill();// 将棋子聚焦到线条的交点上(方式二,性能较低)// var wrap = document.querySelector(".wrapper");// eventX = event.clientX - wrap.offsetLeft;// eventY = event.clientY - wrap.offsetTop;// for (var i = 0; i < chessWidth; i++) {// for (var j = 0; j < chessWidth; j++) {// if (eventX >= (20 + j * 40 - 20) && eventX <= (20 + j * 40 + 20) && eventY >= (20 + i * 40 - 20) && eventY <= (20 + i * 40 + 20)) {// eventX =0 * j + 20;// eventY = 40 * i + 20;// break;// }// }// }// ctx.beginPath();// ctx.arc(eventX, eventY, 10, 0, 360 * Math.PI, true);// ctx.fill();}
(3)玩家下棋
有了棋盘和棋子,就要开始下棋啦,玩家下棋的话没有什么难度,需要注意的就是前面提到的传递给绘制棋子函数的坐标(eventX,eventY)的取整问题,以及和UI之间关联。
//玩家下棋
chess.addEventListener("click", function (event) {event = event || window.event;if (isMan == false) {alert("不要急,还没有轮到你哦。");return}if (gameOver == true) {alert("游戏已经结束,请点击重新开始。");return}// 使棋子落在棋盘线条焦点上eventX = Math.floor(event.offsetX / 40);eventY = Math.floor(event.offsetY / 40);console.log(eventX);// 如果当前位置没有落子才能落子if (chessPlace[eventX][eventY] == 0) {drawChess(eventX, eventY, true);playerData();downMp3.play();isRetract = true;isUnretract = false;isRestart = true;chessPlace[eventX][eventY] = 1;// 判断输赢if (win(eventX, eventY, num = 1)) {gameOver = true;// 记录数据playerData(true);winMp3.play();isMan = true;var choice = confirm("你赢了,再来一局?");if (choice) {clear();// 清空落子步数和分数playerData("", "clearPath");computerData("", "clearPath");gameOver = false;}}// 判断是否平局else if (tie()) {gameOver = true;isMan = true;var choice = confirm("占成平局,再来一局?");if (choice) {clear();// 清空落子步数和分数playerData("", "clearPath");computerData("", "clearPath");gameOver = false;}} else {isMan = false;}} else {alert("当前位置已被占据,请选择其他位置落子");}if (gameOver == false && isMan == false) {// 电脑下棋computerDown();}});
(4)电脑下棋
电脑下棋的话应该是整个游戏制作过程中最为复杂的一步,这里我采用的是五元组和计分表算法来实现电脑AI下棋。
1.五元组:
五子棋盘为15 x 15 的大小,横竖斜四个方向共有572个五元组,给每个五元组一个评分(或权重),这个五元组为它的每个位置贡献的分数就是这个五元组自身的得分,对整个棋盘来说,每个位置的得分就是该位置所在的横竖斜四个方向的所有五元组的得分之和,
然后从所有空位置中选出得分最高的位置就是电脑落子的最优位置。

如图所示:图中的每一种颜色的长方形里面都有五颗棋子,每种颜色的长方形就是一个五元组,。依次从上到下,从左到右,从正斜反斜方向分别都能构成五元组。
2.计分表:
这里采用的是国外某大佬所给出的一套目前来说最优的计分表,这个计分表可以使AI十分聪明。
function chessScore(playerNum, computerNum) {// 机器进攻// 1.既有人类落子,又有机器落子,判分为0if (playerNum > 0 && computerNum > 0) {return 0;}// 2.全部为空没有棋子,判分为7(14)if (playerNum == 0 && computerNum == 0) {return 14;}// 3.机器落一子,判分为35(70)if (computerNum == 1) {return 70;}// 4.机器落两子,判分为800(1600)if (computerNum == 2) {return 1600;}// 5.机器落三子,判分为15000(30000)if (computerNum == 3) {return 30000;}// 6.机器落四子,判分为800000(1600000)if (computerNum == 4) {return 1600000;}// 机器防守// 7.玩家落一子,判分为15(30)if (playerNum == 1) {return 30;}// 8.玩家落两子,判分为400(800)if (playerNum == 2) {return 800;}// 9.玩家落三子,判分为1800(3600)if (playerNum == 3) {return 3600;}// 10.玩家落四子,判分为100000(200000)if (playerNum == 4) {return 200000;}return -1; //如果是其他情况,则出现错误,不会执行该段代码}
遍历五元组得出权重最大的空位置进行落子,此时是AI落子的最优位置。
function computerDown() {// 初始化score评分组for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth; j++) {score[i][j] = 0;}}// 五元组中黑棋(玩家)数量var playerNum = 0;// 五元组中白棋(电脑)数量var computerNum = 0;// 五元组临时得分var tempScore = 0;// 最大得分var maxScore = -1;// 横向寻找for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth - 4; j++) {for (var k = j; k < j + 5; k++) {// 如果是玩家落得子if (chessPlace[k][i] == 1) {playerNum++;} else if (chessPlace[k][i] == 2) { //如果是电脑落子computerNum++;}}// 将每一个五元组中的黑棋和白棋个数传入评分表中tempScore = chessScore(playerNum, computerNum);// 为该五元组的每个位置添加分数for (var k = j; k < j + 5; k++) {score[k][i] += tempScore;}// 清空五元组中棋子数量和五元组临时得分playerNum = 0;computerNum = 0;tempScore = 0;}}// 纵向寻找for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth - 4; j++) {for (var k = 0; k < j + 5; k++) {if (chessPlace[i][k] == 1) {playerNum++;} else if (chessPlace[i][k] == 2) { computerNum++;}}tempScore = chessScore(playerNum, computerNum);for (var k = j; k < j + 5; k++) {score[i][k] += tempScore;}playerNum = 0;computerNum = 0;tempScore = 0;}}// 反斜线寻找// 反斜线上侧部分for (var i = chessWidth - 1; i >= 4; i--) {for (var k = i, j = 0; j < chessWidth && k >= 0; j++, k--) {var m = k; //x 14 13var n = j; //y 0 1for (; m > k - 5 && k - 5 >= -1; m--, n++) {if (chessPlace[m][n] == 1) {playerNum++;} else if (chessPlace[m][n] == 2) { computerNum++;}}// 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况if (m == k - 5) {tempScore = chessScore(playerNum, computerNum);for (m = k, n = j; m > k - 5; m--, n++) {score[m][n] += tempScore;}}playerNum = 0;computerNum = 0;tempScore = 0;}}// 反斜线下侧部分for (var i = 1; i < 15; i++) {for (var k = i, j = chessWidth - 1; j >= 0 && k < 15; j--, k++) {var m = k; //y 1 var n = j; //x 14for (; m < k + 5 && k + 5 <= 15; m++, n--) {if (chessPlace[n][m] == 1) {playerNum++;} else if (chessPlace[n][m] == 2) {computerNum++;}}// 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况if (m == k + 5) {tempScore = chessScore(playerNum, computerNum);for (m = k, n = j; m < k + 5; m++, n--) {score[n][m] += tempScore;}}playerNum = 0;computerNum = 0;tempScore = 0;}}// 正斜线寻找// 正斜线上侧部分for (var i = 0; i < chessWidth - 1; i++) {for (var k = i, j = 0; j < chessWidth && k < chessWidth; j++, k++) {var m = k;var n = j;for (; m < k + 5 && k + 5 <= chessWidth; m++, n++) {if (chessPlace[m][n] == 1) {playerNum++;} else if (chessPlace[m][n] == 2) { computerNum++;}}// 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况if (m == k + 5) {tempScore = chessScore(playerNum, computerNum);for (m = k, n = j; m < k + 5; m++, n++) {score[m][n] += tempScore;}}playerNum = 0;computerNum = 0;tempScore = 0;}}// 正斜线下侧部分for (var i = 1; i < chessWidth - 4; i++) {for (var k = i, j = 0; j < chessWidth && k < chessWidth; j++, k++) {var m = k;var n = j;for (; m < k + 5 && k + 5 <= chessWidth; m++, n++) {if (chessPlace[n][m] == 1) {playerNum++;} else if (chessPlace[n][m] == 2) { computerNum++;}}// 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况if (m == k + 5) {tempScore = chessScore(playerNum, computerNum);for (m = k, n = j; m < k + 5; m++, n++) {score[n][m] += tempScore;}}playerNum = 0;computerNum = 0;tempScore = 0;}}// 从空位置中找到得分最大的位置for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth; j++) {if (chessPlace[i][j] == 0 && score[i][j] > maxScore) {goalX = i;goalY = j;maxScore = score[i][j];}}}if (goalX != -1 && goalY != -1 && chessPlace[goalX][goalY] == 0) {// 落子drawChess(goalX, goalY, false);// 保存游戏数据computerData();// 保存该位置的落子chessPlace[goalX][goalY] = 2;// 判断输赢if (win(goalX, goalY, num = 2)) {gameOver = true;computerData(true);isMan = true;var choice = confirm("你输了,再来一局?");failMp3.play();if (choice) {clear();playerData("", "clearPath");computerData("", "clearPath");gameOver = false;}} else if (tie()) {gameOver = true;isMan = true;var choice = confirm("占成平局,再来一局?");if (choice) {clear();playerData("", "clearPath");computerData("", "clearPath");gameOver = false;}} else {isMan = true;}}}
(5)清空棋盘
清空棋盘可以使用canvas中的clearRect方法来实现,需要注意的是每次清空棋盘都要重置棋盘位置信息。
// 清空棋盘function clear() {// 清空棋盘ctx.clearRect(0, 0, chess.width, chess.height);// 重新绘制棋盘chessBoard();for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth; j++) {// 重置棋盘各个位置信息为0(标识此时棋盘没有落子)chessPlace[i][j] = 0;}}}
(6)悔棋功能
悔棋功能主要是将要悔棋的位置的棋子清空,这里我采用了定点清除,重新绘制的方法。我们首先在要悔棋的坐标处绘制和棋子相同大小的圆将棋盘上的棋子覆盖掉,这时我们会发现,不仅棋子被覆盖掉,棋盘上的棋子覆盖区域的棋盘线条也被覆盖掉了,所以我们还要将覆盖掉的线条补上。
a.悔棋前:

b.悔棋后:

c.通过图中观察我们可以发现,缺失部分正是之前的鼠标点击捕获区域,所以我们只需要将这一块区域的线条补上就可以了。
// 清空棋子function clearChess(x, y) {// 清除该位置棋子ctx.clearRect(x * 40, y * 40, 40, 40);// 清除棋子的位置标记为零chessPlace[x][y] = 0;// 绘制被清除的棋盘线条x = x * 40 + 20;y = y * 40 + 20;// 绘制水平线ctx.beginPath();ctx.moveTo(x - 20, y);ctx.lineTo(x + 20, y);ctx.stroke();// 绘制垂直线ctx.beginPath();ctx.moveTo(x, y - 20);ctx.lineTo(x, y + 20);ctx.stroke();}
d.悔棋功能:
// 悔棋retract.addEventListener("click", function () {// 触发按钮点击音效clickSound.play();if (gameOver) {alert("游戏已经结束,无法悔棋!");} else if (isRetract == false) {alert("无法悔棋!");} else {isRetract = false; //表示已经悔过棋子了,不能再悔棋isUnretract = true; //悔过棋子后才可以撤销悔棋clearChess(eventX, eventY); //清除目标位置玩家棋子clearChess(goalX, goalY); //清除目标位置电脑棋子playerData("", "retract"); //重置玩家游戏数据computerData("", "retract"); //重置电脑游戏数据}});
(7)撤销悔棋功能
撤销悔棋功能还是比较简单的,只需要重新绘制一下上一步的棋子就可以。
// 撤销悔棋unretract.addEventListener("click", function () {// 触发按钮点击音效clickSound.play();if (gameOver) {alert("游戏已经结束,无法撤销悔棋!");} else if (isUnretract == false) {alert("无法撤销!");} else {isUnretract = false; //撤销后不能再次撤销isRetract = true; //撤销悔棋后,可以再次悔棋(当前落子位置)drawChess(eventX, eventY, true); //绘制目标位置玩家棋子drawChess(goalX, goalY, false); //绘制目标位置电脑棋子chessPlace[eventX][eventY] = 1;chessPlace[goalX][goalY] = 2;playerData(); //重置玩家游戏数据computerData(); //重置电脑游戏数据}});
(8)判定输赢
这里判定输赢的方式有很多种,但唯一不变的原则就是五子连珠。
方式一:遍历整个棋盘寻找五子连珠。
for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth; j++) {// 横向获胜if (chessPlace[i][j] != 0 && i < chessWidth - 4 &&chessPlace[i][j] == chessPlace[i + 1][j] &&chessPlace[i][j] == chessPlace[i + 2][j] &&chessPlace[i][j] == chessPlace[i + 3][j] &&chessPlace[i][j] == chessPlace[i + 4][j]) {return flag = "win";}// 纵向获胜if (chessPlace[i][j] != 0 && j < chessWidth - 4 &&chessPlace[i][j] == chessPlace[i][j + 1] &&chessPlace[i][j] == chessPlace[i][j + 2] &&chessPlace[i][j] == chessPlace[i][j + 3] &&chessPlace[i][j] == chessPlace[i][j + 4]) {return flag = "win";}// 正斜线获胜if (chessPlace[i][j] != 0 &&i < chessWidth - 4 && j < chessWidth - 4 &&chessPlace[i][j] == chessPlace[i + 1][j + 1] &&chessPlace[i][j] == chessPlace[i + 2][j + 2] &&chessPlace[i][j] == chessPlace[i + 3][j + 3] &&chessPlace[i][j] == chessPlace[i + 4][j + 4]) {return flag = "win";}}}//反斜线获胜for (var i = 0; i < chessWidth; i++) {for (var j = chessWidth - 1; j > 3; j--) {if (chessPlace[i][j] != 0 &&chessPlace[i][j] == chessPlace[i + 1][j - 1] &&chessPlace[i][j] == chessPlace[i + 2][j - 2] &&chessPlace[i][j] == chessPlace[i + 3][j - 3] &&chessPlace[i][j] == chessPlace[i + 4][j - 4]) {return flag = "win";}}}
方式二:从当前落子位置向四周位置寻找。

我们假设图中的红色方块是当前落子位置,红色圆形是其它棋子,那么只要从当前位置向四周进行寻找,只要有同色五颗连在一起,就判定获胜,否则就判定失败。
function win(eventX, eventY, num) {// 保存相同棋子连在一起的个数var count = 0;// 保存当前棋子坐标var x = eventX;var y = eventY;// 横向获胜for (var i = x - 1; i >= 0; i--) {if (chessPlace[i][y] == num) {count++;} else {break;}}for (var i = x + 1; i < chessWidth; i++) {if (chessPlace[i][y] == num) {count++;} else {break;}}if (count >= 4) {return true;}count = 0;// 纵向获胜for (var i = y - 1; i >= 0; i--) {if (chessPlace[x][i] == num) {count++;} else {break;}}for (var i = y + 1; i < chessWidth; i++) {if (chessPlace[x][i] == num) {count++;} else {break;}}if (count >= 4) {return true;}count = 0;// 正斜线获胜for (var i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {if (chessPlace[i][j] == num) {count++;} else {break;}}for (var i = x + 1, j = y + 1; i < chessWidth && j < chessWidth; i++, j++) {if (chessPlace[i][j] == num) {count++;} else {break;}}if (count >= 4) {return true;}count = 0;// 反斜线获胜for (var i = x - 1, j = y + 1; i >= 0 && j < chessWidth; i--, j++) {if (chessPlace[i][j] == num) {count++;} else {break;}}for (var i = x + 1, j = y - 1; i < chessWidth && j >= 0; i++, j--) {if (chessPlace[i][j] == num) {count++;} else {break;}}if (count >= 4) {return true;}count = 0;}
这里我使用的是第二种方法并且也推荐大家使用第二种方法,因为第一种方式需要从头遍历棋盘,而第二种方式只需从当前位置向四条方向上遍历查看是否有五连珠情况的出现,而不用从头遍历棋盘,所以使用方式二效率更高,且稳定。
(9)判定平局
通过遍历整个棋盘看是否有空位置,如果有则不是平局反之则是平局。
function tie() {var count = 0;for (var i = 0; i < chessWidth; i++) {for (var j = 0; j < chessWidth; j++) {if (chessPlace[i][j] != 0) {count++;} else {break;}}}if (count == 225) {return true;}}
到这里我们的五子棋就差不多开发完成了,接下来就是一些UI上面的处理,当然我这里只设计了人机对战并没有设计玩家对玩家,如果有感兴趣的小伙伴可以试着添加一个玩家对玩家的模块。
三、完整游戏效果图
1.菜单部分:

2.游戏部分:

四、运行结果展示
AI五子棋在线试玩
AI五子棋在线预览
五、项目源码
五子棋项目源码
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
