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.
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.
Si une capture est possible, elle est obligatoire. Des captures multiples sont possibles en chaîne.
| Boutons | Actions |
|---|---|
| ↑ ↓ ← → | Déplacer le curseur |
| Ⓐ | Sélectionner / déplacer |
| Nouvelle partie | |
| Sélection du mode (2P, 1P-Easy-Medium-Hard) |
/********************************************************
*
* ██████╗ ██████╗ █████╗ ██╗ ██╗ ██████╗ ██╗ ██╗████████╗███████╗
* ██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔════╝ ██║ ██║╚══██╔══╝██╔════╝
* ██║ ██║██████╔╝███████║██║ ██║██║ ███╗███████║ ██║ ███████╗
* ██║ ██║██╔══██╗██╔══██║██║ ██║██║ ██║██╔══██║ ██║ ╚════██║
* ██████╔╝██║ ██║██║ ██║╚██████╔╝╚██████╔╝██║ ██║ ██║ ███████║
* ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
*
* -------------------------------------------------------
* 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;
}
}