diff --git a/platformio.ini b/platformio.ini index e9fde22..e8a33f0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,14 +12,12 @@ build_flags = -D ENC_SW=2 -D SENSITIVITY=4 -D SHOW_BORDER=0 - -D BRIGHTNESS=25 - -D IDLE_TIMEOUT=45000 -D DEMO_RESET_PAUSE=20000 - -D DEBOUNCE_DELAY=50 -D DEFAULT_LOOK_AHEAD=8 -D DEFAULT_BRIGHTNESS=25 -D DEFAULT_IDLE_TIMEOUT=45 -D MAX_GAME_LOG=100 + -D WIFI_SSID=\"Connect4\" -D WIFI_PASSWORD=\"youlose4\" lib_deps = fastled/FastLED @ 3.9.12 diff --git a/src/main.cpp b/src/main.cpp index 889431d..54c7d7f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,16 +13,48 @@ #define SENSITIVITY 4 #endif +#ifndef LED_PIN #define LED_PIN 4 +#endif + +#ifndef ENC_A #define ENC_A 0 +#endif + +#ifndef ENC_B #define ENC_B 1 +#endif + +#ifndef ENC_SW #define ENC_SW 2 +#endif + #define NUM_LEDS 64 #ifndef MAX_GAME_LOG #define MAX_GAME_LOG 5 #endif +#ifndef DEFAULT_LOOK_AHEAD +#define DEFAULT_LOOK_AHEAD 8 +#endif + +#ifndef DEFAULT_BRIGHTNESS +#define DEFAULT_BRIGHTNESS 25 +#endif + +#ifndef DEFAULT_IDLE_TIMEOUT +#define DEFAULT_IDLE_TIMEOUT 60 +#endif + +#ifndef DEMO_RESET_PAUSE +#define DEMO_RESET_PAUSE 30000 +#endif + +#ifndef WIFI_SSID +#define WIFI_SSID "Connect4" +#endif + const int COLS = 7; const int ROWS = 6; const int colOrder[] = {3, 2, 4, 1, 5, 0, 6}; @@ -49,11 +81,11 @@ uint8_t demoPly[2] = {4, 4}; bool abortAi = false; bool lastButtonState = HIGH; -uint8_t currentLookAhead = 6; -uint8_t currentBrightness = 30; -uint32_t currentIdleTimeoutMs = 60000; +uint8_t currentLookAhead = DEFAULT_LOOK_AHEAD; +uint8_t currentBrightness = DEFAULT_BRIGHTNESS; +uint32_t currentIdleTimeoutMs = DEFAULT_IDLE_TIMEOUT * 1000; bool blunderEnabled = false; -bool progressiveDifficulty = false; +uint8_t blunderChance = 20; uint8_t aiBrightness = 0; bool aiFadeUp = true; @@ -84,7 +116,6 @@ void renderBoard(); void showMenu(); int getFirstEmptyRow(int col); bool isBoardFull(); -int getDynamicPly(); int8_t scanBoard(); bool checkGameEnd(); void updateThinkingVisuals(int8_t pColor, int8_t column); @@ -217,13 +248,6 @@ bool isBoardFull() { return true; } -int getDynamicPly() { - if (!progressiveDifficulty && gameState != DEMO) return currentLookAhead; - 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(currentLookAhead + (count / 7), 1, 10); -} - int8_t scanBoard() { memset(winMask, 0, sizeof(winMask)); auto check = [&](int c, int r, int dc, int dr) { @@ -243,21 +267,14 @@ int8_t scanBoard() { bool checkGameEnd() { winnerPlayer = scanBoard(); - if (winnerPlayer != 0) { - if (gameState != DEMO) logGame(winnerPlayer); - gameState = FINISHED_WIN; - demoResetTimer = millis(); - lastActivityTime = millis(); - return true; - } - if (isBoardFull()) { - if (gameState != DEMO) logGame(0); - gameState = FINISHED_DRAW; - demoResetTimer = millis(); - lastActivityTime = millis(); - return true; - } - return false; + bool won = winnerPlayer != 0; + bool draw = !won && isBoardFull(); + if (!won && !draw) return false; + if (gameState != DEMO) logGame(won ? winnerPlayer : 0); + gameState = won ? FINISHED_WIN : FINISHED_DRAW; + demoResetTimer = millis(); + lastActivityTime = millis(); + return true; } // --- Animation --- @@ -337,27 +354,41 @@ void performAiMove(int8_t aiP) { int huP = (aiP == 1) ? 2 : 1; int bestScore = -30000; int bestCol = 3; int originalPly = currentLookAhead; - currentLookAhead = (gameState == DEMO) ? demoPly[aiP - 1] : getDynamicPly(); + if (gameState == DEMO) currentLookAhead = demoPly[aiP - 1]; - for (int c = 0; c < COLS; c++) { + // Phase 1: always take an instant win or block an opponent's win + bool found = false; + for (int c = 0; c < COLS && !found; c++) { int r = getFirstEmptyRow(c); if (r != -1) { - board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; goto finalizeMove; } - board[c][r] = huP; if (scanBoard() == huP) { board[c][r]=0; bestCol=c; goto finalizeMove; } + board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; found=true; break; } + board[c][r] = huP; if (scanBoard() == huP) { board[c][r]=0; bestCol=c; found=true; break; } board[c][r] = 0; } } - for (int c : colOrder) { - if (abortAi) goto finalizeMove; - int r = getFirstEmptyRow(c); - if (r != -1) { - board[c][r] = aiP; - int score = minimax(currentLookAhead, -30000, 30000, false, aiP, huP, c); - board[c][r] = 0; - if (score > bestScore) { bestScore = score; bestCol = c; } + + // Phase 2: blunder — pick a random column instead of deep search + if (!found && blunderEnabled && gameState != DEMO && (random(100) < blunderChance)) { + int validCols[COLS], count = 0; + for (int c = 0; c < COLS; c++) if (getFirstEmptyRow(c) != -1) validCols[count++] = c; + bestCol = validCols[random(count)]; + found = true; + } + + // Phase 3: deep minimax search + if (!found) { + for (int c : colOrder) { + if (abortAi) break; + int r = getFirstEmptyRow(c); + if (r != -1) { + board[c][r] = aiP; + int score = minimax(currentLookAhead, -30000, 30000, false, aiP, huP, c); + board[c][r] = 0; + if (score > bestScore) { bestScore = score; bestCol = c; } + } } } -finalizeMove: + currentLookAhead = originalPly; if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } } } @@ -377,8 +408,8 @@ void handleRoot() { html += "Base AI Ply:"; html += "Brightness:"; html += "Idle Timeout (s):"; - html += "Blunders:
"; - html += "Evolution:

"; + html += "Blunders: "; + html += " Chance (%):

"; html += ""; html += ""; html += "

Game Log

"; @@ -405,7 +436,7 @@ void handleSave() { if (server.hasArg("br")) { currentBrightness = server.arg("br").toInt(); FastLED.setBrightness(currentBrightness); prefs.putUChar("br", currentBrightness); } if (server.hasArg("idle")) { currentIdleTimeoutMs = server.arg("idle").toInt() * 1000; prefs.putUInt("idle", currentIdleTimeoutMs / 1000); } blunderEnabled = server.hasArg("blunder"); prefs.putBool("blunder", blunderEnabled); - progressiveDifficulty = server.hasArg("evolve"); prefs.putBool("evolve", progressiveDifficulty); + if (server.hasArg("blunderPct")) { blunderChance = constrain(server.arg("blunderPct").toInt(), 1, 100); prefs.putUChar("blPct", blunderChance); } server.sendHeader("Location", "/"); server.send(303); } @@ -485,7 +516,7 @@ void handleFinished() { } FastLED.show(); } - if (millis() - demoResetTimer > 30000) { + if (millis() - demoResetTimer > DEMO_RESET_PAUSE) { resetBoard(); randomizeDemoPlies(); gameState = DEMO; @@ -498,16 +529,16 @@ void handleFinished() { void setup() { prefs.begin("c4-game", false); - currentLookAhead = prefs.getUChar("ply", 8); - currentBrightness = prefs.getUChar("br", 25); - currentIdleTimeoutMs = prefs.getUInt("idle", 60) * 1000; + currentLookAhead = prefs.getUChar("ply", DEFAULT_LOOK_AHEAD); + currentBrightness = prefs.getUChar("br", DEFAULT_BRIGHTNESS); + currentIdleTimeoutMs = prefs.getUInt("idle", DEFAULT_IDLE_TIMEOUT) * 1000; blunderEnabled = prefs.getBool("blunder", false); - progressiveDifficulty = prefs.getBool("evolve", false); + blunderChance = prefs.getUChar("blPct", 20); loadGameLog(); FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(currentBrightness); pinMode(ENC_SW, INPUT_PULLUP); - WiFi.softAP("Connect4-Config", WIFI_PASSWORD); + WiFi.softAP(WIFI_SSID, WIFI_PASSWORD); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin();