# B2 — Démineur : la cascade qui s'appelle elle-même

## Vous avez trouvé le second niveau caché : bienvenue !

`IDDQD`. En 1993, dans *Doom*, ces cinq lettres tapées au clavier rendaient le
joueur invulnérable — le fameux « mode Dieu ». Ceux qui savent, savent. Ici,
elles vous ouvrent la porte du second sujet bonus, et je vous préviens tout de
suite : celui-ci ne vous rendra pas invulnérable. Il est plus dur que le
premier.

**Ce sujet n'est pas un devoir.** Il prolonge la récursion d'AP1 et les
collections approfondies en AP4. Rien n'est ramassé d'office. Si je le retiens
comme bonus pour un cycle, je préciserai les exercices attendus, le dossier
de dépôt et l'échéance ; son absence ne retire aucun point.

Nous allons coder le **Démineur** — celui de Windows 3.1, celui sur lequel des
générations de bureaux ont perdu des heures. Vous connaissez la règle : une
grille, des mines cachées, et chaque case découverte affiche le nombre de mines
qui l'entourent. Si vous tombez sur une mine, c'est fini.

Mais il y a dans ce jeu un mécanisme bien plus intéressant que les mines, et
c'est celui que tout le monde a remarqué sans jamais y réfléchir : **quand vous
cliquez sur une case vide, ce n'est pas une case qui s'ouvre, c'en est
cinquante**. Une région entière se découvre d'un coup, avec sa frontière de
chiffres bien nette. Comment le programme sait-il où s'arrêter ?

La réponse tient en un mot — la **récursion** — et en une précaution, l'ensemble
des cases déjà vues, que nous ferons circuler en accumulateur. C'est le sujet de
l'exercice 4, et c'est le cœur de ce notebook.

Durée indicative : **2 h**. Les outils sont ceux des séances 3 et 4 : modules et
clauses, récursion avec accumulateur, `Enum`, et surtout le **`MapSet`**, que
nous allons faire travailler de deux façons différentes — pour représenter les
mines, et pour mémoriser ce qui a déjà été révélé. Aucun process n'est
nécessaire, même si le dernier exercice vous donnera une occasion d'en lancer
si le cœur vous en dit.

## Comment travailler ce notebook

Le format est celui de vos rendus :

* Chaque exercice contient une cellule de code avec des trous marqués
  `# À COMPLÉTER (n)`. Remplissez, puis **évaluez** (**Ctrl/Cmd + Entrée**).
* Sous chaque exercice, une cellule de **vérification** affiche `✅` si tout va
  bien, ou lève une erreur explicite sinon. **Ne les modifiez pas** : elles
  testent des **propriétés** — invariants, cas limites, grille-témoin calculée à
  la main — et pas seulement un résultat recopié.
* Les cellules s'évaluent **de haut en bas**. En cas de doute, menu du notebook
  → *Evaluate all*.
* **Aucun corrigé n'est caché ici.**

### Comment nous représentons une grille

Une décision de conception, prise une fois pour toutes, et qui vaut d'être
discutée. Une grille de démineur, c'est un rectangle de cases dont une poignée
cachent une mine. On pourrait stocker un tableau de booléens, une case par
position — mais les mines sont **rares** : sur la grille « débutant » de
Windows, 10 mines pour 81 cases. Stocker 71 `false` pour 10 `true`, c'est du
gâchis.

Nous ne stockons donc que **l'ensemble des positions minées**, dans un
`MapSet` de tuples `{ligne, colonne}` — exactement le raisonnement du jeu de la
vie, où l'on ne garde que les cellules vivantes. Une grille sera donc une map à
trois champs :

```elixir
grille_exemple = %{
  lignes: 4,
  colonnes: 4,
  mines: MapSet.new([{1, 1}, {2, 3}])
}

MapSet.member?(grille_exemple.mines, {1, 1})
```

Les lignes sont numérotées de `0` à `lignes - 1`, les colonnes de `0` à
`colonnes - 1`, et `{0, 0}` est le coin **en haut à gauche**.

Voici la grille-témoin qui nous suivra tout le long. Elle est petite, et je l'ai
calculée à la main — vous pouvez, et devriez, refaire le calcul :

```
colonne :  0 1 2 3
ligne 0 :  1 1 1 0
ligne 1 :  1 💣 2 1
ligne 2 :  1 1 2 💣
ligne 3 :  0 0 1 1
```

Chaque chiffre compte les mines parmi les **huit** cases voisines. Vérifiez par
exemple la case `{1, 2}` : elle touche `{1,1}` et `{2,3}`, donc `2`. Et la case
`{0, 3}` ne touche ni l'une ni l'autre, donc `0` — retenez cette case, elle
servira.

## Exercice 1 — Les voisines d'une case, bords compris

Tout part de là. Une case a **huit** voisines — les quatre côtés et les quatre
diagonales, ce qu'on appelle le *voisinage de Moore*. Sauf sur les bords :
`{0, 0}`, dans le coin, n'en a que **trois**.

C'est ce « sauf » qui fait l'exercice. Il ne s'agit pas d'un détail cosmétique :
une voisine hors grille fausserait tous les comptages de mines, et ferait sortir
la cascade de l'exercice 4 du plateau.

Écrivez `Voisins.voisines/2` qui renvoie la liste des voisines de `{l, c}` qui
sont **effectivement dans la grille**.

Le squelette ci-dessous utilise une **compréhension** `for`, que vous n'avez pas
encore rencontrée en cours. Elle se lit très bien : `for x <- liste, condition,
do: expression` parcourt la liste, **ne garde que** les éléments qui vérifient
toutes les conditions, et applique l'expression. C'est un `filter` suivi d'un
`map`, en une seule écriture. Ici, deux générateurs `dl` et `dc` parcourent les
décalages `-1, 0, 1`, ce qui donne les `3 × 3 = 9` cases du carré autour de
`{l, c}` — dont il faudra retirer le centre, et tout ce qui déborde.

> 💡 Si la compréhension vous rebute, rien ne vous oblige à l'utiliser : un
> `Enum.filter/2` sur une liste des huit décalages fait parfaitement l'affaire.
> Le résultat compte, pas la route.

```elixir
defmodule Voisins do
  def voisines(grille, {l, c}) do
    for dl <- [-1, 0, 1],
        dc <- [-1, 0, 1],
        # À COMPLÉTER (1) : remplacez le `true` ci-dessous par les conditions
        # qui manquent, séparées par des virgules. Il y a trois choses à exiger :
        # - une case n'est pas sa propre voisine ;
        # - la ligne l + dl doit rester dans [0, grille.lignes - 1] ;
        # - la colonne c + dc doit rester dans [0, grille.colonnes - 1].
        # Évaluez la cellule telle quelle d'abord : vous obtiendrez les 9 cases
        # du carré, centre compris et débordements compris. À vous d'élaguer.
        true do
      {l + dl, c + dc}
    end
  end
end

temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
Voisins.voisines(temoin, {0, 0})
```

