前言

大家好,今天分享一个具备智能自动寻路功能的贪吃蛇游戏开发全过程。这个项目不同于普通的贪吃蛇游戏,它能够让蛇自己思考、规划路径,安全地吃到食物并避开障碍物。整个项目的核心亮点在于自动模式的寻路算法设计,我会详细讲解BFS寻路、安全度评估、多策略评分等核心技术的实现原理。

在这里插入图片描述


一、项目概述

1.1 技术栈

  • HTML5 Canvas - 游戏画布渲染
  • 原生JavaScript - 游戏逻辑与算法实现
  • BFS广度优先搜索 - 核心寻路算法
  • 启发式评分系统 - 智能决策机制

1.2 功能特性

功能 描述
手动控制 键盘方向键/WASD控制蛇的移动方向
自动寻路 AI自动规划最优路径,安全觅食
可调速度 滑块控制游戏运行速度
可调画布 支持不同尺寸的游戏区域
自定义颜色 可自定义蛇身颜色
最高分记录 localStorage持久化存储

二、游戏核心架构

2.1 数据结构设计

// 游戏状态变量
let snake = [];                    // 蛇身数组,每节是一个坐标对象
let food = {};                     // 食物位置
let direction = { x: 1, y: 0 };    // 当前移动方向
let nextDirection = { x: 1, y: 0 };// 下一帧的移动方向
let isAutoMode = false;            // 是否为自动模式
const gridSize = 20;               // 网格单元大小(像素)
let tileCount;                     // 网格数量(画布大小/网格大小)

设计要点

  • 使用数组存储蛇身,索引0为蛇头,便于快速访问
  • directionnextDirection分离,避免一帧内方向突变导致的作弊
  • tileCount动态计算,适配不同尺寸画布

2.2 游戏循环机制

function startGame() {
    if (gameLoop) {
        clearInterval(gameLoop);
    }
    init();
    // 根据模式选择不同的更新函数
    gameLoop = setInterval(isAutoMode ? autoPlay : update, gameSpeed);
}

核心逻辑

  • 使用setInterval实现固定时间间隔的游戏循环
  • 自动模式和手动模式共用渲染函数draw(),仅更新逻辑不同
  • 模式切换时重建定时器,确保流畅过渡

三、手动模式实现

3.1 键盘事件处理

document.addEventListener('keydown', (e) => {
    if (isAutoMode) return;  // 自动模式下禁用手动控制
    switch (e.key) {
        case 'ArrowUp':
        case 'w':
        case 'W':
            if (direction.y !== 1) nextDirection = { x: 0, y: -1 };
            break;
        // ... 其他方向类似处理
    }
});

关键点

  • direction.y !== 1 防止180度反向移动(蛇不能直接掉头)
  • 支持方向键和WASD两种控制方式
  • 自动模式下自动忽略键盘输入

3.2 碰撞检测与移动

function update() {
    direction = { ...nextDirection };
    const head = {
        x: snake[0].x + direction.x,
        y: snake[0].y + direction.y
    };

    // 边界碰撞检测
    if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) {
        gameOver();
        return;
    }

    // 自身碰撞检测
    if (snake.some(segment => segment.x === head.x && segment.y === head.y)) {
        gameOver();
        return;
    }

    snake.unshift(head);  // 新蛇头加入

    // 吃食物判定
    if (head.x === food.x && head.y === food.y) {
        score += 10;
        spawnFood();  // 生成新食物
    } else {
        snake.pop();  // 未吃到食物则移除蛇尾
    }

    draw();
}

四、【核心亮点】自动寻路系统详解

这是本文的重点部分,让我们一起探索如何让蛇拥有"智能"。

4.1 整体架构

自动寻路系统采用分层决策架构:

┌─────────────────────────────────────────────┐
│           自动寻路决策流程                    │
├─────────────────────────────────────────────┤
│  1. 获取所有有效移动方向                      │
│           ↓                                  │
│  2. 对每个方向进行多维度评分                   │
│     - 路径评分(到食物的距离)                 │
│     - 安全评分(移动后的可活动空间)            │
│     - 跟随蛇尾加分                            │
│           ↓                                  │
│  3. 选择得分最高的方向                        │
└─────────────────────────────────────────────┘

