Vibe Coding一人即团队系列13:基于风格化提示词的贪吃蛇游戏实战

概述

在 AI 辅助编程(Vibe Coding)的实践中,编写高质量的提示词(Prompt)是决定产出质量的关键环节。

本文档不涉及抽象的理论探讨,而是通过一个具体的网页游戏开发案例,详细拆解如何利用"风格化提示词"与"角色扮演"技术,引导 AI 生成可直接运行的 HTML 游戏文件。

我们将对比普通自然语言描述与结构化风格提示词的差异,并提供完整的代码示例与运行说明。

纲要

  • 提示词工程基础
    • Prompt Engineering 的核心价值
    • 自然语言描述 vs. 风格化提示词
  • 角色扮演与任务设定
    • 定义专家角色(Role Prompting
    • 具体化需求与文件约束
  • 风格化设计语言
    • 视觉基调定义(绿色/浅绿色)
    • 元素属性指定(红色小蛇)
  • 代码生成与验证
    • AI 自动生成 snake.html
    • 跨端适配与交互测试
  • 游戏核心机制拆解
    • 游戏循环与帧率控制
    • 碰撞检测与边界处理
    • 食物生成与分数累积
    • 本地存储与最高分持久化

提示词工程的两种实践路径

在 AI 辅助开发中,提示词的编写方式直接影响生成代码的结构完整性与视觉表现。通常而言,我们可将提示词划分为两种风格:普通自然语言描述优雅风格化提示词

普通自然语言描述侧重于功能逻辑的传达,结构松散,适合快速原型验证。而风格化提示词则在功能描述之外,融合了角色设定、设计约束与交互细节,能够引导 AI 生成具备更高完成度与审美一致性的产出物。本文重点剖析后者在实际案例中的应用策略。

角色扮演与上下文注入

为了让 AI 更准确地理解任务复杂度,我们需要在提示词中为其赋予一个特定的"身份"。这种做法在业内被称为角色提示(Role Prompting)。通过在提示词开头声明"你是一位拥有 20 年经验的资深游戏开发工程师,参与过《王者荣耀》与《黑神话:悟空》等大型项目的开发",可以有效提升 AI 在代码结构设计、游戏循环逻辑以及事件处理方面的专业性。

核心角色定义示例:

text 复制代码
你是一位资深的游戏开发工程师,拥有 20 年全栈开发经验,擅长 HTML5 Canvas 游戏引擎设计与交互优化。

需求拆解与文件约束

在定义角色之后,需要明确具体的开发任务与输出格式。为了确保生成的产物可以直接在浏览器环境中运行,我们必须在提示词中显式声明技术栈限制与文件命名规范。

  • 技术栈限定 :必须使用纯 HTMLCSSJavaScript,生成单一可执行文件。
  • 文件命名 :保存为 snake.html,确保用户可直接双击运行。
  • 功能逻辑:实现经典的贪吃蛇移动、食物生成、碰撞检测与分数累计机制。

结构化需求描述示例:

text 复制代码
### 核心需求
1. 开发一款基于 HTML5 Canvas 的贪吃蛇游戏。
2. 所有代码必须内嵌在单个 `snake.html` 文件中。
3. 游戏需支持键盘方向键(↑/↓/←/→)控制蛇的移动方向。
4. 页面需包含游戏重新开始与暂停/继续的功能按钮。

风格化设计语言的注入

为了使生成的游戏界面具有较高的视觉亲和力,我们需要在提示词中细化设计风格。不同于仅描述功能,风格化提示词强调"视觉基调"与"色彩心理学"的结合。

在本案例中,我们定义了以下视觉规范:

  • 主色调:整体页面基调为绿色与浅绿色,营造清新、自然的视觉氛围。
  • 强调色:蛇身使用红色,与背景绿色形成互补对比,提升游戏主体的辨识度。
  • 交互反馈 :食物被吞噬时触发粒子特效(Particle Effects),增强操作爽感。

通过上述设计约束,AI 在生成 CSS 样式与 JavaScript 动画逻辑时,会主动依据色彩搭配原则进行代码生成,而非随机选取颜色值。

生成流程与文件管理

在 VS Code 环境中,我们将上述整合后的提示词输入 AI 辅助编程插件(如 GitHub Copilot 或 Cursor)。在此过程中,建议将执行模式设置为"手动确认",以便在 AI 生成代码前再次核对文件路径与覆盖策略。

当 AI 完成代码编写后,工作区将生成 snake.html 文件。此时,我们无需启动复杂的开发服务器,直接通过文件管理器双击该文件,即可在系统默认浏览器中运行游戏。

项目目录结构:

dir 复制代码
.
├── snake.html          # 核心游戏文件(包含 HTML/CSS/JS)
└── README.md           # 项目说明文档(可选)

游戏核心机制拆解

生成的贪吃蛇游戏本质上是一个基于 Canvas 的实时状态机。为了深入理解其工作原理,我们需要从以下几个核心维度进行剖析。

游戏循环与帧率控制

游戏循环是驱动所有逻辑更新的引擎。本案例采用 setInterval 配合 requestAnimationFrame 的双重策略:setInterval 负责以固定的时间间隔(140ms)驱动蛇的移动逻辑,而 requestAnimationFrame 则负责 UI 的平滑渲染。这种分离方式确保了游戏逻辑的稳定性与画面表现的流畅性。
#mermaid-svg-7F21TTjN58iLnPlh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7F21TTjN58iLnPlh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7F21TTjN58iLnPlh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7F21TTjN58iLnPlh .error-icon{fill:#552222;}#mermaid-svg-7F21TTjN58iLnPlh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7F21TTjN58iLnPlh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7F21TTjN58iLnPlh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7F21TTjN58iLnPlh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7F21TTjN58iLnPlh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7F21TTjN58iLnPlh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7F21TTjN58iLnPlh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7F21TTjN58iLnPlh .marker.cross{stroke:#333333;}#mermaid-svg-7F21TTjN58iLnPlh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7F21TTjN58iLnPlh p{margin:0;}#mermaid-svg-7F21TTjN58iLnPlh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster-label text{fill:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster-label span{color:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster-label span p{background-color:transparent;}#mermaid-svg-7F21TTjN58iLnPlh .label text,#mermaid-svg-7F21TTjN58iLnPlh span{fill:#333;color:#333;}#mermaid-svg-7F21TTjN58iLnPlh .node rect,#mermaid-svg-7F21TTjN58iLnPlh .node circle,#mermaid-svg-7F21TTjN58iLnPlh .node ellipse,#mermaid-svg-7F21TTjN58iLnPlh .node polygon,#mermaid-svg-7F21TTjN58iLnPlh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .rough-node .label text,#mermaid-svg-7F21TTjN58iLnPlh .node .label text,#mermaid-svg-7F21TTjN58iLnPlh .image-shape .label,#mermaid-svg-7F21TTjN58iLnPlh .icon-shape .label{text-anchor:middle;}#mermaid-svg-7F21TTjN58iLnPlh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .rough-node .label,#mermaid-svg-7F21TTjN58iLnPlh .node .label,#mermaid-svg-7F21TTjN58iLnPlh .image-shape .label,#mermaid-svg-7F21TTjN58iLnPlh .icon-shape .label{text-align:center;}#mermaid-svg-7F21TTjN58iLnPlh .node.clickable{cursor:pointer;}#mermaid-svg-7F21TTjN58iLnPlh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7F21TTjN58iLnPlh .arrowheadPath{fill:#333333;}#mermaid-svg-7F21TTjN58iLnPlh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7F21TTjN58iLnPlh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7F21TTjN58iLnPlh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7F21TTjN58iLnPlh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7F21TTjN58iLnPlh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7F21TTjN58iLnPlh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7F21TTjN58iLnPlh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .cluster text{fill:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster span{color:#333;}#mermaid-svg-7F21TTjN58iLnPlh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7F21TTjN58iLnPlh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7F21TTjN58iLnPlh rect.text{fill:none;stroke-width:0;}#mermaid-svg-7F21TTjN58iLnPlh .icon-shape,#mermaid-svg-7F21TTjN58iLnPlh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7F21TTjN58iLnPlh .icon-shape p,#mermaid-svg-7F21TTjN58iLnPlh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7F21TTjN58iLnPlh .icon-shape .label rect,#mermaid-svg-7F21TTjN58iLnPlh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7F21TTjN58iLnPlh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7F21TTjN58iLnPlh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7F21TTjN58iLnPlh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 运行中


暂停/结束
点击重来
点击暂停
启动游戏
初始化游戏状态
启动 setInterval 循环
检查暂停/结束状态
执行 moveSnake
是否吃到食物?
增加分数, 生成新食物
蛇身移动
绘制 Canvas
等待用户操作

碰撞检测与边界处理

碰撞检测是游戏逻辑的核心安全机制。本案例实现了两种碰撞检测:

  1. 边界碰撞 :检测蛇头坐标是否超出 0BOARD_SIZE - 1 的范围。若超出,则立即触发 gameOver 状态。
  2. 自身碰撞:在蛇移动后,遍历蛇身数组(除头部外),检测是否存在与头部坐标重合的节点。若存在,则判定为游戏结束。

食物生成与分数累积

食物的生成采用"排除法"策略:首先计算整个棋盘所有可用坐标的集合,然后剔除当前蛇身占据的坐标,最后从剩余的空闲坐标中随机选取一个作为新食物的位置。这种算法保证了食物不会生成在蛇身上,避免了逻辑冲突。

本地存储与最高分持久化

为了提升用户体验,本游戏利用 localStorage API 实现了最高分记录功能。每当游戏得分超过当前保存的最高分时,系统会自动调用 localStorage.setItem 更新存储值。页面加载时,则通过 localStorage.getItem 读取历史最高分并展示在画布角落。

交互适配与响应式设计

生成的 snake.html 不仅适配桌面端键盘操作,还通过 viewport 设置与触摸事件监听,实现了对移动端浏览器的兼容。这意味着用户可以将该文件部署至任何静态 Web 服务器,并通过手机浏览器访问,获得完整的触屏操作体验。

API 速览

本案例基于浏览器原生 API 实现,无需引入外部依赖。核心涉及的 API 如下:

Canvas API (HTMLCanvasElement)

  • 方法getContext('2d')
  • 描述:获取 2D 绘图上下文,用于绘制游戏网格、蛇身与食物。
  • 示例
javascript 复制代码
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

requestAnimationFrame

  • 方法window.requestAnimationFrame(callback)
  • 描述:浏览器原生动画循环 API,用于实现稳定的游戏帧率更新。
  • 示例
javascript 复制代码
function gameLoop() {
  updateGameState();
  renderCanvas();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);

LocalStorage API

  • 方法localStorage.setItem(key, value)localStorage.getItem(key)
  • 描述:用于在浏览器本地持久化存储游戏最高分记录。
  • 示例
javascript 复制代码
localStorage.setItem('snakeHighScore', JSON.stringify(highScore));
const savedScore = JSON.parse(localStorage.getItem('snakeHighScore') || '0');

触摸事件 API (TouchEvent)

  • 事件touchstarttouchendtouchcancel
  • 描述:用于捕获移动端手指滑动方向,转化为游戏方向指令。
  • 示例
javascript 复制代码
canvas.addEventListener('touchstart', onTouchStart, { passive: true });
canvas.addEventListener('touchend', onTouchEnd, { passive: true });

Demo 示例

以下提供一个可直接运行的贪吃蛇游戏完整代码。该 Demo 演示了完整的游戏循环、碰撞检测、随机食物生成以及响应式触屏支持。

运行说明:

  1. 新建一个文本文件,命名为 snake.html
  2. 将下方代码完整复制并粘贴至该文件中。
  3. 双击 snake.html 文件,使用 Chrome、Edge 或 Firefox 浏览器打开即可开始游戏。

完整代码示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>可爱贪吃蛇 · 风格化版</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
        }
        body {
            min-height: 100vh;
            background: linear-gradient(145deg, #1b3b2b 0%, #2a5e3a 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            padding: 16px;
        }
        .game-wrapper {
            background: #1f4a2f;
            padding: 28px 24px 32px;
            border-radius: 56px 56px 48px 48px;
            box-shadow: 0 16px 32px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
            border: 1px solid #3c7a53;
        }
        .game-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 8px 16px 8px;
        }
        .game-title {
            color: #b3e0c0;
            font-weight: 600;
            font-size: 1.5rem;
            letter-spacing: 2px;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
        }
        .score-panel {
            background: #0f2b1b;
            padding: 8px 18px;
            border-radius: 40px;
            color: #d4f4dd;
            font-weight: 600;
            font-size: 1.1rem;
            border: 1px solid #3f7a57;
            box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.6);
        }
        canvas {
            display: block;
            margin: 0 auto;
            width: 100%;
            max-width: 500px;
            aspect-ratio: 1 / 1;
            background: #143121;
            border-radius: 36px;
            box-shadow: inset 0 -8px 0 #0c1f14, 0 10px 20px rgba(0, 0, 0, 0.5);
            image-rendering: crisp-edges;
            touch-action: none;
            cursor: pointer;
        }
        .control-bar {
            display: flex;
            justify-content: center;
            gap: 24px;
            padding-top: 20px;
        }
        .ctrl-btn {
            background: #2c5d3d;
            border: none;
            padding: 10px 28px;
            border-radius: 60px;
            font-weight: 700;
            font-size: 1.1rem;
            color: #d2f0da;
            box-shadow: 0 6px 0 #0d2113, 0 4px 12px rgba(0, 0, 0, 0.3);
            transition: all 0.06s ease;
            letter-spacing: 1px;
            border: 1px solid #479a62;
            cursor: pointer;
            flex: 1;
            max-width: 160px;
        }
        .ctrl-btn:active {
            transform: translateY(5px);
            box-shadow: 0 1px 0 #0d2113;
        }
        .ctrl-btn:disabled {
            opacity: 0.5;
            transform: translateY(4px);
            box-shadow: 0 2px 0 #0d2113;
            pointer-events: none;
        }
        @media (max-width: 500px) {
            .game-wrapper {
                padding: 16px 12px 20px;
                border-radius: 36px;
            }
            .game-title {
                font-size: 1.2rem;
            }
            .ctrl-btn {
                font-size: 0.9rem;
                padding: 8px 12px;
                max-width: 120px;
            }
        }
    </style>
