/*
* ============================================================
* Super Mario Bros. Embarqué — Version Simplifiée
* Projet Individuel 2025-2026 — École Hexagone
* Version adaptée au diagramme Wokwi fourni
* ============================================================
*
* Carte : ESP32 DevKit C V4
* Écran : ILI9341 240×320
* Bibliothèques à installer :
* - Adafruit GFX Library
* - Adafruit ILI9341
*
* ---- Branchements issus du diagramme ----
*
* Écran TFT :
* TFT VCC → 3.3V
* TFT GND → GND
* TFT CS → GPIO 5
* TFT DC → GPIO 2
* TFT RST → GPIO 22
* TFT MOSI → GPIO 23
* TFT SCK → GPIO 18 (VSPI SCK par défaut)
*
* Boutons (résistances pull-up 1kΩ externes vers 5V) :
* btn1 HAUT → GPIO 13
* btn2 BAS → GPIO 4
* btn3 GAUCHE → GPIO 15
* btn4 DROITE → GPIO 27
*
* Buzzer passif :
* Borne signal → GPIO 12
* Borne − → GND
* ============================================================
*/
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ============================================================
// CONFIGURATION DES BROCHES GPIO
// (extraites de votre diagram.json)
// ============================================================
// Écran TFT
#define TFT_CS 5
#define TFT_DC 2
#define TFT_RST -1
#define TFT_CLK 18 // GPIO 18 = VSPI SCK par défaut
#define TFT_MOSI 23
#define TFT_MISO -1 // non connecté dans le diagramme
// Boutons (actifs à LOW, pull-up externe 1kΩ)
#define BTN_UP 13 // btn1
#define BTN_DOWN 4 // btn2
#define BTN_LEFT 15 // btn3
#define BTN_RIGHT 27 // btn4
// Buzzer passif (PWM via LEDC — API ESP32 core v3.x)
#define BUZZER 12
#define BUZZER_RESOLUTION 8 // résolution PWM en bits
// ============================================================
// CONSTANTES DU JEU
// ============================================================
#define GRID_ROWS 4
#define GRID_COLS 5
#define CELL_SIZE 50
#define GRID_X 10
#define GRID_Y 40
#define MAX_LIVES 3
#define NUM_LEVELS 2
#define DEBOUNCE_MS 200
#define HIT_DELAY_MS 500
#define LEVEL_PAUSE_MS 1500
// ============================================================
// COULEURS (RGB565)
// ============================================================
#define C_BLACK 0x0000
#define C_WHITE 0xFFFF
#define C_RED 0xF800
#define C_GREEN 0x07E0
#define C_BLUE 0x001F
#define C_CYAN 0x07FF
#define C_YELLOW 0xFFE0
#define C_GRAY 0x7BEF
#define C_DARKGRAY 0x39E7
#define C_NAVY 0x000F
#define C_DARKBLUE 0x0013
#define C_DARKGREEN 0x0300
#define C_DARKRED 0x5000
// ============================================================
// STRUCTURES DE DONNÉES
// ============================================================
struct Level {
int rocks[5][2];
int rockCount;
int flagRow;
int flagCol;
};
struct GameState {
int level;
int lives;
int pRow;
int pCol;
bool gameOver;
bool won;
bool locked;
};
// ============================================================
// DÉFINITION DES NIVEAUX
// ============================================================
/*
* Niveau 1 — fidèle au dessin fourni :
* [J][ ][ ][R][ ]
* [ ][ ][R][ ][ ]
* [R][ ][ ][F][ ]
* [ ][ ][ ][ ][ ]
*
* Niveau 2 — plus difficile :
* [J][ ][ ][R][ ]
* [ ][R][ ][ ][ ]
* [ ][ ][ ][ ][R]
* [ ][ ][R][ ][F]
*/
const Level levels[NUM_LEVELS] = {
// Niveau 1
{
{{0, 3}, {1, 2}, {2, 0}, {0, 0}, {0, 0}},
3,
2, 3
},
// Niveau 2
{
{{0, 3}, {1, 1}, {2, 4}, {3, 2}, {0, 0}},
4,
3, 4
}
};
// ============================================================
// VARIABLES GLOBALES
// ============================================================
/*
* SCK = GPIO 17 n'est pas la broche VSPI par défaut (18).
* On recrée le bus SPI manuellement avec SPIClass,
* puis on le passe à Adafruit_ILI9341.
*/
SPIClass spi(VSPI);
Adafruit_ILI9341 tft(&spi, TFT_DC, TFT_CS, TFT_RST);
GameState game;
unsigned long lastBtnTime = 0;
// ============================================================
// FONCTIONS SONORES — LEDC (ESP32)
// ============================================================
/* Joue une note pendant 'duree' ms
* API ESP32 core v3.x : ledcWriteTone(pin, freq) */
void jouerTon(int frequence, int duree) {
ledcWriteTone(BUZZER, frequence);
delay(duree);
ledcWriteTone(BUZZER, 0); // coupe le son
}
void sonDeplacement() {
jouerTon(523, 40);
}
void sonCollision() {
jouerTon(250, 100);
delay(20);
jouerTon(180, 200);
}
void sonNiveauSuivant() {
jouerTon(523, 120); delay(20);
jouerTon(659, 120); delay(20);
jouerTon(784, 200);
}
void sonVictoire() {
jouerTon(523, 120); delay(20);
jouerTon(659, 120); delay(20);
jouerTon(784, 120); delay(20);
jouerTon(1047, 300);
}
void sonGameOver() {
jouerTon(350, 150); delay(20);
jouerTon(300, 150); delay(20);
jouerTon(250, 150); delay(20);
jouerTon(200, 400);
}
void sonDemarrage() {
int notes[] = {659, 659, 0, 659, 0, 523, 659, 0, 784};
int durees[] = {120, 120, 80, 120, 80, 120, 120, 80, 200};
for (int i = 0; i < 9; i++) {
ledcWriteTone(BUZZER, notes[i]); // 0 = silence
delay(durees[i] + 20);
}
ledcWriteTone(BUZZER, 0);
}
// ============================================================
// FONCTIONS UTILITAIRES — BOUTONS
// ============================================================
/* Les boutons ont des pull-up externes (1kΩ vers 5V) :
état repos = HIGH, appuyé = LOW */
bool estAppuye(int broche) {
return (digitalRead(broche) == LOW);
}
bool lectureAutorisee() {
return (millis() - lastBtnTime > DEBOUNCE_MS);
}
void attendreAppui() {
while (!estAppuye(BTN_UP) &&
!estAppuye(BTN_DOWN) &&
!estAppuye(BTN_LEFT) &&
!estAppuye(BTN_RIGHT)) {
delay(10);
}
delay(DEBOUNCE_MS);
}
// ============================================================
// FONCTIONS LOGIQUES — JEU
// ============================================================
bool estPierre(int row, int col) {
const Level& lv = levels[game.level];
for (int i = 0; i < lv.rockCount; i++) {
if (lv.rocks[i][0] == row && lv.rocks[i][1] == col) return true;
}
return false;
}
void reinitialiserJeu() {
game.level = 0;
game.lives = MAX_LIVES;
game.pRow = 0;
game.pCol = 0;
game.gameOver = false;
game.won = false;
game.locked = false;
}
// ============================================================
// FONCTIONS D'AFFICHAGE — CELLULES
// ============================================================
void dessinerCellule(int row, int col, bool flash) {
int x = GRID_X + col * CELL_SIZE;
int y = GRID_Y + row * CELL_SIZE;
int cx = x + CELL_SIZE / 2;
int cy = y + CELL_SIZE / 2;
const Level& lv = levels[game.level];
bool joueurIci = (row == game.pRow && col == game.pCol);
bool pierreIci = estPierre(row, col);
bool drapeauIci = (row == lv.flagRow && col == lv.flagCol);
uint16_t bg;
if (joueurIci && flash) bg = C_DARKRED;
else if (joueurIci) bg = C_DARKBLUE;
else if (pierreIci) bg = C_DARKGRAY;
else if (drapeauIci) bg = C_DARKGREEN;
else bg = C_NAVY;
tft.fillRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2, bg);
if (joueurIci) {
uint16_t pc = flash ? C_WHITE : C_CYAN;
tft.fillCircle(cx, cy - 10, 6, pc);
tft.drawLine(cx, cy - 4, cx, cy + 9, pc);
tft.drawLine(cx, cy + 1, cx - 7, cy + 7, pc);
tft.drawLine(cx, cy + 1, cx + 7, cy + 7, pc);
tft.drawLine(cx, cy + 9, cx - 6, cy + 18, pc);
tft.drawLine(cx, cy + 9, cx + 6, cy + 18, pc);
} else if (pierreIci) {
tft.fillTriangle(cx, cy - 13, cx - 13, cy + 9, cx + 13, cy + 9, C_GRAY);
tft.drawTriangle(cx, cy - 13, cx - 13, cy + 9, cx + 13, cy + 9, C_WHITE);
} else if (drapeauIci) {
tft.drawLine(cx - 4, cy - 14, cx - 4, cy + 14, C_WHITE);
tft.fillTriangle(cx - 4, cy - 14, cx + 13, cy - 5, cx - 4, cy + 4, C_GREEN);
}
}
// ============================================================
// FONCTIONS D'AFFICHAGE — GRILLE ET HUD
// ============================================================
void dessinerGrille(bool flash) {
for (int r = 0; r < GRID_ROWS; r++) {
for (int c = 0; c < GRID_COLS; c++) {
int x = GRID_X + c * CELL_SIZE;
int y = GRID_Y + r * CELL_SIZE;
tft.drawRect(x, y, CELL_SIZE, CELL_SIZE, C_YELLOW);
dessinerCellule(r, c, flash);
}
}
}
void dessinerHUD() {
tft.fillRect(0, 0, 320, GRID_Y - 1, C_BLACK);
tft.setTextColor(C_WHITE);
tft.setTextSize(1);
tft.setCursor(GRID_X, 5);
tft.print("NIVEAU ");
tft.print(game.level + 1);
tft.print(" / ");
tft.print(NUM_LEVELS);
tft.setCursor(GRID_X, 20);
for (int i = 0; i < MAX_LIVES; i++) {
tft.setTextColor(i < game.lives ? C_RED : C_DARKGRAY);
tft.print("<3 ");
}
}
void dessinerEcranJeu(bool flash) {
dessinerHUD();
dessinerGrille(flash);
}
// ============================================================
// FONCTIONS D'AFFICHAGE — ÉCRANS SPÉCIAUX
// ============================================================
void afficherEcranTitre() {
tft.fillScreen(C_BLACK);
tft.setTextSize(2);
tft.setTextColor(C_RED);
tft.setCursor(45, 60);
tft.println("SUPER MARIO");
tft.setTextColor(C_WHITE);
tft.setCursor(55, 88);
tft.println("EMBARQUE");
tft.setTextSize(1);
tft.setTextColor(C_GRAY);
tft.setCursor(35, 140);
tft.println("Ecole Hexagone 2025-2026");
tft.setTextColor(C_YELLOW);
tft.setCursor(50, 175);
tft.println("Appuyez pour demarrer");
}
void afficherTransitionNiveau(int niveauComplete) {
tft.fillScreen(C_BLACK);
tft.setTextColor(C_GREEN);
tft.setTextSize(2);
tft.setCursor(20, 80);
tft.print("NIVEAU ");
tft.print(niveauComplete);
tft.println(" TERMINE !");
tft.setTextColor(C_WHITE);
tft.setTextSize(1);
tft.setCursor(55, 140);
tft.print("Niveau ");
tft.print(niveauComplete + 1);
tft.println(" en cours...");
}
void afficherGameOver() {
tft.fillScreen(C_BLACK);
tft.setTextColor(C_RED);
tft.setTextSize(2);
tft.setCursor(55, 70);
tft.println("GAME OVER");
tft.setTextColor(C_WHITE);
tft.setTextSize(1);
tft.setCursor(65, 115);
tft.println("Plus de vies !");
tft.setTextColor(C_GRAY);
tft.setCursor(35, 160);
tft.println("Appuyez pour recommencer");
}
void afficherVictoire() {
tft.fillScreen(C_BLACK);
tft.setTextColor(C_YELLOW);
tft.setTextSize(2);
tft.setCursor(15, 65);
tft.println("FELICITATIONS !");
tft.setTextColor(C_WHITE);
tft.setTextSize(1);
tft.setCursor(65, 115);
tft.println("Jeu termine !");
tft.setTextColor(C_GREEN);
tft.setCursor(20, 140);
tft.println("Les 2 niveaux completes.");
tft.setTextColor(C_GRAY);
tft.setCursor(35, 185);
tft.println("Appuyez pour rejouer");
}
// ============================================================
// LOGIQUE PRINCIPALE — DÉPLACEMENT
// ============================================================
void deplacerJoueur(int dr, int dc) {
if (game.locked || game.gameOver || game.won) return;
int nl = game.pRow + dr;
int nc = game.pCol + dc;
if (nl < 0 || nl >= GRID_ROWS || nc < 0 || nc >= GRID_COLS) return;
if (estPierre(nl, nc)) {
game.lives--;
game.pRow = nl;
game.pCol = nc;
game.locked = true;
sonCollision();
dessinerEcranJeu(true);
delay(HIT_DELAY_MS);
game.pRow = 0;
game.pCol = 0;
game.locked = false;
if (game.lives <= 0) {
game.gameOver = true;
sonGameOver();
afficherGameOver();
} else {
dessinerEcranJeu(false);
}
return;
}
game.pRow = nl;
game.pCol = nc;
sonDeplacement();
const Level& lv = levels[game.level];
if (nl == lv.flagRow && nc == lv.flagCol) {
if (game.level < NUM_LEVELS - 1) {
int numComplete = game.level + 1;
sonNiveauSuivant();
afficherTransitionNiveau(numComplete);
delay(LEVEL_PAUSE_MS);
game.level++;
game.pRow = 0;
game.pCol = 0;
tft.fillScreen(C_BLACK);
dessinerEcranJeu(false);
} else {
game.won = true;
sonVictoire();
afficherVictoire();
}
return;
}
dessinerEcranJeu(false);
}
// ============================================================
// SETUP
// ============================================================
void setup() {
// Boutons — pull-up externe fourni par les résistances 1kΩ
// On utilise INPUT (pas INPUT_PULLUP) car le pull-up est externe
pinMode(BTN_UP, INPUT);
pinMode(BTN_DOWN, INPUT);
pinMode(BTN_LEFT, INPUT);
pinMode(BTN_RIGHT, INPUT);
// LEDC pour le buzzer — API v3.x : ledcAttach(pin, freq, resolution)
ledcAttach(BUZZER, 2000, BUZZER_RESOLUTION);
/*
* SCK = GPIO 18 = broche VSPI par défaut sur ESP32
* Le bus SPI est initialisé automatiquement par tft.begin()
*/
spi.begin(TFT_CLK, TFT_MISO, TFT_MOSI, TFT_CS);
// Initialisation de l'écran TFT
tft.begin();
tft.setRotation(1); // mode paysage : 320×240 px
tft.fillScreen(C_BLACK);
// Écran titre
afficherEcranTitre();
sonDemarrage();
attendreAppui();
// Lancement du jeu
reinitialiserJeu();
tft.fillScreen(C_BLACK);
dessinerEcranJeu(false);
}
// ============================================================
// LOOP — BOUCLE PRINCIPALE
// ============================================================
void loop() {
if (game.gameOver || game.won) {
if (estAppuye(BTN_UP) ||
estAppuye(BTN_DOWN) ||
estAppuye(BTN_LEFT) ||
estAppuye(BTN_RIGHT)) {
delay(DEBOUNCE_MS);
reinitialiserJeu();
tft.fillScreen(C_BLACK);
dessinerEcranJeu(false);
}
return;
}
if (!lectureAutorisee()) {
delay(10);
return;
}
if (estAppuye(BTN_UP)) {
lastBtnTime = millis();
deplacerJoueur(-1, 0);
} else if (estAppuye(BTN_DOWN)) {
lastBtnTime = millis();
deplacerJoueur(1, 0);
} else if (estAppuye(BTN_LEFT)) {
lastBtnTime = millis();
deplacerJoueur(0, -1);
} else if (estAppuye(BTN_RIGHT)) {
lastBtnTime = millis();
deplacerJoueur(0, 1);
}
delay(10);
}