一、前言

使用 js+canvas+less 制作一个简易的五子棋小游戏,游戏自带AI下棋,并且带有悔棋功能。这里只介绍一下js部分,其它部分请查看源码。

二、开发流程

(1)绘制棋盘

这里使用canvas绘制一个 15 x 15大小的棋盘。

// 绘制棋盘
    function chessBoard() {
        for (var i = 0; i < chessWidth; i++) {
            // 绘制棋盘横向线段
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(20, 20 + i * 40);
            ctx.lineTo(580, 20 + i * 40);
            ctx.stroke();
            ctx.restore();
            // 绘制棋盘纵向线段
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(20 + i * 40, 20);
            ctx.lineTo(20 + i * 40, 580);
            ctx.stroke();
            ctx.restore();
        }
    }

在这里插入图片描述

(2)绘制棋子

我们的棋子应该要绘制在横线和竖线的交点上,并且要实现鼠标点击捕获绘制功能,意思就是,当玩家鼠标点击以交点为中心的某个范围(如下图 红色区域)时,棋子也能落在线条交点上。

在这里插入图片描述
这里有两种绘制方式,第一种就是将鼠标点击棋盘后相对于棋盘的坐标进行取整(由于我们棋盘间隔是40,所以对坐标除以40取整),然后将取整后的坐标传给绘制棋子函数,以此来确定落子位置。第二种方式是通过数组遍历,计算当前位置的坐标,然后再进行棋子的绘制。不难看出第一种方式要比第二种方式的效率高。

// 绘制棋子
    function drawChess(eventX, eventY, flag) {
        // flag: ture: 真人玩家, false:电脑玩家
        ctx.fillStyle = flag ? "#000" : "#fff";
        ctx.beginPath();
        ctx.arc(20 + eventX * 40, 20 + eventY * 40, 10, 0, 360 * Math.PI / 180, true);
        ctx.fill();
        // 将棋子聚焦到线条的交点上(方式二,性能较低)
        // var wrap = document.querySelector(".game-container");
        // eventX = event.clientX - wrap.offsetLeft;
        // eventY = event.clientY - wrap.offsetTop;
        // for (var i = 0; i < chessWidth; i++) {
        //     for (var j = 0; j < chessWidth; j++) {
        //         if (eventX >= (20 + j * 40 - 20) && eventX <= (20 + j * 40 + 20) && eventY >= (20 + i * 40 - 20) && eventY <= (20 + i * 40 + 20)) {
        //             eventX =0 * j + 20;
        //             eventY = 40 * i + 20;
        //             break;
        //         }
        //     }
        // }
        // ctx.beginPath();
        // ctx.arc(eventX, eventY, 10, 0, 360 * Math.PI, true);
        // ctx.fill();
    }

(3)玩家下棋

有了棋盘和棋子,就要开始下棋啦,玩家下棋的话没有什么难度,需要注意的就是前面提到的传递给绘制棋子函数的坐标(eventX,eventY)的取整问题,以及和UI之间关联。

// 玩家下棋
    chess.addEventListener("click", function (event) {
        event = event || window.event;
        // 如果没有轮到玩家落子
        if (!isPlayer) {
            alert("不要急,还没有轮到你哦。");
            return;
        }
        // 如果游戏已经结束
        if (isGameOver) {
            alert("游戏已经结束,请点击重新开始。");
            return;
        }
        // 使棋子落在棋盘线条焦点上
        eventX = Math.floor(event.offsetX / 40);
        eventY = Math.floor(event.offsetY / 40);
        // 如果当前位置没有落子才能落子
        if (chessPlace[eventX][eventY] === 0) {
            // 落子
            drawChess(eventX, eventY, true);
            // 记录数据
            handlePlayerData();
            // 落子音效
            downMp3.play();
            // 可以悔棋
            isRetract = true;
            // 不可以撤销悔棋
            isUnretract = false;
            // 可以重新开始
            isRestart = true;
            // 保存该落子的位置
            chessPlace[eventX][eventY] = 1;
            // 判断输赢
            if (win(eventX, eventY, num = 1)) {
                // 游戏结束
                isGameOver = true;
                // 记录数据
                handlePlayerData(true);
                // 播放获胜音乐
                winMp3.play();
                // 电脑不能再落子
                isPlayer = true;
                // 提示是否再来一盘
                var choice = confirm("你赢了,再来一局?");
                if (choice) {
                    // 清空棋盘
                    clearBoard();
                    // 清空落子步数和分数
                    handlePlayerData(false, "again");
                    handleComputerData(false, "again");
                    isGameOver = false;
                }
            }
            // 判断是否平局
            else if (tie()) {
                // 游戏结束
                isGameOver = true;
                // 下一局轮到玩家下棋
                isPlayer = true;
                // 是否再来一局
                var choice = confirm("占成平局,再来一局?");
                if (choice) {
                    // 清空棋盘
                    clearBoard();
                    // 清空落子步数和分数
                    handlePlayerData(false, "again");
                    handleComputerData(false, "again");
                    isGameOver = false;
                }
            } else {
                // 轮到电脑落子
                isPlayer = false;
            }
        } else {
            alert("当前位置已被占据,请选择其他位置落子");
        }
        if (!isGameOver && !isPlayer) {
            // 电脑下棋
            computerDown();
        }
    });

(4)电脑下棋

电脑下棋的话应该是整个游戏制作过程中最为复杂的一步,这里我采用的是五元组和计分表算法来实现电脑AI下棋。
1.五元组:
五子棋盘为15 x 15 的大小,横竖斜四个方向共有572个五元组,给每个五元组一个评分(或权重),这个五元组为它的每个位置贡献的分数就是这个五元组自身的得分,对整个棋盘来说,每个位置的得分就是该位置所在的横竖斜四个方向的所有五元组的得分之和,
然后从所有空位置中选出得分最高的位置就是电脑落子的最优位置。

在这里插入图片描述

如图所示:图中的每一种颜色的长方形里面都有五颗棋子,每种颜色的长方形就是一个五元组,。依次从上到下,从左到右,从正斜反斜方向分别都能构成五元组。

2.计分表:
这里采用的是国外某大佬所给出的一套目前来说最优的计分表,这个计分表可以使电脑玩家十分聪明。

function chessScore(playerNum, computerNum) {
        // 机器进攻

        // 1.既有人类落子,又有机器落子,判分为0
        if (playerNum > 0 && computerNum > 0) {
            return 0;
        }
        // 2.全部为空没有棋子,判分为7(14)
        if (playerNum == 0 && computerNum == 0) {
            return 14;
        }
        // 3.机器落一子,判分为35(70)
        if (computerNum == 1) {
            return 70;
        }
        // 4.机器落两子,判分为800(1600)
        if (computerNum == 2) {
            return 1600;
        }
        // 5.机器落三子,判分为15000(30000)
        if (computerNum == 3) {
            return 30000;
        }
        // 6.机器落四子,判分为800000(1600000)
        if (computerNum == 4) {
            return 1600000;
        }

        // 机器防守

        // 7.玩家落一子,判分为15(30)
        if (playerNum == 1) {
            return 30;
        }
        // 8.玩家落两子,判分为400(800)
        if (playerNum == 2) {
            return 800;
        }
        // 9.玩家落三子,判分为1800(3600)
        if (playerNum == 3) {
            return 3600;
        }
        // 10.玩家落四子,判分为100000(200000)
        if (playerNum == 4) {
            return 200000;
        }

        return -1; //如果是其他情况,则出现错误,不会执行该段代码
    }

遍历五元组得出权重最大的空位置进行落子,此时是AI落子的最优位置。

 // 电脑下棋
    function computerDown() {
        // 初始化scoreGroup评分组
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                scoreGroup[i][j] = 0;
            }
        }
        // 五元组中黑棋(玩家)数量
        var playerNum = 0;
        // 五元组中白棋(电脑)数量
        var computerNum = 0;
        // 五元组临时得分
        var tempScore = 0;
        // 最大得分
        var maxScore = -1;

        // 横向寻找
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth - 4; j++) {
                for (var k = j; k < j + 5; k++) {
                    // 如果是玩家落得子
                    if (chessPlace[k][i] === 1) {
                        playerNum++;
                    } else if (chessPlace[k][i] === 2) { //如果是电脑落子
                        computerNum++;
                    }
                }
                // 将每一个五元组中的黑棋和白棋个数传入评分表中
                tempScore = chessScore(playerNum, computerNum);
                // 为该五元组的每个位置添加分数
                for (var k = j; k < j + 5; k++) {
                    scoreGroup[k][i] += tempScore;
                }
                // 清空五元组中棋子数量和五元组临时得分
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // 纵向寻找
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth - 4; j++) {
                for (var k = 0; k < j + 5; k++) {
                    // 如果是玩家落得子
                    if (chessPlace[i][k] === 1) {
                        playerNum++;
                    } else if (chessPlace[i][k] === 2) { //如果是电脑落子
                        computerNum++;
                    }
                }
                // 将每一个五元组中的黑棋和白棋个数传入评分表中
                tempScore = chessScore(playerNum, computerNum);
                // 为该五元组的每个位置添加分数
                for (var k = j; k < j + 5; k++) {
                    scoreGroup[i][k] += tempScore;
                }
                // 清空五元组中棋子数量和瞬时分数值
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }


        // 反斜线寻找

        // 反斜线上侧部分
        for (var i = chessWidth - 1; i >= 4; i--) {
            for (var k = i, j = 0; j < chessWidth && k >= 0; j++, k--) {
                var m = k; //x 14 13
                var n = j; //y 0  1
                for (; m > k - 5 && k - 5 >= -1; m--, n++) {
                    // 如果是玩家落得子
                    if (chessPlace[m][n] === 1) {
                        playerNum++;
                    } else if (chessPlace[m][n] === 2) { //如果是电脑落子
                        computerNum++;
                    }
                }
                // 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况
                if (m === k - 5) {
                    // 将每一个五元组中的黑棋和白棋个数传入评分表中
                    tempScore = chessScore(playerNum, computerNum);
                    // 为该五元组的每个位置添加分数
                    for (m = k, n = j; m > k - 5; m--, n++) {
                        scoreGroup[m][n] += tempScore;
                    }
                }
                // 清空五元组中棋子数量和五元组临时得分
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }
        // 反斜线下侧部分
        for (var i = 1; i < 15; i++) {
            for (var k = i, j = chessWidth - 1; j >= 0 && k < 15; j--, k++) {
                var m = k; //y 1 
                var n = j; //x 14
                for (; m < k + 5 && k + 5 <= 15; m++, n--) {
                    // 如果是玩家落得子
                    if (chessPlace[n][m] === 1) {
                        playerNum++;
                    } else if (chessPlace[n][m] === 2) { //如果是电脑落子
                        computerNum++;
                    }
                }
                // 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况
                if (m === k + 5) {
                    // 将每一个五元组中的黑棋和白棋个数传入评分表 中
                    tempScore = chessScore(playerNum, computerNum);
                    // 为该五元组的每个位置添加分数
                    for (m = k, n = j; m < k + 5; m++, n--) {
                        scoreGroup[n][m] += tempScore;
                    }
                }
                // 清空五元组中棋子数量和五元组临时得分
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // 正斜线寻找

        // 正斜线上侧部分
        for (var i = 0; i < chessWidth - 1; i++) {
            for (var k = i, j = 0; j < chessWidth && k < chessWidth; j++, k++) {
                var m = k;
                var n = j;
                for (; m < k + 5 && k + 5 <= chessWidth; m++, n++) {
                    // 如果是玩家落得子
                    if (chessPlace[m][n] === 1) {
                        playerNum++;
                    } else if (chessPlace[m][n] === 2) { //如果是电脑落子
                        computerNum++;
                    }
                }
                // 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况
                if (m === k + 5) {
                    // 将每一个五元组中的黑棋和白棋个数传入评分表中
                    tempScore = chessScore(playerNum, computerNum);
                    // 为该五元组的每个位置添加分数
                    for (m = k, n = j; m < k + 5; m++, n++) {
                        scoreGroup[m][n] += tempScore;
                    }
                }
                // 清空五元组中棋子数量和五元组临时得分
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // 正斜线下侧部分
        for (var i = 1; i < chessWidth - 4; i++) {
            for (var k = i, j = 0; j < chessWidth && k < chessWidth; j++, k++) {
                var m = k;
                var n = j;
                for (; m < k + 5 && k + 5 <= chessWidth; m++, n++) {
                    // 如果是玩家落得子
                    if (chessPlace[n][m] === 1) {
                        playerNum++;
                    } else if (chessPlace[n][m] === 2) { //如果是电脑落子
                        computerNum++;
                    }
                }
                // 注意在斜向判断时,可能出现构不成五元组(靠近棋盘的四个顶角)的情况,所以要忽略这种情况
                if (m === k + 5) {
                    // 将每一个五元组中的黑棋和白棋个数传入评分表中
                    tempScore = chessScore(playerNum, computerNum);
                    // 为该五元组的每个位置添加分数
                    for (m = k, n = j; m < k + 5; m++, n++) {
                        scoreGroup[n][m] += tempScore;
                    }
                }
                // 清空五元组中棋子数量和五元组临时得分
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // 从空位置中找到得分最大的位置
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                if (chessPlace[i][j] === 0 && scoreGroup[i][j] > maxScore) {
                    goalX = i;
                    goalY = j;
                    maxScore = scoreGroup[i][j];
                }
            }
        }
        if (goalX != -1 && goalY != -1 && chessPlace[goalX][goalY] === 0) {
            // 落子
            drawChess(goalX, goalY, false);
            // 保存游戏数据
            handleComputerData();
            // 保存该位置的落子
            chessPlace[goalX][goalY] = 2;
            // 判断输赢
            if (win(goalX, goalY, num = 2)) {
                // 游戏结束
                isGameOver = true;
                // 保存游戏数据
                handleComputerData(true);
                // 下一轮玩家落子
                isPlayer = true;
                var choice = confirm("你输了,再来一局?");
                // 播放失败音效
                failMp3.play();
                if (choice) {
                    // 清空棋盘
                    clearBoard();
                    // 清空落子步数和分数
                    handlePlayerData(false, "again");
                    handleComputerData(false, "again");
                    isGameOver = false;
                }
            } else if (tie()) {
                // 游戏结束
                isGameOver = true;
                // 下一局轮到玩家下棋
                isPlayer = true;
                // 是否再来一局
                var choice = confirm("占成平局,再来一局?");
                if (choice) {
                    // 清空棋盘
                    clearBoard();
                    // 清空落子步数和分数
                    handlePlayerData(false, "again");
                    handleComputerData(false, "again");
                    isGameOver = false;
                }
            } else {
                // 轮到玩家下棋
                isPlayer = true;
            }
        }

    }

(5)清空棋盘

清空棋盘可以使用canvas中的clearRect方法来实现,需要注意的是每次清空棋盘都要重置棋盘位置信息。

// 清空棋盘
    function clear() {
        // 清空棋盘
        ctx.clearRect(0, 0, chess.width, chess.height);
        // 重新绘制棋盘
        chessBoard();
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                // 重置棋盘各个位置信息为0(标识此时棋盘没有落子)
                chessPlace[i][j] = 0;
            }
        }
    }

(6)悔棋功能

悔棋功能主要是将要悔棋的位置的棋子清空,这里我采用了定点清除,重新绘制的方法。我们首先在要悔棋的坐标处绘制和棋子相同大小的圆将棋盘上的棋子覆盖掉,这时我们会发现,不仅棋子被覆盖掉,棋盘上的棋子覆盖区域的棋盘线条也被覆盖掉了,所以我们还要将覆盖掉的线条补上。

a.悔棋前:
在这里插入图片描述

b.悔棋后:

在这里插入图片描述
c.通过图中观察我们可以发现,缺失部分正是之前的鼠标点击捕获区域,所以我们只需要将这一块区域的线条补上就可以了。

// 清空棋子
    function clearChess(x, y) {
        // 清除该位置棋子
        ctx.clearRect(x * 40, y * 40, 40, 40);
        // 清除棋子的位置标记为零
        chessPlace[x][y] = 0;
        // 绘制被清除的棋盘线条
        x = x * 40 + 20;
        y = y * 40 + 20;
        // 绘制水平线
        ctx.beginPath();
        ctx.moveTo(x - 20, y);
        ctx.lineTo(x + 20, y);
        ctx.stroke();
        // 绘制垂直线
        ctx.beginPath();
        ctx.moveTo(x, y - 20);
        ctx.lineTo(x, y + 20);
        ctx.stroke();
    }

d.悔棋功能:

// 悔棋
    retract.addEventListener("click", function () {
        // 触发按钮点击音效
        clickSound.play();
        if (isGameOver) {
            alert("游戏已经结束,无法悔棋!");
        } else if (!isRetract) {
            alert("无法悔棋!");
        } else {
            isRetract = false; //表示已经悔过棋子了,不能再悔棋
            isUnretract = true; //悔过棋子后才可以撤销悔棋
            clearChess(eventX, eventY); //清除目标位置玩家棋子
            clearChess(goalX, goalY); //清除目标位置电脑棋子
            handlePlayerData(false, "retract"); //重置玩家游戏数据
            handleComputerData(false, "retract"); //重置电脑游戏数据
        }
    });

(7)撤销悔棋功能

撤销悔棋功能还是比较简单的,只需要重新绘制一下上一步的棋子就可以。

// 撤销悔棋
    unretract.addEventListener("click", function () {
        // 触发按钮点击音效
        clickSound.play();
        if (isGameOver) {
            alert("游戏已经结束,无法撤销悔棋!");
        } else if (!isUnretract) {
            alert("无法撤销!");
        } else {
            isUnretract = false; //撤销后不能再次撤销
            isRetract = true; //撤销悔棋后,可以再次悔棋(当前落子位置)
            drawChess(eventX, eventY, true); //绘制目标位置玩家棋子
            drawChess(goalX, goalY, false); //绘制目标位置电脑棋子
            chessPlace[eventX][eventY] = 1;
            chessPlace[goalX][goalY] = 2;
            handlePlayerData(); //重置玩家游戏数据
            handleComputerData(); //重置电脑游戏数据
        }
    });

(8)判定输赢

这里判定输赢的方式有很多种,但唯一不变的原则就是五子连珠。
方式一:遍历整个棋盘寻找五子连珠。

 for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                // 横向获胜
                if (chessPlace[i][j] != 0 && i < chessWidth - 4 &&
                    chessPlace[i][j] == chessPlace[i + 1][j] &&
                    chessPlace[i][j] == chessPlace[i + 2][j] &&
                    chessPlace[i][j] == chessPlace[i + 3][j] &&
                    chessPlace[i][j] == chessPlace[i + 4][j]) {
                    return flag = "win";
                }
                // 纵向获胜
                if (chessPlace[i][j] != 0 && j < chessWidth - 4 &&
                    chessPlace[i][j] == chessPlace[i][j + 1] &&
                    chessPlace[i][j] == chessPlace[i][j + 2] &&
                    chessPlace[i][j] == chessPlace[i][j + 3] &&
                    chessPlace[i][j] == chessPlace[i][j + 4]) {
                    return flag = "win";
                }
                // 正斜线获胜
                if (chessPlace[i][j] != 0 &&
                    i < chessWidth - 4 && j < chessWidth - 4 &&
                    chessPlace[i][j] == chessPlace[i + 1][j + 1] &&
                    chessPlace[i][j] == chessPlace[i + 2][j + 2] &&
                    chessPlace[i][j] == chessPlace[i + 3][j + 3] &&
                    chessPlace[i][j] == chessPlace[i + 4][j + 4]) {
                    return flag = "win";
                }

            }
        }
         //反斜线获胜
        for (var i = 0; i < chessWidth; i++) {
            for (var j = chessWidth - 1; j > 3; j--) {
                if (chessPlace[i][j] != 0 &&
                    chessPlace[i][j] == chessPlace[i + 1][j - 1] &&
                    chessPlace[i][j] == chessPlace[i + 2][j - 2] &&
                    chessPlace[i][j] == chessPlace[i + 3][j - 3] &&
                    chessPlace[i][j] == chessPlace[i + 4][j - 4]) {
                    return flag = "win";
                }
            }
        }

方式二:从当前落子位置向四周位置寻找。

在这里插入图片描述
我们假设图中的红色方块是当前落子位置,红色圆形是其它棋子,那么只要从当前位置向四周进行寻找,只要有同色五颗连在一起,就判定获胜,否则就判定失败。

function win(eventX, eventY, num) {
        // 保存相同棋子连在一起的个数
        var count = 0;
        // 保存当前棋子坐标
        var x = eventX;
        var y = eventY;
        // 横向获胜
        for (var i = x - 1; i >= 0; i--) {
            if (chessPlace[i][y] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = x + 1; i < chessWidth; i++) {
            if (chessPlace[i][y] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
        // 纵向获胜
        for (var i = y - 1; i >= 0; i--) {
            if (chessPlace[x][i] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = y + 1; i < chessWidth; i++) {
            if (chessPlace[x][i] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
        // 正斜线获胜
        for (var i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = x + 1, j = y + 1; i < chessWidth && j < chessWidth; i++, j++) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
        // 反斜线获胜
        for (var i = x - 1, j = y + 1; i >= 0 && j < chessWidth; i--, j++) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = x + 1, j = y - 1; i < chessWidth && j >= 0; i++, j--) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
    }

这里我使用的是第二种方法并且也推荐大家使用第二种方法,因为第一种方式需要从头遍历棋盘,而第二种方式只需从当前位置向四条方向上遍历查看是否有五连珠情况的出现,而不用从头遍历棋盘,所以使用方式二效率更高,且稳定。

(9)判定平局

通过遍历整个棋盘看是否有空位置,如果有则不是平局反之则是平局。

function tie() {
        var count = 0;
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                if (chessPlace[i][j] != 0) {
                    count++;
                } else {
                    break;
                }
            }
        }
        if (count == 225) {
            return true;
        }
    }

到这里我们的五子棋就差不多开发完成了,接下来就是一些UI上面的处理,当然我这里只设计了人机对战并没有设计玩家对玩家,如果有感兴趣的小伙伴可以试着添加一个玩家对玩家的模块。

三、完整游戏效果图

1.菜单部分:
在这里插入图片描述

2.游戏部分:

在这里插入图片描述

四、运行结果展示

AI五子棋在线试玩

AI五子棋在线预览

五、项目源码

五子棋项目源码

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