前端人工智能:防守算法逻辑处理

127 阅读13分钟

快捷目录

前端人工智能:来谈谈前端人工智能

前端人工智能:前端AI研发之算法

前端人工智能:canvas 绘制棋盘线条

前端人工智能:canvas 绘制黑白棋子

前端人工智能:canvas 绘制棋盘及落子逻辑

前端人工智能:【算法实战】胜利方式算法逻辑处理(上)

前端人工智能:【算法实战】胜利方式算法逻辑处理(下)


在上一节中,我们完成了五子棋的基本算法,确定了五子棋 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 ,表示最大的分数;

再设置一个uv,用来表示人工智能的落子点;

  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('游戏结束!恭喜你获胜了')
      }
    }
  }

我只是把ij调换前后位置了,那么它的结果会产生变化吗?我们在代码中做了一个chessBoard的控制台打印,接下来我们来一起看看。

当我们点击两颗棋子时,我们可以看到打印出来的数组是这样的,是跟棋盘一一对应的:

而当我们点击这里的时候,可以看到这个时候棋盘的显示就是反过来的了:

也就是说现在的棋盘稍微得反着看,你可以理解为棋盘转了个 90 度。也就是说现在棋盘的第一行,其实就是数组的第一列了:

是不是到目前为止,就开始觉得这个算法稍微有点复杂烧脑了。但当我们更改ij的前后位置后,其实还是依然维持了之前不能重复落子的逻辑:

因为我们是横向和纵向循环的方式,所以我们把落子点改成了现在这个方式。那我现在问大家一个问题,我们现在有落子点的了,那我们的人工智能计算还有意义吗?

或者这么问,人工智能是应该把棋子落在有落子点的这个位置吗?那我们肯定是把棋子落在没有落子点的位置对吧,也就是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>