⟵ Retour

Bubble Sorter - mode d’emploi
pour shampouiner les poils de lamas

Principe du jeu

Le but du jeu est de trier toutes les bulles par couleur.

Objectif : avoir chaque tube rempli d’une seule couleur.

Commandes

BoutonsActions
JoystickDéplacement dans les quatre directions
Bouton (simple click)Sélectionner/Désélectionner
Bouton (double click)Reset niveau
Bouton (tripple click)Retour au niveau 1

Code du jeu


/*******************************************************************************************************
 *
 * ██████╗ ██╗   ██╗██████╗ ██████╗ ██╗     ███████╗    ███████╗ ██████╗ ██████╗ ████████╗███████╗██████╗ 
 * ██╔══██╗██║   ██║██╔══██╗██╔══██╗██║     ██╔════╝    ██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
 * ██████╔╝██║   ██║██████╔╝██████╔╝██║     █████╗      ███████╗██║   ██║██████╔╝   ██║   █████╗  ██████╔╝
 * ██╔══██╗██║   ██║██╔══██╗██╔══██╗██║     ██╔══╝      ╚════██║██║   ██║██╔══██╗   ██║   ██╔══╝  ██╔══██╗
 * ██████╔╝╚██████╔╝██████╔╝██████╔╝███████╗███████╗    ███████║╚██████╔╝██║  ██║   ██║   ███████╗██║  ██║
 * ╚═════╝  ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝    ╚══════╝ ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝╚═╝  ╚═╝
 *
 * ------------------------------------------------------------------------------------------------------
 *  BUBBLE SORTER – Jeu Arcade NeoPixel (Matrix 8×8)
 * ------------------------------------------------------------------------------------------------------
 *
 *  Auteur       : skuydi
 *  Année        : 09/2025
 *  Plateforme   : Arduino / compatible AVR
 *  Affichage    : Matrice LED NeoPixel 16×16
 *  Bibliothèque : Adafruit_NeoPixel
 *
 *  ------------------------------------------------------------
 *  DESCRIPTION
 *  ------------------------------------------------------------
 *  CE PROGRAMME = "Water Sort Puzzle" sur matrice NeoPixel 16x16
 * - Affiche 9 tubes (capacité 4 bulles)
 * - Le joueur déplace des bulles via joystick + bouton
 * - Génération de niveaux (1 → 40) avec difficulté, mélanges, pièges
 * - Double clic = reset du niveau identique (backup)
 * - Triple clic = regénérer un niveau différent au même numéro
 *
 *******************************************************************************************************/
 
#include <Adafruit_NeoPixel.h>

// ----------------------------------------------------
// HARDWARE
// ----------------------------------------------------

// Broche de la matrice NeoPixel 16x16
#define PIN_NEOPIXEL 6

// Broches du joystick (entrées analogiques)
const int joyXPin = A1;
const int joyYPin = A2;

// Bouton d'action (entrée digitale)
const int buttonPin = 2;

// Zone morte du joystick (évite le bruit au centre)
const int deadZone = 80;

// États actuels du joystick et du bouton
bool leftState = LOW;
bool rightState = LOW;
bool upState = LOW;
bool downState = LOW;
bool action = LOW;

// Locks (anti-répétition trop rapide)
// Principe : quand la direction est détectée, on "verrouille" jusqu'au relâchement.
bool lockLeft = false;
bool lockRight = false;
bool lockUp = false;
bool lockDown = false;
bool lockButton = false;

// États précédents (peu utilisés ici mais utiles si extension plus tard)
bool prevLeft = LOW;
bool prevRight = LOW;
bool prevUp = LOW;
bool prevDown = LOW;
bool prevAction = LOW;


// ----------------------------------------------------
// DOUBLE-CLICK / MULTI-CLICK
// ----------------------------------------------------

// Pour détecter simple / double / triple clic
unsigned long lastButtonPress = 0;

// Fenêtre de temps max entre 2 clics pour les considérer comme "successifs"
const unsigned long doubleClickDelay = 300; // 300 ms => restart de niveau si double appui

// Compteur de clics successifs
uint8_t clickCount = 0;


// ----------------------------------------------------
// Dimensions de la matrice
// ----------------------------------------------------
#define W 16
#define H 16
#define N (W*H)

// Objet NeoPixel (N pixels, pin, protocole GRB 800kHz)
Adafruit_NeoPixel strip(N, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);


// ----------------------------------------------------
// MATRIX BRIGHTNESS
// ----------------------------------------------------

// Luminosité globale (0-255). Plus bas = moins agressif + moins de conso.
const int BRIGHTNESS = 20;

// Facteur multiplicateur pour la force du mélange
// (dans ce code il est déclaré mais pas exploité directement partout)
uint16_t mixMultiplier = 3;


// ----------------------------------------------------
// MATRIX MAPPING (zigzag + rotations + miroirs)
// ----------------------------------------------------

// La plupart des matrices 16x16 "adressables" sont câblées en serpentin (zigzag).
#define MATRIX_ZIGZAG true

// ROTATION DE L’AFFICHAGE
// 0 = normal
// 1 = 90° horaire
// 2 = 180°
// 3 = 270° horaire
uint8_t matrixRotation = 0;

// Miroirs logiciels
bool mirrorH = false;   // miroir horizontal
bool mirrorV = true;    // miroir vertical


// ----------------------------------------------------
// CONFIG DU JEU
// ----------------------------------------------------
#define NUM_TUBES 9    // Nombre total de tubes
#define CAP 4          // Capacité de chaque tube (4 bulles)

// Coordonnées X / Y des tubes sur la matrice
// tubeX = colonne, tubeY = "base" du tube (le bas est dessiné à tubeY)
uint8_t tubeX[NUM_TUBES] = { 2,5,8,11,14, 3,6,9,12 };
uint8_t tubeY[NUM_TUBES] = { 7,7,7,7,7, 14,14,14,14 };

