⟵ Retour

Dames anglaises — mode d’emploi

Principe du jeu

Les dames anglaises sont un jeu de stratégie à deux joueurs. Chaque joueur tente de capturer toutes les pièces adverses ou de les bloquer.

Matériel

Mise en place

Déplacements

Dames (promotion)

Lorsqu’un pion atteint la dernière rangée, il devient une dame. Elle peut alors se déplacer en diagonale dans les deux sens.

Capture

Si une capture est possible, elle est obligatoire. Des captures multiples sont possibles en chaîne.

Victoire

Commandes

BoutonsActions
↑ ↓ ← →Déplacer le curseur
Sélectionner / déplacer
Start Nouvelle partie
Select Sélection du mode (2P, 1P-Easy-Medium-Hard)

Code du jeu

 /********************************************************
 *
 * ██████╗ ██████╗  █████╗ ██╗   ██╗ ██████╗ ██╗  ██╗████████╗███████╗
 * ██╔══██╗██╔══██╗██╔══██╗██║   ██║██╔════╝ ██║  ██║╚══██╔══╝██╔════╝
 * ██║  ██║██████╔╝███████║██║   ██║██║  ███╗███████║   ██║   ███████╗  
 * ██║  ██║██╔══██╗██╔══██║██║   ██║██║   ██║██╔══██║   ██║   ╚════██║  
 * ██████╔╝██║  ██║██║  ██║╚██████╔╝╚██████╔╝██║  ██║   ██║   ███████║
 * ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝  ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝
 *
 * -------------------------------------------------------
 *  DRAUGHTS – Jeu NeoPixel (Matrix 16×16)
 * -------------------------------------------------------
 *
 *  Auteur       : skuydi
 *  Année        : 05/2026
 *  Plateforme   : Arduino / compatible AVR
 *  Affichage    : Matrice LED NeoPixel 16×16
 *  Commande     : Manette NES (original)
 *  Bibliothèque : Adafruit_NeoPixel
 *
 *  -------------------------------------------------------
 *  DESCRIPTION
 *  -------------------------------------------------------
 *
 *
 /*********************************************************

#include <Adafruit_NeoPixel.h>
#include <TM1637Display.h>
#include "GameControllers.h"
#include <math.h>

// =========================================================
// CONFIG MATRICE
// =========================================================
#define PIN_NEOPIXEL  6
#define MATRIX_W      16
#define MATRIX_H      16
#define NUMPIXELS     (MATRIX_W * MATRIX_H)
#define BRIGHTNESS    10

// =========================================================
// CONFIG NES
// =========================================================
#define NES_LATCH   8
#define NES_CLOCK   9
#define NES_DATA    10
#define NES_INDEX   0

// =========================================================
// CONFIG TM1637 (P1=orange, P2=bleu)
// =========================================================
#define TM_P1_CLK   2
#define TM_P1_DIO   3
#define TM_P2_CLK   11
#define TM_P2_DIO   12

// =========================================================
// TIMINGS
// =========================================================
#define ATTRACT_STEP_MS   1200   // cadence IA vs IA en démo
#define AI_THINK_DELAY_MS  220   // pause avant coup IA en partie

// D-PAD répétition
#define DPAD_FIRST_REPEAT  350
#define DPAD_REPEAT_RATE    90

// Pulse curseur (additif, préserve la teinte)
#define CURSOR_PULSE_SPEED  0.004f
#define CURSOR_ADD_MIN      10
#define CURSOR_ADD_MAX      40

// =========================================================
// OBJETS HARDWARE
// =========================================================
Adafruit_NeoPixel strip(NUMPIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
TM1637Display     displayP1(TM_P1_CLK, TM_P1_DIO);
TM1637Display     displayP2(TM_P2_CLK, TM_P2_DIO);
GameControllers   controllers;

// =========================================================
// TYPES
// =========================================================
#define EMPTY 0
#define P1    1
#define P2    2
#define K1    3
#define K2    4

enum GameState : byte { STATE_ATTRACT, STATE_MENU, STATE_PLAY, STATE_GAMEOVER };
enum GameMode  : byte { MODE_2P, MODE_AI_E, MODE_AI_M, MODE_AI_H };
enum AiLevel   : byte { AI_EASY=0, AI_MED=1, AI_HARD=2 };

struct Move {
  int8_t sx, sy, tx, ty;
  bool   capture;
  int8_t cx, cy;
};

// =========================================================
// VARIABLES GLOBALES
// =========================================================
static GameState gameState   = STATE_ATTRACT;
static GameMode  gameMode    = MODE_2P;

static byte  board[8][8];
static byte  currentPlayer   = P1;
static bool  gameOver        = false;
static byte  winner          = EMPTY;

static int   cursorX = 3, cursorY = 3;
static bool  pieceSelected       = false;
static int   selectedX = 0, selectedY = 0;
static bool  mustContinueCapture = false;

static float pulsePhase = 0;

static uint32_t C_BLACK, C_GRAY, C_P1, C_P2;

// D-PAD repeat
static bool          dpadMoving     = false;
static unsigned long dpadStartTime  = 0;
static unsigned long dpadLastTime   = 0;

// Timers états
static unsigned long lastAttractStep = 0;
static unsigned long lastAiAction    = 0;

// Démo vitesse
static unsigned long demoDelayMs = 1200;
static const unsigned long DEMO_MIN_MS = 200;
static const unsigned long DEMO_MAX_MS = 2000;

#define MAX_MOVES 64

// =========================================================
// LED MAPPING — ZIGZAG (identique au code d'origine)
// =========================================================
int getIndex(int x, int y) {
  if (y % 2 == 0) return y * MATRIX_W + (MATRIX_W - 1 - x);
  return y * MATRIX_W + x;
}

void drawBlock2x2(int bx, int by, uint32_t color) {
  if (bx < 0 || bx > 7 || by < 0 || by > 7) return;
  // Rotation 90° anti-horaire : x' = y, y' = 7 - x
  int rbx = by;
  int rby = 7 - bx;
  int x = rbx * 2, y = rby * 2;
  strip.setPixelColor(getIndex(x,   y),   color);
  strip.setPixelColor(getIndex(x+1, y),   color);
  strip.setPixelColor(getIndex(x,   y+1), color);
  strip.setPixelColor(getIndex(x+1, y+1), color);
}

// =========================================================
// UTIL — PIÈCES
// =========================================================
bool isKing(byte p)                     { return p == K1 || p == K2; }
bool isPlayerPiece(byte p, byte player) { return (player==P1) ? (p==P1||p==K1) : (p==P2||p==K2); }

byte ownerOf(byte p) {
  if (p==P1||p==K1) return P1;
  if (p==P2||p==K2) return P2;
  return EMPTY;
}

uint32_t baseColor(byte p) {
  if (p==P1||p==K1) return C_P1;
  if (p==P2||p==K2) return C_P2;
  return 0;
}

// Pulse multiplicatif pour les dames (rois)
uint32_t applyPulse(uint32_t c) {
  float i = 0.6f + 0.4f * (sin(pulsePhase) + 1.0f) * 0.5f;
  return strip.Color(
    (uint8_t)(((c>>16)&0xFF)*i),
    (uint8_t)(((c>>8) &0xFF)*i),
    (uint8_t)(( c     &0xFF)*i)
  );
}

// Pulse ADDITIF pour le curseur (préserve la teinte)
uint32_t pulseAdditive(uint32_t baseCol) {
  float t = (float)millis() * CURSOR_PULSE_SPEED;
  float s = (sinf(t) * 0.5f + 0.5f);
  int add = CURSOR_ADD_MIN + (int)((CURSOR_ADD_MAX - CURSOR_ADD_MIN) * s);
  uint8_t r = min(255, (int)((baseCol>>16)&0xFF) + add);
  uint8_t g = min(255, (int)((baseCol>>8 )&0xFF) + add);
  uint8_t b = min(255, (int)((baseCol    )&0xFF) + add);
  return strip.Color(r, g, b);
}

void resetSelection() { pieceSelected = false; mustContinueCapture = false; }

// =========================================================
// TM1637 — NOMBRE DE PIÈCES
// =========================================================
void updateDisplays() {
  int p1c = 0, p2c = 0;
  for (int y = 0; y < 8; y++)
    for (int x = 0; x < 8; x++) {
      if (isPlayerPiece(board[y][x], P1)) p1c++;
      else if (isPlayerPiece(board[y][x], P2)) p2c++;
    }
  displayP1.showNumberDec(p1c, false);
  displayP2.showNumberDec(p2c, false);
}

void showMenuOnTM() {
  // Affiche le mode sélectionné : 2=2P, E/M/H pour IA
  // On utilise les chiffres disponibles sur TM1637
  // P1 = identifiant mode, P2 = niveau IA
  switch (gameMode) {
    case MODE_2P:
      displayP1.showNumberDec(2, false);
      displayP2.showNumberDec(2, false);
      break;
    case MODE_AI_E:
      displayP1.showNumberDec(1, false);
      displayP2.showNumberDec(0, false);  // 0 = easy
      break;
    case MODE_AI_M:
      displayP1.showNumberDec(1, false);
      displayP2.showNumberDec(1, false);  // 1 = medium
      break;
    case MODE_AI_H:
      displayP1.showNumberDec(1, false);
      displayP2.showNumberDec(2, false);  // 2 = hard
      break;
  }
}

// =========================================================
// INIT PLATEAU
// =========================================================
void initBoardState() {
  for (int y = 0; y < 8; y++)
    for (int x = 0; x < 8; x++) {
      if ((x+y)%2 == 1) {
        if      (y < 3) board[y][x] = P1;
        else if (y > 4) board[y][x] = P2;
        else            board[y][x] = EMPTY;
      } else {
        board[y][x] = EMPTY;
      }
    }
  currentPlayer = P1;
  gameOver      = false;
  winner        = EMPTY;
  cursorX       = 1; cursorY = 0;
  resetSelection();
  updateDisplays();
}

// =========================================================
// AFFICHAGE PLATEAU
// =========================================================
void drawBoard() {
  for (int y = 0; y < 8; y++)
    for (int x = 0; x < 8; x++)
      drawBlock2x2(x, y, ((x+y)%2 == 0) ? C_BLACK : C_GRAY);
}

void drawPieces() {
  for (int y = 0; y < 8; y++)
    for (int x = 0; x < 8; x++) {
      byte p = board[y][x]; if (p == EMPTY) continue;
      uint32_t c = baseColor(p);
      if (isKing(p)) c = applyPulse(c);
      drawBlock2x2(x, y, c);
    }
}

void drawCursor() {
  if (cursorX < 0) return;  // invisible en démo
  //uint32_t base = strip.Color(180, 0, 255);  // violet
  uint32_t base = (currentPlayer == P1) ? C_P1 : C_P2;
  drawBlock2x2(cursorX, cursorY, pulseAdditive(base));
}

void drawSelectedOverlay() {
  if (!pieceSelected) return;
  // Appliquer la même rotation 90° anti-horaire que drawBlock2x2
  int rbx = selectedY;
  int rby = 7 - selectedX;
  int x = rbx * 2, y = rby * 2;
  
  uint32_t diagColor = (currentPlayer == P1)
    ? strip.Color(255, 80, 0)
    : strip.Color(0, 60, 100);
  if (sin(pulsePhase * 3.0f) > 0) {
    strip.setPixelColor(getIndex(x,   y),   diagColor);
    strip.setPixelColor(getIndex(x+1, y+1), diagColor);
  } else {
    strip.setPixelColor(getIndex(x+1, y),   diagColor);
    strip.setPixelColor(getIndex(x,   y+1), diagColor);
  }
}

// =========================================================
// ANIMATION CAPTURE
// =========================================================
void captureFlash(int cx, int cy) {
  for (int i = 0; i < 2; i++) {
    drawBoard(); drawPieces();
    drawBlock2x2(cx, cy, strip.Color(255, 255, 255));
    strip.show(); delay(50);
    drawBoard(); drawPieces();
    strip.show(); delay(50);
  }
}

// =========================================================
// ANIMATION VAINQUEUR (5 secondes)
// =========================================================
void showWinnerScreen() {
  uint32_t c = (winner == EMPTY) ? strip.Color(180, 180, 0)
                                 : (winner == P1) ? C_P1 : C_P2;
  float phase = 0;
  unsigned long start = millis();
  while (millis() - start < 5000) {
    controllers.poll();  // on écoute quand même START pour skipper
    if (controllers.pressed(NES_INDEX, GameControllers::START)) break;
    phase += 0.12f;
    if (phase > TWO_PI) phase = 0;
    for (int y = 0; y < 8; y++)
      for (int x = 0; x < 8; x++) {
        float dx = (float)x - 3.5f;
        float dy = (float)y - 3.5f;
        float dist   = sqrt(dx*dx + dy*dy);
        float ripple = sin(dist * 1.2f - phase * 2.5f);
        float bright = ((ripple + 1.0f) * 0.5f) * (0.6f + 0.4f * sin(phase + x*0.7f + y*0.5f));
        drawBlock2x2(x, y, strip.Color(
          (uint8_t)(((c>>16)&0xFF)*bright),
          (uint8_t)(((c>>8 )&0xFF)*bright),
          (uint8_t)(( c     &0xFF)*bright)
        ));
      }
    strip.show();
    delay(20);
  }
}

// =========================================================
// PROMOTION
// =========================================================
void promoteIfNeeded(int x, int y) {
  if (board[y][x] == P1 && y == 7) board[y][x] = K1;
  if (board[y][x] == P2 && y == 0) board[y][x] = K2;
}

// =========================================================
// GÉNÉRATION DE COUPS
// =========================================================
int genCapturesFrom(byte player, int sx, int sy, Move out[], int maxOut) {
  int n = 0;
  byte piece = board[sy][sx];
  if (!isPlayerPiece(piece, player)) return 0;
  int dirs[4][2] = {{1,1},{-1,1},{1,-1},{-1,-1}};
  for (int i = 0; i < 4; i++) {
    int dx = dirs[i][0], dy = dirs[i][1];
    if (!isKing(piece)) {
      if (player==P1 && dy==-1) continue;
      if (player==P2 && dy== 1) continue;
    }
    int mx=sx+dx, my=sy+dy, tx=sx+2*dx, ty=sy+2*dy;
    if (tx<0||tx>7||ty<0||ty>7||mx<0||mx>7||my<0||my>7) continue;
    byte mid = board[my][mx];
    if (mid!=EMPTY && ownerOf(mid)!=player && board[ty][tx]==EMPTY)
      if (n < maxOut)
        out[n++] = {(int8_t)sx,(int8_t)sy,(int8_t)tx,(int8_t)ty,true,(int8_t)mx,(int8_t)my};
  }
  return n;
}

int genLegalMoves(byte player, Move out[], int maxOut) {
  bool anyCapture = false;
  Move tmp[8];
  for (int y=0;y<8&&!anyCapture;y++)
    for (int x=0;x<8&&!anyCapture;x++)
      if (isPlayerPiece(board[y][x],player) && genCapturesFrom(player,x,y,tmp,8)>0)
        anyCapture = true;

  int n = 0;
  if (anyCapture) {
    for (int y=0;y<8;y++) for (int x=0;x<8;x++) {
      if (!isPlayerPiece(board[y][x],player)) continue;
      int c = genCapturesFrom(player,x,y,tmp,8);
      for (int i=0;i7||ty<0||ty>7||board[ty][tx]!=EMPTY) continue;
      if (n < maxOut) out[n++] = {(int8_t)x,(int8_t)y,(int8_t)tx,(int8_t)ty,false,-1,-1};
    }
  }
  return n;
}

// =========================================================
// APPLICATION D'UN COUP
// =========================================================
bool applyMove(const Move &m, bool doFlash) {
  byte piece = board[m.sy][m.sx];
  board[m.sy][m.sx] = EMPTY;
  board[m.ty][m.tx] = piece;
  if (m.capture) {
    if (doFlash) captureFlash(m.cx, m.cy);
    board[m.cy][m.cx] = EMPTY;
  }
  promoteIfNeeded(m.tx, m.ty);
  updateDisplays();

  byte mover = ownerOf(board[m.ty][m.tx]);
  byte opp   = (mover==P1) ? P2 : P1;
  bool oppHas = false;
  for (int y=0;y<8&&!oppHas;y++)
    for (int x=0;x<8&&!oppHas;x++)
      if (isPlayerPiece(board[y][x],opp)) oppHas = true;
  if (!oppHas) { gameOver=true; winner=mover; return true; }

  if (m.capture) {
    Move cont[8];
    if (genCapturesFrom(mover,m.tx,m.ty,cont,8) > 0) return false;  // rafle possible
  }
  return true;
}

// =========================================================
// IA — ÉVALUATION
// =========================================================
bool squareAttackedBy(byte attacker, int tx, int ty) {
  for (int y=0;y<8;y++) for (int x=0;x<8;x++) {
    byte p = board[y][x];
    if (!isPlayerPiece(p,attacker)) continue;
    int dirs[4][2] = {{1,1},{-1,1},{1,-1},{-1,-1}};
    for (int i=0;i<4;i++) {
      int dx=dirs[i][0], dy=dirs[i][1];
      if (!isKing(p)) {
        if (attacker==P1&&dy==-1) continue;
        if (attacker==P2&&dy== 1) continue;
      }
      int mx=x+dx,my=y+dy,lx=x+2*dx,ly=y+2*dy;
      if (lx<0||lx>7||ly<0||ly>7||mx<0||mx>7||my<0||my>7) continue;
      if (mx==tx&&my==ty&&board[ly][lx]==EMPTY) return true;
    }
  }
  return false;
}

int heuristicMove(byte player, const Move &m) {
  int score = 0;
  if (m.capture) score += 50;
  byte piece = board[m.sy][m.sx];
  if ((piece==P2&&m.ty==0)||(piece==P1&&m.ty==7)) score += 25;
  if (m.tx>=2&&m.tx<=5&&m.ty>=2&&m.ty<=5) score += 4;
  if (squareAttackedBy((player==P1)?P2:P1, m.tx, m.ty)) score -= 30;
  return score;
}

void copyBoard(byte dst[8][8], byte src[8][8]) {
  for (int y=0;y<8;y++) for (int x=0;x<8;x++) dst[y][x] = src[y][x];
}

// =========================================================
// IA — CHOIX DU COUP
// =========================================================
Move chooseAiMove(byte player, AiLevel level) {
  Move moves[MAX_MOVES];
  int n = genLegalMoves(player, moves, MAX_MOVES);
  if (n <= 0) { gameOver=true; winner=(player==P1)?P2:P1; return {0,0,0,0,false,-1,-1}; }
  if (level == AI_EASY) return moves[random(n)];
  if (level == AI_MED) {
    int best=-999999, bestIdx=0;
    for (int i=0;ibest){best=s;bestIdx=i;} }
    return moves[bestIdx];
  }
  // HARD : lookahead 1 coup adversaire
  int best=-999999, bestIdx=0;
  byte savedBoard[8][8]; copyBoard(savedBoard, board);
  bool savedGO=gameOver; byte savedW=winner;
  for (int i=0;ibestC){bestC=sc;bi=k;}}
      px=cont[bi].tx; py=cont[bi].ty;
      turnEnds=applyMove(cont[bi],false); myScore+=40;
    }
    int oppBest=0;
    if (!gameOver) {
      byte opp=(player==P1)?P2:P1; Move oppMoves[MAX_MOVES];
      int on=genLegalMoves(opp,oppMoves,MAX_MOVES);
      if (on<=0) myScore+=9999;
      else {
        int wfm=-999999;
        for (int j=0;jwfm)wfm=s;}
        oppBest=wfm;
      }
    }
    int total=myScore-oppBest;
    if (total>best){best=total;bestIdx=i;}
  }
  copyBoard(board, savedBoard); gameOver=savedGO; winner=savedW;
  return moves[bestIdx];
}

// =========================================================
// TOUR IA
// =========================================================
AiLevel modeToLevel(GameMode m) {
  if (m==MODE_AI_E) return AI_EASY;
  if (m==MODE_AI_M) return AI_MED;
  return AI_HARD;
}

void aiTurn(AiLevel level) {
  if (gameOver) return;
  Move m = chooseAiMove(currentPlayer, level); if (gameOver) return;
  bool endTurn = applyMove(m, true); if (gameOver) return;
  int px=m.tx, py=m.ty;
  while (!endTurn) {
    Move cont[8]; int c=genCapturesFrom(currentPlayer,px,py,cont,8); if(c<=0) break;
    int idx=0;
    if (level==AI_EASY) { idx=random(c); }
    else { int best=-999999; for(int k=0;kbest){best=s;idx=k;}} }
    px=cont[idx].tx; py=cont[idx].ty;
    endTurn=applyMove(cont[idx],true); if(gameOver) return;
  }
  currentPlayer = (currentPlayer==P1) ? P2 : P1;
  Move check[1];
  if (genLegalMoves(currentPlayer,check,1)==0) { gameOver=true; winner=(currentPlayer==P1)?P2:P1; }
}

// =========================================================
// COUP HUMAIN
// =========================================================
bool humanTryMove(int sx, int sy, int tx, int ty) {
  if (mustContinueCapture && (sx!=selectedX || sy!=selectedY)) return false;
  Move moves[MAX_MOVES];
  int n = genLegalMoves(currentPlayer, moves, MAX_MOVES);
  for (int i=0;i DPAD_FIRST_REPEAT) && (now - dpadLastTime > DPAD_REPEAT_RATE)) {
      dpadLastTime = now;
      fire = true;
    }
    if (fire) {
      if (up)    cursorY = max(0, cursorY - 1);
      if (down)  cursorY = min(7, cursorY + 1);
      if (left)  cursorX = max(0, cursorX - 1);
      if (right) cursorX = min(7, cursorX + 1);
    }
  } else {
    dpadMoving = false;
  }
}

// =========================================================
// MENU — RENDU COULEUR
// =========================================================
uint32_t modeColor(GameMode m) {
  if (m==MODE_2P)    return strip.Color(180, 180, 180);  // blanc/gris  → 2P
  if (m==MODE_AI_E)  return strip.Color(0,   255,   0);  // vert        → Easy
  if (m==MODE_AI_M)  return strip.Color(255, 140,   0);  // orange      → Medium
  return                     strip.Color(255,   0,   0);  // rouge       → Hard
}

void renderMenu() {
  strip.clear();
  uint32_t c = modeColor(gameMode);
  // Carré 4×4 pixels centré (LED 6..9 × 6..9)
  for (int y=6; y<10; y++)
    for (int x=6; x<10; x++)
      strip.setPixelColor(getIndex(x, y), c);
  strip.show();
}

// =========================================================
// STATE RUNNERS
// =========================================================

/* ---- ATTRACT (démo IA vs IA) ---- */
void runAttract() {
  // START → menu
  if (controllers.pressed(NES_INDEX, GameControllers::START)) {
    gameState = STATE_MENU;
    showMenuOnTM();
    return;
  }

  // D-PAD haut/bas → vitesse démo
  if (controllers.pressed(NES_INDEX, GameControllers::UP))
    demoDelayMs = (demoDelayMs > DEMO_MIN_MS+20) ? demoDelayMs-20 : DEMO_MIN_MS;
  if (controllers.pressed(NES_INDEX, GameControllers::DOWN))
    demoDelayMs = (demoDelayMs < DEMO_MAX_MS-20) ? demoDelayMs+20 : DEMO_MAX_MS;

  unsigned long now = millis();
  if (now - lastAttractStep >= demoDelayMs) {
    lastAttractStep = now;
    aiTurn(AI_HARD);
    if (gameOver) {           // partie démo terminée → reset silencieux
      gameOver = false; winner = EMPTY;
      resetSelection();
      initBoardState();
    }
  }
  drawBoard(); drawPieces(); strip.show();
}