Vérification :

```elixir
if Code.ensure_loaded?(Voisins) do
  temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
  tri = fn liste -> liste |> Enum.sort() end

  cond do
    tri.(Voisins.voisines(temoin, {1, 1})) != tri.([{0,0},{0,1},{0,2},{1,0},{1,2},{2,0},{2,1},{2,2}]) ->
      raise """
      Une case au milieu a EXACTEMENT 8 voisines. Pour {1, 1} on attend
        [{0,0},{0,1},{0,2},{1,0},{1,2},{2,0},{2,1},{2,2}]
      obtenu #{inspect(tri.(Voisins.voisines(temoin, {1, 1})))}.
      Avez-vous bien exclu la case elle-même ?
      """

    tri.(Voisins.voisines(temoin, {0, 0})) != tri.([{0, 1}, {1, 0}, {1, 1}]) ->
      raise """
      Le coin {0, 0} n'a que 3 voisines : [{0,1}, {1,0}, {1,1}].
      Obtenu #{inspect(tri.(Voisins.voisines(temoin, {0, 0})))}.
      Les cases de ligne ou de colonne négative n'existent pas.
      """

    tri.(Voisins.voisines(temoin, {3, 3})) != tri.([{2, 2}, {2, 3}, {3, 2}]) ->
      raise "Le coin bas-droit {3, 3} n'a que 3 voisines, obtenu #{inspect(tri.(Voisins.voisines(temoin, {3, 3})))} — attention à la borne SUPÉRIEURE (lignes - 1)"

    length(Voisins.voisines(temoin, {0, 2})) != 5 ->
      raise "Une case de bord (hors coin) comme {0, 2} a 5 voisines, obtenu #{length(Voisins.voisines(temoin, {0, 2}))}"

    true ->
      # Propriétés, sur une grille plus grande cette fois.
      grande = %{lignes: 6, colonnes: 7, mines: MapSet.new()}
      cases = for l <- 0..5, c <- 0..6, do: {l, c}

      for case_ <- cases do
        v = Voisins.voisines(grande, case_)

        # 1. jamais soi-même
        if case_ in v do
          raise "#{inspect(case_)} ne doit pas figurer parmi ses propres voisines"
        end

        # 2. jamais de doublon
        if length(Enum.uniq(v)) != length(v) do
          raise "les voisines de #{inspect(case_)} comportent un doublon : #{inspect(v)}"
        end

        # 3. toujours dans la grille
        for {vl, vc} <- v do
          if vl < 0 or vl > 5 or vc < 0 or vc > 6 do
            raise "la voisine #{inspect({vl, vc})} de #{inspect(case_)} sort de la grille 6×7"
          end
        end

        # 4. jamais plus de 8
        if length(v) > 8 do
          raise "#{inspect(case_)} a #{length(v)} voisines : c'est au plus 8"
        end
      end

      # 5. la symétrie : si A est voisine de B, alors B est voisine de A
      for case_ <- cases, voisine <- Voisins.voisines(grande, case_) do
        if case_ not in Voisins.voisines(grande, voisine) do
          raise "le voisinage doit être symétrique : #{inspect(voisine)} est voisine de #{inspect(case_)} mais pas l'inverse"
        end
      end

      IO.puts("✅ Voisinage correct : 8 au centre, 5 au bord, 3 dans un coin, et symétrique")
  end
else
  IO.puts("ℹ️  Définissez d'abord le module Voisins, puis réévaluez cette vérification.")
end
```

## Exercice 2 — Compter les mines autour

Avec les voisines en poche, le chiffre affiché sur une case ne demande plus
qu'un comptage : combien, parmi ces voisines, appartiennent à l'ensemble des
mines ?

Écrivez `Compte.mines_autour/2`. C'est un pipeline de deux maillons : prendre
les voisines, puis les **compter sous condition**. Cherchez dans la
documentation d'`Enum` la fonction qui compte les éléments vérifiant un
prédicat — elle s'appelle `count/2` — et souvenez-vous que `MapSet.member?/2`
répond à la question « cette case est-elle minée ? ».

> **Modules à garder ouverts** : **https://hexdocs.pm/elixir/Enum.html** ·
> **https://hexdocs.pm/elixir/MapSet.html**

```elixir
defmodule Compte do
  # À COMPLÉTER (2) : mines_autour(grille, case) -> le nombre de voisines minées.
  def mines_autour(grille, case_) do
    nil
  end
end

temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
Compte.mines_autour(temoin, {1, 2})
```

Vérification :

```elixir
if Code.ensure_loaded?(Compte) do
  temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}

  # La grille-témoin, recopiée depuis le tableau de l'introduction.
  # Les mines y sont notées nil : on ne teste pas le compte SUR une mine.
  attendu = %{
    {0, 0} => 1, {0, 1} => 1, {0, 2} => 1, {0, 3} => 0,
    {1, 0} => 1, {1, 1} => nil, {1, 2} => 2, {1, 3} => 1,
    {2, 0} => 1, {2, 1} => 1, {2, 2} => 2, {2, 3} => nil,
    {3, 0} => 0, {3, 1} => 0, {3, 2} => 1, {3, 3} => 1
  }

  for {case_, n} <- attendu, n != nil do
    obtenu = Compte.mines_autour(temoin, case_)

    if obtenu != n do
      raise """
      mines_autour(témoin, #{inspect(case_)}) devrait valoir #{n}, obtenu #{inspect(obtenu)}.
      Reprenez le tableau de l'introduction et recomptez à la main les huit
      voisines de cette case (moins celles qui sortent de la grille).
      """
    end
  end

  # Propriété : sans aucune mine, tout vaut 0.
  vide = %{lignes: 5, colonnes: 5, mines: MapSet.new()}

  for l <- 0..4, c <- 0..4 do
    if Compte.mines_autour(vide, {l, c}) != 0 do
      raise "sur une grille sans mine, mines_autour doit valoir 0 partout"
    end
  end

  # Propriété : avec TOUTES les cases minées, chaque case voit exactement
  # autant de mines qu'elle a de voisines.
  pleine = %{lignes: 4, colonnes: 5, mines: MapSet.new(for l <- 0..3, c <- 0..4, do: {l, c})}

  for l <- 0..3, c <- 0..4 do
    attendu_ici = length(Voisins.voisines(pleine, {l, c}))

    if Compte.mines_autour(pleine, {l, c}) != attendu_ici do
      raise """
      Sur une grille entièrement minée, mines_autour(#{inspect({l, c})}) devrait valoir
      #{attendu_ici} (son nombre de voisines), obtenu #{Compte.mines_autour(pleine, {l, c})}.
      Comptez-vous la case elle-même par erreur ?
      """
    end
  end

  IO.puts("✅ Comptage juste sur la grille-témoin, sur une grille vide et sur une grille pleine")
else
  IO.puts("ℹ️  Définissez d'abord le module Compte, puis réévaluez cette vérification.")
end
```

## Exercice 3 — La grille des nombres

Nous savons compter les mines autour d'**une** case. Faisons-le pour **toutes**,
afin d'obtenir d'un coup la carte complète du jeu.

Deux fonctions à écrire :

* `Nombres.cases/1` renvoie la liste de **toutes** les positions de la grille,
  `{0,0}`, `{0,1}`, … jusqu'à `{lignes-1, colonnes-1}`. Une compréhension `for`
  à deux générateurs y suffit, ou deux `Enum.flat_map`/`Enum.map` imbriqués.
* `Nombres.nombres/1` renvoie une **map** associant à chaque case **non minée**
  son nombre de mines voisines. Les cases minées n'y figurent pas du tout :
  elles n'ont pas de chiffre, elles ont une mine.

Le chemin pour `nombres/1` est un pipeline de quatre maillons : toutes les
cases, puis on **écarte** les minées (`Enum.reject/2`), puis on transforme
chaque case en paire `{case, nombre}` (`Enum.map/2`), puis on fabrique une map à
partir de cette liste de paires. Cette dernière fonction, cherchez-la dans la
doc du module `Map` : elle prend une liste de paires et en fait une map.

```elixir
defmodule Nombres do
  # À COMPLÉTER (3) : cases(grille) -> la liste de TOUTES les positions.
  def cases(grille) do
    nil
  end

  # À COMPLÉTER (4) : nombres(grille) -> %{case_non_minee => nb_mines_voisines}
  def nombres(grille) do
    nil
  end
end

temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
Nombres.nombres(temoin)
```

Vérification :

```elixir
if Code.ensure_loaded?(Nombres) do
  temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}

  cond do
    length(Nombres.cases(temoin)) != 16 ->
      raise "une grille 4×4 compte 16 cases, cases/1 en renvoie #{length(Nombres.cases(temoin))}"

    Enum.sort(Nombres.cases(temoin)) != Enum.sort(for l <- 0..3, c <- 0..3, do: {l, c}) ->
      raise "cases/1 doit renvoyer TOUTES les positions de {0,0} à {3,3}, obtenu #{inspect(Enum.sort(Nombres.cases(temoin)))}"

    length(Nombres.cases(%{lignes: 3, colonnes: 7, mines: MapSet.new()})) != 21 ->
      raise "une grille 3×7 compte 21 cases (3 lignes × 7 colonnes) — n'inversez pas lignes et colonnes"

    true ->
      n = Nombres.nombres(temoin)

      # La grille-témoin de l'introduction, chiffre par chiffre.
      attendu = %{
        {0, 0} => 1, {0, 1} => 1, {0, 2} => 1, {0, 3} => 0,
        {1, 0} => 1, {1, 2} => 2, {1, 3} => 1,
        {2, 0} => 1, {2, 1} => 1, {2, 2} => 2,
        {3, 0} => 0, {3, 1} => 0, {3, 2} => 1, {3, 3} => 1
      }

      cond do
        map_size(n) != 14 ->
          raise """
          nombres/1 doit contenir 14 entrées : les 16 cases MOINS les 2 mines.
          Obtenu #{map_size(n)} entrées.
          Une case minée n'a pas de chiffre : elle ne doit pas figurer dans la map.
          """

        Map.has_key?(n, {1, 1}) or Map.has_key?(n, {2, 3}) ->
          raise "les cases minées {1, 1} et {2, 3} ne doivent PAS figurer dans la map des nombres"

        n != attendu ->
          differences =
            attendu
            |> Enum.filter(fn {case_, v} -> Map.get(n, case_) != v end)
            |> Enum.map(fn {case_, v} -> "#{inspect(case_)} : attendu #{v}, obtenu #{inspect(Map.get(n, case_))}" end)

          raise "La grille des nombres ne correspond pas au tableau de l'introduction :\n" <> Enum.join(differences, "\n")

        true ->
          # Propriété : sur une grille sans mine, toutes les cases valent 0
          # et il y en a exactement lignes × colonnes.
          vide = %{lignes: 6, colonnes: 5, mines: MapSet.new()}
          nv = Nombres.nombres(vide)

          cond do
            map_size(nv) != 30 ->
              raise "sans mine, les 30 cases d'une grille 6×5 ont toutes un chiffre, obtenu #{map_size(nv)}"

            Enum.any?(Map.values(nv), fn v -> v != 0 end) ->
              raise "sans mine, tous les chiffres valent 0"

            true ->
              IO.puts("✅ La grille des nombres est conforme au tableau calculé à la main :")

              for l <- 0..3 do
                0..3
                |> Enum.map(fn c ->
                  if MapSet.member?(temoin.mines, {l, c}), do: " *", else: " #{n[{l, c}]}"
                end)
                |> Enum.join()
                |> IO.puts()
              end
          end
      end
  end
else
  IO.puts("ℹ️  Définissez d'abord le module Nombres, puis réévaluez cette vérification.")
end
```

## Exercice 4 — La cascade : le morceau de bravoure

Nous y sommes. C'est l'exercice qui justifie ce sujet, et il ne fait qu'une
dizaine de lignes.

Rappelons la règle du vrai jeu. Vous cliquez sur une case :

* si elle touche **au moins une** mine, elle s'ouvre seule et affiche son
  chiffre. On s'arrête là ;
* si elle n'en touche **aucune** — chiffre `0` —, elle s'ouvre **et ouvre aussi
  toutes ses voisines**. Lesquelles, si elles valent `0` à leur tour, ouvrent
  les leurs. Et ainsi de suite, jusqu'à ce que la région soit bordée de chiffres
  de tous côtés.

Cette phrase — « et ainsi de suite » — est la définition même d'une fonction
**récursive**. Le cas d'arrêt est « la case porte un chiffre », le cas récursif
est « la case est vide, donc je recommence sur chacune de ses huit voisines ».

Sauf qu'écrite comme ça, telle quelle, cette fonction ne s'arrête **jamais**.

### La question à se poser avant d'écrire une ligne

