20-30 五子棋游戏
20-分析五子棋的实现思路_哔哩哔哩_bilibili20-分析五子棋的实现思路是一次性学会 Canvas 动画绘图(核心精讲+50个案例)2023最新教程的第21集视频,该合集共计53集,视频收藏或关注UP主,及时了解更多相关视频内容。https://www.bilibili.com/video/BV16T411B7kP?spm_id_from=333.788.player.switch&vd_source=9218320e7bcc2e793fa8493559f4acd7&p=21https://www.bilibili.com/video/BV16T411B7kP?spm_id_from=333.788.player.switch&vd_source=9218320e7bcc2e793fa8493559f4acd7&p=21https://www.bilibili.com/video/BV16T411B7kP?spm_id_from=333.788.player.switch&vd_source=9218320e7bcc2e793fa8493559f4acd7&p=21https://www.bilibili.com/video/BV16T411B7kP?spm_id_from=333.788.player.switch&vd_source=9218320e7bcc2e793fa8493559f4acd7&p=21
20-分析五子棋的实现思路
逻辑处理用原生JavaScript写,画布与棋子用canvas画
21-绘制网格棋盘
视频里用的绿色太晃眼了,换了一个更像木质棋盘的颜色 #ebc78f 。
页面底色与棋盘颜色,代码如下:
html,
body {
background: #eee;
}
canvas {
display: block;
margin: 0 auto;
background-color: #ebc78f;
}
棋盘是由多条横线和纵线组合而成,一格是50px,左右余宽 50px, (800-50-50) / 50 = 14,如果i从0开始算,0到14一共是15条线,为了方便计算,设 i 从1开始,for循环至 i < 16结束。
// 1.创建canvas
let canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
document.body.append(canvas)
// 2.获取 context
let context = canvas.getContext('2d')
// 3.画棋盘
// 3.1 棋盘是由多条横线和纵线组合而成,一格按50px算,左右余宽 50px, (800-50-50) / 50 = 14,所以我们需要15条线
for (let i = 1; i < 16; i++) {
// 把线段的开头表示出来(横线)
context.moveTo(50, 50 * i) // 起点
context.lineTo(750, 50 * i) // 终点
context.stroke()
}
for (let i = 1; i < 16; i++) {
// 把线段的开头表示出来(竖线)
context.moveTo(50 * i, 50) // 起点
context.lineTo(50 * i, 750) // 终点
context.stroke()
}
22-点击棋盘绘制棋子
通过 context.beginPath(),可以避免上图这种情况,要注意 .beginPath() 要写在画圆 .arc() 之前。
canvas.addEventListener('click', e => {
let { offsetX: x, offsetY: y } = e
context.beginPath() // 避免起点终点被fill()填充颜色
context.arc(x, y, 20, 0, 2 * Math.PI)
context.fill()
context.closePath() // 配合 .beginPath() 使用
})
因为一个格子是50px,我们设置 (offsetX+25)/2 ,这样计算后可以让偏左的对齐左边的线,偏右的对齐右边的线(offsetY也是同理),在 canvas.addEventListener 中设置如下:
let { offsetX, offsetY } = e
let x = Math.floor((offsetX + 25) / 50) * 50 // 让棋子在网格线正确的位置
let y = Math.floor((offsetY + 25) / 50) * 50
23-棋盘的边界判断
判断点击的位置边界,如果超出棋盘范围则不落棋子。如果offsetX < 25,则不落棋子(因为如果 offsetX >= 25,则可知落棋子在第一列上),offsetY与上下左右四边同理。
// 判断点击的位置边界,如果超出棋盘范围则不落棋子
if (offsetX < 25 || offsetY < 25 || offsetX > 775 || offsetY > 775) {
return;
}
24-绘制不不同的棋子
context.beginPath()
context.arc(x, y, 20, 0, 2 * Math.PI)
// 设置黑白棋子的渐变色
let gBlack = context.createRadialGradient(x - 8, y - 8, 0, x, y, 20)
gBlack.addColorStop(0, '#999')
gBlack.addColorStop(1, '#000')
let gWhite = context.createRadialGradient(x - 5, y - 10, 18, x, y, 40)
gWhite.addColorStop(0, '#fff')
gWhite.addColorStop(1, '#666')
// 判断当前棋子颜色变量,给棋子黑白不同颜色
context.fillStyle = isBlack ? gBlack : gWhite
// 视频里老师的代码
// let tx = isBlack ? x - 10 : x + 10;
// let ty = isBlack ? y - 10 : y + 10;
// let g = context.createRadialGradient(tx, ty, 0, tx, ty, 30);
// g.addColorStop(0, isBlack ? '#ccc' : '#666');
// g.addColorStop(1, isBlack ? '#000' : '#fff');
// context.fillStyle = g
// 视频里老师的代码 END
context.fill()
context.closePath()
25-处理重复落子的问题
第1个数组代表第一列的棋子内容(原点 (0,0) 在左上角,row 相当于 X轴,向右方向为正方向, col 是 Y轴,向下方向为正方向)。
使用一个二维数组,把所有的棋子位置存储起来。棋盘有15横列,每个 i 代表一列,循环15次,设置 circles[i] 为一个空数组。
// 6. 使用一个二维数组把所有的棋子位置存储起来
let circles = [];
// 棋盘有15列,每个i指代每一列
for (let i = 1; i < 16; i++) {
// circles[i]里的每一项都是一个空数组
circles[i] = []
}
console.log(circles);
设置 x = i * 50, y = j * 50 ,可以让棋子在网格线十字中心的位置上,添加一个布尔变量 isBlack,用来控制棋子黑白颜色的切换。
let x = i * 50 // 让棋子在网格线十字中心的位置
let y = j * 50
context.beginPath()
context.arc(x, y, 20, 0, 2 * Math.PI)
// 6.1 把棋子的坐标存到二维数组里
circles[i][j] = isBlack ? 'black' : 'white'
// console.log(circles);
如果 circles[i][j] 不为空,说明里面已经有内容,那么不允许重复落子。
// 6.2 判断当前位置是否已经存在棋子
if (circles[i][j]) {
// 提醒用户, 这里已有棋子
tip.innerText = `不能重复落子!当前是${isBlack ? '黑' : '白'}棋的回合`
return;
}
设置顶部的文字提示。
<div class="tip">请黑棋落子</div>
// ...
// 提醒用户换人
let text = isBlack ? '请黑棋落子' : '请白棋落子'
tip.innerText = text
// ...
26-纵向判断棋子连续
连成线的4种情况:
将坐标(row,col)视作(x,y)更加直观,在纵轴(竖轴)方向上检索是否有5个连续的棋子可以想象为:在X轴位置不变,Y轴加减坐标上下移动,在此期间判断各个棋子是否为同一种颜色。
代码逻辑草稿:
// 以 row, col 为起点,在二维数组里向上和向下查找
circles[row][col-1]
circles[row][col-2]
circles[row][col-3]
circles[row][col-4]
circles[row][col+1]
circles[row][col+2]
circles[row][col+3]
// ...
设置一个 checkVertical 函数,来判断在竖轴方向上是否有五子相连,它的返回值是 return count >=5 ,如果 count 是 5,那么函数结果返回 true,会在下面的步骤中将结果传递给一个 endGame变量作为判断结束游戏的依据(其他三个方向的设计逻辑也是这样)。
【b站视频内提供的代码(有bug)】
// 纵向查找是否有5个连续相同的棋子
function checkVertical(row, col) {
// 记录向上的次数
let up = 0;
// 记录向下的次数
let down = 0;
let times = 0;
// 定义当前总共有几个已经连在一起
let count = 1; // 初始值,自己本身算1个
// 为避免出现死循环,设置一个循环上限 10000
while (times < 17) {
times++;
// 如果棋子已经大于一个指定的次数,就不找了
if (count >= 5) {
break;
}
let target = isBlack ? 'black' : 'white';
// 以 row, col 为起点,在二维数组里向上查找
up++;
if (circles[row][col - up] && circles[row][col - up] == target) {
count++;
};
// 以 row, col 为起点,在二维数组里向下查找
down++;
if (circles[row][col + down] && circles[row][col + down] == target) {
count++;
};
}
return count >= 5;
}
27-处理获胜的逻辑
添加一个 endGame 布尔值变量,初始值为 false, 通过将当前棋子的(i, j) 值传递给四个方向的五子相连函数来判断是否有 true 值存在,如果有,那么游戏结束,棋盘无法再被点击。
// 8.1 定义一个变量标识,是否结束游戏
let endGame = false
// ...
// 8. 判断是否有人已经获胜,endGame为true,则无法点击
if (endGame) {
// 游戏结束
return
}
// ...
// 7.判断当前是否已有对应的棋子连成5颗,endGame为true则出现获胜提示字
endGame = checkVertical(i, j)
if(endGame) {
tip.innerText = `${isBlack?'黑': '白'}方获胜!`
return
}
28-处理棋子非连续的情况
如果相邻的棋子都不是同色的,直接 break 这次的判断循环。
// 如果棋子已经大于一个指定的次数,或者相邻棋子不是同色的(同色棋子不连续),就不找了
if (count >= 5 || (circles[row][col - up] != target && circles[row][col + down] != target)) {
break;
}
29-横向判断是否获胜
【b站视频内提供的代码(有bug)】
// 横向查找是否有5个连续相同的棋子
function checkHorizontal(row, col) {
// 记录向左的次数
let left = 0;
// 记录向右的次数
let right = 0;
let times = 0;
// 定义当前总共有几个已经连在一起
let count = 1; // 初始值,自己本身算1个
// 为避免出现死循环,设置一个循环上限 10000
while (times < 17) {
times++;
let target = isBlack ? 'black' : 'white';
// 以 row, col 为起点,在二维数组里向左查找
left++;
if (circles[row-left][col] && circles[row-left][col] == target) {
count++;
};
// 以 row, col 为起点,在二维数组里向右查找
right++;
if (circles[row+right][col] && circles[row+right][col] == target) {
count++;
};
// 如果棋子已经大于一个指定的次数,或者相邻棋子不是同色的(同色棋子不连续),就不找了
if (count >= 5 || (circles[row-left][col] != target && circles[row+right][col] != target)) {
break;
}
}
return count >= 5;
}
最终我们需要4个方向的判断,所以这里会有4个判断函数,上下、左右、左上右下、右上左下。
// 7.判断当前是否已有对应的棋子连成5颗,endGame为true则出现获胜提示字
endGame = checkVertical(i, j) || checkHorizontal(i, j)
30-斜向判断是否获胜
判断方向在左上右下时,方向往左上延伸,那么X-, Y-,方向往右下延伸,那么X+, Y+。
判断方向在右上左下时,方向往右上延伸,那么X+, Y-,方向往左下延伸,那么X-, Y+。
【完整代码-bug修复版本】
落子靠近棋盘四周一圈时,如果要判断(不存在的)超出棋盘范围的棋子代码会报错,所以进行了 if 判断。
* 如果还有其他bug,建议自己修正一下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html,
body {
background: #eee;
}
canvas {
display: block;
margin: 0 auto;
background-color: #ebc78f;
}
.tip {
text-align: center;
padding: 16px;
color: #666;
}
</style>
</head>
<body>
<div class="tip">请黑棋落子</div>
<script>
// 1.创建canvas
let canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
document.body.append(canvas)
// 获取文字提示 tip 元素
let tip = document.querySelector('.tip')
// 2.获取 context
let context = canvas.getContext('2d')
// 3.画棋盘
// 3.1 棋盘是由多条横线和纵线组合而成,一格按50px算,左右余宽 50px, (800-50-50) / 50 = 14,所以我们需要14条线
for (let i = 1; i < 16; i++) {
// 把线段的开头表示出来(横线)
context.moveTo(50, 50 * i) // 起点
context.lineTo(750, 50 * i) // 终点
context.stroke()
}
for (let i = 1; i < 16; i++) {
// 把线段的开头表示出来(竖线)
context.moveTo(50 * i, 50) // 起点
context.lineTo(50 * i, 750) // 终点
context.stroke()
}
// 6. 使用一个二维数组把所有的棋子位置存储起来
let circles = [];
// 棋盘有15列,每个i指代每一列
for (let i = 1; i < 16; i++) {
// circles[i]里的每一项都是一个空数组
circles[i] = []
}
// console.log(circles);
// 5.使用一个变量保存当前的棋子颜色
let isBlack = true
// 8.1 定义一个变量标识,是否结束游戏
let endGame = false
// 4. 当点击在棋盘里的时候,绘制一个棋子
canvas.addEventListener('click', e => {
let { offsetX, offsetY } = e
// 判断点击的位置边界,如果超出棋盘范围则不落棋子
if (offsetX < 25 || offsetY < 25 || offsetX > 775 || offsetY > 775) {
return;
}
// 格子所在的位置
let i = Math.floor((offsetX + 25) / 50)
let j = Math.floor((offsetY + 25) / 50)
// 8. 判断是否有人已经获胜,endGame为true,则无法点击
if (endGame) {
// 游戏结束
return
}
// 6.2 判断当前位置是否已经存在棋子
if (circles[i][j]) {
// 提醒用户, 这里已有棋子
tip.innerText = `不能重复落子!当前是${isBlack ? '黑' : '白'}棋的回合`
return;
}
let x = i * 50 // 让棋子在网格线正确的位置
let y = j * 50
context.beginPath()
context.arc(x, y, 20, 0, 2 * Math.PI)
// 6.1 把棋子的坐标存到二维数组里
circles[i][j] = isBlack ? 'black' : 'white'
// console.log(circles);
// 设置黑白棋子的渐变色
let gBlack = context.createRadialGradient(x - 8, y - 8, 0, x, y, 20)
gBlack.addColorStop(0, '#999')
gBlack.addColorStop(1, '#000')
let gWhite = context.createRadialGradient(x - 5, y - 10, 18, x, y, 40)
gWhite.addColorStop(0, '#fff')
gWhite.addColorStop(1, '#666')
// 判断当前棋子颜色变量,给棋子黑白不同颜色
context.fillStyle = isBlack ? gBlack : gWhite
// 视频里老师的代码
// let tx = isBlack ? x - 10 : x + 10;
// let ty = isBlack ? y - 10 : y + 10;
// let g = context.createRadialGradient(tx, ty, 0, tx, ty, 30);
// g.addColorStop(0, isBlack ? '#ccc' : '#666');
// g.addColorStop(1, isBlack ? '#000' : '#fff');
// context.fillStyle = g
// 视频里老师的代码 END
context.fill()
context.closePath()
// 7.判断当前是否已有对应的棋子连成5颗,endGame为true则出现获胜提示字
endGame = checkVertical(i, j) || checkHorizontal(i, j) || checkNWtoSE(i, j) || checkNEtoSW(i, j)
if (endGame) {
tip.innerText = `${isBlack ? '黑' : '白'}方获胜!`
return
}
// 提醒用户换人
let text = isBlack ? '请白棋落子' : '请黑棋落子'
tip.innerText = text
isBlack = !isBlack
})
//
// 开始判断4个方向的棋子相连情况
//
// 纵向查找是否有5个连续相同的棋子
function checkVertical(row, col) {
// 记录向上的次数
let up = 0;
// 记录向下的次数
let down = 0;
let target = isBlack ? 'black' : 'white';
let times = 0;
// 定义当前总共有几个已经连在一起
let count = 1; // 初始值,自己本身算1个
// 5个棋子连成线即可结束 times < 6
while (times < 6) {
times++;
// 探寻上边
up++;
// 当col - up > 0,说明棋子的上侧还在棋盘内,开始进行判断
if (col - up > 0) {
if (circles[row][col - up] == target) {
count++;
}
}
// 探寻下边
down++;
// 当col+down < 16,说明棋子的下侧还在棋盘内,开始进行判断
if (col + down < 16) {
if (circles[row][col + down] == target) {
count++;
}
}
// 在col - up与col + down都在棋盘里的情况在,如果相邻棋子不是target,则break,或如果count已满足5颗相连,则break
if (col - up > 0 && col + down < 16) {
if (count >= 5 || (circles[row][col - up] != target && circles[row][col + down] != target)) {
break;
}
}
}
return count >= 5;
}
// 横向查找是否有5个连续相同的棋子
function checkHorizontal(row, col) {
// 记录向左的次数
let left = 0;
// 记录向右的次数
let right = 0;
let target = isBlack ? 'black' : 'white';
let times = 0;
// 定义当前总共有几个已经连在一起
let count = 1; // 初始值,自己本身算1个
// 为避免出现死循环,设置一个循环上限 5
while (times < 6) {
times++;
// 探寻左边
left++;
// 当row-left>0,说明棋子的左侧还在棋盘内,开始进行判断
if (row - left > 0) {
if (circles[row - left][col] == target) {
count++;
}
}
right++;
// 当row+right<16,说明棋子的右侧还在棋盘内,开始进行判断
if (row + right < 16) {
if (circles[row + right][col] == target) {
count++;
}
}
if (row + right < 16 && row - left > 0) {
if (count >= 5 || (circles[row + right][col] != target && circles[row - left][col] != target)) {
break;
}
}
}
return count >= 5;
}
// 判断 \ 左上到右下的方向
function checkNWtoSE(row, col) {
let lt = 0; // 左上
let rb = 0; // 右下
let target = isBlack ? 'black' : 'white';
let times = 0;
let count = 1; // 初始值,自己本身算1个
while (times < 5) {
times++;
// 探寻左上方向
lt++;
if (col - lt > 0 && row - lt > 0) {
if (circles[row - lt][col - lt] == target) {
count++;
}
}
// 探寻右下方向
rb++;
if (row + rb < 16 && col + rb < 16) {
if (circles[row + rb][col + rb] == target) {
count++;
}
}
if (row - lt > 0 && col - lt && row + rb < 16 && col + rb < 16) {
if (count >= 5 || (circles[row - lt][col - lt] != target && circles[row + rb][col + rb] != target)) {
break;
}
}
}
return count >= 5;
}
// 判断 / 右上到左下的方向
function checkNEtoSW(row, col) {
let rt = 0; // 右上
let lb = 0; // 左下
let target = isBlack ? 'black' : 'white';
let times = 0;
let count = 1; // 初始值,自己本身算1个
while (times < 5) {
times++;
// 探寻右上
rt++;
if (col - rt > 0 && row + rt < 16) {
if (circles[row + rt][col - rt] && circles[row + rt][col - rt] == target) {
count++;
};
}
// 探寻左下
lb++;
if (row - lb > 0 && col + lb < 16) {
if (circles[row - lb][col + lb] && circles[row - lb][col + lb] == target) {
count++;
};
}
if (row - lb > 0 && col + lb < 16) {
if (count >= 5 || (circles[row + rt][col - rt] != target && circles[row - lb][col + lb] != target)) {
break;
}
}
}
return count >= 5;
}
</script>
</body>
</html>