canvas深入浅出(四)| 小册免费学

简介: 之前三节我们铺垫了canvas的相关知识,这一节我们来“落地”,实现一个五子棋的小游戏

ed40fadc543e63c8e8f7c8cddcd1c78.png

之前三节我们铺垫了canvas的相关知识,这一节我们来“落地”,实现一个五子棋的小游戏

开始之前,我们先想一下要做什么:

  1. 首先我们要绘制一个棋盘
  2. 然后要有黑子和白子
  3. 每次落子时判断当前位置是否已经落子,“米”字型方向上是否有五个子,如果满足条件,当前落子一方获胜

下面马上开始,先做好准备工作,准备一个canvas容器和一个刷新按钮用来重新开盘,然后准备一个结果区显示提示信息

<canvas id="canvas" width="200px" height="200px"></canvas>
<p class="result"></p>
<button onclick="loadPanel(400, 400,30,13)">刷新</button>
复制代码


棋盘


首先我们来绘制棋盘,这个很简单,循环画直线即可,我们将所有的操作放置在一个函数中,以便重新开局

/**
 * @param w 棋盘宽度
 * @param h 棋盘高度
 * @param cs 格子尺寸
 * @param ps 棋子半径
 */
function loadPanel(w, h, cs, ps) {
  let i, j, k;
  //1)绘制棋盘,边缘应隔开棋子半径的距离
  cs = cs || 16;//默认格子宽高
  ps = ps || 4;//棋子半径
  h = h || w;//高度默认等于宽度
  let el = document.getElementById('canvas');
  el.setAttribute('width', w + 'px');
  el.setAttribute('height', h + 'px');
  let context = el.getContext("2d");
  //计算棋盘分割,向下取整
  let splitX = ~~((w - 2 * ps) / cs), 
      splitY = ~~((h - 2 * ps) / cs);
  //循环划线
  context.translate(ps, ps);
  context.beginPath();
  context.strokeStyle = '#000';
  //垂直线
  for (i = 0; i < splitX + 1; i++) {
    context.moveTo(cs * i, 0);
    context.lineTo(cs * i, splitY * cs);
    context.stroke();
  }
  //水平线
  for (j = 0; j < splitY + 1; j++) {
    context.moveTo(0, cs * j);
    context.lineTo(splitX * cs, cs * j);
    context.stroke();
  }
  context.closePath();
}
// 绘制棋盘
loadPanel(400, 400, 30, 13);
复制代码

~~作用是向下取整,可以将浮点类型转为整数,字符串类型的数字也可以,但是无法转为数字的结果为0

现在打开浏览器,看一下效果

1682515089(1).png


棋子

棋子的绘制也不难,就是画圆,黑子填充黑色,白子填充白色,然后描个边,相关API之前也有过介绍,我们先在棋盘上绘制两个静态的棋子观察效果

context.beginPath()
context.arc(cs * 0, cs * 0, ps, 2 * Math.PI, false);
context.fillStyle = '#fefefe'
context.fill()
context.stroke();
context.closePath()
context.beginPath()
context.arc(cs * 1, cs * 0, ps, 2 * Math.PI, false);
context.fillStyle = '#000'
context.fill()
context.stroke();
context.closePath()
复制代码

1682515111(1).png

效果还不错,但是在下棋时需要鼠标点击,这里的棋子渲染是需要鼠标点击事件来触发的,下面来给canvas添加鼠标点击事件,我们需要获取鼠标的点击坐标,然后计算出他应该落在哪个点,还要循环落子,

let user = 0, colors = ['#000', '#fefefe'];
el.addEventListener('click', function (e) {
  let x = e.offsetX,
    y = e.offsetY,
    //计算落子范围
    rx = ~~((x - ps) / cs) + (((x - ps) % cs <= cs / 2) ? 0 : 1),
    ry = ~~((y - ps) / cs) + (((y - ps) % cs <= cs / 2) ? 0 : 1);
  context.beginPath();
  context.arc(cs * rx, cs * ry, ps, 2 * Math.PI, false);
  context.fillStyle = colors[user];
  context.strokeStyle = '#000';
  user ? user = 0 : user = 1;//切换执子者
  context.fill();
  context.stroke();
  context.closePath();
})
复制代码

现在我们已经完成了50%了

1682515156(1).png


胜方判定


现在进行做事后的一步,判定“米”字方向上是否存在5个同色棋子,我们先来考虑一下答题思路,你先不要往下看,先考虑一下自己的想法

1682515187(1).png


好了,现在不管你想没想出来,我来说一下大概思路,首先定义一个对象来存储落子情况,大概的形式是这样的

{
  '1-1': 0,
  '1-2': 1
}
// key是棋盘位置,value是身份
复制代码

然后使用分别使用[0,1]、[1, 0]、[1, 1]、[1, -1]表示上下、左右、斜向上、斜向下四个方位,待会会分别拿这几个方位来进行遍历,原理大概是这样的

