之前三节我们铺垫了canvas的相关知识,这一节我们来“落地”,实现一个五子棋的小游戏
开始之前,我们先想一下要做什么:
- 首先我们要绘制一个棋盘
- 然后要有黑子和白子
- 每次落子时判断当前位置是否已经落子,“米”字型方向上是否有五个子,如果满足条件,当前落子一方获胜
下面马上开始,先做好准备工作,准备一个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
现在打开浏览器,看一下效果
棋子
棋子的绘制也不难,就是画圆,黑子填充黑色,白子填充白色,然后描个边,相关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() 复制代码
效果还不错,但是在下棋时需要鼠标点击,这里的棋子渲染是需要鼠标点击事件来触发的,下面来给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%了
胜方判定
现在进行做事后的一步,判定“米”字方向上是否存在5个同色棋子,我们先来考虑一下答题思路,你先不要往下看,先考虑一下自己的想法
好了,现在不管你想没想出来,我来说一下大概思路,首先定义一个对象来存储落子情况,大概的形式是这样的
{ '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--){},如果当前循环结束没有判定胜利再去其他方向重读此步骤
计算出坐标来之后在存储落子的对象中寻找落子者,如果是当前落子者就累加,如果遍历结束满足获胜条件游戏结束
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; } } }) 复制代码
然后就完事了
但是现在还有很多逻辑问题没有解决,游戏结束之后限制不可继续落子,点击位置如果已经有过落子则不可继续落子等这些就当作业留给你吧,其实不难,好好考虑考虑吧
我最终的实现效果如下