当前位置: 首页 > article >正文

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>


http://www.kler.cn/a/526687.html

相关文章:

  • 进阶数据结构——高精度运算
  • 18、智能驾驶芯片外部接口要求
  • 计算机毕业设计Python+CNN卷积神经网络高考推荐系统 高考分数线预测 高考爬虫 协同过滤推荐算法 Vue.js Django Hadoop 大数据毕设
  • 《一文读懂!Q-learning状态-动作值函数的直观理解》
  • 用HTML、CSS和JavaScript实现庆祝2025蛇年大吉(附源码)
  • Spring AI 在微服务中的应用:支持分布式 AI 推理
  • 【2024年华为OD机试】 (B卷,100分)- 乘坐保密电梯(JavaScriptJava PythonC/C++)
  • 如何用大语言模型做一个Html+CSS+JS的词卡网站
  • WINDOWS安装eiseg遇到的问题和解决方法
  • day1-->day7| 机器学习(吴恩达)学习笔记
  • FLTK - FLTK1.4.1 - 搭建模板,将FLTK自带的实现搬过来做实验
  • 知识管理平台在数字经济时代推动企业智慧决策与知识赋能的路径分析
  • 全面认识了解DeepSeek+利用ollama在本地部署、使用和体验deepseek-r1大模型
  • 【仓颉】仓颉编程语言Windows安装指南 配置环境变量 最简单解决中文乱码问题和其他解决方案大全
  • 360嵌入式开发面试题及参考答案
  • 【Linux指令/信号总结】粘滞位 重定向 系统调用 信号产生 信号处理
  • 【开源免费】基于Vue和SpringBoot的医院资源管理系统(附论文)
  • Python的那些事第六篇:从定义到应用,Python函数的奥秘
  • 将多目标贝叶斯优化与强化学习相结合用于TinyML
  • 2024年数据记录
  • 【16届蓝桥杯寒假刷题营】第1期DAY2
  • 创建 priority_queue - 进阶(内置类型)c++
  • React 低代码项目:项目创建
  • .Net / C# 分析文件编码 并将 各种编码格式 转为 另一个编码格式 ( 比如: GB2312→UTF-8, UTF-8→GB2312)
  • Vue中的动态组件是什么?如何动态切换组件?
  • C 标准库 - `<errno.h>`