目录棋盘绘制棋子的绘制在点击 canvas 的时候获取相对于棋盘数据的坐标点是否结束悔棋功能总结 这里的五子棋只做一些基础的功能,对于相对专业的规则不做处理。 那
这里的五子棋只做一些基础的功能,对于相对专业的规则不做处理。
那么该五子棋实现的规则和功能如下:
<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>
棋盘绘制
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()
}
我们需要在行列线条交接的地方需要放置棋子,所以我们每次绘制需要循环棋盘的数据,根据棋盘数据在指定的地方绘制棋子
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)
}
}
}
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 颗棋子连成一条线。但是我们是依照从左至右,从上往下的顺序来检查的,所以具体检查只需要四个方向即可。
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
}
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]]]
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
}
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:五子棋
到此这篇关于教你用js写一个简单的五子棋小游戏的文章就介绍到这了,更多相关Js写五子棋内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
--结束END--
本文标题: 教你用Js写一个简单的五子棋小游戏
本文链接: https://lsjlt.com/news/153432.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0