/* ---- MENU (choix du mode) ---- */
void runMenu() {
  // SELECT → changer mode (cycle)
  if (controllers.pressed(NES_INDEX, GameControllers::SELECT)) {
    gameMode = (GameMode)((gameMode + 1) % 4);
    showMenuOnTM();
  }
  // D-PAD gauche/droite → changer mode aussi (intuitif)
  if (controllers.pressed(NES_INDEX, GameControllers::LEFT)) {
    gameMode = (gameMode == MODE_2P) ? MODE_AI_H : (GameMode)(gameMode - 1);
    showMenuOnTM();
  }
  if (controllers.pressed(NES_INDEX, GameControllers::RIGHT)) {
    gameMode = (GameMode)((gameMode + 1) % 4);
    showMenuOnTM();
  }
  // A ou START → lancer la partie
  if (controllers.pressed(NES_INDEX, GameControllers::A) ||
      controllers.pressed(NES_INDEX, GameControllers::START)) {
    initBoardState();
    gameState = STATE_PLAY;
    updateDisplays();
  }
  renderMenu();
}

/* ---- JEU (humain vs humain ou vs IA) ---- */
void runPlay() {
  // START → retour menu
  if (controllers.pressed(NES_INDEX, GameControllers::START)) {
    gameState = STATE_MENU;
    showMenuOnTM();
    return;
  }

  // Tour IA (P2) si mode solo
  bool humanTurn = true;
  if (gameMode != MODE_2P && currentPlayer == P2) humanTurn = false;

  if (!humanTurn) {
    unsigned long now = millis();
    if (now - lastAiAction >= (unsigned long)AI_THINK_DELAY_MS) {
      lastAiAction = now;
      aiTurn(modeToLevel(gameMode));
      if (gameOver) { gameState = STATE_GAMEOVER; }
    }
    drawBoard(); drawPieces(); strip.show();
    return;
  }

  // Tour humain
  handleDpad();

  // A → sélectionner / valider
  if (controllers.pressed(NES_INDEX, GameControllers::A)) {
    if (!pieceSelected) {
      if (isPlayerPiece(board[cursorY][cursorX], currentPlayer)) {
        selectedX=cursorX; selectedY=cursorY; pieceSelected=true;
      }
    } else {
      if (cursorX==selectedX && cursorY==selectedY) {
        if (!mustContinueCapture) resetSelection();
      } else if (!mustContinueCapture && isPlayerPiece(board[cursorY][cursorX], currentPlayer)) {
        selectedX=cursorX; selectedY=cursorY;
      } else {
        humanTryMove(selectedX, selectedY, cursorX, cursorY);
        if (gameOver) { gameState = STATE_GAMEOVER; }
      }
    }
  }

  drawBoard();
  drawPieces();
  drawSelectedOverlay();
  drawCursor();
  strip.show();
}