// Contenu des tubes : tubes[tube][niveau]
// Convention : index 0 = bas du tube (après compact), index CAP-1 = haut
uint8_t tubes[NUM_TUBES][CAP];

// Backup des tubes en cours (sert au reset niveau identique par double clic)
uint8_t tubesBackup[NUM_TUBES][CAP];

// Tube affiché/actif ou non (permet de "cacher" certains tubes selon difficulté)
bool tubeActive[NUM_TUBES];

// Sélection utilisateur
uint8_t selectedTube = 0;  // index tube sélectionné (0..NUM_TUBES-1)
bool holding = false;      // joueur tient une bulle ?
uint8_t heldColor = 0;     // couleur tenue
int fromTube = -1;         // tube d'origine (pour remettre si mouvement invalide)

// Effets visuels (phases de pulsation)
uint8_t pulsePhaseSlow = 0;
uint8_t pulsePhaseFast = 0;

// Progression
uint8_t currentLevel = 1;
uint32_t moves = 0;        // compteur de mouvements réussis


// ----------------------------------------------------
// IA DEMO AU DEMARRAGE
// ----------------------------------------------------
bool aiDemoMode = true;              // actif uniquement au lancement
unsigned long lastAIMove = 0;        // tempo entre deux mouvements IA
const unsigned long aiMoveDelay = 450; // vitesse de la démo (ms)


// ----------------------------------------------------
// STRUCTURE D’UN NIVEAU
// ----------------------------------------------------
struct LevelConfig {
  uint8_t colors;        // nb de couleurs (donc nb de tubes "à compléter")
  uint8_t trueEmpty;     // nb de tubes vides de départ (pour manœuvrer)
  uint16_t mixStrength;  // intensité du mélange (nombre d'opérations)
  bool chaos;            // mélange chaotique (sans règle) ou "safe" (respecte règles)
  uint8_t trapCount;     // nb de pièges
  uint8_t trapMode;      // type de piège 1→5
};


// ----------------------------------------------------
// Table des niveaux
// (levels[0] = niveau 1)
// ----------------------------------------------------
LevelConfig levels[] = {

  // ---- Débutant (1→5) ----
  {2, 2,   40, false, 0, 0},   // 1
  {2, 2,   60, false, 0, 0},   // 2
  {3, 2,   80, false, 0, 0},   // 3
  {3, 2,  100, false, 0, 0},   // 4
  {4, 2,  120, false, 0, 0},   // 5

  // ---- Initiation pièges (6→12) ----
  {4, 2,  150, false, 1, 1},   // 6 - 1 piège fixe → tube 0
  {4, 2,  180, false, 1, 2},   // 7 - 1 piège aléatoire → tube 0
  {4, 2,  200, false, 1, 1},   // 8
  {5, 2,  220, false, 1, 2},   // 9
  {5, 2,  260, false, 1, 1},   // 10
  {5, 2,  300, false, 1, 2},   // 11
  {5, 2,  330, false, 1, 2},   // 12

  // ---- Niveau intermédiaire (13→20) ----
  {5, 2,  340, false, 1, 3},   // 13 - 1 swap tube 0
  {6, 2,  370, false, 1, 3},   // 14
  {6, 2,  400, false, 1, 1},   // 15
  {6, 2,  420, false, 1, 3},   // 16
  {6, 2,  450, false, 1, 2},   // 17
  {6, 2,  480, false, 1, 3},   // 18
  {6, 2,  520, false, 2, 1},   // 19
  {6, 2,  550, false, 2, 2},   // 20

  // ---- Hard (21→30) ----
  {7, 2,  600, true,  1, 4},   // 21 - SWAP aléatoire entre tubes
  {7, 2,  650, true,  1, 4},   // 22
  {7, 2,  700, false, 2, 3},   // 23
  {7, 2,  750, true,  2, 4},   // 24
  {7, 2,  800, false, 2, 3},   // 25
  {7, 2,  850, true,  2, 4},   // 26
  {7, 2,  900, false, 2, 3},   // 27
  {7, 2,  950, true,  2, 4},   // 28
  {7, 2, 1000, false, 2, 3},   // 29
  {7, 2, 1050, true,  2, 4},   // 30

  // ---- Expert (31→40) ----
  {7, 2, 1100, true,  1, 5},   // 31 - inversion !
  {7, 2, 1150, true,  1, 5},   // 32
  {7, 2, 1200, true,  2, 5},   // 33
  {7, 1, 1300, true,  2, 5},   // 34
  {7, 1, 1500, true,  2, 4},   // 35
  {7, 1, 1600, true,  2, 5},   // 36
  {7, 1, 1700, true,  2, 5},   // 37
  {7, 1, 1800, true,  2, 4},   // 38
  {7, 1, 2000, true,  2, 5},   // 39
  {7, 1, 2200, true,  3, 4},   // 40 - Ultradifficile
};


// ----------------------------------------------------
// MAPPING XY → INDEX LED
// ----------------------------------------------------
/*
  Objectif : convertir (x,y) en index linéaire [0..N-1] pour strip.setPixelColor().
  - On applique d’abord miroirs et rotation si nécessaire (pour adapter le montage).
  - Ensuite on calcule l’index selon le câblage (zigzag / linéaire).
*/
uint16_t XY(uint8_t x, uint8_t y) {

  uint8_t rx = x;
  uint8_t ry = y;

  // MIRROIRS
  if (mirrorH) rx = (W - 1) - rx;
  if (mirrorV) ry = (H - 1) - ry;

  // ROTATION (0,90,180,270°)
  switch(matrixRotation) {
    case 1:
      // 90° horaire : (x,y) -> (y, W-1-x)
      rx = ry;
      ry = (W - 1 - x);
      break;

    case 2:
      // 180° : (x,y) -> (W-1-x, H-1-y)
      rx = (W - 1 - rx);
      ry = (H - 1 - ry);
      break;

    case 3:
      // 270° horaire : (x,y) -> (H-1-y, x)
      rx = (H - 1 - ry);
      ry = x;
      break;
  }

  // MODE ZIGZAG OU NON
  if (!MATRIX_ZIGZAG)
    return ry * W + rx;

  // Zigzag : lignes paires normales, impaires inversées
  if (ry % 2 == 0)
    return ry * W + rx;
  else
    return ry * W + (W - 1 - rx);
}


