[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:
+1
-3
@@ -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
|
||||
|
||||
+80
-49
@@ -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:<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();
|
||||
|
||||
Reference in New Issue
Block a user