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

快捷目录

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

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

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

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

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


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

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

二维数组逻辑处理

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

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

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

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

我们先定义一个数组:

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

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

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

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

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

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

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

获胜规则逻辑处理

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

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

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

横向获胜规则

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

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

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

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

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

因为到第 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)。

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

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

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

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

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

纵向获胜规则

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

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

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

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

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

本节代码

js 复制代码
<!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>
相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom7 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试