// Place une couleur sur la coordonnée XY (sécurisé: check bounds)
void setXY(uint8_t x, uint8_t y, uint32_t c){
  if(x < W && y < H)
    strip.setPixelColor(XY(x,y), c);
}


// Conversion code couleur (1..9) → RGB
// Remarque : les couleurs sont vives "brutes" (pas de gamma-correction ici)
uint32_t colorId(uint8_t c){
  switch(c){
    case 1: return strip.Color(255,0,0);     // rouge
    case 2: return strip.Color(0,255,0);     // vert
    case 3: return strip.Color(0,0,255);     // bleu
    case 4: return strip.Color(255,255,0);   // jaune
    case 5: return strip.Color(255,0,255);   // magenta
    case 6: return strip.Color(0,255,255);   // cyan
    case 7: return strip.Color(255,128,0);   // orange
    case 8: return strip.Color(128,255,0);   // jaune-vert
    case 9: return strip.Color(255,0,128);   // rose
  }
  return strip.Color(0,0,0);                 // noir = vide / éteint
}


// ----------------------------------------------------
// GAME LOGIC (manipulation des tubes)
// ----------------------------------------------------

// Trouve l'index de la bulle du haut (la dernière non nulle)
int8_t topIndex(uint8_t t){
  for(int8_t i = CAP-1; i >= 0; i--)
    if(tubes[t][i] != 0) return i;
  return -1; // tube vide
}

// Trouve un emplacement libre (premier 0 en partant du bas)
int8_t freeIndex(uint8_t t){
  for(uint8_t i=0; i= 0){
    holding = true;
    heldColor = tubes[t][idx];
    tubes[t][idx] = 0;
    compactTube(t);
    fromTube = t;
  }
}

// Dépose une bulle dans un tube
// Règles "Water Sort":
// - On ne peut déposer que si tube pas plein
// - On peut déposer si tube vide OU si couleur du haut == couleur déposée
// - Sinon mouvement invalide -> on remet dans tube d'origine
void dropBall(uint8_t t){

  // Empêcher dépôt dans un tube invisible (inactif)
  if (!tubeActive[t]) {
      // On remet la bulle dans le tube d'où elle vient
      tubes[fromTube][freeIndex(fromTube)] = heldColor;
      compactTube(fromTube);
      holding = false;
      return;
  }

  int8_t fr = freeIndex(t);

  // Si tube plein -> mouvement invalide -> remise dans tube d'origine
  if(fr < 0){
    tubes[fromTube][freeIndex(fromTube)] = heldColor;
    compactTube(fromTube);
    holding=false;
    return;
  }

  // Couleur au sommet du tube cible (0 si vide)
  int8_t top = topIndex(t);
  uint8_t tc = (top>=0)?tubes[t][top]:0;

  // Règle de dépôt
  if(tc == 0 || tc == heldColor){
    tubes[t][fr] = heldColor;
    compactTube(t);
    moves++; // mouvement réussi => compteur
  } else {
    // mouvement invalide => rollback
    tubes[fromTube][freeIndex(fromTube)] = heldColor;
    compactTube(fromTube);
  }

  holding = false;
}


// ----------------------------------------------------
// IA DEMO : mouvements valides aléatoires
// ----------------------------------------------------
void aiPlayRandomMove() {

  // tempo visuelle
  if (millis() - lastAIMove < aiMoveDelay) return;
  lastAIMove = millis();

  // tente plusieurs fois de trouver un mouvement valide
  for (uint8_t attempt = 0; attempt < 25; attempt++) {

    uint8_t from = random(NUM_TUBES);
    uint8_t to   = random(NUM_TUBES);

    if (from == to) continue;
    if (!tubeActive[from] || !tubeActive[to]) continue;

    int8_t topA = topIndex(from);
    if (topA < 0) continue; // tube vide

    int8_t freeB = freeIndex(to);
    if (freeB < 0) continue; // tube plein

    uint8_t colorA = tubes[from][topA];

    int8_t topB = topIndex(to);
    uint8_t colorB = (topB >= 0) ? tubes[to][topB] : 0;

    // règle water sort
    if (colorB != 0 && colorB != colorA) continue;

    // mouvement valide trouvé
    selectedTube = from;
    pickBall(from);

    selectedTube = to;
    dropBall(to);

    return;
  }
}


// Vérifie si toutes les couleurs sont triées :
// Un tube "complet" = non vide ET toutes les cases contiennent la même couleur (sans trous).
bool checkWin(){
  LevelConfig cfg = levels[0]; // (variable locale inutilisée ensuite; le vrai cfg est via getLevelConfig)

  uint8_t complete = 0;

  for(uint8_t t=0;t trou => invalide
        if(ref!=0) fullColor = false;
      }
    }

    if(!empty && fullColor) complete++;
  }

  // Pour gagner : nombre de tubes complets == nombre de couleurs du niveau
  return (complete == getLevelConfig(currentLevel).colors);
}


// Sélection du niveau par progression
// - lvl commence à 1
// - clamp au max si dépasse
LevelConfig getLevelConfig(uint8_t lvl) {
    // Nombre total de niveaux définis dans levels[]
    uint8_t maxLvl = sizeof(levels) / sizeof(levels[0]);

    // Sécurité : pas de niveau 0
    if (lvl == 0)
        lvl = 1;

    // Si le niveau dépasse la liste → rester au dernier niveau
    if (lvl > maxLvl)
        lvl = maxLvl;

    // Retourne le niveau correspondant (levels[0] = niveau 1)
    return levels[lvl - 1];
}