Prenez deux cases voisines, `A` et `B`, toutes deux à `0`. Suivez le programme
dans votre tête : on révèle `A`, qui vaut `0`, donc on passe à ses voisines,
dont `B`. On révèle `B`, qui vaut `0`, donc on passe à ses voisines… dont `A`.

Combien de temps cela dure-t-il ? Que manque-t-il à cette fonction pour
qu'elle sache qu'elle est déjà passée par là ?

Vous avez la réponse : il faut **mémoriser les cases déjà révélées**, et cette
mémoire doit **circuler** d'un appel au suivant. C'est exactement la notion
d'**accumulateur** de la leçon sur la récursion — sauf qu'ici l'accumulateur
n'est pas un compteur, c'est un **ensemble**, et c'est le moment où le `MapSet`
montre ce qu'il vaut : `MapSet.member?/2` répond instantanément, quelle que
soit la taille de l'ensemble.

Écrivez `Revele.revele/3`. Elle prend la grille, la case d'où part le clic, et
l'ensemble `vues` des cases **déjà révélées** ; elle renvoie l'ensemble `vues`
**enrichi** de tout ce que ce clic découvre. Trois branches, et un `cond do`
les accueille très bien :

1. la case est **déjà** dans `vues` : il n'y a rien à faire, on renvoie `vues`
   tel quel — c'est le garde-fou qui fait terminer la récursion ;
2. la case touche au moins une mine : on l'ajoute à `vues` et on s'arrête là ;
3. sinon : on l'ajoute à `vues`, puis on **enchaîne** l'appel récursif sur
   chacune de ses voisines.

Le point 3 mérite qu'on s'y arrête, parce que c'est là que tout se joue. Il ne
s'agit **pas** d'appeler `revele` sur les huit voisines chacune de son côté,
puis de réunir les résultats : chaque branche repartirait du même ensemble
`vues` et re-explorerait les mêmes régions, encore et encore. Il s'agit de
traiter les voisines **l'une après l'autre**, en passant à chacune l'ensemble
**enrichi par les précédentes**. Autrement dit, un ensemble qui circule le long
d'une liste en se faisant enrichir à chaque étape — cherchez bien, vous
connaissez la fonction d'`Enum` qui fait exactement cela, vous l'avez vue en
séance 3.

> ⚠️ La vérification mesure le **temps** que met votre cascade sur une grille
> 20 × 20 vide. Ce n'est pas de la coquetterie : la version qui réunit les huit
> résultats au lieu de faire circuler l'accumulateur donne le **bon** résultat,
> mais met plusieurs minutes là où la bonne version met une milliseconde. Si le
> test vous dit « trop lent », relisez le paragraphe ci-dessus.

Enfin, une convention qui simplifie tout : `revele/3` suppose que la case de
départ **n'est pas une mine**. C'est `jouer/2`, à l'exercice suivant, qui se
chargera de ce cas.

```elixir
defmodule Revele do
  # À COMPLÉTER (5) : revele(grille, case, vues) -> le MapSet des cases révélées.
  # Trois branches (voir l'énoncé), et un accumulateur qui CIRCULE.
  def revele(grille, case_, vues) do
    cond do
      true -> nil
    end
  end
end

temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
Revele.revele(temoin, {3, 0}, MapSet.new()) |> MapSet.to_list() |> Enum.sort()
```

Vérification :

```elixir
if Code.ensure_loaded?(Revele) do
  temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}

  # Petit utilitaire : on lance la cascade dans un process séparé, avec un
  # budget de temps. Une cascade qui ne termine pas (ou qui re-explore sans
  # fin les mêmes régions) est ainsi interrompue proprement, au lieu de figer
  # votre notebook.
  cascade = fn grille, depart, budget_ms ->
    tache = Task.async(fn -> Revele.revele(grille, depart, MapSet.new()) end)

    case Task.yield(tache, budget_ms) || Task.shutdown(tache, :brutal_kill) do
      {:ok, vues} -> vues
      nil -> :trop_lent
      {:exit, raison} -> {:plante, raison}
    end
  end

  sorti = fn ens -> ens |> MapSet.to_list() |> Enum.sort() end

  # --- Cas 1 : un clic dans le coin bas-gauche, sur un 0 ------------------
  # Attendu, calculé à la main sur le tableau de l'introduction :
  # {3,0} vaut 0, donc il ouvre {2,0}=1, {2,1}=1 et {3,1}=0 ;
  # {3,1} vaut 0, donc il ouvre à son tour {2,2}=2 et {3,2}=1. Puis c'est fini :
  # tous les nouveaux venus portent un chiffre, ils ne propagent rien.
  cas1 = cascade.(temoin, {3, 0}, 5_000)

  if cas1 == :trop_lent do
    raise """
    Votre cascade n'a pas répondu en 5 secondes sur une grille 4×4 !
    C'est le symptôme d'une récursion qui ne s'arrête pas : deux cases voisines
    à 0 s'ouvrent mutuellement, indéfiniment. La première branche de votre cond
    (« cette case est-elle DÉJÀ dans vues ? ») est-elle bien en place, et bien
    en PREMIER ?
    """
  end

  attendu1 = [{2, 0}, {2, 1}, {2, 2}, {3, 0}, {3, 1}, {3, 2}]

  if sorti.(cas1) != attendu1 do
    raise """
    revele(témoin, {3, 0}) devrait révéler exactement
      #{inspect(attendu1)}  (6 cases)
    obtenu #{inspect(sorti.(cas1))}.

    Le détail : {3,0} vaut 0 → il ouvre {2,0}, {2,1} et {3,1}.
    {2,0} vaut 1, {2,1} vaut 1 : ils s'arrêtent là.
    {3,1} vaut 0 → il ouvre {2,2} et {3,2}, qui valent 2 et 1 : fin de la cascade.
    Une case à chiffre est RÉVÉLÉE mais ne propage PAS.
    """
  end

  # --- Cas 2 : le coin haut-droit, une région plus petite -----------------
  cas2 = cascade.(temoin, {0, 3}, 5_000)
  attendu2 = [{0, 2}, {0, 3}, {1, 2}, {1, 3}]

  if cas2 == :trop_lent or sorti.(cas2) != attendu2 do
    raise """
    revele(témoin, {0, 3}) devrait révéler #{inspect(attendu2)} (4 cases),
    obtenu #{inspect(if cas2 == :trop_lent, do: :trop_lent, else: sorti.(cas2))}.
    {0,3} est le seul 0 de ce coin : il ouvre ses trois voisines {0,2}, {1,2}
    et {1,3}, qui portent toutes un chiffre et arrêtent la cascade.
    """
  end

  # --- Cas 3 : cliquer sur une case à chiffre n'ouvre QUE cette case ------
  cas3 = cascade.(temoin, {0, 0}, 5_000)

  if cas3 == :trop_lent or sorti.(cas3) != [{0, 0}] do
    raise """
    {0, 0} vaut 1 : le clic ne doit révéler QUE cette case, soit [{0, 0}].
    Obtenu #{inspect(if cas3 == :trop_lent, do: :trop_lent, else: sorti.(cas3))}.
    """
  end

  # --- Propriété : sans mine, un seul clic révèle TOUTE la grille ---------
  for {n, m} <- [{5, 7}, {3, 3}, {1, 9}, {8, 4}] do
    vide = %{lignes: n, colonnes: m, mines: MapSet.new()}
    tout = cascade.(vide, {0, 0}, 5_000)

    cond do
      tout == :trop_lent ->
        raise """
        Votre cascade n'a pas fini en 5 secondes sur une grille #{n}×#{m} SANS mine,
        où toutes les cases valent 0 et s'ouvrent donc en chaîne.

        Deux causes possibles, et elles se distinguent facilement :
        - elle ne s'arrête jamais → la première branche (« déjà dans vues ? »)
          manque, ou la case courante n'est ajoutée à `vues` qu'APRÈS l'appel
          récursif sur les voisines ;
        - elle s'arrête mais recalcule sans fin les mêmes régions → vos huit
          voisines repartent chacune du même `vues`, et vous réunissez les
          résultats. Il faut au contraire les enchaîner l'une après l'autre,
          chacune recevant l'ensemble enrichi par les précédentes.
        """

      MapSet.size(tout) != n * m ->
        raise """
        Sur une grille #{n}×#{m} SANS aucune mine, toutes les cases valent 0 :
        un seul clic doit donc révéler les #{n * m} cases.
        Obtenu #{MapSet.size(tout)}.
        """

      true ->
        :ok
    end
  end

  # --- Propriété : la cascade ne révèle JAMAIS une mine -------------------
  # (une case à 0 n'a par définition aucune mine parmi ses voisines)
  for depart <- [{3, 0}, {0, 3}, {0, 0}, {3, 3}, {2, 2}] do
    revelees = cascade.(temoin, depart, 5_000)

    if revelees != :trop_lent and not MapSet.disjoint?(revelees, temoin.mines) do
      raise """
      En partant de #{inspect(depart)}, votre cascade a révélé une mine :
      #{inspect(sorti.(MapSet.intersection(revelees, temoin.mines)))}.
      C'est impossible si la règle est respectée : une case à 0 ne touche
      aucune mine, elle ne peut donc pas en ouvrir une.
      """
    end
  end

  # --- Propriété : l'accumulateur de départ est respecté ------------------
  deja = MapSet.new([{0, 0}, {1, 3}])
  enrichi = Revele.revele(temoin, {3, 0}, deja)

  if not MapSet.subset?(deja, enrichi) do
    raise """
    revele/3 doit ENRICHIR l'ensemble reçu, pas le remplacer : les cases déjà
    présentes dans `vues` doivent toujours y être à la sortie.
    Entré avec #{inspect(sorti.(deja))}, sorti sans elles : #{inspect(sorti.(enrichi))}.
    """
  end

  # --- Le test de vitesse : 400 cases, une milliseconde ------------------
  vide20 = %{lignes: 20, colonnes: 20, mines: MapSet.new()}
  grand = cascade.(vide20, {10, 10}, 5_000)

  cond do
    grand == :trop_lent ->
      raise """
      Votre cascade est CORRECTE sur les petites grilles mais n'a pas fini en
      5 secondes sur une grille 20×20 vide (400 cases).

      C'est la signature d'un accumulateur qui ne circule pas : si vous appelez
      revele sur chacune des huit voisines EN PARTANT DU MÊME `vues`, puis que
      vous réunissez les huit résultats, chaque branche re-parcourt tout ce que
      ses sœurs ont déjà parcouru. Le résultat est juste, le coût explose.

      Il faut enchaîner les voisines l'une après l'autre, chacune recevant
      l'ensemble enrichi par les précédentes. La fonction d'Enum qui promène un
      accumulateur le long d'une liste, vous la connaissez depuis la séance 3.
      """

    MapSet.size(grand) != 400 ->
      raise "sur une grille 20×20 sans mine, un clic révèle les 400 cases, obtenu #{MapSet.size(grand)}"

    true ->
      IO.puts("✅ La cascade s'arrête, ne révèle aucune mine, et avale 400 cases sans broncher")
      IO.puts("   Depuis {3,0} : #{MapSet.size(cas1)} cases · depuis {0,3} : #{MapSet.size(cas2)} · depuis {0,0} : #{MapSet.size(cas3)}")
  end
else
  IO.puts("ℹ️  Définissez d'abord le module Revele, puis réévaluez cette vérification.")
end
```

> **Deux questions pour consolider.** Écrivez vos réponses ici, en deux ou trois
> phrases chacune.
>
> 1. **Pourquoi votre `revele/3` s'arrête-t-elle toujours ?** Nommez la quantité
>    qui progresse strictement à chaque appel récursif *utile*, et dites
>    pourquoi elle ne peut pas progresser indéfiniment. (C'est le même
>    raisonnement que pour une récurrence en maths : il faut une quantité
>    bornée qui varie toujours dans le même sens.)
>
>    → …
>
> 2. On aurait pu mémoriser les cases vues dans une **liste** plutôt que dans un
>    `MapSet`. Le programme serait-il resté **correct** ? Serait-il resté
>    **rapide** ? Comparez le coût de « cette case est-elle déjà vue ? » dans
>    les deux structures, sur une grille de 10 000 cases.
>
>    → …

## Exercice 5 — Jouer, et voir le plateau

Le moteur est écrit ; donnons-lui une partie et un visage.

Une **partie** est une map à trois champs : la grille, l'ensemble des cases
révélées, et l'état courant.

```elixir
partie_exemple = %{
  grille: %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])},
  vues: MapSet.new(),
  etat: :en_cours
}
```

Vous écrivez trois fonctions.

**`Demineur.nouvelle/1`** prend une grille et renvoie une partie fraîche : aucune
case vue, état `:en_cours`.

**`Demineur.jouer/2`** prend la partie et la case cliquée, et renvoie un tuple
`{resultat, partie_mise_a_jour}` où `resultat` vaut :

* `:boum` si la case cliquée est minée — la partie passe à l'état `:boum` ;
* `:gagne` si, après la cascade, **toutes** les cases non minées sont révélées ;
* `:continue` sinon.

