这儿的五子棋只做一些基础的功用,对于相对专业的规则不做处理。

那么该五子棋完成的规则和功用如下:

  • 全体功用选用canvas完成
  • 队伍都规则 20 个数量,那么棋子的队伍数量是 20 + 1
  • 棋盘数据选用稀疏数组格局
  • 棋子:0 为黑色,1 为白色
  • 能够悔棋
  • 输赢完毕判别

棋盘制作

用 Js 写一个简略的五子棋

<template>
  <div class="gobang">
    <canvas id="my-canvas" ref="canvasRef" width="640" height="640" @click="canvasClick">
    </canvas>
  </div>
</template>
<script lang="ts" setup>
type GobangData = (0 | 1 | undefined)[][]
/* 一些常量 */
// canvas dom 元素
const canvasRef = ref<InstanceType<typeof HTMLCanvasElement>>()
// 队伍数
const rcs = 20
// 队伍的间隔间隔
const gap = 30
// 棋子的半径
const radius = 12
// 棋盘的边距
const padding = 20
// 是否完毕符号
const gameOver = ref(false)
// 当时下棋方
let current = ref<0 | 1>(1)
// canvas 的 2d 实例
let ctx: CanvasRenderingContext2D
// 初始化棋盘数据
let data: GobangData = new Array(rcs + 1).fill(0).map(() => new Array(rcs + 1))
</script>
<style lang="scss" scope>
.gobang {
  width: 640px;
  margin: 0 auto;
}
.header {
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
  .btns button {
    margin-left: 10px;
    padding: 0 5px;
  }
}
#my-canvas {
  background-color: #e6a23c;
  border-radius: 4px;
}
</style>

棋盘制作

/**
 * 制作棋盘
 * @param ctx canvas的2d实例
 * @param number 队伍数
 * @param gap 队伍间隔间隔
 * @param padding 棋盘边距
 */
const drawChessboard = (
  ctx: CanvasRenderingContext2D, rcs: number, gap: number, padding: number
) => {
  ctx.beginPath()
  ctx.lineWidth = 1
  // 行
  for (let i = 0; i <= rcs; i++) {
    ctx.moveTo(padding + gap * i, padding)
    ctx.lineTo(padding + gap * i, padding + gap * rcs)
  }
  // 列
  for (let i = 0; i <= rcs; i++) {
    ctx.moveTo(padding, padding + gap * i)
    ctx.lineTo(padding + gap * rcs, padding + gap * i)
  }
  ctx.strokeStyle = '#000'
  ctx.stroke()
  ctx.closePath()
  // 制作中心圆点  
  ctx.beginPath()
  ctx.arc(
    padding + gap * rcs / 2, padding + gap * rcs / 2, 5, 0, 2 * Math.PI
  )
  ctx.fillStyle = '#000'
  ctx.fill()
  ctx.closePath()
}

棋子的制作

咱们需求在队伍线条交接的当地需求放置棋子,所以咱们每次制作需求循环棋盘的数据,依据棋盘数据在指定的当地制作棋子

/**
 * 制作棋子,先循环列,再循环行
 * @param ctx canvas的2d实例
 * @param data 棋盘数据
 * @param number 队伍数
 * @param gap 队伍间隔间隔
 * @param padding 棋盘边距
 * @param radius 棋子的半径
 */
const drawPieces = (
  ctx: CanvasRenderingContext2D,
  data: GobangData,
  gap: number,
  padding: number,
  radius = 12
) => {
  const m = data.length, n = data[0].length
  for (let i = 0; i < m; i++) {
    const cj = i * gap + padding + 6 - padding
    const sj = padding + i * gap
    for (let j = 0; j < n; j++) {
      // 值为 undefined 时跳过
      if (data[i][j] === undefined) {
        continue
      }
      const ci = j * gap + padding + 6 - padding
      const si = padding + j * gap
      if (!data[i][j]) {
        // 值为 1 时,制作黑棋
        drawBlackPieces(
          ctx, ci, cj, si, sj, radius
        )
      } else {
        // 值为 0 时,制作黑棋
        drawWhitePieces(
          ctx, ci, cj, si, sj, radius
        )
      }
    }
  }
}

黑白子的制作,仅仅色彩初始化电脑的后果不相同

