Appearance
使用 canvas 开发五子棋
主要思路是: 1、 绘制棋盘 2、 绘制棋子 3、 判断边界位置 4、 增加数组存储棋子位置 5、 判断棋子是否连成 5 个,判断胜利或失败
1. 绘制棋盘
javascript
// 棋盘尺寸
const size = 15
// 绘制棋盘
// 棋盘是由多条横线和纵线组成 700 / 50 = 14
// 循环 size 次绘制线条
for (let i = 1; i <= size; i++) {
ctx.beginPath()
// 表示线段的开头
ctx.moveTo(50, 50 * i)
ctx.font = '14px Arial'
ctx.fillText(i < 10 ? `0${i}` : i, 25, 50 * i + 5)
ctx.lineTo(size * 50, 50 * i)
ctx.stroke()
ctx.closePath()
ctx.beginPath()
ctx.moveTo(50 * i, 50)
ctx.fillText(i < 10 ? `0${i}` : i, 50 * i - 10, 45)
ctx.lineTo(50 * i, size * 50)
ctx.stroke()
ctx.closePath()
}
2. 绘制棋子
主要是监听了canvas
的点击事件,然后根据点击的位置绘制棋子,点击后根据当前位置增加 25 (棋子半径 )然后除以 50 取整之后再乘以 50. 这样就可以实现靠近鼠标点击的附近的整数位置(二维数组第几行第几列)
javascript
// 假设棋子是黑色
let isBlack = true
canvas.addEventListener('click', (e) => {
const { offsetX, offsetY } = e
const i = Math.floor((offsetX + 25) / 50)
const j = Math.floor((offsetY + 25) / 50)
const XI = i - 1
const YJ = j - 1
ctx.beginPath()
ctx.arc(x, y, 20, 0, Math.PI * 2)
// 根据棋子判断渐变的颜色及圆心
const tx = isBlack ? x - 10 : x + 10
const ty = isBlack ? y - 10 : y + 10
let g = ctx.createRadialGradient(tx, ty, 0, tx, ty, 30)
g.addColorStop(0, isBlack ? '#ccc' : '#ccc')
g.addColorStop(1, isBlack ? '#000' : '#fff')
ctx.fillStyle = g
// 设置阴影美化棋子
ctx.shadowColor = '#333'
ctx.shadowOffsetX = 4
ctx.shadowOffsetY = 4
ctx.shadowBlur = 4
})
3. 判断边界位置
边界判断,需要判断在棋盘内点击的才可以落子
javascript
// 判断落子
if (
offsetX < 25 ||
offsetX > size * 50 + 25 ||
offsetY < 25 ||
offsetY > size * 50 + 25
)
return
4. 增加数组存储棋子位置
增加数组存储棋子
javascript
const circles = new Array(15).fill([]).map((item) => new Array(15).fill())
在第二步中已经获取了当前点击的第几行第几列,此时就可以用于记录,并且还可以用来判断是否已经落子
javascript
// ...
const XI = i - 1
const YJ = j - 1
// 有了棋子位置,就可以判断能否落子, 并发出提示
if (circles[i][j]) {
alert('此处已有棋子,请重新落子')
return
}
// 向数组中记录棋子位置
// 并且判断棋子颜色
circles[i][j] = isBlack ? 'black' : 'white'
5. 判断棋子是否连成 5 个,判断胜利或失败
在canvas
监听的点击方法中触发,一开始的方法,需要循环的次数很多;
javascript
canvas.addEventListener('click', (e) => {
// ...
// 判断是否有对应棋子连成5个
endGame =
checkLine(XI, YJ, 'vertical') ||
checkLine(XI, YJ, 'horizontal') ||
checkLine(XI, YJ, 'topLeftToBottomRight') ||
checkLine(XI, YJ, 'topRightToBottomLeft')
})
- 优化前的写法
javascript
const checkVertical = (i, j) => {
// 定义变量记录向上的次数
let up = 0
// 定义变量记录向下的次数
let down = 0
let left = 0
let right = 0
let times = 0
// 定义当前总计有几个连在一起
// 初始值是1 ,因为本身已经算一个了
let count = 1
while (times < 1000) {
times++
const target = isBlack ? 'black' : 'white'
// 向上查找
up++
// 还需要判断点是否存在,点存在的情况下,还需要等于当前棋子的颜色
if (circles[i][j - up] && circles[i][j - up] === target) {
count++
}
// 向下查找
down++
if (circles[i][j + down] && circles[i][j + down] === target) {
count++
}
// 如果棋子大于5
if (
count > 5 ||
(circles[i][j - up] !== target && circles[i][j + down] !== target)
) {
// alert('游戏结束')
break
}
}
return count >= 5
}
后续经过优化,发现每个方向最多判断 4 次(因为是五子棋),并且可以根据方向来判断值的正负,最后查找的值相加大于等于 5 就说明是胜利
- 优化后的写法
javascript
const checkLine = (i, j, direction) => {
let xDelta = 0
let yDelta = 0
// 根据方向确定坐标增量
switch (direction) {
case 'vertical':
yDelta = 1
break
case 'horizontal':
xDelta = 1
break
case 'topLeftToBottomRight':
xDelta = 1
yDelta = 1
break
case 'topRightToBottomLeft':
xDelta = 1
yDelta = -1
break
default:
break
}
// 向一个方向查找
const findInDirection = (x, y, xDelta, yDelta) => {
let count = 0
const target = isBlack ? 'black' : 'white'
while (count <= 4) {
count++
const newX = x + count * xDelta
const newY = y + count * yDelta
if (circles[newX] && circles[newX][newY] === target) {
// 如果找到目标,继续计数
} else {
// 否则中断循环
break
}
}
return count
}
// 计算两个方向的计数总和
const count =
findInDirection(i, j, xDelta, yDelta) +
findInDirection(i, j, -xDelta, -yDelta) -
1 // 减去一个,因为开始时已经计数了一个
console.log(`查找完 ${direction} 方向 count 为`, count)
return count >= 5
}
6. 判断胜利
在棋子连续之后判断是否胜利
javascript
canvas.addEventListener('click', (e) => {
// ...
// 判断是否有对应棋子连成5个
endGame =
checkLine(XI, YJ, 'vertical') ||
checkLine(XI, YJ, 'horizontal') ||
checkLine(XI, YJ, 'topLeftToBottomRight') ||
checkLine(XI, YJ, 'topRightToBottomLeft')
if (endGame) {
text.textContent = `${isBlack ? '白方' : '黑方'}胜`
return
}
})
7. 优化
使用 new Proxy
代理数组,当数组发生改变时,触发 set
方法,绘制棋子和修改数组,主要是用于后续可以使用 WebSocket
改写成两人联网下棋的方式
javascript
// 增加数组存储棋子
const circles = new Array(size).fill([]).map((item) => new Array(size).fill())
// 判断是否是对象
function _isObject(v) {
return typeof v === 'object' && v !== null
}
function myProxy(obj, lastTimeX = '') {
const proxy = new Proxy(obj, {
get(target, k) {
let v = target[k]
// 递归监听子元素
if (_isObject(v)) {
v = myProxy(v, k)
}
return v
},
set(target, k, val) {
const X = (lastTimeX * 1 + 1) * 50
const Y = (k * 1 + 1) * 50
if (target[k] !== val) {
draw(X, Y)
target[k] = val
}
}
})
return proxy
}
// 使用代理
const proxyCircles = myProxy(circles)