| 本帖最后由 创客编程张 于 2025-10-15 17:19 编辑 
 前两天在B站上刷到了井字棋的起源和发展,忽然来了灵感,正愁没素材呢,这不,咱用新板子配合着OLED制作一个井字棋玩玩。
 
 关于FireBeetle 2 ESP32-C5
 FireBeetle 2 ESP32-C5 IO套装包括两部分:Firebeetle 2 ESP32-C5 开发板和其专用的IO扩展底板。IO扩展板方便快速连接各种传感器外设,让Firebeetle 2 ESP32-C5开发板到手即用,无需焊接。
 
  FireBeetle 2 ESP32-C5是一款搭载乐鑫 ESP32-C5 模组的低功耗 IoT 开发板,面向智能家居和广泛物联网场景,集高性能计算、多协议支持与智能电源管理于一体,为各种部署需求提供高可靠性、高灵活性与长续航的解决方案。
 
 
 前期准备
 1.软件准备
 下载Arduino IDE,打开安装ESP32开发板,然后安装U8g2库(用于驱动OLED显示屏)
 2.硬件清单
 
 ESP32-C5OLED显示屏(128*64)Keyboard模拟按键
 准备完成,我们开始干正事
 将OLED连接到开发板的I2C引脚上,将Keyboard模拟按键连接至引脚2
 接下来编写程序
 首先初始化
 
 变量复制代码#include <Arduino.h>
#include <U8g2lib.h>
#define I2C_SDA 9    // ESP32-C5 专用 I2C 数据引脚(SDA)
#define I2C_SCL 10   // ESP32-C5 专用 I2C 时钟引脚(SCL)
#define AD_KEY_PIN A2  // ADKeyboard模拟输入引脚
#define EMPTY 0        // 棋盘空状态
#define PLAYER 1       // 玩家(空心圆)
#define AI 2           // 人机(实心圆)
#define LONG_PRESS_TIME 1000  // 新增:长按判定时长(1000ms=1秒,可调整)
// 1. OLED初始化(0.96寸单色I2C,根据屏幕芯片调整)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
 
 keyboard模拟按键复制代码int chessBoard[3][3] = {0};  // 3x3棋盘,存储落子状态
int currX = 1, currY = 1;    // 当前选中的棋盘坐标(0-2,初始居中)
bool isPlayerTurn = true;    // 是否玩家回合(true=玩家,false=人机)
bool gameOver = false;       // 游戏是否结束
int winner = EMPTY;          // 赢家(0=未分胜负,1=玩家,2=人机)
int winLine[4] = {0};        // 赢棋连线坐标(x1,y1,x2,y2)
 
 棋盘显示复制代码unsigned long pressStartTime = 0;  // 记录S5按键按下的起始时间(毫秒)
bool isKey5Pressed = false;        // 标记S5是否处于按下状态(避免重复触发)
// 3. ADKeyboard按键识别
int readADKey() {
  int adVal = analogRead(AD_KEY_PIN);
  delay(100);  // 消抖:避免按键机械抖动导致的误读数
  if (adVal >= 2500 && adVal <= 2599) return 1;    // S1(上)
  else if (adVal >= 2600 && adVal <= 2699) return 2;  // S2(左)
  else if (adVal >= 2700 && adVal <= 2799) return 3;  // S3(下)
  else if (adVal >= 2800 && adVal <= 2899) return 4;  // S4(右)
  else if (adVal >= 3000 && adVal <= 3099) return 5;  // S5(确定/长按重启)
  else return -1;  // 无按键按下,返回-1
 
 输赢检查复制代码// 4. 棋盘坐标转OLED像素坐标
void chessToOled(int chessX, int chessY, int &oledX, int &oledY) {
  oledX = 20 + chessX * 28;  // 横向:左偏移20(居中),每个格子宽28像素
  oledY = 20 + chessY * 22;  // 纵向:上偏移20(留提示栏),每个格子高22像素
}
// 5. 绘制棋盘线(无修改,确保3x3格子清晰)
void drawChessBoard() {
  u8g2.drawLine(48, 20, 48, 62);  // 第一条竖线(分割第1、2列)
  u8g2.drawLine(76, 20, 76, 62);  // 第二条竖线(分割第2、3列)
  u8g2.drawLine(20, 42, 98, 42);  // 第一条横线(分割第1、2行)
  u8g2.drawLine(20, 64, 98, 64);  // 第二条横线(分割第2、3行)
}
// 6. 绘制棋子(无修改,保留闪烁选中效果)
void drawChess() {
  static unsigned long lastFlashTime = 0;
  static bool flashFlag = true;  // 闪烁标志(300ms切换一次,实现选中闪烁)
  if (millis() - lastFlashTime > 300) {
    flashFlag = !flashFlag;
    lastFlashTime = millis();
  }
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      int oledX, oledY;
      chessToOled(x, y, oledX, oledY);
      if (chessBoard[y][x] == PLAYER) u8g2.drawCircle(oledX, oledY, 8);  // 玩家:空心圆
      else if (chessBoard[y][x] == AI) u8g2.drawDisc(oledX, oledY, 8); // 人机:实心圆
      else if (x == currX && y == currY && !gameOver && flashFlag) u8g2.drawCircle(oledX, oledY, 8); // 选中空位:闪烁
    }
  }
}
// 7. 绘制赢棋连线
void drawWinLine() {
  if (winner == EMPTY) return;  // 无赢家时,不绘制连线
  u8g2.drawLine(winLine[0], winLine[1], winLine[2], winLine[3]);  // 绘制提前记录的赢棋连线
}
// 8. 绘制顶部提示栏(新增:gameOver时显示“长按S5重启”提示,让用户知晓操作)
void drawTipBar() {
  u8g2.setFont(u8g2_font_6x12_tf);  // 适配128x64屏幕的小字体,避免文字溢出
  if (gameOver) {
    // 先显示输赢/平局结果
    if (winner == PLAYER) u8g2.drawStr(30, 12, "你赢了!");
    else if (winner == AI) u8g2.drawStr(30, 12, "人机赢了!");
    else u8g2.drawStr(30, 12, "平局!");
    // 新增:显示重启提示,位置在结果下方,用户一眼能看到
    u8g2.drawStr(15, 22, "长按S5 重新开始");
  } else {
    // 非游戏结束时,显示回合提示
    if (isPlayerTurn) u8g2.drawStr(20, 12, "你的回合(空心圆)");
    else u8g2.drawStr(20, 12, "人机回合(实心圆)");
  }
}
 
 落子逻辑复制代码// 9. 检查是否赢棋
bool checkWin(int player) {
  // 检查3行
  for (int y = 0; y < 3; y++) {
    if (chessBoard[y][0] == player && chessBoard[y][1] == player && chessBoard[y][2] == player) {
      chessToOled(0, y, winLine[0], winLine[1]);  // 记录行连线起点
      chessToOled(2, y, winLine[2], winLine[3]);  // 记录行连线终点
      return true;
    }
  }
  // 检查3列
  for (int x = 0; x < 3; x++) {
    if (chessBoard[0][x] == player && chessBoard[1][x] == player && chessBoard[2][x] == player) {
      chessToOled(x, 0, winLine[0], winLine[1]);  // 记录列连线起点
      chessToOled(x, 2, winLine[2], winLine[3]);  // 记录列连线终点
      return true;
    }
  }
  // 检查左上-右下对角线
  if (chessBoard[0][0] == player && chessBoard[1][1] == player && chessBoard[2][2] == player) {
    chessToOled(0, 0, winLine[0], winLine[1]);  // 记录对角线起点
    chessToOled(2, 2, winLine[2], winLine[3]);  // 记录对角线终点
    return true;
  }
  // 检查右上-左下对角线
  if (chessBoard[0][2] == player && chessBoard[1][1] == player && chessBoard[2][0] == player) {
    chessToOled(2, 0, winLine[0], winLine[1]);  // 记录对角线起点
    chessToOled(0, 2, winLine[2], winLine[3]);  // 记录对角线终点
    return true;
  }
  return false;
}
// 10. 检查是否平局
bool checkDraw() {
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      if (chessBoard[y][x] == EMPTY) return false;  // 只要有一个空位,就不是平局
    }
  }
  return true;  // 棋盘满且无赢家,判定为平局
}
 
 游戏参数重置复制代码// 11. 人机落子逻辑
void aiMove() {
  if (gameOver) return;  // 游戏结束,不执行AI落子
  // 第一步:优先自己赢(试落子,能赢就落)
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      if (chessBoard[y][x] == EMPTY) {
        chessBoard[y][x] = AI;  // 试落AI棋子
        if (checkWin(AI)) {     // 试落后果:AI赢
          winner = AI;          // 标记AI为赢家
          gameOver = true;      // 标记游戏结束
          isPlayerTurn = true;  // 重置回合,不影响重启
          return;
        }
        chessBoard[y][x] = EMPTY;  // 回溯:取消试落,避免影响后续判断
      }
    }
  }
  // 第二步:防玩家赢(试落玩家棋子,堵玩家赢点)
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      if (chessBoard[y][x] == EMPTY) {
        chessBoard[y][x] = PLAYER;  // 试落玩家棋子
        if (checkWin(PLAYER)) {     // 试落后果:玩家要赢
          chessBoard[y][x] = AI;    // 落AI棋子,堵住这个赢点
          isPlayerTurn = true;      // 切换回玩家回合
          return;
        }
        chessBoard[y][x] = EMPTY;  // 回溯:取消试落
      }
    }
  }
  // 第三步:随机落子(无赢/防赢机会时,避免AI落子固定)
  while (true) {
    int x = random(0, 3);
    int y = random(0, 3);
    if (chessBoard[y][x] == EMPTY) {
      chessBoard[y][x] = AI;
      isPlayerTurn = true;
      return;
    }
  }
}
 
 初始化函数复制代码void resetGame() {
  memset(chessBoard, 0, sizeof(chessBoard));  // 重置棋盘:所有位置变为“空”
  currX = 1; currY = 1;                       // 重置选中位置:回到棋盘正中间
  isPlayerTurn = true;                        // 重置回合:玩家先落子(符合常规习惯)
  gameOver = false;                           // 重置游戏状态:从“结束”变为“进行中”
  winner = EMPTY;                             // 重置赢家:无赢家
  memset(winLine, 0, sizeof(winLine));        // 重置赢棋连线:清空连线坐标
  // 重置长按相关变量:避免重启后残留按键状态,导致误触发
  pressStartTime = 0;
  isKey5Pressed = false;
}
 
 复制代码// 12. 初始化函数
void setup() {
  Serial.begin(115200);  // 初始化串口,方便调试AD按键值(可选关闭)
  u8g2.begin();          // 初始化OLED(I2C通信,自动识别设备地址)
  u8g2.clearBuffer();    // 清空OLED缓存,避免残留乱码
  resetGame();           // 调用新增的重置函数,初始化首次游戏参数(替代原重复代码)
  randomSeed(analogRead(A1));  // 用空闲模拟引脚做随机种子,让AI落子更随机
 主循环
 
 完整代码复制代码void loop() {
  int key = readADKey();  // 读取当前按键(先获取按键值,再分状态处理)
  if (!gameOver && isPlayerTurn) {
    // 状态1:游戏进行中+玩家回合
    switch (key) {
      case 1:  // S1:向上(y坐标-1,避免超出棋盘上边界0)
        if (currY > 0) currY--;
        break;
      case 2:  // S2:向左(x坐标-1,避免超出棋盘左边界0)
        if (currX > 0) currX--;
        break;
      case 3:  // S3:向下(y坐标+1,避免超出棋盘下边界2)
        if (currY < 2) currY++;
        break;
      case 4:  // S4:向右(x坐标+1,避免超出棋盘右边界2)
        if (currX < 2) currX++;
        break;
      case 5:  // S5:确定落子(仅当前位置为空时有效,避免重复落子)
        if (chessBoard[currY][currX] == EMPTY) {
          chessBoard[currY][currX] = PLAYER;  // 落玩家棋子(空心圆)
          if (checkWin(PLAYER)) {             // 检查玩家是否赢
            winner = PLAYER;
            gameOver = true;
          } else if (checkDraw()) {           // 检查是否平局
            gameOver = true;
          } else {
            isPlayerTurn = false;             // 切换到人机回合
          }
        }
        break;
    }
  } else if (!gameOver && !isPlayerTurn) {
    // 状态2:游戏进行中+人机回合
    delay(500);
    aiMove();  // 执行AI落子
    // 落子后检查结果:人机赢或平局
    if (checkWin(AI)) {
      winner = AI;
      gameOver = true;
    } else if (checkDraw()) {
      gameOver = true;
    }
  } else {
    // 新增:状态3:游戏结束(赢/输/平局通用),处理S5长按重启
    switch (key) {
      case 5:  // 检测到S5按键按下
        if (!isKey5Pressed) {  // 仅当按键未被标记为“按下”时,记录起始时间(避免重复记录)
          isKey5Pressed = true;                  // 标记S5已按下
          pressStartTime = millis();             // 记录按下的起始时间(单位:毫秒)
        }
        break;
      case -1:  // 检测到无按键按下(即S5已松开,判断是否为有效长按)
        if (isKey5Pressed) {  // 之前S5被按下过,现在松开,开始计算时长
          unsigned long pressDuration = millis() - pressStartTime;  // 计算按键按下的总时长
          if (pressDuration >= LONG_PRESS_TIME) {  // 时长≥1秒,判定为有效长按
            resetGame();  // 调用新增的重置函数,重启游戏
          }
          // 无论是否长按成功,都重置长按相关变量,为下次长按做准备
          isKey5Pressed = false;
          pressStartTime = 0;
        }
        break;
    }
  }
  // OLED显示更新(原逻辑不变,按“提示栏→棋盘→棋子→连线”顺序,避免遮挡)
  u8g2.clearBuffer();
  drawTipBar();
  drawChessBoard();
  drawChess();
  drawWinLine();
  u8g2.sendBuffer();
}
 
 进行上传后即可问题说明:1.若按键反馈有问题,你可以修改按键输入阈值。2.当前代码为第一版本,可能代码中会有部分bug,最终版已完成,暂时没有时间发表,请期待最终稳定板。复制代码#include <Arduino.h>
#include <U8g2lib.h>
#define I2C_SDA 9    // ESP32-C5 专用 I2C 数据引脚(SDA)
#define I2C_SCL 10   // ESP32-C5 专用 I2C 时钟引脚(SCL)
#define AD_KEY_PIN A2  // ADKeyboard模拟输入引脚
#define EMPTY 0        // 棋盘空状态
#define PLAYER 1       // 玩家(空心圆)
#define AI 2           // 人机(实心圆)
#define LONG_PRESS_TIME 1000
// 1. OLED初始化
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
int chessBoard[3][3] = {0};  // 3x3棋盘,存储落子状态
int currX = 1, currY = 1;    // 当前选中的棋盘坐标(0-2,初始居中)
bool isPlayerTurn = true;    // 是否玩家回合(true=玩家,false=人机)
bool gameOver = false;       // 游戏是否结束
int winner = EMPTY;          // 赢家(0=未分胜负,1=玩家,2=人机)
int winLine[4] = {0};        // 赢棋连线坐标(x1,y1,x2,y2)
// 长按重启相关全局变量(跟踪按键按下时长,避免误触)
unsigned long pressStartTime = 0;  // 记录S5按键按下的起始时间(毫秒)
bool isKey5Pressed = false;        // 标记S5是否处于按下状态(避免重复触发)
// 3. ADKeyboard按键识别
int readADKey() {
  int adVal = analogRead(AD_KEY_PIN);
  delay(100);  // 消抖:避免按键机械抖动导致的误读数
  if (adVal >= 2500 && adVal <= 2599) return 1;    // S1(上)
  else if (adVal >= 2600 && adVal <= 2699) return 2;  // S2(左)
  else if (adVal >= 2700 && adVal <= 2799) return 3;  // S3(下)
  else if (adVal >= 2800 && adVal <= 2899) return 4;  // S4(右)
  else if (adVal >= 3000 && adVal <= 3099) return 5;  // S5(确定/长按重启)
  else return -1;  // 无按键按下,返回-1
}
// 4. 棋盘坐标转OLED像素坐标
void chessToOled(int chessX, int chessY, int &oledX, int &oledY) {
  oledX = 20 + chessX * 28;  // 横向:左偏移20(居中),每个格子宽28像素
  oledY = 20 + chessY * 22;  // 纵向:上偏移20(留提示栏),每个格子高22像素
}
// 5. 绘制棋盘线(无修改,确保3x3格子清晰)
void drawChessBoard() {
  u8g2.drawLine(48, 20, 48, 62);  // 第一条竖线(分割第1、2列)
  u8g2.drawLine(76, 20, 76, 62);  // 第二条竖线(分割第2、3列)
  u8g2.drawLine(20, 42, 98, 42);  // 第一条横线(分割第1、2行)
  u8g2.drawLine(20, 64, 98, 64);  // 第二条横线(分割第2、3行)
}
// 6. 绘制棋子
void drawChess() {
  static unsigned long lastFlashTime = 0;
  static bool flashFlag = true;  // 闪烁标志(300ms切换一次,实现选中闪烁)
  if (millis() - lastFlashTime > 300) {
    flashFlag = !flashFlag;
    lastFlashTime = millis();
  }
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      int oledX, oledY;
      chessToOled(x, y, oledX, oledY);
      if (chessBoard[y][x] == PLAYER) u8g2.drawCircle(oledX, oledY, 8);  // 玩家:空心圆
      else if (chessBoard[y][x] == AI) u8g2.drawDisc(oledX, oledY, 8); // 人机:实心圆
      else if (x == currX && y == currY && !gameOver && flashFlag) u8g2.drawCircle(oledX, oledY, 8); // 选中空位:闪烁
    }
  }
}
// 7. 绘制赢棋连线
void drawWinLine() {
  if (winner == EMPTY) return;  // 无赢家时,不绘制连线
  u8g2.drawLine(winLine[0], winLine[1], winLine[2], winLine[3]);  // 绘制提前记录的赢棋连线
}
// 8. 绘制顶部提示栏(新增:gameOver时显示“长按S5重启”提示,让用户知晓操作)
void drawTipBar() {
  u8g2.setFont(u8g2_font_6x12_tf);  // 适配128x64屏幕的小字体,避免文字溢出
  if (gameOver) {
    // 先显示输赢/平局结果
    if (winner == PLAYER) u8g2.drawStr(30, 12, "你赢了!");
    else if (winner == AI) u8g2.drawStr(30, 12, "人机赢了!");
    else u8g2.drawStr(30, 12, "平局!");
    // 新增:显示重启提示,位置在结果下方,用户一眼能看到
    u8g2.drawStr(15, 22, "长按S5 重新开始");
  } else {
    // 非游戏结束时,显示回合提示(原逻辑不变)
    if (isPlayerTurn) u8g2.drawStr(20, 12, "你的回合(空心圆)");
    else u8g2.drawStr(20, 12, "人机回合(实心圆)");
  }
}
// 9. 检查是否赢棋(无修改,确保赢棋判定准确)
bool checkWin(int player) {
  // 检查3行
  for (int y = 0; y < 3; y++) {
    if (chessBoard[y][0] == player && chessBoard[y][1] == player && chessBoard[y][2] == player) {
      chessToOled(0, y, winLine[0], winLine[1]);  // 记录行连线起点
      chessToOled(2, y, winLine[2], winLine[3]);  // 记录行连线终点
      return true;
    }
  }
  // 检查3列
  for (int x = 0; x < 3; x++) {
    if (chessBoard[0][x] == player && chessBoard[1][x] == player && chessBoard[2][x] == player) {
      chessToOled(x, 0, winLine[0], winLine[1]);  // 记录列连线起点
      chessToOled(x, 2, winLine[2], winLine[3]);  // 记录列连线终点
      return true;
    }
  }
  // 检查左上-右下对角线
  if (chessBoard[0][0] == player && chessBoard[1][1] == player && chessBoard[2][2] == player) {
    chessToOled(0, 0, winLine[0], winLine[1]);  // 记录对角线起点
    chessToOled(2, 2, winLine[2], winLine[3]);  // 记录对角线终点
    return true;
  }
  // 检查右上-左下对角线
  if (chessBoard[0][2] == player && chessBoard[1][1] == player && chessBoard[2][0] == player) {
    chessToOled(2, 0, winLine[0], winLine[1]);  // 记录对角线起点
    chessToOled(0, 2, winLine[2], winLine[3]);  // 记录对角线终点
    return true;
  }
  return false;
}
// 10. 检查是否平局
bool checkDraw() {
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      if (chessBoard[y][x] == EMPTY) return false;  // 只要有一个空位,就不是平局
    }
  }
  return true;  // 棋盘满且无赢家,判定为平局
}
// 11. 人机落子逻辑(无修改,保持“优先赢、次防输、最后随机”策略)
void aiMove() {
  if (gameOver) return;  // 游戏结束,不执行AI落子
  // 第一步:优先自己赢(试落子,能赢就落)
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      if (chessBoard[y][x] == EMPTY) {
        chessBoard[y][x] = AI;  // 试落AI棋子
        if (checkWin(AI)) {     // 试落后果:AI赢
          winner = AI;          // 标记AI为赢家
          gameOver = true;      // 标记游戏结束
          isPlayerTurn = true;  // 重置回合,不影响重启
          return;
        }
        chessBoard[y][x] = EMPTY;  // 回溯:取消试落,避免影响后续判断
      }
    }
  }
  // 第二步:防玩家赢(试落玩家棋子,堵玩家赢点)
  for (int y = 0; y < 3; y++) {
    for (int x = 0; x < 3; x++) {
      if (chessBoard[y][x] == EMPTY) {
        chessBoard[y][x] = PLAYER;  // 试落玩家棋子
        if (checkWin(PLAYER)) {     // 试落后果:玩家要赢
          chessBoard[y][x] = AI;    // 落AI棋子,堵住这个赢点
          isPlayerTurn = true;      // 切换回玩家回合
          return;
        }
        chessBoard[y][x] = EMPTY;  // 回溯:取消试落
      }
    }
  }
  // 第三步:随机落子(无赢/防赢机会时,避免AI落子固定)
  while (true) {
    int x = random(0, 3);
    int y = random(0, 3);
    if (chessBoard[y][x] == EMPTY) {
      chessBoard[y][x] = AI;
      isPlayerTurn = true;
      return;
    }
  }
}
// 新增:游戏参数重置函数(单独封装,避免代码重复,长按重启时调用)
void resetGame() {
  memset(chessBoard, 0, sizeof(chessBoard));  // 重置棋盘:所有位置变为“空”
  currX = 1; currY = 1;                       // 重置选中位置:回到棋盘正中间
  isPlayerTurn = true;                        // 重置回合:玩家先落子(符合常规习惯)
  gameOver = false;                           // 重置游戏状态:从“结束”变为“进行中”
  winner = EMPTY;                             // 重置赢家:无赢家
  memset(winLine, 0, sizeof(winLine));        // 重置赢棋连线:清空连线坐标
  // 重置长按相关变量:避免重启后残留按键状态,导致误触发
  pressStartTime = 0;
  isKey5Pressed = false;
}
// 12. 初始化函数
void setup() {
  Serial.begin(115200);  // 初始化串口,方便调试AD按键值
  u8g2.begin();          // 初始化OLED(I2C通信,自动识别设备地址)
  u8g2.clearBuffer();    // 清空OLED缓存,避免残留乱码
  resetGame();           // 调用新增的重置函数,初始化首次游戏参数(替代原重复代码)
  randomSeed(analogRead(A1));  // 用空闲模拟引脚做随机种子,让AI落子更随机
}
// 13. 主循环(修改:新增gameOver状态下的长按S5处理逻辑)
void loop() {
  int key = readADKey();  // 读取当前按键(先获取按键值,再分状态处理)
  if (!gameOver && isPlayerTurn) {
    // 状态1:游戏进行中+玩家回合(原逻辑不变,处理上下左右移动和确定落子)
    switch (key) {
      case 1:  // S1:向上(y坐标-1,避免超出棋盘上边界0)
        if (currY > 0) currY--;
        break;
      case 2:  // S2:向左(x坐标-1,避免超出棋盘左边界0)
        if (currX > 0) currX--;
        break;
      case 3:  // S3:向下(y坐标+1,避免超出棋盘下边界2)
        if (currY < 2) currY++;
        break;
      case 4:  // S4:向右(x坐标+1,避免超出棋盘右边界2)
        if (currX < 2) currX++;
        break;
      case 5:  // S5:确定落子(仅当前位置为空时有效,避免重复落子)
        if (chessBoard[currY][currX] == EMPTY) {
          chessBoard[currY][currX] = PLAYER;  // 落玩家棋子(空心圆)
          if (checkWin(PLAYER)) {             // 检查玩家是否赢
            winner = PLAYER;
            gameOver = true;
          } else if (checkDraw()) {           // 检查是否平局
            gameOver = true;
          } else {
            isPlayerTurn = false;             // 切换到人机回合
          }
        }
        break;
    }
  } else if (!gameOver && !isPlayerTurn) {
    // 状态2:游戏进行中+人机回合
    delay(500);
    aiMove();  // 执行AI落子
    // 落子后检查结果:人机赢或平局
    if (checkWin(AI)) {
      winner = AI;
      gameOver = true;
    } else if (checkDraw()) {
      gameOver = true;
    }
  } else {
    // 新增:状态3:游戏结束(赢/输/平局通用),处理S5长按重启
    switch (key) {
      case 5:  // 检测到S5按键按下
        if (!isKey5Pressed) {  // 仅当按键未被标记为“按下”时,记录起始时间(避免重复记录)
          isKey5Pressed = true;                  // 标记S5已按下
          pressStartTime = millis();             // 记录按下的起始时间(单位:毫秒)
        }
        break;
      case -1:  // 检测到无按键按下(即S5已松开,判断是否为有效长按)
        if (isKey5Pressed) {  // 之前S5被按下过,现在松开,开始计算时长
          unsigned long pressDuration = millis() - pressStartTime;  // 计算按键按下的总时长
          if (pressDuration >= LONG_PRESS_TIME) {  // 时长≥1秒,判定为有效长按
            resetGame();  // 调用新增的重置函数,重启游戏
          }
          // 无论是否长按成功,都重置长按相关变量,为下次长按做准备
          isKey5Pressed = false;
          pressStartTime = 0;
        }
        break;
    }
  }
  // OLED显示更新
  u8g2.clearBuffer();
  drawTipBar();
  drawChessBoard();
  drawChess();
  drawWinLine();
  u8g2.sendBuffer();
}
 
 
 
 |