4.2 BFS寻路算法

**广度优先搜索(BFS)**是寻路的核心,它能找到两点之间的最短路径。

function findPath(start, end, snakeBody, willEatFood) {
    const queue = [[start]];      // 队列存储路径
    const visited = new Set();    // 已访问节点集合
    visited.add(`${start.x},${start.y}`);

    // 将蛇身标记为障碍物(吃食物时蛇尾会移动,故不减1)
    for (let i = 0; i < snakeBody.length - (willEatFood ? 0 : 1); i++) {
        visited.add(`${snakeBody[i].x},${snakeBody[i].y}`);
    }

    while (queue.length > 0) {
        const path = queue.shift();
        const current = path[path.length - 1];

        // 到达终点,返回完整路径
        if (current.x === end.x && current.y === end.y) {
            return path;
        }

        // 探索四个方向的邻居
        const neighbors = [
            { x: current.x + 1, y: current.y },  // 右
            { x: current.x - 1, y: current.y },  // 左
            { x: current.x, y: current.y + 1 },  // 下
            { x: current.x, y: current.y - 1 }   // 上
        ];

        for (const neighbor of neighbors) {
            const key = `${neighbor.x},${neighbor.y}`;
            // 边界检查 + 障碍物检查 + 是否已访问
            if (!visited.has(key) &&
                neighbor.x >= 0 && neighbor.x < tileCount &&
                neighbor.y >= 0 && neighbor.y < tileCount) {
                visited.add(key);
                queue.push([...path, neighbor]);  // 扩展路径
            }
        }
    }
    return null;  // 无法到达
}

算法复杂度:O(V + E),V为节点数,E为边数

关键参数说明

参数 类型 说明
start Object 起始坐标
end Object 目标坐标(食物位置)
snakeBody Array 蛇身数组
willEatFood Boolean 移动后是否吃掉食物(影响障碍物计算)

4.3 安全度评估

仅仅找到食物是不够的,蛇还需要知道自己移动后是否安全。安全度定义为:从新蛇头位置能到达的最大空格子数

function calculateSafety(newHead, snakeBody, willEatFood) {
    const reachable = new Set();  // 可达格子集合
    const queue = [newHead];

    // 构建障碍物集合(蛇身)
    const obstacles = new Set();
    for (let i = 0; i < snakeBody.length - (willEatFood ? 0 : 1); i++) {
        obstacles.add(`${snakeBody[i].x},${snakeBody[i].y}`);
    }

    // BFS洪水填充,计算可达空间
    while (queue.length > 0) {
        const current = queue.shift();
        const key = `${current.x},${current.y}`;
        if (reachable.has(key)) continue;
        reachable.add(key);

        const neighbors = [
            { x: current.x + 1, y: current.y },
            { x: current.x - 1, y: current.y },
            { x: current.x, y: current.y + 1 },
            { x: current.x, y: current.y - 1 }
        ];

        for (const neighbor of neighbors) {
            const nKey = `${neighbor.x},${neighbor.y}`;
            if (!reachable.has(nKey) &&
                !obstacles.has(nKey) &&
                neighbor.x >= 0 && neighbor.x < tileCount &&
                neighbor.y >= 0 && neighbor.y < tileCount) {
                queue.push(neighbor);
            }
        }
    }
    return reachable.size;  // 返回可达格子数量
}

设计思路

  • 使用BFS洪水填充算法遍历所有可达格子
  • 吃食物时蛇尾会移动,所以蛇尾不算障碍物
  • 可达空间越大,蛇越安全

4.4 智能方向选择 - 多策略评分系统

这是整个自动系统的核心决策逻辑。我们给每种可能的移动方向打分,选择得分最高的。

