Attraper les bons objets, éviter les pièges et réaliser des combos pour obtenir le meilleur score.
| Action | Bouton |
|---|---|
| Déplacement à gauche | Bouton gauche |
| Déplacement à droite | Bouton droit |
| Reset high score | Boutons gauche + droit au démarrage |
| Mode jour (luminosité++) | Bouton gauche au démarrage |
| 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



/******************************************************************************
* █████╗ ██████╗██╗██████╗ ██████╗ █████╗ ██╗███╗ ██╗
* ██╔══██╗██╔════╝██║██╔══██╗ ██╔══██╗██╔══██╗██║████╗ ██║
* ███████║██║ ██║██║ ██║ ██████╔╝███████║██║██╔██╗ ██║
* ██╔══██║██║ ██║██║ ██║ ██╔══██╗██╔══██║██║██║╚██╗██║
* ██║ ██║╚██████╗██║██████╔╝ ██║ ██║██║ ██║██║██║ ╚████║
* ╚═╝ ╚═╝ ╚═════╝╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝
*
* ---------------------------------------------------------------------------
* 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();
}
}