快捷目录
在上一节中,我们完成了五子棋的基本算法,确定了五子棋 15 * 15 棋盘中的 572 种获胜方法,并且我们完成了横向、纵向、斜向胜利的算法。那么在本节当中,我们将要开启人工智能的算法模块。
那我们先来说说这个电脑人工智能怎么做吧,因为人工智能的算法其实是有N多种的。换句话说,你写的这种一定不是最好的。为什么这么说呢?
因为人工智能这种东西肯定是有登峰造极的,你自己的算法是不可能达到那么全面的。最简单的例子就是你看我们第一节中举例的阿尔法狗,强如它击败了韩国的超级围棋高手,但也是需要不停地完善的。所以说再厉害的团队写出来的再厉害的算法,也肯定会有能超越它的算法。
所以说我们现在的人工智能的算法仅作为一种参考的算法,也希望能作为大家学习人工智能算法的一个启蒙,但不代表现在的人工智能的算法就是最好的,我更希望能看到大家在学完本小册后能举一反三,写出更好的算法。
那接下来我们就正式开始人工智能算法的 - 防守算法篇:
人工智能防守算法推演
在五子棋的规则中,黑棋先行。而在上一节中,我们用了黑棋去执行算法,所以接下来我们就用人工智能去执行防守。也就是我们落子后,让人工智能去进行防守。
那我们期望的人工智能要怎么去执行防守呢?我们来看一下:
比如我们落子在图中的位置,那么是不是周围的 8 个点其实都有威胁性。这时候人工智能就会判断这 8 个点的威胁性已经达到了 10 分,而其他地方是 0 分,然后人工智能就会选择其中一个 10 分的点去进行防守。
而当我一旦这么落子的话,下面红色点的位置是不是就比上一张图的其他位置分数就更高了,也就是人工智能会更优先去走这两个点。
而这也是我们人工智能最主要的一个算法,那我们直接开始运算。我们先定义一个人工智能的函数,然后再定义一个myScore
数组,用来装载我们上面说的分值:
function computerAI() {
var myScore = []
}
那么下面我们还是一样去做一个二维数组,因为是 15 * 15 的盘面,所以我们得循环两遍。并且我们要初始化它的分数都为 0 :
function computerAI() {
var myScore = []
for (var i = 0; i < 15; i++) {
myScore[i] = []
for (var j = 0; j < 15; j++) {
myScore[i][j] = 0
}
}
}
那么现在横向和纵向的分数我们都有了,都设置为了 0 。然后我们还得再设置几个字段:
一个是 max
,表示最大的分数;
再设置一个u
很v
,用来表示人工智能的落子点;
var max = 0
var u = 0, v = 0
我们做好前期的准备工作之后呢,接下来的逻辑就开始复杂起来了。
我们接下来就得做一次彻底的循环:
function computerAI() {
var myScore = []
var max = 0
var u = 0, v = 0
for (var i = 0; i < 15; i++) {
myScore[i] = []
for (var j = 0; j < 15; j++) {
myScore[i][j] = 0
}
}
for (var i = 0; i < 15; i++) {
for (var j = 0; j < 15; j++) {}
}
}
那么循环有了,可是什么时候我们这个东西才有判断的意义呢?我先问下大家还记得chessBoard
吗?前两节中,我们用它作为装载棋盘的一个数组,并且把它用于判断同一个落子点:
那当我换种写法的时候,大家还看得懂吗:
// 落子
chess.onclick = function (e) {
// 横坐标
var i = Math.floor(e.offsetX / 30)
// 纵坐标
var j = Math.floor(e.offsetY / 30)
if (chessBoard[i][j] == 1) {
return
}
var context = chess.getContext("2d");
context.beginPath();
context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
var grd = context.createRadialGradient(15 + i * 30, 15 + j * 30, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0)
grd.addColorStop(0, 'black')
grd.addColorStop(1, 'white')
context.fillStyle = grd
context.fill()
context.stroke()
chessBoard[i][j] = 1
console.log(chessBoard);
for (var k = 0; k < count; k++) {
if (wins[i][j][k]) {
myWin[k]++
}
if (myWin[k] == 5) {
console.log('游戏结束!恭喜你获胜了')
}
}
}
我只是把i
和j
调换前后位置了,那么它的结果会产生变化吗?我们在代码中做了一个chessBoard
的控制台打印,接下来我们来一起看看。
当我们点击两颗棋子时,我们可以看到打印出来的数组是这样的,是跟棋盘一一对应的:
而当我们点击这里的时候,可以看到这个时候棋盘的显示就是反过来的了:
也就是说现在的棋盘稍微得反着看,你可以理解为棋盘转了个 90 度。也就是说现在棋盘的第一行,其实就是数组的第一列了:
是不是到目前为止,就开始觉得这个算法稍微有点复杂烧脑了。但当我们更改i
和j
的前后位置后,其实还是依然维持了之前不能重复落子的逻辑:
因为我们是横向和纵向循环的方式,所以我们把落子点改成了现在这个方式。那我现在问大家一个问题,我们现在有落子点的了,那我们的人工智能计算还有意义吗?
或者这么问,人工智能是应该把棋子落在有落子点的这个位置吗?那我们肯定是把棋子落在没有落子点的位置对吧,也就是chessBoard
中为 0 的地方。
于是我们就可以做判断,当这个落子点chessBoard[i][j]
为 0 的时候,再去做相关的逻辑操作:
function computerAI() {
var myScore = []
var max = 0
var u = 0, v = 0
for (var i = 0; i < 15; i++) {
myScore[i] = []
for (var j = 0; j < 15; j++) {
myScore[i][j] = 0
}
}
// 横向和纵向循环
for (var i = 0; i < 15; i++) {
for (var j = 0; j < 15; j++) {
if (chessBoard[i][j] == 0) {
}
}
}
}
什么样的逻辑操作呢?还是做循环:
if (chessBoard[i][j] == 0) {
for (var k = 0; k < count; k++) {
}
}
上面逻辑的意思是,我们在所有没有落子的位置,去循环了一遍它所有的赢法。什么意思呢,这里是稍微有点难理解的,我们结合图来看一下:
当我们落子到了这三个点时,那是不是这三个点其实就已经被我们的算法给剔除了,然后我们在剩下的为 0 的位置去进行计算。
而有 0 的位置也有它自己的赢法方式,所以我们需要做一个判断:
if (chessBoard[i][j] == 0) {
for (var k = 0; k < count; k++) {
if (wins[i][j][k]) {
}
}
}
这个wins[i][j][k]
肯定也是我们剩余有 0 的位置的赢法方式中的其中一种,那么我们一旦找着了,接下来就使用 JavaScript 中的 switch
语句去进行判断 - switch
语句用于根据不同的条件执行不同的代码块。
我们判断myWin
(用于统计某种赢法上已有多少个棋子)中的这个赢法到底有多少颗棋子。
如果我方棋子数为 1,那么我们可以给电脑的分数定为 200;
如果我方棋子数为 2,那么我们可以给电脑的分数定为 500,提醒电脑说这时候你要注意了;
如果我方棋子数为 3,那么我们可以给电脑的分数定为 2000,提醒电脑要更注意了;
如果我方棋子数为 4,那么我们可以给电脑的分数定为 10000,提醒电脑必须要去堵,不然就要输了;
如果我方棋子数为 5,那么其实已经不需要再去进行任何堵截了,对吧。
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 (myWin[k]) {
case 1: myScore[i][j] += 200;
break;
case 2: myScore[i][j] += 500;
break;
case 3: myScore[i][j] += 2000;
break;
case 4: myScore[i][j] += 10000;
break;
}
}
}
}
}
}
console.log(myScore);
那我们看当我方落子时,人工智能的分数是不是达到我们想要的效果了:
当我们横向落子三颗时,这时对应的是myScore
的纵向。然后我们可以看到前三个位置的分值为 0 ,因为这时候我们已经落子了。而第四颗的分值为 2700,就代表我们的分数是没有问题的。
那我们分数有了,接下来我们就要告诉人工智能该怎么堵了,所以我们得把要堵的这个点给它算出来。也就是说其实我们只需要循环数组,然后找到分值最大的那一个,给它堵上其实就没有问题了。如果分值最大的有多个,那我们就把循环到的第一个堵上就可以了。
这个时候我们就可以用到经典的 “打擂台算法”,可以用于求若干个数中的最大/小。也就是循环这堆数值,一旦数值比max
大,那么就替换掉max
的值,直至找到最大值。
而一旦我们找到最大值,就去记录这个值对应的坐标。
// 计算要堵的点
if (myScore[i][j] > max) {
max = myScore[i][j];
u = i
v = j
}
在我们循环结束后,我们来看看我们这个点算得对不对:
我们可以看到当我们横向落了四颗棋子后,电脑计算的坐标为(4,0),刚好防守住了我们的五子互连。证明我们的算法是生效了的,那么接下来我们就把棋子填充上去:
var context = chess.getContext('2d')
context.beginPath();
context.arc(15 + u * 30, 15 + v * 30, 13, 0, 2 * Math.PI);
context.fillStyle = 'white';
context.fill();
context.stroke()
然后我们再来看看现在的效果:
我们可以看到前面的效果都挺好,人工智能都能很快速地去将我们的位置补上。但是当我们这么去落子的时候,哎,我们突然发现人工智能失效了:
解决人工智能重复落子问题
其实是因为这样的,当我们去进行落子的时候,我们是不是在这之前已经去做过记录了,并且还利用这个记录来判断不能落子在同一个位置。
可是我们却没跟人工智能说到这一点,所以其实上面的效果不是人工智能失效了,而是它重复落子了。所以最后在电脑落子完以后,我们也需要去记录它的落子位置,将这个点赋值为 2 ,因为 1 是我们自己落子的值。
// 填充白色棋子
var context = chess.getContext('2d')
context.beginPath();
context.arc(15 + u * 30, 15 + v * 30, 13, 0, 2 * Math.PI);
context.fillStyle = 'white';
context.fill();
context.stroke()
chessBoard[u][v] = 2
这时候我们再去试试看:
我们可以看到就不会出现像刚才那个问题了,而且人工智能防守得还挺好的。反正就我这实力而言,还是得绞尽脑汁才能找到突破口。
那么截至目前为止,我们人工智能的防守算法就做完了。本节的算法相比之前确实复杂很多,而且我们也多次利用到了二维数组、三维数组。而我相信大家也没遗忘还有最重要的一点,就是目前人工智能还只会防守,那它是不是还得会进攻?
没错了,在下一节开始,我们将开始人工智能的攻击算法。也希望大家多阅读几遍本节内容,争取去把本节的算法熟悉透,然后自己动手去写一下。
本节代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<canvas id="chess" width=800 height=800></canvas>
</body>
<script type="text/javascript">
var chess = document.getElementById("chess");
// 棋盘数组
var chessBoard = []
// 赢法数组
var wins = []
// 一共有多少种赢法
var count = 0
// 赢法统计数组
var myWin = []
var context = chess.getContext("2d");
context.strokeStyle = "#666";
context.beginPath()
// 绘制棋盘
for (var i = 0; i < 15; i++) {
// 横线起点
context.moveTo(15, 15 + i * 30)
// 横线终点
context.lineTo(435, 15 + i * 30)
// 竖线起点
context.moveTo(15 + i * 30, 15)
// 竖线终点
context.lineTo(15 + i * 30, 435)
// 结束绘画
context.stroke()
}
// 数组显示棋盘
for (var i = 0; i < 15; i++) {
chessBoard[i] = []
for (var j = 0; j < 15; j++) {
chessBoard[i][j] = 0
}
}
// 初始化赢法数组
for (var i = 0; i < 15; i++) {
wins[i] = []
for (var j = 0; j < 15; j++) {
wins[i][j] = []
}
}
// 计算横向有多少种赢法
// i - 纵向 j - 横向
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++
}
}
// 计算纵向有多少种赢法
// i - 纵向 j - 横向
for (var i = 0; i < 11; i++) {
for (var j = 0; j < 15; j++) {
for (var k = 0; k < 5; k++) {
wins[i + k][j][count] = true
}
count++
}
}
//计算左斜有多少种赢法
// i - 纵向 j - 横向
for (var i = 14; i >= 4; i--) {
for (var j = 0; j < 11; j++) {
for (var k = 0; k < 5; k++) {
wins[i - k][j + k][count] = true
}
count++
}
}
//计算右斜有多少种赢法
// i - 纵向 j - 横向
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 < count; i++) {
myWin[i] = 0
}
// 落子
chess.onclick = function (e) {
// 横坐标
var i = Math.floor(e.offsetX / 30)
// 纵坐标
var j = Math.floor(e.offsetY / 30)
if (chessBoard[i][j] == 1) {
console.log('不能重复落子');
return
}
var context = chess.getContext("2d");
context.beginPath();
context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
var grd = context.createRadialGradient(15 + i * 30, 15 + j * 30, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0)
grd.addColorStop(0, 'black')
grd.addColorStop(1, 'white')
context.fillStyle = grd
context.fill()
context.stroke()
chessBoard[i][j] = 1
for (var k = 0; k < count; k++) {
if (wins[i][j][k]) {
myWin[k]++
}
if (myWin[k] == 5) {
console.log('游戏结束!恭喜你获胜了')
}
}
computerAI()
}
// 人工智能模块
function computerAI() {
var myScore = []
var max = 0
var u = 0, v = 0
for (var i = 0; i < 15; i++) {
myScore[i] = []
for (var j = 0; j < 15; j++) {
myScore[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 (myWin[k]) {
case 1: myScore[i][j] += 200;
break;
case 2: myScore[i][j] += 500;
break;
case 3: myScore[i][j] += 2000;
break;
case 4: myScore[i][j] += 10000;
break;
}
}
}
// 计算要堵的点
if (myScore[i][j] > max) {
max = myScore[i][j];
u = i
v = j
}
}
}
}
// 填充白色棋子
var context = chess.getContext('2d')
context.beginPath();
context.arc(15 + u * 30, 15 + v * 30, 13, 0, 2 * Math.PI);
context.fillStyle = 'white';
context.fill();
context.stroke()
chessBoard[u][v] = 2
}
</script>
</html>