[refactor] Game log, persistence, demo mode, indicator LED, and documentation.

This commit is contained in:
2026-03-15 15:47:43 +01:00
parent b5d696bf30
commit f489442cb5
5 changed files with 560 additions and 233 deletions
+369 -166
View File
@@ -10,7 +10,7 @@
#endif
#ifndef SENSITIVITY
#define SENSITIVITY 4
#define SENSITIVITY 4
#endif
#define LED_PIN 4
@@ -19,8 +19,13 @@
#define ENC_SW 2
#define NUM_LEDS 64
#ifndef MAX_GAME_LOG
#define MAX_GAME_LOG 5
#endif
const int COLS = 7;
const int ROWS = 6;
const int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
CRGB leds[NUM_LEDS];
Encoder myEnc(ENC_A, ENC_B);
@@ -32,122 +37,191 @@ bool winMask[NUM_LEDS];
enum State { MENU, PLAYING, AI_TURN, FINISHED_WIN, FINISHED_DRAW, DEMO };
State gameState = MENU;
int8_t menuMode = 0;
int8_t menuMode = 0;
int8_t currentPlayer = 1;
int8_t winnerPlayer = 0;
int8_t winnerPlayer = 0;
int8_t activeCol = 3;
long oldEncPos = -999;
uint32_t lastActivityTime = 0;
uint32_t demoResetTimer = 0;
uint32_t globalInputCooldown = 0;
uint8_t demoPly = 4;
uint8_t demoPly[2] = {4, 4};
bool abortAi = false;
bool lastButtonState = HIGH;
bool lastButtonState = HIGH;
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 currentLookAhead = 6;
uint8_t currentBrightness = 30;
uint32_t currentIdleTimeoutMs = 60000;
bool blunderEnabled = false;
bool progressiveDifficulty = false;
uint8_t aiBrightness = 0;
bool aiFadeUp = true;
struct GameEntry {
char type;
uint8_t level;
char winner;
String moves;
};
GameEntry gameLog[MAX_GAME_LOG];
uint8_t gameLogCount = 0;
String currentMoves = "";
int8_t gameMenuMode = 0;
uint8_t gameLevel = 0;
// --- Prototypes ---
CRGB playerColor(int8_t player);
int getIdx(int x, int y);
void resetBoard();
void drawBorder(CRGB color);
void logGame(int8_t winner);
void saveGameLog();
void loadGameLog();
void drawStaticUI();
void renderBoard();
void showMenu();
int getFirstEmptyRow(int col);
bool isBoardFull();
int8_t scanBoard();
int getDynamicPly();
int8_t scanBoard();
bool checkGameEnd();
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 aiP, int8_t huP, int8_t rootCol);
void performAiMove(int8_t aiP);
void showMenu();
int getDynamicPly();
void randomizeDemoPlies();
void handleRoot();
void handleSave();
void handleMenu(long newPos, bool pressed);
void handlePlaying(long newPos, bool pressed);
void handleAiTurn();
void handleDemo();
void handleFinished();
// --- Helpers ---
CRGB playerColor(int8_t player) {
return (player == 1) ? CRGB::Yellow : CRGB::Red;
}
int getIdx(int x, int y) { return (y * 8) + x; }
void resetBoard() {
memset(board, 0, sizeof(board));
winnerPlayer = 0;
}
void drawBorder(CRGB color) {
for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = color;
for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = color;
}
void logGame(int8_t winner) {
char type = (gameMenuMode == 0) ? 'Y' : (gameMenuMode == 1) ? 'R' : '2';
char winChar = (winner == 1) ? 'Y' : (winner == 2) ? 'R' : 'D';
GameEntry entry = { type, gameLevel, winChar, currentMoves };
if (gameLogCount < MAX_GAME_LOG) { gameLog[gameLogCount++] = entry; }
else { for (int i = 0; i < MAX_GAME_LOG - 1; i++) gameLog[i] = gameLog[i + 1]; gameLog[MAX_GAME_LOG - 1] = entry; }
saveGameLog();
}
void saveGameLog() {
prefs.putUChar("glc", gameLogCount);
for (int i = 0; i < gameLogCount; i++) {
String val = String(gameLog[i].type) + ":" + String(gameLog[i].level) + ":" + String(gameLog[i].winner) + ":" + gameLog[i].moves;
prefs.putString(("g" + String(i)).c_str(), val);
}
}
void loadGameLog() {
gameLogCount = prefs.getUChar("glc", 0);
if (gameLogCount > MAX_GAME_LOG) gameLogCount = MAX_GAME_LOG;
for (int i = 0; i < gameLogCount; i++) {
String val = prefs.getString(("g" + String(i)).c_str(), "");
if (val.length() < 5) { gameLogCount = i; break; }
gameLog[i].type = val.charAt(0);
int sep1 = val.indexOf(':', 2);
gameLog[i].level = val.substring(2, sep1).toInt();
gameLog[i].winner = val.charAt(sep1 + 1);
gameLog[i].moves = val.substring(sep1 + 3);
}
}
void randomizeDemoPlies() {
uint8_t strong = random(4, 6);
uint8_t weak = random(2, 4);
if (random(2) == 0) { demoPly[0] = strong; demoPly[1] = weak; }
else { demoPly[0] = weak; demoPly[1] = strong; }
}
// --- Display ---
void drawStaticUI() {
FastLED.clear();
#if SHOW_BORDER == 1
CRGB borderColor = CRGB::Blue;
if (gameState == DEMO || gameState >= 3) {
if (gameState == DEMO || gameState == FINISHED_WIN || gameState == FINISHED_DRAW) {
uint8_t glow = beat8(15);
borderColor = blend(CRGB::Blue, CRGB::White, glow / 4);
}
for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = borderColor;
for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = borderColor;
drawBorder(borderColor);
#endif
}
void renderBoard() {
drawStaticUI();
if (gameState == PLAYING || gameState == AI_TURN) {
CRGB indicator = (menuMode == 0) ? CRGB::Yellow : (menuMode == 1) ? CRGB::Red : CRGB::Blue;
indicator.nscale8(25);
leds[getIdx(7, 0)] = indicator;
}
for (int c = 0; c < COLS; c++) {
for (int r = 0; r < ROWS; r++) {
if (board[c][r] == 1) leds[getIdx(c, 7 - r)] = CRGB::Yellow;
if (board[c][r] == 2) leds[getIdx(c, 7 - r)] = CRGB::Red;
if (board[c][r] != 0) leds[getIdx(c, 7 - r)] = playerColor(board[c][r]);
}
}
}
void showMenu() {
FastLED.clear();
#if SHOW_BORDER == 1
drawBorder(CRGB::Blue);
#endif
CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
if (menuMode < 2) {
for (int y = 3; y <= 6; y++) 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 {
for (int y = 3; y <= 6; y++) { leds[getIdx(2, y)] = CRGB::Yellow; leds[getIdx(4, y)] = CRGB::Red; }
leds[getIdx(1, 3)] = CRGB::Yellow; leds[getIdx(3, 3)] = CRGB::Yellow;
leds[getIdx(1, 6)] = CRGB::Yellow; leds[getIdx(3, 6)] = CRGB::Yellow;
leds[getIdx(3, 3)] = CRGB::Red; leds[getIdx(5, 3)] = CRGB::Red;
leds[getIdx(3, 6)] = CRGB::Red; leds[getIdx(5, 6)] = CRGB::Red;
}
FastLED.show();
}
// --- Game logic ---
int getFirstEmptyRow(int col) {
for (int r = 0; r < ROWS; r++) if (board[col][r] == 0) return r;
return -1;
}
bool isBoardFull() {
for (int c = 0; c < COLS; c++) if (board[c][ROWS-1] == 0) return false;
for (int c = 0; c < COLS; c++) if (board[c][ROWS - 1] == 0) return false;
return true;
}
int getDynamicPly() {
if (!progressive_difficulty && gameState != DEMO) return current_look_ahead;
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(current_look_ahead + (count / 7), 1, 10);
}
void updateThinkingVisuals(int8_t pColor, int8_t column) {
static uint32_t lastCycle = 0;
if (millis() - lastCycle < 20) return;
lastCycle = millis();
if (aiFadeUp) { aiBrightness += 25; if (aiBrightness >= 230) aiFadeUp = false; }
else { aiBrightness -= 25; if (aiBrightness <= 25) aiFadeUp = true; }
for (int x = 0; x < COLS; x++) leds[getIdx(x, 0)] = CRGB::Black;
CRGB aiColor = (pColor == 1) ? CRGB(CRGB::Yellow) : CRGB(CRGB::Red);
aiColor.nscale8(aiBrightness);
leds[getIdx(column, 0)] = aiColor;
FastLED.show();
}
void animateDrop(int col, int player) {
int targetRow = getFirstEmptyRow(col);
if (targetRow == -1) return;
for (int r = 5; r >= targetRow; r--) {
renderBoard();
leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red;
FastLED.show();
delay(max(10, 60 - (5 - r) * 10));
}
board[col][targetRow] = player;
}
void moveDiscToCol(int startCol, int targetCol, int player, int speed) {
int current = startCol;
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)] = colr;
FastLED.show();
delay(speed);
}
activeCol = targetCol;
return constrain(currentLookAhead + (count / 7), 1, 10);
}
int8_t scanBoard() {
@@ -167,9 +241,73 @@ int8_t scanBoard() {
return 0;
}
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;
}
// --- Animation ---
void updateThinkingVisuals(int8_t pColor, int8_t column) {
static uint32_t lastCycle = 0;
if (millis() - lastCycle < 20) return;
lastCycle = millis();
if (aiFadeUp) { aiBrightness += 25; if (aiBrightness >= 230) aiFadeUp = false; }
else { aiBrightness -= 25; if (aiBrightness <= 25) aiFadeUp = true; }
for (int x = 0; x < COLS; x++) leds[getIdx(x, 0)] = CRGB::Black;
CRGB aiColor = playerColor(pColor);
aiColor.nscale8(aiBrightness);
leds[getIdx(column, 0)] = aiColor;
FastLED.show();
}
void animateDrop(int col, int player) {
int targetRow = getFirstEmptyRow(col);
if (targetRow == -1) return;
if (gameState != DEMO) currentMoves += String(col);
for (int r = 5; r >= targetRow; r--) {
renderBoard();
leds[getIdx(col, 7 - r)] = playerColor(player);
FastLED.show();
delay(max(10, 60 - (5 - r) * 10));
}
board[col][targetRow] = player;
}
void moveDiscToCol(int startCol, int targetCol, int player, int speed) {
int current = startCol;
CRGB colr = playerColor(player);
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)] = colr;
FastLED.show();
delay(speed);
}
activeCol = targetCol;
}
// --- AI ---
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol) {
if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { abortAi = true; return 0; }
if (depth >= current_look_ahead - 1) updateThinkingVisuals(aiP, rootCol);
if (depth >= currentLookAhead - 1) updateThinkingVisuals(aiP, rootCol);
else yield();
if (abortAi) return 0;
@@ -178,14 +316,13 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP,
if (win == huP) return -1000 - depth;
if (depth == 0 || isBoardFull()) return 0;
int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
int best = isMax ? -10000 : 10000;
for (int c : colOrder) {
if (abortAi) return 0;
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = isMax ? aiP : huP;
int score = minimax(depth - 1, alpha, beta, !isMax, aiP, huP, (depth == current_look_ahead ? c : rootCol));
int score = minimax(depth - 1, alpha, beta, !isMax, aiP, huP, (depth == currentLookAhead ? c : rootCol));
board[c][r] = 0;
if (isMax) { if (score > best) best = score; if (best > alpha) alpha = best; }
else { if (score < best) best = score; if (best < beta) beta = best; }
@@ -199,10 +336,10 @@ void performAiMove(int8_t aiP) {
abortAi = false;
int huP = (aiP == 1) ? 2 : 1;
int bestScore = -30000; int bestCol = 3;
int originalPly = current_look_ahead;
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
int originalPly = currentLookAhead;
currentLookAhead = (gameState == DEMO) ? demoPly[aiP - 1] : getDynamicPly();
for (int c=0; c<COLS; c++) {
for (int c = 0; c < COLS; c++) {
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
@@ -210,144 +347,210 @@ void performAiMove(int8_t aiP) {
board[c][r] = 0;
}
}
for (int c : {3, 2, 4, 1, 5, 0, 6}) {
for (int c : colOrder) {
if (abortAi) goto finalizeMove;
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = aiP;
int score = minimax(current_look_ahead, -30000, 30000, false, aiP, huP, c);
int score = minimax(currentLookAhead, -30000, 30000, false, aiP, huP, c);
board[c][r] = 0;
if (score > bestScore) { bestScore = score; bestCol = c; }
}
}
finalizeMove:
current_look_ahead = originalPly;
currentLookAhead = originalPly;
if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } }
}
// --- Web handlers ---
void handleRoot() {
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'><style>body{font-family:sans-serif;background:#121212;color:white;text-align:center;} .card{background:#222;padding:25px;border-radius:15px;display:inline-block;margin-top:20px;} input{width:100%;padding:10px;margin:10px 0;border-radius:5px;border:none;}</style></head><body><h1>Connect 4 Admin</h1><div class='card'><form action='/save' method='POST'>";
html += "Base AI Ply:<input type='number' name='ply' value='" + String(current_look_ahead) + "'>Brightness:<input type='number' name='br' value='" + String(current_brightness) + "'>Idle Timeout (s):<input type='number' name='idle' value='" + String(current_idle_timeout_ms / 1000) + "'>";
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunder_enabled ? "checked" : "") + "><br>Evolution: <input type='checkbox' name='evolve' " + String(progressive_difficulty ? "checked" : "") + "><br><br><input type='submit' value='Save Settings' style='background:#28a745;color:white;'></form></div></body></html>";
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>"
"<style>"
"body{font-family:sans-serif;background:#121212;color:white;text-align:center;}"
".card{background:#222;padding:25px;border-radius:15px;display:inline-block;margin-top:20px;}"
"input{width:100%;padding:10px;margin:10px 0;border-radius:5px;border:none;}"
"table{width:100%;} th,td{padding:4px;}"
"</style></head><body>"
"<h1>Connect 4 Admin</h1>"
"<div class='card'><form action='/save' method='POST'>";
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 += "<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>";
if (gameLogCount == 0) {
html += "<p style='text-align:center;'>No games played yet.</p>";
} else {
html += "<table><tr><th>Type</th><th>Lvl</th><th>Winner</th><th>Moves</th></tr>";
for (int i = gameLogCount - 1; i >= 0; i--) {
bool playerWon = gameLog[i].type != '2' && gameLog[i].type == gameLog[i].winner;
html += playerWon ? "<tr style='color:#ff4444;'>" : "<tr>";
html += "<td>" + String(gameLog[i].type) + "</td>";
html += "<td>" + String(gameLog[i].level) + "</td>";
html += "<td>" + String(gameLog[i].winner) + "</td>";
html += "<td>" + gameLog[i].moves + "</td></tr>";
}
html += "</table>";
}
html += "</div></body></html>";
server.send(200, "text/html", html);
}
void handleSave() {
if (server.hasArg("ply")) { current_look_ahead = server.arg("ply").toInt(); prefs.putUChar("ply", current_look_ahead); }
if (server.hasArg("br")) { current_brightness = server.arg("br").toInt(); FastLED.setBrightness(current_brightness); prefs.putUChar("br", current_brightness); }
if (server.hasArg("idle")) { 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);
progressive_difficulty = server.hasArg("evolve"); prefs.putBool("evolve", progressive_difficulty);
if (server.hasArg("ply")) { currentLookAhead = server.arg("ply").toInt(); prefs.putUChar("ply", currentLookAhead); }
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);
server.sendHeader("Location", "/"); server.send(303);
}
void showMenu() {
FastLED.clear();
#if SHOW_BORDER == 1
for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = CRGB::Blue;
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) { for (int y = 3; y <= 6; y++) 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 { for (int y = 3; y <= 6; y++) { leds[getIdx(2, y)] = CRGB::Yellow; leds[getIdx(4, y)] = CRGB::Red; } leds[getIdx(1, 3)] = CRGB::Yellow; leds[getIdx(3, 3)] = CRGB::Yellow; leds[getIdx(1, 6)] = CRGB::Yellow; leds[getIdx(3, 6)] = CRGB::Yellow; leds[getIdx(3, 3)] = CRGB::Red; leds[getIdx(5, 3)] = CRGB::Red; leds[getIdx(3, 6)] = CRGB::Red; leds[getIdx(5, 6)] = CRGB::Red; }
FastLED.show();
// --- State handlers ---
void handleMenu(long newPos, bool pressed) {
if (millis() > globalInputCooldown) {
if (newPos != oldEncPos) { menuMode = (newPos % 3 + 3) % 3; oldEncPos = newPos; showMenu(); }
if (pressed) {
resetBoard();
currentMoves = "";
gameMenuMode = menuMode;
gameLevel = currentLookAhead;
currentPlayer = 1;
if (menuMode == 1) gameState = AI_TURN;
else gameState = PLAYING;
globalInputCooldown = millis() + 500;
}
}
}
void handlePlaying(long newPos, bool pressed) {
if (newPos != oldEncPos) { activeCol = (newPos % 7 + 7) % 7; oldEncPos = newPos; lastActivityTime = millis(); }
renderBoard();
leds[getIdx(activeCol, 0)] = playerColor(currentPlayer);
FastLED.show();
if (pressed) {
int row = getFirstEmptyRow(activeCol);
if (row != -1) {
animateDrop(activeCol, currentPlayer);
if (!checkGameEnd()) {
if (menuMode < 2) gameState = AI_TURN;
else currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
lastActivityTime = millis();
}
}
}
void handleAiTurn() {
int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove(aiP);
if (abortAi) { gameState = MENU; showMenu(); return; }
if (!checkGameEnd()) {
gameState = PLAYING;
currentPlayer = (aiP == 1) ? 2 : 1;
}
lastActivityTime = millis();
}
void handleDemo() {
renderBoard(); FastLED.show(); delay(300);
performAiMove(currentPlayer);
if (abortAi) { gameState = MENU; showMenu(); globalInputCooldown = millis() + 600; lastButtonState = LOW; return; }
if (!checkGameEnd()) {
currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
}
void handleFinished() {
static uint32_t lastFlash = 0;
static bool toggle = true;
if (millis() - lastFlash > 300) {
lastFlash = millis();
toggle = !toggle;
renderBoard();
for (int i = 0; i < NUM_LEDS; i++) {
#if SHOW_BORDER == 1
if (leds[i] == CRGB::Blue) continue;
#endif
if (gameState == FINISHED_WIN) {
if (winMask[i]) leds[i] = toggle ? playerColor(winnerPlayer) : CRGB::Black;
else { CRGB c = leds[i]; c.nscale8(60); leds[i] = c; }
} else if (gameState == FINISHED_DRAW) {
if (!toggle) leds[i] = CRGB::Black;
}
}
FastLED.show();
}
if (millis() - demoResetTimer > 30000) {
resetBoard();
randomizeDemoPlies();
gameState = DEMO;
demoResetTimer = 0;
lastActivityTime = millis();
}
}
// --- Main ---
void setup() {
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<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(current_brightness);
pinMode(ENC_SW, INPUT_PULLUP); WiFi.softAP("Connect4-Config", "12345678");
server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin();
lastActivityTime = millis(); showMenu();
currentLookAhead = prefs.getUChar("ply", 8);
currentBrightness = prefs.getUChar("br", 25);
currentIdleTimeoutMs = prefs.getUInt("idle", 60) * 1000;
blunderEnabled = prefs.getBool("blunder", false);
progressiveDifficulty = prefs.getBool("evolve", false);
loadGameLog();
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(currentBrightness);
pinMode(ENC_SW, INPUT_PULLUP);
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.begin();
lastActivityTime = millis();
showMenu();
}
void loop() {
server.handleClient();
long rawPos = myEnc.read();
long newPos = rawPos / SENSITIVITY;
long newPos = rawPos / SENSITIVITY;
bool currentButton = digitalRead(ENC_SW);
bool pressed = false;
if (currentButton == LOW && lastButtonState == HIGH) { if (millis() > globalInputCooldown) pressed = true; }
lastButtonState = currentButton;
// Interrupt check
if ((newPos != oldEncPos || pressed) && (gameState >= 3 || gameState == DEMO)) {
abortAi = true; memset(board, 0, sizeof(board)); winnerPlayer = 0;
// Interrupt: return to menu from finished/demo states
if ((newPos != oldEncPos || pressed) && (gameState == FINISHED_WIN || gameState == FINISHED_DRAW || gameState == DEMO)) {
abortAi = true;
resetBoard();
for (int i = 0; i < 10; i++) { fadeToBlackBy(leds, NUM_LEDS, 50); FastLED.show(); delay(15); }
gameState = MENU; showMenu(); oldEncPos = newPos; lastActivityTime = millis();
globalInputCooldown = millis() + 600; return;
globalInputCooldown = millis() + 600;
return;
}
// Idle watchdog logic (Added specific exemption for FINISHED state)
if (gameState != DEMO && (gameState < 3)) {
if (millis() - lastActivityTime > current_idle_timeout_ms) {
gameState = DEMO; memset(board, 0, sizeof(board)); currentPlayer = 1; return;
// Idle timeout: enter demo mode
if (gameState != DEMO && gameState != FINISHED_WIN && gameState != FINISHED_DRAW) {
if (millis() - lastActivityTime > currentIdleTimeoutMs) {
resetBoard();
randomizeDemoPlies();
gameState = DEMO;
currentPlayer = 1;
return;
}
}
if (gameState == MENU) {
if (millis() > globalInputCooldown) {
if (newPos != oldEncPos) { menuMode = (newPos % 3 + 3) % 3; oldEncPos = newPos; showMenu(); }
if (pressed) { memset(board, 0, sizeof(board)); if (menuMode == 1) { currentPlayer = 1; gameState = AI_TURN; } else { currentPlayer = 1; gameState = PLAYING; } globalInputCooldown = millis() + 500; }
}
}
else if (gameState == PLAYING) {
if (newPos != oldEncPos) { activeCol = (newPos % 7 + 7) % 7; oldEncPos = newPos; lastActivityTime = millis(); }
renderBoard(); leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red; FastLED.show();
if (pressed) {
int row = getFirstEmptyRow(activeCol);
if (row != -1) {
animateDrop(activeCol, currentPlayer);
winnerPlayer = scanBoard();
if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); }
else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); }
else { if (menuMode < 2) { gameState = AI_TURN; } else { currentPlayer = (currentPlayer == 1) ? 2 : 1; } }
lastActivityTime = millis();
}
}
}
else if (gameState == AI_TURN) {
int8_t aiP = (menuMode == 0) ? 2 : 1; performAiMove(aiP);
if (abortAi) { gameState = MENU; showMenu(); return; }
winnerPlayer = scanBoard();
if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); }
else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); }
else { gameState = PLAYING; currentPlayer = (aiP == 1) ? 2 : 1; }
lastActivityTime = millis();
switch (gameState) {
case MENU: handleMenu(newPos, pressed); break;
case PLAYING: handlePlaying(newPos, pressed); break;
case AI_TURN: handleAiTurn(); break;
case DEMO: handleDemo(); break;
case FINISHED_WIN:
case FINISHED_DRAW: handleFinished(); break;
}
else if (gameState == DEMO) {
renderBoard(); FastLED.show(); delay(300); performAiMove(currentPlayer);
if (abortAi) { gameState = MENU; showMenu(); return; }
winnerPlayer = scanBoard();
if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); }
else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = 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) {
lastFlash = millis(); toggle = !toggle; renderBoard();
for (int i = 0; i < NUM_LEDS; i++) {
#if SHOW_BORDER == 1
if (leds[i] == CRGB::Blue) continue;
#endif
if (gameState == FINISHED_WIN) {
if (winMask[i]) leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black;
else { CRGB c = leds[i]; c.nscale8(60); leds[i] = c; }
} else if (gameState == FINISHED_DRAW) { if (!toggle) leds[i] = CRGB::Black; }
}
FastLED.show();
}
// RECENT FIX: Prolonged display time for win (30 seconds)
if (millis() - demoResetTimer > 30000) {
memset(board, 0, sizeof(board));
gameState = DEMO;
demoResetTimer = 0;
lastActivityTime = millis(); // Refresh watchdog
}
}
}
}