[fix] Encoder sensitivity. Extend delay after win.

This commit is contained in:
2026-03-12 12:59:39 +01:00
parent c73d6e3686
commit 52c84301dc
+147 -455
View File
@@ -9,11 +9,19 @@
#define SHOW_BORDER 1 #define SHOW_BORDER 1
#endif #endif
#ifndef SENSITIVITY
#define SENSITIVITY 4
#endif
#define LED_PIN 4
#define ENC_A 0
#define ENC_B 1
#define ENC_SW 2
#define NUM_LEDS 64 #define NUM_LEDS 64
const int COLS = 7; const int COLS = 7;
const int ROWS = 6; const int ROWS = 6;
// --- Global Variables ---
CRGB leds[NUM_LEDS]; CRGB leds[NUM_LEDS];
Encoder myEnc(ENC_A, ENC_B); Encoder myEnc(ENC_A, ENC_B);
WebServer server(80); WebServer server(80);
@@ -21,15 +29,7 @@ Preferences prefs;
int8_t board[COLS][ROWS]; int8_t board[COLS][ROWS];
bool winMask[NUM_LEDS]; bool winMask[NUM_LEDS];
enum State enum State { MENU, PLAYING, AI_TURN, FINISHED_WIN, FINISHED_DRAW, DEMO };
{
MENU,
PLAYING,
AI_TURN,
FINISHED_WIN,
FINISHED_DRAW,
DEMO
};
State gameState = MENU; State gameState = MENU;
int8_t menuMode = 0; int8_t menuMode = 0;
@@ -40,10 +40,9 @@ long oldEncPos = -999;
uint32_t lastActivityTime = 0; uint32_t lastActivityTime = 0;
uint32_t demoResetTimer = 0; uint32_t demoResetTimer = 0;
uint32_t globalInputCooldown = 0; uint32_t globalInputCooldown = 0;
uint8_t demoPly = 4; // FIXED: Restored global declaration uint8_t demoPly = 4;
bool abortAi = false; bool abortAi = false;
bool lastButtonState = HIGH; bool lastButtonState = HIGH;
bool buttonBlocked = false;
uint8_t current_look_ahead = 6; uint8_t current_look_ahead = 6;
uint8_t current_brightness = 30; uint8_t current_brightness = 30;
@@ -69,105 +68,65 @@ void performAiMove(int8_t aiP);
void showMenu(); void showMenu();
int getDynamicPly(); int getDynamicPly();
// --- Functions ---
int getIdx(int x, int y) { return (y * 8) + x; } int getIdx(int x, int y) { return (y * 8) + x; }
void drawStaticUI() void drawStaticUI() {
{
FastLED.clear(); FastLED.clear();
#if SHOW_BORDER == 1 #if SHOW_BORDER == 1
CRGB borderColor = CRGB::Blue; CRGB borderColor = CRGB::Blue;
if (gameState == DEMO || gameState >= 3) if (gameState == DEMO || gameState >= 3) {
{
uint8_t glow = beat8(15); uint8_t glow = beat8(15);
borderColor = blend(CRGB::Blue, CRGB::White, glow / 4); borderColor = blend(CRGB::Blue, CRGB::White, glow / 4);
} }
for (int x = 0; x < 7; x++) for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = borderColor;
leds[getIdx(x, 1)] = borderColor; for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = borderColor;
for (int y = 1; y < 8; y++)
leds[getIdx(7, y)] = borderColor;
#endif #endif
} }
void renderBoard() void renderBoard() {
{
drawStaticUI(); drawStaticUI();
for (int c = 0; c < COLS; c++) for (int c = 0; c < COLS; c++) {
{ for (int r = 0; r < ROWS; r++) {
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] == 1)
leds[getIdx(c, 7 - r)] = CRGB::Yellow;
if (board[c][r] == 2)
leds[getIdx(c, 7 - r)] = CRGB::Red;
} }
} }
} }
int getFirstEmptyRow(int col) int getFirstEmptyRow(int col) {
{ for (int r = 0; r < ROWS; r++) if (board[col][r] == 0) return r;
for (int r = 0; r < ROWS; r++)
if (board[col][r] == 0)
return r;
return -1; return -1;
} }
bool isBoardFull() 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; return true;
} }
int getDynamicPly() int getDynamicPly() {
{ if (!progressive_difficulty && gameState != DEMO) return current_look_ahead;
if (!progressive_difficulty && gameState != DEMO)
return current_look_ahead;
int count = 0; int count = 0;
for (int c = 0; c < COLS; c++) for (int c = 0; c < COLS; c++) for (int r = 0; r < ROWS; r++) if (board[c][r] != 0) count++;
for (int r = 0; r < ROWS; r++)
if (board[c][r] != 0)
count++;
return constrain(current_look_ahead + (count / 7), 1, 10); return constrain(current_look_ahead + (count / 7), 1, 10);
} }
void updateThinkingVisuals(int8_t pColor, int8_t column) void updateThinkingVisuals(int8_t pColor, int8_t column) {
{
static uint32_t lastCycle = 0; static uint32_t lastCycle = 0;
if (millis() - lastCycle < 20) if (millis() - lastCycle < 20) return;
return;
lastCycle = millis(); lastCycle = millis();
if (aiFadeUp) if (aiFadeUp) { aiBrightness += 25; if (aiBrightness >= 230) aiFadeUp = false; }
{ else { aiBrightness -= 25; if (aiBrightness <= 25) aiFadeUp = true; }
aiBrightness += 25; for (int x = 0; x < COLS; x++) leds[getIdx(x, 0)] = CRGB::Black;
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;
// FIXED: Explicit color initialization to avoid nscale8 compiler error
CRGB aiColor = (pColor == 1) ? CRGB(CRGB::Yellow) : CRGB(CRGB::Red); CRGB aiColor = (pColor == 1) ? CRGB(CRGB::Yellow) : CRGB(CRGB::Red);
aiColor.nscale8(aiBrightness); aiColor.nscale8(aiBrightness);
leds[getIdx(column, 0)] = aiColor; leds[getIdx(column, 0)] = aiColor;
FastLED.show(); FastLED.show();
} }
void animateDrop(int col, int player) void animateDrop(int col, int player) {
{
int targetRow = getFirstEmptyRow(col); int targetRow = getFirstEmptyRow(col);
if (targetRow == -1) if (targetRow == -1) return;
return; for (int r = 5; r >= targetRow; r--) {
for (int r = 5; r >= targetRow; r--)
{
renderBoard(); renderBoard();
leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red; leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red;
FastLED.show(); FastLED.show();
@@ -176,17 +135,11 @@ void animateDrop(int col, int player)
board[col][targetRow] = player; board[col][targetRow] = player;
} }
void moveDiscToCol(int startCol, int targetCol, int player, int speed) void moveDiscToCol(int startCol, int targetCol, int player, int speed) {
{
int current = startCol; int current = startCol;
CRGB colr = (player == 1) ? CRGB::Yellow : CRGB::Red; CRGB colr = (player == 1) ? CRGB::Yellow : CRGB::Red;
while (current != targetCol && !abortAi) while (current != targetCol && !abortAi) {
{ if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { abortAi = true; break; }
if (gameState == DEMO && digitalRead(ENC_SW) == LOW)
{
abortAi = true;
break;
}
leds[getIdx(current, 0)] = CRGB::Black; leds[getIdx(current, 0)] = CRGB::Black;
current += (targetCol > current) ? 1 : -1; current += (targetCol > current) ? 1 : -1;
renderBoard(); renderBoard();
@@ -197,465 +150,204 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed)
activeCol = targetCol; activeCol = targetCol;
} }
int8_t scanBoard() int8_t scanBoard() {
{
memset(winMask, 0, sizeof(winMask)); memset(winMask, 0, sizeof(winMask));
auto check = [&](int c, int r, int dc, int dr) auto check = [&](int c, int r, int dc, int dr) {
{
int8_t p = board[c][r]; int8_t p = board[c][r];
if (p != 0 && board[c + dc][r + dr] == p && board[c + 2 * dc][r + 2 * dr] == p && board[c + 3 * dc][r + 3 * dr] == p) if (p != 0 && board[c+dc][r+dr] == p && board[c+2*dc][r+2*dr] == p && board[c+3*dc][r+3*dr] == p) {
{ for (int i = 0; i < 4; i++) winMask[getIdx(c+i*dc, 7-(r+i*dr))] = true;
for (int i = 0; i < 4; i++)
winMask[getIdx(c + i * dc, 7 - (r + i * dr))] = true;
return p; return p;
} }
return (int8_t)0; return (int8_t)0;
}; };
for (int r = 0; r < 6; r++) for (int r=0; r<6; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,0); if(res) return res; }
for (int c = 0; c < 4; c++) for (int r=0; r<3; r++) for (int c=0; c<7; c++) { int8_t res = check(c,r,0,1); if(res) return res; }
{ for (int r=0; r<3; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,1); if(res) return res; }
int8_t res = check(c, r, 1, 0); for (int r=3; r<6; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,-1); if(res) return res; }
if (res)
return res;
}
for (int r = 0; r < 3; r++)
for (int c = 0; c < 7; c++)
{
int8_t res = check(c, r, 0, 1);
if (res)
return res;
}
for (int r = 0; r < 3; r++)
for (int c = 0; c < 4; c++)
{
int8_t res = check(c, r, 1, 1);
if (res)
return res;
}
for (int r = 3; r < 6; r++)
for (int c = 0; c < 4; c++)
{
int8_t res = check(c, r, 1, -1);
if (res)
return res;
}
return 0; return 0;
} }
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol) 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 (gameState == DEMO && digitalRead(ENC_SW) == LOW) if (depth >= current_look_ahead - 1) updateThinkingVisuals(aiP, rootCol);
{ else yield();
abortAi = true; if (abortAi) return 0;
return 0;
}
if (depth >= current_look_ahead - 1)
updateThinkingVisuals(aiP, rootCol);
else
yield();
if (abortAi)
return 0;
int8_t win = scanBoard(); int8_t win = scanBoard();
if (win == aiP) if (win == aiP) return 1000 + depth;
return 1000 + depth; if (win == huP) return -1000 - depth;
if (win == huP) if (depth == 0 || isBoardFull()) return 0;
return -1000 - depth;
if (depth == 0 || isBoardFull())
return 0;
int colOrder[] = {3, 2, 4, 1, 5, 0, 6}; int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
int best = isMax ? -10000 : 10000; int best = isMax ? -10000 : 10000;
for (int c : colOrder) for (int c : colOrder) {
{ if (abortAi) return 0;
if (abortAi)
return 0;
int r = getFirstEmptyRow(c); int r = getFirstEmptyRow(c);
if (r != -1) if (r != -1) {
{
board[c][r] = isMax ? aiP : huP; 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 == current_look_ahead ? c : rootCol));
board[c][r] = 0; board[c][r] = 0;
if (isMax) if (isMax) { if (score > best) best = score; if (best > alpha) alpha = best; }
{ else { if (score < best) best = score; if (best < beta) beta = best; }
if (score > best) if (beta <= alpha) break;
best = score;
if (best > alpha)
alpha = best;
}
else
{
if (score < best)
best = score;
if (best < beta)
beta = best;
}
if (beta <= alpha)
break;
} }
} }
return best; return best;
} }
void performAiMove(int8_t aiP) void performAiMove(int8_t aiP) {
{
abortAi = false; abortAi = false;
int huP = (aiP == 1) ? 2 : 1; int huP = (aiP == 1) ? 2 : 1;
int bestScore = -30000; int bestScore = -30000; int bestCol = 3;
int bestCol = 3;
int originalPly = current_look_ahead; int originalPly = current_look_ahead;
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly(); current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
for (int c = 0; c < COLS; c++) for (int c=0; c<COLS; c++) {
{
if (gameState == DEMO && digitalRead(ENC_SW) == LOW)
{
abortAi = true;
goto finalizeMove;
}
int r = getFirstEmptyRow(c); int r = getFirstEmptyRow(c);
if (r != -1) if (r != -1) {
{ board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
board[c][r] = aiP; board[c][r] = huP; if (scanBoard() == huP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
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] = 0; board[c][r] = 0;
} }
} }
for (int c : {3, 2, 4, 1, 5, 0, 6}) for (int c : {3, 2, 4, 1, 5, 0, 6}) {
{ if (abortAi) goto finalizeMove;
if (abortAi)
goto finalizeMove;
int r = getFirstEmptyRow(c); int r = getFirstEmptyRow(c);
if (r != -1) if (r != -1) {
{
board[c][r] = aiP; board[c][r] = aiP;
int score = minimax(current_look_ahead, -30000, 30000, false, aiP, huP, c); int score = minimax(current_look_ahead, -30000, 30000, false, aiP, huP, c);
board[c][r] = 0; board[c][r] = 0;
if (score > bestScore) if (score > bestScore) { bestScore = score; bestCol = c; }
{
bestScore = score;
bestCol = c;
}
} }
} }
finalizeMove: finalizeMove:
current_look_ahead = originalPly; current_look_ahead = originalPly;
if (!abortAi) if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } }
{
moveDiscToCol(activeCol, bestCol, aiP, 80);
if (!abortAi)
{
delay(150);
animateDrop(bestCol, aiP);
}
}
} }
void handleRoot() 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'>"; 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 += "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>"; 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>";
server.send(200, "text/html", html); server.send(200, "text/html", html);
} }
void handleSave() void handleSave() {
{ if (server.hasArg("ply")) { current_look_ahead = server.arg("ply").toInt(); prefs.putUChar("ply", current_look_ahead); }
if (server.hasArg("ply")) 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); }
current_look_ahead = server.arg("ply").toInt(); blunder_enabled = server.hasArg("blunder"); prefs.putBool("blunder", blunder_enabled);
prefs.putUChar("ply", current_look_ahead); progressive_difficulty = server.hasArg("evolve"); prefs.putBool("evolve", progressive_difficulty);
} server.sendHeader("Location", "/"); server.send(303);
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);
server.sendHeader("Location", "/");
server.send(303);
} }
void showMenu() void showMenu() {
{
FastLED.clear(); FastLED.clear();
#if SHOW_BORDER == 1 #if SHOW_BORDER == 1
for (int x = 0; x < 7; x++) for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = CRGB::Blue;
leds[getIdx(x, 1)] = CRGB::Blue; for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = CRGB::Blue;
for (int y = 1; y < 8; y++)
leds[getIdx(7, y)] = CRGB::Blue;
#endif #endif
CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
if (menuMode < 2) 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; }
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(); FastLED.show();
} }
void setup() void setup() {
{
prefs.begin("c4-game", false); prefs.begin("c4-game", false);
current_look_ahead = prefs.getUChar("ply", 8); current_look_ahead = prefs.getUChar("ply", 8); current_brightness = prefs.getUChar("br", 25);
current_brightness = prefs.getUChar("br", 25); current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000; blunder_enabled = prefs.getBool("blunder", false);
current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000;
blunder_enabled = prefs.getBool("blunder", false);
progressive_difficulty = prefs.getBool("evolve", false); progressive_difficulty = prefs.getBool("evolve", false);
FastLED.addLeds<WS2812B, 4, GRB>(leds, NUM_LEDS); FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(current_brightness);
FastLED.setBrightness(current_brightness); pinMode(ENC_SW, INPUT_PULLUP); WiFi.softAP("Connect4-Config", "12345678");
pinMode(2, INPUT_PULLUP); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin();
WiFi.softAP("Connect4-Config", "12345678"); lastActivityTime = millis(); showMenu();
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.begin();
lastActivityTime = millis();
showMenu();
} }
void loop() void loop() {
{
server.handleClient(); server.handleClient();
long newPos = myEnc.read() / 2; long rawPos = myEnc.read();
bool currentButton = digitalRead(2); long newPos = rawPos / SENSITIVITY;
bool currentButton = digitalRead(ENC_SW);
bool pressed = false; bool pressed = false;
if (currentButton == LOW && lastButtonState == HIGH) if (currentButton == LOW && lastButtonState == HIGH) { if (millis() > globalInputCooldown) pressed = true; }
{
if (millis() > globalInputCooldown)
pressed = true;
}
lastButtonState = currentButton; lastButtonState = currentButton;
if ((newPos != oldEncPos || pressed) && (gameState >= 3 || gameState == DEMO)) // Interrupt check
{ if ((newPos != oldEncPos || pressed) && (gameState >= 3 || gameState == DEMO)) {
abortAi = true; abortAi = true; memset(board, 0, sizeof(board)); winnerPlayer = 0;
memset(board, 0, sizeof(board)); for (int i = 0; i < 10; i++) { fadeToBlackBy(leds, NUM_LEDS, 50); FastLED.show(); delay(15); }
winnerPlayer = 0; gameState = MENU; showMenu(); oldEncPos = newPos; lastActivityTime = millis();
for (int i = 0; i < 10; i++) globalInputCooldown = millis() + 600; return;
{
fadeToBlackBy(leds, NUM_LEDS, 50);
FastLED.show();
delay(15);
}
gameState = MENU;
showMenu();
oldEncPos = newPos;
lastActivityTime = millis();
globalInputCooldown = millis() + 600;
return;
} }
if (gameState != DEMO && (gameState < 3) && (millis() - lastActivityTime > current_idle_timeout_ms)) // Idle watchdog logic (Added specific exemption for FINISHED state)
{ if (gameState != DEMO && (gameState < 3)) {
gameState = DEMO; if (millis() - lastActivityTime > current_idle_timeout_ms) {
memset(board, 0, sizeof(board)); gameState = DEMO; memset(board, 0, sizeof(board)); currentPlayer = 1; return;
currentPlayer = 1; }
return;
} }
if (gameState == MENU) if (gameState == MENU) {
{ if (millis() > globalInputCooldown) {
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; }
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() + 400;
}
} }
} }
else if (gameState == PLAYING) else if (gameState == PLAYING) {
{ if (newPos != oldEncPos) { activeCol = (newPos % 7 + 7) % 7; oldEncPos = newPos; lastActivityTime = millis(); }
if (newPos != oldEncPos) renderBoard(); leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red; FastLED.show();
{ if (pressed) {
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); int row = getFirstEmptyRow(activeCol);
if (row != -1) if (row != -1) {
{
animateDrop(activeCol, currentPlayer); animateDrop(activeCol, currentPlayer);
winnerPlayer = scanBoard(); winnerPlayer = scanBoard();
if (winnerPlayer != 0) if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); }
{ else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); }
gameState = FINISHED_WIN; else { if (menuMode < 2) { gameState = AI_TURN; } else { currentPlayer = (currentPlayer == 1) ? 2 : 1; } }
demoResetTimer = millis();
}
else if (isBoardFull())
{
gameState = FINISHED_DRAW;
demoResetTimer = millis();
}
else
{
if (menuMode < 2)
{
gameState = AI_TURN;
}
else
{
currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
}
lastActivityTime = millis(); lastActivityTime = millis();
} }
} }
} }
else if (gameState == AI_TURN) else if (gameState == AI_TURN) {
{ int8_t aiP = (menuMode == 0) ? 2 : 1; performAiMove(aiP);
int8_t aiP = (menuMode == 0) ? 2 : 1; if (abortAi) { gameState = MENU; showMenu(); return; }
performAiMove(aiP); 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(); lastActivityTime = millis();
if (!abortAi)
{
winnerPlayer = scanBoard();
if (winnerPlayer != 0)
{
gameState = FINISHED_WIN;
demoResetTimer = millis();
}
else if (isBoardFull())
{
gameState = FINISHED_DRAW;
demoResetTimer = millis();
}
else
{
gameState = PLAYING;
currentPlayer = (aiP == 1) ? 2 : 1;
}
}
} }
else if (gameState == DEMO) else if (gameState == DEMO) {
{ renderBoard(); FastLED.show(); delay(300); performAiMove(currentPlayer);
renderBoard(); if (abortAi) { gameState = MENU; showMenu(); return; }
FastLED.show(); winnerPlayer = scanBoard();
delay(300); if (winnerPlayer != 0) { gameState = FINISHED_WIN; demoResetTimer = millis(); lastActivityTime = millis(); }
performAiMove(currentPlayer); else if (isBoardFull()) { gameState = FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); }
if (!abortAi) 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 else { // FINISHED state (WIN/DRAW)
{ static uint32_t lastFlash = 0; static bool toggle = true;
static uint32_t lastFlash = 0; if (millis() - lastFlash > 300) {
static bool toggle = true; lastFlash = millis(); toggle = !toggle; renderBoard();
if (millis() - lastFlash > 300) for (int i = 0; i < NUM_LEDS; i++) {
{
lastFlash = millis();
toggle = !toggle;
renderBoard();
for (int i = 0; i < NUM_LEDS; i++)
{
#if SHOW_BORDER == 1 #if SHOW_BORDER == 1
if (leds[i] == CRGB::Blue) if (leds[i] == CRGB::Blue) continue;
continue;
#endif #endif
if (gameState == FINISHED_WIN) if (gameState == FINISHED_WIN) {
{ if (winMask[i]) leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black;
if (winMask[i]) else { CRGB c = leds[i]; c.nscale8(60); leds[i] = c; }
leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; } else if (gameState == FINISHED_DRAW) { if (!toggle) leds[i] = 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(); FastLED.show();
} }
if (millis() - demoResetTimer > 15000)
{ // RECENT FIX: Prolonged display time for win (30 seconds)
if (millis() - demoResetTimer > 30000) {
memset(board, 0, sizeof(board)); memset(board, 0, sizeof(board));
gameState = DEMO; gameState = DEMO;
demoResetTimer = 0; demoResetTimer = 0;
lastActivityTime = millis(); // Refresh watchdog
} }
} }
} }