> 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/jp/moderu/glm-5.2.md).

# GLM-5.2 - ローカルでの実行方法

GLM-5.2はZ.aiの新しいオープンモデルで、長期的なコーディング、推論、エージェント系タスク全般でSOTAの性能を発揮します。さらに **744Bパラメータ**、40Bのアクティブパラメータ、そして **100万コンテキスト** ウィンドウを備え、現在は [Unsloth Dynamic](/docs/jp/ji-ben/unsloth-dynamic-2.0-ggufs.md) GGUFsを使ってローカル実行できます。GLM-5.2は **最強のオープンモデル** です。Artificial Analysisやその他多くのベンチマーク全体で、Claude 4.8 Opus、GPT-5.5、Gemini 3.1 Proと同等の性能を示しています。

[Dynamic 1-bitは **\~76.2%**](/docs/jp/moderu/glm-5.2.md#quantization-analysis) トップ1精度を達成しながら **86%小型化**します。Dynamic 2-bitは **\~82%** の精度を達成しながら **84%小型化**します。つまり、84%小さくなっているからといってモデルの性能が82%悪いわけではなく、実際にはフル版の **1.5TB** モデルより精度が約18%低いだけです。Unslothに初日アクセスを提供してくれたZ.aiに感謝します。 [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)

<a href="/pages/1d43caebcc2925464cc42990bf5ebdc3d13c3c85#run-glm-5.2-tutorials" class="button primary">GLM-5.2チュートリアルを実行</a><a href="/pages/1d43caebcc2925464cc42990bf5ebdc3d13c3c85#quantization-analysis" class="button secondary">量子化結果</a>

#### &#x20;:gear: 使用ガイド

{% columns %}
{% column %}
2ビットの動的量子化 `UD-IQ2_M` は **239GB** のディスク容量を使用します。これは **256GBユニファイドメモリ搭載Mac** にそのまま収まり、 **1x24GB GPU** および **256GBのRAM** でもMoEオフロードを使えば問題なく動作します。

この **1ビット** 量子化は223GBのRAMに収まり、8ビットには810GBのRAMが必要です。右側で1ビットGGUFの動作をご覧ください:
{% endcolumn %}

{% column %}

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

**表: 推論ハードウェア要件** （単位 = 総メモリ: RAM + VRAM、またはユニファイドメモリ）

<table><thead><tr><th>1ビット</th><th>2ビット</th><th width="128">3ビット</th><th>4ビット</th><th>5ビット</th><th>8ビット</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つの思考モードがあります**。非思考と思考の2つのモード: **高** + **最大**。複雑なタスクにはMax Thinkingを使用してください。 [Unsloth Studio](#run-glm-5.2-in-unsloth-studio) では、UIでHigh + Max Thinkingと非Thinkingを簡単に切り替えられます。

ほとんどのユースケースでは、以下の設定を使用してください:

| デフォルト設定（ほとんどのタスク）   | SWE-Bench Pro       |
| ------------------- | ------------------- |
| `temperature` = 1.0 | `temperature` = 1.0 |
| `top_p` = 0.95      | `top_p` = 1.0       |

* **最大コンテキストウィンドウ:** `1,048,576`.

#### Thinkingの無効化、推論努力の変更

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` をllama.cppでも使えるようになりました!

推論努力のカスタマイズや推論の無効化には、以下の例を使用してください:

{% 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 %}

### 📈 量子化分析

また、GLM-5.2-GGUFの量子化の精度を評価するために、KLD（KLダイバージェンス）のベンチマークも実行しました。動的4ビットのUD-Q4\_K\_XLと動的5ビットのUD-Q5\_K\_XLはほぼ無損失で、より小さい量子化でも、重要な層を動的により高精度に残し、重要でない層を低ビットにすることで、非常にうまく動作します。

純粋なトップ1%精度では、 **動的1ビットは約76.2%の精度でありながら、サイズは86%小さくなります**! 動的2ビットは約82%の精度でありながら、サイズは84%小さくなります。これは、一部の層をより高い精度に動的量子化しても、モデルの性能が86%悪化するわけではなく、しかもサイズは86%小さくなることを示しています。つまり、フルの1.5TBモデルに比べて能力が24%低いだけです。

**では、「76%の精度」とは実際には何を表しているのでしょうか?**

76%のトップ1%は、「フランスの首都は」が76%パリ、24%シドニーを選ぶようになるという意味ではありません。モデルが24%「賢くない」わけでもありません。これに関しては、パリは常に100%、シドニーは0%です。76%という数値には、たとえば以下のようなコーパス全体にわたる埋め草語やストップワードが含まれます:

「小説を作って」と言うと、LLMのサンプリングにより:

* これから小説を作成します...
* 小説は以下です:
* どんなジャンルにしますか?

各例は正しいですが、\[I, The, What] の分布が変わるのです。ベースラインでは \[I] を100%の確率で使うかもしれませんが、今は \[I] が76%、\[The] が24%です。

それは **決して** 、支離滅裂な出力や誤った出力が24%の確率で出るという意味ではありません。

<figure><img src="/files/5b4b7d95fee669331f01f493bf4b4314f9f60dbe" alt=""><figcaption></figcaption></figure>

99.9%のKLDも一般的には良好です。とはいえ、4bit以降で大きな向上があるため、分布外の巨大なタスクには動的4ビットがおそらく最適です。

トップ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)` のロジット出力と量子化モデルの出力との「距離」をできるだけ小さくすることです。KLDを0にできれば、モデルを完全に再構成できたことになります！

<figure><img src="/files/268764476d2838487e310bdae85fca6cc89e094b" alt=""><figcaption></figcaption></figure>

私たちは **平均KLD** を以下のように使います。トレーニングコーパス全体（たとえば15Tトークン）でKLDを実行するのは高コストだからです。その代わりに **サンプリング、** トレーニングコーパスの小さな代表的サブセットを取得し **/** 下流タスクに対してそれを最適化します。 平均KLDは一般にディスク容量に対して単調な傾向を示し、1ビットでもGLM 5.2がうまく動作することがわかります！

Top-1%精度は、argmaxの項目が選ばれると仮定する単なる貪欲デコード演算子であり、1bitでは76%でベースラインのargmaxと同じものが選ばれます。

<figure><img src="/files/2baa1a96e90ec939b318ef37024fbbc389a15f17" alt=""><figcaption></figcaption></figure>

## GLM-5.2のチュートリアルを実行：

これでGLM-5.2を [llama.cpp](#run-in-llama.cpp) および [Unsloth Studio](https://unsloth.ai/docs/jp/moderu/pages/1d43caebcc2925464cc42990bf5ebdc3d13c3c85#run-glm-5.2-in-unsloth-studio)で実行できます。アクセシビリティと精度の面で最良の結果を得るために、239GBの [`UD-IQ2_M`](https://huggingface.co/unsloth/GLM-5.2-GGUF/tree/main/UD-IQ2_M) 量子化版を使用します。

### 🦥 Unsloth StudioでGLM-5.2を実行

GLM-5.2は [Unsloth Studio](/docs/jp/shii/studio.md)で実行できます。これはローカルAI向けのオープンソースWeb UIです。 **Unsloth Studioは自動的にRAMへオフロードし、マルチGPU構成を検出します**。Unsloth Studioを使えば、モデルをローカルで **MacOS、Windows**、Linux、そして：

{% columns %}
{% column %}

* 検索、ダウンロード、 [GGUFを実行](/docs/jp/shii/studio.md#run-models-locally) およびsafetensorモデル
* [**自己修復** ツール呼び出し](/docs/jp/shii/studio.md#execute-code--heal-tool-calling) + **ウェブ検索**
* [**コード実行**](/docs/jp/shii/studio.md#run-models-locally) （Python、Bash）
* [自動推論](/docs/jp/shii/studio.md#model-arena) パラメータ調整（temp、top-pなど）
* llama.cppによる高速なCPU＋GPU推論
* [LLMを学習](/docs/jp/shii/studio.md#no-code-training) VRAMを70%削減し、2倍高速
  {% endcolumn %}

{% column %}

<figure><img src="/files/cb7ca1e4a565a72a3e686c1141b8e66e7fd5e1e3" alt=""><figcaption><p>2ビットの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を安全に起動**

**NEW!** Unslothでは、無料のCloudflareトンネルを通じてHTTPSでStudioを安全に起動する方法が提供されるようになりました。以下を使用してください（Windows、Mac、Linuxで動作します）:

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

{% endstep %}

{% step %}
**GLM-5.2を検索してダウンロード**

Unsloth Studioは自動的にRAMへオフロードし、マルチGPU構成を検出します。初回起動時には、アカウントを保護するためのパスワードを作成し、後で再度サインインする必要があります。

それから [Unsloth Chat](/docs/jp/shii/studio/chat.md) タブに移動して **GLM-5.2** を検索バーで検索し、希望するモデルと量子化をダウンロードしてください。モデルを実行するのに十分な計算資源があることを確認してください。

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

{% step %}
**GLM-5.2を実行**

Unsloth Studioを使用する場合、推論パラメータは自動設定されるはずですが、手動で変更することもできます。コンテキスト長、チャットテンプレート、その他の設定も編集できます。

詳細は、 [Unsloth Studio推論ガイド](/docs/jp/shii/studio/chat.md).

<figure><img src="/files/312f4757d818526fa891f62e4040a86961a57174" alt=""><figcaption><p>Canvasで動作する1ビットGLM-5.2の例</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/jp/ji-ben/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/ad5b1435204b34ff26907e7d70b17440bff46df2" alt=""><figcaption></figcaption></figure>

その後、短い Flappy Bird ゲームを作るように促すと、次のようになります：

<figure><img src="/files/bc3eac30b8d52d03576e0735ff857d890a965e9a" 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らしさのあるキャップ＋ハイライト描画のパイプ
- 鳥がパイプを通過するとスコアが増え、パーティクルの弾けとチャイムSEが鳴る
- パイプ、天井、地面との衝突でゲームオーバーになり、画面フラッシュ、画面シェイク、死亡パーティクルが発生する

**仕上げ**:
- Press Start 2PのピクセルフォントHUDで、スコア表示と、自己ベスト更新時の「新記録！」の点滅
- スコアしきい値に応じたメダルシステム（★ / ✦ / ✧ / •）付きのゲームオーバーパネル
- `localStorage`でハイスコアを保存
- 揺れるパネルのあるアニメーション付きスタート画面
- Web Audio APIによる手続き的な効果音（羽ばたき、スコア、ヒット、死亡）— 外部素材なし
- 移動する地面の縞模様／小石でスピード感を強調
- レスポンシブ対応：ビューポートに合わせて拡大縮小し、マウス、タッチ、キーボード（Space/↑/Enter）をサポート
- リトライボタン + どこをタップしても再スタートできる便利機能

ブラウザでファイルを開くだけです — タップ/クリック/スペースキーで飛べます。
````

{% endcode %}

</details>

しかもゲームにはサウンドがあり、素晴らしく動作します！ これは1ビット量子化で、うまく機能したことを忘れないでください！

<figure><img src="/files/5211292ec96eb409b06055380cd69b2bd6d17c8a" 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キャッシュdtypeがサポートされています：

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

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

{% endcode %}

デフォルトでは `f16` が使われます。もし `q4_0` を使うなら、重み1つあたり約4.5ビットなので、約16 / 4.5 = **3.5倍長いコンテキスト長**まで拡張できます！ つまり、元のモデルが10Kまで対応していたなら、35Kも視野に入ります！ `q4_1` のほうが、シフトパラメータも得られるのでおそらく良く、重み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/0d76bb2a73078352bbec741573213012fa3d39ba" alt=""><figcaption></figcaption></figure> <figure><img src="/files/0011dcf0f52482be86b28b096ad76b9b8f243cd6" 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（報告済み最高性能ハーネス） |   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   |      -      |      -     |        -        |
| **エージェント型**                      |         |                 |         |                |         |             |            |                 |
| 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/jp/moderu/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.
