⟵ Retour

Acid Rain - mode d’emploi
pour décolorer les poils de lamas

Principe du jeu

Attraper les bons objets, éviter les pièges et réaliser des combos pour obtenir le meilleur score.

Commandes

ActionBouton
Déplacement à gaucheBouton gauche
Déplacement à droiteBouton droit
Reset high scoreBoutons gauche + droit au démarrage
Mode jour (luminosité++)Bouton gauche au démarrage

Score (objets, malus, erreurs et combos)

Objet Effet Malus Erreur Combo
🟢 +10 points -6 points (si raté) +1 (si raté) 3 → +15
🔵 +5 points -2 points (si raté) +1 (si raté) 3 → +10
🟡 +3 points -2 points (si raté) +1 (si raté) 3 → +8
🟣 Bonus Aucun -5 (si touché)
🔴 Danger -5 points (si touché) +1 (si touché) Annule
Wrap Aucun 0 3 wraps → +5

⚠️ Attention
Chaque pièce rouge touchée = 1 erreur, chaque autre pièce ratée (sauf violet) = 1 erreur
10 erreurs = GAME OVER

Galerie

Code du jeu


/******************************************************************************
 *   █████╗  ██████╗██╗██████╗     ██████╗  █████╗ ██╗███╗   ██╗
 *  ██╔══██╗██╔════╝██║██╔══██╗    ██╔══██╗██╔══██╗██║████╗  ██║
 *  ███████║██║     ██║██║  ██║    ██████╔╝███████║██║██╔██╗ ██║
 *  ██╔══██║██║     ██║██║  ██║    ██╔══██╗██╔══██║██║██║╚██╗██║
 *  ██║  ██║╚██████╗██║██████╔╝    ██║  ██║██║  ██║██║██║ ╚████║
 *  ╚═╝  ╚═╝ ╚═════╝╚═╝╚═════╝     ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝
 *
 * ---------------------------------------------------------------------------
 *  ACID RAIN – Jeu Arcade NeoPixel (Matrix 8×8)
 * ---------------------------------------------------------------------------
 *
 *  Auteur       : skuydi
 *  Année        : 11/2025
 *  Plateforme   : Arduino / compatible AVR
 *  Affichage    : Matrice LED NeoPixel 8×8
 *  Bibliothèque : Adafruit_NeoPixel
 *
 *  ------------------------------------------------------------
 *  DESCRIPTION
 *  ------------------------------------------------------------
 *  Jeu d’adresse sur matrice LED 8x8.
 *  Attrape les bons objets, évite les pièges,
 *  réalise des combos et des wraps pour marquer
 *  un maximum de points.

 *  ------------------------------------------------------------
 *  COMMANDES
 *  ------------------------------------------------------------
 *  - Bouton GAUCHE  : déplacement à gauche
 *  - Bouton DROIT   : déplacement à droite
 *  - Wrap possible (bord gauche ↔ bord droit)

 *  ------------------------------------------------------------
 *  MODE JOUR / NUIT
 *  ------------------------------------------------------------
 *  - Mode NUIT (par défaut)
 *    → Luminosité réduite

 *  - Mode JOUR
 *    → Maintenir le bouton GAUCHE appuyé
 *      au démarrage de la console

 *  ------------------------------------------------------------
 *  RESET DU HIGH SCORE
 *  ------------------------------------------------------------
 *  1. Éteindre la console
 *  2. Maintenir le bouton DROIT
 *  3. Allumer la console
 *  4. Garder appuyé ~2 secondes
 *  → Le meilleur score est réinitialisé

 *  ------------------------------------------------------------
 *  GAMEPLAY
 *  ------------------------------------------------------------
 *  - Objets colorés avec score et malus
 *  - Combos par couleur (x3)
 *  - Combo wrap : 3 passages bord à bord = +5 pts
 *  - Système d’erreurs (10 erreurs = Game Over)
 *  - High score sauvegardé en EEPROM

 *  ------------------------------------------------------------
 *  LICENCE
 *  ------------------------------------------------------------
 *  Projet OPEN SOURCE.
 *  Utilisation, modification et partage autorisés
 *  2025 – skuydi
 *  ============================================================ */

#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>

// =======================================================
// ================= CONFIGURATION =======================
// =======================================================

#define MATRIX_WIDTH 8
#define MATRIX_HEIGHT 8
bool matrixZigzag = true;