// Vide tous les tubes (met tout à 0)
void clearTubes(){
  for(uint8_t t=0;t= 0) {
            uint8_t colorB = tubes[B][topB];
            if (colorA != colorB) continue;
        }

        // Déplacement A -> B
        tubes[A][topA] = 0;
        compactTube(A);

        tubes[B][freeB] = colorA;
        compactTube(B);
    }
}

/*
  safeMixChaos = mélange chaotique :
  - Même principe A->B, mais on ignore la contrainte de couleur
  - Donc ça peut produire un état plus "sale" / difficile
*/
void safeMixChaos(uint16_t mixes) {
    for (uint16_t m = 0; m < mixes; m++) {

        uint8_t A = random(NUM_TUBES);
        uint8_t B = random(NUM_TUBES);
        if (A == B) continue;

        int8_t topA = topIndex(A);
        int8_t freeB = freeIndex(B);

        if (topA < 0) continue;
        if (freeB < 0) continue;
        if (topA != freeIndex(A) - 1) continue;

        uint8_t colorA = tubes[A][topA];

        tubes[A][topA] = 0;
        compactTube(A);

        tubes[B][freeB] = colorA;
        compactTube(B);
    }
}


// ----------------------------------------------------
// GENERATEUR DE NIVEAU
// ----------------------------------------------------
/*
  generateLevel(lvl) :
  1) Vide les tubes
  2) Active un certain nombre de tubes (couleurs + vides)
  3) Remplit les "tubes pleins" avec un pool de bulles randomisé
  4) Mélange (safe ou chaos)
  5) Applique éventuellement des pièges (trapMode)
  6) Sauvegarde l'état initial en backup (pour double clic)
*/
void generateLevel(uint8_t lvl){
  clearTubes();

  // Reset tubes actifs
  for (uint8_t i = 0; i < NUM_TUBES; i++)
      tubeActive[i] = false;

  LevelConfig cfg = getLevelConfig(lvl);

  uint8_t colors   = cfg.colors;
  uint8_t trueE    = cfg.trueEmpty;
  uint16_t mixes   = cfg.mixStrength;
  bool chaos       = cfg.chaos;
  uint8_t trapCnt  = cfg.trapCount;
  uint8_t trapMode = cfg.trapMode;

  // Nombre de tubes pleins (un tube par couleur)
  uint8_t fullTubes = colors;

  // Pool des bulles (max 36 → ici 40)
  // On stocke toutes les bulles avant de les distribuer.
  uint8_t pool[40];
  uint8_t idx = 0;

  // Génère les 4 bulles de chaque couleur
  for(uint8_t c = 1; c <= colors; c++)
    for(uint8_t i = 0; i < CAP; i++)
      pool[idx++] = c;

  // Mélange du pool (Fisher-Yates)
  for(int i = idx - 1; i > 0; i--){
    int j = random(i + 1);
    uint8_t tmp = pool[i];
    pool[i] = pool[j];
    pool[j] = tmp;
  }

  uint8_t t = 0;
  idx = 0;

  // Tubes pleins : on prend 4 bulles à la suite depuis pool[]
  for(; t < fullTubes; t++){
    for(uint8_t i = 0; i < CAP; i++)
      tubes[t][i] = pool[idx++];
    compactTube(t);
    tubeActive[t] = true;
  }

  // Tubes vides (mais actifs = visibles et jouables)
  for(uint8_t k = 0; k < trueE; k++, t++){
    tubeActive[t] = true;
  }

  // ---------------------------------------------------------
  // Mélange global avant les pièges
  // ---------------------------------------------------------
  if(chaos)
      safeMixChaos(mixes);
  else
      safeMix(mixes);

  // ---------------------------------------------------------
  // PIÈGES (trapMode 1 à 5)
  // ---------------------------------------------------------
  /*
    trapCnt = nombre de pièges à appliquer
    trapMode :
      1) tube fixe (1 puis 2) -> tube 0
      2) tube aléatoire -> tube 0
      3) swap top(tube X) <-> top(tube 0)
      4) swap top(A) <-> top(B) entre deux tubes aléatoires != 0
      5) inversion complète d'un tube aléatoire != 0
  */
  if (trapCnt > 0 && NUM_TUBES > 2) {

    for (uint8_t n = 0; n < trapCnt; n++) {

        int A = -1;
        int B = -1;

        // --------------------------------------------
        // trapMode 1 : tube fixe (1 puis 2) → tube 0
        // --------------------------------------------
        if (trapMode == 1) {
            A = 1 + n;
            if (A >= NUM_TUBES) continue;

            int8_t topA = topIndex(A);
            int8_t free0 = freeIndex(0);

            if (topA >= 0 && free0 >= 0) {
                uint8_t b = tubes[A][topA];
                tubes[A][topA] = 0;
                compactTube(A);

                tubes[0][free0] = b;
                compactTube(0);
            }
            continue;
        }

        // --------------------------------------------
        // trapMode 2 : tube ALÉATOIRE → tube 0
        // --------------------------------------------
        if (trapMode == 2) {
            for(uint8_t attempts = 0; attempts < 20; attempts++) {
                uint8_t tSel = random(1, NUM_TUBES);
                if (tubeActive[tSel] && topIndex(tSel) >= 0) {
                    A = tSel;
                    break;
                }
            }

            if (A < 0) continue;

            int8_t topA = topIndex(A);
            int8_t free0 = freeIndex(0);

            if (topA >= 0 && free0 >= 0) {
                uint8_t b = tubes[A][topA];
                tubes[A][topA] = 0;
                compactTube(A);

                tubes[0][free0] = b;
                compactTube(0);
            }
            continue;
        }

        // --------------------------------------------
        // trapMode 3 : SWAP top(A) ↔ top(tube 0)
        // --------------------------------------------
        if (trapMode == 3) {

            for(uint8_t attempts = 0; attempts < 20; attempts++) {
                uint8_t tSel = random(1, NUM_TUBES);
                if (tubeActive[tSel] &&
                    topIndex(tSel) >= 0 &&
                    topIndex(0) >= 0)
                {
                    A = tSel;
                    break;
                }
            }

            if (A < 0) continue;

            int8_t topA = topIndex(A);
            int8_t top0 = topIndex(0);

            uint8_t cA = tubes[A][topA];
            uint8_t c0 = tubes[0][top0];

            tubes[A][topA] = c0;
            tubes[0][top0] = cA;

            compactTube(A);
            compactTube(0);

            continue;
        }

        // --------------------------------------------
        // trapMode 4 : SWAP entre deux tubes aléatoires ≠ 0
        // --------------------------------------------
        if (trapMode == 4) {

            // Choix du tube A
            for(uint8_t attempts = 0; attempts < 20; attempts++) {
                uint8_t tSel = random(1, NUM_TUBES);
                if (tubeActive[tSel] && topIndex(tSel) >= 0) {
                    A = tSel;
                    break;
                }
            }

            // Choix du tube B
            for(uint8_t attempts = 0; attempts < 20; attempts++) {
                uint8_t tSel = random(1, NUM_TUBES);
                if (tSel != A &&
                    tubeActive[tSel] &&
                    topIndex(tSel) >= 0)
                {
                    B = tSel;
                    break;
                }
            }

            if (A < 0 || B < 0) continue;

            int8_t topA = topIndex(A);
            int8_t topB = topIndex(B);

            uint8_t cA = tubes[A][topA];
            uint8_t cB = tubes[B][topB];

            tubes[A][topA] = cB;
            tubes[B][topB] = cA;

            compactTube(A);
            compactTube(B);

            continue;
        }

        // --------------------------------------------
        // trapMode 5 : INVERSION d'un tube aléatoire ≠ 0
        // --------------------------------------------
        if (trapMode == 5) {

            // choisir un tube A
            for(uint8_t attempts = 0; attempts < 20; attempts++) {
                uint8_t tSel = random(1, NUM_TUBES);
                if (tubeActive[tSel] && topIndex(tSel) >= 0) {
                    A = tSel;
                    break;
                }
            }

            if (A < 0) continue;

            // recopier les bulles
            uint8_t temp[CAP];
            for (uint8_t i = 0; i < CAP; i++)
                temp[i] = tubes[A][i];

            // inversion (symétrie haut/bas)
            for (uint8_t i = 0; i < CAP; i++)
                tubes[A][i] = temp[CAP - 1 - i];

            compactTube(A);

            continue;
        }
    }
  }

  // Reset compteur de moves au démarrage du niveau
  moves = 0;

  // Sauvegarde INITIALE pour double clic (reset identique)
  for (uint8_t a = 0; a < NUM_TUBES; a++)
    for (uint8_t b = 0; b < CAP; b++)
      tubesBackup[a][b] = tubes[a][b];
}


