Code du jeu
/******************************************************************************
* ███╗ ███╗███████╗████████╗███████╗ ██████╗ ██████╗
* ████╗ ████║██╔════╝╚══██╔══╝██╔════╝██╔═══██╗██╔══██╗
* ██╔████╔██║█████╗ ██║ █████╗ ██║ ██║██████╔╝
* ██║╚██╔╝██║██╔══╝ ██║ ██╔══╝ ██║ ██║██╔══██╗
* ██║ ╚═╝ ██║███████╗ ██║ ███████╗╚██████╔╝██║ ██║
* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝
*
* ---------------------------------------------------------------------------
* METEOR – 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 DU JEU
* ---------------------------------------------------------------------------
* - Le joueur contrôle une barre de 2 pixels en bas de l’écran
* - Des météores tombent du ciel
* - Le joueur doit les éviter
* - Les bonus bleus donnent une vie + du score
* - La difficulté augmente progressivement
* - Le meilleur score est sauvegardé en EEPROM
*
* ---------------------------------------------------------------------------
* COMMANDES
* ---------------------------------------------------------------------------
* Bouton GAUCHE → Déplacement à gauche
* Bouton DROIT → Déplacement à droite
*
* Au démarrage :
* - Bouton DROIT maintenu → Mode JOUR
* - Bouton GAUCHE maintenu → Reset du High Score
*
* ---------------------------------------------------------------------------
* RÈGLES DE JEU
* ---------------------------------------------------------------------------
* +1 point : météore évité
* +10 points : passage par les bords (wrap)
* +10 points +1 vie : bonus bleu
* -1 vie : collision
* 0 vie : GAME OVER
*
* ---------------------------------------------------------------------------
* FONCTIONNALITÉS
* ---------------------------------------------------------------------------
* ✔ Mapping matriciel intelligent (rotation / miroir)
* ✔ RNG renforcé (anti-pattern)
* ✔ Sauvegarde EEPROM sécurisée
* ✔ Effets sonores
* ✔ Défilement du score
* ✔ Difficulté progressive
* ✔ Mode jour / nuit
*
* ---------------------------------------------------------------------------
* BROCHAGE
* ---------------------------------------------------------------------------
* MATRICE LED : Pin 6
* BOUTON GAUCHE : Pin 5
* BOUTON DROIT : Pin 7
* BUZZER : Pin 9
*
* ---------------------------------------------------------------------------
* LICENCE
* ---------------------------------------------------------------------------
* Projet open source
* inspiré par : https://github.com/B1T3X/ArduinoStuff/blob/main/SpaceInvaderThingy
* Utilisation, modification et partage autorisés
* 2025 – skuydi
*
*****************************************************************************/
#include
#include
// =====================================================================================
// Compatibilité PROGMEM (AVR / autres architectures)
// =====================================================================================
#if defined(ARDUINO_ARCH_AVR)
#include
#else
#ifndef PROGMEM
#define PROGMEM
#endif
#ifndef pgm_read_byte
#define pgm_read_byte(addr) (*(const unsigned char *)(addr))
#endif
#endif
// =====================================================================================
// Anti-conflits de macros WIDTH / HEIGHT (souvent définies ailleurs)
// =====================================================================================
#ifdef WIDTH
#undef WIDTH
#endif
#ifdef HEIGHT
#undef HEIGHT
#endif
// =====================================================================================
// DEBUG série (optionnel)
// =====================================================================================
// 0 = désactivé
// 1 = affiche score et vies sans spam
#define SERIAL_DEBUG_SCORE_LIVES 0
// =====================================================================================
// Configuration matrice
// =====================================================================================
#define WIDTH 8
#define HEIGHT 8
// 0 = câblage linéaire
// 1 = câblage serpentin (zigzag)
#define WIRING_ZIGZAG 1
// =====================================================================================
// Rotation / miroir matériel
// Permet d’adapter le code à n’importe quel sens de montage physique
// =====================================================================================
#define MATRIX_ROTATION 180 // 0 / 90 / 180 / 270
#define MATRIX_MIRROR_X 0
#define MATRIX_MIRROR_Y 0
// =====================================================================================
// Gameplay
// =====================================================================================
// 0 = bords bloquants
// 1 = wrap horizontal (le joueur traverse)
#define PLAYER_WRAP_EDGES 1
// Points bonus lors d’un wrap complet
#define WRAP_WRAP_SCORE_BONUS 10
// =====================================================================================
// Texte
// =====================================================================================
#define SCORE_SCROLL_SPEED_MS 90
#define TEXT_ROT_180 1 // rotation du texte indépendamment du mapping LED
// =====================================================================================
// Vies & bonus
// =====================================================================================
#define MAX_LIVES 2
#define BONUS_EVERY_SCORE 15
// =====================================================================================
// Son
// =====================================================================================
#define SOUND_ENABLED 1
// =====================================================================================
// Brochage matériel
// =====================================================================================
const uint8_t MATRIX_PIN = 6;
const uint16_t LED_COUNT = WIDTH * HEIGHT;
const uint8_t LEFT_BTN_PIN = 5;
const uint8_t RIGHT_BTN_PIN = 7;
const uint8_t BUZZER_PIN = 9;
Adafruit_NeoPixel pixels(LED_COUNT, MATRIX_PIN, NEO_GRB + NEO_KHZ800);
// =====================================================================================
// Sons (désactivables globalement)
// =====================================================================================
void playStartSound(){
#if SOUND_ENABLED
tone(BUZZER_PIN,523,100); delay(130);
tone(BUZZER_PIN,659,100); delay(130);
tone(BUZZER_PIN,784,120); delay(150);
noTone(BUZZER_PIN);
#endif
}
void playLoseSound(){
#if SOUND_ENABLED
tone(BUZZER_PIN,784,120); delay(150);
tone(BUZZER_PIN,659,120); delay(150);
tone(BUZZER_PIN,523,180); delay(210);
noTone(BUZZER_PIN);
#endif
}
void playNewRecordSound(){
#if SOUND_ENABLED
tone(BUZZER_PIN,523,90); delay(115);
tone(BUZZER_PIN,659,90); delay(115);
tone(BUZZER_PIN,784,90); delay(115);
tone(BUZZER_PIN,988,160); delay(190);
noTone(BUZZER_PIN);
#endif
}
void playLifeSound(){
#if SOUND_ENABLED
tone(BUZZER_PIN,988,120); delay(140);
noTone(BUZZER_PIN);
#endif
}
// =====================================================================================
// Mode jour / nuit (luminosité)
// =====================================================================================
#define BRIGHTNESS_DAY 120
#define BRIGHTNESS_NIGHT 15
bool isDayMode = false;
// =====================================================================================
// Couleurs
// =====================================================================================
const uint8_t COLOR_INTENSITY = 50;
uint32_t COLOR_BG, COLOR_PLAYER, COLOR_ENEMY, COLOR_TEXT, COLOR_RECORD;
uint32_t COLOR_BONUS;
uint32_t COLOR_PLAYER_1LIFE;
uint32_t COLOR_PLAYER_2LIVES;
uint32_t COLOR_PLAYER_3LIVES;
// =====================================================================================
// Joueur
// =====================================================================================
int playerY = HEIGHT - 1;
int playerLeftX = 3;
int playerRightX = 4;
uint8_t playerLives = 0;
// =====================================================================================
// Ennemis
// =====================================================================================
const uint8_t MAX_ENEMIES = 4;
bool enemyActive[MAX_ENEMIES];
bool enemyJustSpawned[MAX_ENEMIES];
int enemyX[MAX_ENEMIES];
int enemyY[MAX_ENEMIES];
// =====================================================================================
// Bonus (un seul à la fois)
// =====================================================================================
bool bonusActive = false;
int bonusX = 0;
int bonusY = 0;
long lastBonusScore = -BONUS_EVERY_SCORE;
// =====================================================================================
// Timings dynamiques (difficulté progressive)
// =====================================================================================
const unsigned long START_MOVE_MS = 230; // plus grand, moins vite au début [default 220]
const unsigned long MIN_MOVE_MS = 60; // plus petit, vitesse minimale plus rapide default [60]
const unsigned long MOVE_ACCEL_MS = 1; // plus grand, augmentation de vitesse plus rapide [default 4]
const unsigned long START_SPAWN_MS = 1100; // plus grand, moins de spawn au début [default 1100]
const unsigned long MIN_SPAWN_MS = 250; // [default 250]
const unsigned long SPAWN_ACCEL_MS = 60; // plus grand, plus de spawn [defaul 15]
unsigned long enemyMoveInterval;
unsigned long enemySpawnInterval;
unsigned long lastMoveAt = 0;
unsigned long lastSpawnAt = 0;
unsigned long lastInputAt = 0;
unsigned long inputRepeatMs = 95;
// =====================================================================================
// Score / High Score
// =====================================================================================
const uint16_t SCORE_PER_LED = 8;
long score = 0;
long highScore = 0;
bool lost = false;
bool needShow = false;
bool newRecordJustSet = false;
// =====================================================================================
// RNG renforcé (évite patterns répétitifs)
// =====================================================================================
static uint32_t entropy_pool = 0x12345678UL;
static bool reseededFromFirstInput = false;
static inline uint32_t rotl32(uint32_t x, uint8_t r){
return (x<>(32-r));
}
static inline void rng_mix(uint32_t v){
entropy_pool ^= v + 0x9E3779B9UL;
entropy_pool = rotl32(entropy_pool, (uint8_t)(micros() & 31));
entropy_pool ^= (uint32_t)millis();
}
// =====================================================================================
// --- FIX: #if multi-lignes, pas de code sur la même ligne que #if / #endif
// Objectif : ajouter de l’entropie (bruit) via des lectures analogiques si dispo.
// =====================================================================================
static inline void rng_addAnalogOnce(){
#if defined(A0)
rng_mix(((uint32_t)analogRead(A0)) << 16);
#endif
#if defined(A1)
rng_mix(((uint32_t)analogRead(A1)) << 8);
#endif
#if defined(A2)
rng_mix((uint32_t)analogRead(A2));
#endif
}
// Récupère un peu d’entropie au démarrage en bouclant quelques ms
static void rng_gatherStartupEntropy(uint16_t ms=25){
unsigned long endT=millis()+ms;
while((long)(endT-millis())>0){
rng_addAnalogOnce();
rng_mix((uint32_t)micros());
delayMicroseconds((uint8_t)(micros()&0x3F));
}
}
// Applique le "pool" interne comme graine du PRNG Arduino + jette quelques valeurs
static void rng_seedFromPool(){
randomSeed(entropy_pool);
for(uint8_t i=0;i<7;i++) (void)random();
}
// Premier seed : combine micros/millis + analog + petites boucles
static void rng_seedInitial(){
uint32_t s=micros()^millis();
rng_mix(s);
rng_addAnalogOnce();
rng_gatherStartupEntropy(25);
rng_seedFromPool();
}
// =====================================================================================
// EEPROM High Score
// Stratégie :
// - signature (octet) pour détecter "EEPROM jamais init"
// - valeur 16 bits + son inverse bitwise (anti-corruption simple)
// =====================================================================================
const int EE_ADDR_SIG=0, EE_ADDR_HS_L=1, EE_ADDR_HS_H=2, EE_ADDR_HS_INV_L=3, EE_ADDR_HS_INV_H=4;
const uint8_t HS_SIGNATURE=0xB6;
void saveHighScore(long hs){
if(hs<0)hs=0;
if(hs>65535)hs=65535;
uint16_t v=(uint16_t)hs, inv=~v;
EEPROM.update(EE_ADDR_SIG,HS_SIGNATURE);
EEPROM.update(EE_ADDR_HS_L,(uint8_t)(v&0xFF));
EEPROM.update(EE_ADDR_HS_H,(uint8_t)(v>>8));
EEPROM.update(EE_ADDR_HS_INV_L,(uint8_t)(inv&0xFF));
EEPROM.update(EE_ADDR_HS_INV_H,(uint8_t)(inv>>8));
}
long loadHighScore(){
if(EEPROM.read(EE_ADDR_SIG)!=HS_SIGNATURE) return 0;
uint16_t v=EEPROM.read(EE_ADDR_HS_L)|((uint16_t)EEPROM.read(EE_ADDR_HS_H)<<8);
uint16_t inv=EEPROM.read(EE_ADDR_HS_INV_L)|((uint16_t)EEPROM.read(EE_ADDR_HS_INV_H)<<8);
if((uint16_t)~v!=inv) return 0;
return (long)v;
}
void updateHighScoreIfNeeded(){
if(score>highScore){
highScore=score;
saveHighScore(highScore);
newRecordJustSet=true;
}
}
// =====================================================================================
// Boutons (INPUT_PULLUP → appui = LOW)
// =====================================================================================
inline bool btnLeftPressed(){ return digitalRead(LEFT_BTN_PIN)==LOW; }
inline bool btnRightPressed(){ return digitalRead(RIGHT_BTN_PIN)==LOW; }
// =====================================================================================
// Rotation/Miroir matériel
// Convertit (x,y) logiques → (xo,yo) dans le repère "top-left" du mapping.
// But : tu peux écrire ton jeu en coords logiques standard, et corriger le montage ici.
// =====================================================================================
static inline void mapXYHardware(int x,int y,int &xo,int &yo){
// 1) rotation
#if MATRIX_ROTATION == 0
xo = x; yo = y;
#elif MATRIX_ROTATION == 90 // horaire
xo = HEIGHT - 1 - y; yo = x;
#elif MATRIX_ROTATION == 180
xo = WIDTH - 1 - x; yo = HEIGHT - 1 - y;
#elif MATRIX_ROTATION == 270 // anti-horaire
xo = y; yo = WIDTH - 1 - x;
#else
#error "MATRIX_ROTATION must be 0/90/180/270"
#endif
// 2) miroirs optionnels
#if MATRIX_MIRROR_X
xo = WIDTH - 1 - xo;
#endif
#if MATRIX_MIRROR_Y
yo = HEIGHT - 1 - yo;
#endif
}
// Mapping XY -> index (attend des coords en repère top-left)
// Zigzag : une ligne sur deux est inversée (typique des matrices NeoPixel)
int xyToIndex_topLeft(int x,int y){
if(x<0||x>=WIDTH||y<0||y>=HEIGHT) return -1;
#if WIRING_ZIGZAG
if(y%2==0) return y*WIDTH + x;
else return y*WIDTH + (WIDTH-1-x);
#else
return y*WIDTH + x;
#endif
}
// Pose un pixel en coordonnées logiques, en appliquant rotation/miroir + sécurité bornes
void setPixelSafeXY(int x,int y,uint32_t c){
int xo,yo; mapXYHardware(x,y,xo,yo);
int idx = xyToIndex_topLeft(xo,yo);
if(idx>=0){ pixels.setPixelColor(idx,c); needShow=true; }
}
void clearMatrix(){
for(int i=0;i<(int)LED_COUNT;i++) pixels.setPixelColor(i,0);
needShow=true;
}
void fillMatrix(uint32_t c){
for(int i=0;i<(int)LED_COUNT;i++) pixels.setPixelColor(i,c);
needShow=true;
}
// =====================================================================================
// Gameplay — affichage joueur
// =====================================================================================
void drawPlayer(){
setPixelSafeXY(playerLeftX,playerY,COLOR_PLAYER);
setPixelSafeXY(playerRightX,playerY,COLOR_PLAYER);
}
// --- Mode wrap conditionnel (autorise ou bloque le passage par les bords)
bool canMoveLeft(){
#if PLAYER_WRAP_EDGES
return true;
#else
return playerLeftX > 0;
#endif
}
bool canMoveRight(){
#if PLAYER_WRAP_EDGES
return true;
#else
return playerRightX < (WIDTH - 1);
#endif
}
// Déplacement gauche : efface ancienne position → calcule nouvelle → redraw
void movePlayerLeft(){
if(!canMoveLeft()) return;
// Effacer l’ancienne barre
setPixelSafeXY(playerLeftX, playerY, COLOR_BG);
setPixelSafeXY(playerRightX, playerY, COLOR_BG);
#if PLAYER_WRAP_EDGES
if (playerLeftX == 0){
// wrap vers la droite (barre réapparaît à droite)
playerLeftX = WIDTH - 2;
playerRightX = WIDTH - 1;
score+=WRAP_WRAP_SCORE_BONUS; // bonus de score si wrap
updateDifficulty();
} else {
playerLeftX--; playerRightX--;
}
#else
playerLeftX--; playerRightX--;
#endif
drawPlayer();
}
void movePlayerRight(){
if(!canMoveRight()) return;
setPixelSafeXY(playerLeftX, playerY, COLOR_BG);
setPixelSafeXY(playerRightX, playerY, COLOR_BG);
#if PLAYER_WRAP_EDGES
if (playerRightX == (WIDTH - 1)){
// wrap vers la gauche
playerLeftX = 0;
playerRightX = 1;
score+=WRAP_WRAP_SCORE_BONUS;
updateDifficulty();
} else {
playerLeftX++; playerRightX++;
}
#else
playerLeftX++; playerRightX++;
#endif
drawPlayer();
}
// =====================================================================================
// Ennemis — génération / déplacement
// =====================================================================================
// Empêche 2 ennemis d’être dans la même colonne (lisibilité + difficulté contrôlée)
bool isColumnTaken(int col){
for(uint8_t i=0;i=MAX_ENEMIES) return;
// Tente de trouver une colonne libre
for(uint8_t attempt=0;attempt=HEIGHT){
enemyActive[i]=false;
score++;
updateDifficulty();
continue;
}
// Redessiner l’ennemi à sa nouvelle position
setPixelSafeXY(enemyX[i],enemyY[i],COLOR_ENEMY);
}
}
// =====================================================================================
// Bonus — apparaît tous les BONUS_EVERY_SCORE points, tombe comme un ennemi
// =====================================================================================
void trySpawnBonusByScore(){
if(bonusActive) return;
// Déclenchement : score multiple de BONUS_EVERY_SCORE, mais une seule fois par palier
if(score>0 && (score % BONUS_EVERY_SCORE)==0 && score!=lastBonusScore){
lastBonusScore = score;
for(uint8_t attempt=0; attempt=HEIGHT){
bonusActive=false;
return;
}
setPixelSafeXY(bonusX, bonusY, COLOR_BONUS);
}
// =====================================================================================
// Collisions :
// - Bonus : donne une vie (jusqu’à MAX_LIVES), ne tue pas
// - Ennemi : consomme une vie si dispo, sinon défaite
// =====================================================================================
void checkCollision(){
// BONUS: ne tue pas, donne une vie (max MAX_LIVES)
if(bonusActive &&
bonusY == playerY &&
(bonusX == playerLeftX || bonusX == playerRightX)){
bonusActive = false;
setPixelSafeXY(bonusX, bonusY, COLOR_BG);
if(playerLives < MAX_LIVES){
playerLives++;
updatePlayerColor();
drawPlayer();
playLifeSound();
}
return; // ESSENTIEL : ne pas enchaîner avec la collision ennemis le même tick
}
// ENNEMIS: si vie dispo => consomme vie, sinon mort
for(uint8_t i=0;i0){
playerLives--;
updatePlayerColor();
drawPlayer(); // redraw immédiat
setPixelSafeXY(enemyX[i], enemyY[i], COLOR_BG);
enemyActive[i]=false;
return;
} else {
lost=true;
bonusActive=false;
return;
}
}
}
}
// =====================================================================================
// Animation “X” rouge en cas de mort
// =====================================================================================
void flashBigX(uint8_t times=3){
for(uint8_t t=0;t0)?(w-1):0; // retire l’espace final
}
// Teste si un pixel texte est allumé pour une colonne globale et une hauteur y (0..6)
bool textPixelOnAt(const String&t,int globalCol,int y){
if(y<0||y>6) return false;
int col=0;
for(uint16_t i=0;i=charStart && globalCol<=charEnd){
uint8_t glyphCol=(uint8_t)(globalCol-charStart);
uint8_t colByte=glyphColByte(t.charAt(i),glyphCol);
return (colByte>>y)&0x01;
}
col+=w;
// Colonne “espace” entre caractères
if(globalCol==col) return false;
col+=1;
}
return false;
}
// Scroll horizontal du texte (défilement de gauche à droite sur la matrice)
void scrollText(const String&text,uint32_t color,uint16_t speedMs=SCORE_SCROLL_SPEED_MS){
int totalCols=(int)measureTextCols(text);
for(int offset=-WIDTH; offset<=totalCols; offset++){
clearMatrix();
for(int x=0;x=totalCols) continue;
for(int y=0;y<7;y++){
if(textPixelOnAt(text,srcCol,y)){
#if TEXT_ROT_180
int xr=WIDTH-1-x, yr=HEIGHT-1-y;
#else
int xr=x, yr=y;
#endif
setPixelSafeXY(xr,yr,color);
}
}
}
pixels.show();
delay(speedMs);
}
}
void displayScoreAsText(){
String s=F("S: ");
s+=String(score);
scrollText(s,COLOR_TEXT,SCORE_SCROLL_SPEED_MS);
}
void displayHighScoreText(uint32_t col){
String s=F("HS: ");
s+=String(highScore);
scrollText(s,col,SCORE_SCROLL_SPEED_MS);
}
// =====================================================================================
// Dégradé score (affiche score sous forme de remplissage progressif + animation)
// =====================================================================================
void displayScoreGradient(){
clearMatrix();
uint32_t leds=(SCORE_PER_LED==0)?0:(uint32_t)(score/SCORE_PER_LED);
int maxLeds=(leds<(uint32_t)LED_COUNT)?(int)leds:(int)LED_COUNT;
if(maxLeds<=0){ pixels.show(); return; }
int lit=0;
for(int y=0;y=1 vie = bleu
void updatePlayerColor(){
if(playerLives > 1){
COLOR_PLAYER = COLOR_PLAYER_3LIVES; // 2 vies ou plus
}
else if(playerLives == 1){
COLOR_PLAYER = COLOR_PLAYER_2LIVES; // 1 vie
}
else {
COLOR_PLAYER = COLOR_PLAYER_1LIFE; // 0 vie
}
}
// =====================================================================================
// SETUP : init pins, pixels, couleurs, RNG, HS, difficulté, etc.
// =====================================================================================
void setup(){
Serial.begin(9600);
pinMode(MATRIX_PIN, OUTPUT);
pinMode(LEFT_BTN_PIN, INPUT_PULLUP);
pinMode(RIGHT_BTN_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
pixels.begin();
selectModeAtBootWithRightButton();
// Couleurs de base
COLOR_BG = pixels.Color(0,0,0);
COLOR_PLAYER = pixels.Color(0,COLOR_INTENSITY,0);
COLOR_ENEMY = pixels.Color(COLOR_INTENSITY,0,0);
COLOR_TEXT = pixels.Color(0,0,COLOR_INTENSITY);
COLOR_RECORD = pixels.Color(COLOR_INTENSITY,(uint8_t)(COLOR_INTENSITY*0.6f),0);
COLOR_PLAYER_1LIFE = pixels.Color(0, COLOR_INTENSITY, 0); // vert
COLOR_PLAYER_2LIVES = pixels.Color(0, 0, COLOR_INTENSITY); // bleu
COLOR_PLAYER_3LIVES = pixels.Color(COLOR_INTENSITY, 0, COLOR_INTENSITY); // violet
// Bonus
COLOR_BONUS = pixels.Color(0, 0, COLOR_INTENSITY);
clearMatrix();
drawPlayer();
pixels.show();
// Init tableaux ennemis
for(uint8_t i=0;i= inputRepeatMs){
bool L = btnLeftPressed();
bool R = btnRightPressed();
// Reseed RNG au premier input réel (anti-patterns au boot)
if((L || R) && !reseededFromFirstInput){
rng_mix((uint32_t)micros());
rng_addAnalogOnce();
rng_seedFromPool();
reseededFromFirstInput = true;
}
// Déplacement exclusif (pas gauche + droite en même temps)
if(L && !R){
movePlayerLeft();
lastInputAt = now;
}
if(R && !L){
movePlayerRight();
lastInputAt = now;
}
}
// -----------------------------------------------------------------
// Spawn des ennemis (si pas en état "lost")
// -----------------------------------------------------------------
if(!lost && (now - lastSpawnAt) >= enemySpawnInterval){
trySpawnEnemy();
lastSpawnAt = now;
}
// -----------------------------------------------------------------
// Spawn bonus déclenché par le score
// -----------------------------------------------------------------
if(!lost){
trySpawnBonusByScore();
}
// -----------------------------------------------------------------
// Tick principal : déplacement ennemis + bonus + collisions
// -----------------------------------------------------------------
if(!lost && (now - lastMoveAt) >= enemyMoveInterval){
stepEnemies();
stepBonus();
checkCollision();
drawPlayer();
lastMoveAt = now;
// ---------------------------------------------------------------
// FIN DE PARTIE
// ---------------------------------------------------------------
if(lost){
newRecordJustSet = false;
updateHighScoreIfNeeded();
// Logs série (debug humain)
Serial.print(F("Score: "));
Serial.println(score);
Serial.print(F("High Score: "));
Serial.println(highScore);
playLoseSound();
// Animation de mort
flashBigX(3);
delay(750);
// Affichage score texte
displayScoreAsText();
// High Score
if(newRecordJustSet){
celebrateNewRecord();
playNewRecordSound();
displayHighScoreText(COLOR_RECORD);
displayHighScoreText(COLOR_RECORD); // double affichage = plus lisible
} else {
displayHighScoreText(COLOR_TEXT);
}
// Affichage graphique du score
displayScoreGradient();
}
}
// -----------------------------------------------------------------
// Affichage consolidé (un seul pixels.show() par frame si possible)
// -----------------------------------------------------------------
if(needShow){
pixels.show();
needShow = false;
}
// -----------------------------------------------------------------
// DEBUG série sans spam (uniquement si changement)
// -----------------------------------------------------------------
#if SERIAL_DEBUG_SCORE_LIVES
static long lastSerialScore = -1;
static int lastSerialLives = -1;
if(score != lastSerialScore || (int)playerLives != lastSerialLives){
Serial.print(F("[GAME] Score="));
Serial.print(score);
Serial.print(F(" | Vies="));
Serial.println(playerLives);
lastSerialScore = score;
lastSerialLives = (int)playerLives;
}
#endif
// -----------------------------------------------------------------
// Reset de la partie après défaite
// (bouton gauche maintenu)
// -----------------------------------------------------------------
if(lost && btnLeftPressed()){
delay(30); // anti-rebond simple
while(btnLeftPressed()){} // attend relâchement
resetGame();
}
}