[refactor] Replace hardcoded values with build flags and cleanup duplicates.

- Add #ifndef guards for pin defines duplicated between platformio.ini and main.cpp
- Use DEFAULT_LOOK_AHEAD, DEFAULT_BRIGHTNESS, DEFAULT_IDLE_TIMEOUT, DEMO_RESET_PAUSE
  build flags instead of hardcoded magic numbers
- Add WIFI_SSID build flag for configurable access point name
- Remove unused build flags (BRIGHTNESS, IDLE_TIMEOUT, DEBOUNCE_DELAY)
- Remove progressive difficulty / evolution feature (getDynamicPly)
- Replace goto with structured control flow in performAiMove
- Deduplicate checkGameEnd win/draw branches
- Implement blunder mode: configurable chance (%) to pick a random column,
  preserving instant win/block detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 16:03:41 +01:00
parent 2eecc94cfd
commit 0fc20da274
2 changed files with 81 additions and 52 deletions
+1 -3
View File
@@ -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
+70 -39
View File
@@ -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;
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;
}
if (isBoardFull()) {
if (gameState != DEMO) logGame(0);
gameState = FINISHED_DRAW;
demoResetTimer = millis();
lastActivityTime = millis();
return true;
}
return false;
}
// --- Animation ---
@@ -337,18 +354,31 @@ 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;
}
}
// 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) goto finalizeMove;
if (abortAi) break;
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = aiP;
@@ -357,7 +387,8 @@ void performAiMove(int8_t aiP) {
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:<input type='number' name='ply' value='" + String(currentLookAhead) + "'>";
html += "Brightness:<input type='number' name='br' value='" + String(currentBrightness) + "'>";
html += "Idle Timeout (s):<input type='number' name='idle' value='" + String(currentIdleTimeoutMs / 1000) + "'>";
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunderEnabled ? "checked" : "") + "><br>";
html += "Evolution: <input type='checkbox' name='evolve' " + String(progressiveDifficulty ? "checked" : "") + "><br><br>";
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunderEnabled ? "checked" : "") + ">";
html += " Chance (%):<input type='number' name='blunderPct' min='1' max='100' value='" + String(blunderChance) + "'><br><br>";
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;'>";
html += "</form></div>";
html += "<div class='card' style='margin-top:15px;text-align:left;'><h3 style='text-align:center;'>Game Log</h3>";
@@ -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<WS2812B, LED_PIN, GRB>(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();