<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
這裡的五子棋只做一些基礎的功能,對於相對專業的規則不做處理。
那麼該五子棋實現的規則和功能如下:
<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
是用於棋子繪製的圓心座標。
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 }
這裡來判斷點選的當前位置是否是有效的,並且具體座標的規則是:
遊戲結束分為兩種情況:
在每一次棋子放下之後,就需要判斷一次是否結束,我們每次需要判斷一個座標點的八個方向是否有相同的 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寫五子棋內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45