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 = "

Connect 4 Admin

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

"; + html += "Blunders:
Evolution:

"; server.send(200, "text/html", html); } @@ -407,7 +401,6 @@ void handleSave() void showMenu() { - isDemoOver = false; FastLED.clear(); #if SHOW_BORDER == 1 for (int x = 0; x < 7; x++) @@ -415,9 +408,9 @@ void showMenu() 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) { - 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; @@ -446,17 +439,16 @@ void showMenu() void setup() { - Serial.begin(115200); 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); progressive_difficulty = prefs.getBool("evolve", false); - FastLED.addLeds(leds, NUM_LEDS); + FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(current_brightness); - pinMode(ENC_SW, INPUT_PULLUP); - WiFi.softAP("Connect4-Config", WIFI_PASSWORD); + pinMode(2, INPUT_PULLUP); + WiFi.softAP("Connect4-Config", "12345678"); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); @@ -467,65 +459,69 @@ void setup() void loop() { server.handleClient(); - long newPos = myEnc.read() / SENSITIVITY; - bool pressed = (digitalRead(ENC_SW) == LOW); + long newPos = myEnc.read() / 2; + bool currentButton = digitalRead(2); - if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500))) + bool pressed = false; + if (currentButton == LOW && lastButtonState == HIGH) { - if (gameState >= 2 || gameState == DEMO) + 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++) { - 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; + 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)) + if (gameState != DEMO && (gameState < 3) && (millis() - lastActivityTime > current_idle_timeout_ms)) { gameState = DEMO; memset(board, 0, sizeof(board)); currentPlayer = 1; - demoPly = random(3, 7); return; } if (gameState == MENU) { - if (newPos != oldEncPos) + if (millis() > globalInputCooldown) { - menuMode = (newPos % 3 + 3) % 3; - oldEncPos = newPos; - showMenu(); - } - if (pressed) - { - memset(board, 0, sizeof(board)); - gameState = PLAYING; - if (menuMode == 1) + if (newPos != oldEncPos) { - performAiMove(1); - currentPlayer = 2; + menuMode = (newPos % 3 + 3) % 3; + oldEncPos = newPos; + showMenu(); } - else + if (pressed) { - currentPlayer = 1; + memset(board, 0, sizeof(board)); + if (menuMode == 1) + { + currentPlayer = 1; + gameState = AI_TURN; + } + else + { + currentPlayer = 1; + gameState = PLAYING; + } + globalInputCooldown = millis() + 400; } - delay(300); } } else if (gameState == PLAYING) @@ -541,7 +537,6 @@ void loop() FastLED.show(); if (pressed) { - lastActivityTime = millis(); int row = getFirstEmptyRow(activeCol); if (row != -1) { @@ -561,30 +556,39 @@ void loop() { 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(); - } - } + gameState = AI_TURN; } else { currentPlayer = (currentPlayer == 1) ? 2 : 1; } } - delay(300); + lastActivityTime = millis(); + } + } + } + else if (gameState == AI_TURN) + { + 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 + { + gameState = PLAYING; + currentPlayer = (aiP == 1) ? 2 : 1; } } } @@ -592,7 +596,7 @@ void loop() { renderBoard(); FastLED.show(); - delay(600); + delay(300); performAiMove(currentPlayer); if (!abortAi) { @@ -633,7 +637,11 @@ void loop() if (winMask[i]) leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; else - leds[i].nscale8(60); + { + CRGB c = leds[i]; + c.nscale8(60); + leds[i] = c; + } } else if (gameState == FINISHED_DRAW) { @@ -648,7 +656,6 @@ void loop() memset(board, 0, sizeof(board)); gameState = DEMO; demoResetTimer = 0; - demoPly = random(3, 7); } } }