#define MAX_OBJECTS 5
#define BASE_SPEED 450  // Plus petit = plus rapide
#define SPEED_STEP 0.5    // Plus grand = progression plus rapide
#define MIN_SPEED 50    // Plus petit = plus rapide
#define WRAP_AROUND true

// ===== GAMEPLAY: OBJECTIFS / SACRIFICE =====
#define MIN_ACTIVE_OBJECTS     3   // Toujours au moins 3 objets "en jeu"
#define MIN_GAP_ROWS           2   // écart vertical minimum en LIGNES entre objets
#define SPAWN_ABOVE_MAX_ROWS   6   // objets peuvent démarrer jusqu'à 6 lignes au-dessus

// ===== POINTS & PENALITES =====
// Points gagnés quand on attrape
#define SCORE_GREEN        10
#define SCORE_BLUE         5  // 3
#define SCORE_YELLOW       3

// Pénalités de score
#define SCORE_MISS_GREEN   6  // 8
#define SCORE_MISS_NORMAL  2
#define SCORE_CATCH_RED    5

// Erreurs (missed)
#define ERROR_MISS_GREEN   1  //2
#define ERROR_MISS_NORMAL  1
#define ERROR_CATCH_RED    1  //2
#define ERROR_BONUS_PURPLE 3

#define MISS_LIMIT         10

// ===== TEXTE =====
#define TEXT_MIRROR_X 1
#define TEXT_MIRROR_Y 1

// ===== COMBOS =====
#define COMBO_MIN_COUNT  3   // à partir de combien on déclenche un combo

#define COMBO_GREEN_BONUS   10  // 15
#define COMBO_BLUE_BONUS    15  // 10
#define COMBO_YELLOW_BONUS  15  // 8
#define COMBO_PURPLE_BONUS  0

#define WRAP_COMBO_COUNT 3
#define WRAP_COMBO_SCORE 5

// ===== COMBO STATE =====
uint32_t lastComboColor = 0;
int comboCount = 0;
int wrapCombo = 0;

// ===== SCORE =====
#define SCORE_WRAP 2

// =======================================================
// ================= PINS / HARDWARE =====================
// =======================================================

const int matrixPin = 6;
const int leftButtonPin = 5;
const int rightButtonPin = 7;
const int ledCount = 64;

Adafruit_NeoPixel pixels(ledCount, matrixPin, NEO_GRB + NEO_KHZ800);

bool ROTATE_180 = true;

// =======================================================
// =============== Mode jour / nuit =======================
// =======================================================

#define BRIGHTNESS_DAY   50
#define BRIGHTNESS_NIGHT 10
bool isDayMode = false;

// =======================================================
// ======================= EEPROM =========================
// =======================================================

const int EE_ADDR_SIG = 0;
const int EE_ADDR_HS_L = 1;
const int EE_ADDR_HS_H = 2;
const int EE_ADDR_HS_INV_L = 3;
const int EE_ADDR_HS_INV_H = 4;
const uint8_t HS_SIGNATURE = 0xB6;

// =======================================================
// ======================== GAME =========================
// =======================================================
int missed = 0;
bool gameOver = false;

// =======================================================
// ======================== SCORE ========================
// =======================================================
int score = 0;
long highScore = 0;
const float SCORE_PER_LED = 20.0;

// ================= PLAYER (2 LEDs) =================
int playerLeft = 59;
int playerRight = 60;

// ================= TIMING =================
unsigned long lastMove = 0;
unsigned long lastFall = 0;
unsigned long lastSpawn = 0;

// ================= COLORS =================
uint32_t RED, YELLOW, BLUE, GREEN, PURPLE, CYAN;

// ================= OBJECT =================
struct FallingObject {
  int pos;          // peut être négatif (au-dessus de l'écran)
  uint32_t color;
  int points;
  bool active;
};

FallingObject objects[MAX_OBJECTS];

// =======================================================
// ======================== MATRIX ========================
// =======================================================

int mapIndex(int i){
  int x = i % 8;
  int y = i / 8;

  if(ROTATE_180){
    x = 7 - x;
    y = 7 - y;
  }

  return matrixZigzag
    ? (y % 2 == 0 ? y * 8 + x : y * 8 + (7 - x))
    : y * 8 + x;
}

