Attention, les paiements par chèques ne sont pas acceptés !

Horaires d'ouverture

Du lundi au vendredi

9h00 à 12h00
14h00 à 19h00

Samedi de 9h à 12h00

Le jeu de la vie

Inventé en 1970 par le mathématicien John Conway, le jeu de la vie est un « automate cellulaire » : un univers quadrillé où chaque case, appelée cellule, est soit vivante, soit morte. Aucun joueur : à chaque génération, chaque cellule regarde uniquement ses huit voisines et obéit à deux règles.

    • Naissance (B) : une cellule morte qui possède exactement le bon nombre de voisines vivantes devient vivante.

    • Survie (S) : une cellule vivante qui possède le bon nombre de voisines vivantes reste en vie ; sinon elle meurt.

La règle classique de Conway s'écrit B3/S23 : naissance avec 3 voisines, survie avec 2 ou 3. De ces deux règles minuscules naissent des structures étonnantes : planeurs qui voyagent, canons qui les produisent, naves qui se reproduisent. C'est l'émergence : du complexe sort du simple, sans aucune direction centrale.

Modifier les règles

Les trois menus de droite modifient les conditions de naissance, de survie, ou chargent un univers célèbre. La même grille de départ peut alors s'éteindre, exploser ou se figer en corridors. Quelques exemples :

UniversRègleComportement
ConwayB3/S23           L'équilibre classique, ni mort ni explosion.
HighLifeB36/S23Contient un motif qui se réplique à l'identique.
GrainesB2/S—Aucune cellule ne survit : tout meurt, tout renaît, feu d'artifice permanent.
LabyrintheB3/S12345Le monde se structure en couloirs et parois.
Day & NightB3678/S34678Vie et mort sont symétriques : le negatif du monde suit la même règle.

L'IA qui apprend la règle

À droite de l'écran, un petit réseau de neurones est lâché dans cet univers sans connaître ses lois. Il ne reçoit jamais la règle. On lui montre uniquement des milliers de situations tirées au hasard — une cellule, ses huit voisines — et, après coup, la bonne réponse : la cellule naît ou meurt.

    • Au début, il répond au hasard : précision d'environ 50 %, et son monde à lui est un chaos.
    • À chaque exemple, il ajuste légèrement ses connexions. Regardez la précision grimper.
    • Une fois entraîné, il joue son propre jeu. Ses erreurs sont marquées en rouge.

Le message essentiel : la règle de gauche a été programmée par un humain, celle de droite a été apprise par la machine à partir d'exemples. C'est exactement le principe des IA modernes : elles ne reçoivent pas de règles, elles en déduisent à partir de données.

Couleurs et repères

    • Une cellule vient de naître : elle est éclatante. Plus elle vieillit, plus elle s'assombrit
    • Trois palettes sont proposées pour varier les présentations.
    • Sur la grille du réseau, une cellule encadrée de rouge signale une erreur de prédiction.

Voici le code source pour les amateurs :

