中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么用Js寫一個簡單的五子棋小游戲

發布時間:2022-07-05 09:30:58 來源:億速云 閱讀:233 作者:iii 欄目:開發技術

這篇文章主要講解了“怎么用Js寫一個簡單的五子棋小游戲”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么用Js寫一個簡單的五子棋小游戲”吧!

怎么用Js寫一個簡單的五子棋小游戲

這里的五子棋只做一些基礎的功能,對于相對專業的規則不做處理。

那么該五子棋實現的規則和功能如下:

  • 整體功能采用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()
}

其中 ci 和 cj 是用于棋子上漸變的坐標,si 和 sj 是用于棋子繪制的圓心坐標。

在點擊 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

是否結束

游戲結束分為兩種情況:

  • 所有格子全部填滿,平局

  • 已有相同的 5 顆棋子連成一條線,判勝負

在每一次棋子放下之后,就需要判斷一次是否結束,我們每次需要判斷一個坐標點的八個方向是否有相同的 4 顆棋子連成一條線。但是我們是依照從左至右,從上往下的順序來檢查的,所以具體檢查只需要四個方向即可。

/**
 * 判斷是否結束
 * 從當前點查詢八個方向的連續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
}

關于是否結束的優化

是否結束還有一個優化的點,就是我們不需要判斷所有坐標點是否滿足,我們只需要判斷最后一個放置棋子的點是否滿足結束條件,但是如果只判斷單個點的話,我們需要判斷這個點的八個方向,所以可以優化下:

//           右上      左下       右      左          右下    左上        下      上
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()
  }
}

感謝各位的閱讀,以上就是“怎么用Js寫一個簡單的五子棋小游戲”的內容了,經過本文的學習后,相信大家對怎么用Js寫一個簡單的五子棋小游戲這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

js
AI

奎屯市| 防城港市| 兴城市| 凭祥市| 抚顺县| 左权县| 天峻县| 闽侯县| 隆德县| 白城市| 廉江市| 同德县| 分宜县| 陆良县| 海淀区| 七台河市| 郴州市| 宝兴县| 仁布县| 岗巴县| 报价| 杂多县| 雷山县| 济阳县| 德惠市| 谢通门县| 大关县| 荆州市| 呼图壁县| 长葛市| 东乌| 惠东县| 尖扎县| 横峰县| 赞皇县| 普安县| 嘉定区| 平邑县| 什邡市| 乌鲁木齐市| 龙泉市|