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

223 阅读8分钟

快捷目录

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

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

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

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

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


上一节中,我们完成了整个棋盘的布局以及处理了落子的逻辑。但是其实还没完善,还是有一些很致命的问题的。比如同样的位置,我再点一遍,其实 canvas 还是会在那个位置再绘画一个棋子。那这也违反了五子棋中一个最简单的规则 - 不能在一个地方落俩子。

image.png

那接下来我们先来完善一下这个落子逻辑,把这个问题给解决掉:

二维数组逻辑处理

在开始前我想问下大家知道什么叫“二维数组”吗?那么二维数组通俗点来讲呢,就是数组套数组。那么我们为什么要使用这个呢?因为我们需要这些数据类型去帮我们进行计算,我们需要一个数组,然后把棋盘上所有的点都给放进去,用数组显示出一个棋盘。

因为我们用的是 canvas,不是 DOM,如果是 DOM 的话我们还可以在元素上做个简单判断。这也是我们使用canvas 的一个弊端,就跟每一种框架和技术类型都有其独特的优缺点。就像人类一样,每个人都有自己的长处和不足,我们需要根据自己的需求和目标来选择最适合自己的工具。有时候,我们需要在不同的框架和技术之间进行权衡和取舍,以便找到最佳的解决方案。

那么接下来,我们就开始正式去做第一步 - 用数组去表示棋盘:

我们用数组去定义一个棋盘,用于存储游戏中的棋子。在 JavaScript 中,可以使用一个二维数组来表示棋盘,其中每个元素表示一个棋子的状态,0 可以用来表示该位置没有棋子,1 可以用来表示该位置有黑子,2 可以表示该位置有白子。

我们先定义一个数组:

然后我们定义一个for循环,让chessBoard这个数组里面的每一项至少还有个数组。然后我们还有纵向的,给它赋值为 0,就代表没有子::

接下来我们打印一下这个数组,可以看到这个数组目前长得跟棋盘是一模一样的,都是 15 * 15 的盘面,其中数组的每一项可以看作是一个横排:

image.png

那么按逻辑来说,我们点击第一行第二列的这个格子,对应的数组是不是也得有变化呢?

点击的这个点的坐标是已知的对吧,这个我们在上一节中就做过打印。那我们在落子的逻辑里面增加chessBoard[j][i] = 1的赋值,这样点击后,数值就会变成 1:

image.png

那我们就可以去加一层判断逻辑,当用户再次点击这个区域时,直接return立即停止执行:

那这个重复放子的事情解决之后呢,我们接下来就要去做更深一步的逻辑运算了。比如除此之外,五子棋是不是还有更重要的规则 - 五子相连才能获胜。

那我们就按部就班,先不谈人工智能,我们得至少先确认我们的获胜逻辑。

获胜规则逻辑处理

我们重复落子的问题解决后,接下来我们就得需要一个更大的新数组去进行计算。首先我们得需要知道这个五子棋到底有多少种赢法,这个听起来是不是挺难的。

image.png

简单来说就是五子棋可以横着、竖着、斜着,那我们就去定义一个三维数组,然后来看看有多少种赢法。

我们先定义一个wins数组,用于查看五子棋里一共有多少种赢法;然后定义一个count,用于记录一下一共有多少种用法;这两者其实并不冲突的。你看到后面就知道了,接下来我们先把有多少种赢法给算出来。

横向获胜规则

我们先来说一下我们的思路吧。我们可以通过遍历棋盘上的所有可能的五子连棋的情况,来判断当前棋局是否有一方获胜。其中,wins数组用于记录所有可能的五子连棋情况,count用于记录总共有多少种五子连棋情况,myWin数组用于记录当前玩家在每种五子连棋情况下已经有多少个棋子。

然后我们可以通过三重循环来遍历所有可能的五子连棋情况,然后将这些情况记录到wins数组中。wins数组中的每个元素代表一种五子连棋情况,如果当前玩家在这种情况下已经有了五个棋子,那么就代表着当前玩家获胜了。

第一步我们先定义三个字段:

然后我们初始化一下赢法数组,还是做个循环,跟上面做数组显示棋盘一样。但是代码我们先不做合并优化,为了大家能更直观地看到实现过程:

接下来我们来算一下到底有多少种赢法,首先我们先来看横排的,横排的是不是有以下几种赢法,分别从第 0 列开始到第 10 列:

image.png

因为到第 11 列开始,就没有足够的空间去支撑五子连棋获胜了。所以横排的循环就得从 0 开始,然后小于 11;但是纵向的就不受影响了,还是 15 ;然后我们还得定义k,当k小于 5 的时候(0~4),我们才能判定它获胜,因为获胜规则就是五颗棋子:

根据上面的代码我们再详细解释一遍,因为这里的逻辑确实有点难。首先i纵向是 0(取值范围只能在:0~14),就代表着是第一列;然后j横向也是 0(取值范围只能在:0~10),就代表着是第一行的第一列。

这个时候k循环的值在 0、1、2、3、4 这个区间。

那么[j + k]就是第一行第一列(0+0=0)、第一行第二列(0+1=1)、第一行第三列(0+2=2)、第一行第四列(0+3=3)、第一行第五列(0+4=4)。

image.png

这个时候五棋其实就互连获胜了,我们就可以把count赋值为true,代表着这是其中一种赢法。但是别忘了,count得累加,不然就会出错。

然后我们来打印一下这个数组,看看现在计算完横排的赢法后变成什么样了:

image.png

我们可以看到数组的第 0 项是个true,它对应的是横排的第一个。也意味着第一行第一个在横排中,只有一次获胜的机会。

然后数组的第 1 项是[true,true],就意味着它有两次获胜的机会,分别是:

image.png

当它放在第一个或第二个时,都能取得胜利。那我们横向就没问题了,这部分的逻辑确实很复杂。

纵向获胜规则

在做完横向的胜利逻辑处理后,接下来我们就来做纵向的获胜规则。

image.png

如上图,这就是我们纵向的赢法。也就是说跟横向刚好是反过来的,要小于 11;但是横向的就不受影响了,还是 15 。我们先把上一节的横向代码注释掉,然后把与横向的反向逻辑写进去:

通过打印这时候我们可以看到第一行到纵向全都是只有一种赢法:

image.png

那就证明我们做对了,跟横向的刚好完全是反过来,如果我们理解了横向的逻辑,那纵向的其实就简单多了。而这一节也建议大家多阅读几遍,把这里的算法逻辑全都跟着代码理清楚,对你以后的代码逻辑和算法思维都会有巨大的提升。

而本节中,我们对二维数组有了初步的认识,懂得这么去利用它修复我们的重复落子问题。然后我们又对横向、纵向的获胜方法做了算法的逻辑处理。那么在下一节中,我们将继续对获胜的规则去进行完善,而下一节所涉及的算法思维会比本节难很多,大家也要做好准备,往前冲!

本节代码

<!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] = []
    }
  }

  // 计算横向有多少种赢法
  // 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 < 11; i++) {
    for (var j = 0; j < 15; j++) {
      for (var k = 0; k < 5; k++) {
        wins[i + k][j][count] = true
      }
      count++
    }
  }
  console.log(wins);


  // 落子
  chess.onclick = function (e) {
    // 横坐标
    var i = Math.floor(e.offsetX / 30)
    // 纵坐标
    var j = Math.floor(e.offsetY / 30)

    if (chessBoard[j][i] == 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[j][i] = 1
  }
</script>

</html>