17浏览
查看: 17|回复: 0

[ESP8266/ESP32] 基于ESP32-C5开发板驱动OLED井字棋小游戏

[复制链接]
本帖最后由 创客编程张 于 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-C5
  • OLED显示屏(128*64)
  • Keyboard模拟按键

准备完成,我们开始干正事
将OLED连接到开发板的I2C引脚上,将Keyboard模拟按键连接至引脚2
接下来编写程序
首先初始化
  1. #include <Arduino.h>
  2. #include <U8g2lib.h>
  3. #define I2C_SDA 9    // ESP32-C5 专用 I2C 数据引脚(SDA)
  4. #define I2C_SCL 10   // ESP32-C5 专用 I2C 时钟引脚(SCL)
  5. #define AD_KEY_PIN A2  // ADKeyboard模拟输入引脚
  6. #define EMPTY 0        // 棋盘空状态
  7. #define PLAYER 1       // 玩家(空心圆)
  8. #define AI 2           // 人机(实心圆)
  9. #define LONG_PRESS_TIME 1000  // 新增:长按判定时长(1000ms=1秒,可调整)
  10. // 1. OLED初始化(0.96寸单色I2C,根据屏幕芯片调整)
  11. U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
复制代码
变量
  1. int chessBoard[3][3] = {0};  // 3x3棋盘,存储落子状态
  2. int currX = 1, currY = 1;    // 当前选中的棋盘坐标(0-2,初始居中)
  3. bool isPlayerTurn = true;    // 是否玩家回合(true=玩家,false=人机)
  4. bool gameOver = false;       // 游戏是否结束
  5. int winner = EMPTY;          // 赢家(0=未分胜负,1=玩家,2=人机)
  6. int winLine[4] = {0};        // 赢棋连线坐标(x1,y1,x2,y2)
复制代码
keyboard模拟按键
  1. unsigned long pressStartTime = 0;  // 记录S5按键按下的起始时间(毫秒)
  2. bool isKey5Pressed = false;        // 标记S5是否处于按下状态(避免重复触发)
  3. // 3. ADKeyboard按键识别
  4. int readADKey() {
  5.   int adVal = analogRead(AD_KEY_PIN);
  6.   delay(100);  // 消抖:避免按键机械抖动导致的误读数
  7.   if (adVal >= 2500 && adVal <= 2599) return 1;    // S1(上)
  8.   else if (adVal >= 2600 && adVal <= 2699) return 2;  // S2(左)
  9.   else if (adVal >= 2700 && adVal <= 2799) return 3;  // S3(下)
  10.   else if (adVal >= 2800 && adVal <= 2899) return 4;  // S4(右)
  11.   else if (adVal >= 3000 && adVal <= 3099) return 5;  // S5(确定/长按重启)
  12.   else return -1;  // 无按键按下,返回-1
复制代码
棋盘显示
  1. // 4. 棋盘坐标转OLED像素坐标
  2. void chessToOled(int chessX, int chessY, int &oledX, int &oledY) {
  3.   oledX = 20 + chessX * 28;  // 横向:左偏移20(居中),每个格子宽28像素
  4.   oledY = 20 + chessY * 22;  // 纵向:上偏移20(留提示栏),每个格子高22像素
  5. }
  6. // 5. 绘制棋盘线(无修改,确保3x3格子清晰)
  7. void drawChessBoard() {
  8.   u8g2.drawLine(48, 20, 48, 62);  // 第一条竖线(分割第1、2列)
  9.   u8g2.drawLine(76, 20, 76, 62);  // 第二条竖线(分割第2、3列)
  10.   u8g2.drawLine(20, 42, 98, 42);  // 第一条横线(分割第1、2行)
  11.   u8g2.drawLine(20, 64, 98, 64);  // 第二条横线(分割第2、3行)
  12. }
  13. // 6. 绘制棋子(无修改,保留闪烁选中效果)
  14. void drawChess() {
  15.   static unsigned long lastFlashTime = 0;
  16.   static bool flashFlag = true;  // 闪烁标志(300ms切换一次,实现选中闪烁)
  17.   if (millis() - lastFlashTime > 300) {
  18.     flashFlag = !flashFlag;
  19.     lastFlashTime = millis();
  20.   }
  21.   for (int y = 0; y < 3; y++) {
  22.     for (int x = 0; x < 3; x++) {
  23.       int oledX, oledY;
  24.       chessToOled(x, y, oledX, oledY);
  25.       if (chessBoard[y][x] == PLAYER) u8g2.drawCircle(oledX, oledY, 8);  // 玩家:空心圆
  26.       else if (chessBoard[y][x] == AI) u8g2.drawDisc(oledX, oledY, 8); // 人机:实心圆
  27.       else if (x == currX && y == currY && !gameOver && flashFlag) u8g2.drawCircle(oledX, oledY, 8); // 选中空位:闪烁
  28.     }
  29.   }
  30. }
  31. // 7. 绘制赢棋连线
  32. void drawWinLine() {
  33.   if (winner == EMPTY) return;  // 无赢家时,不绘制连线
  34.   u8g2.drawLine(winLine[0], winLine[1], winLine[2], winLine[3]);  // 绘制提前记录的赢棋连线
  35. }
  36. // 8. 绘制顶部提示栏(新增:gameOver时显示“长按S5重启”提示,让用户知晓操作)
  37. void drawTipBar() {
  38.   u8g2.setFont(u8g2_font_6x12_tf);  // 适配128x64屏幕的小字体,避免文字溢出
  39.   if (gameOver) {
  40.     // 先显示输赢/平局结果
  41.     if (winner == PLAYER) u8g2.drawStr(30, 12, "你赢了!");
  42.     else if (winner == AI) u8g2.drawStr(30, 12, "人机赢了!");
  43.     else u8g2.drawStr(30, 12, "平局!");
  44.     // 新增:显示重启提示,位置在结果下方,用户一眼能看到
  45.     u8g2.drawStr(15, 22, "长按S5 重新开始");
  46.   } else {
  47.     // 非游戏结束时,显示回合提示
  48.     if (isPlayerTurn) u8g2.drawStr(20, 12, "你的回合(空心圆)");
  49.     else u8g2.drawStr(20, 12, "人机回合(实心圆)");
  50.   }
  51. }
复制代码
输赢检查
  1. // 9. 检查是否赢棋
  2. bool checkWin(int player) {
  3.   // 检查3行
  4.   for (int y = 0; y < 3; y++) {
  5.     if (chessBoard[y][0] == player && chessBoard[y][1] == player && chessBoard[y][2] == player) {
  6.       chessToOled(0, y, winLine[0], winLine[1]);  // 记录行连线起点
  7.       chessToOled(2, y, winLine[2], winLine[3]);  // 记录行连线终点
  8.       return true;
  9.     }
  10.   }
  11.   // 检查3列
  12.   for (int x = 0; x < 3; x++) {
  13.     if (chessBoard[0][x] == player && chessBoard[1][x] == player && chessBoard[2][x] == player) {
  14.       chessToOled(x, 0, winLine[0], winLine[1]);  // 记录列连线起点
  15.       chessToOled(x, 2, winLine[2], winLine[3]);  // 记录列连线终点
  16.       return true;
  17.     }
  18.   }
  19.   // 检查左上-右下对角线
  20.   if (chessBoard[0][0] == player && chessBoard[1][1] == player && chessBoard[2][2] == player) {
  21.     chessToOled(0, 0, winLine[0], winLine[1]);  // 记录对角线起点
  22.     chessToOled(2, 2, winLine[2], winLine[3]);  // 记录对角线终点
  23.     return true;
  24.   }
  25.   // 检查右上-左下对角线
  26.   if (chessBoard[0][2] == player && chessBoard[1][1] == player && chessBoard[2][0] == player) {
  27.     chessToOled(2, 0, winLine[0], winLine[1]);  // 记录对角线起点
  28.     chessToOled(0, 2, winLine[2], winLine[3]);  // 记录对角线终点
  29.     return true;
  30.   }
  31.   return false;
  32. }
  33. // 10. 检查是否平局
  34. bool checkDraw() {
  35.   for (int y = 0; y < 3; y++) {
  36.     for (int x = 0; x < 3; x++) {
  37.       if (chessBoard[y][x] == EMPTY) return false;  // 只要有一个空位,就不是平局
  38.     }
  39.   }
  40.   return true;  // 棋盘满且无赢家,判定为平局
  41. }
复制代码
落子逻辑
  1. // 11. 人机落子逻辑
  2. void aiMove() {
  3.   if (gameOver) return;  // 游戏结束,不执行AI落子
  4.   // 第一步:优先自己赢(试落子,能赢就落)
  5.   for (int y = 0; y < 3; y++) {
  6.     for (int x = 0; x < 3; x++) {
  7.       if (chessBoard[y][x] == EMPTY) {
  8.         chessBoard[y][x] = AI;  // 试落AI棋子
  9.         if (checkWin(AI)) {     // 试落后果:AI赢
  10.           winner = AI;          // 标记AI为赢家
  11.           gameOver = true;      // 标记游戏结束
  12.           isPlayerTurn = true;  // 重置回合,不影响重启
  13.           return;
  14.         }
  15.         chessBoard[y][x] = EMPTY;  // 回溯:取消试落,避免影响后续判断
  16.       }
  17.     }
  18.   }
  19.   // 第二步:防玩家赢(试落玩家棋子,堵玩家赢点)
  20.   for (int y = 0; y < 3; y++) {
  21.     for (int x = 0; x < 3; x++) {
  22.       if (chessBoard[y][x] == EMPTY) {
  23.         chessBoard[y][x] = PLAYER;  // 试落玩家棋子
  24.         if (checkWin(PLAYER)) {     // 试落后果:玩家要赢
  25.           chessBoard[y][x] = AI;    // 落AI棋子,堵住这个赢点
  26.           isPlayerTurn = true;      // 切换回玩家回合
  27.           return;
  28.         }
  29.         chessBoard[y][x] = EMPTY;  // 回溯:取消试落
  30.       }
  31.     }
  32.   }
  33.   // 第三步:随机落子(无赢/防赢机会时,避免AI落子固定)
  34.   while (true) {
  35.     int x = random(0, 3);
  36.     int y = random(0, 3);
  37.     if (chessBoard[y][x] == EMPTY) {
  38.       chessBoard[y][x] = AI;
  39.       isPlayerTurn = true;
  40.       return;
  41.     }
  42.   }
  43. }
复制代码
游戏参数重置
  1. void resetGame() {
  2.   memset(chessBoard, 0, sizeof(chessBoard));  // 重置棋盘:所有位置变为“空”
  3.   currX = 1; currY = 1;                       // 重置选中位置:回到棋盘正中间
  4.   isPlayerTurn = true;                        // 重置回合:玩家先落子(符合常规习惯)
  5.   gameOver = false;                           // 重置游戏状态:从“结束”变为“进行中”
  6.   winner = EMPTY;                             // 重置赢家:无赢家
  7.   memset(winLine, 0, sizeof(winLine));        // 重置赢棋连线:清空连线坐标
  8.   // 重置长按相关变量:避免重启后残留按键状态,导致误触发
  9.   pressStartTime = 0;
  10.   isKey5Pressed = false;
  11. }
复制代码
初始化函数
  1. // 12. 初始化函数
  2. void setup() {
  3.   Serial.begin(115200);  // 初始化串口,方便调试AD按键值(可选关闭)
  4.   u8g2.begin();          // 初始化OLED(I2C通信,自动识别设备地址)
  5.   u8g2.clearBuffer();    // 清空OLED缓存,避免残留乱码
  6.   resetGame();           // 调用新增的重置函数,初始化首次游戏参数(替代原重复代码)
  7.   randomSeed(analogRead(A1));  // 用空闲模拟引脚做随机种子,让AI落子更随机
复制代码

主循环
  1. void loop() {
  2.   int key = readADKey();  // 读取当前按键(先获取按键值,再分状态处理)
  3.   if (!gameOver && isPlayerTurn) {
  4.     // 状态1:游戏进行中+玩家回合
  5.     switch (key) {
  6.       case 1:  // S1:向上(y坐标-1,避免超出棋盘上边界0)
  7.         if (currY > 0) currY--;
  8.         break;
  9.       case 2:  // S2:向左(x坐标-1,避免超出棋盘左边界0)
  10.         if (currX > 0) currX--;
  11.         break;
  12.       case 3:  // S3:向下(y坐标+1,避免超出棋盘下边界2)
  13.         if (currY < 2) currY++;
  14.         break;
  15.       case 4:  // S4:向右(x坐标+1,避免超出棋盘右边界2)
  16.         if (currX < 2) currX++;
  17.         break;
  18.       case 5:  // S5:确定落子(仅当前位置为空时有效,避免重复落子)
  19.         if (chessBoard[currY][currX] == EMPTY) {
  20.           chessBoard[currY][currX] = PLAYER;  // 落玩家棋子(空心圆)
  21.           if (checkWin(PLAYER)) {             // 检查玩家是否赢
  22.             winner = PLAYER;
  23.             gameOver = true;
  24.           } else if (checkDraw()) {           // 检查是否平局
  25.             gameOver = true;
  26.           } else {
  27.             isPlayerTurn = false;             // 切换到人机回合
  28.           }
  29.         }
  30.         break;
  31.     }
  32.   } else if (!gameOver && !isPlayerTurn) {
  33.     // 状态2:游戏进行中+人机回合
  34.     delay(500);
  35.     aiMove();  // 执行AI落子
  36.     // 落子后检查结果:人机赢或平局
  37.     if (checkWin(AI)) {
  38.       winner = AI;
  39.       gameOver = true;
  40.     } else if (checkDraw()) {
  41.       gameOver = true;
  42.     }
  43.   } else {
  44.     // 新增:状态3:游戏结束(赢/输/平局通用),处理S5长按重启
  45.     switch (key) {
  46.       case 5:  // 检测到S5按键按下
  47.         if (!isKey5Pressed) {  // 仅当按键未被标记为“按下”时,记录起始时间(避免重复记录)
  48.           isKey5Pressed = true;                  // 标记S5已按下
  49.           pressStartTime = millis();             // 记录按下的起始时间(单位:毫秒)
  50.         }
  51.         break;
  52.       case -1:  // 检测到无按键按下(即S5已松开,判断是否为有效长按)
  53.         if (isKey5Pressed) {  // 之前S5被按下过,现在松开,开始计算时长
  54.           unsigned long pressDuration = millis() - pressStartTime;  // 计算按键按下的总时长
  55.           if (pressDuration >= LONG_PRESS_TIME) {  // 时长≥1秒,判定为有效长按
  56.             resetGame();  // 调用新增的重置函数,重启游戏
  57.           }
  58.           // 无论是否长按成功,都重置长按相关变量,为下次长按做准备
  59.           isKey5Pressed = false;
  60.           pressStartTime = 0;
  61.         }
  62.         break;
  63.     }
  64.   }
  65.   // OLED显示更新(原逻辑不变,按“提示栏→棋盘→棋子→连线”顺序,避免遮挡)
  66.   u8g2.clearBuffer();
  67.   drawTipBar();
  68.   drawChessBoard();
  69.   drawChess();
  70.   drawWinLine();
  71.   u8g2.sendBuffer();
  72. }
复制代码
完整代码
  1. #include <Arduino.h>
  2. #include <U8g2lib.h>
  3. #define I2C_SDA 9    // ESP32-C5 专用 I2C 数据引脚(SDA)
  4. #define I2C_SCL 10   // ESP32-C5 专用 I2C 时钟引脚(SCL)
  5. #define AD_KEY_PIN A2  // ADKeyboard模拟输入引脚
  6. #define EMPTY 0        // 棋盘空状态
  7. #define PLAYER 1       // 玩家(空心圆)
  8. #define AI 2           // 人机(实心圆)
  9. #define LONG_PRESS_TIME 1000
  10. // 1. OLED初始化
  11. U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
  12. int chessBoard[3][3] = {0};  // 3x3棋盘,存储落子状态
  13. int currX = 1, currY = 1;    // 当前选中的棋盘坐标(0-2,初始居中)
  14. bool isPlayerTurn = true;    // 是否玩家回合(true=玩家,false=人机)
  15. bool gameOver = false;       // 游戏是否结束
  16. int winner = EMPTY;          // 赢家(0=未分胜负,1=玩家,2=人机)
  17. int winLine[4] = {0};        // 赢棋连线坐标(x1,y1,x2,y2)
  18. // 长按重启相关全局变量(跟踪按键按下时长,避免误触)
  19. unsigned long pressStartTime = 0;  // 记录S5按键按下的起始时间(毫秒)
  20. bool isKey5Pressed = false;        // 标记S5是否处于按下状态(避免重复触发)
  21. // 3. ADKeyboard按键识别
  22. int readADKey() {
  23.   int adVal = analogRead(AD_KEY_PIN);
  24.   delay(100);  // 消抖:避免按键机械抖动导致的误读数
  25.   if (adVal >= 2500 && adVal <= 2599) return 1;    // S1(上)
  26.   else if (adVal >= 2600 && adVal <= 2699) return 2;  // S2(左)
  27.   else if (adVal >= 2700 && adVal <= 2799) return 3;  // S3(下)
  28.   else if (adVal >= 2800 && adVal <= 2899) return 4;  // S4(右)
  29.   else if (adVal >= 3000 && adVal <= 3099) return 5;  // S5(确定/长按重启)
  30.   else return -1;  // 无按键按下,返回-1
  31. }
  32. // 4. 棋盘坐标转OLED像素坐标
  33. void chessToOled(int chessX, int chessY, int &oledX, int &oledY) {
  34.   oledX = 20 + chessX * 28;  // 横向:左偏移20(居中),每个格子宽28像素
  35.   oledY = 20 + chessY * 22;  // 纵向:上偏移20(留提示栏),每个格子高22像素
  36. }
  37. // 5. 绘制棋盘线(无修改,确保3x3格子清晰)
  38. void drawChessBoard() {
  39.   u8g2.drawLine(48, 20, 48, 62);  // 第一条竖线(分割第1、2列)
  40.   u8g2.drawLine(76, 20, 76, 62);  // 第二条竖线(分割第2、3列)
  41.   u8g2.drawLine(20, 42, 98, 42);  // 第一条横线(分割第1、2行)
  42.   u8g2.drawLine(20, 64, 98, 64);  // 第二条横线(分割第2、3行)
  43. }
  44. // 6. 绘制棋子
  45. void drawChess() {
  46.   static unsigned long lastFlashTime = 0;
  47.   static bool flashFlag = true;  // 闪烁标志(300ms切换一次,实现选中闪烁)
  48.   if (millis() - lastFlashTime > 300) {
  49.     flashFlag = !flashFlag;
  50.     lastFlashTime = millis();
  51.   }
  52.   for (int y = 0; y < 3; y++) {
  53.     for (int x = 0; x < 3; x++) {
  54.       int oledX, oledY;
  55.       chessToOled(x, y, oledX, oledY);
  56.       if (chessBoard[y][x] == PLAYER) u8g2.drawCircle(oledX, oledY, 8);  // 玩家:空心圆
  57.       else if (chessBoard[y][x] == AI) u8g2.drawDisc(oledX, oledY, 8); // 人机:实心圆
  58.       else if (x == currX && y == currY && !gameOver && flashFlag) u8g2.drawCircle(oledX, oledY, 8); // 选中空位:闪烁
  59.     }
  60.   }
  61. }
  62. // 7. 绘制赢棋连线
  63. void drawWinLine() {
  64.   if (winner == EMPTY) return;  // 无赢家时,不绘制连线
  65.   u8g2.drawLine(winLine[0], winLine[1], winLine[2], winLine[3]);  // 绘制提前记录的赢棋连线
  66. }
  67. // 8. 绘制顶部提示栏(新增:gameOver时显示“长按S5重启”提示,让用户知晓操作)
  68. void drawTipBar() {
  69.   u8g2.setFont(u8g2_font_6x12_tf);  // 适配128x64屏幕的小字体,避免文字溢出
  70.   if (gameOver) {
  71.     // 先显示输赢/平局结果
  72.     if (winner == PLAYER) u8g2.drawStr(30, 12, "你赢了!");
  73.     else if (winner == AI) u8g2.drawStr(30, 12, "人机赢了!");
  74.     else u8g2.drawStr(30, 12, "平局!");
  75.     // 新增:显示重启提示,位置在结果下方,用户一眼能看到
  76.     u8g2.drawStr(15, 22, "长按S5 重新开始");
  77.   } else {
  78.     // 非游戏结束时,显示回合提示(原逻辑不变)
  79.     if (isPlayerTurn) u8g2.drawStr(20, 12, "你的回合(空心圆)");
  80.     else u8g2.drawStr(20, 12, "人机回合(实心圆)");
  81.   }
  82. }
  83. // 9. 检查是否赢棋(无修改,确保赢棋判定准确)
  84. bool checkWin(int player) {
  85.   // 检查3行
  86.   for (int y = 0; y < 3; y++) {
  87.     if (chessBoard[y][0] == player && chessBoard[y][1] == player && chessBoard[y][2] == player) {
  88.       chessToOled(0, y, winLine[0], winLine[1]);  // 记录行连线起点
  89.       chessToOled(2, y, winLine[2], winLine[3]);  // 记录行连线终点
  90.       return true;
  91.     }
  92.   }
  93.   // 检查3列
  94.   for (int x = 0; x < 3; x++) {
  95.     if (chessBoard[0][x] == player && chessBoard[1][x] == player && chessBoard[2][x] == player) {
  96.       chessToOled(x, 0, winLine[0], winLine[1]);  // 记录列连线起点
  97.       chessToOled(x, 2, winLine[2], winLine[3]);  // 记录列连线终点
  98.       return true;
  99.     }
  100.   }
  101.   // 检查左上-右下对角线
  102.   if (chessBoard[0][0] == player && chessBoard[1][1] == player && chessBoard[2][2] == player) {
  103.     chessToOled(0, 0, winLine[0], winLine[1]);  // 记录对角线起点
  104.     chessToOled(2, 2, winLine[2], winLine[3]);  // 记录对角线终点
  105.     return true;
  106.   }
  107.   // 检查右上-左下对角线
  108.   if (chessBoard[0][2] == player && chessBoard[1][1] == player && chessBoard[2][0] == player) {
  109.     chessToOled(2, 0, winLine[0], winLine[1]);  // 记录对角线起点
  110.     chessToOled(0, 2, winLine[2], winLine[3]);  // 记录对角线终点
  111.     return true;
  112.   }
  113.   return false;
  114. }
  115. // 10. 检查是否平局
  116. bool checkDraw() {
  117.   for (int y = 0; y < 3; y++) {
  118.     for (int x = 0; x < 3; x++) {
  119.       if (chessBoard[y][x] == EMPTY) return false;  // 只要有一个空位,就不是平局
  120.     }
  121.   }
  122.   return true;  // 棋盘满且无赢家,判定为平局
  123. }
  124. // 11. 人机落子逻辑(无修改,保持“优先赢、次防输、最后随机”策略)
  125. void aiMove() {
  126.   if (gameOver) return;  // 游戏结束,不执行AI落子
  127.   // 第一步:优先自己赢(试落子,能赢就落)
  128.   for (int y = 0; y < 3; y++) {
  129.     for (int x = 0; x < 3; x++) {
  130.       if (chessBoard[y][x] == EMPTY) {
  131.         chessBoard[y][x] = AI;  // 试落AI棋子
  132.         if (checkWin(AI)) {     // 试落后果:AI赢
  133.           winner = AI;          // 标记AI为赢家
  134.           gameOver = true;      // 标记游戏结束
  135.           isPlayerTurn = true;  // 重置回合,不影响重启
  136.           return;
  137.         }
  138.         chessBoard[y][x] = EMPTY;  // 回溯:取消试落,避免影响后续判断
  139.       }
  140.     }
  141.   }
  142.   // 第二步:防玩家赢(试落玩家棋子,堵玩家赢点)
  143.   for (int y = 0; y < 3; y++) {
  144.     for (int x = 0; x < 3; x++) {
  145.       if (chessBoard[y][x] == EMPTY) {
  146.         chessBoard[y][x] = PLAYER;  // 试落玩家棋子
  147.         if (checkWin(PLAYER)) {     // 试落后果:玩家要赢
  148.           chessBoard[y][x] = AI;    // 落AI棋子,堵住这个赢点
  149.           isPlayerTurn = true;      // 切换回玩家回合
  150.           return;
  151.         }
  152.         chessBoard[y][x] = EMPTY;  // 回溯:取消试落
  153.       }
  154.     }
  155.   }
  156.   // 第三步:随机落子(无赢/防赢机会时,避免AI落子固定)
  157.   while (true) {
  158.     int x = random(0, 3);
  159.     int y = random(0, 3);
  160.     if (chessBoard[y][x] == EMPTY) {
  161.       chessBoard[y][x] = AI;
  162.       isPlayerTurn = true;
  163.       return;
  164.     }
  165.   }
  166. }
  167. // 新增:游戏参数重置函数(单独封装,避免代码重复,长按重启时调用)
  168. void resetGame() {
  169.   memset(chessBoard, 0, sizeof(chessBoard));  // 重置棋盘:所有位置变为“空”
  170.   currX = 1; currY = 1;                       // 重置选中位置:回到棋盘正中间
  171.   isPlayerTurn = true;                        // 重置回合:玩家先落子(符合常规习惯)
  172.   gameOver = false;                           // 重置游戏状态:从“结束”变为“进行中”
  173.   winner = EMPTY;                             // 重置赢家:无赢家
  174.   memset(winLine, 0, sizeof(winLine));        // 重置赢棋连线:清空连线坐标
  175.   // 重置长按相关变量:避免重启后残留按键状态,导致误触发
  176.   pressStartTime = 0;
  177.   isKey5Pressed = false;
  178. }
  179. // 12. 初始化函数
  180. void setup() {
  181.   Serial.begin(115200);  // 初始化串口,方便调试AD按键值
  182.   u8g2.begin();          // 初始化OLED(I2C通信,自动识别设备地址)
  183.   u8g2.clearBuffer();    // 清空OLED缓存,避免残留乱码
  184.   resetGame();           // 调用新增的重置函数,初始化首次游戏参数(替代原重复代码)
  185.   randomSeed(analogRead(A1));  // 用空闲模拟引脚做随机种子,让AI落子更随机
  186. }
  187. // 13. 主循环(修改:新增gameOver状态下的长按S5处理逻辑)
  188. void loop() {
  189.   int key = readADKey();  // 读取当前按键(先获取按键值,再分状态处理)
  190.   if (!gameOver && isPlayerTurn) {
  191.     // 状态1:游戏进行中+玩家回合(原逻辑不变,处理上下左右移动和确定落子)
  192.     switch (key) {
  193.       case 1:  // S1:向上(y坐标-1,避免超出棋盘上边界0)
  194.         if (currY > 0) currY--;
  195.         break;
  196.       case 2:  // S2:向左(x坐标-1,避免超出棋盘左边界0)
  197.         if (currX > 0) currX--;
  198.         break;
  199.       case 3:  // S3:向下(y坐标+1,避免超出棋盘下边界2)
  200.         if (currY < 2) currY++;
  201.         break;
  202.       case 4:  // S4:向右(x坐标+1,避免超出棋盘右边界2)
  203.         if (currX < 2) currX++;
  204.         break;
  205.       case 5:  // S5:确定落子(仅当前位置为空时有效,避免重复落子)
  206.         if (chessBoard[currY][currX] == EMPTY) {
  207.           chessBoard[currY][currX] = PLAYER;  // 落玩家棋子(空心圆)
  208.           if (checkWin(PLAYER)) {             // 检查玩家是否赢
  209.             winner = PLAYER;
  210.             gameOver = true;
  211.           } else if (checkDraw()) {           // 检查是否平局
  212.             gameOver = true;
  213.           } else {
  214.             isPlayerTurn = false;             // 切换到人机回合
  215.           }
  216.         }
  217.         break;
  218.     }
  219.   } else if (!gameOver && !isPlayerTurn) {
  220.     // 状态2:游戏进行中+人机回合
  221.     delay(500);
  222.     aiMove();  // 执行AI落子
  223.     // 落子后检查结果:人机赢或平局
  224.     if (checkWin(AI)) {
  225.       winner = AI;
  226.       gameOver = true;
  227.     } else if (checkDraw()) {
  228.       gameOver = true;
  229.     }
  230.   } else {
  231.     // 新增:状态3:游戏结束(赢/输/平局通用),处理S5长按重启
  232.     switch (key) {
  233.       case 5:  // 检测到S5按键按下
  234.         if (!isKey5Pressed) {  // 仅当按键未被标记为“按下”时,记录起始时间(避免重复记录)
  235.           isKey5Pressed = true;                  // 标记S5已按下
  236.           pressStartTime = millis();             // 记录按下的起始时间(单位:毫秒)
  237.         }
  238.         break;
  239.       case -1:  // 检测到无按键按下(即S5已松开,判断是否为有效长按)
  240.         if (isKey5Pressed) {  // 之前S5被按下过,现在松开,开始计算时长
  241.           unsigned long pressDuration = millis() - pressStartTime;  // 计算按键按下的总时长
  242.           if (pressDuration >= LONG_PRESS_TIME) {  // 时长≥1秒,判定为有效长按
  243.             resetGame();  // 调用新增的重置函数,重启游戏
  244.           }
  245.           // 无论是否长按成功,都重置长按相关变量,为下次长按做准备
  246.           isKey5Pressed = false;
  247.           pressStartTime = 0;
  248.         }
  249.         break;
  250.     }
  251.   }
  252.   // OLED显示更新
  253.   u8g2.clearBuffer();
  254.   drawTipBar();
  255.   drawChessBoard();
  256.   drawChess();
  257.   drawWinLine();
  258.   u8g2.sendBuffer();
  259. }
复制代码
进行上传后即可问题说明:1.若按键反馈有问题,你可以修改按键输入阈值。2.当前代码为第一版本,可能代码中会有部分bug,最终版已完成,暂时没有时间发表,请期待最终稳定板。


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail