From b5d696bf3083d83a0a17099540d198923d6416cd Mon Sep 17 00:00:00 2001 From: Seppe De Loore Date: Tue, 10 Mar 2026 08:29:13 +0100 Subject: [PATCH] [fix] Demo interrupt, encoder sensitivity, and win delay. --- src/main.cpp | 693 +++++++++++++++------------------------------------ 1 file changed, 196 insertions(+), 497 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index d449247..4b7a615 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,7 +9,16 @@ #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; @@ -20,635 +29,325 @@ Preferences prefs; int8_t board[COLS][ROWS]; bool winMask[NUM_LEDS]; -enum State -{ - MENU, - PLAYING, - 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; -bool isDemoOver = false; +uint32_t globalInputCooldown = 0; uint8_t demoPly = 4; bool abortAi = false; +bool lastButtonState = HIGH; -uint8_t current_look_ahead; -uint8_t current_brightness; -uint32_t current_idle_timeout_ms; +uint8_t current_look_ahead = 6; +uint8_t current_brightness = 30; +uint32_t current_idle_timeout_ms = 60000; bool blunder_enabled = false; bool progressive_difficulty = false; uint8_t aiBrightness = 0; bool aiFadeUp = true; -// --- Function Prototypes --- +// --- Prototypes --- int getIdx(int x, int y); void drawStaticUI(); void renderBoard(); int getFirstEmptyRow(int col); bool isBoardFull(); -int8_t scanBoard(); -void updateThinkingVisuals(int8_t playerColor, int8_t column); +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); -int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t humanPlayer, int8_t rootCol); -void performAiMove(int8_t aiPlayer); +int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol); +void performAiMove(int8_t aiP); void showMenu(); int getDynamicPly(); 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 >= 2) - { + 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 column = 0; column < COLS; column++) - { - for (int row = 0; row < ROWS; row++) - { - if (board[column][row] == 1) - leds[getIdx(column, 7 - row)] = CRGB::Yellow; - if (board[column][row] == 2) - leds[getIdx(column, 7 - row)] = 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 row = 0; row < ROWS; row++) - { - if (board[col][row] == 0) - return row; - } +int getFirstEmptyRow(int col) { + for (int r = 0; r < ROWS; r++) if (board[col][r] == 0) return r; return -1; } -bool isBoardFull() -{ - for (int column = 0; column < COLS; column++) - { - if (board[column][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 occupiedCount = 0; - for (int column = 0; column < COLS; column++) - for (int row = 0; row < ROWS; row++) - if (board[column][row] != 0) - occupiedCount++; - return constrain(current_look_ahead + (occupiedCount / 7), 1, 10); +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++; + return constrain(current_look_ahead + (count / 7), 1, 10); } -void updateThinkingVisuals(int8_t playerColor, int8_t column) -{ +void updateThinkingVisuals(int8_t pColor, int8_t column) { static uint32_t lastCycle = 0; - if (millis() - lastCycle < 25) - return; + if (millis() - lastCycle < 20) return; lastCycle = millis(); - if (aiFadeUp) - { - aiBrightness += 15; - if (aiBrightness >= 240) - aiFadeUp = false; - } - else - { - aiBrightness -= 15; - if (aiBrightness <= 15) - aiFadeUp = true; - } - for (int x = 0; x < COLS; x++) - leds[getIdx(x, 0)] = CRGB::Black; - CRGB aiColor = (playerColor == 1) ? CRGB::Yellow : CRGB::Red; - leds[getIdx(column, 0)] = aiColor.nscale8(aiBrightness); + 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 row = 5; row >= targetRow; row--) - { + if (targetRow == -1) return; + for (int r = 5; r >= targetRow; r--) { renderBoard(); - leds[getIdx(col, 7 - row)] = (player == 1) ? CRGB::Yellow : CRGB::Red; + leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red; FastLED.show(); - delay(max(20, 80 - (5 - row) * 15)); + delay(max(10, 60 - (5 - r) * 10)); } board[col][targetRow] = player; - renderBoard(); - FastLED.show(); } -void moveDiscToCol(int startCol, int targetCol, int player, int speed) -{ +void moveDiscToCol(int startCol, int targetCol, int player, int speed) { int current = startCol; - CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red; - while (current != targetCol && !abortAi) - { + CRGB colr = (player == 1) ? CRGB::Yellow : CRGB::Red; + 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(); - leds[getIdx(current, 0)] = pColor; + leds[getIdx(current, 0)] = colr; FastLED.show(); delay(speed); - if (digitalRead(ENC_SW) == LOW) - abortAi = true; } activeCol = targetCol; } -int8_t scanBoard() -{ +int8_t scanBoard() { memset(winMask, 0, sizeof(winMask)); - auto checkMatch = [&](int col, int row, int dCol, int dRow) - { - int8_t pAtPos = board[col][row]; - if (pAtPos != 0 && board[col + dCol][row + dRow] == pAtPos && - board[col + 2 * dCol][row + 2 * dRow] == pAtPos && board[col + 3 * dCol][row + 3 * dRow] == pAtPos) - { - for (int i = 0; i < 4; i++) - winMask[getIdx(col + i * dCol, 7 - (row + i * dRow))] = true; - return pAtPos; + 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; + return p; } return (int8_t)0; }; - for (int r = 0; r < 6; r++) - for (int c = 0; c < 4; c++) - { - int8_t res = checkMatch(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 = checkMatch(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 = checkMatch(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 = checkMatch(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 aiPlayer, int8_t humanPlayer, int8_t rootCol) -{ - if (depth % 2 == 0) - { - if (digitalRead(ENC_SW) == LOW) - { - abortAi = true; - return 0; - } - } - if (depth >= current_look_ahead - 1) - updateThinkingVisuals(aiPlayer, 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 winner = scanBoard(); - if (winner == aiPlayer) - return 1000 + depth; // Win sooner is better - if (winner == humanPlayer) - return -1000 - depth; // Lose later is better - if (depth == 0 || isBoardFull()) - return 0; + int8_t win = scanBoard(); + 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 bestScore = isMax ? -10000 : 10000; - - for (int column : colOrder) - { - if (abortAi) - return 0; - int row = getFirstEmptyRow(column); - if (row != -1) - { - board[column][row] = isMax ? aiPlayer : humanPlayer; - int score = minimax(depth - 1, alpha, beta, !isMax, aiPlayer, humanPlayer, (depth == current_look_ahead ? column : rootCol)); - board[column][row] = 0; - if (isMax) - { - bestScore = max(bestScore, score); - alpha = max(alpha, bestScore); - } - else - { - bestScore = min(bestScore, score); - beta = min(beta, bestScore); - } - if (beta <= alpha) - break; + int best = isMax ? -10000 : 10000; + for (int c : colOrder) { + if (abortAi) return 0; + int r = getFirstEmptyRow(c); + 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; } } - return bestScore; + return best; } -void performAiMove(int8_t aiPlayer) -{ +void performAiMove(int8_t aiP) { abortAi = false; - int humanPlayer = (aiPlayer == 1) ? 2 : 1; - int bestScore = -30000; - int bestCol = 3; + int huP = (aiP == 1) ? 2 : 1; + int bestScore = -30000; int bestCol = 3; int originalPly = current_look_ahead; current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly(); - // PHASE 1: Immediate Win Check (OFFENSE) - for (int column = 0; column < COLS; column++) - { - int row = getFirstEmptyRow(column); - if (row != -1) - { - board[column][row] = aiPlayer; - if (scanBoard() == aiPlayer) - { - board[column][row] = 0; - bestCol = column; - goto finalizeMove; // TAKE THE WIN IMMEDIATELY - } - board[column][row] = 0; + for (int c=0; c bestScore) { bestScore = score; bestCol = c; } } } - - // PHASE 3: Minimax Look-ahead - for (int column : {3, 2, 4, 1, 5, 0, 6}) - { - if (abortAi) - goto finalizeMove; - int row = getFirstEmptyRow(column); - if (row != -1) - { - board[column][row] = aiPlayer; - int score = minimax(current_look_ahead, -30000, 30000, false, aiPlayer, humanPlayer, column); - board[column][row] = 0; - if (score > bestScore) - { - bestScore = score; - bestCol = column; - } - } - } - - if ((gameState == DEMO || blunder_enabled) && random(100) < 20 && !abortAi) - { - int randomColumn = random(0, 7); - if (getFirstEmptyRow(randomColumn) != -1) - bestCol = randomColumn; - } - finalizeMove: current_look_ahead = originalPly; - if (!abortAi) - { - moveDiscToCol(activeCol, bestCol, aiPlayer, 100); - delay(450); - animateDrop(bestCol, aiPlayer); - } + 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:

