> For the complete documentation index, see [llms.txt](https://unsloth.ai/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://unsloth.ai/docs/zh/mo-xing/glm-5.2.md).

# GLM-5.2 - 如何在本地运行

GLM-5.2 是 Z.ai 的新开源模型，在长程编码、推理和智能体任务上实现了 SOTA 性能。拥有 **7440亿参数**，400亿激活参数，以及 **100万上下文** 窗口，现在可以使用 [Unsloth Dynamic](/docs/zh/ji-chu/unsloth-dynamic-2.0-ggufs.md) GGUF。GLM-5.2 是 **迄今最强的开源模型** ，在 Artificial Analysis 以及许多其他基准上，其表现可与 Claude 4.8 Opus、GPT-5.5 和 Gemini 3.1 Pro 相媲美。

[动态 1-bit 可达到 **\~76.2%**](/docs/zh/mo-xing/glm-5.2.md#quantization-analysis) 顶级准确率，同时仅为 **缩小 86%**。动态 2-bit 可达到 **\~82%** 准确率，同时仅为 **缩小 84%**。这意味着模型并不是因为小了 84% 就差了 82%——它实际上只比完整的 **1.5TB** 模型低了约 18% 的准确率。感谢 Z.ai 给予 Unsloth 首发日访问权限。 [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)

<a href="/pages/986f178fb44971bd081c36c351922d60469ba3ee#run-glm-5.2-tutorials" class="button primary">运行 GLM-5.2 教程</a><a href="/pages/986f178fb44971bd081c36c351922d60469ba3ee#quantization-analysis" class="button secondary">量化结果</a>

#### &#x20;:gear: 使用指南

{% columns %}
{% column %}
2-bit 动态量化 `UD-IQ2_M` 占用 **239GB** 磁盘空间——这足以直接放在一台 **256GB 统一内存的 Mac** 并且在以下环境中表现良好： **1x24GB GPU** 以及 **256GB RAM** ，配合 MoE 卸载。

该 **1-bit** 量化可容纳在 223GB RAM 中，而 8-bit 需要 810GB RAM。右侧可查看 1-bit GGUF 的实际运行：
{% endcolumn %}

{% column %}

<figure><img src="/files/987f97b87ca2ace6c3d5ac774a5af4bd1452417b" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

**表：推理硬件需求** （单位 = 总内存：RAM + VRAM，或统一内存）

<table><thead><tr><th>1-bit</th><th>2-bit</th><th width="128">3-bit</th><th>4-bit</th><th>5-bit</th><th>8-bit</th></tr></thead><tbody><tr><td>223 GB</td><td>245 GB</td><td>290-360 GB</td><td>372-475 GB</td><td>570 GB</td><td>810 GB</td></tr></tbody></table>

{% hint style="success" %}
为了获得最佳性能，请确保可用总内存（包括 VRAM 和系统 RAM）比量化后模型文件大小有足够余量。
{% endhint %}

### 推荐设置

GLM-5.2 有 **3 种思考模式**。两种模式下的非思考与思考： **高** + **最大**。复杂任务请使用最大思考。在 [Unsloth Studio](#run-glm-5.2-in-unsloth-studio) 中，你可以通过 UI 轻松切换 High + Max Thinking 和非思考模式。

大多数使用场景下请使用以下设置：

| 默认设置（大多数任务）         | SWE-Bench Pro       |
| ------------------- | ------------------- |
| `temperature` = 1.0 | `temperature` = 1.0 |
| `top_p` = 0.95      | `top_p` = 1.0       |

* **最大上下文窗口：** `1,048,576`.

#### 禁用思考，修改推理强度

GLM 5.2 默认使用推理。它还支持不同的推理强度，其中 `reasoning_effort` 可以为“high”、“max”或禁用。

要禁用思考，请使用 `--chat-template-kwargs '{"enable_thinking":false}'`。如果你在 **Windows** Powershell 中，请使用： `--chat-template-kwargs "{\"enable_thinking\":false}"`

你也可以使用 `--reasoning on` 或 `--reasoning off` ！

如需自定义推理强度或禁用推理，请使用以下示例：

{% code overflow="wrap" expandable="true" %}

```bash
--chat-template-kwargs '{"reasoning_effort":"max"}'
--chat-template-kwargs '{"reasoning_effort":"high"}'
--chat-template-kwargs '{"enable_thinking":false}'
```

{% endcode %}

### 📈 量化分析

我们还运行了 KLD（KL 散度）基准，以衡量我们对 GLM-5.2-GGUF 量化的准确性。动态 4-bit 的 UD-Q4\_K\_XL 和动态 5-bit 的 UD-Q5\_K\_XL 基本无损，而更小的量化版本也表现出色，因为它们会动态地将重要层保留在更高精度，而将不重要的层放到低比特。

在纯 top-1% 准确率上， **动态 1-bit 的准确率约为 76.2%，但仅为**缩小 86%！动态 2-bit 的准确率约为 82%，同时仅为缩小 84%。这表明，把某些层动态量化到更高精度，并不会让模型因为小了 86% 就差 86%——而只是比完整的 1.5TB 模型少了约 24% 的能力。

**但“76% 准确率”到底描述的是什么？**

76% 的 top-1% 并不意味着“法国的首都是”会变成 76% 选择巴黎、24% 选择悉尼。模型并不会因此“更笨”24%。在这里，巴黎始终是 100%，悉尼是 0%。这个 76% 的数字包含了整个语料库中的填充词和停用词，例如提问：

“创作一部小说” 会由于 LLM 采样而得到：

* 我现在将创作一部小说……
* 小说如下：
* 你希望它是什么类型？

每个示例都是正确的，但 \[I, The, What] 的分布会变化——基线可能会 100% 使用 \[I]，但现在 \[I] 是 76%，\[The] 是 24%。

这 **并不** 意味着你会在 24% 的时间里得到乱码或错误输出。

<figure><img src="/files/56fb444007638c9871603420d95794b326998ce0" alt=""><figcaption></figcaption></figure>

99.9% 的 KLD 通常也不错——不过从 4bit 起会有更大的提升，因此对于大规模分布外任务，动态 4-bit 可能是最佳选择。

Top-1% 是 KLD 本身的一个“强制”二项分布。KLD 是基线（BF16 或 Q8\_0）与量化版本之间概率的“距离”。量化的目标是最小化下面的目标函数：

$$
\text{minimize    } \frac{1}{n} \sum{\text{D}\_{\text{KL}}\[\text{ }f(q(W))\text{ }||\text{ } f(W))\text{ }]}
$$

其中 `f` 是语言模型的前向传播，而 `q` 是量化操作，而 `W` 是模型的参数或权重。目标是尽可能缩小基线 `f(W)` 与量化模型输出之间的“距离”。如果你能做到 0 KLD，那就说明你完美重建了模型！

<figure><img src="/files/6403fa414cdce1933b3a025cc1effad04fd9ecff" alt=""><figcaption></figcaption></figure>

我们使用 **平均 KLD** ，如下所示，因为在完整训练语料库上运行 KLD 成本很高（例如 15T tokens）——因此我们改用 **采样，** 并获得训练语料库中一个很小且有代表性的子集 **/** 下游任务，并对其进行优化。 平均 KLD 通常会随着磁盘空间呈单调趋势变化，并显示即使在 1-bit 下 GLM 5.2 也表现良好！

Top-1% 准确率本质上是一个贪婪解码算子，我们假设会选中 argmax 项；对于 1bit，76% 的情况下它会选择与基线相同的结果。

<figure><img src="/files/9f06ac28998d05d5b31cd2cf5e6fdf50916fd1b0" alt=""><figcaption></figcaption></figure>

## 运行 GLM-5.2 教程：

你现在可以在 [llama.cpp](#run-in-llama.cpp) 以及 [Unsloth Studio](https://unsloth.ai/docs/zh/mo-xing/pages/986f178fb44971bd081c36c351922d60469ba3ee#run-glm-5.2-in-unsloth-studio)中运行 GLM-5.2。 [`UD-IQ2_M`](https://huggingface.co/unsloth/GLM-5.2-GGUF/tree/main/UD-IQ2_M) 我们将使用 239GB 量化版本，以在可访问性和准确性方面获得最佳结果。

### 🦥 在 Unsloth Studio 中运行 GLM-5.2

GLM-5.2 可以运行在 [Unsloth Studio](/docs/zh/xin/studio.md)一个用于本地 AI 的开源 Web UI 中。 **Unsloth Studio 会自动卸载到 RAM 并检测多 GPU 设置**。借助 Unsloth Studio，你可以在以下系统上本地运行模型： **MacOS、Windows**、Linux，以及：

{% columns %}
{% column %}

* 搜索、下载、 [运行 GGUF](/docs/zh/xin/studio.md#run-models-locally) 和 safetensor 模型
* [**自我修复** 工具调用](/docs/zh/xin/studio.md#execute-code--heal-tool-calling) + **网页搜索**
* [**代码执行**](/docs/zh/xin/studio.md#run-models-locally) （Python、Bash）
* [自动推理](/docs/zh/xin/studio.md#model-arena) 参数调优（temp、top-p 等）
* 通过 llama.cpp 实现快速 CPU + GPU 推理
* [训练 LLM](/docs/zh/xin/studio.md#no-code-training) 速度提升 2 倍，VRAM 减少 70%
  {% endcolumn %}

{% column %}

<figure><img src="/files/5df03bf298fa3050e0d7c80885f1195f18b47756" alt=""><figcaption><p>2-bit GLM-5.2 GGUF 输出</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% stepper %}
{% step %}
**安装并启动 Unsloth**

要安装，请在终端中运行：

MacOS、Linux、WSL：

```bash
curl -fsSL https://unsloth.ai/install.sh | sh
```

Windows PowerShell：

```bash
irm https://unsloth.ai/install.ps1 | iex
```

**启动 Unsloth**

MacOS、Linux、WSL 和 Windows：

```bash
unsloth studio -H 0.0.0.0 -p 8888
```

然后打开 `http://127.0.0.1:8888` （或你指定的 URL）在浏览器中。

**通过 HTTPS 和 Cloudflare 安全启动 Unsloth**

**新！** Unsloth 现在提供一种通过免费的 Cloudflare 隧道，以 HTTPS 安全启动 Studio 的方式。使用以下命令（适用于 Windows、Mac 和 Linux）：

```bash
unsloth studio --secure
```

{% endstep %}

{% step %}
**搜索并下载 GLM-5.2**

Unsloth Studio 会自动卸载到 RAM 并检测多 GPU 设置。首次启动时，你需要创建一个密码来保护账户，并在之后重新登录。

然后转到 [Unsloth Chat](/docs/zh/xin/studio/chat.md) 选项卡，并在 **GLM-5.2** 搜索栏中搜索并下载你想要的模型和量化版本。确保你有足够的计算资源来运行该模型。

<div data-with-frame="true"><figure><img src="/files/b8b3f8a3783bbc5e3ef3502df75a1f0e5769d166" alt="" width="563"><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}
**运行 GLM-5.2**

使用 Unsloth Studio 时，推理参数应自动设置，不过你仍然可以手动更改。你也可以编辑上下文长度、聊天模板和其他设置。

更多信息请参阅我们的 [Unsloth Studio 推理指南](/docs/zh/xin/studio/chat.md).

<figure><img src="/files/34808194c8d909e964b377cfea60b5b02c6a0736" alt=""><figcaption><p>1-bit GLM-5.2 在 Canvas 中运行示例</p></figcaption></figure>
{% endstep %}
{% endstepper %}

### 🦙 在 llama.cpp 中运行 GLM-5.2

在本指南中，我们将运行 `UD-IQ2_M` 量化版本，它至少需要 245GB RAM。欢迎自行更改量化类型。对于这些教程，我们将使用 [llama.cpp](llama.cpphttps://github.com/ggml-org/llama.cpp) 以便快速本地推理。GGUF： [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)&#x20;

{% stepper %}
{% step %}
获取最新的 `llama.cpp` **在** [**GitHub 上**](https://github.com/ggml-org/llama.cpp)。你也可以按以下构建说明操作。将 `-DGGML_CUDA=ON` 改为 `-DGGML_CUDA=OFF` 如果你没有 GPU，或者只想进行 CPU 推理。 **对于 Apple Mac / Metal 设备**，设置 `-DGGML_CUDA=OFF` 然后按常规继续——Metal 支持默认开启。

```bash
apt-get update
apt-get install pciutils build-essential cmake curl libcurl4-openssl-dev -y
git clone https://github.com/ggml-org/llama.cpp
cmake llama.cpp -B llama.cpp/build \\
    -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON
cmake --build llama.cpp/build --config Release -j --clean-first --target llama-cli llama-mtmd-cli llama-server llama-gguf-split
cp llama.cpp/build/bin/llama-* llama.cpp
```

{% endstep %}

{% step %}
你现在可以直接使用 `llama.cpp` 来加载和下载模型，就像 `ollama run`。首先，选择你想要的量化类型，例如 `UD-IQ2_M`。也请使用 `export LLAMA_CACHE="unsloth/GLM-5.2-GGUF"` 强制 `llama.cpp` 保存到特定位置。 **请注意，这个下载过程可能会非常慢**，所以下一节的手动下载流程可能更好。

```bash
export LLAMA_CACHE="unsloth/GLM-5.2-GGUF"
./llama.cpp/llama-cli \\
    -hf unsloth/GLM-5.2-GGUF:UD-IQ2_M \\
    --temp 1.0 \\
    --top-p 0.95 \\
    --min-p 0.01
```

{% endstep %}

{% step %}
如果你想手动下载模型 **（快得多！）**，我们可以通过下面的代码下载模型（在安装 `pip install huggingface_hub`之后）。如果下载卡住，请查看： [Hugging Face Hub、XET 调试](/docs/zh/ji-chu/troubleshooting-and-faqs/hugging-face-hub-xet-debugging.md)

```bash
hf download unsloth/GLM-5.2-GGUF \\
    --local-dir unsloth/GLM-5.2-GGUF \\
    --include "*UD-IQ2_M*" # 对于接近完整精度，请使用 "*UD-Q8_K_XL*"
```

如果你想使用动态 1bit，请执行：

{% code overflow="wrap" expandable="true" %}

```bash
hf download unsloth/GLM-5.2-GGUF \\
    --local-dir unsloth/GLM-5.2-GGUF \\
    --include "*UD-IQ1_S*"
```

{% endcode %}
{% endstep %}

{% step %}
然后以对话模式运行模型。使用 `unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf` 用于 2bit，或 `unsloth/GLM-5.2-GGUF/UD-IQ1_S/GLM-5.2-UD-IQ1_S-00001-of-00006.gguf`  用于 1bit。

{% code overflow="wrap" %}

```bash
./llama.cpp/llama-cli \\
    --model unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf \\
    --temp 1.0 \\
    --top-p 0.95 \\
    --min-p 0.01
```

{% endcode %}
{% endstep %}

{% step %}
当你启动 llama-cli 时，你会看到：

<figure><img src="/files/48643b68022ac721e3a17f6a7a04fb5af945e739" alt=""><figcaption></figcaption></figure>

然后在提示它制作一个简短的 Flappy Bird 游戏后，我们得到：

<figure><img src="/files/724ed044585167917048ea5bac1927ff98f45711" alt=""><figcaption></figcaption></figure>

下面是完整的对话和游戏：

<details>

<summary>HTML 中的完整游戏</summary>

{% code overflow="wrap" expandable="true" %}

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>夕阳飞行者</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --sunset-1: #ff6b6b;
    --sunset-2: #feca50;
    --sunset-3: #ff9ff3;
    --dusk: #36306b;
    --night: #1a1746;
    --accent: #ffd93b;
    --coral: #ff6b6b;
    --pipe: #4a902b;
    --pipe-dark: #2d5a1a;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: var(--night);
    font-family: 'Fred', sans-serif;
    -webkit-user-select: none;
    user-select: none;
    touch-action: manipulation;
  }

  #game-wrap {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
  }

  #game-frame {
    position: relative;
    width: min(100vw, 480px);
    height: min(100vh, 720px);
    max-height: 100vh;
    box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
    overflow: hidden;
    background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
  }

  canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }

  .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    pointer-events: none;
    z-index: 10;
    transition: opacity 0.3s ease;
  }

  .overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  .overlay.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .panel {
    background: rgba(26, 23, 70, 0.85);
    border: 3px solid var(--accent);
    border-radius: 16px;
    padding: 28px 36px;
    text-align: center;
    color: #fff;
    box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
    backdrop-filter: blur(4px);
    transform: translateY(0);
    animation: bob 3s ease-in-out infinite;
  }

  @keyframes bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-6px); }
  }

  .title {
    font-family: 'Press Start 2P', monospace;
    font-size: 26px;
    color: var(--accent);
    text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
    letter-spacing: 1px;
    margin-bottom: 6px;
    line-height: 1.3;
  }

  .subtitle {
    font-size: 14px;
    color: #ffe8a8;
    margin-bottom: 20px;
    font-weight: 700;
  }

  .tap-icon {
    font-size: 42px;
    margin: 8px 0;
    animation: tap 1.2s ease-in-out infinite;
  }

  @keyframes tap {
    0%, 50% { transform: translateY(0) scale(1); }
    20% { transform: translateY(-8px) scale(1.1); }
  }

  .instructions {
    font-size: 13px;
    color: #fff;
    opacity: 0.85;
    margin-top: 10px;
    font-weight: 400;
  }

  .score-row {
    display: flex;
    gap: 24px;
    justify-content: center;
    margin: 12px 0 20px;
  }

  .score-box {
    background: rgba(0,0,0,0.4);
    border: 2px solid var(--accent);
    border-radius: 10px;
    padding: 10px 18px;
    min-width: 80px;
  }

  .score-box .label {
    font-family: 'Press Start 2p', monospace;
    font-size: 9px;
    color: var(--accent);
    margin-bottom: 4px;
    letter-spacing: 1px;
  }

  .score-box .value {
    font-family: 'Press Start 2p', monospace;
    font-size: 18px;
    color: #fff;
  }

  .score-box.best .value { color: var(--coral); }

  .btn {
    font-family: 'Press Start 2p', monospace;
    font-size: 12px;
    color: var(--night);
    background: var(--accent);
    border: none;
    padding: 12px 22px;
    border-radius: 8px;
    cursor: pointer;
    letter-spacing: 1px;
    box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
    transition: transform 0.1s, box-shadow 0.1s;
    pointer-events: auto;
  }

  .btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
  .btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }

  #hud {
    position: absolute;
    top: 24px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    font-family: 'Press Start 2P', monospace;
    font-size: 36px;
    color: #fff;
    text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
    pointer-events: none;
    transition: opacity 0.3s;
    opacity: 0;
  }

  #hud.visible { opacity: 1; }

  #hud .new-best {
    font-size: 11px;
    color: var(--coral);
    text-shadow: 2px 2px 0 #000;
    margin-top: 8px;
    opacity: 0;
    transition: opacity 0.3s;
  }

  #hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }

  @keyframes pulse {
    from { transform: translateX(-50%) scale(1); }
    to { transform: translateX(-50%) scale(1.15); }
  }

  .medal {
    font-family: 'Press Start 2P', monospace;
    font-size: 48px;
    margin: 10px 0;
    text-shadow: 3px 3px 0 #000;
  }

  .flash {
    position: absolute;
    inset: 0;
    background: #fff;
    opacity: 0;
    pointer-events: none;
    z-index: 8;
  }

  .footer {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    color: rgba(255,255,255,0.7);
    font-weight: 400;
    text-align: center;
    pointer-events: none;
    z-index: 9;
  }
</style>
</head>
<body>
<div id="game-wrap">
  <div id="game-frame">
    <canvas id="canvas"></canvas>
    <div id="hud" class="hud">
      <div id="hud-score">0</div>
      <div class="new-best">新纪录！</div>
    </div>
    <div id="flash" class="flash"></div>

    <!-- 开始界面 -->
    <div id="start-screen" class="overlay visible">
      <div class="panel">
        <div class="title">落日<br>飞行者</div>
        <div class="subtitle">— 暮色飞行 —</div>
        <div class="tap-icon">✊</div>
        <div class="instructions">点击 / 空格 / 单击 使其拍翅</div>
      </div>
    </div>

    <!-- 游戏结束界面 -->
    <div id="end-screen" class="overlay hidden">
      <div class="panel">
        <div class="title" style="font-size:22px;color:var(--coral);">游戏结束</div>
        <div id="medal" class="medal" style="display:none;">★</div>
        <div class="score-row">
          <div class="score-box">
            <div class="label">得分</div>
            <div class="value" id="end-score">0</div>
          </div>
          <div class="score-box best">
            <div class="label">最佳</div>
            <div class="value" id="end-best">0</div>
          </div>
        </div>
        <button class="btn" id="restart-btn">重试</button>
      </div>
    </div>

    <div class="footer">声音开启 • 点击起飞</div>
  </div>
</div>

<script>
(() => {
  // ============ 设置 ============
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const frame = document.getElementById('game-frame');
  const hud = document.getElementById('hud');
  const hudScore = document.getElementById('hud-score');
  const startScreen = document.getElementById('start-screen');
  const endScreen = document.getElementById('end-screen');
  const endScore = document.getElementById('end-score');
  const endBest = document.getElementById('end-best');
  const medalEl = document.getElementById('medal');
  const flashEl = document.getElementById('flash');
  const restartBtn = document.getElementById('restart-btn');

  let W = 480, H = 720;
  const dpr = window.devicePixelRatio || 1;

  function resize() {
    const rect = frame.getBoundingClientRect();
    W = rect.width;
    H = rect.height;
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);
  resize();

  // ============ 音频（Web Audio API） ============
  let audioCtx = null;
  function getAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch(e) { audioCtx = null; }
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
    return audioCtx;
  }

  function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
    const ac = getAudio();
    if (!ac) return;
    const osc = ac.createOscillator();
    const gain = ac.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
    gain.gain.value = vol;
    gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.start();
    osc.stop(ac.currentTime + dur);
  }

  const sfx = {
    flap: () => beep(420, 0.12, 'square', 0.08, 720),
    score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); },
    hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); },
    die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
  };

  // ============ 游戏状态 ============
  const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
  let state = STATE.READY;

  let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;

  best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
  endBest.textContent = best;

  // ============ 小鸟 ============
  function makeBird() {
    return {
      x: W * 0.32,
      y: H * 0.5,
      vy: 0,
      radius: 14,
      wingPhase: 0,
      rotation: 0,
      alive: true
    };
  }

  // ============ 管道 ============
  const PIPE_W = 68;
  const PIPE_GAP = 180;
  const PIPE_SPEED = 2.6;
  const PIPE_SPACING = 220;

  function makePipe(x) {
    const minY = 90;
    const maxY = H - 90 - PIPE_GAP;
    const gapY = minY + Math.random() * (maxY - minY);
    return { x, gapY, scored: false, passed: false };
  }

  function initPipes() {
    pipes = [];
    let x = W + 80;
    for (let i = 0; i < 4; i++) {
      pipes.push(makePipe(x));
      x += PIPE_SPACING;
    }
  }

  // ============ 粒子 ============
  function spawnParticles(x, y, color, count=10, vel=3) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = (Math.random() * 0.6 + 0.4) * vel;
      particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - 1,
        life: 1,
        size: Math.random() * 4 + 2,
        color
      });
    }
  }

  // ============ 背景 ============
  function initBg() {
    clouds = [];
    for (let i = 0; i < 4; i++) {
      clouds.push({
        x: Math.random() * W,
        y: 80 + Math.random() * 180,
        scale: 0.6 + Math.random() * 0.8,
        speed: 0.2 + Math.random() * 0.3
      });
    }
    stars = [];
    for (let i = 0; i < 30; i++) {
      stars.push({
        x: Math.random() * W,
        y: Math.random() * H * 0.4,
        r: Math.random() * 1.5 + 0.3,
        tw: Math.random() * Math.PI * 2
      });
    }
  }

  // ============ 重置 / 开始 ============
  function reset() {
    bird = makeBird();
    pipes = [];
    particles = [];
    score = 0;
    frameCount = 0;
    shake = 0;
    groundX = 0;
    initPipes();
    initBg();
    hudScore.textContent = '0';
    hud.classList.remove('has-new-best');
  }

  function startGame() {
    state = STATE.PLAYING;
    startScreen.classList.add('hidden');
    startScreen.classList.remove('visible');
    hud.classList.add('visible');
    bird.vy = -7.2;
    sfx.flap();
  }

  function gameOver() {
    state = STATE.DYING;
    sfx.hit();
    setTimeout(sfx.die, 120);
    shake = 18;
    spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
    spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
    flashScreen();
    bird.alive = false;

    const isNewBest = score > best;
    if (isNewBest) {
      best = score;
      localStorage.setItem('sunsetFlierBest', String(best));
    }

    setTimeout(() => {
      state = STATE.OVER;
      endScore.textContent = score;
      endBest.textContent = best;
      const medal = getMedal(score);
      if (medal) {
        medalEl.style.display = 'block';
        medalEl.textContent = medal.symbol;
        medalEl.style.color = medal.color;
      } else {
        medalEl.style.display = 'none';
      }
      endScreen.classList.remove('hidden');
      endScreen.classList.add('visible');
      if (isNewBest) {
        setTimeout(()=>hud.classList.add('has-new-best'), 0);
      }
    }, 700);
  }

  function flashScreen() {
    flashEl.style.transition = 'none';
    flashEl.style.opacity = '0.9';
    requestAnimationFrame(() => {
      flashEl.style.transition = 'opacity 0.4s ease-out';
      flashEl.style.opacity = '0';
    });
  }

  function getMedal(s) {
    if (s >= 30) return { symbol: '★', color: '#ffd93b' };
    if (s >= 20) return { symbol: '✦', color: '#c0c0c0' };
    if (s >= 10) return { symbol: '✧', color: '#cd7f32' };
    if (s >= 5)  return { symbol: '•', color: '#ff9ff3' };
    return null;
  }

  function restart() {
    reset();
    state = STATE.READY;
    endScreen.classList.add('hidden');
    endScreen.classList.remove('visible');
    startScreen.classList.remove('hidden');
    startScreen.classList.add('visible');
    hud.classList.remove('visible');
    hud.classList.remove('has-new-best');
  }

  // ============ 输入 ============
  function flap() {
    if (state === STATE.READY) { startGame(); return; }
    if (state === STATE.PLAYING) {
      bird.vy = -7.2;
      bird.wingPhase = 0;
      sfx.flap();
      spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
    }
    if (state === STATE.OVER) {
      // 在结束界面点击任意位置也会重新开始
      // 但主要由按钮处理
    }
  }

  function handleInput(e) {
    if (e.target.closest('button')) return;
    e.preventDefault();
    flap();
  }

  frame.addEventListener('mousedown', handleInput);
  frame.addEventListener('touchstart', handleInput, { passive: false });
  window.addEventListener('keydown', (e) => {
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
      e.preventDefault();
      if (state === STATE.OVER) restart();
      else flap();
    }
  });
  restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });

  // ============ 更新 ============
  const GRAVITY = 0.42;
  const MAX_FALL = 9;

  function update() {
    frameCount++;
    if (state === STATE.READY) {
      // 待机摆动
      bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
      bird.wingPhase += 0.2;
      bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
    }

    if (state === STATE.PLAYING) {
      bird.vy += GRAVITY;
      if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
      bird.y += bird.vy;
      bird.wingPhase += 0.4;
      bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));

      // 管道
      for (const p of pipes) {
        p.x -= PIPE_SPEED;
        if (!p.scored && p.x + PIPE_W < bird.x) {
          p.scored = true;
          score++;
          hudScore.textContent = score;
          sfx.score();
          spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
        }
      }
      // 移除屏幕外的管道，并添加新的
      while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
      const last = pipes[pipes.length - 1];
      if (last && last.x < W - PIPE_SPACING) {
        pipes.push(makePipe(last.x + PIPE_SPACING));
      }

      // 碰撞
      // 地面和天花板
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
        bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
        gameOver();
        return;
      }
      // 管道
      for (const p of pipes) {
        if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
        const top = p.gapY;
        const bottom = p.gapY + PIPE_GAP;
        if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
          // 碰撞
          gameOver();
          return;
        }
      }
    }

    if (state === STATE.DYING) {
      bird.vy += GRAVITY * 1.2;
      bird.y += bird.vy;
      bird.rotation += 0.06;
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY) {
        bird.y = groundY - bird.radius;
        bird.vy = 0;
      }
    }

    // 粒子
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.15;
      p.life -= 0.02;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // 抖动衰减
    if (shake > 0) shake = Math.max(0, shake - 1.2);

    // 背景动画
    for (const c of clouds) {
      c.x -= c.speed;
      if (c.x < -100) c.x = W + 100;
    }
    for (const s of stars) {
      s.tw += 0.05;
    }

    groundX = (groundX - PIPE_SPEED) % 120;
  }

  // ============ 渲染 ============
  function render() {
    ctx.save();
    // 抖动
    if (shake > 0) {
      const sx = (Math.random() - 0.5) * shake;
      const sy = (Math.random() - 0.5) * shake;
      ctx.translate(sx, sy);
    }

    // 天空渐变
    const sky = ctx.createLinearGradient(0, 0, 0, H);
    sky.addColorStop(0, '#1a1746');
    sky.addColorStop(0.3, '#4a3a8e');
    sky.addColorStop(0.55, '#ff6b6b');
    sky.addColorStop(0.8, '#feca50');
    sky.addColorStop(1, '#ffd93b');
    ctx.fillStyle = sky;
    ctx.fillRect(-20, -20, W + 40, H + 40);

    // 星星
    for (const s of stars) {
      const a = 0.4 + Math.sin(s.tw) * 0.4;
      ctx.fillStyle = `rgba(255,255,255,${a})`;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // 太阳
    const sunX = W * 0.75;
    const sunY = H * 0.65;
    const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
    sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
    sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
    sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
    ctx.fillStyle = sunGlow;
    ctx.fillRect(sunX - 200, sunY - 200, 400, 400);

    ctx.fillStyle = '#fff6d0';
    ctx.beginPath();
    ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
    ctx.fill();

    // 云朵
    for (const c of clouds) {
      drawCloud(c.x, c.y, c.scale);
    }

    // 山脉剪影
    ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
    ctx.beginPath();
    ctx.moveTo(0, H * 0.72);
    const peaks = [
      [W * 0.15, H * 0.58],
      [W * 0.25, H * 0.68],
      [W * 0.4, H * 0.52],
      [W * 0.55, H * 0.66],
      [W * 0.7, H * 0.6],
      [W * 0.85, H * 0.68],
      [W, H * 0.62]
    ];
    for (const [px, py] of peaks) ctx.lineTo(px, py);
    ctx.lineTo(W, H);
    ctx.lineTo(0, H);
    ctx.closePath();
    ctx.fill();

    // 管道
    for (const p of pipes) {
      drawPipe(p);
    }

    // 地面
    drawGround();

    // 小鸟
    drawBird();

    // 粒子
    for (const part of particles) {
      ctx.fillStyle = part.color;
      ctx.globalAlpha = part.life;
      ctx.beginPath();
      ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.restore();
  }

  function drawCloud(x, y, scale) {
    ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
    const s = scale;
    ctx.beginPath();
    ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
    ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawPipe(p) {
    const x = p.x;
    const topY = p.gapY;
    const bottomY = p.gapY + PIPE_GAP;
    const groundY = H - 80;

    // 渐变
    const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
    grad.addColorStop(0, '#2d5a1a');
    grad.addColorStop(0.15, '#4a902b');
    grad.addColorStop(0.5, '#7bc94a');
    grad.addColorStop(0.85, '#4a902b');
    grad.addColorStop(1, '#2d5a1a');

    // 上管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, 0, PIPE_W, topY - 18);
    // 顶部盖帽
    ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
    // 顶部盖帽高光
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, topY - 18, 14, 18);

    // 下管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    // 底部盖帽
    ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, bottomY, 14, 18);

    // 描边
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 2;
    ctx.strokeRect(x, 0, PIPE_W, topY - 18);
    ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
    ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
  }

  function drawGround() {
    const groundY = H - 80;
    // 沙地/黄昏地面
    const g = ctx.createLinearGradient(0, groundY, 0, H);
    g.addColorStop(0, '#8b5a2b');
    g.addColorStop(0.4, '#5e3a1c');
    g.addColorStop(1, '#2d1a0c');
    ctx.fillStyle = g;
    ctx.fillRect(0, groundY, W, 80);

    // 表面线
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, groundY);
    ctx.lineTo(W, groundY);
    ctx.stroke();

    // 图案条纹（移动）
    ctx.fillStyle = 'rgba(0,0,0,0.15)';
    for (let i = -2; i < W / 40 + 2; i++) {
      const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
      ctx.beginPath();
      ctx.moveTo(stripeX, groundY);
      ctx.lineTo(stripeX + 30, groundY + 80);
      ctx.lineTo(stripeX + 50, groundY + 80);
      ctx.lineTo(stripeX + 20, groundY);
      ctx.closePath();
      ctx.fill();
    }

    // 顶部小石子
    ctx.fillStyle = '#3d2412';
    for (let i = 0; i < 12; i++) {
      const px = (i * 60 + groundX + 60) % (W + 40);
      ctx.beginPath();
      ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawBird() {
    const b = bird;
    ctx.save();
    ctx.translate(b.x, b.y);
    ctx.rotate(b.rotation);

    // 阴影
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.beginPath();
    ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
    ctx.fill();

    // 身体
    const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
    bodyGrad.addColorStop(0, '#ff9ff3');
    bodyGrad.addColorStop(0.5, '#ff6b6b');
    bodyGrad.addColorStop(1, '#c0334a');
    ctx.fillStyle = bodyGrad;
    ctx.beginPath();
    ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
    ctx.fill();

    // 翅膀（扇动）
    const wingY = Math.sin(b.wingPhase) * 8;
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    // 眼睛
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(8, -4, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#1a1746';
    ctx.beginPath();
    ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(10, -5, 1, 0, Math.PI * 2);
    ctx.fill();

    // 喙
    ctx.fillStyle = '#ffaa33';
    ctx.beginPath();
    ctx.moveTo(13, -1);
    ctx.lineTo(20, 1);
    ctx.lineTo(13, 3);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1;
    ctx.stroke();

    // 尾巴
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.moveTo(-12, -2);
    ctx.lineTo(-18, -5);
    ctx.lineTo(-16, 0);
    ctx.lineTo(-18, 5);
    ctx.lineTo(-12, 3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.restore();
  }

  // ============ 循环 ============
  function loop() {
    update();
    render();
    requestAnimationFrame(loop);
  }

  reset();
  loop();
})();
</script>
</body>
</html>
```

{% endcode %}

</details>

<details>

<summary>完整对话</summary>

{% code overflow="wrap" expandable="true" %}

````
[开始思考]
用户想要一个简短的 Flappy Bird 游戏。我将创建一个完整、独立的 HTML 文件，包含：

1. 经典的 Flappy Bird 风格游戏
2. 精美的图形和动画
3. 分数追踪
4. 游戏结束画面
5. 重开功能
6. 良好的物理效果（重力、跳跃机制）
7. 随机位置移动的管道
8. 音效（使用 Web Audio API 生成音调）
9. 精美的视觉风格——我会避免经典的黄色小鸟，改用更有趣的配色方案

让我这样设计：
- 漂亮的渐变天空背景
- 带有扇动翅膀动画的小鸟
- 具有精美绿色/金属质感外观的管道
- 分数显示
- 开始界面、进行中状态、游戏结束状态
- 使用 localStorage 追踪最高分
- 得分时的粒子效果
- 流畅动画

我会使用日落/黄昏配色，让它更有辨识度——温暖的橙色和紫色，小鸟则采用温暖的珊瑚红色。

让我仔细把它写出来：

主要特性：
- 基于 Canvas 的渲染
- requestAnimationFrame 游戏循环
- 小鸟物理：重力、跳跃冲量
- 管道生成：随机间隙、向左移动
- 碰撞检测
- 通过管道时分数增加
- 得分时的粒子效果
- 死亡动画
- 使用 localStorage 保存最高分
- 通过 Web Audio API 提供音效
- 支持移动端/触摸操作
- 干净、精致的 UI 覆层

让我来写代码。
[结束思考]

我将创建一个精致的 Flappy Bird 游戏，具有独特的黄昏/日落美学、流畅的物理效果、粒子效果和音效。

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>夕阳飞行者</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --sunset-1: #ff6b6b;
    --sunset-2: #feca50;
    --sunset-3: #ff9ff3;
    --dusk: #36306b;
    --night: #1a1746;
    --accent: #ffd93b;
    --coral: #ff6b6b;
    --pipe: #4a902b;
    --pipe-dark: #2d5a1a;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: var(--night);
    font-family: 'Fred', sans-serif;
    -webkit-user-select: none;
    user-select: none;
    touch-action: manipulation;
  }

  #game-wrap {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
  }

  #game-frame {
    position: relative;
    width: min(100vw, 480px);
    height: min(100vh, 720px);
    max-height: 100vh;
    box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
    overflow: hidden;
    background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
  }

  canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }

  .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    pointer-events: none;
    z-index: 10;
    transition: opacity 0.3s ease;
  }

  .overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  .overlay.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .panel {
    background: rgba(26, 23, 70, 0.85);
    border: 3px solid var(--accent);
    border-radius: 16px;
    padding: 28px 36px;
    text-align: center;
    color: #fff;
    box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
    backdrop-filter: blur(4px);
    transform: translateY(0);
    animation: bob 3s ease-in-out infinite;
  }

  @keyframes bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-6px); }
  }

  .title {
    font-family: 'Press Start 2P', monospace;
    font-size: 26px;
    color: var(--accent);
    text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
    letter-spacing: 1px;
    margin-bottom: 6px;
    line-height: 1.3;
  }

  .subtitle {
    font-size: 14px;
    color: #ffe8a8;
    margin-bottom: 20px;
    font-weight: 700;
  }

  .tap-icon {
    font-size: 42px;
    margin: 8px 0;
    animation: tap 1.2s ease-in-out infinite;
  }

  @keyframes tap {
    0%, 50% { transform: translateY(0) scale(1); }
    20% { transform: translateY(-8px) scale(1.1); }
  }

  .instructions {
    font-size: 13px;
    color: #fff;
    opacity: 0.85;
    margin-top: 10px;
    font-weight: 400;
  }

  .score-row {
    display: flex;
    gap: 24px;
    justify-content: center;
    margin: 12px 0 20px;
  }

  .score-box {
    background: rgba(0,0,0,0.4);
    border: 2px solid var(--accent);
    border-radius: 10px;
    padding: 10px 18px;
    min-width: 80px;
  }

  .score-box .label {
    font-family: 'Press Start 2p', monospace;
    font-size: 9px;
    color: var(--accent);
    margin-bottom: 4px;
    letter-spacing: 1px;
  }

  .score-box .value {
    font-family: 'Press Start 2p', monospace;
    font-size: 18px;
    color: #fff;
  }

  .score-box.best .value { color: var(--coral); }

  .btn {
    font-family: 'Press Start 2p', monospace;
    font-size: 12px;
    color: var(--night);
    background: var(--accent);
    border: none;
    padding: 12px 22px;
    border-radius: 8px;
    cursor: pointer;
    letter-spacing: 1px;
    box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
    transition: transform 0.1s, box-shadow 0.1s;
    pointer-events: auto;
  }

  .btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
  .btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }

  #hud {
    position: absolute;
    top: 24px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    font-family: 'Press Start 2P', monospace;
    font-size: 36px;
    color: #fff;
    text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
    pointer-events: none;
    transition: opacity 0.3s;
    opacity: 0;
  }

  #hud.visible { opacity: 1; }

  #hud .new-best {
    font-size: 11px;
    color: var(--coral);
    text-shadow: 2px 2px 0 #000;
    margin-top: 8px;
    opacity: 0;
    transition: opacity 0.3s;
  }

  #hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }

  @keyframes pulse {
    from { transform: translateX(-50%) scale(1); }
    to { transform: translateX(-50%) scale(1.15); }
  }

  .medal {
    font-family: 'Press Start 2P', monospace;
    font-size: 48px;
    margin: 10px 0;
    text-shadow: 3px 3px 0 #000;
  }

  .flash {
    position: absolute;
    inset: 0;
    background: #fff;
    opacity: 0;
    pointer-events: none;
    z-index: 8;
  }

  .footer {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    color: rgba(255,255,255,0.7);
    font-weight: 400;
    text-align: center;
    pointer-events: none;
    z-index: 9;
  }
</style>
</head>
<body>
<div id="game-wrap">
  <div id="game-frame">
    <canvas id="canvas"></canvas>
    <div id="hud" class="hud">
      <div id="hud-score">0</div>
      <div class="new-best">新纪录！</div>
    </div>
    <div id="flash" class="flash"></div>

    <!-- 开始界面 -->
    <div id="start-screen" class="overlay visible">
      <div class="panel">
        <div class="title">落日<br>飞行者</div>
        <div class="subtitle">— 暮色飞行 —</div>
        <div class="tap-icon">✊</div>
        <div class="instructions">点击 / 空格 / 单击 使其拍翅</div>
      </div>
    </div>

    <!-- 游戏结束界面 -->
    <div id="end-screen" class="overlay hidden">
      <div class="panel">
        <div class="title" style="font-size:22px;color:var(--coral);">游戏结束</div>
        <div id="medal" class="medal" style="display:none;">★</div>
        <div class="score-row">
          <div class="score-box">
            <div class="label">得分</div>
            <div class="value" id="end-score">0</div>
          </div>
          <div class="score-box best">
            <div class="label">最佳</div>
            <div class="value" id="end-best">0</div>
          </div>
        </div>
        <button class="btn" id="restart-btn">重试</button>
      </div>
    </div>

    <div class="footer">声音开启 • 点击起飞</div>
  </div>
</div>

<script>
(() => {
  // ============ 设置 ============
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const frame = document.getElementById('game-frame');
  const hud = document.getElementById('hud');
  const hudScore = document.getElementById('hud-score');
  const startScreen = document.getElementById('start-screen');
  const endScreen = document.getElementById('end-screen');
  const endScore = document.getElementById('end-score');
  const endBest = document.getElementById('end-best');
  const medalEl = document.getElementById('medal');
  const flashEl = document.getElementById('flash');
  const restartBtn = document.getElementById('restart-btn');

  let W = 480, H = 720;
  const dpr = window.devicePixelRatio || 1;

  function resize() {
    const rect = frame.getBoundingClientRect();
    W = rect.width;
    H = rect.height;
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);
  resize();

  // ============ 音频（Web Audio API） ============
  let audioCtx = null;
  function getAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch(e) { audioCtx = null; }
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
    return audioCtx;
  }

  function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
    const ac = getAudio();
    if (!ac) return;
    const osc = ac.createOscillator();
    const gain = ac.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
    gain.gain.value = vol;
    gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.start();
    osc.stop(ac.currentTime + dur);
  }

  const sfx = {
    flap: () => beep(420, 0.12, 'square', 0.08, 720),
    score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); },
    hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); },
    die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
  };

  // ============ 游戏状态 ============
  const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
  let state = STATE.READY;

  let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;

  best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
  endBest.textContent = best;

  // ============ 小鸟 ============
  function makeBird() {
    return {
      x: W * 0.32,
      y: H * 0.5,
      vy: 0,
      radius: 14,
      wingPhase: 0,
      rotation: 0,
      alive: true
    };
  }

  // ============ 管道 ============
  const PIPE_W = 68;
  const PIPE_GAP = 180;
  const PIPE_SPEED = 2.6;
  const PIPE_SPACING = 220;

  function makePipe(x) {
    const minY = 90;
    const maxY = H - 90 - PIPE_GAP;
    const gapY = minY + Math.random() * (maxY - minY);
    return { x, gapY, scored: false, passed: false };
  }

  function initPipes() {
    pipes = [];
    let x = W + 80;
    for (let i = 0; i < 4; i++) {
      pipes.push(makePipe(x));
      x += PIPE_SPACING;
    }
  }

  // ============ 粒子 ============
  function spawnParticles(x, y, color, count=10, vel=3) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = (Math.random() * 0.6 + 0.4) * vel;
      particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - 1,
        life: 1,
        size: Math.random() * 4 + 2,
        color
      });
    }
  }

  // ============ 背景 ============
  function initBg() {
    clouds = [];
    for (let i = 0; i < 4; i++) {
      clouds.push({
        x: Math.random() * W,
        y: 80 + Math.random() * 180,
        scale: 0.6 + Math.random() * 0.8,
        speed: 0.2 + Math.random() * 0.3
      });
    }
    stars = [];
    for (let i = 0; i < 30; i++) {
      stars.push({
        x: Math.random() * W,
        y: Math.random() * H * 0.4,
        r: Math.random() * 1.5 + 0.3,
        tw: Math.random() * Math.PI * 2
      });
    }
  }

  // ============ 重置 / 开始 ============
  function reset() {
    bird = makeBird();
    pipes = [];
    particles = [];
    score = 0;
    frameCount = 0;
    shake = 0;
    groundX = 0;
    initPipes();
    initBg();
    hudScore.textContent = '0';
    hud.classList.remove('has-new-best');
  }

  function startGame() {
    state = STATE.PLAYING;
    startScreen.classList.add('hidden');
    startScreen.classList.remove('visible');
    hud.classList.add('visible');
    bird.vy = -7.2;
    sfx.flap();
  }

  function gameOver() {
    state = STATE.DYING;
    sfx.hit();
    setTimeout(sfx.die, 120);
    shake = 18;
    spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
    spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
    flashScreen();
    bird.alive = false;

    const isNewBest = score > best;
    if (isNewBest) {
      best = score;
      localStorage.setItem('sunsetFlierBest', String(best));
    }

    setTimeout(() => {
      state = STATE.OVER;
      endScore.textContent = score;
      endBest.textContent = best;
      const medal = getMedal(score);
      if (medal) {
        medalEl.style.display = 'block';
        medalEl.textContent = medal.symbol;
        medalEl.style.color = medal.color;
      } else {
        medalEl.style.display = 'none';
      }
      endScreen.classList.remove('hidden');
      endScreen.classList.add('visible');
      if (isNewBest) {
        setTimeout(()=>hud.classList.add('has-new-best'), 0);
      }
    }, 700);
  }

  function flashScreen() {
    flashEl.style.transition = 'none';
    flashEl.style.opacity = '0.9';
    requestAnimationFrame(() => {
      flashEl.style.transition = 'opacity 0.4s ease-out';
      flashEl.style.opacity = '0';
    });
  }

  function getMedal(s) {
    if (s >= 30) return { symbol: '★', color: '#ffd93b' };
    if (s >= 20) return { symbol: '✦', color: '#c0c0c0' };
    if (s >= 10) return { symbol: '✧', color: '#cd7f32' };
    if (s >= 5)  return { symbol: '•', color: '#ff9ff3' };
    return null;
  }

  function restart() {
    reset();
    state = STATE.READY;
    endScreen.classList.add('hidden');
    endScreen.classList.remove('visible');
    startScreen.classList.remove('hidden');
    startScreen.classList.add('visible');
    hud.classList.remove('visible');
    hud.classList.remove('has-new-best');
  }

  // ============ 输入 ============
  function flap() {
    if (state === STATE.READY) { startGame(); return; }
    if (state === STATE.PLAYING) {
      bird.vy = -7.2;
      bird.wingPhase = 0;
      sfx.flap();
      spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
    }
    if (state === STATE.OVER) {
      // 在结束界面点击任意位置也会重新开始
      // 但主要由按钮处理
    }
  }

  function handleInput(e) {
    if (e.target.closest('button')) return;
    e.preventDefault();
    flap();
  }

  frame.addEventListener('mousedown', handleInput);
  frame.addEventListener('touchstart', handleInput, { passive: false });
  window.addEventListener('keydown', (e) => {
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
      e.preventDefault();
      if (state === STATE.OVER) restart();
      else flap();
    }
  });
  restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });

  // ============ 更新 ============
  const GRAVITY = 0.42;
  const MAX_FALL = 9;

  function update() {
    frameCount++;
    if (state === STATE.READY) {
      // 待机摆动
      bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
      bird.wingPhase += 0.2;
      bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
    }

    if (state === STATE.PLAYING) {
      bird.vy += GRAVITY;
      if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
      bird.y += bird.vy;
      bird.wingPhase += 0.4;
      bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));

      // 管道
      for (const p of pipes) {
        p.x -= PIPE_SPEED;
        if (!p.scored && p.x + PIPE_W < bird.x) {
          p.scored = true;
          score++;
          hudScore.textContent = score;
          sfx.score();
          spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
        }
      }
      // 移除屏幕外的管道，并添加新的
      while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
      const last = pipes[pipes.length - 1];
      if (last && last.x < W - PIPE_SPACING) {
        pipes.push(makePipe(last.x + PIPE_SPACING));
      }

      // 碰撞
      // 地面和天花板
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
        bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
        gameOver();
        return;
      }
      // 管道
      for (const p of pipes) {
        if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
        const top = p.gapY;
        const bottom = p.gapY + PIPE_GAP;
        if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
          // 碰撞
          gameOver();
          return;
        }
      }
    }

    if (state === STATE.DYING) {
      bird.vy += GRAVITY * 1.2;
      bird.y += bird.vy;
      bird.rotation += 0.06;
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY) {
        bird.y = groundY - bird.radius;
        bird.vy = 0;
      }
    }

    // 粒子
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.15;
      p.life -= 0.02;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // 抖动衰减
    if (shake > 0) shake = Math.max(0, shake - 1.2);

    // 背景动画
    for (const c of clouds) {
      c.x -= c.speed;
      if (c.x < -100) c.x = W + 100;
    }
    for (const s of stars) {
      s.tw += 0.05;
    }

    groundX = (groundX - PIPE_SPEED) % 120;
  }

  // ============ 渲染 ============
  function render() {
    ctx.save();
    // 抖动
    if (shake > 0) {
      const sx = (Math.random() - 0.5) * shake;
      const sy = (Math.random() - 0.5) * shake;
      ctx.translate(sx, sy);
    }

    // 天空渐变
    const sky = ctx.createLinearGradient(0, 0, 0, H);
    sky.addColorStop(0, '#1a1746');
    sky.addColorStop(0.3, '#4a3a8e');
    sky.addColorStop(0.55, '#ff6b6b');
    sky.addColorStop(0.8, '#feca50');
    sky.addColorStop(1, '#ffd93b');
    ctx.fillStyle = sky;
    ctx.fillRect(-20, -20, W + 40, H + 40);

    // 星星
    for (const s of stars) {
      const a = 0.4 + Math.sin(s.tw) * 0.4;
      ctx.fillStyle = `rgba(255,255,255,${a})`;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // 太阳
    const sunX = W * 0.75;
    const sunY = H * 0.65;
    const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
    sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
    sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
    sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
    ctx.fillStyle = sunGlow;
    ctx.fillRect(sunX - 200, sunY - 200, 400, 400);

    ctx.fillStyle = '#fff6d0';
    ctx.beginPath();
    ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
    ctx.fill();

    // 云朵
    for (const c of clouds) {
      drawCloud(c.x, c.y, c.scale);
    }

    // 山脉剪影
    ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
    ctx.beginPath();
    ctx.moveTo(0, H * 0.72);
    const peaks = [
      [W * 0.15, H * 0.58],
      [W * 0.25, H * 0.68],
      [W * 0.4, H * 0.52],
      [W * 0.55, H * 0.66],
      [W * 0.7, H * 0.6],
      [W * 0.85, H * 0.68],
      [W, H * 0.62]
    ];
    for (const [px, py] of peaks) ctx.lineTo(px, py);
    ctx.lineTo(W, H);
    ctx.lineTo(0, H);
    ctx.closePath();
    ctx.fill();

    // 管道
    for (const p of pipes) {
      drawPipe(p);
    }

    // 地面
    drawGround();

    // 小鸟
    drawBird();

    // 粒子
    for (const part of particles) {
      ctx.fillStyle = part.color;
      ctx.globalAlpha = part.life;
      ctx.beginPath();
      ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.restore();
  }

  function drawCloud(x, y, scale) {
    ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
    const s = scale;
    ctx.beginPath();
    ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
    ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawPipe(p) {
    const x = p.x;
    const topY = p.gapY;
    const bottomY = p.gapY + PIPE_GAP;
    const groundY = H - 80;

    // 渐变
    const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
    grad.addColorStop(0, '#2d5a1a');
    grad.addColorStop(0.15, '#4a902b');
    grad.addColorStop(0.5, '#7bc94a');
    grad.addColorStop(0.85, '#4a902b');
    grad.addColorStop(1, '#2d5a1a');

    // 上管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, 0, PIPE_W, topY - 18);
    // 顶部盖帽
    ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
    // 顶部盖帽高光
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, topY - 18, 14, 18);

    // 下管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    // 底部盖帽
    ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, bottomY, 14, 18);

    // 描边
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 2;
    ctx.strokeRect(x, 0, PIPE_W, topY - 18);
    ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
    ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
  }

  function drawGround() {
    const groundY = H - 80;
    // 沙地/黄昏地面
    const g = ctx.createLinearGradient(0, groundY, 0, H);
    g.addColorStop(0, '#8b5a2b');
    g.addColorStop(0.4, '#5e3a1c');
    g.addColorStop(1, '#2d1a0c');
    ctx.fillStyle = g;
    ctx.fillRect(0, groundY, W, 80);

    // 表面线
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, groundY);
    ctx.lineTo(W, groundY);
    ctx.stroke();

    // 图案条纹（移动）
    ctx.fillStyle = 'rgba(0,0,0,0.15)';
    for (let i = -2; i < W / 40 + 2; i++) {
      const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
      ctx.beginPath();
      ctx.moveTo(stripeX, groundY);
      ctx.lineTo(stripeX + 30, groundY + 80);
      ctx.lineTo(stripeX + 50, groundY + 80);
      ctx.lineTo(stripeX + 20, groundY);
      ctx.closePath();
      ctx.fill();
    }

    // 顶部小石子
    ctx.fillStyle = '#3d2412';
    for (let i = 0; i < 12; i++) {
      const px = (i * 60 + groundX + 60) % (W + 40);
      ctx.beginPath();
      ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawBird() {
    const b = bird;
    ctx.save();
    ctx.translate(b.x, b.y);
    ctx.rotate(b.rotation);

    // 阴影
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.beginPath();
    ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
    ctx.fill();

    // 身体
    const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
    bodyGrad.addColorStop(0, '#ff9ff3');
    bodyGrad.addColorStop(0.5, '#ff6b6b');
    bodyGrad.addColorStop(1, '#c0334a');
    ctx.fillStyle = bodyGrad;
    ctx.beginPath();
    ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
    ctx.fill();

    // 翅膀（扇动）
    const wingY = Math.sin(b.wingPhase) * 8;
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    // 眼睛
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(8, -4, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#1a1746';
    ctx.beginPath();
    ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(10, -5, 1, 0, Math.PI * 2);
    ctx.fill();

    // 喙
    ctx.fillStyle = '#ffaa33';
    ctx.beginPath();
    ctx.moveTo(13, -1);
    ctx.lineTo(20, 1);
    ctx.lineTo(13, 3);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1;
    ctx.stroke();

    // 尾巴
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.moveTo(-12, -2);
    ctx.lineTo(-18, -5);
    ctx.lineTo(-16, 0);
    ctx.lineTo(-18, 5);
    ctx.lineTo(-12, 3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.restore();
  }

  // ============ 循环 ============
  function loop() {
    update();
    render();
    requestAnimationFrame(loop);
  }

  reset();
  loop();
})();
</script>
</body>
</html>
```

我做出来的是：

**主题**："Sunset Flier"——我没有采用经典的绿色/蓝绿色 Flappy Bird 风格，而是使用了黄昏日落配色（深紫 → 珊瑚色 → 金色），并加入了山脉剪影、发光的太阳、飘动的云朵，以及上方天空中闪烁的星星。

**小鸟**：一只珊瑚粉色的小鸟，带有黄色扇动翅膀、可动画化的喙、尾巴和会追踪的眼睛。当小鸟上升/下降时，翅膀会以物理方式动画化。

**机制**：
- 经典的重力 + 扇动冲量物理效果，经过调校，手感更灵敏
- 随机间隙位置的管道、平滑滚动、带高光的顶部盖帽渲染，呈现厚实的 2D 风格
- 小鸟穿过管道时分数增加，并伴随粒子爆裂和提示音效
- 与管道、天花板和地面的碰撞会触发游戏结束，并伴随屏幕闪烁、屏幕抖动和死亡粒子

**打磨**：
- 使用 Press Start 2P 像素字体的 HUD，包含分数和记录刷新时的“新纪录！”脉冲效果
- 游戏结束面板带有基于分数阈值的奖牌系统（★ / ✦ / ✧ / •）
- 最高分通过 `localStorage` 持久化保存
- 带有上下浮动面板的动画开始界面
- 通过 Web Audio API 生成的程序化音效（拍翅、得分、碰撞、死亡）——不依赖外部资源
- 移动的地面条纹/小石子，加强速度感
- 响应式：适配视口，支持鼠标、触摸和键盘（Space/↑/Enter）
- 重试按钮 + 点击任意位置重新开始的便捷操作

只需在浏览器中打开该文件——轻点/点击/按空格即可飞行。
````

{% endcode %}

</details>

而且游戏还有声音，运行得非常棒！提醒一下，这可是 1 位量化，而且效果很好！

<figure><img src="/files/66d373652b7bc6795ab4fee922b3bcc94282da3e" alt="" width="375"><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

### :triangular\_ruler:通过 KV 缓存量化实现长上下文

要在 llama.cpp 中利用长上下文，我们需要使用 KV 缓存量化来减少内存占用。最近 llama.cpp 为 KV 缓存量化加入了更高精度的技巧—— [见](https://github.com/ggml-org/llama.cpp/pull/21038) 以及其他 PR！

目前支持以下 KV 缓存数据类型：

{% code overflow="wrap" expandable="true" %}

```
f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1
```

{% endcode %}

默认情况下 `f16` 会被使用。如果你使用 `q4_0` 它大约每个权重 4.5 位，你可以将上下文长度扩展约 16 / 4.5 = **3.5 倍**！所以如果你的模型原本支持 10K，那么 35K 也可能触手可及！ `q4_1` 可能更好，因为你还能获得一个偏移参数，而且每个权重 5 位——因此上下文可延长 3.2 倍。

像下面这样使用：

{% code overflow="wrap" expandable="true" %}

```bash
./llama.cpp/llama-cli \\
    --model unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf \\
    --temp 1.0 \\
    --top-p 0.95 \\
    --min-p 0.01 \\
    --cache-type-k q4_1 \\
    --cache-type-v q4_1
```

{% endcode %}

## 📊 基准测试

你可以在下方以表格形式查看 GLM-5.2 基准测试：

<div><figure><img src="/files/50c2128619f2e95ad4e319131bf476b1bca38331" alt=""><figcaption></figcaption></figure> <figure><img src="/files/622f79622eea2672e4dcb6b62dee842bf3cc1ca5" alt=""><figcaption></figcaption></figure></div>

| 基准                               | GLM-5.2 | Claude Opus 4.8 | GPT-5.5 | Gemini 3.1 Pro | GLM-5.1 | Qwen3.7-Max | MiniMax M3 | DeepSeek-V4-Pro |
| -------------------------------- | :-----: | :-------------: | :-----: | :------------: | :-----: | :---------: | :--------: | :-------------: |
| **推理**                           |         |                 |         |                |         |             |            |                 |
| HLE                              |   40.5  |      49.8\*     |  41.4\* |       45       |    31   |     41.4    |     37     |       37.7      |
| HLE（含工具）                         |   54.7  |      57.9\*     |  52.2\* |     51.4\*     |   52.3  |     53.5    |      -     |       48.2      |
| CritPt                           |   20.9  |       20.9      |   27.1  |      17.7      |   4.6   |     13.4    |     3.7    |       12.9      |
| AIME 2026                        |   99.2  |       95.7      |   98.3  |      98.2      |   95.3  |      97     |      -     |       94.6      |
| HMMT 2025 年 11 月                 |   94.4  |       96.5      |   96.5  |      94.8      |    94   |      95     |    84.4    |       94.4      |
| HMMT 2026 年 2 月                  |   92.5  |       96.7      |   96.7  |      87.3      |   82.6  |     97.1    |    84.4    |       95.2      |
| IMOAnswerBench                   |   91.0  |       83.5      |    -    |       81       |   83.8  |      90     |      -     |       89.8      |
| GPQA-Diamond                     |   91.2  |       93.6      |   93.6  |      94.3      |   86.2  |      90     |     93     |       90.1      |
| **编程**                           |         |                 |         |                |         |             |            |                 |
| SWE-bench Pro                    |   62.1  |       69.2      |   58.6  |      54.2      |   58.4  |     60.6    |     59     |       55.4      |
| NL2Repo                          |   48.9  |       69.7      |   50.7  |      33.4      |   42.7  |     47.2    |    42.1    |       35.5      |
| DeepSWE                          |   46.2  |        58       |    70   |       10       |    18   |      18     |     20     |        8        |
| ProgramBench                     |   63.7  |       71.9      |   70.8  |      39.5      |   50.9  |      -      |      -     |       47.8      |
| Terminal Bench 2.1 (Terminus-2)  |   81.0  |        85       |    84   |       74       |   63.5  |      75     |     65     |        64       |
| Terminal Bench 2.1（最佳报告 Harness） |   82.7  |       78.9      |   83.4  |      70.7      |    69   |      -      |      -     |        -        |
| FrontierSWE（主导）                  |   74.4  |       75.1      |   72.6  |      39.6      |   30.5  |      -      |      -     |       29.0      |
| PostTrainBench                   |   34.3  |       37.2      |   28.4  |      21.6      |   20.1  |      -      |      -     |        -        |
| SWE-Marathon                     |   13.0  |       26.0      |   12.0  |       4.0      |   1.0   |      -      |      -     |        -        |
| **Agentic**                      |         |                 |         |                |         |             |            |                 |
| MCP-Atlas（公开集）                   |   76.8  |       77.8      |   75.3  |      69.2      |   71.8  |     76.4    |    74.2    |       73.6      |
| Tool-Decathlon                   |   48.2  |       59.9      |   55.6  |      48.8      |   40.7  |      -      |      -     |       52.8      |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://unsloth.ai/docs/zh/mo-xing/glm-5.2.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