</head>
<body>
    <div class="game-wrapper">
        <div class="game-header">
            <span class="game-title">🐍 小青蛇</span>
            <span class="score-panel">🍎 <span id="scoreDisplay">0</span></span>
        </div>
        <canvas id="gameCanvas" width="500" height="500"></canvas>
        <div class="control-bar">
            <button class="ctrl-btn" id="restartBtn">🔄 重来</button>
            <button class="ctrl-btn" id="pauseBtn">⏸️ 暂停</button>
        </div>
    </div>
    <script>
        (function () {
            // --- 配置 ---
            const BOARD_SIZE = 20;
            const CELL_SIZE = 25; // 500 / 20

            const canvas = document.getElementById('gameCanvas');
            const ctx = canvas.getContext('2d');
            const scoreSpan = document.getElementById('scoreDisplay');
            const restartBtn = document.getElementById('restartBtn');
            const pauseBtn = document.getElementById('pauseBtn');

            // --- 游戏状态 ---
            let snake = [];
            let direction = { dx: 1, dy: 0 };
            let nextDirection = { dx: 1, dy: 0 };
            let food = { x: 8, y: 10 };
            let score = 0;
            let highScore = parseInt(localStorage.getItem('snakeHighScore')) || 0;

            let gameOver = false;
            let winFlag = false;
            let paused = false;
            let gameInterval = null;
            const MOVE_INTERVAL_MS = 140;

            // --- 初始化 ---
            function initGame() {
                // 蛇: 初始长度为 3,水平放置
                snake = [
                    { x: 7, y: 10 },
                    { x: 8, y: 10 },
                    { x: 9, y: 10 }
                ];
                direction = { dx: 1, dy: 0 };
                nextDirection = { dx: 1, dy: 0 };
                score = 0;
                gameOver = false;
                winFlag = false;
                paused = false;
                pauseBtn.textContent = '⏸️ 暂停';
                updateScoreDisplay();
                generateFood();
                clearGameLoop();
                startGameLoop();
                drawCanvas();
            }

            // --- 游戏循环控制 ---
            function startGameLoop() {
                if (gameInterval) clearInterval(gameInterval);
                gameInterval = setInterval(() => {
                    if (!paused && !gameOver && !winFlag) {
                        moveSnake();
                        drawCanvas();
                    }
                }, MOVE_INTERVAL_MS);
            }

            function clearGameLoop() {
                if (gameInterval) {
                    clearInterval(gameInterval);
                    gameInterval = null;
                }
            }

            // --- 食物生成 (避开蛇身) ---
            function generateFood() {
                const totalCells = BOARD_SIZE * BOARD_SIZE;
                if (snake.length >= totalCells) {
                    winFlag = true;
                    return;
                }
                const snakeSet = new Set(snake.map(cell => `${cell.x},${cell.y}`));
                const freeCells = [];
                for (let i = 0; i < BOARD_SIZE; i++) {
                    for (let j = 0; j < BOARD_SIZE; j++) {
                        if (!snakeSet.has(`${i},${j}`)) freeCells.push({ x: i, y: j });
                    }
                }
                if (freeCells.length === 0) {
                    winFlag = true;
                    return;
                }
                const randIndex = Math.floor(Math.random() * freeCells.length);
                food = freeCells[randIndex];
            }

            // --- 移动逻辑 ---
            function moveSnake() {
                if (gameOver || winFlag) return;

                // 应用有效方向
                const tryDx = nextDirection.dx;
                const tryDy = nextDirection.dy;
                if (!(direction.dx === -tryDx && direction.dy === -tryDy)) {
                    direction = { dx: tryDx, dy: tryDy };
                }

                const head = snake[snake.length - 1];
                const newHead = {
                    x: head.x + direction.dx,
                    y: head.y + direction.dy
                };

                // 边界碰撞
                if (newHead.x < 0 || newHead.x >= BOARD_SIZE || newHead.y < 0 || newHead.y >= BOARD_SIZE) {
                    gameOver = true;
                    clearGameLoop();
                    drawCanvas();
                    return;
                }

                // 检查是否吃到食物
                const isEating = (newHead.x === food.x && newHead.y === food.y);

                // 构造新蛇 (先拷贝)
                let newSnake = [...snake];
                if (isEating) {
                    // 吃到了: 不删尾部,直接加头
                    newSnake.push(newHead);
                } else {
                    // 没吃到: 移除尾部,加头
                    newSnake.shift();
                    newSnake.push(newHead);
                }

                // 检查蛇头是否撞到自己 (新蛇中除了头部自身)
                const headPos = newSnake[newSnake.length - 1];
                for (let i = 0; i < newSnake.length - 1; i++) {
                    if (newSnake[i].x === headPos.x && newSnake[i].y === headPos.y) {
                        gameOver = true;
                        clearGameLoop();
                        snake = newSnake; // 显示撞到的状态
                        drawCanvas();
                        return;
                    }
                }

                // 更新蛇
                snake = newSnake;

                if (isEating) {
                    score++;
                    updateScoreDisplay();
                    if (score > highScore) {
                        highScore = score;
                        localStorage.setItem('snakeHighScore', JSON.stringify(highScore));
                    }
                    generateFood();
                    if (winFlag) {
                        clearGameLoop();
                        drawCanvas();
                        return;
                    }
                }

                // 绘制在下一帧由 interval 触发,但需确保食物生成后立即重绘
                // 这里不重绘,由 interval 统一绘制
            }

            // --- 渲染 ---
            function drawCanvas() {
                ctx.clearRect(0, 0, 500, 500);

                // 绘制网格 (浅绿虚线)
                ctx.strokeStyle = '#2b5a3b';
                ctx.lineWidth = 0.6;
                for (let i = 0; i <= BOARD_SIZE; i++) {
                    ctx.beginPath();
                    ctx.moveTo(i * CELL_SIZE, 0);
                    ctx.lineTo(i * CELL_SIZE, 500);
                    ctx.stroke();
                    ctx.beginPath();
                    ctx.moveTo(0, i * CELL_SIZE);
                    ctx.lineTo(500, i * CELL_SIZE);
                    ctx.stroke();
                }

                // 绘制食物 (红色发光苹果)
                ctx.shadowColor = '#ff7b7b';
                ctx.shadowBlur = 18;
                ctx.fillStyle = '#e33b3b';
                ctx.beginPath();
                const fx = food.x * CELL_SIZE + CELL_SIZE / 2;
                const fy = food.y * CELL_SIZE + CELL_SIZE / 2;
                const rad = CELL_SIZE / 2 - 2;
                ctx.arc(fx, fy, rad, 0, Math.PI * 2);
                ctx.fill();
                // 高光
                ctx.shadowBlur = 0;
                ctx.fillStyle = '#ffa5a5';
                ctx.beginPath();
                ctx.arc(fx - 4, fy - 4, 5, 0, Math.PI * 2);
                ctx.fill();
                ctx.shadowBlur = 0;

                // 绘制蛇 (红色渐变)
                snake.forEach((seg, index) => {
                    const x = seg.x * CELL_SIZE + 1;
                    const y = seg.y * CELL_SIZE + 1;
                    const w = CELL_SIZE - 2;
                    const gradient = ctx.createRadialGradient(
                        x + 4, y + 4, 2,
                        x + w / 2, y + w / 2, w / 2
                    );
                    if (index === snake.length - 1) {
                        // 蛇头:亮红
                        gradient.addColorStop(0, '#ff5e5e');
                        gradient.addColorStop(1, '#cc2222');
                    } else {
                        gradient.addColorStop(0, '#e64949');
                        gradient.addColorStop(1, '#a81c1c');
                    }
                    ctx.fillStyle = gradient;
                    ctx.shadowColor = '#cc4444';
                    ctx.shadowBlur = 8;
                    ctx.beginPath();
                    ctx.roundRect(x, y, w, w, 6);
                    ctx.fill();

                    // 蛇眼睛 (蛇头)
                    if (index === snake.length - 1) {
                        ctx.shadowBlur = 0;
                        ctx.fillStyle = '#f5f9ff';
                        let eyeOffsets = [];
                        if (direction.dx === 1) {
                            eyeOffsets = [
                                [12, 6],
                                [12, 14]
                            ];
                        } else if (direction.dx === -1) {
                            eyeOffsets = [
                                [4, 6],
                                [4, 14]
                            ];
                        } else if (direction.dy === -1) {
                            eyeOffsets = [
                                [6, 4],
                                [14, 4]
                            ];
                        } else if (direction.dy === 1) {
                            eyeOffsets = [
                                [6, 12],
                                [14, 12]
                            ];
                        } else {
                            eyeOffsets = [
                                [12, 6],
                                [12, 14]
                            ];
                        }
                        eyeOffsets.forEach(([ox, oy]) => {
                            ctx.beginPath();
                            ctx.arc(x + ox, y + oy, 3.5, 0, Math.PI * 2);
                            ctx.fill();
                            ctx.fillStyle = '#1a1a2c';
                            ctx.beginPath();
                            ctx.arc(x + ox + (direction.dx * 1.5), y + oy + (direction.dy * 1.5), 1.8, 0, Math.PI * 2);
                            ctx.fill();
                            ctx.fillStyle = '#f5f9ff';
                        });
                        ctx.shadowBlur = 0;
                    }
                });

                // --- 游戏结束/胜利蒙层 ---
                if (gameOver || winFlag) {
                    ctx.fillStyle = 'rgba(10, 25, 15, 0.7)';
                    ctx.shadowBlur = 0;
                    ctx.fillRect(0, 0, 500, 500);
                    ctx.font = 'bold 36px "Segoe UI", system-ui, sans-serif';
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    if (winFlag) {
                        ctx.fillStyle = '#c7f7d3';
                        ctx.fillText('🎉 你赢了!', 250, 220);
                    } else if (gameOver) {
                        ctx.fillStyle = '#fbc2c2';
                        ctx.fillText('💔 游戏结束', 250, 220);
                    }
                    ctx.font = '18px sans-serif';
                    ctx.fillStyle = '#d0e8d0';
                    ctx.fillText('点击 "重来" 继续挑战', 250, 290);
                }

                // 显示最高分
                ctx.font = '14px sans-serif';
                ctx.fillStyle = '#b5d6b5';
                ctx.textAlign = 'right';
                ctx.textBaseline = 'bottom';
                ctx.fillText(`🏆 最高: ${highScore}`, 480, 490);
                ctx.shadowBlur = 0;
            }

            // --- 辅助 roundRect ---
            CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
                if (w < 2 * r) r = w / 2;
                if (h < 2 * r) r = h / 2;
                this.moveTo(x + r, y);
                this.lineTo(x + w - r, y);
                this.quadraticCurveTo(x + w, y, x + w, y + r);
                this.lineTo(x + w, y + h - r);
                this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
                this.lineTo(x + r, y + h);
                this.quadraticCurveTo(x, y + h, x, y + h - r);
                this.lineTo(x, y + r);
                this.quadraticCurveTo(x, y, x + r, y);
                this.closePath();
                return this;
            };

            // --- UI 更新 ---
            function updateScoreDisplay() {
                scoreSpan.textContent = score;
            }

            // --- 方向控制 (防止反向) ---
            function setDirection(dx, dy) {
                if (gameOver || winFlag || paused) return;
                // 不允许直接掉头
                if (direction.dx === -dx && direction.dy === -dy) return;
                nextDirection = { dx, dy };
            }

            // --- 键盘事件 ---
            function onKeyDown(e) {
                const key = e.key;
                e.preventDefault();
                if (key === 'ArrowUp') setDirection(0, -1);
                else if (key === 'ArrowDown') setDirection(0, 1);
                else if (key === 'ArrowLeft') setDirection(-1, 0);
                else if (key === 'ArrowRight') setDirection(1, 0);
                else if (key === ' ' || key === 'Space') {
                    e.preventDefault();
                    togglePause();
                }
            }

            // --- 触屏滑动支持 (移动端) ---
            let touchStartX = 0,
                touchStartY = 0;
            let isSwiping = false;

            function onTouchStart(e) {
                const touch = e.touches[0];
                if (!touch) return;
                const rect = canvas.getBoundingClientRect();
                touchStartX = touch.clientX - rect.left;
                touchStartY = touch.clientY - rect.top;
                isSwiping = true;
            }

            function onTouchEnd(e) {
                if (!isSwiping) return;
                isSwiping = false;
                const touch = e.changedTouches[0];
                if (!touch) return;
                const rect = canvas.getBoundingClientRect();
                const endX = touch.clientX - rect.left;
                const endY = touch.clientY - rect.top;
                const dx = endX - touchStartX;
                const dy = endY - touchStartY;
                if (Math.abs(dx) < 10 && Math.abs(dy) < 10) return; // 点击视为暂停
                if (Math.abs(dx) > Math.abs(dy)) {
                    setDirection(dx > 0 ? 1 : -1, 0);
                } else {
                    setDirection(0, dy > 0 ? 1 : -1);
                }
            }

            // --- 暂停切换 ---
            function togglePause() {
                if (gameOver || winFlag) return;
                paused = !paused;
                pauseBtn.textContent = paused ? '▶️ 运行' : '⏸️ 暂停';
                drawCanvas();
            }

            // --- 重置 ---
            function resetGame() {
                clearGameLoop();
                initGame();
                drawCanvas();
            }

            // --- 绑定事件 ---
            window.addEventListener('keydown', onKeyDown);
            canvas.addEventListener('touchstart', onTouchStart, { passive: true });
            canvas.addEventListener('touchend', onTouchEnd, { passive: true });
            canvas.addEventListener('touchcancel', () => { isSwiping = false; }, { passive: true });
            pauseBtn.addEventListener('click', togglePause);
            restartBtn.addEventListener('click', resetGame);

            // --- 启动游戏 ---
            initGame();
            drawCanvas();

            // 窗口失焦时防止键盘卡键
            window.addEventListener('blur', () => { /* ignore */ });
        })();
    </script>
