From 52c84301dcbf630119a723ffdfc97a8d3c69e27f Mon Sep 17 00:00:00 2001 From: Seppe De Loore Date: Thu, 12 Mar 2026 12:59:39 +0100 Subject: [PATCH] [fix] Encoder sensitivity. Extend delay after win. --- src/main.cpp | 624 +++++++++++++-------------------------------------- 1 file changed, 158 insertions(+), 466 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index e51420c..4b7a615 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,11 +9,19 @@ #define SHOW_BORDER 1 #endif +#ifndef SENSITIVITY +#define SENSITIVITY 4 +#endif + +#define LED_PIN 4 +#define ENC_A 0 +#define ENC_B 1 +#define ENC_SW 2 #define NUM_LEDS 64 + const int COLS = 7; const int ROWS = 6; -// --- Global Variables --- CRGB leds[NUM_LEDS]; Encoder myEnc(ENC_A, ENC_B); WebServer server(80); @@ -21,29 +29,20 @@ Preferences prefs; int8_t board[COLS][ROWS]; bool winMask[NUM_LEDS]; -enum State -{ - MENU, - PLAYING, - AI_TURN, - FINISHED_WIN, - FINISHED_DRAW, - DEMO -}; +enum State { MENU, PLAYING, AI_TURN, FINISHED_WIN, FINISHED_DRAW, DEMO }; State gameState = MENU; -int8_t menuMode = 0; +int8_t menuMode = 0; int8_t currentPlayer = 1; -int8_t winnerPlayer = 0; +int8_t winnerPlayer = 0; int8_t activeCol = 3; long oldEncPos = -999; uint32_t lastActivityTime = 0; uint32_t demoResetTimer = 0; uint32_t globalInputCooldown = 0; -uint8_t demoPly = 4; // FIXED: Restored global declaration +uint8_t demoPly = 4; bool abortAi = false; -bool lastButtonState = HIGH; -bool buttonBlocked = false; +bool lastButtonState = HIGH; uint8_t current_look_ahead = 6; uint8_t current_brightness = 30; @@ -60,7 +59,7 @@ void drawStaticUI(); void renderBoard(); int getFirstEmptyRow(int col); bool isBoardFull(); -int8_t scanBoard(); +int8_t scanBoard(); void updateThinkingVisuals(int8_t pColor, int8_t column); void animateDrop(int col, int player); void moveDiscToCol(int startCol, int targetCol, int player, int speed); @@ -69,105 +68,65 @@ void performAiMove(int8_t aiP); void showMenu(); int getDynamicPly(); -// --- Functions --- int getIdx(int x, int y) { return (y * 8) + x; } -void drawStaticUI() -{ +void drawStaticUI() { FastLED.clear(); #if SHOW_BORDER == 1 CRGB borderColor = CRGB::Blue; - if (gameState == DEMO || gameState >= 3) - { + if (gameState == DEMO || gameState >= 3) { uint8_t glow = beat8(15); borderColor = blend(CRGB::Blue, CRGB::White, glow / 4); } - for (int x = 0; x < 7; x++) - leds[getIdx(x, 1)] = borderColor; - for (int y = 1; y < 8; y++) - leds[getIdx(7, y)] = borderColor; + for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = borderColor; + for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = borderColor; #endif } -void renderBoard() -{ +void renderBoard() { drawStaticUI(); - for (int c = 0; c < COLS; c++) - { - for (int r = 0; r < ROWS; r++) - { - if (board[c][r] == 1) - leds[getIdx(c, 7 - r)] = CRGB::Yellow; - if (board[c][r] == 2) - leds[getIdx(c, 7 - r)] = CRGB::Red; + for (int c = 0; c < COLS; c++) { + for (int r = 0; r < ROWS; r++) { + if (board[c][r] == 1) leds[getIdx(c, 7 - r)] = CRGB::Yellow; + if (board[c][r] == 2) leds[getIdx(c, 7 - r)] = CRGB::Red; } } } -int getFirstEmptyRow(int col) -{ - for (int r = 0; r < ROWS; r++) - if (board[col][r] == 0) - return r; +int getFirstEmptyRow(int col) { + for (int r = 0; r < ROWS; r++) if (board[col][r] == 0) return r; return -1; } -bool isBoardFull() -{ - for (int c = 0; c < COLS; c++) - if (board[c][ROWS - 1] == 0) - return false; +bool isBoardFull() { + for (int c = 0; c < COLS; c++) if (board[c][ROWS-1] == 0) return false; return true; } -int getDynamicPly() -{ - if (!progressive_difficulty && gameState != DEMO) - return current_look_ahead; +int getDynamicPly() { + if (!progressive_difficulty && gameState != DEMO) return current_look_ahead; int count = 0; - for (int c = 0; c < COLS; c++) - for (int r = 0; r < ROWS; r++) - if (board[c][r] != 0) - count++; + for (int c = 0; c < COLS; c++) for (int r = 0; r < ROWS; r++) if (board[c][r] != 0) count++; return constrain(current_look_ahead + (count / 7), 1, 10); } -void updateThinkingVisuals(int8_t pColor, int8_t column) -{ +void updateThinkingVisuals(int8_t pColor, int8_t column) { static uint32_t lastCycle = 0; - if (millis() - lastCycle < 20) - return; + if (millis() - lastCycle < 20) return; lastCycle = millis(); - if (aiFadeUp) - { - aiBrightness += 25; - if (aiBrightness >= 230) - aiFadeUp = false; - } - else - { - aiBrightness -= 25; - if (aiBrightness <= 25) - aiFadeUp = true; - } - - for (int x = 0; x < COLS; x++) - leds[getIdx(x, 0)] = CRGB::Black; - - // FIXED: Explicit color initialization to avoid nscale8 compiler error + if (aiFadeUp) { aiBrightness += 25; if (aiBrightness >= 230) aiFadeUp = false; } + else { aiBrightness -= 25; if (aiBrightness <= 25) aiFadeUp = true; } + for (int x = 0; x < COLS; x++) leds[getIdx(x, 0)] = CRGB::Black; CRGB aiColor = (pColor == 1) ? CRGB(CRGB::Yellow) : CRGB(CRGB::Red); aiColor.nscale8(aiBrightness); leds[getIdx(column, 0)] = aiColor; FastLED.show(); } -void animateDrop(int col, int player) -{ +void animateDrop(int col, int player) { int targetRow = getFirstEmptyRow(col); - if (targetRow == -1) - return; - for (int r = 5; r >= targetRow; r--) - { + if (targetRow == -1) return; + for (int r = 5; r >= targetRow; r--) { renderBoard(); leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red; FastLED.show(); @@ -176,17 +135,11 @@ void animateDrop(int col, int player) board[col][targetRow] = player; } -void moveDiscToCol(int startCol, int targetCol, int player, int speed) -{ +void moveDiscToCol(int startCol, int targetCol, int player, int speed) { int current = startCol; CRGB colr = (player == 1) ? CRGB::Yellow : CRGB::Red; - while (current != targetCol && !abortAi) - { - if (gameState == DEMO && digitalRead(ENC_SW) == LOW) - { - abortAi = true; - break; - } + while (current != targetCol && !abortAi) { + if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { abortAi = true; break; } leds[getIdx(current, 0)] = CRGB::Black; current += (targetCol > current) ? 1 : -1; renderBoard(); @@ -197,465 +150,204 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed) activeCol = targetCol; } -int8_t scanBoard() -{ +int8_t scanBoard() { memset(winMask, 0, sizeof(winMask)); - auto check = [&](int c, int r, int dc, int dr) - { + auto check = [&](int c, int r, int dc, int dr) { int8_t p = board[c][r]; - if (p != 0 && board[c + dc][r + dr] == p && board[c + 2 * dc][r + 2 * dr] == p && board[c + 3 * dc][r + 3 * dr] == p) - { - for (int i = 0; i < 4; i++) - winMask[getIdx(c + i * dc, 7 - (r + i * dr))] = true; + if (p != 0 && board[c+dc][r+dr] == p && board[c+2*dc][r+2*dr] == p && board[c+3*dc][r+3*dr] == p) { + for (int i = 0; i < 4; i++) winMask[getIdx(c+i*dc, 7-(r+i*dr))] = true; return p; } return (int8_t)0; }; - for (int r = 0; r < 6; r++) - for (int c = 0; c < 4; c++) - { - int8_t res = check(c, r, 1, 0); - if (res) - return res; - } - for (int r = 0; r < 3; r++) - for (int c = 0; c < 7; c++) - { - int8_t res = check(c, r, 0, 1); - if (res) - return res; - } - for (int r = 0; r < 3; r++) - for (int c = 0; c < 4; c++) - { - int8_t res = check(c, r, 1, 1); - if (res) - return res; - } - for (int r = 3; r < 6; r++) - for (int c = 0; c < 4; c++) - { - int8_t res = check(c, r, 1, -1); - if (res) - return res; - } + for (int r=0; r<6; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,0); if(res) return res; } + for (int r=0; r<3; r++) for (int c=0; c<7; c++) { int8_t res = check(c,r,0,1); if(res) return res; } + for (int r=0; r<3; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,1); if(res) return res; } + for (int r=3; r<6; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,-1); if(res) return res; } return 0; } -int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol) -{ - if (gameState == DEMO && digitalRead(ENC_SW) == LOW) - { - abortAi = true; - return 0; - } - - if (depth >= current_look_ahead - 1) - updateThinkingVisuals(aiP, rootCol); - else - yield(); - - if (abortAi) - return 0; +int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol) { + if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { abortAi = true; return 0; } + if (depth >= current_look_ahead - 1) updateThinkingVisuals(aiP, rootCol); + else yield(); + if (abortAi) return 0; int8_t win = scanBoard(); - if (win == aiP) - return 1000 + depth; - if (win == huP) - return -1000 - depth; - if (depth == 0 || isBoardFull()) - return 0; + if (win == aiP) return 1000 + depth; + if (win == huP) return -1000 - depth; + if (depth == 0 || isBoardFull()) return 0; int colOrder[] = {3, 2, 4, 1, 5, 0, 6}; int best = isMax ? -10000 : 10000; - for (int c : colOrder) - { - if (abortAi) - return 0; + for (int c : colOrder) { + if (abortAi) return 0; int r = getFirstEmptyRow(c); - if (r != -1) - { + if (r != -1) { board[c][r] = isMax ? aiP : huP; int score = minimax(depth - 1, alpha, beta, !isMax, aiP, huP, (depth == current_look_ahead ? c : rootCol)); board[c][r] = 0; - if (isMax) - { - if (score > best) - best = score; - if (best > alpha) - alpha = best; - } - else - { - if (score < best) - best = score; - if (best < beta) - beta = best; - } - if (beta <= alpha) - break; + if (isMax) { if (score > best) best = score; if (best > alpha) alpha = best; } + else { if (score < best) best = score; if (best < beta) beta = best; } + if (beta <= alpha) break; } } return best; } -void performAiMove(int8_t aiP) -{ +void performAiMove(int8_t aiP) { abortAi = false; int huP = (aiP == 1) ? 2 : 1; - int bestScore = -30000; - int bestCol = 3; + int bestScore = -30000; int bestCol = 3; int originalPly = current_look_ahead; current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly(); - for (int c = 0; c < COLS; c++) - { - if (gameState == DEMO && digitalRead(ENC_SW) == LOW) - { - abortAi = true; - goto finalizeMove; - } + for (int c=0; c bestScore) - { - bestScore = score; - bestCol = c; - } + if (score > bestScore) { bestScore = score; bestCol = c; } } } finalizeMove: current_look_ahead = originalPly; - if (!abortAi) - { - moveDiscToCol(activeCol, bestCol, aiP, 80); - if (!abortAi) - { - delay(150); - animateDrop(bestCol, aiP); - } - } + if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } } } -void handleRoot() -{ +void handleRoot() { String html = "

Connect 4 Admin

"; html += "Base AI Ply:Brightness:Idle Timeout (s):"; html += "Blunders:
Evolution:

"; server.send(200, "text/html", html); } -void handleSave() -{ - if (server.hasArg("ply")) - { - current_look_ahead = server.arg("ply").toInt(); - prefs.putUChar("ply", current_look_ahead); - } - if (server.hasArg("br")) - { - current_brightness = server.arg("br").toInt(); - FastLED.setBrightness(current_brightness); - prefs.putUChar("br", current_brightness); - } - if (server.hasArg("idle")) - { - current_idle_timeout_ms = server.arg("idle").toInt() * 1000; - prefs.putUInt("idle", current_idle_timeout_ms / 1000); - } - blunder_enabled = server.hasArg("blunder"); - prefs.putBool("blunder", blunder_enabled); - progressive_difficulty = server.hasArg("evolve"); - prefs.putBool("evolve", progressive_difficulty); - server.sendHeader("Location", "/"); - server.send(303); +void handleSave() { + if (server.hasArg("ply")) { current_look_ahead = server.arg("ply").toInt(); prefs.putUChar("ply", current_look_ahead); } + if (server.hasArg("br")) { current_brightness = server.arg("br").toInt(); FastLED.setBrightness(current_brightness); prefs.putUChar("br", current_brightness); } + if (server.hasArg("idle")) { current_idle_timeout_ms = server.arg("idle").toInt() * 1000; prefs.putUInt("idle", current_idle_timeout_ms / 1000); } + blunder_enabled = server.hasArg("blunder"); prefs.putBool("blunder", blunder_enabled); + progressive_difficulty = server.hasArg("evolve"); prefs.putBool("evolve", progressive_difficulty); + server.sendHeader("Location", "/"); server.send(303); } -void showMenu() -{ +void showMenu() { FastLED.clear(); #if SHOW_BORDER == 1 - for (int x = 0; x < 7; x++) - leds[getIdx(x, 1)] = CRGB::Blue; - for (int y = 1; y < 8; y++) - leds[getIdx(7, y)] = CRGB::Blue; + for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = CRGB::Blue; + for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = CRGB::Blue; #endif CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; - if (menuMode < 2) - { - for (int y = 3; y <= 6; y++) - leds[getIdx(3, y)] = pCol; - leds[getIdx(2, 3)] = pCol; - leds[getIdx(4, 3)] = pCol; - leds[getIdx(2, 6)] = pCol; - leds[getIdx(4, 6)] = pCol; - } - else - { - for (int y = 3; y <= 6; y++) - { - leds[getIdx(2, y)] = CRGB::Yellow; - leds[getIdx(4, y)] = CRGB::Red; - } - leds[getIdx(1, 3)] = CRGB::Yellow; - leds[getIdx(3, 3)] = CRGB::Yellow; - leds[getIdx(1, 6)] = CRGB::Yellow; - leds[getIdx(3, 6)] = CRGB::Yellow; - leds[getIdx(3, 3)] = CRGB::Red; - leds[getIdx(5, 3)] = CRGB::Red; - leds[getIdx(3, 6)] = CRGB::Red; - leds[getIdx(5, 6)] = CRGB::Red; - } + if (menuMode < 2) { for (int y = 3; y <= 6; y++) leds[getIdx(3, y)] = pCol; leds[getIdx(2, 3)] = pCol; leds[getIdx(4, 3)] = pCol; leds[getIdx(2, 6)] = pCol; leds[getIdx(4, 6)] = pCol; } + else { for (int y = 3; y <= 6; y++) { leds[getIdx(2, y)] = CRGB::Yellow; leds[getIdx(4, y)] = CRGB::Red; } leds[getIdx(1, 3)] = CRGB::Yellow; leds[getIdx(3, 3)] = CRGB::Yellow; leds[getIdx(1, 6)] = CRGB::Yellow; leds[getIdx(3, 6)] = CRGB::Yellow; leds[getIdx(3, 3)] = CRGB::Red; leds[getIdx(5, 3)] = CRGB::Red; leds[getIdx(3, 6)] = CRGB::Red; leds[getIdx(5, 6)] = CRGB::Red; } FastLED.show(); } -void setup() -{ +void setup() { prefs.begin("c4-game", false); - current_look_ahead = prefs.getUChar("ply", 8); - current_brightness = prefs.getUChar("br", 25); - current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000; - blunder_enabled = prefs.getBool("blunder", false); + current_look_ahead = prefs.getUChar("ply", 8); current_brightness = prefs.getUChar("br", 25); + current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000; blunder_enabled = prefs.getBool("blunder", false); progressive_difficulty = prefs.getBool("evolve", false); - FastLED.addLeds(leds, NUM_LEDS); - FastLED.setBrightness(current_brightness); - pinMode(2, INPUT_PULLUP); - WiFi.softAP("Connect4-Config", "12345678"); - server.on("/", handleRoot); - server.on("/save", HTTP_POST, handleSave); - server.begin(); - lastActivityTime = millis(); - showMenu(); + FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(current_brightness); + pinMode(ENC_SW, INPUT_PULLUP); WiFi.softAP("Connect4-Config", "12345678"); + server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); + lastActivityTime = millis(); showMenu(); } -void loop() -{ +void loop() { server.handleClient(); - long newPos = myEnc.read() / 2; - bool currentButton = digitalRead(2); - + long rawPos = myEnc.read(); + long newPos = rawPos / SENSITIVITY; + bool currentButton = digitalRead(ENC_SW); bool pressed = false; - if (currentButton == LOW && lastButtonState == HIGH) - { - if (millis() > globalInputCooldown) - pressed = true; - } + if (currentButton == LOW && lastButtonState == HIGH) { if (millis() > globalInputCooldown) pressed = true; } lastButtonState = currentButton; - if ((newPos != oldEncPos || pressed) && (gameState >= 3 || gameState == DEMO)) - { - abortAi = true; - memset(board, 0, sizeof(board)); - winnerPlayer = 0; - for (int i = 0; i < 10; i++) - { - fadeToBlackBy(leds, NUM_LEDS, 50); - FastLED.show(); - delay(15); - } - gameState = MENU; - showMenu(); - oldEncPos = newPos; - lastActivityTime = millis(); - globalInputCooldown = millis() + 600; - return; + // Interrupt check + if ((newPos != oldEncPos || pressed) && (gameState >= 3 || gameState == DEMO)) { + abortAi = true; memset(board, 0, sizeof(board)); winnerPlayer = 0; + for (int i = 0; i < 10; i++) { fadeToBlackBy(leds, NUM_LEDS, 50); FastLED.show(); delay(15); } + gameState = MENU; showMenu(); oldEncPos = newPos; lastActivityTime = millis(); + globalInputCooldown = millis() + 600; return; } - if (gameState != DEMO && (gameState < 3) && (millis() - lastActivityTime > current_idle_timeout_ms)) - { - gameState = DEMO; - memset(board, 0, sizeof(board)); - currentPlayer = 1; - return; + // Idle watchdog logic (Added specific exemption for FINISHED state) + if (gameState != DEMO && (gameState < 3)) { + if (millis() - lastActivityTime > current_idle_timeout_ms) { + gameState = DEMO; memset(board, 0, sizeof(board)); currentPlayer = 1; return; + } } - if (gameState == MENU) - { - if (millis() > globalInputCooldown) - { - if (newPos != oldEncPos) - { - menuMode = (newPos % 3 + 3) % 3; - oldEncPos = newPos; - showMenu(); - } - if (pressed) - { - memset(board, 0, sizeof(board)); - if (menuMode == 1) - { - currentPlayer = 1; - gameState = AI_TURN; - } - else - { - currentPlayer = 1; - gameState = PLAYING; - } - globalInputCooldown = millis() + 400; - } + if (gameState == MENU) { + if (millis() > globalInputCooldown) { + if (newPos != oldEncPos) { menuMode = (newPos % 3 + 3) % 3; oldEncPos = newPos; showMenu(); } + if (pressed) { memset(board, 0, sizeof(board)); if (menuMode == 1) { currentPlayer = 1; gameState = AI_TURN; } else { currentPlayer = 1; gameState = PLAYING; } globalInputCooldown = millis() + 500; } } - } - else if (gameState == PLAYING) - { - if (newPos != oldEncPos) - { - activeCol = (newPos % 7 + 7) % 7; - oldEncPos = newPos; - lastActivityTime = millis(); - } - renderBoard(); - leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red; - FastLED.show(); - if (pressed) - { + } + else if (gameState == PLAYING) { + if (newPos != oldEncPos) { activeCol = (newPos % 7 + 7) % 7; oldEncPos = newPos; lastActivityTime = millis(); } + renderBoard(); leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red; FastLED.show(); + if (pressed) { int row = getFirstEmptyRow(activeCol); - if (row != -1) - { + if (row != -1) { animateDrop(activeCol, currentPlayer); winnerPlayer = scanBoard(); - if (winnerPlayer != 0) - { - gameState = FINISHED_WIN; - demoResetTimer = millis(); - } - else if (isBoardFull()) - { - gameState = FINISHED_DRAW; - demoResetTimer = millis(); - } - else - { - if (menuMode < 2) - { - gameState = AI_TURN; - } - else - { - currentPlayer = (currentPlayer == 1) ? 2 : 1; - } - } + if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); } + else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); } + else { if (menuMode < 2) { gameState = AI_TURN; } else { currentPlayer = (currentPlayer == 1) ? 2 : 1; } } lastActivityTime = millis(); } } - } - else if (gameState == AI_TURN) - { - int8_t aiP = (menuMode == 0) ? 2 : 1; - performAiMove(aiP); + } + else if (gameState == AI_TURN) { + int8_t aiP = (menuMode == 0) ? 2 : 1; performAiMove(aiP); + if (abortAi) { gameState = MENU; showMenu(); return; } + winnerPlayer = scanBoard(); + if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); } + else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); } + else { gameState = PLAYING; currentPlayer = (aiP == 1) ? 2 : 1; } lastActivityTime = millis(); - if (!abortAi) - { - winnerPlayer = scanBoard(); - if (winnerPlayer != 0) - { - gameState = FINISHED_WIN; - demoResetTimer = millis(); - } - else if (isBoardFull()) - { - gameState = FINISHED_DRAW; - demoResetTimer = millis(); - } - else - { - gameState = PLAYING; - currentPlayer = (aiP == 1) ? 2 : 1; - } - } } - else if (gameState == DEMO) - { - renderBoard(); - FastLED.show(); - delay(300); - performAiMove(currentPlayer); - if (!abortAi) - { - winnerPlayer = scanBoard(); - if (winnerPlayer != 0) - { - gameState = FINISHED_WIN; - demoResetTimer = millis(); - } - else if (isBoardFull()) - { - gameState = FINISHED_DRAW; - demoResetTimer = millis(); - } - else - { - currentPlayer = (currentPlayer == 1) ? 2 : 1; - } - } - } - else - { - static uint32_t lastFlash = 0; - static bool toggle = true; - if (millis() - lastFlash > 300) - { - lastFlash = millis(); - toggle = !toggle; - renderBoard(); - for (int i = 0; i < NUM_LEDS; i++) - { + else if (gameState == DEMO) { + renderBoard(); FastLED.show(); delay(300); performAiMove(currentPlayer); + if (abortAi) { gameState = MENU; showMenu(); return; } + winnerPlayer = scanBoard(); + if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); } + else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); } + else { currentPlayer = (currentPlayer == 1) ? 2 : 1; } + } + else { // FINISHED state (WIN/DRAW) + static uint32_t lastFlash = 0; static bool toggle = true; + if (millis() - lastFlash > 300) { + lastFlash = millis(); toggle = !toggle; renderBoard(); + for (int i = 0; i < NUM_LEDS; i++) { #if SHOW_BORDER == 1 - if (leds[i] == CRGB::Blue) - continue; + if (leds[i] == CRGB::Blue) continue; #endif - if (gameState == FINISHED_WIN) - { - if (winMask[i]) - leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; - else - { - CRGB c = leds[i]; - c.nscale8(60); - leds[i] = c; - } - } - else if (gameState == FINISHED_DRAW) - { - if (!toggle) - leds[i] = CRGB::Black; - } + if (gameState == FINISHED_WIN) { + if (winMask[i]) leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; + else { CRGB c = leds[i]; c.nscale8(60); leds[i] = c; } + } else if (gameState == FINISHED_DRAW) { if (!toggle) leds[i] = CRGB::Black; } } FastLED.show(); } - if (millis() - demoResetTimer > 15000) - { - memset(board, 0, sizeof(board)); - gameState = DEMO; - demoResetTimer = 0; + + // RECENT FIX: Prolonged display time for win (30 seconds) + if (millis() - demoResetTimer > 30000) { + memset(board, 0, sizeof(board)); + gameState = DEMO; + demoResetTimer = 0; + lastActivityTime = millis(); // Refresh watchdog } } -} +} \ No newline at end of file