// 制作白子
function drawWhitePieces(
  ctx: CanvasRenderingContext2D, ci: number, cj: number, si: number, sj: number, radius = 12
) {
  ctx.beginPath()
  const lg2 = ctx.createRadialGradient(
    ci, cj, 5, ci, cj, 20
  )
  // 向圆形渐变上增加色彩 
  lg2.addColorStop(0.1, '#fff')
  lg2.addColorStop(0.9, '#ddd')
  ctx.fillStyle = lg2
  ctx.arc(
    si, sj, radius, 0, 2 * Math.PI
  )
  ctx.fill()
  ctx.closePath()
}
// 制作黑子
function drawBlackPieces(
  ctx: CanvasRenderingContext2D, ci: number, cj: number, si: number, sj: number, radius = 12
) {
  ctx.beginPath()
  const lg2 = ctx.createRadialGradient(
    ci, cj, 5, ci, cj, 20
  )
  // 向圆形渐变上增加色彩 
  lg2.addColorStop(0.1, '#666')
  lg2.addColorStop(0.9, '#000')
  ctx.fillStyle = lg2
  ctx.arc(
    si, sj, radius, 0, 2 * Math.PI
  )
  ctx.fill()
  ctx.closePath()
}

其中 cicj 是用于棋子上渐变的坐标,sisj 是用于棋子制作的圆心坐标。

在点击 canvas 的时分获取相对于棋盘数据的坐标点

const canvasClick = (e: MouseEvent) => {
  if (gameOver.value) {
    return
  }
  const { offsetX, offsetY } = e
  const posi = getPostions(
    offsetX, offsetY, gap, padding, radius
  )
  // 当时方位在放置棋子范围内且没有放置棋子
  if (posi && !data[posi[0]][posi[1]]) {
    data[posi[0]][posi[1]] = current.value
    init()
    pushStack(data)
    const res = isOver(data)
    if (res) {
      gameOver.value = true
      setTimeout(() => {
        const msg = (Array.isArray(res) ? `${data[res[0]][res[1]] ? '白' : '黑'}方取胜!` : '平局!')
        alert('游戏完毕,' + msg)
      }, 50)
    }
  }
}
/**
 * 依据点击的坐标来获取棋盘数据的坐标
 * @param offsetX 相对于父级元素的 x => 列方位
 * @param offsetY 相对于父级元素的 Y => 行方位
 * @param gap 队伍间隔间隔
 */
const getPostions = (
  offsetX: number, offsetY: number, gap: number, padding: number, r = 12
): [number, number] | false => {
  const x = Math.round((offsetY - padding) / gap)
  const y = Math.round((offsetX - padding) / gap)
  // x1, y1 为圆心坐标
  const x1 = x * gap + padding, y1 = y * gap + padding
  const nr = Math.pow(Math.pow(x1 - offsetY, 2) + Math.pow(y1 - offsetX, 2), 0.5)
  if (nr <= r) {
    return [x, y]
  }
  return false
}

这儿来判别点击的当时方位是否是有用的,而且详细坐标的规则是:

  • 首先需求获取当时点最靠近哪一个棋子的圆初始化英文心坐标
  • 然后因为棋子的半径是 12,所以点击的方位间隔棋子圆心的间隔不能超过 12
  • 满意则回来详细坐标,不满意则回来 false初始化是什么意思

是否完毕

游戏完毕分为两种情况:

  • 一切格子悉数填满,平局
  • Canvas有相同的 5 颗棋子连成一条线,判输赢

在每一次棋子放下之后,就jsonobject需求判别一次是否完毕,咱们每次需求判别一个坐标点的八个方向是否有相同的 4 颗棋子连json格式成一canvas上交条线。可是咱们是依照从左至右,从上往下的次序来查看的,所以详细查看只需求初始化是什么意思四个方向即可。

/**
 * 判别是否完毕
 * 从当时点查询八个方向的连续5个方位是否能连城线
 * 可是在详细的逻辑判别中,是从左往右,从上往下一次判别的,
 * 所以在真正的执行过程中,只需求判别4个方向即可
 * 这儿挑选的四个方向是:右上、右、右下、下
 * @param {GobangData} data 棋盘数据
 */
const isOver = (data: GobangData) => {
  const m = data.length, n = data[0].length
  let nullCnt = m * n
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (data[i][j] !== undefined) {
        nullCnt--
        if (getPostionResult(data, i, j, m, n)) {
          return [i, j]
        }
      }
    }
  }
  // 是否一切格子都已已有棋子
  return !nullCnt
}
/**
 * 判读当时坐标是否满意完毕要求
 * @param {GobangData} data 棋盘数据
 * @param {number} x x 轴
 * @param {number} y y 轴
 * @param {number} m 最大行数
 * @param {number} n 最大列数
 * @returns {boolean}
 */