function getAutoDirection() {
    const head = snake[0];
    const tail = snake[snake.length - 1];

    // 1. 先过滤掉无效移动
    const validMoves = allDirections.filter(dir => {
        const nextHead = { x: head.x + dir.x, y: head.y + dir.y };

        // 边界检查
        if (nextHead.x < 0 || nextHead.x >= tileCount ||
            nextHead.y < 0 || nextHead.y >= tileCount) {
            return false;
        }

        // 撞自己检测(动态计算是否吃食物)
        const moveWillEatFood = (nextHead.x === food.x && nextHead.y === food.y);
        const checkLength = moveWillEatFood ? snake.length : snake.length - 1;
        for (let i = 0; i < checkLength; i++) {
            if (snake[i].x === nextHead.x && snake[i].y === nextHead.y) {
                return false;
            }
        }

        // 不能180度掉头
        if (dir.x === -direction.x && dir.y === -direction.y) {
            return false;
        }
        return true;
    });

    if (validMoves.length === 0) {
        return direction;  // 无路可走,保持原方向
    }

    // 2. 对每个有效移动进行评分
    let bestMove = validMoves[0];
    let bestScore = -Infinity;

    for (const move of validMoves) {
        const newHead = { x: head.x + move.x, y: head.y + move.y };
        let score = 0;

        const moveWillEatFood = (newHead.x === food.x && newHead.y === food.y);

        // 【评分项1】路径评分:能到达食物的路径越短越好
        const pathToFood = findPath(newHead, food, snake, moveWillEatFood);
        if (pathToFood) {
            score += (tileCount * tileCount - pathToFood.length) * 10;
        } else {
            score -= 500;  // 无法到达食物,严重惩罚
        }

        // 【评分项2】安全评分:移动后的安全空间越大越好
        const safety = calculateSafety(newHead, snake, moveWillEatFood);
        score += safety * 5;

        // 【评分项3】跟随蛇尾策略:朝蛇尾方向移动可以保持空间连通
        const tailDirectionBonus = (Math.abs(newHead.x - tail.x) + Math.abs(newHead.y - tail.y));
        score += tailDirectionBonus;

        // 【评分项4】贪心评分:朝食物方向移动
        const distToFood = Math.abs(newHead.x - food.x) + Math.abs(newHead.y - food.y);
        score += (tileCount * 2 - distToFood);

        // 【评分项5】安全底线:如果安全空间太小,直接否决
        if (safety < tileCount * 3) {
            score -= 1000;
        }

        if (score > bestScore) {
            bestScore = score;
            bestMove = move;
        }
    }
    return bestMove;
}

评分策略详解

评分项 权重 说明
路径评分 ×10 路径越短得分越高,确保高效觅食
安全评分 ×5 可达空间越大越安全
跟随蛇尾 +N 保持与蛇尾连通,避免被困
贪心加分 +N 曼哈顿距离越近越好
安全底线 -1000 安全空间低于阈值直接淘汰

4.5 自动游戏主循环

function autoPlay() {
    if (!isAutoMode) return;

    // 检查食物是否可达,不可达则重新生成
    if (!isFoodReachable()) {
        spawnFood();
        draw();
    }

    // 获取最优方向并执行
    nextDirection = getAutoDirection();
    update();
}

五、食物生成优化

普通贪食蛇可能生成无法到达的食物,导致游戏陷入死局。这就需要优化食物生成逻辑:

function spawnFood() {
    let attempts = 0;
    const maxAttempts = 100;
    let bestPosition = null;
    let bestSafety = 0;

    while (attempts < maxAttempts) {
        const candidate = {
            x: Math.floor(Math.random() * tileCount),
            y: Math.floor(Math.random() * tileCount)
        };

        // 跳过蛇身占据的位置
        if (snake.some(segment => segment.x === candidate.x && segment.y === candidate.y)) {
            attempts++;
            continue;
        }

        // 跳过不可达的位置
        if (!findPath(snake[0], candidate, snake, false)) {
            attempts++;
            continue;
        }

        // 计算该位置的安全度
        const safety = calculateSafety(snake[0], snake, false);

        // 优先选择安全度高的位置
        if (safety > bestSafety) {
            bestSafety = safety;
            bestPosition = candidate;
        }

        // 安全度足够高时直接使用
        if (safety >= tileCount * tileCount * 0.3) {
            food = candidate;
            return;
        }

        attempts++;
    }

    // 使用最佳备选位置
    food = bestPosition || generateRandomFreePosition();
}

六、完整代码

以下是小游戏的完整实现,直接保存为HTML文件运行:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪吃蛇游戏 - 自动模式</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            min-height: 100vh;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            padding: 20px;
        }
        h1 {
            color: #e94560;
            text-align: center;
            margin-bottom: 20px;
            font-size: 2.5rem;
            text-shadow: 0 0 20px rgba(233, 69, 96, 0.5);
        }
        .game-container {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 20px;
            padding: 30px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            backdrop-filter: blur(10px);
        }
        .stats {
            display: flex;
            justify-content: space-between;
            margin-bottom: 15px;
            color: #fff;
            font-size: 1.2rem;
        }
        .stat {
            background: rgba(233, 69, 96, 0.2);
            padding: 10px 20px;
            border-radius: 10px;
            border: 1px solid rgba(233, 69, 96, 0.3);
        }
        .stat span {
            color: #e94560;
            font-weight: bold;
        }
        #gameCanvas {
            border: 3px solid #e94560;
            border-radius: 10px;
            background: #0a0a0a;
            display: block;
            margin: 0 auto;
        }
        .controls {
            margin-top: 20px;
            display: flex;
            gap: 15px;
            justify-content: center;
            flex-wrap: wrap;
        }
        button {
            padding: 12px 30px;
            font-size: 1rem;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s ease;
            font-weight: bold;
        }
        #autoBtn {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        #autoBtn.active {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            box-shadow: 0 0 20px rgba(245, 87, 108, 0.5);
        }
        #restartBtn {
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            color: white;
        }
        .settings-panel {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 15px;
            padding: 20px;
            margin-top: 20px;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
        }
        .setting-item {
            display: flex;
            flex-direction: column;
            gap: 8px;
            background: rgba(0, 0, 0, 0.2);
            padding: 15px;
            border-radius: 10px;
        }
        .setting-item label {
            color: #fff;
            font-size: 0.95rem;
            font-weight: 600;
        }
        .mode-indicator {
            text-align: center;
            margin-top: 15px;
            padding: 10px;
            border-radius: 8px;
            font-weight: bold;
            transition: all 0.3s ease;
        }
        .mode-indicator.manual {
            background: rgba(102, 126, 234, 0.2);
            color: #667eea;
            border: 1px solid #667eea;
        }
        .mode-indicator.auto {
            background: rgba(245, 87, 108, 0.2);
            color: #f5576c;
            border: 1px solid #f5576c;
        }
        .instructions {
            margin-top: 20px;
            color: rgba(255, 255, 255, 0.7);
            text-align: center;
            font-size: 0.9rem;
        }
    </style>
</head>
<body>
    <h1>贪吃蛇游戏</h1>
    <div class="game-container">
        <div class="stats">
            <div class="stat">得分:<span id="score">0</span></div>
            <div class="stat">长度:<span id="length">3</span></div>
            <div class="stat">最高分:<span id="highScore">0</span></div>
        </div>
        <canvas id="gameCanvas" width="400" height="400"></canvas>
        <div class="controls">
            <button id="autoBtn">自动游戏</button>
            <button id="restartBtn">重新开始</button>
        </div>
        <div id="modeIndicator" class="mode-indicator manual">
            当前模式:手动控制
        </div>
        <div class="settings-panel">
            <div class="setting-item">
                <label>游戏速度</label>
                <input type="range" id="speedSlider" min="50" max="300" value="100" step="10">
                <span id="speedValue">100ms</span>
            </div>
            <div class="setting-item">
                <label>区域大小</label>
                <input type="range" id="sizeSlider" min="300" max="600" value="400" step="100">
                <span id="sizeValue">400x400</span>
            </div>
            <div class="setting-item">
                <label>蛇身颜色</label>
                <input type="color" id="colorPicker" value="#4ade80">
                <span id="colorValue">#4ade80</span>
            </div>
        </div>
        <div class="instructions">
            <p>手动模式:使用 ↑↓←→ 或 WASD 控制方向</p>
            <p>自动模式:蛇会自动寻找食物并避开墙壁和自身</p>
        </div>
    </div>

    <script>
        // ========== 完整游戏代码(省略前述已展示的核心算法)==========

        // 初始化游戏
        function init() {
            canvas.width = canvasSize;
            canvas.height = canvasSize;
            tileCount = canvas.width / gridSize;
            snake = [
                { x: 5, y: 10 },
                { x: 4, y: 10 },
                { x: 3, y: 10 }
            ];
            direction = { x: 1, y: 0 };
            nextDirection = { x: 1, y: 0 };
            score = 0;
            scoreEl.textContent = score;
            lengthEl.textContent = snake.length;
            spawnFood();
            draw();
        }

        // 绘制游戏
        function draw() {
            ctx.fillStyle = '#0a0a0a';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // 绘制网格
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
            ctx.lineWidth = 0.5;
            for (let i = 0; i < tileCount; i++) {
                ctx.beginPath();
                ctx.moveTo(i * gridSize, 0);
                ctx.lineTo(i * gridSize, canvas.height);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(0, i * gridSize);
                ctx.lineTo(canvas.width, i * gridSize);
                ctx.stroke();
            }

            // 绘制食物
            ctx.fillStyle = '#e94560';
            ctx.shadowColor = '#e94560';
            ctx.shadowBlur = 15;
            ctx.beginPath();
            ctx.arc(food.x * gridSize + gridSize / 2, food.y * gridSize + gridSize / 2, gridSize / 2 - 2, 0, Math.PI * 2);
            ctx.fill();
            ctx.shadowBlur = 0;

            // 绘制蛇
            snake.forEach((segment, index) => {
                const gradient = ctx.createRadialGradient(
                    segment.x * gridSize + gridSize / 2,
                    segment.y * gridSize + gridSize / 2,
                    0,
                    segment.x * gridSize + gridSize / 2,
                    segment.y * gridSize + gridSize / 2,
                    gridSize / 2
                );
                if (index === 0) {
                    gradient.addColorStop(0, snakeColor);
                    gradient.addColorStop(1, adjustColor(snakeColor, -20));
                } else {
                    const alpha = 1 - (index / snake.length) * 0.5;
                    gradient.addColorStop(0, hexToRgba(snakeColor, alpha));
                    gradient.addColorStop(1, hexToRgba(adjustColor(snakeColor, -20), alpha));
                }
                ctx.fillStyle = gradient;
                ctx.beginPath();
                ctx.roundRect(segment.x * gridSize + 1, segment.y * gridSize + 1, gridSize - 2, gridSize - 2, 4);
                ctx.fill();
            });
        }

        // 更多实现细节见文章前述代码...
    </script>
</body>
</html>

七、运行效果

7.1 手动模式

  • 使用键盘方向键或WASD控制蛇的移动
  • 适合练习操作技巧

7.2 自动模式

  • 点击"自动游戏"按钮开启AI模式
  • 蛇会自动规划最优路径觅食
  • 遇到危险会主动规避
  • 可以实时调整速度观察AI决策过程

八、技术总结

8.1 核心算法回顾

算法 用途 时间复杂度
BFS寻路 找到从蛇头到食物的最短路径 O(V+E)
洪水填充 计算某位置的安全空间大小 O(V+E)
多策略评分 综合评估选择最优移动方向 O(4×V)

8.2 可扩展方向

  1. A*算法优化 - 使用启发式搜索加速长距离寻路
  2. Hamiltonian路径 - 蛇走完整个棋盘的死路解决方案
  3. 机器学习 - 训练神经网络学习更好的决策策略
  4. 多蛇模式 - 添加AI对手实现对战

结语

通过本文,我们完整实现了一个具备智能自动寻路功能的贪吃蛇游戏。核心在于BFS寻路算法配合多维度安全评分系统,能够在保证安全的前提下高效觅食。

完整代码已在上文提供,可以直接复制运行测试。建议在自动模式下降低游戏速度(调整滑块到较慢值),观察自动决策过程和路径选择。

如果有任何问题,欢迎在评论区留言讨论!

Logo

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

更多推荐