<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Le Jeu de la Vie — et l'IA qui apprend</title>
<style>
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    background: #0b0d16;
    color: #e8eaf2;
  }
  header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 14px 20px;
    border-bottom: 1px solid #1d2136;
  }
  header h1 { font-size: 28px; margin: 0; letter-spacing: 0.3px; }
  header h1 span { color: #8f83f2; }
  .header-buttons { display: flex; gap: 8px; }
  .layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 440px;
    gap: 18px;
    padding: 18px;
    max-width: 1860px;
    margin: 0 auto;
  }
  @media (max-width: 1020px) { .layout { grid-template-columns: 1fr; } }
  .card {
    background: #141728;
    border: 1px solid #262b44;
    border-radius: 14px;
    padding: 14px;
  }
  .card h2 {
    font-size: 16px;
    text-transform: uppercase;
    letter-spacing: 1.2px;
    color: #aab1d0;
    margin: 0 0 10px 0;
  }
  .card + .card { margin-top: 16px; }
  #grid { width: 100%; display: block; border-radius: 10px; touch-action: none; cursor: crosshair; }
  .toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
  }
  button, select {
    font-family: inherit;
    font-size: 16px;
    padding: 11px 16px;
    border-radius: 10px;
    background: #232741;
    color: #e8eaf2;
    border: 1px solid #32385c;
    cursor: pointer;
  }
  button:hover, select:hover { background: #2c3152; }
  button.big { background: #7c6cf0; border-color: #7c6cf0; color: #fff; font-weight: 600; }
  button.big:hover { background: #8d7ff5; }
  button.danger { background: #3a2434; border-color: #5c3550; }
  button.danger:hover { background: #4a2c42; }
  .field { margin-bottom: 12px; }
  .field label {
    display: block;
    font-size: 14px;
    color: #aab1d0;
    margin-bottom: 4px;
  }
  .field select { width: 100%; }
  .badge-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 10px;
  }
  .badge {
    font-size: 24px;
    font-weight: 700;
    letter-spacing: 1px;
    color: #62d0a5;
    background: #16281f;
    border: 1px solid #24553f;
    border-radius: 10px;
    padding: 6px 12px;
  }
  .gen { color: #aab1d0; font-size: 17px; }
  .hint { color: #8d94b3; font-size: 15px; margin-top: 10px; }
  .ai-grid-row { display: flex; gap: 12px; margin-top: 10px; }
  .ai-grid-row .mini { flex: 1; min-width: 0; }
  .mini p { text-align: center; margin: 6px 0 4px 0; font-size: 15px; color: #aab1d0; }
  .mini canvas { width: 100%; display: block; border-radius: 8px; }
  .gauge-wrap { margin: 10px 0; }
  .gauge {
    height: 16px;
    border-radius: 8px;
    background: #232741;
    overflow: hidden;
    border: 1px solid #32385c;
  }
  .gauge > div {
    height: 100%;
    width: 0%;
    background: linear-gradient(90deg, #7c6cf0, #62d0a5);
    transition: width 0.15s linear;
  }
  .train-stats { display: flex; justify-content: space-between; font-size: 15px; color: #aab1d0; margin-top: 6px; }
  .train-stats b { color: #e8eaf2; }
  .warning {
    display: none;
    margin-top: 10px;
    padding: 9px 12px;
    border-radius: 10px;
    background: #33270f;
    border: 1px solid #6b5220;
    color: #ffb454;
    font-size: 15px;
  }
  .warning.visible { display: block; }
  .status { margin-top: 8px; font-size: 15px; color: #62d0a5; min-height: 20px; }
  .legend { margin-top: 10px; font-size: 14px; color: #8d94b3; }
  .legend .mistake { color: #ff4d6d; font-weight: 600; }
  .speed-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
  .speed-row input { flex: 1; accent-color: #7c6cf0; }
  .speed-row span { font-size: 14px; color: #aab1d0; min-width: 90px; text-align: right; }
  footer {
    text-align: center;
    color: #5c6389;
    font-size: 13px;
    padding: 10px 16px 24px 16px;
  }
  .modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(4, 5, 12, 0.75);
    padding: 24px;
    overflow-y: auto;
    z-index: 50;
  }
  .modal-overlay.open { display: block; }
  .modal {
    max-width: 820px;
    margin: 0 auto;
    background: #151828;
    border: 1px solid #2c3152;
    border-radius: 14px;
    padding: 24px 28px;
  }
  .modal h2 { margin-top: 22px; font-size: 20px; color: #8f83f2; }
  .modal h2:first-child { margin-top: 0; }
  .modal p, .modal li { font-size: 15.5px; line-height: 1.6; color: #cdd2e6; }
  .modal .close-row { text-align: right; margin-bottom: 6px; }
  .modal code {
    background: #232741;
    padding: 2px 7px;
    border-radius: 6px;
    color: #62d0a5;
    font-size: 14px;
  }
  .modal table { border-collapse: collapse; width: 100%; font-size: 14.5px; color: #cdd2e6; }
  .modal td, .modal th { border: 1px solid #32385c; padding: 7px 10px; text-align: left; }
  .modal th { color: #aab1d0; }
</style>
</head>
<body>

<header>
  <h1>Le Jeu de la Vie <span>— et l'IA qui apprend</span></h1>
  <div class="header-buttons">
    <button type="button" id="btnHelp">Comment ça marche ?</button>
    <button type="button" id="btnFull">Plein écran</button>
  </div>
</header>

<div class="layout">
  <div>
    <div class="card">
      <div class="badge-row">
        <div class="badge" id="ruleBadge">B3 / S23</div>
        <div class="gen">Génération : <span id="genCount">0</span></div>
      </div>
      <canvas id="grid" width="1300" height="780"></canvas>
      <div class="toolbar">
        <button type="button" class="big" id="btnPlay">Lecture</button>
        <button type="button" id="btnStep">Pas à pas</button>
        <button type="button" id="btnGlider">Planeur</button>
        <button type="button" id="btnGun">Canon à planeurs</button>
        <button type="button" id="btnBlob">Amas aléatoire</button>
        <button type="button" class="danger" id="btnReset">Réinitialiser le jeu</button>
      </div>
      <p class="hint">Cliquez ou glissez sur la grille pour dessiner des cellules. La grille est un tore : les bords gauche et droit, haut et bas, sont reliés entre eux. Chaque cellule naît éclatante puis s'assombrit en vieillissant.</p>
    </div>

    <div class="card">
      <h2>L'intelligence artificielle qui apprend la règle</h2>
      <p style="font-size:15px; color:#cdd2e6; margin:0 0 4px 0;">
        Un petit réseau de neurones observe des milliers de situations, sans jamais connaître la règle.
        À force d'exemples et de corrections, il tente de la deviner, puis joue son propre jeu ci-dessous.
      </p>
      <div class="badge-row">
        <span class="gen">Règle à apprendre : <b id="aiRuleBadge" style="color:#62d0a5;">B3 / S23</b></span>
      </div>
      <div class="toolbar" style="margin-top:2px;">
        <button type="button" class="big" id="btnTrain">Entraîner le réseau</button>
        <button type="button" id="btnNetReset">Réinitialiser le réseau</button>
      </div>
      <div class="gauge-wrap">
        <div class="gauge"><div id="trainGauge"></div></div>
        <div class="train-stats">
          <span>Précision : <b id="trainPct">—</b></span>
          <span>Exemples vus : <b id="trainExamples">0</b></span>
        </div>
      </div>
      <div class="status" id="trainStatus"></div>
      <div class="warning" id="aiWarning">
        La règle a changé depuis le dernier entraînement. Le réseau doit être réentraîné pour la découvrir.
      </div>

      <div class="ai-grid-row">
        <div class="mini">
          <p>Règle programmée</p>
          <canvas id="canvasRule" width="560" height="378"></canvas>
        </div>
        <div class="mini">
          <p>Réseau de neurones</p>
          <canvas id="canvasAI" width="560" height="378"></canvas>
        </div>
      </div>
      <div class="toolbar">
        <button type="button" id="btnReseed">Relancer la comparaison</button>
      </div>
      <p class="legend">
        Le même motif de départ évolue à gauche selon la règle programmée, à droite selon les prédictions du réseau.
        Les cellules marquées en <span class="mistake">rouge</span> sont des erreurs du réseau sur la situation présente.
        Erreurs actuelles : <b id="mistakesCount">—</b>
      </p>
    </div>
  </div>

  <div>
    <div class="card">
      <h2>Les règles de l'univers</h2>
      <div class="field">
        <label for="selB">Naissance (B) — voisines nécessaires pour qu'une cellule apparaisse</label>
        <select id="selB">
          <option value="3">3 voisines (Conway)</option>
          <option value="2">2 voisines (explosif)</option>
          <option value="36">3 ou 6 voisines (HighLife)</option>
          <option value="3678">3, 6, 7 ou 8 voisines (Day &amp; Night)</option>
          <option value="1">1 voisine (invasion)</option>
        </select>
      </div>
      <div class="field">
        <label for="selS">Survie (S) — voisines nécessaires pour qu'une cellule persiste</label>
        <select id="selS">
          <option value="23">2 ou 3 voisines (Conway)</option>
          <option value="">Aucune (Graines)</option>
          <option value="12345">1 à 5 voisines (Labyrinthe)</option>
          <option value="34678">3, 4, 6, 7 ou 8 voisines (Day &amp; Night)</option>
          <option value="0123">0 à 3 voisines (envahissement)</option>
        </select>
      </div>
      <div class="field" style="margin-bottom:0;">
        <label for="selUnivers">Univers prédéfini — des règles célèbres en un clic</label>
        <select id="selUnivers">
          <option value="custom">Règle personnalisée</option>
          <option value="conway">Conway — B3/S23</option>
          <option value="highlife">HighLife — B36/S23</option>
          <option value="graines">Graines — B2/S—</option>
          <option value="labyrinthe">Labyrinthe — B3/S12345</option>
          <option value="daynight">Day &amp; Night — B3678/S34678</option>
        </select>
      </div>
    </div>

    <div class="card">
      <h2>Affichage</h2>
      <div class="field" style="margin-bottom:0;">
        <label for="selPalette">Palette de couleurs</label>
        <select id="selPalette">
          <option value="nebuleuse">Nébuleuse</option>
          <option value="aurores">Aurores</option>
          <option value="thermique">Thermique</option>
        </select>
      </div>
      <div class="field" style="margin-top:12px; margin-bottom:0;">
        <label for="speedRange">Vitesse de simulation</label>
        <div class="speed-row">
          <input type="range" id="speedRange" min="1" max="60" value="10" />
          <span id="speedLabel">10 pas / seconde</span>
        </div>
      </div>
    </div>

    <div class="card">
      <h2>À observer sur le stand</h2>
      <p style="font-size:15px; color:#cdd2e6; margin:0; line-height:1.6;">
        Un seul chiffre de la règle modifié, et le même monde devient une extinction, un chaos ou un labyrinthe.
        Puis demandez au réseau de réapprendre : personne ne lui donne la règle, il la déduit seul des exemples.
        Voilà toute la différence entre un programme écrit à la main et une intelligence artificielle entraînée.
      </p>
    </div>
  </div>
</div>

<footer>Animation conçue pour la Fête de la Science — fichier autonome, fonctionne intégralement hors ligne.</footer>

<div class="modal-overlay" id="modal">
  <div class="modal">
    <div class="close-row"><button type="button" class="big" id="btnCloseModal">Fermer</button></div>
    <h2>Le jeu de la vie</h2>
    <p>
      Inventé en 1970 par le mathématicien John Conway, le jeu de la vie est un « automate cellulaire » :
      un univers quadrillé où chaque case, appelée cellule, est soit vivante, soit morte. Aucun joueur :
      à chaque génération, chaque cellule regarde uniquement ses huit voisines et obéit à deux règles.
    </p>
    <ul>
      <li><b>Naissance (B)</b> : une cellule morte qui possède exactement le bon nombre de voisines vivantes devient vivante.</li>
      <li><b>Survie (S)</b> : une cellule vivante qui possède le bon nombre de voisines vivantes reste en vie ; sinon elle meurt.</li>
    </ul>
    <p>
      La règle classique de Conway s'écrit <code>B3/S23</code> : naissance avec 3 voisines, survie avec 2 ou 3.
      De ces deux règles minuscules naissent des structures étonnantes : planeurs qui voyagent, canons qui les
      produisent, naves qui se reproduisent. C'est l'émergence : du complexe sort du simple, sans aucune direction centrale.
    </p>

    <h2>Modifier les règles</h2>
    <p>
      Les trois menus de droite modifient les conditions de naissance, de survie, ou chargent un univers célèbre.
      La même grille de départ peut alors s'éteindre, exploser ou se figer en corridors. Quelques exemples :
    </p>
    <table>
      <tr><th>Univers</th><th>Règle</th><th>Comportement</th></tr>
      <tr><td>Conway</td><td><code>B3/S23</code></td><td>L'équilibre classique, ni mort ni explosion.</td></tr>
      <tr><td>HighLife</td><td><code>B36/S23</code></td><td>Contient un motif qui se réplique à l'identique.</td></tr>
      <tr><td>Graines</td><td><code>B2/S—</code></td><td>Aucune cellule ne survit : tout meurt, tout renaît, feu d'artifice permanent.</td></tr>
      <tr><td>Labyrinthe</td><td><code>B3/S12345</code></td><td>Le monde se structure en couloirs et parois.</td></tr>
      <tr><td>Day &amp; Night</td><td><code>B3678/S34678</code></td><td>Vie et mort sont symétriques : le negatif du monde suit la même règle.</td></tr>
    </table>

    <h2>L'IA qui apprend la règle</h2>
    <p>
      À droite de l'écran, un petit réseau de neurones est lâché dans cet univers sans connaître ses lois.
      Il ne reçoit jamais la règle. On lui montre uniquement des milliers de situations tirées au hasard —
      une cellule, ses huit voisines — et, après coup, la bonne réponse : la cellule naît ou meurt.
    </p>
    <ul>
      <li>Au début, il répond au hasard : précision d'environ 50 %, et son monde à lui est un chaos.</li>
      <li>À chaque exemple, il ajuste légèrement ses connexions. Regardez la précision grimper.</li>
      <li>Une fois entraîné, il joue son propre jeu. Ses erreurs sont marquées en rouge.</li>
    </ul>
    <p>
      Le message essentiel : la règle de gauche a été <b>programmée</b> par un humain, celle de droite a été
      <b>apprise</b> par la machine à partir d'exemples. C'est exactement le principe des IA modernes :
      elles ne reçoivent pas de règles, elles en déduisent à partir de données.
    </p>

    <h2>Couleurs et repères</h2>
    <ul>
      <li>Une cellule vient de naître : elle est éclatante. Plus elle vieillit, plus elle s'assombrit.</li>
      <li>Trois palettes sont proposées pour varier les présentations.</li>
      <li>Sur la grille du réseau, une cellule encadrée de rouge signale une erreur de prédiction.</li>
    </ul>
  </div>
</div>

<script>
'use strict';

/* ============================================================
   Configuration
============================================================ */
var COLS = 100, ROWS = 60, CELL = 13;     // grille principale (1300 x 780 px, natif FHD)
var ACOLS = 40, AROWS = 27, ACCELL = 14;  // grilles de comparaison IA (560 x 378 px)
var TPS_IA = 6;                            // pas par seconde des grilles IA

/* ============================================================
   Éléments du document
============================================================ */
function $(id) { return document.getElementById(id); }
var canvas = $('grid'), ctx = canvas.getContext('2d');
var canvasRule = $('canvasRule'), ctxRule = canvasRule.getContext('2d');
var canvasAI = $('canvasAI'), ctxAI = canvasAI.getContext('2d');

/* ============================================================
   Règle courante (B = naissance, S = survie)
============================================================ */
var ruleB = digitsToSet('3'), ruleS = digitsToSet('23');
var PRESETS = {
  conway:    { b: '3',    s: '23'    },
  highlife:  { b: '36',   s: '23'    },
  graines:   { b: '2',    s: ''      },
  labyrinthe:{ b: '3',    s: '12345' },
  daynight:  { b: '3678', s: '34678' }
};

function digitsToSet(str) {
  var s = new Set();
  for (var i = 0; i < str.length; i++) s.add(str.charCodeAt(i) - 48);
  return s;
}
function ruleNext(center, count) {
  if (center === 1) return ruleS.has(count) ? 1 : 0;
  return ruleB.has(count) ? 1 : 0;
}
function ruleLabel() {
  var b = sortedDigits(ruleB), s = sortedDigits(ruleS);
  return 'B' + b + ' / S' + (s === '' ? '—' : s);
}
function sortedDigits(set) {
  var out = [];
  set.forEach(function (d) { out.push(d); });
  out.sort(function (a, b) { return a - b; });
  return out.join('');
}

/* ============================================================
   Palettes (graphismes 100 % procéduraux)
============================================================ */
var paletteName = 'nebuleuse';
function palette() {
  switch (paletteName) {
    case 'aurores':
      return {
        bg: '#02100c', line: 'rgba(120,255,220,0.05)',
        hue: function (a) { return Math.min(205, 145 + a * 2.2); },
        light: function (a) { return Math.max(42, 70 - Math.min(a, 40) * 0.6); },
        sat: 95
      };
    case 'thermique':
      return {
        bg: '#0a0503', line: 'rgba(255,140,60,0.05)',
        hue: function (a) { return Math.max(5, 52 - a * 1.3); },
        light: function (a) { return Math.max(38, 78 - Math.min(a, 35) * 1.1); },
        sat: 100
      };
    default: // nébuleuse
      return {
        bg: '#08061a', line: 'rgba(180,160,255,0.05)',
        hue: function (a) { return Math.max(225, 310 - a * 2.6); },
        light: function (a) { return Math.max(38, 74 - Math.min(a, 35) * 0.9); },
        sat: 95
      };
  }
}

/* ============================================================
   Grille principale
============================================================ */
var grid = new Uint8Array(COLS * ROWS);
var age = new Uint16Array(COLS * ROWS);
var generation = 0;
var running = false;
var speed = 10;

function neighborCount(g, x, y, cols, rows) {
  var xm = (x - 1 + cols) % cols, xp = (x + 1) % cols;
  var ym = (y - 1 + rows) % rows, yp = (y + 1) % rows;
  return g[ym * cols + xm] + g[ym * cols + x] + g[ym * cols + xp]
       + g[y * cols + xm] + g[y * cols + xp]
       + g[yp * cols + xm] + g[yp * cols + x] + g[yp * cols + xp];
}

function stepGrid(g, ageArr, cols, rows) {
  var next = new Uint8Array(g.length);
  for (var y = 0; y < rows; y++) {
    for (var x = 0; x < cols; x++) {
      var i = y * cols + x;
      var c = g[i];
      var n = neighborCount(g, x, y, cols, rows);
      var alive = ruleNext(c, n);
      next[i] = alive;
      ageArr[i] = alive ? (c ? Math.min(ageArr[i] + 1, 600) : 1) : 0;
    }
  }
  return next;
}
function stepMain() {
  grid = stepGrid(grid, age, COLS, ROWS);
  generation++;
  $('genCount').textContent = generation;
}

/* Motifs préenregistrés */
var GLIDER = [[1,0],[2,1],[0,2],[1,2],[2,2]];
var GUN = [
  [0,4],[0,5],[1,4],[1,5],
  [10,4],[10,5],[10,6],[11,3],[11,7],[12,2],[12,8],[13,2],[13,8],
  [14,5],[15,3],[15,7],[16,4],[16,5],[16,6],[17,5],
  [20,2],[20,3],[20,4],[21,2],[21,3],[21,4],[22,1],[22,5],
  [24,0],[24,1],[24,5],[24,6],
  [34,2],[34,3],[35,2],[35,3]
];
function insertPattern(cells, cx, cy) {
  cells.forEach(function (p) {
    var x = (cx + p[0] + COLS) % COLS, y = (cy + p[1] + ROWS) % ROWS;
    grid[y * COLS + x] = 1;
    age[y * COLS + x] = 1;
  });
}
function insertBlob() {
  for (var dy = -6; dy <= 6; dy++) {
    for (var dx = -10; dx <= 10; dx++) {
      if (Math.random() < 0.35) {
        var x = (Math.floor(COLS / 2) + dx + COLS) % COLS;
        var y = (Math.floor(ROWS / 2) + dy + ROWS) % ROWS;
        grid[y * COLS + x] = 1;
        age[y * COLS + x] = 1;
      }
    }
  }
}

/* Dessin de la grille */
function drawBoard(context, g, ageArr, cols, rows, cell, mistakes) {
  var pal = palette();
  context.fillStyle = pal.bg;
  context.fillRect(0, 0, cols * cell, rows * cell);
  context.strokeStyle = pal.line;
  context.lineWidth = 1;
  context.beginPath();
  for (var x = 1; x < cols; x++) { context.moveTo(x * cell + 0.5, 0); context.lineTo(x * cell + 0.5, rows * cell); }
  for (var y = 1; y < rows; y++) { context.moveTo(0, y * cell + 0.5); context.lineTo(cols * cell, y * cell + 0.5); }
  context.stroke();

  // halo lumineux
  for (var i = 0; i < g.length; i++) {
    if (!g[i]) continue;
    var a = ageArr[i];
    var h = pal.hue(a), l = pal.light(a);
    context.fillStyle = 'hsla(' + h + ',' + pal.sat + '%,' + Math.min(l + 12, 92) + '%,0.30)';
    var px = (i % cols) * cell, py = Math.floor(i / cols) * cell;
    context.fillRect(px - 2, py - 2, cell + 3, cell + 3);
  }
  // cellules
  for (var j = 0; j < g.length; j++) {
    if (!g[j]) continue;
    var a2 = ageArr[j];
    context.fillStyle = 'hsl(' + pal.hue(a2) + ',' + pal.sat + '%,' + pal.light(a2) + '%)';
    var px2 = (j % cols) * cell, py2 = Math.floor(j / cols) * cell;
    context.fillRect(px2, py2, cell - 1, cell - 1);
  }
  // erreurs du réseau (grille IA uniquement)
  if (mistakes) {
    context.strokeStyle = '#ff4d6d';
    context.lineWidth = 1.6;
    for (var k = 0; k < mistakes.length; k++) {
      if (!mistakes[k]) continue;
      var px3 = (k % cols) * cell, py3 = Math.floor(k / cols) * cell;
      context.strokeRect(px3 + 0.5, py3 + 0.5, cell - 1.5, cell - 1.5);
    }
  }
}

/* ============================================================
   Réseau de neurones (JavaScript pur, aucune dépendance)
   9 entrées (cellule + 8 voisines) -> couche cachée (tanh) -> sortie (sigmoide)
============================================================ */
function Net(nin, nh) {
  this.nin = nin; this.nh = nh;
  this.W1 = []; this.b1 = new Float64Array(nh);
  for (var j = 0; j < nh; j++) {
    var r = new Float64Array(nin);
    for (var k = 0; k < nin; k++) r[k] = (Math.random() * 2 - 1) * 0.5;
    this.W1.push(r);
  }
  this.W2 = new Float64Array(nh);
  for (var j2 = 0; j2 < nh; j2++) this.W2[j2] = (Math.random() * 2 - 1) * 0.5;
  this.b2 = 0;
  this.h = new Float64Array(nh);
  this.out = 0;
  this.gW1 = []; this.gb1 = new Float64Array(nh);
  this.gW2 = new Float64Array(nh); this.gb2 = 0;
  for (var j3 = 0; j3 < nh; j3++) this.gW1.push(new Float64Array(nin));
}
Net.prototype.forward = function (x) {
  var h = this.h, nin = this.nin, nh = this.nh;
  for (var j = 0; j < nh; j++) {
    var s = this.b1[j], w = this.W1[j];
    for (var k = 0; k < nin; k++) s += w[k] * x[k];
    h[j] = Math.tanh(s);
  }
  var o = this.b2;
  for (var j2 = 0; j2 < nh; j2++) o += this.W2[j2] * h[j2];
  this.out = 1 / (1 + Math.exp(-o));
  return this.out;
};
Net.prototype.trainBatch = function (batch, lr) {
  var nin = this.nin, nh = this.nh;
  var gW1 = this.gW1, gb1 = this.gb1, gW2 = this.gW2, h = this.h;
  for (var z = 0; z < nh; z++) { gb1[z] = 0; gW2[z] = 0; for (var q = 0; q < nin; q++) gW1[z][q] = 0; }
  this.gb2 = 0;
  for (var s = 0; s < batch.length; s++) {
    var x = batch[s].x, y = batch[s].y;
    this.forward(x);
    var dz2 = this.out - y;
    for (var j = 0; j < nh; j++) {
      var dz1 = dz2 * this.W2[j] * (1 - h[j] * h[j]);
      gb1[j] += dz1;
      var gw = gW1[j];
      for (var k = 0; k < nin; k++) gw[k] += dz1 * x[k];
      gW2[j] += dz2 * h[j];
    }
    this.gb2 += dz2;
  }
  var m = batch.length;
  for (var j2 = 0; j2 < nh; j2++) {
    this.b1[j2] -= lr * gb1[j2] / m;
    var w1 = this.W1[j2], gw1 = gW1[j2];
    for (var k2 = 0; k2 < nin; k2++) w1[k2] -= lr * gw1[k2] / m;
    this.W2[j2] -= lr * gW2[j2] / m;
  }
  this.b2 -= lr * this.gb2 / m;
};

var net = new Net(9, 16);
var LR = 0.6;
var trainRunning = false;
var examplesSeen = 0;
var aiStale = false;
var valSet = [];

/* Génération d'exemples : situation aléatoire + réponse correcte selon la règle */
function makeSample() {
  var x = new Float64Array(9);
  x[0] = Math.random() < 0.5 ? 1 : 0;
  var count = 0;
  for (var k = 1; k <= 8; k++) {
    var v = Math.random() < 0.4 ? 1 : 0;
    x[k] = v;
    count += v;
  }
  return { x: x, y: ruleNext(x[0], count) };
}
function rebuildValSet() {
  valSet = [];
  for (var i = 0; i < 1024; i++) valSet.push(makeSample());
}
function netAccuracy() {
  var ok = 0;
  for (var i = 0; i < valSet.length; i++) {
    var s = valSet[i];
    var pred = net.forward(s.x) > 0.5 ? 1 : 0;
    if (pred === s.y) ok++;
  }
  return ok / valSet.length;
}
function trainFrame() {
  for (var b = 0; b < 3; b++) {
    var batch = [];
    for (var s = 0; s < 12; s++) batch.push(makeSample());
    net.trainBatch(batch, LR);
    examplesSeen += 12;
  }
  var acc = netAccuracy();
  $('trainGauge').style.width = (acc * 100).toFixed(1) + '%';
  $('trainPct').textContent = (acc * 100).toFixed(1).replace('.', ',') + ' %';
  $('trainExamples').textContent = examplesSeen.toLocaleString('fr-FR');
  if (acc >= 0.999) {
    trainRunning = false;
    $('btnTrain').textContent = 'Entraîner le réseau';
    $('trainStatus').textContent = 'Entraînement terminé : le réseau maîtrise la règle.';
  }
}

/* ============================================================
   Comparaison : règle programmée contre réseau appris
============================================================ */
var ruleGrid = new Uint8Array(ACOLS * AROWS);
var ruleAge = new Uint16Array(ACOLS * AROWS);
var aiGrid = new Uint8Array(ACOLS * AROWS);
var aiAge = new Uint16Array(ACOLS * AROWS);
var aiMistakes = new Uint8Array(ACOLS * AROWS);
var aiErrors = 0;
var netInput = new Float64Array(9);

function seedComparison() {
  var pattern = [];
  var cx = Math.floor(ACOLS / 2), cy = Math.floor(AROWS / 2);
  for (var dy = -5; dy <= 5; dy++) {
    for (var dx = -8; dx <= 8; dx++) {
      if (Math.random() < 0.35) pattern.push([cx + dx, cy + dy]);
    }
  }
  GLIDER.forEach(function (p) { pattern.push([cx - 14 + p[0], cy - 10 + p[1]]); });
  ruleGrid.fill(0); ruleAge.fill(0);
  aiGrid.fill(0); aiAge.fill(0); aiMistakes.fill(0);
  pattern.forEach(function (p) {
    var x = (p[0] + ACOLS) % ACOLS, y = (p[1] + AROWS) % AROWS;
    ruleGrid[y * ACOLS + x] = 1; ruleAge[y * ACOLS + x] = 1;
    aiGrid[y * ACOLS + x] = 1; aiAge[y * ACOLS + x] = 1;
  });
}
function stepRuleGrid() {
  ruleGrid = stepGrid(ruleGrid, ruleAge, ACOLS, AROWS);
}
function stepAIGrid() {
  var next = new Uint8Array(ACOLS * AROWS);
  aiMistakes.fill(0);
  aiErrors = 0;
  var x = netInput;
  for (var y = 0; y < AROWS; y++) {
    for (var v = 0; v < ACOLS; v++) {
      var i = y * ACOLS + v;
      var xm = (v - 1 + ACOLS) % ACOLS, xp = (v + 1) % ACOLS;
      var ym = (y - 1 + AROWS) % AROWS, yp = (y + 1) % AROWS;
      x[0] = aiGrid[i];
      x[1] = aiGrid[ym * ACOLS + xm]; x[2] = aiGrid[ym * ACOLS + xp];
      x[3] = aiGrid[y * ACOLS + xp];  x[4] = aiGrid[yp * ACOLS + xp];
      x[5] = aiGrid[yp * ACOLS + xm]; x[6] = aiGrid[yp * ACOLS + v];
      x[7] = aiGrid[y * ACOLS + xm];  x[8] = aiGrid[ym * ACOLS + v];
      var count = x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8];
      var ruleVal = ruleNext(x[0], count);
      var pred = net.forward(x) > 0.5 ? 1 : 0;
      if (pred !== ruleVal) { aiMistakes[i] = 1; aiErrors++; }
      next[i] = pred;
      aiAge[i] = pred ? (aiGrid[i] ? Math.min(aiAge[i] + 1, 600) : 1) : 0;
    }
  }
  aiGrid = next;
  $('mistakesCount').textContent = aiErrors;
}

/* ============================================================
   Boucle principale
============================================================ */
var last = 0, simAcc = 0, aiAcc = 0;
function loop(t) {
  var dt = last ? Math.min(t - last, 100) : 16;
  last = t;
  if (running) {
    simAcc += dt;
    var stepMs = 1000 / speed;
    var guard = 0;
    while (simAcc >= stepMs && guard < 30) { stepMain(); simAcc -= stepMs; guard++; }
    if (guard >= 30) simAcc = 0;
  }
  if (trainRunning) trainFrame();
  aiAcc += dt;
  while (aiAcc >= 1000 / TPS_IA) { stepRuleGrid(); stepAIGrid(); aiAcc -= 1000 / TPS_IA; }

  drawBoard(ctx, grid, age, COLS, ROWS, CELL, null);
  drawBoard(ctxRule, ruleGrid, ruleAge, ACOLS, AROWS, ACCELL, null);
  drawBoard(ctxAI, aiGrid, aiAge, ACOLS, AROWS, ACCELL, aiMistakes);
  requestAnimationFrame(loop);
}

/* ============================================================
   Dessin à la souris / au doigt
============================================================ */
var painting = false, paintValue = 1;
function cellFromEvent(e) {
  var rect = canvas.getBoundingClientRect();
  var sx = canvas.width / rect.width, sy = canvas.height / rect.height;
  var px = (e.clientX - rect.left) * sx, py = (e.clientY - rect.top) * sy;
  return {
    x: Math.floor(px / CELL),
    y: Math.floor(py / CELL),
    inside: px >= 0 && py >= 0 && px < canvas.width && py < canvas.height
  };
}
canvas.addEventListener('pointerdown', function (e) {
  e.preventDefault();
  var c = cellFromEvent(e);
  if (!c.inside) return;
  painting = true;
  paintValue = grid[c.y * COLS + c.x] ? 0 : 1;
  grid[c.y * COLS + c.x] = paintValue;
  age[c.y * COLS + c.x] = paintValue ? 1 : 0;
  canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointermove', function (e) {
  if (!painting) return;
  var c = cellFromEvent(e);
  if (!c.inside) return;
  grid[c.y * COLS + c.x] = paintValue;
  age[c.y * COLS + c.x] = paintValue ? 1 : 0;
});
canvas.addEventListener('pointerup', function () { painting = false; });
canvas.addEventListener('pointercancel', function () { painting = false; });

/* ============================================================
   Commandes
============================================================ */
function refreshBadges() {
  var label = ruleLabel();
  $('ruleBadge').textContent = label;
  $('aiRuleBadge').textContent = label;
}
function markStale() {
  aiStale = true;
  $('aiWarning').classList.add('visible');
  rebuildValSet();
}
function applyRuleChange(fromPresetKey) {
  if (fromPresetKey) {
    var p = PRESETS[fromPresetKey];
    ruleB = digitsToSet(p.b);
    ruleS = digitsToSet(p.s);
    $('selB').value = p.b;
    $('selS').value = p.s;
  } else {
    ruleB = digitsToSet($('selB').value);
    ruleS = digitsToSet($('selS').value);
    var key = null;
    Object.keys(PRESETS).forEach(function (k) {
      if (PRESETS[k].b === $('selB').value && PRESETS[k].s === $('selS').value) key = k;
    });
    $('selUnivers').value = key || 'custom';
  }
  refreshBadges();
  markStale();
}
$('selB').addEventListener('change', function () { applyRuleChange(null); });
$('selS').addEventListener('change', function () { applyRuleChange(null); });
$('selUnivers').addEventListener('change', function () {
  var v = $('selUnivers').value;
  if (v === 'custom') return;
  applyRuleChange(v);
});
$('selPalette').addEventListener('change', function () { paletteName = $('selPalette').value; });
$('speedRange').addEventListener('input', function () {
  speed = parseInt($('speedRange').value, 10);
  $('speedLabel').textContent = speed + ' pas / seconde';
});

$('btnPlay').addEventListener('click', function () {
  running = !running;
  $('btnPlay').textContent = running ? 'Pause' : 'Lecture';
});
$('btnStep').addEventListener('click', function () { stepMain(); });
$('btnGlider').addEventListener('click', function () {
  insertPattern(GLIDER, Math.floor(COLS / 2), Math.floor(ROWS / 2));
});
$('btnGun').addEventListener('click', function () {
  insertPattern(GUN, Math.floor(COLS / 2) - 18, Math.floor(ROWS / 2) - 4);
});
$('btnBlob').addEventListener('click', insertBlob);
$('btnReset').addEventListener('click', function () {
  grid.fill(0); age.fill(0); generation = 0;
  $('genCount').textContent = '0';
});

$('btnTrain').addEventListener('click', function () {
  trainRunning = !trainRunning;
  $('btnTrain').textContent = trainRunning ? 'Arrêter l\'entraînement' : 'Entraîner le réseau';
  if (trainRunning) {
    aiStale = false;
    $('aiWarning').classList.remove('visible');
    $('trainStatus').textContent = 'Le réseau observe des exemples et se corrige…';
  } else {
    $('trainStatus').textContent = 'Entraînement en pause.';
  }
});
$('btnNetReset').addEventListener('click', function () {
  trainRunning = false;
  net = new Net(9, 16);
  examplesSeen = 0;
  $('btnTrain').textContent = 'Entraîner le réseau';
  $('trainGauge').style.width = '0%';
  $('trainPct').textContent = '—';
  $('trainExamples').textContent = '0';
  $('trainStatus').textContent = 'Réseau réinitialisé : il ne sait plus rien.';
});
$('btnReseed').addEventListener('click', seedComparison);

$('btnHelp').addEventListener('click', function () { $('modal').classList.add('open'); });
$('btnCloseModal').addEventListener('click', function () { $('modal').classList.remove('open'); });
$('modal').addEventListener('click', function (e) {
  if (e.target === $('modal')) $('modal').classList.remove('open');
});
$('btnFull').addEventListener('click', function () {
  if (!document.fullscreenElement) {
    document.documentElement.requestFullscreen();
  } else {
    document.exitFullscreen();
  }
});

/* ============================================================
   Initialisation
============================================================ */
refreshBadges();
rebuildValSet();
insertBlob();
insertPattern(GLIDER, Math.floor(COLS / 2) - 20, Math.floor(ROWS / 2) - 10);
seedComparison();
running = true;
$('btnPlay').textContent = 'Pause';
requestAnimationFrame(loop);
</script> </body> </html>