/* ---- GAME OVER ---- */
void runGameOver() {
  showWinnerScreen();
  // Après l'animation → retour attract
  gameOver  = false;
  winner    = EMPTY;
  resetSelection();
  initBoardState();
  gameState      = STATE_ATTRACT;
  lastAttractStep = millis();
  demoDelayMs    = 1200;
}

// =========================================================
// SETUP / LOOP
// =========================================================
void setup() {
  strip.begin();
  strip.setBrightness(BRIGHTNESS);
  strip.show();

  displayP1.setBrightness(3);
  displayP2.setBrightness(3);

  controllers.init(NES_LATCH, NES_CLOCK);
  controllers.setController(NES_INDEX, GameControllers::NES, NES_DATA);

  C_BLACK = strip.Color(0,   0,   0);
  C_GRAY  = strip.Color(20,  20,  20);
  C_P1    = strip.Color(255, 50,  0);    // orange
  C_P2    = strip.Color(0,   150, 200);  // bleu

  randomSeed(analogRead(A0));

  gameMode  = MODE_2P;
  gameState = STATE_ATTRACT;
  initBoardState();
  lastAttractStep = millis();
  lastAiAction    = millis();

  showMenuOnTM();
}

void loop() {
  // Pulse globale (rois + sélection)
  pulsePhase += 0.04f;
  if (pulsePhase > TWO_PI) pulsePhase = 0;

  controllers.poll();

  switch (gameState) {
    case STATE_ATTRACT:  runAttract();  break;
    case STATE_MENU:     runMenu();     break;
    case STATE_PLAY:     runPlay();     break;
    case STATE_GAMEOVER: runGameOver(); break;
  }
}