// ----------------------------------------------------
// PULSE VISUEL
// ----------------------------------------------------

/*
  Ces fonctions génèrent une intensité (0..255) modulée dans le temps,
  afin de faire "respirer" des éléments UI (curseur, etc.).
*/

// Pulsation lente (note : l'expression (0) est toujours faux, donc la forme actuelle est asymétrique)
uint8_t pulseIntensitySlow() {
  // Génère une pulsation entre 100 et 255
  uint8_t p = (0) ? pulsePhaseSlow * 2 : (255 - (pulsePhaseSlow - 128) * 2);
  return map(p, 0, 255, 100, 200);
}

// Pulsation rapide (respiration 0→255→0)
uint8_t pulseIntensityFast() {
  uint8_t p = (pulsePhaseFast < 128) ? pulsePhaseFast * 2 : (255 - (pulsePhaseFast - 128) * 2);
  return map(p, 0, 255, 50, 200);
  //return p;
}


// ----------------------------------------------------
// PROGRESSION DES MOUVEMENTS (barre visuelle)
// ----------------------------------------------------
/*
  Idée : utiliser la colonne 0 comme un "compteur" vertical.
  Ici la fonction est prête, mais son appel est commenté dans drawTubes().
*/
void drawMovesProgress() {

  // Combien de leds allumer (modulo H)
  uint8_t leds = moves % H;

  // Phase de couleur selon tranches de 16 moves
  uint8_t phase = (moves / 16) % 4;
  uint32_t color;

  switch (phase) {
    case 0: color = strip.Color(0, 180, 0);   break;
    case 1: color = strip.Color(180, 150, 0); break;
    case 2: color = strip.Color(180, 0, 0);   break;
    case 3: color = strip.Color(150, 0, 150); break;
  }

  // Affichage dans la colonne 0, du bas vers le haut
  for (uint8_t i = 0; i < leds; i++) {
    setXY(0, H - 1 - i, color);
  }
}


// ----------------------------------------------------
// Gestion de la luminosité des LED (scaling)
// ----------------------------------------------------
/*
  Permet d'assombrir une couleur RGB en multipliant par un facteur [0..1] environ.
  Utile pour les éléments décoratifs (tubes) sans éclater les yeux.
*/
uint32_t scaleColor(uint32_t color, float factor) {
  uint8_t r = ((color >> 16) & 0xFF) * factor;
  uint8_t g = ((color >>  8) & 0xFF) * factor;
  uint8_t b = ((color      ) & 0xFF) * factor;

  // Clamp 0..255
  if (r > 255) r = 255;
  if (g > 255) g = 255;
  if (b > 255) b = 255;

  return strip.Color(r, g, b);
}