</body>
</html>

技术点总结:

  1. 单文件架构 :所有样式与逻辑均集成于 snake.html,无需构建工具即可运行。
  2. Canvas 绘制 :利用 Canvas 2D Context 实现网格、蛇身渐变与粒子效果模拟。
  3. 响应式适配 :通过 viewport 与 CSS aspect-ratio 实现移动端自适应。
  4. 触摸与键盘双模控制:同时支持桌面键盘与移动端滑动手势。
  5. 本地持久化 :使用 localStorage 存储最高分,提升用户体验。

两种提示词策略的对比分析

为了更清晰地展示不同提示词策略对生成结果的影响,下表从多个维度进行了对比:

对比维度 普通自然语言描述 优雅风格化提示词
角色设定 缺失或模糊,AI 仅作为通用助手 明确专家角色,激活领域知识
视觉风格 默认值或随机,缺乏统一性 显式定义色彩搭配与交互细节
代码完整性 仅实现核心逻辑,边缘情况处理较少 包含边界检测、本地存储与粒子特效
响应式支持 通常忽略移动端适配 主动添加 viewport 与触摸事件
迭代效率 需要多次对话修正样式与逻辑 一次性生成高完成度原型

实践表明,采用风格化提示词虽然增加了前期的编写时间,但显著降低了后续的调试与返工成本,尤其适合追求产出质量的单人开发团队。

