Skip to content

使用 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)