From 0994c11f0b3dd21c0ef750cb7550dc2ea9dd1d50 Mon Sep 17 00:00:00 2001 From: Seppe De Loore Date: Mon, 9 Mar 2026 10:38:42 +0100 Subject: [PATCH] [fix] AI strategy detect win first, killer instinct and button press. Although not 100% okay as button is not always detected during 'thinking" --- src/main.cpp | 263 +++++++++++++++++++++++++++------------------------ 1 file changed, 138 insertions(+), 125 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5f03e81..d449247 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,6 @@ #include #include -// Build Flag Default (can be overridden in platformio.ini) #ifndef SHOW_BORDER #define SHOW_BORDER 1 #endif @@ -14,7 +13,6 @@ const int COLS = 7; const int ROWS = 6; -// --- Configuration & Globals --- CRGB leds[NUM_LEDS]; Encoder myEnc(ENC_A, ENC_B); WebServer server(80); @@ -41,6 +39,7 @@ uint32_t lastActivityTime = 0; uint32_t demoResetTimer = 0; bool isDemoOver = false; uint8_t demoPly = 4; +bool abortAi = false; uint8_t current_look_ahead; uint8_t current_brightness; @@ -66,8 +65,6 @@ void performAiMove(int8_t aiPlayer); void showMenu(); int getDynamicPly(); -// --- Utility & Rendering --- - int getIdx(int x, int y) { return (y * 8) + x; } void drawStaticUI() @@ -112,6 +109,16 @@ int getFirstEmptyRow(int col) return -1; } +bool isBoardFull() +{ + for (int column = 0; column < COLS; column++) + { + if (board[column][ROWS - 1] == 0) + return false; + } + return true; +} + int getDynamicPly() { if (!progressive_difficulty && gameState != DEMO) @@ -124,8 +131,6 @@ int getDynamicPly() return constrain(current_look_ahead + (occupiedCount / 7), 1, 10); } -// --- Visuals & Animations --- - void updateThinkingVisuals(int8_t playerColor, int8_t column) { static uint32_t lastCycle = 0; @@ -172,7 +177,7 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed) { int current = startCol; CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red; - while (current != targetCol) + while (current != targetCol && !abortAi) { leds[getIdx(current, 0)] = CRGB::Black; current += (targetCol > current) ? 1 : -1; @@ -180,89 +185,90 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed) leds[getIdx(current, 0)] = pColor; FastLED.show(); delay(speed); + if (digitalRead(ENC_SW) == LOW) + abortAi = true; } activeCol = targetCol; } -// --- AI Engine --- - -bool isBoardFull() -{ - for (int column = 0; column < COLS; column++) - if (board[column][5] == 0) - return false; - return true; -} - int8_t scanBoard() { memset(winMask, 0, sizeof(winMask)); auto checkMatch = [&](int col, int row, int dCol, int dRow) { - int8_t playerAtPos = board[col][row]; - if (playerAtPos != 0 && - board[col + dCol][row + dRow] == playerAtPos && - board[col + 2 * dCol][row + 2 * dRow] == playerAtPos && - board[col + 3 * dCol][row + 3 * dRow] == playerAtPos) + 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 playerAtPos; + return pAtPos; } return (int8_t)0; }; - - for (int row = 0; row < 6; row++) - for (int col = 0; col < 4; col++) + for (int r = 0; r < 6; r++) + for (int c = 0; c < 4; c++) { - int8_t result = checkMatch(col, row, 1, 0); - if (result) - return result; + int8_t res = checkMatch(c, r, 1, 0); + if (res) + return res; } - for (int row = 0; row < 3; row++) - for (int col = 0; col < 7; col++) + for (int r = 0; r < 3; r++) + for (int c = 0; c < 7; c++) { - int8_t result = checkMatch(col, row, 0, 1); - if (result) - return result; + int8_t res = checkMatch(c, r, 0, 1); + if (res) + return res; } - for (int row = 0; row < 3; row++) - for (int col = 0; col < 4; col++) + for (int r = 0; r < 3; r++) + for (int c = 0; c < 4; c++) { - int8_t result = checkMatch(col, row, 1, 1); - if (result) - return result; + int8_t res = checkMatch(c, r, 1, 1); + if (res) + return res; } - for (int row = 3; row < 6; row++) - for (int col = 0; col < 4; col++) + for (int r = 3; r < 6; r++) + for (int c = 0; c < 4; c++) { - int8_t result = checkMatch(col, row, 1, -1); - if (result) - return result; + int8_t res = checkMatch(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; - // Check winner via temporary scan (logic check only) int8_t winner = scanBoard(); if (winner == aiPlayer) - return 1000 + depth; + return 1000 + depth; // Win sooner is better if (winner == humanPlayer) - return -1000 - depth; + return -1000 - depth; // Lose later is better if (depth == 0 || isBoardFull()) return 0; int colOrder[] = {3, 2, 4, 1, 5, 0, 6}; - int bestScore = isMax ? -2000 : 2000; + int bestScore = isMax ? -10000 : 10000; + for (int column : colOrder) { + if (abortAi) + return 0; int row = getFirstEmptyRow(column); if (row != -1) { @@ -288,13 +294,14 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t void performAiMove(int8_t aiPlayer) { + abortAi = false; int humanPlayer = (aiPlayer == 1) ? 2 : 1; int bestScore = -30000; int bestCol = 3; int originalPly = current_look_ahead; current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly(); - // Instant win/block logic + // PHASE 1: Immediate Win Check (OFFENSE) for (int column = 0; column < COLS; column++) { int row = getFirstEmptyRow(column); @@ -305,21 +312,34 @@ void performAiMove(int8_t aiPlayer) { board[column][row] = 0; bestCol = column; - goto finalizeMove; - } - board[column][row] = humanPlayer; - if (current_look_ahead >= 2 && scanBoard() == humanPlayer) - { - board[column][row] = 0; - bestCol = column; - goto finalizeMove; + goto finalizeMove; // TAKE THE WIN IMMEDIATELY } board[column][row] = 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}) { + if (abortAi) + goto finalizeMove; int row = getFirstEmptyRow(column); if (row != -1) { @@ -334,7 +354,7 @@ void performAiMove(int8_t aiPlayer) } } - if ((gameState == DEMO || blunder_enabled) && random(100) < 20) + if ((gameState == DEMO || blunder_enabled) && random(100) < 20 && !abortAi) { int randomColumn = random(0, 7); if (getFirstEmptyRow(randomColumn) != -1) @@ -343,26 +363,19 @@ void performAiMove(int8_t aiPlayer) finalizeMove: current_look_ahead = originalPly; - moveDiscToCol(activeCol, bestCol, aiPlayer, 100); - delay(450); - animateDrop(bestCol, aiPlayer); + if (!abortAi) + { + moveDiscToCol(activeCol, bestCol, aiPlayer, 100); + delay(450); + animateDrop(bestCol, aiPlayer); + } } -// --- Web Portal --- - void handleRoot() { - String html = "" - ""; - html += "