// ----------------------------------------------------
// AFFICHAGE DES TUBES SUR LA MATRICE
// ----------------------------------------------------
/*
  drawTubes() :
  - Efface l’écran
  - Dessine chaque tube actif :
      - une base (tubeBaseColor) au niveau tubeY[t]
      - 4 positions verticales au-dessus (CAP)
        - couleur de bulle si tubes[t][i] != 0
        - sinon "tubeGray"
  - Dessine le curseur de sélection + éventuellement la bulle tenue
  - strip.show() applique au hardware
*/
void drawTubes(){
  strip.clear();

  // Couleur "vide" à l'intérieur des tubes (gris sombre)
  uint32_t tubeGray = scaleColor(strip.Color(15,15,15), 0.9);

  for(uint8_t t = 0; t < NUM_TUBES; t++){

    // Si tube inactif => on ne le dessine pas
    if(!tubeActive[t])
        continue;

    uint8_t x = tubeX[t];
    uint8_t tubeBase = tubeY[t];

    // Couleur de la base du tube (teinte faible)
    uint32_t tubeBaseColor = scaleColor(strip.Color(30,80,80), 0.5);

    // Base du tube
    setXY(x, tubeBase, tubeBaseColor);

    // Les 4 emplacements de bulles (au-dessus de la base)
    for(uint8_t i = 0; i < CAP; i++){
      int8_t y = tubeBase - (i + 1);
      if (y < 0) continue;

      if (tubes[t][i] != 0)
        setXY(x, y, colorId(tubes[t][i]));
      else
        setXY(x, y, tubeGray);
    }
  }

  // Curseur de sélection (même si tube inactif : on montre où est le curseur)
  {
    uint8_t cx = tubeX[selectedTube];
    int8_t cy  = tubeY[selectedTube] - (CAP + 1); // au-dessus du tube
    if (cy < 0) cy = 0;

    int8_t p = pulseIntensityFast();
    setXY(cx, cy, strip.Color(p, 0, p));  // curseur pulsé violet

    // Si le joueur tient une bulle : l'afficher juste au-dessus du curseur
    if (holding) {
      int8_t hy = cy - 1;
      if (hy >= 0)
        setXY(cx, hy, colorId(heldColor));
    }
  }

  // Barre de moves prête mais désactivée
  //drawMovesProgress();

  strip.show();
}


// ----------------------------------------------------
// JOYSTICK + MULTI-CLICK
// ----------------------------------------------------
/*
  updateJoystick() :
  - Lit analogRead X/Y
  - Convertit en 4 directions via deadZone
  - Déplace selectedTube sur une grille implicite :
      - Gauche/Droite : +/- 1
      - Haut/Bas : +/- 5 (car layout 5 tubes en haut, 4 en bas)
  - Gère le bouton :
      - simple clic : pick/drop
      - double clic : reset exact via backup
      - triple clic : regenerate le niveau (nouveau mélange/pièges)
*/
void updateJoystick() {

  int x = analogRead(joyXPin);
  int y = analogRead(joyYPin);
  int center = 512; // valeur approx au repos (10 bits ADC)

/*
  leftState  = (x < center - deadZone);
  rightState = (x > center + deadZone);
  upState    = (y < center - deadZone);
  downState  = (y > center + deadZone);
*/

  // Sens du joystick INVERSE (volontaire)
  leftState  = (x > center + deadZone);   // inversion
  rightState = (x < center - deadZone);   // inversion
  upState    = (y > center + deadZone);   // inversion
  downState  = (y < center - deadZone);   // inversion

  // Bouton en INPUT_PULLUP => appuyé = LOW
  action = (digitalRead(buttonPin) == LOW);

  // Si joueur appuie pendant la démo → arrêt définitif de l’IA
  if (aiDemoMode && action) {
      aiDemoMode = false;
  }

  // -------------------------------
  // Déplacements (avec locks)
  // -------------------------------

  // Gauche = tube index - 1 (si possible)
  if (leftState && !lockLeft && selectedTube>0) {
    selectedTube--;
    lockLeft = true;
  }
  if (!leftState) lockLeft = false;

  // Droite = tube index + 1 (si possible)
  if (rightState && !lockRight && selectedTube=5) {
    selectedTube-=5;
    lockUp = true;
  }
  if (!upState) lockUp = false;

  // Bas = +5 (passage de rangée haute vers rangée basse)
  if (downState && !lockDown && selectedTube<=3) {
    selectedTube+=5;
    lockDown = true;
  }
  if (!downState) lockDown = false;

  // -------------------------------
  // GESTION SIMPLE / DOUBLE / TRIPLE CLIC
  // -------------------------------
  if (action && !lockButton) {

      unsigned long now = millis();

      // Si bouton pressé rapidement après le dernier => clic successif
      if (now - lastButtonPress < doubleClickDelay) {
          clickCount++;
      } else {
          clickCount = 1; // nouveau cycle
      }

      lastButtonPress = now;
      lockButton = true;

      // TRIPLE CLIC ?
      if (clickCount == 3) {

          // Nouveau niveau totalement différent (même numéro, nouvel état)
          generateLevel(currentLevel);

          // reset état joueur
          holding = false;
          fromTube = -1;
          moves = 0;

          clickCount = 0;  // reset compteur
          return;
      }

      // DOUBLE CLIC ?
      if (clickCount == 2) {

          // RESET DU NIVEAU IDENTIQUE (restauration exacte)
          for (uint8_t t = 0; t < NUM_TUBES; t++)
              for (uint8_t i = 0; i < CAP; i++)
                  tubes[t][i] = tubesBackup[t][i];

          holding = false;
          fromTube = -1;
          moves = 0;

          return;
      }

      // SIMPLE CLIC
      if (clickCount == 1) {
          if (!holding) pickBall(selectedTube);   // prendre
          else dropBall(selectedTube);           // déposer
      }
  }

  // Quand le bouton est relâché, on déverrouille
  if (!action) lockButton = false;

  // Petite tempo pour lisser les entrées
  delay(50);
}


// ----------------------------------------------------
// POLICE 5x7 ASCII 32 à 90 (COMPLÈTE ET ALIGNÉE)
// ----------------------------------------------------
/*
  font5x7 : tableau de bitmaps 5 colonnes par char, 7 pixels de haut.
  Chaque octet représente une colonne, bits 0..6 = pixels.
*/
const uint8_t font5x7[][5] = {

  // 32–47 (espace → /)
  {0x00,0x00,0x00,0x00,0x00}, // 32 space
  {0x00,0x00,0x5F,0x00,0x00}, // 33 !
  {0x00,0x07,0x00,0x07,0x00}, // 34 "
  {0x14,0x7F,0x14,0x7F,0x14}, // 35 #
  {0x24,0x2A,0x7F,0x2A,0x12}, // 36 $
  {0x23,0x13,0x08,0x64,0x62}, // 37 %
  {0x36,0x49,0x55,0x22,0x50}, // 38 &
  {0x00,0x05,0x03,0x00,0x00}, // 39 '
  {0x00,0x1C,0x22,0x41,0x00}, // 40 (
  {0x00,0x41,0x22,0x1C,0x00}, // 41 )
  {0x14,0x08,0x3E,0x08,0x14}, // 42 *
  {0x08,0x08,0x3E,0x08,0x08}, // 43 +
  {0x00,0x50,0x30,0x00,0x00}, // 44 ,
  {0x08,0x08,0x08,0x08,0x08}, // 45 -
  {0x00,0x60,0x60,0x00,0x00}, // 46 .
  {0x20,0x10,0x08,0x04,0x02}, // 47 /

  // 48–57 (0–9)
  {0x3E,0x51,0x49,0x45,0x3E}, // 48 0
  {0x00,0x42,0x7F,0x40,0x00}, // 49 1
  {0x42,0x61,0x51,0x49,0x46}, // 50 2
  {0x21,0x41,0x45,0x4B,0x31}, // 51 3
  {0x18,0x14,0x12,0x7F,0x10}, // 52 4
  {0x27,0x45,0x45,0x45,0x39}, // 53 5
  {0x3C,0x4A,0x49,0x49,0x30}, // 54 6
  {0x01,0x71,0x09,0x05,0x03}, // 55 7
  {0x36,0x49,0x49,0x49,0x36}, // 56 8
  {0x06,0x49,0x49,0x29,0x1E}, // 57 9

  // 58–64 ( : ; < = > ? @ )
  {0x00,0x36,0x36,0x00,0x00}, // 58 :
  {0x00,0x56,0x36,0x00,0x00}, // 59 ;
  {0x08,0x14,0x22,0x41,0x00}, // 60 <
  {0x14,0x14,0x14,0x14,0x14}, // 61 =
  {0x00,0x41,0x22,0x14,0x08}, // 62 >
  {0x02,0x01,0x51,0x09,0x06}, // 63 ?
  {0x3E,0x41,0x5D,0x59,0x4E}, // 64 @

  // 65–90 (A–Z)
  {0x7E,0x11,0x11,0x11,0x7E}, // 65 A
  {0x7F,0x49,0x49,0x49,0x36}, // 66 B
  {0x3E,0x41,0x41,0x41,0x22}, // 67 C
  {0x7F,0x41,0x41,0x22,0x1C}, // 68 D
  {0x7F,0x49,0x49,0x49,0x41}, // 69 E
  {0x7F,0x09,0x09,0x09,0x01}, // 70 F
  {0x3E,0x41,0x49,0x49,0x7A}, // 71 G
  {0x7F,0x08,0x08,0x08,0x7F}, // 72 H
  {0x00,0x41,0x7F,0x41,0x00}, // 73 I
  {0x20,0x40,0x41,0x3F,0x01}, // 74 J
  {0x7F,0x08,0x14,0x22,0x41}, // 75 K
  {0x7F,0x40,0x40,0x40,0x40}, // 76 L
  {0x7F,0x02,0x0C,0x02,0x7F}, // 77 M
  {0x7F,0x04,0x08,0x10,0x7F}, // 78 N
  {0x3E,0x41,0x41,0x41,0x3E}, // 79 O
  {0x7F,0x09,0x09,0x09,0x06}, // 80 P
  {0x3E,0x41,0x51,0x21,0x5E}, // 81 Q
  {0x7F,0x09,0x19,0x29,0x46}, // 82 R
  {0x46,0x49,0x49,0x49,0x31}, // 83 S
  {0x01,0x01,0x7F,0x01,0x01}, // 84 T
  {0x3F,0x40,0x40,0x40,0x3F}, // 85 U
  {0x1F,0x20,0x40,0x20,0x1F}, // 86 V
  {0x3F,0x40,0x38,0x40,0x3F}, // 87 W
  {0x63,0x14,0x08,0x14,0x63}, // 88 X
  {0x07,0x08,0x70,0x08,0x07}, // 89 Y
  {0x61,0x51,0x49,0x45,0x43}  // 90 Z
};


// ----------------------------------------------------
// AFFICHE UN CARACTÈRE 5x7
// ----------------------------------------------------
/*
  drawChar5x7(x, y, c, color):
  - c doit être dans [32..90]
  - On lit les 5 colonnes du bitmap
  - Pour chaque bit actif => on allume le pixel correspondant
*/
void drawChar5x7(int x, uint8_t y, char c, uint32_t color) {
  if (c < 32 || c > 90) return;
  const uint8_t* bitmap = font5x7[c - 32];

  for (uint8_t col = 0; col < 5; col++) {
    for (uint8_t row = 0; row < 7; row++) {
      if (bitmap[col] & (1 << row)) {
        setXY(x + col, y + row, color);
      }
    }
  }
}