参考文档

总结

本文通过一个完整的贪吃蛇游戏开发案例,系统阐述了在 Vibe Coding 工作流中,如何从角色扮演、需求拆解、风格注入三个维度构造高质量的提示词。

相较于简单的自然语言指令,风格化提示词能够引导 AI 生成具备完整视觉体系、健壮交互逻辑与跨端适配能力的生产级代码。该方法论不仅适用于游戏开发,同样可扩展至数据可视化、后台管理面板等前端场景。

最终产出的 snake.html 文件展示了浏览器原生 API 在游戏开发中的全部潜力,为后续更复杂的 HTML5 应用开发提供了可复用的范式。

相关推荐
Zguigo42 分钟前
【DL】链式法则|反向传播|神经网络梯度
人工智能·深度学习·神经网络
Ai-_Man1 小时前
AI办公智能体工作平台能否电脑批量导出?我们拆解了“AI导出鸭”的底层逻辑
人工智能·ai·小程序·电脑
TMT星球1 小时前
知乎2026年Q2营收6.9亿元,环比增长5.9%
人工智能
老郑聊AI业财智造1 小时前
给大模型装上“金融之眼”:Kronos-Report的量化预测架构与技术全景剖析
人工智能·python·深度学习·语言模型·金融·架构·软件工程
chunmiao30321 小时前
GPT-5.6一个月两次降价,大模型API价格战来了
人工智能·gpt
广州智造1 小时前
HyperMesh 产品功能清单|有限元前处理软件|HyperMesh 中国代理
人工智能·教程·设计·cad·建模·cae
极客猴子1 小时前
录音内容需要对外翻译:录音转写自动翻译工具横评
人工智能·自然语言处理·机器翻译
beiju1 小时前
别把品牌手册塞进 Prompt:营销 Agent 的五层上下文架构
人工智能
柳叶方舟1 小时前
Nature Aging IF=19.4 | Transformer聚类框架:纵向电子健康记录解析阿尔茨海默病与帕金森病亚型
论文阅读·人工智能·深度学习·transformer·健康医疗·聚类