C'est l'idiome que vous croisez partout en Elixir depuis la séance 1 : une
fonction ne renvoie pas seulement une valeur, elle renvoie une **étiquette** et
une valeur, `{:ok, ...}` / `{:error, ...}`, et l'appelant fait son `case` sur
l'étiquette. Ici les étiquettes sont trois, mais l'esprit est le même.

> 💡 Comment savoir qu'on a gagné ? Il n'y a rien à chercher : le nombre de
> cases à découvrir est connu d'avance, c'est `lignes × colonnes` moins le
> nombre de mines. Comparez-le à la taille de `vues`.

**`Demineur.afficher/1`** renvoie la **chaîne** du plateau, une ligne par ligne
de grille, avec ces symboles :

| situation | symbole |
|---|---|
| case non révélée | ⬛ |
| case révélée, aucune mine autour | 🟦 |
| case révélée, `n` mines autour | 1️⃣ 2️⃣ 3️⃣ … |
| mine, **et** la partie est perdue | 💣 |

Tant que la partie n'est pas perdue, les mines restent cachées comme les autres
cases : c'est un jeu, pas une visite guidée. L'attribut `@chiffres` vous donne
la traduction d'un nombre en symbole.

```elixir
defmodule Demineur do
  @chiffres %{0 => "🟦", 1 => "1️⃣", 2 => "2️⃣", 3 => "3️⃣", 4 => "4️⃣",
              5 => "5️⃣", 6 => "6️⃣", 7 => "7️⃣", 8 => "8️⃣"}

  # À COMPLÉTER (6) : nouvelle(grille) -> une partie fraîche.
  def nouvelle(grille) do
    nil
  end

  # À COMPLÉTER (7) : jouer(partie, case) -> {:boum | :gagne | :continue, partie}
  def jouer(partie, case_) do
    nil
  end

  # À COMPLÉTER (8) : afficher(partie) -> la chaîne du plateau.
  # Une piste de structure : pour chaque ligne, fabriquez la liste des symboles
  # de ses cases et recollez-la ; puis recollez les lignes avec "\n".
  def afficher(partie) do
    nil
  end
end

temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
{resultat, partie} = Demineur.jouer(Demineur.nouvelle(temoin), {3, 0})
IO.puts(Demineur.afficher(partie))
IO.inspect({resultat, MapSet.size(partie.vues)}, label: "clic en {3, 0} : {résultat, cases révélées}")
```

Vérification :

```elixir
if Code.ensure_loaded?(Demineur) do
  temoin = %{lignes: 4, colonnes: 4, mines: MapSet.new([{1, 1}, {2, 3}])}
  neuve = Demineur.nouvelle(temoin)

  cond do
    MapSet.size(neuve.vues) != 0 ->
      raise "une partie neuve n'a aucune case révélée, obtenu #{MapSet.size(neuve.vues)}"

    neuve.etat != :en_cours ->
      raise "une partie neuve est à l'état :en_cours, obtenu #{inspect(neuve.etat)}"

    neuve.grille != temoin ->
      raise "nouvelle/1 doit conserver la grille reçue telle quelle dans le champ :grille"

    true ->
      # --- Coup 1 : {3, 0}, la région du bas-gauche -----------------------
      {r1, p1} = Demineur.jouer(neuve, {3, 0})

      # --- Coup 2 : {0, 3}, la région du haut-droit -----------------------
      {r2, p2} = Demineur.jouer(p1, {0, 3})

      # --- Coup 3 : sur une mine -----------------------------------------
      {r3, p3} = Demineur.jouer(p2, {1, 1})

      lignes1 = String.split(Demineur.afficher(p1), "\n")
      lignes2 = String.split(Demineur.afficher(p2), "\n")
      lignes3 = String.split(Demineur.afficher(p3), "\n")

      cond do
        r1 != :continue ->
          raise "après un premier clic en {3, 0}, il reste des cases à découvrir : on attend :continue, obtenu #{inspect(r1)}"

        MapSet.size(p1.vues) != 6 ->
          raise "le clic en {3, 0} révèle 6 cases (exercice 4), la partie en compte #{MapSet.size(p1.vues)}"

        r2 != :continue ->
          raise "après le second clic en {0, 3}, on attend encore :continue, obtenu #{inspect(r2)}"

        MapSet.size(p2.vues) != 10 ->
          raise """
          Après {3,0} (6 cases) puis {0,3} (4 cases de plus, sans recouvrement),
          la partie doit compter 10 cases révélées, obtenu #{MapSet.size(p2.vues)}.
          jouer/2 doit PARTIR de partie.vues, pas repartir d'un ensemble vide.
          """

        r3 != :boum ->
          raise "cliquer sur la mine {1, 1} doit renvoyer :boum, obtenu #{inspect(r3)}"

        p3.etat != :boum ->
          raise "après un :boum, la partie passe à l'état :boum, obtenu #{inspect(p3.etat)}"

        length(lignes1) != 4 ->
          raise "afficher/1 doit produire 4 lignes pour une grille 4×4, obtenu #{length(lignes1)}"

        lignes1 != ["⬛⬛⬛⬛", "⬛⬛⬛⬛", "1️⃣1️⃣2️⃣⬛", "🟦🟦1️⃣⬛"] ->
          raise """
          Plateau attendu après le clic en {3, 0} :
            ⬛⬛⬛⬛
            ⬛⬛⬛⬛
            1️⃣1️⃣2️⃣⬛
            🟦🟦1️⃣⬛
          obtenu :
            #{Enum.join(lignes1, "\n  ")}
          (⬛ non révélée · 🟦 révélée à 0 · le chiffre sinon · les mines restent
          cachées tant que la partie n'est pas perdue)
          """

        lignes2 != ["⬛⬛1️⃣🟦", "⬛⬛2️⃣1️⃣", "1️⃣1️⃣2️⃣⬛", "🟦🟦1️⃣⬛"] ->
          raise """
          Plateau attendu après {3,0} puis {0,3} :
            ⬛⬛1️⃣🟦
            ⬛⬛2️⃣1️⃣
            1️⃣1️⃣2️⃣⬛
            🟦🟦1️⃣⬛
          obtenu :
            #{Enum.join(lignes2, "\n  ")}
          """

        lignes3 != ["⬛⬛1️⃣🟦", "⬛💣2️⃣1️⃣", "1️⃣1️⃣2️⃣💣", "🟦🟦1️⃣⬛"] ->
          raise """
          Après le :boum, TOUTES les mines se montrent (les deux, pas seulement
          celle sur laquelle on a cliqué) :
            ⬛⬛1️⃣🟦
            ⬛💣2️⃣1️⃣
            1️⃣1️⃣2️⃣💣
            🟦🟦1️⃣⬛
          obtenu :
            #{Enum.join(lignes3, "\n  ")}
          """

        true ->
          # --- La partie gagnante, jouée jusqu'au bout --------------------
          # {3,0} ouvre 6 cases, {0,3} en ouvre 4 de plus ; restent alors
          # exactement {0,0}, {0,1}, {1,0} et {3,3}, qui portent toutes un
          # chiffre et s'ouvrent une par une. Six coups, 14 cases.
          coups = [{3, 0}, {0, 3}, {0, 0}, {0, 1}, {1, 0}, {3, 3}]

          {resultats, finale} =
            Enum.map_reduce(coups, neuve, fn coup, partie ->
              {r, suivante} = Demineur.jouer(partie, coup)
              {r, suivante}
            end)

          dernier = List.last(resultats)
          avant_dernier = Enum.at(resultats, -2)

          cond do
            avant_dernier == :gagne ->
              raise "la partie ne doit pas être déclarée gagnée avant que les 14 cases non minées soient révélées"

            dernier != :gagne ->
              raise """
              Après les 6 coups #{inspect(coups)}, les 14 cases non minées de la
              grille-témoin sont découvertes : le dernier coup doit renvoyer
              :gagne, obtenu #{inspect(dernier)} (#{MapSet.size(finale.vues)} cases révélées sur 14).
              On gagne quand vues atteint lignes × colonnes − nombre de mines,
              SANS avoir à découvrir les mines elles-mêmes.
              """

            finale.etat != :gagne ->
              raise "après la victoire, la partie passe à l'état :gagne, obtenu #{inspect(finale.etat)}"

            true ->
              IO.puts("✅ Une partie complète se joue : cascade, boum, et victoire")
              IO.puts("\nAprès {3,0} puis {0,3} :")
              IO.puts(Demineur.afficher(p2))
              IO.puts("\nLe boum :")
              IO.puts(Demineur.afficher(p3))
              IO.puts("\nLa victoire :")
              IO.puts(Demineur.afficher(finale))
          end
      end
  end
else
  IO.puts("ℹ️  Définissez d'abord le module Demineur, puis réévaluez cette vérification.")
end
```

