diff --git a/src/main.cpp b/src/main.cpp index d449247..e51420c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ const int COLS = 7; const int ROWS = 6; +// --- Global Variables --- CRGB leds[NUM_LEDS]; Encoder myEnc(ENC_A, ENC_B); WebServer server(80); @@ -24,6 +25,7 @@ enum State { MENU, PLAYING, + AI_TURN, FINISHED_WIN, FINISHED_DRAW, DEMO @@ -37,34 +39,37 @@ int8_t activeCol = 3; long oldEncPos = -999; uint32_t lastActivityTime = 0; uint32_t demoResetTimer = 0; -bool isDemoOver = false; -uint8_t demoPly = 4; +uint32_t globalInputCooldown = 0; +uint8_t demoPly = 4; // FIXED: Restored global declaration bool abortAi = false; +bool lastButtonState = HIGH; +bool buttonBlocked = false; -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); +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(); +// --- Functions --- int getIdx(int x, int y) { return (y * 8) + x; } void drawStaticUI() @@ -72,7 +77,7 @@ 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); @@ -87,35 +92,31 @@ void drawStaticUI() void renderBoard() { drawStaticUI(); - for (int column = 0; column < COLS; column++) + for (int c = 0; c < COLS; c++) { - for (int row = 0; row < ROWS; row++) + for (int r = 0; r < ROWS; r++) { - if (board[column][row] == 1) - leds[getIdx(column, 7 - row)] = CRGB::Yellow; - if (board[column][row] == 2) - leds[getIdx(column, 7 - row)] = CRGB::Red; + 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; - } + 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) + for (int c = 0; c < COLS; c++) + if (board[c][ROWS - 1] == 0) return false; - } return true; } @@ -123,36 +124,40 @@ 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 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) + if (millis() - lastCycle < 20) return; lastCycle = millis(); if (aiFadeUp) { - aiBrightness += 15; - if (aiBrightness >= 240) + aiBrightness += 25; + if (aiBrightness >= 230) aiFadeUp = false; } else { - aiBrightness -= 15; - if (aiBrightness <= 15) + aiBrightness -= 25; + if (aiBrightness <= 25) 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); + + // FIXED: Explicit color initialization to avoid nscale8 compiler error + CRGB aiColor = (pColor == 1) ? CRGB(CRGB::Yellow) : CRGB(CRGB::Red); + aiColor.nscale8(aiBrightness); + leds[getIdx(column, 0)] = aiColor; FastLED.show(); } @@ -161,32 +166,33 @@ void animateDrop(int col, int player) int targetRow = getFirstEmptyRow(col); if (targetRow == -1) return; - for (int row = 5; row >= targetRow; row--) + 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) { int current = startCol; - CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red; + 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; } @@ -194,180 +200,168 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed) int8_t scanBoard() { memset(winMask, 0, sizeof(winMask)); - auto checkMatch = [&](int col, int row, int dCol, int dRow) + auto check = [&](int c, int r, int dc, int dr) { - 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) + 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(col + i * dCol, 7 - (row + i * dRow))] = true; - return pAtPos; + 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); + 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 = checkMatch(c, r, 0, 1); + 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 = checkMatch(c, r, 1, 1); + 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 = checkMatch(c, r, 1, -1); + 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) +int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol) { - if (depth % 2 == 0) + if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { - if (digitalRead(ENC_SW) == LOW) - { - abortAi = true; - return 0; - } + abortAi = true; + return 0; } + if (depth >= current_look_ahead - 1) - updateThinkingVisuals(aiPlayer, rootCol); + 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 + 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) + int best = isMax ? -10000 : 10000; + for (int c : colOrder) { if (abortAi) return 0; - int row = getFirstEmptyRow(column); - if (row != -1) + int r = getFirstEmptyRow(c); + if (r != -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; + 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) { - bestScore = max(bestScore, score); - alpha = max(alpha, bestScore); + if (score > best) + best = score; + if (best > alpha) + alpha = best; } else { - bestScore = min(bestScore, score); - beta = min(beta, bestScore); + 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 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++) + for (int c = 0; c < COLS; c++) { - int row = getFirstEmptyRow(column); - if (row != -1) + if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { - board[column][row] = aiPlayer; - if (scanBoard() == aiPlayer) + abortAi = true; + goto finalizeMove; + } + int r = getFirstEmptyRow(c); + if (r != -1) + { + board[c][r] = aiP; + if (scanBoard() == aiP) { - board[column][row] = 0; - bestCol = column; - goto finalizeMove; // TAKE THE WIN IMMEDIATELY + board[c][r] = 0; + bestCol = c; + goto finalizeMove; } - board[column][row] = 0; + board[c][r] = huP; + if (scanBoard() == huP) + { + board[c][r] = 0; + bestCol = c; + goto finalizeMove; + } + board[c][r] = 0; } } - - // PHASE 2: Immediate Block Check (DEFENSE) - for (int column = 0; column < COLS; column++) - { - int row = getFirstEmptyRow(column); - if (row != -1) - { - board[column][row] = humanPlayer; - if (scanBoard() == humanPlayer) - { - board[column][row] = 0; - bestCol = column; - goto finalizeMove; // MUST BLOCK - } - board[column][row] = 0; - } - } - - // PHASE 3: Minimax Look-ahead - for (int column : {3, 2, 4, 1, 5, 0, 6}) + for (int c : {3, 2, 4, 1, 5, 0, 6}) { if (abortAi) goto finalizeMove; - int row = getFirstEmptyRow(column); - if (row != -1) + int r = getFirstEmptyRow(c); + if (r != -1) { - board[column][row] = aiPlayer; - int score = minimax(current_look_ahead, -30000, 30000, false, aiPlayer, humanPlayer, column); - board[column][row] = 0; + board[c][r] = aiP; + int score = minimax(current_look_ahead, -30000, 30000, false, aiP, huP, c); + board[c][r] = 0; if (score > bestScore) { bestScore = score; - bestCol = column; + bestCol = c; } } } - - 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); + moveDiscToCol(activeCol, bestCol, aiP, 80); + if (!abortAi) + { + delay(150); + animateDrop(bestCol, aiP); + } } } @@ -375,7 +369,7 @@ void handleRoot() { String html = "