Le but du jeu est de trier toutes les bulles par couleur.
Objectif : avoir chaque tube rempli d’une seule couleur.
| Boutons | Actions |
|---|---|
| Joystick | Dé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 |
/*******************************************************************************************************
*
* ██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ███████╗ ███████╗ ██████╗ ██████╗ ████████╗███████╗██████╗
* ██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔════╝ ██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
* ██████╔╝██║ ██║██████╔╝██████╔╝██║ █████╗ ███████╗██║ ██║██████╔╝ ██║ █████╗ ██████╔╝
* ██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔══╝ ╚════██║██║ ██║██╔══██╗ ██║ ██╔══╝ ██╔══██╗
* ██████╔╝╚██████╔╝██████╔╝██████╔╝███████╗███████╗ ███████║╚██████╔╝██║ ██║ ██║ ███████╗██║ ██║
* ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
*
* ------------------------------------------------------------------------------------------------------
* 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);
}