## Exercice 6 (ouvert) — Le premier clic, ou Monte-Carlo à la maison

Changeons de casquette. Vous avez un jeu ; posons-lui une question de
**probabilités**, et répondons-y comme vous avez appris à le faire en Python
numérique : en simulant.

**La question.** Sur une grille `lignes × colonnes` où l'on place `nb_mines`
mines au hasard, et où l'on clique ensuite au hasard, quelle est la probabilité
de **survivre au premier clic** ?

Celle-là, vous savez y répondre de tête — et c'est justement l'intérêt : nous
allons vérifier notre simulateur sur un cas dont nous connaissons déjà la
réponse exacte, avant de lui poser des questions dont nous ne la connaissons
pas. C'est la bonne façon de se servir d'une méthode de Monte-Carlo.

Deux fonctions :

* `MonteCarlo.grille_aleatoire/3` construit une grille de `lignes × colonnes`
  avec **exactement** `nb_mines` mines, placées au hasard et **sans doublon**.
  Le piège est là : tirer `nb_mines` fois une case au hasard peut retomber deux
  fois sur la même, et vous n'auriez alors que 9 mines au lieu de 10. Cherchez
  dans la doc d'`Enum` la fonction qui prélève `n` éléments **distincts** d'une
  liste au hasard — son nom contient `random`.
* `MonteCarlo.survie/4` répète `tirages` fois l'expérience « je tire une grille,
  je clique sur une case au hasard », et renvoie la **proportion** de fois où la
  case n'était pas minée. Un `Enum.count/2` sur `1..tirages` fait l'affaire, et
  `Enum.random/1` tire un élément d'une plage.

```elixir
defmodule MonteCarlo do
  # À COMPLÉTER (9) : grille_aleatoire(lignes, colonnes, nb_mines)
  # -> %{lignes: ..., colonnes: ..., mines: MapSet de nb_mines cases DISTINCTES}
  def grille_aleatoire(lignes, colonnes, nb_mines) do
    nil
  end

  # À COMPLÉTER (10) : survie(lignes, colonnes, nb_mines, tirages)
  # -> la proportion (entre 0.0 et 1.0) de premiers clics qui ne tombent pas
  #    sur une mine, estimée sur `tirages` expériences indépendantes.
  def survie(lignes, colonnes, nb_mines, tirages) do
    nil
  end
end

MonteCarlo.survie(9, 9, 10, 2000)
```

**Avant d'évaluer la vérification, prédisez.** Sur la grille « débutant » de
Windows — 9 × 9, 10 mines — que doit valoir cette probabilité ? Vous devriez
pouvoir l'écrire en une fraction. Notez-la, puis comparez.

Vérification :

```elixir
if Code.ensure_loaded?(MonteCarlo) do
  # --- La grille tirée est-elle bien formée ? ----------------------------
  for _ <- 1..200 do
    g = MonteCarlo.grille_aleatoire(9, 9, 10)

    cond do
      g.lignes != 9 or g.colonnes != 9 ->
        raise "grille_aleatoire/3 doit renvoyer les dimensions demandées, obtenu #{g.lignes}×#{g.colonnes}"

      MapSet.size(g.mines) != 10 ->
        raise """
        grille_aleatoire(9, 9, 10) doit contenir EXACTEMENT 10 mines distinctes,
        obtenu #{MapSet.size(g.mines)}.
        Si vous tirez 10 fois une case au hasard, vous pouvez retomber deux fois
        sur la même : le MapSet les fusionne et il ne vous reste que 9 mines.
        Prélevez plutôt 10 cases distinctes d'un coup.
        """

      Enum.any?(g.mines, fn {l, c} -> l < 0 or l > 8 or c < 0 or c > 8 end) ->
        raise "une mine est tombée hors de la grille : #{inspect(MapSet.to_list(g.mines))}"

      true ->
        :ok
    end
  end

  # Sur 200 tirages de 10 mines parmi 81 cases, il serait extraordinaire
  # qu'une case ne soit JAMAIS minée : on teste ainsi que le tirage couvre
  # bien toute la grille et ne privilégie pas un coin.
  vues_minees =
    Enum.reduce(1..200, MapSet.new(), fn _, acc ->
      MapSet.union(acc, MonteCarlo.grille_aleatoire(9, 9, 10).mines)
    end)

  if MapSet.size(vues_minees) != 81 do
    raise """
    Sur 200 grilles de 10 mines, les 81 cases devraient toutes avoir été minées
    au moins une fois ; seules #{MapSet.size(vues_minees)} l'ont été.
    Votre tirage ne couvre pas toute la grille de façon uniforme.
    """
  end

  # --- Les cas extrêmes, où la réponse est certaine ----------------------
  if MonteCarlo.survie(5, 5, 0, 300) != 1.0 do
    raise "sans aucune mine, on survit toujours : survie(5, 5, 0, _) doit valoir 1.0, obtenu #{inspect(MonteCarlo.survie(5, 5, 0, 300))}"
  end

  if MonteCarlo.survie(4, 4, 16, 300) != 0.0 do
    raise "si les 16 cases d'une grille 4×4 sont minées, on ne survit jamais : survie doit valoir 0.0, obtenu #{inspect(MonteCarlo.survie(4, 4, 16, 300))}"
  end

  # --- L'estimation face à la valeur exacte ------------------------------
  # Le premier clic tombe sur une case uniforme parmi lignes × colonnes, dont
  # nb_mines sont minées : la probabilité de survie vaut donc exactement
  # 1 - nb_mines / (lignes × colonnes). Notre simulation doit s'en approcher.
  for {l, c, m} <- [{9, 9, 10}, {16, 16, 40}, {8, 8, 32}] do
    estimee = MonteCarlo.survie(l, c, m, 3000)
    exacte = 1 - m / (l * c)
    ecart = abs(estimee - exacte)

    cond do
      not is_float(estimee) and not is_integer(estimee) ->
        raise "survie/4 doit renvoyer un nombre entre 0.0 et 1.0, obtenu #{inspect(estimee)}"

      estimee < 0 or estimee > 1 ->
        raise "survie/4 est une PROPORTION : elle vit entre 0.0 et 1.0, obtenu #{estimee}"

      ecart > 0.05 ->
        raise """
        Sur une grille #{l}×#{c} à #{m} mines, la probabilité exacte de survie au
        premier clic vaut 1 - #{m}/#{l * c} = #{Float.round(exacte, 4)}.
        Votre simulation sur 3000 tirages donne #{Float.round(estimee * 1.0, 4)},
        soit un écart de #{Float.round(ecart, 4)} — bien trop grand pour du hasard.

        Pistes : comptez-vous les survies ou les explosions ? Divisez-vous bien
        par le NOMBRE DE TIRAGES ? Tirez-vous une NOUVELLE grille à chaque
        expérience (et non la même 3000 fois) ?
        """

      true ->
        :ok
    end
  end

  estimee = MonteCarlo.survie(9, 9, 10, 5000)
  exacte = 1 - 10 / 81

  IO.puts("✅ Simulateur juste : grilles bien formées, cas extrêmes exacts, estimation qui converge")
  IO.puts("   Grille 9×9 à 10 mines, 5000 tirages :")
  IO.puts("     survie estimée = #{Float.round(estimee * 1.0, 4)}")
  IO.puts("     survie exacte  = #{Float.round(exacte, 4)}  (soit 1 - 10/81)")
else
  IO.puts("ℹ️  Définissez d'abord le module MonteCarlo, puis réévaluez cette vérification.")
end
```

### Pour aller plus loin (vraiment ouvert, aucune vérification)

Le simulateur est validé sur une question dont nous connaissions la réponse.
Servons-nous-en maintenant pour des questions dont nous ne la connaissons pas —
et celles-ci, aucune formule ne les résout : il **faut** simuler.

1. **La taille de la première ouverture.** Quand le premier clic ne vous tue
   pas, combien de cases ouvre-t-il ? Faites-en la moyenne, pour une densité de
   mines donnée. Puis faites varier la densité de 5 % à 40 % et regardez la
   courbe : il existe une densité autour de laquelle le comportement bascule
   brutalement, d'un plateau qui s'ouvre presque en entier à des ouvertures
   minuscules. Ceux qui ont entendu parler de **percolation** reconnaîtront le
   phénomène ; les autres viennent de le découvrir par l'expérience, ce qui est
   la meilleure façon.
2. **Le premier clic protégé.** Dans le vrai démineur, le jeu triche : la
   première case cliquée n'est **jamais** minée, les mines sont placées après.
   Modifiez `grille_aleatoire` pour prendre une case à épargner. Quelle est
   alors la probabilité que ce premier clic ouvre au moins dix cases ?
3. **Un solveur, pour finir.** Une règle très simple résout déjà beaucoup de
   grilles : si une case révélée affiche `n` et qu'il lui reste exactement `n`
   voisines cachées, alors **toutes** sont minées ; et si une case affiche `n`
   et qu'on lui connaît déjà `n` mines voisines, alors toutes ses autres
   voisines cachées sont **sûres**. Programmez ces deux règles, appliquez-les en
   boucle jusqu'à ce que plus rien ne bouge, et mesurez la proportion de
   grilles 9 × 9 que cela suffit à terminer. Vous serez surpris — dans les deux
   sens.
4. **Et si vous avez envie de processus** (séance 4) : une campagne
   Monte-Carlo est l'exemple parfait du calcul *parallèle sans effort*, puisque
   les tirages sont indépendants. Découpez les `tirages` en quatre paquets,
   lancez-les avec `Task.async/1`, rassemblez avec `Task.await/1`, et mesurez
   l'accélération avec `:timer.tc/1`. Attention à la conclusion : sur 3000
   tirages aussi légers, le gain n'est peut-être pas celui que vous imaginez —
   et comprendre **pourquoi** vaut mieux que le gain lui-même.

## Et voilà

Vous avez écrit un démineur complet : la géométrie du plateau, le comptage, la
cascade, la partie, l'affichage, et une campagne de simulation pour finir.

Le passage difficile n'était pas la récursion — vous saviez la faire. C'était
l'**accumulateur** : comprendre qu'une mémoire doit circuler le long des appels,
et qu'elle doit circuler **en série**, chaque voisine recevant ce que les
précédentes ont découvert. Cette idée-là, vous la retrouverez partout : c'est
celle de tout parcours de graphe, et le plateau du démineur n'est rien d'autre
qu'un graphe où chaque case est reliée à ses huit voisines.

Enregistrez ce notebook complété (**Ctrl/Cmd + S**) : c'est ce fichier `.livemd`
que vous rendrez, si vous le souhaitez, selon les modalités décrites sur la page
**« Rendre un devoir »** du site. Signalez-moi simplement dans votre message que
c'est un rendu bonus.

Il n'y a pas de troisième niveau caché. Enfin, je crois.

Pour toute question, vous pouvez me contacter par email :
[aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu?cc=aurelien.noce@imagine-app.fr),
avec `aurelien.noce@imagine-app.fr` en copie.