Connect 4 Admin

"; - html += "Base AI Ply (1-10):"; - html += "Brightness (5-255):"; - html += "Idle Timeout (Sec):"; - html += "Enable Blunders:
"; - html += "Evolution Mode:

"; - html += "
"; + String html = "

Connect 4 Admin

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

"; server.send(200, "text/html", html); } @@ -381,9 +394,8 @@ void handleSave() } if (server.hasArg("idle")) { - uint32_t s = server.arg("idle").toInt(); - current_idle_timeout_ms = s * 1000; - prefs.putUInt("idle", s); + 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); @@ -405,13 +417,13 @@ void showMenu() #endif if (menuMode < 2) { - CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; + CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; for (int y = 3; y <= 6; y++) - leds[getIdx(3, y)] = p1Col; - leds[getIdx(2, 3)] = p1Col; - leds[getIdx(4, 3)] = p1Col; - leds[getIdx(2, 6)] = p1Col; - leds[getIdx(4, 6)] = p1Col; + 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 { @@ -441,7 +453,6 @@ void setup() 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); @@ -459,23 +470,25 @@ void loop() long newPos = myEnc.read() / SENSITIVITY; bool pressed = (digitalRead(ENC_SW) == LOW); - // Activity check if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500))) { if (gameState >= 2 || gameState == DEMO) { - for (int index = 0; index < 10; index++) - { - fadeToBlackBy(leds, NUM_LEDS, 32); - FastLED.show(); - delay(30); - } - delay(500); - gameState = MENU; + 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(); - lastActivityTime = millis(); oldEncPos = newPos; + lastActivityTime = millis(); + delay(300); return; } lastActivityTime = millis(); @@ -550,17 +563,20 @@ void loop() { int8_t aiP = (menuMode == 0) ? 2 : 1; performAiMove(aiP); - lastActivityTime = millis(); // Reset after AI thinking - winnerPlayer = scanBoard(); - if (winnerPlayer != 0) + lastActivityTime = millis(); + if (!abortAi) { - gameState = FINISHED_WIN; - demoResetTimer = millis(); - } - else if (isBoardFull()) - { - gameState = FINISHED_DRAW; - demoResetTimer = millis(); + winnerPlayer = scanBoard(); + if (winnerPlayer != 0) + { + gameState = FINISHED_WIN; + demoResetTimer = millis(); + } + else if (isBoardFull()) + { + gameState = FINISHED_DRAW; + demoResetTimer = millis(); + } } } else @@ -578,24 +594,27 @@ void loop() FastLED.show(); delay(600); performAiMove(currentPlayer); - winnerPlayer = scanBoard(); - if (winnerPlayer != 0) + if (!abortAi) { - gameState = FINISHED_WIN; - demoResetTimer = millis(); - } - else if (isBoardFull()) - { - gameState = FINISHED_DRAW; - demoResetTimer = millis(); - } - else - { - currentPlayer = (currentPlayer == 1) ? 2 : 1; + 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 - { // FINISHED state (WIN/DRAW) + { static uint32_t lastFlash = 0; static bool toggle = true; if (millis() - lastFlash > 300) @@ -631,11 +650,5 @@ void loop() demoResetTimer = 0; demoPly = random(3, 7); } - if (pressed) - { - gameState = MENU; - showMenu(); - delay(300); - } } -} \ No newline at end of file +}