10分钟-带你走进H5-五子棋

简介: 10分钟-带你走进H5-五子棋


用到的技术

  1. es6
  2. canvas

canvas

Canvas API 提供了一个通过JavaScriptHTML的元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

初识canvas

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no"
    />
    <title>初识canvas.html</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      canvas {
        background-color: aqua;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
  </body>
</html>



CF}$[0BTBVNFSOW2X()WKC0.png

可以看到 canvas是一个 默认宽高为 300*150、内联的一个元素。

画直线 lineTo

以下代码画出一个对角线

// 1 获取dom元素
      const canvas = document.querySelector('canvas');
      // 2 获取画布对象
      const ctx = canvas.getContext('2d');
      // 3 设定起点
      ctx.moveTo(0, 1);
      // 4 设定终点
      ctx.lineTo(300, 150);
      // 5 描边
      ctx.stroke();


LHP9GK[@9(V2)GNSW`4X2}X.png

改画布大小

需要注意的是,想要修改 canvas标签的宽度和高度,不能直接在css中改,否则是拉伸canvas。只能在标签的属性上通过 widthheight来改

错误示例

canvas {
    width: 600px;
    height: 300px;
  }

)R6YOX(89FP46P)}~90OE@V.png


正确示例

<canvas width="600" height="300"></canvas>

BW5{BJZ~J[0Z8~%JT4C07YL.png

修改颜色 strokeStyle

当我们不想要默认的黑色线条时,可以通过 strokeStyle来修改颜色

ctx.strokeStyle = 'pink';
  ctx.stroke();

{R@5DBQ9}OY773}`8$~(Y3Q.png

画圆弧

// arc(圆心坐标x,圆心坐标y,半径长度,开始弧度,结束弧度)
  ctx.arc(300, 150, 100, 0, Math.PI * 2);
  ctx.stroke();


SHMQP4EPM1P3AM($C2%XML4.png

也可以修改为 填充的圆弧 fill

ctx.arc(300, 150, 100, 0, Math.PI * 2);
ctx.fill();


E}LYU5BBAE~ZO9%UZXT3G`I.png

如果想要修改填充的颜色的话 就要使用 fillStyle

ctx.fillStyle = 'yellow';
ctx.fill();


Z%}V]G6WOEZMH]W1~S{(F$M.png

show time

下列的代码示例,有注释的表示是新增的代码

搭建好静态结构

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no"
    />
    <title>五子棋</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        text-align: center;
        height: 100vh;
      }
      canvas {
        /* 可以自己写一个背景颜色来代替以下四行代码 */
        background-image: url(./images/20211227092309212.jpg);
        background-repeat: no-repeat;
        background-size: cover;
        background-position: center;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
  </body>
</html>



新建一个类,声明相关方法

下列的代码示例,有注释的表示是新增的代码

主要有四个功能

  1. 初始化相关属性 如 棋盘宽高等
  2. 声明描绘棋盘的方法
  3. 声明点击棋盘,进行下棋的方法
  4. 声明下棋时,判断是否胜利的方法
class Gobang {
        /** 构造函数 */
        constructor() {}
        /** 初始化画布 */
        init() {}
        /** 描绘棋盘 */
        drawCheckerBroad() {}
        /** canvas绑定点击事件,实现下棋功能 */
        checkerBroadEvent() {}
        /** 判断是否胜利 * */
        victory() {}
      }

初始化画布

下列的代码示例,有注释的表示是新增的代码

这里面需要初始化的属性有

需求是想要的棋盘 一共 18*18个格子,那么就需要用到19条线,每一个格子大小为20*20

  1. this.canvas 用来在后面绑定点击事件使用
  2. this.checkerBoradWidth 用来声明一个格子多大
  3. this.lineNums 用来声明棋盘一个要画多少条线段
  4. this.blaorwh 用来声明当前下棋的选手是黄色还是白色
  5. this.chessMartrixs 棋盘位置矩阵,用来记录下棋的棋子位置和判断是否胜利
init() {
      // 格子的宽度宽度
      this.checkerBoradWidth = 20;
      this.lineNums = 19;
      // 白色还是黄色  为黄色
      this.blaorwh = '黄色';
      // 存放棋子的位置 二维数组 19*19
      this.chessMartrixs = [];
      for (let i = 0; i <= this.lineNums; i++) {
        this.chessMartrixs.push(
          new Array(this.lineNums).fill(0, 0, this.lineNums)
        );
      }
      this.canvas = document.querySelector('canvas');
      // 设置 画布的大小 刚好等于20个格子的大小
      this.canvas.width = this.checkerBoradWidth * (this.lineNums - 1);
      this.canvas.height = this.checkerBoradWidth * (this.lineNums - 1);
      this.ctx = this.canvas.getContext('2d');
    }

描绘棋盘

下列的代码示例,有注释的表示是新增的代码

需求是想要的棋盘 一共 18*18个格子,那么就需要用到19条线,每一个格子大小为20*20

// 开辟新的路径
      this.ctx.beginPath();
      // 设定线条颜色
      this.ctx.strokeStyle = '#ccc';
      // 遍历描绘 棋盘上的每一条线 19*19条线
      for (let i = 0; i < this.lineNums; i++) {
        // 描绘横线  checkerBoradWidth 为 棋盘 格子的宽度
        this.ctx.moveTo(0, i * this.checkerBoradWidth);
        this.ctx.lineTo(
          // lineNums 为 线条的个数
          this.lineNums * this.checkerBoradWidth,
          i * this.checkerBoradWidth
        );
        // 描绘竖线
        this.ctx.moveTo(i * this.checkerBoradWidth, 0);
        this.ctx.lineTo(
          i * this.checkerBoradWidth,
          this.lineNums * this.checkerBoradWidth
        );
      }
      this.ctx.stroke();
 

然后在 构造函数中分别调用 初始化画布和描绘棋盘的方法,然后实例化

constructor() {
  this.init();
  this.drawCheckerBroad();
}

实例化

const gobang = new Gobang();

出现效果


点击棋盘,进行下棋

下列的代码示例,有注释的表示是新增的代码

通过给canvas标签绑定点击事件,获取鼠标坐标,然后就地画圆即可

checkerBroadEvent() {
      // 注意要使用箭头函数,否则容易导致 this指向问题
      this.canvas.addEventListener('click', (e) => {
        // 获取鼠标在画布中的坐标
        const x = e.offsetX;
        const y = e.offsetY;
        // 每次点击 重新开辟路径
        this.ctx.beginPath();
        // 根据点击的坐标绘制 棋子
        this.ctx.arc(x, y, this.checkerBoradWidth / 2, 0, 2 * Math.PI);
        // 判断白棋还是黑棋
        if (this.blaorwh === '黄色') {
          // 黄色
          this.ctx.fillStyle = 'yellow';
        } else {
          // 白色 描边 否则容易和背景混淆
          this.ctx.fillStyle = '#fff';
          this.ctx.stroke();
        }
        this.ctx.fill();
        this.blaorwh = this.blaorwh === '黄色' ? '白色' : '黄色';
      });
    }
  

在构造函数中也调用它

constructor() {
      this.init();
      this.drawCheckerBroad();
      this.checkerBroadEvent(); // 鼠标点击下棋功能
    }

这时候会出现两个问题

  1. 下棋的位置不是在线段的中心,有可能歪了
    XBEB)I]OX@`U2N@GQ_4(CT3.png
  2. 同一个位置,可以放无数个棋子(需要判断是否已经有棋子)
    ()OOT`@4MPP93(GRBUXLG~W.png

我们需要解决它

修正下棋的标准位置

下列的代码示例,有注释的表示是新增的代码

主要通过判断当前下棋的坐标位置和水平或竖直方向上的哪一条线段的位置近,那么就设置下棋的位置等于距离最近的线段的坐标。

/** canvas绑定点击事件,实现下棋功能 */
        checkerBroadEvent() {
          // 注意要使用箭头函数,否则容易导致 this指向问题
          this.canvas.addEventListener('click', (e) => {
            const x = e.offsetX;
            const y = e.offsetY;
            // 设定画圆弧的圆心坐标 需要加上每一个格子20px大小的
            let arcX, arcY;
            // 用来记录 棋子的坐标点  如 (0,1)  (1,3) 等
            for (let i = 0; i <= this.lineNums; i++) {
              // 判断鼠标的x坐标靠近哪一条线  水平方向
              if (
                Math.abs(x - i * this.checkerBoradWidth) <=
                this.checkerBoradWidth / 2
              ) {
                arcX = i * this.checkerBoradWidth;
              }
              // 判断鼠标的y坐标靠近哪一条边线  垂直方向
              if (
                Math.abs(y - i * this.checkerBoradWidth) <=
                this.checkerBoradWidth / 2
              ) {
                arcY = i * this.checkerBoradWidth;
              }
            }
            // 如果点出了其他位置,直接返回
            if (arcX === undefined || arcY === undefined) return;
            this.ctx.beginPath();
            // 。。。。。
          });
        }
   

判断当前位置是否已经有棋子了

下列的代码示例,有注释的表示是新增的代码

使用一个二维数组 this.checkerBoradWidth ,记录下棋的坐标位置,当再次下棋时,判断当前位置是否有棋子来实现功能

checkerBroadEvent() {
          this.canvas.addEventListener('click', (e) => {
            const x = e.offsetX;
            const y = e.offsetY;
            let arcX, arcY;
            // 用来记录 棋子的坐标点  把当前棋子的位置记录在 棋子矩阵中
            let chessX, chessY;
            for (let i = 0; i <= this.lineNums; i++) {
              if (
                Math.abs(x - i * this.checkerBoradWidth) <=
                this.checkerBoradWidth / 2
              ) {
                arcX = i * this.checkerBoradWidth;
                // 记录当前棋子的位置 用来做下棋时的位置重复判断
                chessX = i;
              }
              if (
                Math.abs(y - i * this.checkerBoradWidth) <=
                this.checkerBoradWidth / 2
              ) {
                arcY = i * this.checkerBoradWidth;
                // 记录当前棋子的位置 用来做下棋时的位置重复判断
                chessY = i;
              }
            }
            // 如果点出了其他位置,直接返回
            if (arcX === undefined || arcY === undefined) return;
            // 如果有人下过了,那么就返回
            if (this.chessMartrixs[chessY][chessX]) return;
            // 把当前棋子位置存一份到 矩阵中
            this.chessMartrixs[chessY][chessX] = this.blaorwh;
            this.ctx.beginPath();
            this.ctx.arc(
              arcX,
              arcY,
              this.checkerBoradWidth / 2,
              0,
              2 * Math.PI
            );
         
            if (this.blaorwh === '黄色') {
              this.ctx.fillStyle = 'yellow';
            } else {
              this.ctx.fillStyle = '#fff';
              this.ctx.stroke();
            }
            this.ctx.fill();
            this.blaorwh = this.blaorwh === '黄色' ? '白色' : '黄色';
          });
        }

判断选手胜利-重点

我们实现判断选手是否胜利的思路如下

以下四条轴中,任意一条轴(由两条短轴连接而成)上有5个同色的棋子就表示胜利

A{8X37NREVS77CSG4E{[RVA.png

通过观察以上图表,得出一个规律:我们需要设置一个判断棋子是否相连的关键数组

[[-1, -1], [-1, 0], [1, -1], [0, 1]]

  1. 比如我们对上面的数组进行遍历 比如 可以获取到 第一个元素 [-1,-1]
  2. 同时我们对 [-1,-1] 进行取反 ,便也得到 [1,1],那么便可以确定一条长轴
    @4I)96J_KMA~XIK]G2WJXAO.png
  3. 此时,我们拿 [-1,-1] 单独开启遍历,计算 这边方向的棋子个数
    HW~CXZOGVYOS7$MC[JJ]INQ.png
  4. 同时,我们拿 [1,1] 也开始遍历,计算这边方向的棋子个数
    JUSD[V@GN`%8V0U2HE8{H}U.png
  5. 如果两者加起来大于等于 5 ,便表示胜利

代码

/** * 判断选手是否胜利 */
        victory(matrix, x, y, borw) {
          // 判断胜利条件的4个轴方向
          let victoryUnits = [
            [-1, -1],
            [-1, 0],
            [1, -1],
            [0, 1],
          ];
          const getLineNums = (matrix, direcsx, x, y, borw) => {
            // 用来统计相连的同色的棋子的数量
            let results = 0;
            for (let i = 0; i < 5; i++) {
              const vicx = x + i * direcsx[0];
              const vicy = y + i * direcsx[1];
              // 判断位置没有超出
              if (
                matrix[vicy] !== undefined &&
                matrix[vicy][vicx] !== undefined &&
                borw === matrix[vicy][vicx]
              ) {
                // 判断是否和当前的颜色相等
                results++;
              } else {
                break;
              }
            }
            return results;
          };
          // 判断是否胜利
          const result = victoryUnits.some((item) => {
            // 统计一侧短轴的同色棋子的个数
            const n1 = getLineNums(matrix, item, x, y, borw);
            // 统计另一侧短轴的同色棋子的个数
            const n2 = getLineNums(
              matrix,
              item.map((v) => -v),
              x,
              y,
              borw
            );
            // 因为自己也算一个了
            return n1 + n2 - 1 >= 5;
          });
          return result;
        }

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no"
    />
    <title>H5-五子棋</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        text-align: center;
        height: 100vh;
      }
      canvas {
        background-image: url(./images/20211227092309212.jpg);
        background-repeat: no-repeat;
        background-size: cover;
        background-position: center;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
    <script>
      class Gobang {
        constructor() {
          this.init();
          this.drawCheckerBroad();
          this.checkerBroadEvent();
        }
        init() {
          this.checkerBoradWidth = 20;
          this.lineNums = 19;
          this.blaorwh = '黄色';
          this.chessMartrixs = [];
          for (let i = 0; i <= this.lineNums; i++) {
            this.chessMartrixs.push(
              new Array(this.lineNums).fill(0, 0, this.lineNums)
            );
          }
          this.canvas = document.querySelector('canvas');
          this.canvas.width = this.checkerBoradWidth * (this.lineNums - 1);
          this.canvas.height = this.checkerBoradWidth * (this.lineNums - 1);
          this.ctx = this.canvas.getContext('2d');
        }
        drawCheckerBroad() {
          this.ctx.beginPath();
          this.ctx.strokeStyle = '#ccc';
          for (let i = 0; i < this.lineNums; i++) {
            this.ctx.moveTo(i * this.checkerBoradWidth, 0);
            this.ctx.lineTo(
              i * this.checkerBoradWidth,
              this.lineNums * this.checkerBoradWidth
            );
            this.ctx.moveTo(0, i * this.checkerBoradWidth);
            this.ctx.lineTo(
              this.lineNums * this.checkerBoradWidth,
              i * this.checkerBoradWidth
            );
          }
          this.ctx.stroke();
        }
        checkerBroadEvent() {
          this.canvas.addEventListener('click', (e) => {
            const x = e.offsetX;
            const y = e.offsetY;
            let arcX, arcY;
            let chessX, chessY;
            for (let i = 0; i <= this.lineNums; i++) {
              if (
                Math.abs(x - i * this.checkerBoradWidth) <=
                this.checkerBoradWidth / 2
              ) {
                arcX = i * this.checkerBoradWidth;
                chessX = i;
              }
              if (
                Math.abs(y - i * this.checkerBoradWidth) <=
                this.checkerBoradWidth / 2
              ) {
                arcY = i * this.checkerBoradWidth;
                chessY = i;
              }
            }
            if (arcX === undefined || arcY === undefined) return;
            if (this.chessMartrixs[chessY][chessX]) return;
            this.chessMartrixs[chessY][chessX] = this.blaorwh;
            this.ctx.beginPath();
            this.ctx.arc(
              arcX,
              arcY,
              this.checkerBoradWidth / 2,
              0,
              2 * Math.PI
            );
            if (this.blaorwh === '黄色') {
              this.ctx.fillStyle = 'yellow';
            } else {
              this.ctx.fillStyle = '#fff';
              this.ctx.stroke();
            }
            this.ctx.fill();
            if (
              this.victory(this.chessMartrixs, chessX, chessY, this.blaorwh)
            ) {
              console.log(this.blaorwh + '胜利');
              return;
            }
            this.blaorwh = this.blaorwh === '黄色' ? '白色' : '黄色';
          });
        }
        victory(matrix, x, y, borw) {
          let victoryUnits = [
            [-1, -1],
            [-1, 0],
            [1, -1],
            [0, 1],
          ];
          const getLineNums = (matrix, direcsx, x, y, borw) => {
            let results = 0;
            for (let i = 0; i < 5; i++) {
              const vicx = x + i * direcsx[0];
              const vicy = y + i * direcsx[1];
              if (
                matrix[vicy] !== undefined &&
                matrix[vicy][vicx] !== undefined &&
                borw === matrix[vicy][vicx]
              ) {
                results++;
              } else {
                break;
              }
            }
            return results;
          };
          const result = victoryUnits.some((item) => {
            const n1 = getLineNums(matrix, item, x, y, borw);
            const n2 = getLineNums(
              matrix,
              item.map((v) => -v),
              x,
              y,
              borw
            );
            return n1 + n2 - 1 >= 5;
          });
          return result;
        }
      }
      const gobang = new Gobang();
    </script>
  </body>
</html>


目录
相关文章
|
9月前
|
人工智能 数据可视化 数据库
低代码:真能“0代码”开发应用吗?
低代码平台通过“0代码”开发模式,利用拖拽组件和配置参数,大幅降低应用开发门槛,使无编程基础的用户也能快速构建应用。这种模式并非完全无需代码,而是将复杂代码封装成可视化组件,简化开发流程。低代码平台适用于多种场景,如企业内部工具、数据可视化等,尤其适合需要快速迭代和灵活调整的应用开发。其核心优势在于提升开发效率、支持多人协作、简化部署流程,并确保数据一致性。未来,低代码平台将进一步融合AI技术,提供智能代码助手、自动化测试等功能,优化开发体验,助力企业在数字化转型中实现高效创新。
|
10月前
|
数据采集 存储 供应链
Pandas数据应用:库存管理
本文介绍Pandas在库存管理中的应用,涵盖数据读取、清洗、查询及常见报错的解决方法。通过具体代码示例,讲解如何处理多样数据来源、格式不一致、缺失值和重复数据等问题,并解决KeyError、ValueError等常见错误,帮助提高库存管理效率和准确性。
303 72
|
小程序 Linux 区块链
Python PyInstaller 打包成 Win、Mac 应用程序(app / exe)
Python PyInstaller 打包成 Win、Mac 应用程序(app / exe)
942 0
|
网络协议 Java 数据安全/隐私保护
tcp 可以建立多个连接吗?
【10月更文挑战第25天】TCP(传输控制协议)是一种面向连接的、可靠的传输层协议,它在网络通信中起着重要的作用。在 TCP 中,可以建立多个连接,这种特性被称为TCP 连接复用。
|
存储 移动开发 数据库
HTML5 Web IndexedDB 数据库常用数据存储类型
IndexedDB 支持多种数据存储类型,满足复杂数据结构的存储需求。它包括基本数据类型(如 Number、String、Boolean、Date)、对象(简单和嵌套对象)、数组、Blob(用于二进制数据如图像和视频)、ArrayBuffer 和 Typed Arrays(处理二进制数据)、结构化克隆(支持 Map 和 Set 等复杂对象),以及 JSON 数据。尽管不直接支持非序列化数据(如函数和 DOM 节点),但可以通过转换实现存储。开发者应根据具体需求选择合适的数据类型,以优化性能和使用体验。
|
测试技术 API 开发工具
ElasticSearch的IK分词器
ElasticSearch的IK分词器
238 7
|
存储 JSON 监控
大数据-167 ELK Elasticsearch 详细介绍 特点 分片 查询
大数据-167 ELK Elasticsearch 详细介绍 特点 分片 查询
767 4
|
Windows
XMind 常用快捷键(思维导图总结)
XMind 常用快捷键(思维导图总结)
531 0
|
监控 关系型数据库 MySQL
『Jmeter入门万字长文』 | 从环境搭建、脚本设计、执行步骤到生成监控报告完整过程
『Jmeter入门万字长文』 | 从环境搭建、脚本设计、执行步骤到生成监控报告完整过程
1027 2
|
机器学习/深度学习 算法
【机器学习】十大算法之一 “随机森林”
随机森林算法(Random Forest, RF)是由Leo Breiman和Adele Cutler于2001年提出的一种集成学习(Ensemble Learning)算法。它是由多个决策树构成的分类器,通过对每个决策树的投票结果来确定最终的预测结果。随机森林算法可以用于分类和回归分析。在分类问题中,每个决策树的输出结果为一个类别标签,通过投票来确定样本所属的类别。在回归问题中,每个决策树的输出结果为一个连续值,取所有决策树输出结果的平均值作为最终结果。可以处理高维度数据;可以处理不平衡的数据集。
2182 1
【机器学习】十大算法之一 “随机森林”