void setPixelXY(int x,int y,uint32_t c){
  if(x<0||x>=8||y<0||y>=8) return;
  pixels.setPixelColor(mapIndex(y*8+x),c);
}

// =======================================================
// ======================== PLAYER ========================
// =======================================================

void clearPlayerRow(){
  for(int i=56;i<64;i++){
    pixels.setPixelColor(mapIndex(i), 0);
  }
}

void drawPlayer(){
  pixels.setPixelColor(mapIndex(playerLeft), GREEN);
  pixels.setPixelColor(mapIndex(playerRight), GREEN);
}

// =======================================================
// ======================== EEPROM ========================
// =======================================================

void saveHighScore(long hs){
  uint16_t v=(uint16_t)hs;
  uint16_t inv=~v;
  EEPROM.update(EE_ADDR_SIG,HS_SIGNATURE);
  EEPROM.update(EE_ADDR_HS_L,v&0xFF);
  EEPROM.update(EE_ADDR_HS_H,v>>8);
  EEPROM.update(EE_ADDR_HS_INV_L,inv&0xFF);
  EEPROM.update(EE_ADDR_HS_INV_H,inv>>8);
}

long loadHighScore(){
  if(EEPROM.read(EE_ADDR_SIG)!=HS_SIGNATURE) return 0;
  uint16_t v=EEPROM.read(EE_ADDR_HS_L)|(EEPROM.read(EE_ADDR_HS_H)<<8);
  uint16_t inv=EEPROM.read(EE_ADDR_HS_INV_L)|(EEPROM.read(EE_ADDR_HS_INV_H)<<8);
  if((uint16_t)~v!=inv) return 0;
  return v;
}

void resetHighScoreIfBootHeld(){
  delay(20);
  if(!digitalRead(leftButtonPin)){
    delay(200);
    if(!digitalRead(leftButtonPin)){
      saveHighScore(0);
      highScore=0;
      Serial.println(F("[HS] Reset au boot"));
    }
  }
}

void updateHighScore(){
  if(score>highScore){
    highScore=score;
    saveHighScore(highScore);
  }
}

// =======================================================
// =================== BRIGHTNESS MODE ====================
// =======================================================

void applyBrightnessByMode(){
  pixels.setBrightness(isDayMode?BRIGHTNESS_DAY:BRIGHTNESS_NIGHT);
}

void selectModeAtBootWithRightButton(){
  delay(20);
  if(!digitalRead(rightButtonPin)){
    delay(200);
    if(!digitalRead(rightButtonPin)){
      isDayMode=true;
      Serial.println(F("[MODE] Jour"));
    }
  }
  applyBrightnessByMode();
}

// =======================================================
// ======================== FONT ==========================
// =======================================================

const uint8_t DIGITS[10][5] PROGMEM={
 {0x3E,0x45,0x49,0x51,0x3E},
 {0x00,0x21,0x7F,0x01,0x00},
 {0x21,0x43,0x45,0x49,0x31},
 {0x22,0x41,0x49,0x49,0x36},
 {0x0C,0x14,0x24,0x7F,0x04},
 {0x72,0x51,0x51,0x51,0x4E},
 {0x3E,0x49,0x49,0x49,0x06},
 {0x40,0x47,0x48,0x50,0x60},
 {0x36,0x49,0x49,0x49,0x36},
 {0x30,0x49,0x49,0x49,0x3E}
};

const uint8_t GLYPH_S[5] PROGMEM = {0x32, 0x49, 0x49, 0x49, 0x26};
const uint8_t GLYPH_H[5] PROGMEM = {0x7F, 0x08, 0x08, 0x08, 0x7F};
const uint8_t GLYPH_COLON[5] PROGMEM = {0x00, 0x36, 0x36, 0x00, 0x00};

// =======================================================
// ======================== TEXT ==========================
// =======================================================

