⟵ Retour

Reversi — mode d’emploi

Principe du jeu

Reversi est un jeu stratégique à deux joueurs. Le but est d’avoir plus de pions de sa couleur que l’adversaire à la fin de la partie.

Préparation

Les 4 pions de départ sont placés au centre en diagonale : orange-bleu / bleu-orange.

Commandes

BoutonsActions
↑ ↓ ← →Déplacement dans les quatre directions
Poser le pion
Afficher les possibilités
Start Démarre une partie
Select Sélection du mode

Attention : Chaque appui sur Ⓑ enlève un point.

Déroulement d’un tour

Règles importantes

Victoire

Le joueur ayant le plus de pions de sa couleur à la fin gagne.

Code du jeu

 / *******************************************************
 *
 * ██████╗ ███████╗██╗   ██╗███████╗██████╗ ███████╗██╗
 * ██╔══██╗██╔════╝██║   ██║██╔════╝██╔══██╗██╔════╝██║
 * ██████╔╝█████╗  ██║   ██║█████╗  ██████╔╝███████╗██║
 * ██╔══██╗██╔══╝  ╚██╗ ██╔╝██╔══╝  ██╔══██╗╚════██║██║
 * ██║  ██║███████╗ ╚████╔╝ ███████╗██║  ██║███████║██║
 * ╚═╝  ╚═╝╚══════╝  ╚═══╝  ╚══════╝╚═╝  ╚═╝╚══════╝╚═╝
 *
 * -------------------------------------------------------
 *  REVERSI – 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, TM1637Displayl, GameControllers
 *
 *  -------------------------------------------------------
 *  DESCRIPTION
 *  -------------------------------------------------------
 *
 *
 / ********************************************************

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

// =========================================================
// CONFIG
// =========================================================
#define BRIGHTNESS             8     // 0..255
#define MATRIX_SERPENTINE      1     // 0 = linéaire, 1 = zigzag
#define ATTRACT_STEP_MS        260   // vitesse démo IA vs IA
#define AI_THINK_DELAY_MS      220   // vitesse IA en SOLO
#define FLIP_WHITE_MS          350   // flash blanc
#define FLIP_COLOR_MS          250   // retour couleur
#define SHOW_VALID_MOVES_BY_B  1     // B maintenu -> coups valides en jaune pulsé

// Pulse "additif" (évite que l'orange vire rouge)
#define CURSOR_PULSE_ADD_MIN   10
#define CURSOR_PULSE_ADD_MAX   40
#define CURSOR_PULSE_SPEED     0.004f

// =========================================================
// HARDWARE PINS (Nano Every / ATmega4809)
// =========================================================
#define PIN_NEOPIXEL  6
#define MATRIX_W      16
#define MATRIX_H      16
#define NUMPIXELS     (MATRIX_W * MATRIX_H)

#define NES_LATCH     8
#define NES_CLOCK     9
#define NES_DATA      10
#define NES_INDEX     0

#define TM_P1_CLK     2  // Joueur 1 (orange)
#define TM_P1_DIO     3
#define TM_P2_CLK    11  // Joueur 2 (bleu)
#define TM_P2_DIO    12

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;

// =========================================================
// STATES / MODES
// =========================================================
enum GameState { STATE_ATTRACT, STATE_MENU, STATE_PLAY, STATE_GAMEOVER };
enum GameMode  { MODE_VS, MODE_SOLO }; // MENU: VS (1/1) default, SOLO (0/1)
static GameState gameState = STATE_ATTRACT;
static GameMode  gameMode  = MODE_VS;

// =========================================================
// REVERSI BOARD
// =========================================================
static int board[8][8];        // 0 empty, 1 P1 (orange), 2 P2 (bleu)
static int cursorX = 3, cursorY = 3;
static int currentPlayer = 1;  // 1 or 2

static bool showMovesHeld = false; // B held
static int penaltyP1 = 0;
static int penaltyP2 = 0;

// =========================================================
// TIMERS
// =========================================================
static unsigned long lastAttractStep = 0;
static unsigned long lastAiAction    = 0;

// pulse (yellow & cursor)
static unsigned long pulseTimer = 0;
static float pulseValue = 0.0f;
static bool pulseUp = true;

// D-pad repeat (smooth)
static bool moving = false;
static unsigned long moveStartTime = 0;
static unsigned long lastMoveTime  = 0;
static const unsigned long firstRepeatDelay = 350;
static const unsigned long repeatRate       = 90;

