> 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/fr/modeles/glm-5.2.md).

# GLM-5.2 - comment exécuter localement

GLM-5.2 est le nouveau modèle ouvert de Z.ai, offrant des performances SOTA pour le codage à long horizon, le raisonnement et les tâches agentiques. Avec **744 milliards de paramètres**, 40 milliards de paramètres actifs, et une **fenêtre de contexte de 1M** fenêtre, il peut désormais être exécuté localement avec [Unsloth Dynamic](/docs/fr/bases/dynamic-3.0-ggufs.md) les GGUF. GLM-5.2 est le **modèle ouvert le plus puissant** à ce jour, avec des performances équivalentes à Claude 4.8 Opus, GPT-5.5 et Gemini 3.1 Pro sur de nombreux benchmarks.

[Le 1-bit dynamique atteint **\~76.2%**](/docs/fr/modeles/glm-5.2.md#quantization-analysis) une précision top-1 tout en étant **86 % plus petit**. Le 2-bit dynamique atteint **\~82%** une précision tout en étant **84 % plus petit**. Cela signifie que le modèle n'est pas 82 % moins bon parce qu'il est 84 % plus petit - il n'est en réalité que \~18 % moins précis que le modèle complet de **1.5TB** modèle. Merci à Z.ai d'avoir donné à Unsloth un accès dès le premier jour. [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)

<a href="/pages/5a7cb89811055498b527bf86240cffd678766c37#run-glm-5.2-tutorials" class="button primary">Tutoriels pour exécuter GLM-5.2</a><a href="/pages/5a7cb89811055498b527bf86240cffd678766c37#quantization-analysis" class="button secondary">Résultats de quantification</a>

#### &#x20;:gear: Guide d'utilisation

{% columns %}
{% column %}
La quantification dynamique 2-bit `UD-IQ2_M` utilise **239GB** d'espace disque - cela peut directement tenir sur un **Mac avec 256 Go de mémoire unifiée** et fonctionne bien sur un **GPU 1x24 Go** et **256 Go de RAM** avec déchargement MoE.

La **1 bit** quantification tiendra sur 223 Go de RAM et le 8-bit nécessite 810 Go de RAM. Voyez le GGUF 1 bit en action à droite :
{% endcolumn %}

{% column %}

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

**Tableau : exigences matérielles pour l'inférence** (unités = mémoire totale : RAM + VRAM, ou mémoire unifiée)

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

{% hint style="success" %}
Pour de meilleures performances, assurez-vous que votre mémoire totale disponible, y compris la VRAM et la RAM système, dépasse largement la taille du fichier du modèle quantifié.
{% endhint %}

### Paramètres recommandés

GLM-5.2 dispose de **3 modes de pensée**. Sans pensée et avec pensée en deux modes : **Élevé** + **Max**. Utilisez Max Thinking pour les tâches compliquées. Dans [Unsloth Studio](#run-glm-5.2-in-unsloth-studio) vous pouvez facilement basculer entre High + Max Thinking et sans pensée via une interface.

Utilisez ces paramètres pour la plupart des cas d'utilisation :

| Paramètres par défaut (la plupart des tâches) | SWE-Bench Pro       |
| --------------------------------------------- | ------------------- |
| `température` = 1.0                           | `température` = 1.0 |
| `top_p` = 0.95                                | `top_p` = 1.0       |

* **Fenêtre de contexte maximale :** `1,048,576`.

#### Désactiver la pensée, modifier l'effort de raisonnement

GLM 5.2 utilise le raisonnement par défaut. Il prend aussi en charge des niveaux d'effort de raisonnement où `reasoning_effort` peut être « high », « max » ou désactivé.

Pour désactiver la pensée, utilisez `--chat-template-kwargs '{"enable_thinking":false}'`. Si vous êtes sur **Windows** Powershell, utilisez : `--chat-template-kwargs "{\"enable_thinking\":false}"`

Vous pouvez aussi utiliser `--reasoning on` ou `--reasoning off` dans llama.cpp également maintenant !

Pour personnaliser l'effort de raisonnement ou désactiver le raisonnement, utilisez les exemples ci-dessous :

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

### 📈 Analyse de quantification

Nous avons également exécuté des benchmarks KLD (divergence KL) pour évaluer la précision de nos quantifications de GLM-5.2-GGUF. Le 4-bit dynamique UD-Q4\_K\_XL et le 5-bit dynamique UD-Q5\_K\_XL sont en grande partie sans perte, et les quantifications plus petites fonctionnent aussi très bien en laissant dynamiquement les couches importantes en précision plus élevée, et les couches peu importantes en bits faibles.

Sur la précision top-1 % pure, **le 1-bit dynamique atteint environ 76,2 % de précision tout en étant 86 % plus petit**! Le 2-bit dynamique atteint environ 82 % de précision tout en étant 84 % plus petit. Cela montre que quantifier dynamiquement certaines couches avec une précision plus élevée ne rend pas le modèle 86 % pire tout en étant 86 % plus petit - mais seulement 24 % moins capable que le modèle complet de 1.5TB.

**Mais que décrit réellement « 76 % de précision » ?**

76 % top-1 % ne signifie PAS que « La capitale de la France est » se transforme en un choix de Paris à 76 % et de Sydney à 24 %. Le modèle n'est PAS « plus bête » de 24 %. Ici, Paris est toujours à 100 %, et Sydney à 0 %. Le chiffre de 76 % inclut les mots de remplissage et les mots vides sur l'ensemble du corpus, par exemple en demandant :

« Crée un roman » donnera, en raison de l'échantillonnage du LLM :

* Je vais maintenant créer un roman...
* Le roman est ci-dessous :
* Quel genre souhaitez-vous qu'il soit ?

Chaque exemple est correct, mais c'est la distribution \[I, The, What] qui change - le modèle de base pourrait utiliser \[I] 100 % du temps, mais maintenant \[I] est à 76 % et \[The] à 24 %.

Cela **PAS** signifie que vous obtenez des sorties incorrectes comme du charabia ou des sorties incorrectes 24 % du temps.

<figure><img src="/files/07dcd972724cde320926015d473656e7284ef9bf" alt=""><figcaption></figcaption></figure>

Un KLD de 99,9 % est aussi généralement bon - il y a toutefois un gain plus important à partir du 4bit, donc pour les tâches massives hors distribution, le 4-bit dynamique est probablement le meilleur.

Top-1 % est une distribution binomiale « forcée » du KLD lui-même. Le KLD est la « distance » entre les probabilités de la version de base (BF16 ou Q8\_0) et celles de la version quantifiée. L'objectif de la quantification est de minimiser l'objectif ci-dessous :

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

Où `f` est la propagation avant du modèle de langage et `q` est l'opération de quantification et `W` sont les paramètres ou les poids du modèle. L'objectif est de rendre la « distance » entre la sortie des logits de la version de base `f(W)` et la sortie du modèle quantifié aussi petite que possible. Si vous pouvez obtenir un KLD de 0, alors vous avez parfaitement reconstruit le modèle !

<figure><img src="/files/80f302ebb96d8b5253d22c7c1fcb9fd356945b9e" alt=""><figcaption></figcaption></figure>

Nous utilisons **le KLD moyen** comme ci-dessous, car il est coûteux d'exécuter le KLD sur l'ensemble du corpus d'entraînement (15T de jetons, par exemple) - à la place, nous faisons **un échantillonnage,** et obtenons un petit sous-ensemble représentatif du corpus d'entraînement **/** pour la tâche en aval, puis l'optimisons. Le KLD moyen suit généralement une tendance monotone par rapport à l'espace disque, et montre que même en 1 bit GLM 5.2 fonctionne bien !

La précision top-1 % est simplement un opérateur de décodage glouton où l'on suppose que l'élément argmax sera choisi, et pour le 1 bit, à 76 %, il choisit le même que l'argmax de la version de base.

<figure><img src="/files/9983aa43ab1ba48a8ab1ec158e9fbef136d8f948" alt=""><figcaption></figcaption></figure>

## Exécuter les tutoriels GLM-5.2 :

Vous pouvez désormais exécuter GLM-5.2 dans [llama.cpp](#run-in-llama.cpp) et [Unsloth Studio](https://unsloth.ai/docs/fr/modeles/pages/5a7cb89811055498b527bf86240cffd678766c37#run-glm-5.2-in-unsloth-studio). Nous utiliserons la quantification de 239 Go [`UD-IQ2_M`](https://huggingface.co/unsloth/GLM-5.2-GGUF/tree/main/UD-IQ2_M) pour obtenir les meilleurs résultats en termes d'accessibilité et de précision.

### 🦥 Exécuter GLM-5.2 dans Unsloth Studio

GLM-5.2 peut s'exécuter dans [Unsloth Studio](/docs/fr/nouveau/studio.md), une interface web open source pour l'IA locale. **Unsloth Studio décharge automatiquement en RAM et détecte les configurations multi-GPU**. Avec Unsloth Studio, vous pouvez exécuter des modèles localement sur **MacOS, Windows**, Linux et :

{% columns %}
{% column %}

* Rechercher, télécharger, [exécuter des GGUF](/docs/fr/nouveau/studio.md#run-models-locally) et des modèles safetensor
* [**Auto-réparation** appel d’outils](/docs/fr/nouveau/studio.md#execute-code--heal-tool-calling) + **recherche web**
* [**Exécution de code**](/docs/fr/nouveau/studio.md#run-models-locally) (Python, Bash)
* [Ajustement automatique des paramètres d’inférence](https://unsloth.ai/docs/desktop#feature-deep-dive) (temp, top-p, etc.)
* Inférence CPU + GPU rapide via llama.cpp
* [Entraîner des LLM](/docs/fr/nouveau/studio.md#no-code-training) 2 fois plus vite avec 70 % de VRAM en moins
  {% endcolumn %}

{% column %}

<figure><img src="/files/79d3ac1aa3419bfab18dbb1b1b6d7bd1eb4dcae6" alt=""><figcaption><p>Sortie GGUF de GLM-5.2 2 bits</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% stepper %}
{% step %}
**Installer et lancer Unsloth**

Pour l'installer, exécutez dans votre terminal :

MacOS, Linux, WSL :

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

Windows PowerShell :

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

**Lancer Unsloth**

MacOS, Linux, WSL et Windows :

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

Ensuite ouvrez `http://127.0.0.1:8888` (ou votre URL spécifique) dans votre navigateur.

**Lancer Unsloth en toute sécurité avec HTTPS et Cloudflare**

**NOUVEAU !** Unsloth propose désormais un moyen sécurisé de lancer Studio via HTTPS grâce à un tunnel Cloudflare gratuit. Utilisez ce qui suit (fonctionne sous Windows, Mac et Linux) :

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

{% endstep %}

{% step %}
**Rechercher et télécharger GLM-5.2**

Unsloth Studio décharge automatiquement en RAM et détecte les configurations multi-GPU. Lors du premier lancement, vous devrez créer un mot de passe pour sécuriser votre compte et vous reconnecter plus tard.

Puis allez dans l’onglet [Unsloth Chat](/docs/fr/nouveau/studio/chat.md) onglet et recherchez **GLM-5.2** dans la barre de recherche et téléchargez le modèle et la quantification souhaités. Assurez-vous d'avoir suffisamment de calcul pour exécuter le modèle.

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

{% step %}
**Exécuter GLM-5.2**

Les paramètres d’inférence devraient être définis automatiquement lors de l’utilisation d’Unsloth Studio, mais vous pouvez toujours les modifier manuellement. Vous pouvez également modifier la longueur du contexte, le modèle de chat et d’autres paramètres.

Pour plus d’informations, vous pouvez consulter notre [guide d’inférence Unsloth Studio](/docs/fr/nouveau/studio/chat.md).

<figure><img src="/files/211d8e16fdfcbdf198b4fcf31aa214ac5e6e4235" alt=""><figcaption><p>Exemple de GLM-5.2 1 bit exécuté avec Canvas</p></figcaption></figure>
{% endstep %}
{% endstepper %}

### 🦙 Exécuter GLM-5.2 dans llama.cpp

Dans ce guide, nous utiliserons la `UD-IQ2_M` quantification, qui nécessitera au moins 245 Go de RAM. N'hésitez pas à changer le type de quantification. Pour ces tutoriels, nous utiliserons [llama.cpp](llama.cpphttps://github.com/ggml-org/llama.cpp) pour une inférence locale rapide. GGUF : [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)&#x20;

{% stepper %}
{% step %}
Obtenez le dernier `llama.cpp` **sur** [**GitHub ici**](https://github.com/ggml-org/llama.cpp). Vous pouvez également suivre les instructions de compilation ci-dessous. Modifiez `-DGGML_CUDA=ON` en `-DGGML_CUDA=OFF` si vous n’avez pas de GPU ou si vous voulez simplement une inférence CPU. **Pour les appareils Apple Mac / Metal**, définissez `-DGGML_CUDA=OFF` puis continuez normalement - la prise en charge de Metal est activée par défaut.

```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 %}
Vous pouvez désormais utiliser `llama.cpp` directement pour charger et télécharger des modèles, tout comme `ollama run`. D'abord, sélectionnez le type de quantification souhaité, comme `UD-IQ2_M`. Utilisez aussi `export LLAMA_CACHE="unsloth/GLM-5.2-GGUF"` pour forcer `llama.cpp` à enregistrer à un emplacement spécifique. **Notez que ce processus de téléchargement peut être très lent**, il est donc probablement préférable d'utiliser le processus de téléchargement manuel dans la section suivante.

```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 %}
Si vous souhaitez télécharger le modèle manuellement **(beaucoup plus rapide !)**, nous pouvons télécharger le modèle via le code ci-dessous (après avoir installé `pip install huggingface_hub`). Si les téléchargements se bloquent, voir : [Hugging Face Hub, débogage XET](/docs/fr/bases/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*" # Utilisez "*UD-Q8_K_XL*" pour une précision quasi complète
```

Si vous souhaitez utiliser le 1 bit dynamique, faites alors :

{% 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 %}
Ensuite, exécutez le modèle en mode conversation. Utilisez `unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf` pour le 2 bits ou `unsloth/GLM-5.2-GGUF/UD-IQ1_S/GLM-5.2-UD-IQ1_S-00001-of-00006.gguf`  pour le 1 bit.

{% 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 %}
Lorsque vous lancez llama-cli, vous verrez :

<figure><img src="/files/97b3af2188fc8567e64e3bd8712b4682d754672e" alt=""><figcaption></figcaption></figure>

Puis, après lui avoir demandé de créer un petit jeu Flappy Bird, nous obtenons :

<figure><img src="/files/47463a94eec210b7fa42041cbb292bd498bcdada" alt=""><figcaption></figcaption></figure>

Avec la conversation complète et le jeu ci-dessous :

<details>

<summary>Jeu complet en HTML</summary>

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

```html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Planeur du coucher de soleil</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">NOUVEAU RECORD !</div>
    </div>
    <div id="flash" class="flash"></div>

    <!-- Écran de démarrage -->
    <div id="start-screen" class="overlay visible">
      <div class="panel">
        <div class="title">GLISSEUR<br>DU CRÉPUSCULE</div>
        <div class="subtitle">— vol au crépuscule —</div>
        <div class="tap-icon">✊</div>
        <div class="instructions">TOUCHEZ / ESPACE / CLIQUEZ pour battre des ailes</div>
      </div>
    </div>

    <!-- Écran de fin de partie -->
    <div id="end-screen" class="overlay hidden">
      <div class="panel">
        <div class="title" style="font-size:22px;color:var(--coral);">FIN DE PARTIE</div>
        <div id="medal" class="medal" style="display:none;">★</div>
        <div class="score-row">
          <div class="score-box">
            <div class="label">SCORE</div>
            <div class="value" id="end-score">0</div>
          </div>
          <div class="score-box best">
            <div class="label">MEILLEUR</div>
            <div class="value" id="end-best">0</div>
          </div>
        </div>
        <button class="btn" id="restart-btn">RÉESSAYER</button>
      </div>
    </div>

    <div class="footer">Son activé • Touchez pour voler</div>
  </div>
</div>

<script>
(() => {
  // ============ Configuration ============
  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();

  // ============ Audio (API Web Audio) ============
  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); }
  };

  // ============ État du jeu ============
  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;

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

  // ============ Tuyaux ============
  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;
    }
  }

  // ============ Particules ============
  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
      });
    }
  }

  // ============ Arrière-plan ============
  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
      });
    }
  }

  // ============ Réinitialisation / Démarrage ============
  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');
  }

  // ============ Entrée ============
  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) {
      // toucher n'importe où sur l'écran de fin redémarre aussi
      // mais nous laissons principalement le bouton gérer cela
    }
  }

  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(); });

  // ============ Mise à jour ============
  const GRAVITY = 0.42;
  const MAX_FALL = 9;

  function update() {
    frameCount++;
    if (state === STATE.READY) {
      // balancement d'inactivité
      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));

      // Tuyaux
      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);
        }
      }
      // Supprimer les tuyaux hors écran, en ajouter un nouveau
      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));
      }

      // Collision
      // Sol et plafond
      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;
      }
      // Tuyaux
      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) {
          // collision
          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;
      }
    }

    // Particules
    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);
    }

    // Décroissance du tremblement
    if (shake > 0) shake = Math.max(0, shake - 1.2);

    // Animations de l'arrière-plan
    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;
  }

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

    // Dégradé du ciel
    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);

    // Étoiles
    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();
    }

    // Soleil
    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();

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

    // Silhouette des montagnes
    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();

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

    // Sol
    drawGround();

    // Oiseau
    drawBird();

    // Particules
    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;

    // dégradé
    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');

    // Tuyau du haut
    ctx.fillStyle = grad;
    ctx.fillRect(x, 0, PIPE_W, topY - 18);
    // Capuchon supérieur
    ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
    // Surbrillance du capuchon supérieur
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, topY - 18, 14, 18);

    // Tuyau du bas
    ctx.fillStyle = grad;
    ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    // Capuchon inférieur
    ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, bottomY, 14, 18);

    // Contour
    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;
    // Sol sableux/crépusculaire
    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);

    // ligne de surface
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, groundY);
    ctx.lineTo(W, groundY);
    ctx.stroke();

    // Rayures du motif (en mouvement)
    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();
    }

    // Cailloux au-dessus
    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);

    // Ombre
    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();

    // Corps
    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();

    // Aile (battante)
    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();

    // Œil
    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();

    // Bec
    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();

    // Queue
    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();
  }

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

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

{% endcode %}

</details>

<details>

<summary>Conversation complète</summary>

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

````
[Début de réflexion]
L'utilisateur veut un petit jeu Flappy Bird. Je vais créer un fichier HTML complet et autonome avec :

1. Un jeu classique dans le style de Flappy Bird
2. De jolis graphismes et animations
3. Le suivi du score
4. Un écran de fin de partie
5. Une fonctionnalité de redémarrage
6. Une bonne physique (gravité, mécanique de saut)
7. Des tuyaux qui se déplacent et sont placés aléatoirement
8. Des effets sonores (avec l'API Web Audio pour les tonalités)
9. Un style visuel sympa - j'éviterai le classique oiseau jaune et utiliserai une palette de couleurs plus intéressante

Laissez-moi concevoir cela avec :
- Un joli fond de ciel en dégradé
- Un oiseau animé avec des ailes battantes
- Des tuyaux au look vert/métallique élégant
- Un affichage du score
- Un écran de démarrage, un état de jeu, un état de fin de partie
- Le suivi du meilleur score avec localStorage
- Des effets de particules lors du score
- Des animations fluides

Je vais utiliser une palette de couleurs coucher de soleil/crépuscule pour le rendre distinctif - des oranges et des violets chaleureux, avec un oiseau d'une couleur corail/rouge chaude.

Laissez-moi coder cela soigneusement :

Fonctionnalités clés :
- Rendu basé sur Canvas
- Boucle de jeu requestAnimationFrame
- Physique de l'oiseau : gravité, impulsion du saut
- Génération des tuyaux : interstices aléatoires, déplacement vers la gauche
- Détection des collisions
- Incrément du score lorsque l'on franchit des tuyaux
- Effets de particules au score
- Animation de mort
- localStorage pour le meilleur score
- Effets sonores via Web Audio API
- Prise en charge du mobile/tactile
- Superposition d'interface propre et soignée

Laissez-moi écrire le code.
[Fin de réflexion]

Je vais créer un jeu Flappy Bird soigné avec une esthétique crépusculaire/coucher de soleil distinctive, une physique fluide, des effets de particules et du son.

```html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Planeur du coucher de soleil</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">NOUVEAU RECORD !</div>
    </div>
    <div id="flash" class="flash"></div>

    <!-- Écran de démarrage -->
    <div id="start-screen" class="overlay visible">
      <div class="panel">
        <div class="title">GLISSEUR<br>DU CRÉPUSCULE</div>
        <div class="subtitle">— vol au crépuscule —</div>
        <div class="tap-icon">✊</div>
        <div class="instructions">TOUCHEZ / ESPACE / CLIQUEZ pour battre des ailes</div>
      </div>
    </div>

    <!-- Écran de fin de partie -->
    <div id="end-screen" class="overlay hidden">
      <div class="panel">
        <div class="title" style="font-size:22px;color:var(--coral);">FIN DE PARTIE</div>
        <div id="medal" class="medal" style="display:none;">★</div>
        <div class="score-row">
          <div class="score-box">
            <div class="label">SCORE</div>
            <div class="value" id="end-score">0</div>
          </div>
          <div class="score-box best">
            <div class="label">MEILLEUR</div>
            <div class="value" id="end-best">0</div>
          </div>
        </div>
        <button class="btn" id="restart-btn">RÉESSAYER</button>
      </div>
    </div>

    <div class="footer">Son activé • Touchez pour voler</div>
  </div>
</div>

<script>
(() => {
  // ============ Configuration ============
  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();

  // ============ Audio (API Web Audio) ============
  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); }
  };

  // ============ État du jeu ============
  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;

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

  // ============ Tuyaux ============
  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;
    }
  }

  // ============ Particules ============
  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
      });
    }
  }

  // ============ Arrière-plan ============
  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
      });
    }
  }

  // ============ Réinitialisation / Démarrage ============
  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');
  }

  // ============ Entrée ============
  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) {
      // toucher n'importe où sur l'écran de fin redémarre aussi
      // mais nous laissons principalement le bouton gérer cela
    }
  }

  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(); });

  // ============ Mise à jour ============
  const GRAVITY = 0.42;
  const MAX_FALL = 9;

  function update() {
    frameCount++;
    if (state === STATE.READY) {
      // balancement d'inactivité
      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));

      // Tuyaux
      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);
        }
      }
      // Supprimer les tuyaux hors écran, en ajouter un nouveau
      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));
      }

      // Collision
      // Sol et plafond
      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;
      }
      // Tuyaux
      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) {
          // collision
          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;
      }
    }

    // Particules
    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);
    }

    // Décroissance du tremblement
    if (shake > 0) shake = Math.max(0, shake - 1.2);

    // Animations de l'arrière-plan
    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;
  }

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

    // Dégradé du ciel
    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);

    // Étoiles
    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();
    }

    // Soleil
    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();

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

    // Silhouette des montagnes
    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();

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

    // Sol
    drawGround();

    // Oiseau
    drawBird();

    // Particules
    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;

    // dégradé
    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');

    // Tuyau du haut
    ctx.fillStyle = grad;
    ctx.fillRect(x, 0, PIPE_W, topY - 18);
    // Capuchon supérieur
    ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
    // Surbrillance du capuchon supérieur
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, topY - 18, 14, 18);

    // Tuyau du bas
    ctx.fillStyle = grad;
    ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    // Capuchon inférieur
    ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, bottomY, 14, 18);

    // Contour
    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;
    // Sol sableux/crépusculaire
    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);

    // ligne de surface
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, groundY);
    ctx.lineTo(W, groundY);
    ctx.stroke();

    // Rayures du motif (en mouvement)
    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();
    }

    // Cailloux au-dessus
    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);

    // Ombre
    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();

    // Corps
    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();

    // Aile (battante)
    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();

    // Œil
    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();

    // Bec
    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();

    // Queue
    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();
  }

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

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

Voici ce que j'ai construit :

**Thème** : "Sunset Flier" — au lieu du look vert/turquoise classique de Flappy Bird, j'ai choisi une palette de crépuscule/coucher de soleil (violet profond → corail → or) avec des montagnes en silhouette, un soleil lumineux, des nuages dérivants et des étoiles scintillantes dans le ciel supérieur.

**Oiseau** : un oiseau rose corail avec une aile jaune battante, un bec animé, une queue et un œil mobile. Les battements d'ailes s'animent physiquement lorsque l'oiseau monte/descend.

**Mécaniques** :
- Physique classique de gravité + impulsion de battement, réglée pour être réactive
- Tuyaux avec placement aléatoire des ouvertures, défilement fluide, rendu avec capuchon et surbrillance pour un look 2D épais
- Le score augmente lorsque l'oiseau dépasse un tuyau, avec un pop de particules et un effet sonore de carillon
- Les collisions avec les tuyaux, le plafond et le sol déclenchent la fin de partie avec flash d'écran, secousse de l'écran et particules de mort

**Finitions** :
- HUD en police pixel Press Start 2P avec score et pulsation "NEW BEST !" lors des runs record
- Panneau de fin de partie avec système de médailles (★ / ✦ / ✧ / •) selon des seuils de score
- Meilleur score conservé via `localStorage`
- Écran de démarrage animé avec panneau oscillant
- Effets sonores procéduraux via Web Audio API (battement d'ailes, score, impact, mort) — sans ressource externe
- Bandes/cailloux du sol en mouvement pour renforcer la sensation de vitesse
- Réactif : s'adapte à la fenêtre, prend en charge la souris, le tactile et le clavier (Espace/↑/Entrée)
- Bouton Réessayer + possibilité de redémarrer en tapant n'importe où

Ouvrez simplement le fichier dans un navigateur — touchez/cliquez/appuyez sur Espace pour voler.
````

{% endcode %}

</details>

Et le jeu a du son et fonctionne à merveille ! Rappel : c'était une quantification 1 bit et cela a bien fonctionné !

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

### :triangular\_ruler:Long contexte via la quantification du cache KV

Pour exploiter un contexte long dans llama.cpp, nous devons utiliser la quantification du cache KV afin de réduire l'utilisation mémoire. Récemment, llama.cpp a ajouté des astuces de plus grande précision à la quantification du cache KV - [voir](https://github.com/ggml-org/llama.cpp/pull/21038) et d'autres PR !

Actuellement, ces types de données de cache KV sont pris en charge :

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

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

{% endcode %}

Par défaut `f16` est utilisé. Si vous utilisez `q4_0` qui est d'environ 4,5 bits par poids, vous pouvez allonger d'environ 16 / 4,5 = **des longueurs de contexte 3,5 fois plus longues** ! Donc, si votre modèle prenait auparavant en charge 10K, 35K peut être à portée ! `q4_1` est probablement meilleur puisque vous obtenez aussi un paramètre de décalage, et fait 5 bits par poids - donc des contextes 3,2 fois plus longs.

Utilisez-le comme ci-dessous :

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

## 📊 Benchmarks

Vous pouvez voir plus bas les benchmarks GLM-5.2 sous forme de tableau :

<div><figure><img src="/files/2f8afe72b0118e99e437f6da635f4f50bf1883b7" alt=""><figcaption></figcaption></figure> <figure><img src="/files/aaf0fc9f56870884edd25775496f0a4f6f5a2424" alt=""><figcaption></figcaption></figure></div>

| Benchmark                                           | 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 |
| --------------------------------------------------- | :-----: | :-------------: | :-----: | :------------: | :-----: | :---------: | :--------: | :-------------: |
| **Raisonnement**                                    |         |                 |         |                |         |             |            |                 |
| HLE                                                 |   40.5  |      49.8\*     |  41.4\* |       45       |    31   |     41.4    |     37     |       37.7      |
| HLE (avec outils)                                   |   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 Nov. 2025                                      |   94.4  |       96.5      |   96.5  |      94.8      |    94   |      95     |    84.4    |       94.4      |
| HMMT Feb. 2026                                      |   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      |
| **Codage**                                          |         |                 |         |                |         |             |            |                 |
| 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 (meilleur banc d'essai rapporté) |   82.7  |       78.9      |   83.4  |      70.7      |    69   |      -      |      -     |        -        |
| FrontierSWE (Dominance)                             |   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   |      -      |      -     |        -        |
| **Agentique**                                       |         |                 |         |                |         |             |            |                 |
| MCP-Atlas (ensemble public)                         |   76.8  |       77.8      |   75.3  |      69.2      |   71.8  |     76.4    |    74.2    |       73.6      |
| Décathlon d'outils                                  |   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/fr/modeles/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.
