// =============================================================================
// Jeu d'échecs sur écran TFT ILI9341, piloté en Bluetooth (HC-05)
// Carte : Nucleo-64 (core STM32duino)
//
// Principe :
// - le plateau est stocké dans un tableau 8x8 de caractères (lettres = pièces)
// - le téléphone envoie des lettres (F, B, L, R, C) via le HC-05
// - un curseur jaune se déplace sur le plateau, "C" sélectionne / déplace
// - aucune règle d'échecs n'est vérifiée pour l'instant
// =============================================================================
#include <Arduino.h>
#include <Wire.h> // Inclus mais pas utilisé pour l'instant (I2C)
#include <Adafruit_GFX.h> // Fonctions graphiques de base (rectangles, texte)
#include <Adafruit_ILI9341.h> // Pilote de l'écran ILI9341
// ===== Communication Bluetooth HC-05 =====
// Port série matériel dédié au HC-05 : USART1
// PA10 = RX de la Nucleo (relié au TX du HC-05) -> D2 sur le connecteur Arduino
// PA9 = TX de la Nucleo (relié au RX du HC-05) -> D8 sur le connecteur Arduino
HardwareSerial BT(PA10, PA9); // (RX, TX)
// ===== Configuration écran TFT ILI9341 =====
// Broches de contrôle (le SPI utilise D11 = MOSI et D13 = SCK automatiquement)
#define TFT_CS 10 // Chip Select : active l'écran sur le bus SPI
#define TFT_DC 7 // Data/Command : indique si l'octet envoyé est une commande ou une donnée
#define TFT_RST 9 // Reset de l'écran
// Création de l'objet écran
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
// ===== Configuration échiquier =====
const int Taille_case = 28; // Côté d'une case en pixels (8 x 28 = 224 px)
const int Marge = 8; // Marge autour du plateau (240 - 224 = 16 -> 8 de chaque côté)
// Couleurs au format RGB565 (16 bits)
const uint16_t COL_BLANC = 0xEF5D; // Cases claires
const uint16_t COL_NOIR = 0x4A49; // Cases foncées
const uint16_t COL_CURSEUR = ILI9341_YELLOW; // Contour du curseur
const uint16_t COL_SELECTION = ILI9341_RED; // Contour de la pièce sélectionnée
// ===== Touches du gamepad =====
// Caractères envoyés par l'application du téléphone
const char TOUCHE_HAUT = 'F'; // Flèche haut (Forward)
const char TOUCHE_BAS = 'B'; // Flèche bas (Backward)
const char TOUCHE_GAUCHE = 'L'; // Flèche gauche (Left)
const char TOUCHE_DROITE = 'R'; // Flèche droite (Right)
const char TOUCHE_ROND = 'C'; // Bouton rond (Cercle) = valider
// ===== Position du curseur =====
int curLigne = 6; // Départ : sur le pion devant le roi blanc (ligne 6, colonne 4)
int curCol = 4;
// ===== Pièce sélectionnée =====
bool selActive = false; // true si une pièce est actuellement sélectionnée
int selLigne = 0; // Position de la pièce sélectionnée
int selCol = 0;
// ===== Plateau d'échecs =====
// Majuscules = pièces blanches, minuscules = pièces noires, ' ' = case vide
// R/r roi, D/d dame, T/t tour, F/f fou, C/c cavalier, P/p pion
// Ligne 0 en haut : pièces noires
char plateau[8][8] = {
{'t','c','f','d','r','f','c','t'},
{'p','p','p','p','p','p','p','p'},
{' ',' ',' ',' ',' ',' ',' ',' '},
{' ',' ',' ',' ',' ',' ',' ',' '},
{' ',' ',' ',' ',' ',' ',' ',' '},
{' ',' ',' ',' ',' ',' ',' ',' '},
{'P','P','P','P','P','P','P','P'},
{'T','C','F','D','R','F','C','T'}
};
// ===== Outils =====
// Renvoie true si la pièce est blanche (lettre majuscule)
bool estBlanc(char p)
{
return (p >= 'A' && p <= 'Z');
}
// ===== Dessiner un contour de 2 pixels d'épaisseur =====
// decalage : nombre de pixels de retrait vers l'intérieur de la case
void dessinerContour(int ligne, int col, uint16_t couleur, int decalage)
{
// Coin haut-gauche de la case, plus le décalage
int x = Marge + col * Taille_case + decalage;
int y = Marge + ligne * Taille_case + decalage;
// Taille du carré, réduite de chaque côté par le décalage
int t = Taille_case - 2 * decalage;
tft.drawRect(x, y, t, t, couleur); // Trait extérieur
tft.drawRect(x + 1, y + 1, t - 2, t - 2, couleur); // Trait intérieur (2 px au total)
}
// ===== Dessiner une case (fond + pièce + sélection + curseur) =====
// Redessine tout depuis le fond, ce qui efface proprement l'ancien contenu
void dessinerCase(int ligne, int col)
{
int x = Marge + col * Taille_case;
int y = Marge + ligne * Taille_case;
// Fond : (ligne + col) impair = case foncée, pair = case claire
uint16_t couleur = ((ligne + col) & 1) ? COL_NOIR : COL_BLANC;
tft.fillRect(x, y, Taille_case, Taille_case, couleur);
// Pièce : lettre en vert pour les blancs, en bleu pour les noirs
char p = plateau[ligne][col];
if (p != ' ')
{
tft.setTextSize(3); // Lettre de 18 x 24 px
tft.setTextColor(estBlanc(p) ? ILI9341_GREEN : ILI9341_BLUE);
tft.setCursor(x + 5, y + 2); // Centrage approximatif dans la case
tft.print(p);
}
// Contour de sélection (rouge, sur le bord extérieur de la case)
bool estSelection = (selActive && ligne == selLigne && col == selCol);
if (estSelection)
dessinerContour(ligne, col, COL_SELECTION, 0);
// Curseur (jaune) : décalé de 2 px vers l'intérieur s'il est sur la case
// sélectionnée, pour que les deux contours restent visibles ensemble
if (ligne == curLigne && col == curCol)
dessinerContour(ligne, col, COL_CURSEUR, estSelection ? 2 : 0);
}
// ===== Dessiner l'échiquier complet =====
void chessboard()
{
for (int ligne = 0; ligne < 8; ligne++)
for (int col = 0; col < 8; col++)
dessinerCase(ligne, col);
}
// ===== Actualiser l'affichage =====
// Simple raccourci : redessine tout le plateau
void actualiserEchiquier()
{
chessboard();
}
// ===== Déplacer le curseur =====
// dLigne / dCol valent -1, 0 ou +1
void deplacerCurseur(int dLigne, int dCol)
{
int nLigne = curLigne + dLigne;
int nCol = curCol + dCol;
// Ne rien faire si on sort du plateau
if (nLigne < 0 || nLigne > 7 || nCol < 0 || nCol > 7)
return;
// Mémoriser l'ancienne position pour pouvoir effacer son contour
int ancienneLigne = curLigne;
int ancienneCol = curCol;
curLigne = nLigne;
curCol = nCol;
// On ne redessine que 2 cases (plus rapide que tout le plateau)
dessinerCase(ancienneLigne, ancienneCol);
dessinerCase(curLigne, curCol);
}
// ===== Validation (bouton ROND) =====
// Selon la situation : sélectionne, annule, change de sélection ou déplace
void valider()
{
char pieceCurseur = plateau[curLigne][curCol]; // Contenu de la case sous le curseur
// --- Cas 1 : aucune pièce sélectionnée -> on tente de sélectionner ---
if (!selActive)
{
// On ne peut pas sélectionner une case vide
if (pieceCurseur == ' ')
{
Serial.println("Case vide : rien a selectionner");
return;
}
// Mémoriser la sélection et afficher le contour rouge
selActive = true;
selLigne = curLigne;
selCol = curCol;
dessinerCase(curLigne, curCol);
Serial.print("Piece selectionnee : ");
Serial.println(pieceCurseur);
return;
}
// --- Une pièce est déjà sélectionnée ---
// Cas 2 : on revalide la même case -> on annule la sélection
if (curLigne == selLigne && curCol == selCol)
{
selActive = false;
dessinerCase(curLigne, curCol); // Efface le contour rouge
Serial.println("Selection annulee");
return;
}
char pieceSel = plateau[selLigne][selCol]; // Pièce actuellement sélectionnée
// Cas 3 : la case visée contient une pièce de la même couleur
// -> on ne capture pas, on change simplement de sélection
if (pieceCurseur != ' ' && estBlanc(pieceCurseur) == estBlanc(pieceSel))
{
int ancienneLigne = selLigne;
int ancienneCol = selCol;
selLigne = curLigne;
selCol = curCol;
dessinerCase(ancienneLigne, ancienneCol); // Retire le contour de l'ancienne
dessinerCase(selLigne, selCol); // Affiche le contour de la nouvelle
Serial.print("Nouvelle selection : ");
Serial.println(pieceCurseur);
return;
}
// Cas 4 : case vide ou pièce adverse -> déplacement (ou capture)
// Aucune règle d'échecs n'est vérifiée : tout déplacement est accepté
plateau[curLigne][curCol] = pieceSel; // La pièce arrive (écrase l'éventuelle pièce capturée)
plateau[selLigne][selCol] = ' '; // La case de départ devient vide
int departLigne = selLigne;
int departCol = selCol;
selActive = false; // Plus de sélection
// Redessiner uniquement les deux cases modifiées
dessinerCase(departLigne, departCol);
dessinerCase(curLigne, curCol);
// Trace dans le moniteur série
Serial.print("Deplacement : ");
Serial.print(pieceSel);
Serial.print(" (");
Serial.print(departLigne);
Serial.print(",");
Serial.print(departCol);
Serial.print(") -> (");
Serial.print(curLigne);
Serial.print(",");
Serial.print(curCol);
Serial.println(")");
}
// ===== Traitement des commandes Bluetooth =====
// Associe chaque caractère reçu à une action
void Commande(char command)
{
switch (command)
{
case TOUCHE_HAUT:
Serial.println("Fleche HAUT");
deplacerCurseur(-1, 0); // Une ligne plus haut
break;
case TOUCHE_BAS:
Serial.println("Fleche BAS");
deplacerCurseur(1, 0); // Une ligne plus bas
break;
case TOUCHE_GAUCHE:
Serial.println("Fleche GAUCHE");
deplacerCurseur(0, -1); // Une colonne à gauche
break;
case TOUCHE_DROITE:
Serial.println("Fleche DROITE");
deplacerCurseur(0, 1); // Une colonne à droite
break;
case TOUCHE_ROND:
Serial.println("Validation (ROND)");
valider();
break;
default:
// Caractère inconnu : ignoré
break;
}
}
// ===== Initialisation =====
void setup()
{
Serial.begin(115200); // Moniteur série (USB du ST-Link) pour le débogage
BT.begin(38400); // Vitesse du HC-05 : doit correspondre à sa configuration
Wire.begin(); // Démarre l'I2C (non utilisé pour l'instant)
tft.begin(); // Initialise l'écran
tft.setRotation(0); // Orientation portrait
tft.fillScreen(ILI9341_BLACK); // Fond noir
actualiserEchiquier(); // Affiche le plateau, les pièces et le curseur
Serial.println("Initialisation terminee");
Serial.println("Pret, en attente du gamepad...");
}
// ===== Boucle principale =====
void loop()
{
// Y a-t-il au moins un caractère reçu du téléphone ?
if (BT.available() > 0)
{
char c = BT.read(); // Lire un caractère
// Affichage de debug : caractère et son code ASCII
Serial.print("Recu : '");
Serial.print(c);
Serial.print("' (code ");
Serial.print((int)c);
Serial.println(")");
Commande(c); // Exécuter l'action correspondante
}
}
Comments