void scrollText(String txt,uint32_t color){
  for(int offset=8;offset>-(int)txt.length()*6;offset--){
    pixels.clear();
    for(int i=0;i='0'&&c<='9') g=DIGITS[c-'0'];
      else if(c=='S') g=GLYPH_S;
      else if(c=='H') g=GLYPH_H;
      else if(c==':') g=GLYPH_COLON;
      else continue;

      for(int col=0;col<5;col++){
        uint8_t line=pgm_read_byte(&g[col]);
        for(int row=0;row<7;row++){
          if(line&(1<= 0) return p / 8;
  return -(((-p) + 7) / 8);
}

int rowOfPos(int p){
  return floorDiv8(p);
}

void flashPurple() {
  for(int i = 0; i < 64; i++){
    pixels.setPixelColor(i, PURPLE);
  }
  pixels.show();
  delay(60);

  pixels.clear();
  pixels.show();
}

void flashColor(uint32_t color, int times = 1) {
  for(int i = 0; i < times; i++){
    for(int j = 0; j < 64; j++){
      pixels.setPixelColor(j, color);
    }
    pixels.show();
    delay(70);

    pixels.clear();
    pixels.show();
    delay(70);
  }
}

// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// >>>>>>>>>>>> AJOUT : FLASH JAUNE (HS battu) <<<<<<<<<<<
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
void flashYellow() {
  for(int i = 0; i < 64; i++){
    pixels.setPixelColor(i, YELLOW);
  }
  pixels.show();
  delay(80);

  pixels.clear();
  pixels.show();
  delay(80);
}

// =======================================================
// ====== AFFICHAGE DU SCORE EN DEGRADÉ ANIMÉ ============
// =======================================================

void displayScoreGradient() {
  pixels.clear();

  const int MAX_LEDS = MATRIX_WIDTH*MATRIX_HEIGHT;
  const uint8_t COLOR_INTENSITY = 180;

  int leds = (int)(score / SCORE_PER_LED);
  if(leds > MAX_LEDS) leds = MAX_LEDS;

  int lit = 0;

  for(int y = 0; y < 8 && lit < leds; y++){
    for(int x = 0; x < 8 && lit < leds; x++){

      float t = (leds <= 1) ? 0.0f : (float)lit / (float)(leds - 1);

      uint8_t r = (uint8_t)((1.0f - t) * COLOR_INTENSITY);
      uint8_t g = (uint8_t)(t * COLOR_INTENSITY);

      setPixelXY(x, y, pixels.Color(r, g, 0));
      pixels.show();
      delay(35);

      lit++;
    }
  }
}

// =======================================================
// ==================== GAME LOGIC =======================
// =======================================================

void spawnObject(int i){
  // Spawn avec décalage vertical (pos négatives) + contrainte d'écart vertical en lignes
  const int maxTries = 80;

  for(int t=0; t highScore);

  updateHighScore();

  // === FLASH ROUGE (GAME OVER) ===
  for(int i=0;i<3;i++){
    pixels.clear();
    for(int j=0;j<8;j++){
      pixels.setPixelColor(mapIndex(j*9), RED);
      pixels.setPixelColor(mapIndex(7 + j*7), RED);
    }
    pixels.show();
    delay(150);
    pixels.clear();
    pixels.show();
    delay(150);
  }

  delay(500);

  // === FLASH JAUNE SI NOUVEAU RECORD ===
  if(hsBeaten){
    for(int i=0;i<3;i++){
      flashYellow();
    }
  }

  delay(400);

  // === AFFICHAGE DU SCORE ===
  scrollText("S:" + String(score), GREEN);

  // HS jaune si battu, sinon couleur normale
  uint32_t hsColor = hsBeaten ? YELLOW : GREEN;
  scrollText("HS:" + String(highScore), hsColor);

  // >>> AJOUT ICI <<<
  displayScoreGradient();

  // Attente d'un bouton pour recommencer
  while(true){
    if(!digitalRead(leftButtonPin) || !digitalRead(rightButtonPin)){
      delay(250);
      resetGame();
      return;
    }
  }
}

void moveObjects(){
  for(int i=0;i= 0 && objects[i].pos < 64){
      pixels.setPixelColor(mapIndex(objects[i].pos), 0);
    }

    objects[i].pos += 8;

    // objet sorti de l'écran
    if(objects[i].pos >= 64){

      if(objects[i].color == RED){
        // éviter rouge = OK (pas d'erreur, pas de score)
      }
      else if(objects[i].color == GREEN){
        missed += ERROR_MISS_GREEN;
        score -= SCORE_MISS_GREEN;
      }
      else if(objects[i].color == PURPLE){
        // rater violet = pas grave
      }
      else{
        missed += ERROR_MISS_NORMAL;
        score -= SCORE_MISS_NORMAL;
      }

      if(score < 0) score = 0;

      if(missed >= MISS_LIMIT){
        gameOver = true;
        gameOverScreen();
      }

      objects[i].active = false;
      continue;
    }

    // dessiner l'objet seulement si visible
    if(objects[i].pos >= 0 && objects[i].pos < 64){
      pixels.setPixelColor(mapIndex(objects[i].pos), objects[i].color);

      // collision joueur
      if(objects[i].pos == playerLeft || objects[i].pos == playerRight){

        if(objects[i].color == RED){
          missed += ERROR_CATCH_RED;
          score -= SCORE_CATCH_RED;
        }
        else if(objects[i].color == PURPLE){
          missed = missed - ERROR_BONUS_PURPLE;
          //if(missed < 0) missed = 0;
          flashPurple();
        }
        else{
          
// === COMBO LOGIC ===
if(objects[i].color == GREEN ||
   objects[i].color == BLUE  ||
   objects[i].color == YELLOW){

  // même couleur → on continue le combo
  if(objects[i].color == lastComboColor){
    comboCount++;
  }
  // couleur différente → reset puis nouveau départ
  else{
    comboCount = 1;
    lastComboColor = objects[i].color;
  }

  score += objects[i].points;

  // déclenchement du combo
  if(comboCount >= COMBO_MIN_COUNT){
    int bonus = 0;

    if(objects[i].color == GREEN)  bonus = COMBO_GREEN_BONUS;
    else if(objects[i].color == BLUE)   bonus = COMBO_BLUE_BONUS;
    else if(objects[i].color == YELLOW) bonus = COMBO_YELLOW_BONUS;

    score += bonus;
    flashColor(objects[i].color);

    // reset après combo
    comboCount = 0;
    lastComboColor = 0;
    wrapCombo = 0;
  }
}
else {
  // Rouge ou Violet → pas de combo + reset
  score += objects[i].points;
  comboCount = 0;
  lastComboColor = 0;
  wrapCombo = 0;
}

      }

        if(score < 0) score = 0;

        if(missed >= MISS_LIMIT){
          gameOver = true;
          gameOverScreen();
        }

        objects[i].active = false;
      }
    }
  }

  drawPlayer();
  pixels.show();
}

void trySpawn(){
  // Progression simple : 3 au début, puis 4, puis 5
  int maxActive = 3;
  if(score > 15) maxActive = 4;
  if(score > 50) maxActive = 5;
  if(maxActive > MAX_OBJECTS) maxActive = MAX_OBJECTS;

  int activeCount = 0;
  for(int i=0;i= MAX_OBJECTS) break;
  }

  // 2) progression jusqu'à maxActive
  if(activeCount >= maxActive) return;

  for(int i=0;i140){
  lastMove=now;

  int oldCol = playerLeft - 56;

  clearPlayerRow();

  int rel = (oldCol + 7) % 8;
  playerLeft = WRAP_AROUND ? 56 + rel : max(playerLeft - 1, 56);
  playerRight = 56 + ((playerLeft - 56 + 1) % 8);

  // WRAP GAUCHE → DROITE
  if(oldCol == 0 && rel == 7){
    wrapCombo++;

    if(wrapCombo >= WRAP_COMBO_COUNT){
      score += WRAP_COMBO_SCORE;
      flashColor(CYAN);
      wrapCombo = 0;
    }
  }

  drawPlayer();
  pixels.show();
}

if(!digitalRead(rightButtonPin)&&now-lastMove>140){
  lastMove=now;

  int oldCol = playerLeft - 56;

  clearPlayerRow();

  int rel = (oldCol + 1) % 8;
  playerLeft = WRAP_AROUND ? 56 + rel : min(playerLeft + 1, 62);
  playerRight = 56 + ((playerLeft - 56 + 1) % 8);

  // WRAP DROITE → GAUCHE
  if(oldCol == 7 && rel == 0){
    wrapCombo++;

    if(wrapCombo >= WRAP_COMBO_COUNT){
      score += WRAP_COMBO_SCORE;
      flashColor(CYAN);
      wrapCombo = 0;
    }
  }

  drawPlayer();
  pixels.show();
}

  float fallSpeed=BASE_SPEED-score*SPEED_STEP;
  if(fallSpeedfallSpeed){
    lastFall=now;
    moveObjects();
  }

  // Spawn un peu plus fréquent pour maintenir plusieurs objets visibles
  if(now-lastSpawn>450){
    lastSpawn=now;
    trySpawn();
  }
}