function getPostionResult(
  data: GobangData, x: number, y: number, m: number, n: number
) {
  //          右上      右      右下    下
  const ds = [[-1, 1], [0, 1], [1, 1], [1, 0]]
  const val = data[x][y]
  for (let i = 0; i < ds.length; i++) {
    const [dx, dy] = ds[i]
    let nx = x, ny = y, flag = true
    for (let i = 0; i < 4; i++) {
      nx += dx
      ny += dy
      // 是否是有用坐标,且值是否相同
      if (!(nx >= 0 && nx < m && ny >= 0 && ny < n) || data[nx][ny] !== val) {
        flag = false
        break
      }
    }
    // 已有 5 颗连成一条线
    if (flag) {
      return true
    }
  }
  return false
}

关于是否完毕的优化

是否完毕还有一个优化的点,便是咱们不需求判别一切坐标点是否满意,canvas英语咱们只需求判别最终一个放置棋子的点是否满意完毕条canvas平台件,可是如果只缓存的视频在哪判别单个点的话,咱们需求判别这个点的八个方向,所以能够优化下:

//           右上      左下       右      左          右下    左上        下      上
const ds = [[[-1, 1], [1, -1]], [[0, 1], [0, -1]], [[1, 1], [-1, -1]], [[1, 0], [-1, 0]]]
/**
 * 判读当时坐标是否满意完毕要求
 * @param {GobangData} data 棋盘数据
 * @param {number} x x 轴
 * @param {number} y y 轴
 * @param {number} m 最大行数
 * @param {number} n 最大列数
 * @returns {boolean}
 */
function getPostionResult(
  data: GobangData, x: number, y: number, m: number, n: number
) {
  const val = data[x][y]
  for (let i = 0; i < ds.length; i++) {
    const [[lx, ly], [rx, ry]] = ds[i]
    let nx = x, ny = y, cnt = 1
    for (let j = 0; j < 4; j++) {
      nx += lx
      ny += ly
      if (!(nx >= 0 && nx < m && ny >= 0 && ny < n) || data[nx][ny] !== val) {
        break
      }
      cnt++
    }
    nx = x
    ny = y
    for (let j = 0; j < 4; j++) {
      nx += rx
      ny += ry
      if (!(nx >= 0 && nx < m && ny >= 0 && ny < n) || data[nx][ny] !== val) {
        break
      }
      cnt++
    }
    if (cnt >= 5) {
      return true
    }
  }
  return false
}
/**
 * 判别是否完毕
 * 从当时点查询八个方向的连续5个方位是否能连城线
 * 一切格子是否悉数填满
 * 最终下棋的坐标是否连城线
 * @param {GobangData} data 棋盘数据
 * @param {[number, number]} posi 最终一个是否满意完毕的坐标点
 */
export const isOver = (data: GobangData, posi: [number, number]) => {
  const m = data.length, n = data[0].length
  let nullCnt = m * n
  // 先判别最终一个点是否满意完毕
  if (getPostionResult(data, posi[0], posi[1], m, n)) {
    return posi
  }
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (data[i][j] !== undefined) {
        nullCnt--
      }
    }
  }
  return !nullCnt
}

悔棋功用

悔棋,也便是吊销功用,在放子的时分,保存当时的棋盘数据的快照,在悔棋的时分,拿到前一个快照的数据渲染出来。在做数初始化电脑时出现问题未进行更改据深复制的时分,用 JSON 的字符串解析办法,和 lodash 的深复制办法,都会讲原稀疏数组的空值都会填满,会破坏稀疏数组的结构定义,所以就自己依据场景写了一个复制办法:

// 深复制稀疏数组
function cloneDeep<T extends GobangData>(data: T):T {
  const m = data.length, n = data[0].length
  const res = new Array(m).fill(0).map(() => new Array(n)) as T
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (data[i][j] !== undefined) {
        res[i][j] = data[i][j]
      }
    }
  }
  return res
}
// 缓存
const cacheData: GobangData[] = [cloneDeep<GobangData>(data)]
const cacheIndex = ref(0)
const pushStack = (data: GobangData) => {
  cacheData.push(cloneDeep<GobangData>(data))
  cacheIndex.value++
}
const popStack = () => {
  if (cacheIndex.value && !gameOver.value) {
    data = cloneDeep(cacheData[--cacheIndex.value])
    cacheData.length = cacheIndex.value + 1
    init()
  }
}

到这儿,一个简略的五子棋就完成了。

GitHub:五子棋