// =========================================================
// COLORS
// =========================================================
static uint32_t C_EMPTY, C_P1, C_P2, C_CURSOR_P1, C_CURSOR_P2, C_FLIP;

// 8 directions
static const int directions[8][2] = {
  { 1, 0},{-1, 0},{ 0, 1},{ 0,-1},
  { 1, 1},{-1,-1},{ 1,-1},{-1, 1}
};

// =========================================================
// UTILS
// =========================================================
static inline int clamp8(int v){ if(v<0) return 0; if(v>7) return 7; return v; }
static inline bool isOnBoard(int x,int y){ return x>=0 && x<8 && y>=0 && y<8; }

// =========================================================
// LED MAPPING
// =========================================================
int getLEDIndex(int x, int y){
#if MATRIX_SERPENTINE == 1
  if ((y & 1) == 0) return y * MATRIX_W + x;
  return y * MATRIX_W + (MATRIX_W - 1 - x);
#else
  return y * MATRIX_W + x;
#endif
}

void drawCell(int cx, int cy, uint32_t color){
  // board 8x8 -> 2x2 pixels on 16x16
  for(int dy=0; dy<2; dy++){
    for(int dx=0; dx<2; dx++){
      int x = cx*2 + dx;
      int y = cy*2 + dy;
      strip.setPixelColor(getLEDIndex(x,y), color);
    }
  }
}

// =========================================================
// PULSE
// =========================================================
void updatePulse(){
  if(millis() - pulseTimer > 30){
    pulseTimer = millis();
    if(pulseUp) pulseValue += 0.05f;
    else        pulseValue -= 0.05f;

    if(pulseValue >= 1.0f){ pulseValue = 1.0f; pulseUp = false; }
    if(pulseValue <= 0.0f){ pulseValue = 0.0f; pulseUp = true;  }
  }
}

// Jaune pulsé (clair) pour les coups possibles
uint32_t getPulseYellow(){
  int base = 120;
  int range = 135;
  int intensity = base + (int)(pulseValue * range); // 120..255
  return strip.Color(intensity, intensity, 0);
}

// Pulse ADDITIF: conserve la teinte (évite orange -> rouge)
uint32_t pulseAdditive(uint32_t baseColor){
  float t = (float)millis() * CURSOR_PULSE_SPEED;
  float s = (sinf(t) * 0.5f + 0.5f); // 0..1

  int add = CURSOR_PULSE_ADD_MIN + (int)((CURSOR_PULSE_ADD_MAX - CURSOR_PULSE_ADD_MIN) * s);

  uint8_t r = (baseColor >> 16) & 0xFF;
  uint8_t g = (baseColor >>  8) & 0xFF;
  uint8_t b = (baseColor      ) & 0xFF;

  r = (uint8_t)min(255, (int)r + add);
  g = (uint8_t)min(255, (int)g + add);
  b = (uint8_t)min(255, (int)b + add);

  return strip.Color(r,g,b);
}

// =========================================================
// REVERSI RULES
// =========================================================
bool isValidMoveForPlayer(int x,int y,int player){
  if(!isOnBoard(x,y)) return false;
  if(board[y][x] != 0) return false;

  int opp = (player==1)?2:1;

  for(int d=0; d<8; d++){
    int dx = directions[d][0];
    int dy = directions[d][1];
    int nx = x + dx;
    int ny = y + dy;

    bool foundOpp = false;
    while(isOnBoard(nx,ny) && board[ny][nx] == opp){
      nx += dx; ny += dy;
      foundOpp = true;
    }
    if(foundOpp && isOnBoard(nx,ny) && board[ny][nx] == player) return true;
  }
  return false;
}

bool playerHasMove(int player){
  for(int y=0;y<8;y++)
    for(int x=0;x<8;x++)
      if(isValidMoveForPlayer(x,y,player)) return true;
  return false;
}

bool anyMoveExists(){
  return playerHasMove(1) || playerHasMove(2);
}

// =========================================================
// SCORES
// =========================================================
void computeCounts(int &p1,int &p2){
  p1=0; p2=0;
  for(int y=0;y<8;y++){
    for(int x=0;x<8;x++){
      if(board[y][x]==1) p1++;
      else if(board[y][x]==2) p2++;
    }
  }
}

void showScoresOnTM(){
  int p1,p2; computeCounts(p1,p2);

  int p1Disp = p1 - penaltyP1;
  int p2Disp = p2 - penaltyP2;
  if(p1Disp < 0) p1Disp = 0;
  if(p2Disp < 0) p2Disp = 0;

  displayP1.showNumberDec(p1Disp, false);
  displayP2.showNumberDec(p2Disp, false);
}

void showMenuOnTM(){
  // default = MODE_VS -> 1 / 1
  if(gameMode == MODE_VS){
    displayP1.showNumberDec(1, false);
    displayP2.showNumberDec(1, false);
  } else {
    // SOLO: 0 / 1 (IA / humain)
    displayP1.showNumberDec(0, false);
    displayP2.showNumberDec(1, false);
  }
}

// =========================================================
// FLIP ANIMATION
// =========================================================
void animateFlipCell(int x,int y,int player){
  drawCell(x,y, C_FLIP);
  strip.show();
  delay(FLIP_WHITE_MS);

  drawCell(x,y, (player==1)?C_P1:C_P2);
  strip.show();
  delay(FLIP_COLOR_MS);
}

void flipPiecesAnimated(int x,int y,int player){
  int opp = (player==1)?2:1;

  for(int d=0; d<8; d++){
    int dx = directions[d][0];
    int dy = directions[d][1];

    int nx = x + dx;
    int ny = y + dy;

    int path[8][2];
    int count = 0;

    while(isOnBoard(nx,ny) && board[ny][nx] == opp){
      if(count < 8){
        path[count][0] = nx;
        path[count][1] = ny;
      }
      count++;
      nx += dx; ny += dy;
      if(count > 7) break; // safety
    }

    if(count > 0 && isOnBoard(nx,ny) && board[ny][nx] == player){
      int flips = (count > 8) ? 8 : count;
      for(int i=0;i demo varies)
// =========================================================
int countFlipsForMove(int x,int y,int player){
  int opp = (player==1)?2:1;
  int total = 0;

  for(int d=0; d<8; d++){
    int dx = directions[d][0];
    int dy = directions[d][1];

    int nx = x + dx;
    int ny = y + dy;
    int count = 0;

    while(isOnBoard(nx,ny) && board[ny][nx] == opp){
      nx += dx; ny += dy;
      count++;
    }
    if(count > 0 && isOnBoard(nx,ny) && board[ny][nx] == player){
      total += count;
    }
  }
  return total;
}

int evalMoveAI(int x,int y,int player){
  int score = 0;

  // corners
  if((x==0&&y==0)||(x==7&&y==0)||(x==0&&y==7)||(x==7&&y==7)) score += 1000;

  // X-squares (danger)
  if((x==1&&y==1)||(x==6&&y==1)||(x==1&&y==6)||(x==6&&y==6)) score -= 80;

  // edges
  if(x==0||x==7||y==0||y==7) score += 60;

  // flips
  score += countFlipsForMove(x,y,player) * 5;

  return score;
}

bool findBestMoveAI(int player,int &bx,int &by){
  int bestScore = -999999;
  int candX[64];
  int candY[64];
  int candCount = 0;

  for(int y=0;y<8;y++){
    for(int x=0;x<8;x++){
      if(isValidMoveForPlayer(x,y,player)){
        int s = evalMoveAI(x,y,player);

        if(s > bestScore){
          bestScore = s;
          candCount = 0;
          candX[candCount] = x;
          candY[candCount] = y;
          candCount++;
        } else if(s == bestScore){
          candX[candCount] = x;
          candY[candCount] = y;
          candCount++;
        }
      }
    }
  }

  if(candCount <= 0) return false;

  int pick = random(candCount);
  bx = candX[pick];
  by = candY[pick];
  return true;
}

void doAITurn(int aiPlayer){
  if(!playerHasMove(aiPlayer)){
    // pass-turn
    int other = (aiPlayer==1)?2:1;
    if(playerHasMove(other)) currentPlayer = other;
    else gameState = STATE_GAMEOVER;
    return;
  }

  int bx,by;
  if(findBestMoveAI(aiPlayer,bx,by)){
    attemptMove(bx,by);
  } else {
    int other = (aiPlayer==1)?2:1;
    if(playerHasMove(other)) currentPlayer = other;
    else gameState = STATE_GAMEOVER;
  }
}

// =========================================================
// INPUT (NES)
// =========================================================
void handleDpadRepeat(){
  bool left  = controllers.down(NES_INDEX,GameControllers::UP);
  bool right = controllers.down(NES_INDEX,GameControllers::DOWN);
  bool up    = controllers.down(NES_INDEX,GameControllers::LEFT);
  bool down  = controllers.down(NES_INDEX,GameControllers::RIGHT);

  unsigned long now = millis();

  if(up||down||left||right){
    if(!moving){
      moving = true;
      moveStartTime = now;
      lastMoveTime = now;

      if(up)    cursorY = clamp8(cursorY - 1);
      if(down)  cursorY = clamp8(cursorY + 1);
      if(left)  cursorX = clamp8(cursorX - 1);
      if(right) cursorX = clamp8(cursorX + 1);

    } else if((now - moveStartTime > firstRepeatDelay) && (now - lastMoveTime > repeatRate)) {

      if(up)    cursorY = clamp8(cursorY - 1);
      if(down)  cursorY = clamp8(cursorY + 1);
      if(left)  cursorX = clamp8(cursorX - 1);
      if(right) cursorX = clamp8(cursorX + 1);

      lastMoveTime = now;
    }
  } else {
    moving = false;
  }
}

// =========================================================
// GAME OVER SCREEN
// =========================================================
void showWinnerScreen(){
  static bool doneAnim = false;

  int p1,p2; computeCounts(p1,p2);
  int p1Disp = p1 - penaltyP1; if(p1Disp<0) p1Disp=0;
  int p2Disp = p2 - penaltyP2; if(p2Disp<0) p2Disp=0;

  displayP1.showNumberDec(p1Disp, false);
  displayP2.showNumberDec(p2Disp, false);

  uint32_t winColor;
  if(p1Disp > p2Disp) winColor = C_P1;
  else if(p2Disp > p1Disp) winColor = C_P2;
  else winColor = strip.Color(255,255,255);

  if(!doneAnim){
    for(int i=0;i start game
  if(controllers.pressed(NES_INDEX, GameControllers::A) ||
     controllers.pressed(NES_INDEX, GameControllers::START))
  {
    resetBoardToStart();
    gameState = STATE_PLAY;
    drawBoard(true);
    return;
  }

  drawBoard(false);
}

void runPlay(){
  // START -> back to menu
  if(controllers.pressed(NES_INDEX, GameControllers::START)){
    gameState = STATE_MENU;
    showMenuOnTM();
    return;
  }

  // B held -> show valid moves
  showMovesHeld = controllers.down(NES_INDEX, GameControllers::B);

  // B pressed -> penalty -1
  if(controllers.pressed(NES_INDEX, GameControllers::B)){
    if(currentPlayer == 1) penaltyP1++;
    else penaltyP2++;
    showScoresOnTM();
  }

  // SOLO: human=P1 (1), AI=P2 (2)
  if(gameMode == MODE_SOLO && currentPlayer == 2){
    unsigned long now = millis();
    if(now - lastAiAction >= (unsigned long)AI_THINK_DELAY_MS){
      lastAiAction = now;
      doAITurn(2);
    }
    drawBoard(false);
    return;
  }

  // human turn
  handleDpadRepeat();

  if(controllers.pressed(NES_INDEX, GameControllers::A)){
    attemptMove(cursorX, cursorY);
  }

  if(!anyMoveExists()){
    gameState = STATE_GAMEOVER;
  }

  drawBoard(true);
}

void runGameOver(){
  showWinnerScreen();
}

// =========================================================
// 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_EMPTY  = strip.Color(0,0,0);
  C_P1     = strip.Color(255,50,0);   //(orange)
  C_P2     = strip.Color(0,150,200);  // (bleu)
  C_CURSOR_P1 = strip.Color(255, 50, 0);
  C_CURSOR_P2 = strip.Color(0, 150, 200);

  C_FLIP   = strip.Color(255,255,255);

  // randomness for demo variability
  randomSeed(analogRead(A0));

  // default menu mode = VS (1/1)
  gameMode = MODE_VS;

  resetBoardToStart();
  currentPlayer = 1;

  gameState = STATE_ATTRACT;
  lastAttractStep = millis();
  lastAiAction = millis();

  showMenuOnTM();
}

void loop(){
  updatePulse();
  controllers.poll();

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