整体步骤:
- 搭建结构
- 美化样式
- js逻辑
- 定义相关变量
- 定义 绘制棋盘函数
- 定义 绘制棋子函数
- 定义 初始化棋子数组函数
- 全部初始化为 -1 (表示没有棋子,只占位)
- 定义 点击canvs事件
- 判断是正常点击还是悔棋操作
- 通过Array.isArray(),判断是否e对象参数,还是back数组参数
- 正常操作:拿到x,y通过在计算棋盘的中的位置(x,y),其中x:几行,y:几列
- 悔棋操作:使用传入的坐标作为下棋位置
- 判断该位置是否为空
- 绘制棋子
- 记录棋子 0 或1 到 棋盘数组中
- 判断是否胜利
- 切换玩家 0或1
- 判断是正常点击还是悔棋操作
- 定义 判断胜利事件(是否连成五子)
- 监听重新开始按钮,并定义事件 -> 重置所有相关的变量
- 监听悔棋按钮,并定义事件 -> 游戏未胜利,或棋子数量足够,才可进行
- for循环调用 点击 canvas事件,并传入 back 数组,该数组专门存储从开始到现在所下的棋子信息[行数,列数,当前玩家]
- 通过判断 点击 canvas事件 的事件参数,判断是正常点击事件还是悔棋操作
- 最后, 使用传入的坐标作为下棋位置
html结构:
目录
<div class="container">
<h2 class="text-center" style="text-align: center;color: #0feab0;">五子棋</h2>
<div class="btn-box">
<button class="btn">重新开始</button>
<button class="btn2">悔棋</button>
</div>
<canvas width="360" height="360" id="canvas"></canvas>
</div>
</div>
css样式:
.container,
body {
margin: 0;
width: 100vw;
height: 100vh;
background-image: url('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F386c15f2-d308-42f4-b741-113b9ffb6585%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1685778512&t=e162bf3543dc0adab286d27e73078539');
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#canvas {
border: 1px solid black;
}
h2 {
margin-bottom: 6vw;
font-size: 28px;
}
.btn-box {
display: flex;
justify-content: space-between;
}
button {
width: 25vw;
height: 15vw;
border: none;
background-color: #f14e25;
margin: 5vw 5vw;
color: rgba(255, 255, 255, .5);
font-size: 18px;
border-radius: 5vw;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.btn {
background-color: #dff089;
color: rgba(216, 78, 78, 0.5);
}
js代码:
1.定义变量
// 1.渲染上下文
var canvas=document.querySelector('#canvas')
var ctx=canvas.getContext('2d');
// 棋盘格子的宽度
var cellWidth=30;
// 棋盘格子的行数和列数
var rows=11;
var cols=11;
// 棋盘的边缘空白区域
var margin=15;
// 棋盘的宽度 12个棋子
var boardWidth=360;
// 棋子的颜色
var chessColor=['white','black'];
// 当下的玩家
var currentPlayer=0;
// 棋盘上的棋子
var chessboard=[];
// 是否游戏结束
var gameOver=false;
// 悔棋
var back=[]
2.开局调用 初始化棋盘与棋子
initChessboard() // 初始化棋子
drawChessboard() // 初始化棋盘
3.定义 初始化棋盘布局函数
函数中先绘制了棋盘的背景和大小,然后通过beginPath()
开始一条新路径,依次绘制横向和纵向的网格线。在绘制横向和纵向的网格线时,使用了两个嵌套的for
循环,分别处理每一行和每一列上的点。其中,调用了moveTo()
方法将笔触移动到指定位置,并调用lineTo()
方法创建从当前点到指定点的一条线条。最后,调用stroke()
方法,以进行描边操作,完成棋盘的绘制。
// 2.绘制棋盘
function drawChessboard() {
// 绘制棋盘背景与大小
ctx.fillStyle='#d1b18f' // 设置矩形填充色
ctx.fillRect(0,0,boardWidth,boardWidth)
// 绘制棋盘格线
ctx.beginPath()
// 绘制 横线(两点成线)
for(let i=0;i<rows;i++) {
// 点1: moveTo()方法用于将路径移动到画布中的指定点,
// 点2:lineTo()方法用于创建从当前点到指定点的一条线条
// 必须先 moveTo() 再 linTo(),不然线会斜
ctx.moveTo(margin+cellWidth/2,margin+i*cellWidth+cellWidth/2)
ctx.lineTo(boardWidth-margin-cellWidth/2,margin+i*cellWidth+cellWidth/2)
}
// 绘制 竖线
for(let i=0;i<cols;i++) {
ctx.moveTo(margin+i*cellWidth+cellWidth/2,margin+cellWidth/2)
ctx.lineTo(margin+i*cellWidth+cellWidth/2,boardWidth-margin-cellWidth/2)
}
ctx.stroke() // 画线
}
4.定义 初始化棋子
函数中使用了两个嵌套的for
循环,分别处理每一行和每一列上的点。对于每一个点,将其设为默认占位的-1
,作为棋子未落下时的状态。
在脚本的其他部分中,会使用该数组记录已经落下的棋子,以及当前棋手的编号。
// 4.初始化 棋子
function initChessboard() {
for(let i=0;i<rows;i++) {
chessboard[i]=[]
for(let j=0;j<cols;j++) {
chessboard[i][j]=-1 // 默认占位
}
}
}
5.定义 绘制棋子函数
函数接受三个参数:x
和y
表示棋子的行数和列数,color
表示棋子颜色。
在函数中,首先计算出棋子的坐标,然后定义了一个radius
变量来表示棋子的半径。接着使用beginPath()
方法开始一条新路径,然后使用arc()
方法画圆,并设置颜色填充。最后调用fill()
方法,以进行填充操作,完成棋子的绘制。
在绘制棋子时,还添加了悔棋功能的逻辑判断。如果当前落下的棋子并没有在back
数组中,就将该落子信息存储到数组中,以便在悔棋时可以撤销该步棋。
// 5.绘制棋子
function drawChess(x,y,color) {
// 棋子半径
var radius=cellWidth/2
// 计算棋子坐标
var cx=parseInt(margin+y*cellWidth+cellWidth/2) // x坐标 = 边距+列数x棋子宽度÷2
var cy=parseInt(margin+x*cellWidth+cellWidth/2) // y坐标 = 边距+行数x棋子宽度÷2
// 悔棋功能逻辑
// 1.判断当前落下的棋子是否已经在back数组中
// 1.1 如果没有 -> 将落下的棋子信息存放在back数组中
// 1.2 如果存在 -> 否则不做任何操作
let xyc=[x,y,color]
if(back.findIndex(item => item.length&&item[0]===xyc[0]&&item[1]===xyc[1])===-1) {
back.push(xyc)
}
// 绘制棋子
ctx.beginPath()
ctx.arc(cx,cy,radius,2*Math.PI,0)
ctx.fillStyle=color // 填充颜色
ctx.fill() // 填充棋子
}
6.定义 判断胜利函数
思路: 函数中先定义了八个方向,即垂直、水平和对角线方向。然后遍历每个方向,将棋子在该方向上延伸并计算相同颜色棋子的数量,如果找到连成五子的情况,则返回true
,否则继续遍历其他方向。如果所有方向上都没有找到连成五子的情况,则返回false
。
// 6.判断胜利
/**
* 检查给定位置是否连成五子,如果是则返回true,否则返回false。
* @param {number} row - 给定的位置的行坐标。
* @param {number} col - 给定的位置的列坐标。
* @param {number} player - 当前棋手的编号,0表示黑方,1表示白方。
*/
function checkWin(row,col,player) {
// 定义八个方向
var dirs=[
[-1,-1],[-1,0],[-1,1],
[0,-1], [0,1],
[1,-1], [1,0], [1,1]
];
// 遍历每一个方向
for(let i=0;i<dirs.length;i++) {
var count=1;
var dx=dirs[i][0];
var dy=dirs[i][1];
// 延伸直线连同颜色的棋子个数
for(let j=1;j<5;j++) {
var x=row+j*dx;
var y=col+j*dy;
// 如果超出了边界或者下一个棋子与当前棋子颜色不同,则停止延伸
if(x<0||x>=rows||y<0||y>=cols||chessboard[x][y]!==player) {
break;
}
count++;
}
// 如果找到了连成五子的情况,返回true
if(count===5) {
return true;
}
}
// 如果没有找到连成五子的情况,返回false
return false;
}
7.监听 canvas
// 监听 canvas 画布
canvas.addEventListener('click',function(e) {
onTouchStart(e)
})
8.定义 点击 canvas事件
思路:
- 处理点击事件,落子并判断胜负。
- 如果参数e是数组,则为悔棋操作,使用数组中的坐标作为下棋位置。
- 如果不是数组,说明是正常点击事件,获取触摸点的坐标并计算其在棋盘上的行列坐标,并落子,在chessboard数组中记录该位置,并检查是否胜利,如果已经胜利则更新gameOver变量为true。
// 5.点击画布
function onTouchStart(e) {
// 判断是正常点击事件还是悔棋操作
if(Array.isArray(e)) {
// 使用传入的坐标作为下棋位置
var i=e[0]
var j=e[1]
} else {
// 获取点击位置的坐标
// offsetY:相对于当前元素的y坐标,(以元素自身为依据,与css中的position的属性值relative类似)
var x=e.offsetX
var y=e.offsetY
// 计算在棋盘的中的位置
// i: 第几行 = (y坐标-边距)/棋子宽度
// j: 第几列 = (x坐标-边距)/棋子宽度
var i=Math.floor((y-margin)/cellWidth)
var j=Math.floor((x-margin)/cellWidth)
}
// 判断该位置是否为空
if(i>=0&&j>=0&&i<=cols&&j<=rows&&chessboard[i][j]===-1&&!gameOver) {
// 绘制棋子
drawChess(i,j,chessColor[currentPlayer])
// 记录棋子,我方棋子为0,对方棋子为1
chessboard[i][j]=currentPlayer
// 判断是否胜利
if(checkWin(i,j,currentPlayer)) {
// 标记游戏结束
gameOver=true
// 弹出胜利信息
return alert(`${chessColor[currentPlayer]==="white"? '白方':"黑方"} 玩家胜利!`)
}
// 切换玩家 1 => 0 或 0 => 1
currentPlayer=1-currentPlayer
// 此函数:处理了点击棋盘的事件,并进行相应的处理。
// 如果是悔棋操作,则使用数组中的坐标作为下棋位置;
// 如果不是,则获取触摸点的坐标并计算其在棋盘上的行列坐标,并落子,记录该位置在chessboard数组中,并检查是否胜利,如果胜利则更新gameOver变量为true。
}
}
9.重新开始
思路: 清除画布,并重置数据
// 重新开始逻辑
document.querySelector('.btn').addEventListener('click',function() {
ctx.clearRect(0,0,canvas.width,canvas.height);
initChessboard() // 初始化棋子
drawChessboard() // 初始化棋盘
back=[]
gameOver=false
currentPlayer=0
})
10.悔棋
思路:
- 判断棋子是否已下满8个,如果没满则不能进行悔棋操作。
- 判断当前游戏是否已经结束,如果结束则不能进行悔棋操作。
- 清空画布,并重新绘制棋盘和棋子。
- 从存储棋子坐标的back数组中删除最后两个元素,模拟悔棋操作。
- 重置当前玩家为0。
- 根据存储在back数组中的棋子坐标依次调用onTouchStart()方法,以重新绘制棋子。
// 悔棋-逻辑
document.querySelector('.btn2').addEventListener('click',function() {
if(back.length<8) {
return alert('总棋子到8步后,方可悔棋!')
} else if(gameOver) {
return alert('此局已结束,请开始下一局!')
}
// 清空棋盘
ctx.clearRect(0,0,canvas.width,canvas.height);
initChessboard() // 初始化棋子
// 重新渲染棋盘
drawChessboard()
// 从back数组中过滤最后两个棋子信息
back=back.slice(0,-2)
// 重置当前选手为0
currentPlayer=0
// for 循环模拟点击 悔棋之后的逻辑
for(let index=0;index<back.length;index++) {
// 传入index 方便调用back中的每一项
onTouchStart(back[index])
}
})