// ----------------------------------------------------
// SCROLLING TEXTE SUR L'ÉCRAN
// ----------------------------------------------------
/*
  scrollText(msg, color, speed):
  - Affiche msg en défilement horizontal sur la matrice
  - Chaque caractère prend 6 colonnes (5 + 1 espace)
*/
void scrollText(const char* msg, uint32_t color, int speed) {
  int msgLen = strlen(msg);
  int totalWidth = msgLen * 6;

  for (int offset = W; offset > -totalWidth; offset--) {
    strip.clear();

    for (int i = 0; i < msgLen; i++) {
      int charX = offset + i * 6;
      if (charX > -6 && charX < W) {
        drawChar5x7(charX, 5, msg[i], color);
      }
    }

    strip.show();
    delay(speed);
  }
}


// ----------------------------------------------------
// AFFICHE LEVEL + MOVES (sur la matrice)
// ----------------------------------------------------
/*
  showLevelStats():
  - Construit une chaîne "LEVEL: X - MOVES: Y"
  - La fait défiler sur la matrice
*/
void showLevelStats() {
  char buf[32];
  sprintf(buf, "LEVEL: %u - MOVES: %u", currentLevel, moves);

  scrollText(buf, strip.Color(150, 150, 0), 40);
}


// ----------------------------------------------------
// ANIMATION
// ----------------------------------------------------
/*
  playAnimation_1():
  - Petite animation d'étincelles colorées
  - Puis un "fade" des pixels hors tubes
  - Utilisée au démarrage et quand on gagne un niveau
*/
void playAnimation_1(){
    LevelConfig cfg = getLevelConfig(currentLevel);

    // Phase étincelles
    for(int frame = 0; frame < 25; frame++){
        strip.clear();

        uint32_t tubeGray = strip.Color(113,113,13);   // Couleur tube - haut
        uint32_t tubeBase  = strip.Color(140,140,40);  // Couleur tube - bas

        // Redessine les tubes (ici sans tubeActive, pour l'effet visuel)
        for(uint8_t t = 0; t < NUM_TUBES; t++){
            uint8_t x = tubeX[t];
            uint8_t base = tubeY[t];

            setXY(x, base, tubeBase);

            for(uint8_t i = 0; i < CAP; i++){
                int8_t y = base - (i + 1);
                if(y < 0) continue;

                if(tubes[t][i] != 0)
                    setXY(x, y, colorId(tubes[t][i]));
                else
                    setXY(x, y, tubeGray);
            }
        }

        // Étincelles colorées aléatoires
        for(int n = 0; n < (N / 8); n++){
            int px = random(W);
            int py = random(H);

            uint8_t col = random(1, cfg.colors + 1);
            uint32_t color = colorId(col);

            // NOTE: ici l’index est calculé en "py*W+px" et pas via XY()
            // Donc si rotation/miroirs/zigzag sont utilisés, l'effet sera différent.
            strip.setPixelColor(py * W + px, color);
        }

        strip.show();
        delay(40);
    }

    // Effet de fondu : réduit les pixels hors tubes
    for(int f = 0; f < 5; f++){
        for(int i = 0; i < N; i++){
            uint32_t c = strip.getPixelColor(i);

            int y = i / W;
            int x = i % W;

            // Détecte si le pixel appartient à un tube (colonne tubeX et zone base/colonne)
            bool isTubePixel = false;
            for(uint8_t t = 0; t < NUM_TUBES; t++){
                if(x == tubeX[t]){
                    int8_t base = tubeY[t];
                    if(y == base) isTubePixel = true;
                    if(y < base && y >= base - CAP) isTubePixel = true;
                }
            }

            // On ne fade pas les pixels de tube (on garde leur lisibilité)
            if(isTubePixel) continue;

            // Sinon on réduit l'intensité (division par 2)
            uint8_t r = (c>>16)&0xFF;
            uint8_t g = (c>>8 )&0xFF;
            uint8_t b = (c    )&0xFF;
            strip.setPixelColor(i, r/2, g/2, b/2);
        }

        strip.show();
        delay(40);
    }
}


// ----------------------------------------------------
// MAIN SETUP
// ----------------------------------------------------
void setup(){
  // Initialise la librairie NeoPixel
  strip.begin();

  // Applique luminosité globale
  strip.setBrightness(BRIGHTNESS);

  // Bouton en pull-up interne : HIGH au repos, LOW quand appuyé
  pinMode(buttonPin, INPUT_PULLUP);

  // Init RNG (seed) : lecture analogique "flottante"
  randomSeed(analogRead(0));

  // Animation d'intro
  playAnimation_1();

  // Génère premier niveau
  generateLevel(currentLevel);
  delay(500);
}


// ----------------------------------------------------
// MAIN LOOP
// ----------------------------------------------------
void loop(){

    // Mise à jour pulsations UI
    pulsePhaseSlow++;
    pulsePhaseFast += 6;

    drawTubes(); // affichage

    // --------------------------------------------------
    // MODE DEMO IA AU DEMARRAGE
    // --------------------------------------------------
    if (aiDemoMode) {

        aiPlayRandomMove();   // l'IA joue seule
        updateJoystick();     // uniquement pour détecter appui bouton

        // Si joueur appuie → contrôle humain
        if (!aiDemoMode) return;

    } else {

        // Jeu normal joueur
        updateJoystick();
    }

    // --------------------------------------------------
    // CONDITION DE VICTOIRE
    // --------------------------------------------------
    if(checkWin()){
      showLevelStats();
      playAnimation_1();
      currentLevel++;
      generateLevel(currentLevel);
    }

    delay(20);
}