以[0, 1]为例,在for(let i = 1; i<= 4 ; i++){}中,分别用0和1去乘以i,然后加上当前点击未知的x或者y坐标,就能够遍历,因为i无论是多少乘0都是0,所以x坐标是不会变的,那么y坐标会遍历上边的四个,如果这四个的都是当前落子选手的即判定为赢,如果找不到就向反方向去找,即循环for(let i = -1; i>= -4 ; i--){},如果当前循环结束没有判定胜利再去其他方向重读此步骤

1682515217(1).png

计算出坐标来之后在存储落子的对象中寻找落子者,如果是当前落子者就累加,如果遍历结束满足获胜条件游戏结束

let user = 0, colors = ['#000', '#fefefe'];
let chks = [[1, 0], [0, 1], [1, 1], [1, -1]]; // 四个方向
let pieces = {}; // 记录游戏者落子位置
let successNum = 5;//赢棋标准
let resultEl = document.querySelector('.result');
el.addEventListener('click', function (e) {
  let x = e.offsetX,
    y = e.offsetY,
    //计算落子范围
    rx = ~~((x - ps) / cs) + (((x - ps) % cs <= cs / 2) ? 0 : 1),
    ry = ~~((y - ps) / cs) + (((y - ps) % cs <= cs / 2) ? 0 : 1);
  context.beginPath();
  context.arc(cs * rx, cs * ry, ps, 2 * Math.PI, false);
  context.fillStyle = colors[user];
  context.strokeStyle = '#000';
  user ? user = 0 : user = 1;//切换执子者
  context.fill();
  context.stroke();
  context.closePath();
  piece = pieces[rx + '-' + ry] = user;
  for (j = 0; j < chks.length; j++) {
    let num = 1, chk = chks[j];
    for (i = 1; i <= 4; i++) {
      if (pieces[(rx + chk[0] * i) + '-' + (ry + chk[1] * i)] == piece) {
        num++
      } else {
        for (i = -1; i >= -4; i--) {
          if (pieces[(rx + chk[0] * i) + '-' + (ry + chk[1] * i)] == piece) {
            num++
          }
        }
        break
      }
    }
    if (num == successNum) {
      status = false
      resultEl.innerHTML = ['白', '黑'][user] + '方赢';
      break;
    }
  }
})
复制代码

然后就完事了

1682515242(1).png


但是现在还有很多逻辑问题没有解决,游戏结束之后限制不可继续落子,点击位置如果已经有过落子则不可继续落子等这些就当作业留给你吧,其实不难,好好考虑考虑吧

我最终的实现效果如下

相关文章
|
Linux 芯片
Linux内核学习(六):linux kernel的Kconfig分析
Linux内核学习(六):linux kernel的Kconfig分析
1198 0
|
存储 Linux 应用服务中间件
基于CentOS 7.6的Docker新手教学
采用本地虚拟机+阿里云镜像加速器
1446 5
基于CentOS 7.6的Docker新手教学
|
10月前
|
机器学习/深度学习 弹性计算 人工智能
在阿里云ECS上一键部署DeepSeek-R1
Open WebUI 和 Ollama 的联合,通过集成 DeepSeek-R1 的强大功能,赋予每一位用户使用尖端 AI 技术的能力,使得复杂的 AI 技术不再是遥不可及的梦想。无论是研究人员、开发者,还是企业用户,您都能从这一创新中获得新的灵感和增长点。本文介绍通过计算巢一键部署和使用DeepSeek-R1。
在阿里云ECS上一键部署DeepSeek-R1
|
存储 监控 固态存储
如何在 Linux 上检查 SSD/HDD 健康状况?
【10月更文挑战第14天】
1437 1
如何在 Linux 上检查 SSD/HDD 健康状况?
|
SQL 测试技术 数据库
【SQL】已解决:SQL错误(15048): 数据兼容级别有效值为100、110或120
【SQL】已解决:SQL错误(15048): 数据兼容级别有效值为100、110或120
450 0
|
JSON 小程序 JavaScript
面试官说,布局小程序页面记得用TDesign组件库
面试官说,布局小程序页面记得用TDesign组件库
|
存储 弹性计算 固态存储
2023阿里云服务器租用价格表(优惠报价和官方收费标准)
阿里云服务器租用价格表包括ECS共享型s6、通用算力型u1、计算型c6、计算型c7、通用型g6、通用型g7及GPU云服务器等,包括云服务器ECS实例CPU内存价格、公网带宽价格和系统盘收费标准
2726 0
2023阿里云服务器租用价格表(优惠报价和官方收费标准)
|
监控 C语言 Perl
西门子S7-1200编程实例,基本位逻辑指令如何使用?
今天我们来介绍一下西门子S7-1200基本位逻辑指令,通过一个简单的起保停控制实例来学习基本位逻辑指令如何使用。
西门子S7-1200编程实例,基本位逻辑指令如何使用?
|
开发工具 Windows
SDL开发笔记(三):使用SDL渲染窗口颜色和图片
SDL开发笔记(三):使用SDL渲染窗口颜色和图片
SDL开发笔记(三):使用SDL渲染窗口颜色和图片