"; + 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() -{ - isDemoOver = false; +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 - if (menuMode < 2) - { - CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; - 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; - } + 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; } FastLED.show(); } -void setup() -{ - Serial.begin(115200); +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(ENC_SW, INPUT_PULLUP); - WiFi.softAP("Connect4-Config", WIFI_PASSWORD); - 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() / SENSITIVITY; - bool pressed = (digitalRead(ENC_SW) == LOW); + 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; } + lastButtonState = currentButton; - if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500))) - { - if (gameState >= 2 || gameState == DEMO) - { - abortAi = true; - memset(board, 0, sizeof(board)); - winnerPlayer = 0; - demoResetTimer = 0; - for (int i = 0; i < 10; i++) - { - fadeToBlackBy(leds, NUM_LEDS, 40); - FastLED.show(); - delay(20); - } - gameState = MENU; - showMenu(); - oldEncPos = newPos; - lastActivityTime = millis(); - delay(300); - return; - } - lastActivityTime = millis(); + // 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; } - uint32_t activeLimit = (gameState == PLAYING) ? (current_idle_timeout_ms * 2) : current_idle_timeout_ms; - if (gameState != DEMO && (gameState < 2) && (millis() - lastActivityTime > activeLimit)) - { - gameState = DEMO; - memset(board, 0, sizeof(board)); - currentPlayer = 1; - demoPly = random(3, 7); - 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 (newPos != oldEncPos) - { - menuMode = (newPos % 3 + 3) % 3; - oldEncPos = newPos; - showMenu(); + 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; } } - if (pressed) - { - memset(board, 0, sizeof(board)); - gameState = PLAYING; - if (menuMode == 1) - { - performAiMove(1); - currentPlayer = 2; - } - else - { - currentPlayer = 1; - } - delay(300); - } - } - 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) - { - lastActivityTime = millis(); + } + 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) - { - int8_t aiP = (menuMode == 0) ? 2 : 1; - performAiMove(aiP); - lastActivityTime = millis(); - 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; - } - } - delay(300); + 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); + 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(); } - else if (gameState == DEMO) - { - renderBoard(); - FastLED.show(); - delay(600); - 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 - leds[i].nscale8(60); - } - 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; - demoPly = random(3, 7); + + // 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