[refactor] Progressive difficulty, blunder logic and documentation.

This commit is contained in:
2026-03-06 22:14:25 +01:00
parent 8e338b5e1c
commit 73981c95c5
2 changed files with 242 additions and 180 deletions
+58 -71
View File
@@ -1,95 +1,82 @@
# Connect Four: ESP32-C3 LED Edition # 🕹️ Connect 4 AI: Master Edition
A hardware-based Connect Four game featuring an 8x8 NeoPixel matrix, a strategic Minimax AI, and a dynamic "Attract Mode" for public display. A high-performance Connect 4 implementation for ESP32-C3 and 8x8 WS2812B matrices. Features dynamic difficulty scaling, "humanized" AI movement, and a mobile-friendly web administration portal.
## How the Program Works
The program is built as a **Finite State Machine (FSM)**.
It manages the game flow by transitioning between distinct states based on user input, game outcomes, or inactivity timers.
### 1. Game States
- **Menu**: Displays stylized Roman numerals (**I** or **II**) to select between Single Player or Two Player modes.
- **Game Play**: The main loop handles the real-time gravity of falling discs, encoder tracking for column selection, and the hand-off between the human and the AI.
- **Game Over**: Triggered when a win or draw is detected. It "locks" the board, dims the background discs to 15% intensity, and flashes the winning line.
- **Demo Mode**: Triggered after 60 seconds of inactivity. The AI plays against itself to act as a visual "attract mode."
### 2. Win Detection Logic
To ensure 100% accuracy, the program performs a synchronous, multi-directional scan of the 7x6 grid after every single move.
It checks for four matching non-zero values in the following patterns:
- **Horizontal**: `[column] [row]` to `[column + 3] [row]`
- **Vertical**: `[column] [row]` to `[column] [row + 3]`
- **Diagonal Up**: `[column] [row]` to `[column + 3] [row + 3]`
- **Diagonal Down**: `[column] [row]` to `[column + 3] [row - 3]`
--- ---
## The AI: Strategic Minimax ## 🛠 Hardware Configuration
The computer opponent uses the **Minimax Algorithm**, a classic artificial intelligence method for zero-sum games. ### 🔌 Pin Mapping (Lolin C3 Mini)
### 1. Look-Ahead (Depth Search) | Component | ESP32-C3 Pin | Function |
| :------------------- | :----------- | :--------------- |
| **NeoPixel Matrix** | `GPIO 4` | Data Input (DIN) |
| **Rotary Encoder A** | `GPIO 0` | Directional CLK |
| **Rotary Encoder B** | `GPIO 1` | Directional DT |
| **Encoder Button** | `GPIO 2` | Selection (SW) |
The AI does not just look at the current board; it simulates the game **6 to 8 moves into the future**. ### 📐 Physical Dimensions
It explores a "tree" of possibilities: _"If I play here, and the player plays there, then I can play here..."_
### 2. Alpha-Beta Pruning Designed for standard 8x8 matrix modules (approx. 65mm x 67mm).
Because searching millions of possibilities would be too slow for a microcontroller, we use **Alpha-Beta Pruning**. - **Top Row (0):** Interaction and AI decision visualization.
This allows the AI to "prune" (ignore) branches of the game tree that are mathematically - **Game Board:** Standard $7 \times 6$ grid.
guaranteed to be worse than moves it has already found, significantly speeding up the calculation. - **UI Borders:** Fixed blue frame for visibility.
### 3. Immediate Threat Reaction
To prevent the AI from being "distracted" by deep strategies while missing a simple win or loss,
we implemented a high-priority **Reaction Scanner**:
- **Kill Move**: If the AI can win in exactly one move, it takes it immediately.
- **Block Move**: If the player is one move away from winning (3-in-a-row), the AI identifies the threat and blocks it regardless of the Minimax score.
### 4. Controlled Randomness (Demo Mode)
To keep the Demo Mode interesting for spectators, the AI has a 25% chance to ignore the "perfect" move and pick a random column.
This ensures that every demo game is unique and not a repetitive loop of the same strategy.
--- ---
## Technical Specifications ## 🧠 Advanced AI Features
### Hardware Pins (Lolin C3 Mini) ### 1. Progressive Difficulty (Evolution Mode)
| Component | Pin | Function | The AI search depth (Ply) increases as the board fills. This ensures the AI is fast in the opening and lethal in the endgame.
| :---------- | :-- | :--------------------------------------- |
| **LED_PIN** | 4 | WS2812B NeoPixel Data |
| **ENC_A** | 0 | Rotary Encoder Phase A |
| **ENC_B** | 1 | Rotary Encoder Phase B |
| **ENC_SW** | 2 | Switch (Includes 50ms Software Debounce) |
### NeoPixel Grid Layout - **Formula:** $DynamicPly = BasePly + \lfloor \frac{DiscsOnBoard}{7} \rfloor$
- **Benefit:** High-level tactical precision exactly when the game becomes critical.
The 8x8 matrix is mapped as follows: ### 2. Strategic Blunder Injection
- **Play Area**: 7 columns (0-6) by 6 rows (0-5). To avoid endless stalemate draws between high-level AIs, a "Blunder" logic is used.
- **Boundaries**: Row 1 and Column 7 are lit in **Blue** to mark the board limits.
- **Indicators**: The top-right pixel (7,0) pulses in the computer's color while it is "thinking." - **Demo Mode:** Always active; 20% chance to make a suboptimal move.
- **Glowing Frame**: During Demo mode, the blue borders pulse with a white "glow" effect using a `beat8` sine wave to indicate autonomous play. - **Player Mode:** Toggleable via Web Portal to make the AI more "human."
### 3. Alpha-Beta Pruning & Column Ordering
The engine evaluates the center column first. This triggers pruning earlier in the search tree, skipping millions of unnecessary calculations and keeping the ESP32-C3 responsive.
--- ---
## Controls & Interaction ## 📖 Code Architecture Details
- **Rotate Encoder**: Move the cursor (top row) to select a column. ### 🔄 State Machine
- **Press Encoder Button**: Drop a disc.
- **Full Column Warning**: If you attempt to play in a full column, the selection disc will blink rapidly, and the move will be ignored.
- **Reset**: After a game ends, press the button once to return to the Menu.
## Build Flags (platformio.ini) The software cycles through states:
Tweak the game performance without changing the source code: - **MENU:** Select mode using the rotary encoder.
- **PLAYING:** Manages player turns and the gravity-acceleration drop animation.
- **DEMO:** Auto-starts after inactivity. Randomizes Ply (3-6) and enforces blunders to ensure definitive game results.
- `IDLE_TIMEOUT`: Time (ms) before Demo Mode starts. ### 🎨 Rendering & Mapping
- `DEMO_RESET_PAUSE`: Delay (ms) between games in Demo Mode.
- `DEBOUNCE_DELAY`: Sensitivity of the encoder button. The `getIdx(x, y)` function maps the 2D game board to the 1D NeoPixel array. The `updateThinkingVisuals()` function provides real-time feedback of the AI's internal search process by moving a pulsing disc across the top row.
- `BRIGHTNESS`: Global brightness of the NeoPixels.
---
## 🌐 Web Admin Portal
Connect to the **"Connect4-Config"** Access Point to adjust:
- **Base Ply:** Minimum search depth.
- **Brightness:** Global LED intensity.
- **Idle Timeout:** Inactivity period before Demo Mode.
- **Toggles:** Enable/Disable Blunders and Evolution Mode.
---
## 🛠 Setup & Installation
1. Install **PlatformIO**.
2. Add dependencies: `FastLED`, `Encoder`.
3. Set your WiFi Password in `platformio.ini`: `-D WIFI_PASSWORD=\"your_pass\"`.
4. Upload to ESP32-C3.
+184 -109
View File
@@ -9,6 +9,7 @@
const int COLS = 7; const int COLS = 7;
const int ROWS = 6; const int ROWS = 6;
// --- Configuration & Globals ---
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);
@@ -33,41 +34,36 @@ long oldEncPos = -999;
uint32_t lastActivityTime = 0; uint32_t lastActivityTime = 0;
uint32_t demoResetTimer = 0; uint32_t demoResetTimer = 0;
bool isDemoOver = false; bool isDemoOver = false;
uint8_t demoPly = 4;
// Web-Configurable Parameters (Stored in Flash) // Configurable Parameters
uint8_t current_look_ahead; uint8_t current_look_ahead;
uint8_t current_brightness; uint8_t current_brightness;
uint32_t current_idle_timeout_ms; uint32_t current_idle_timeout_ms;
bool blunder_enabled = false;
bool progressive_difficulty = false;
// Thinking Animation Helpers
uint8_t aiBrightness = 0; uint8_t aiBrightness = 0;
bool aiFadeUp = true; bool aiFadeUp = true;
// --- Helper Functions --- // --- Function Prototypes ---
int getIdx(int x, int y) { return (y * 8) + x; } int getIdx(int x, int y);
void drawStaticUI();
void renderBoard();
int getFirstEmptyRow(int col);
bool isBoardFull();
bool scanBoard(int8_t p);
void updateThinkingVisuals(int8_t p, int8_t col);
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 updateThinkingLED(int8_t p) // --- Utility & Rendering ---
{
static uint32_t lastCycle = 0; int getIdx(int x, int y) { return (y * 8) + x; }
if (millis() - lastCycle < 20)
return;
lastCycle = millis();
if (aiFadeUp)
{
aiBrightness += 15;
if (aiBrightness >= 240)
aiFadeUp = false;
}
else
{
aiBrightness -= 15;
if (aiBrightness <= 15)
aiFadeUp = true;
}
CRGB compColor = (p == 1) ? CRGB::Yellow : CRGB::Red;
leds[getIdx(7, 0)] = compColor.nscale8(aiBrightness);
FastLED.show();
}
void drawStaticUI() void drawStaticUI()
{ {
@@ -109,6 +105,85 @@ int getFirstEmptyRow(int col)
return -1; return -1;
} }
int getDynamicPly()
{
if (!progressive_difficulty && gameState != DEMO)
return current_look_ahead;
int count = 0;
for (int c = 0; c < COLS; c++)
for (int r = 0; r < ROWS; r++)
if (board[c][r] != 0)
count++;
int evolution = count / 7;
return constrain(current_look_ahead + evolution, 1, 10);
}
// --- Visuals & Animations ---
void updateThinkingVisuals(int8_t p, int8_t col)
{
static uint32_t lastCycle = 0;
if (millis() - lastCycle < 25)
return;
lastCycle = millis();
if (aiFadeUp)
{
aiBrightness += 15;
if (aiBrightness >= 240)
aiFadeUp = false;
}
else
{
aiBrightness -= 15;
if (aiBrightness <= 15)
aiFadeUp = true;
}
for (int x = 0; x < COLS; x++)
leds[getIdx(x, 0)] = CRGB::Black;
CRGB aiColor = (p == 1) ? CRGB::Yellow : CRGB::Red;
leds[getIdx(col, 0)] = aiColor.nscale8(aiBrightness);
FastLED.show();
activeCol = col;
}
void animateDrop(int col, int player)
{
int targetRow = getFirstEmptyRow(col);
if (targetRow == -1)
return;
int currentDelay = 80;
for (int r = 5; r >= targetRow; r--)
{
renderBoard();
leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red;
FastLED.show();
delay(currentDelay);
if (currentDelay > 20)
currentDelay -= 15;
}
board[col][targetRow] = player;
renderBoard();
FastLED.show();
}
void moveDiscToCol(int startCol, int targetCol, int player, int speed)
{
int current = startCol;
CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red;
while (current != targetCol)
{
leds[getIdx(current, 0)] = CRGB::Black;
current += (targetCol > current) ? 1 : -1;
renderBoard();
leds[getIdx(current, 0)] = pColor;
FastLED.show();
delay(speed);
}
activeCol = targetCol;
}
// --- AI Engine ---
bool isBoardFull() bool isBoardFull()
{ {
for (int c = 0; c < COLS; c++) for (int c = 0; c < COLS; c++)
@@ -150,21 +225,18 @@ bool scanBoard(int8_t p)
return found; return found;
} }
// --- AI Engine --- 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)
{ {
if (depth >= current_look_ahead - 1) if (depth >= current_look_ahead - 1)
updateThinkingLED(aiP); updateThinkingVisuals(aiP, rootCol);
else else
yield(); yield();
if (scanBoard(aiP)) if (scanBoard(aiP))
return 1000 + depth; return 1000 + depth;
if (scanBoard(huP)) if (scanBoard(huP))
return -1000 - depth; return -1000 - depth;
if (depth == 0 || isBoardFull()) if (depth == 0 || isBoardFull())
return 0; return 0;
int order[] = {3, 2, 4, 1, 5, 0, 6}; int order[] = {3, 2, 4, 1, 5, 0, 6};
int best = isMax ? -2000 : 2000; int best = isMax ? -2000 : 2000;
for (int c : order) for (int c : order)
@@ -173,7 +245,7 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP)
if (r != -1) if (r != -1)
{ {
board[c][r] = isMax ? aiP : huP; board[c][r] = isMax ? aiP : huP;
int val = minimax(depth - 1, alpha, beta, !isMax, aiP, huP); int val = 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)
{ {
@@ -196,9 +268,12 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP)
void performAiMove(int8_t aiP) void performAiMove(int8_t aiP)
{ {
int8_t huP = (aiP == 1) ? 2 : 1; int huP = (aiP == 1) ? 2 : 1;
aiBrightness = 0; int bestScore = -30000;
aiFadeUp = true; int bestCol = 3;
int originalPly = current_look_ahead;
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
for (int c = 0; c < COLS; c++) for (int c = 0; c < COLS; c++)
{ {
int r = getFirstEmptyRow(c); int r = getFirstEmptyRow(c);
@@ -207,28 +282,28 @@ void performAiMove(int8_t aiP)
board[c][r] = aiP; board[c][r] = aiP;
if (scanBoard(aiP)) if (scanBoard(aiP))
{ {
leds[getIdx(7, 0)] = CRGB::Black; board[c][r] = 0;
return; bestCol = c;
goto finalize;
} }
board[c][r] = huP; board[c][r] = huP;
if (current_look_ahead >= 2 && scanBoard(huP)) if (current_look_ahead >= 2 && scanBoard(huP))
{ {
board[c][r] = aiP; board[c][r] = 0;
leds[getIdx(7, 0)] = CRGB::Black; bestCol = c;
return; goto finalize;
} }
board[c][r] = 0; board[c][r] = 0;
} }
} }
int bestScore = -30000;
int bestCol = 3;
for (int c : {3, 2, 4, 1, 5, 0, 6}) for (int c : {3, 2, 4, 1, 5, 0, 6})
{ {
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); 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)
{ {
@@ -237,11 +312,63 @@ void performAiMove(int8_t aiP)
} }
} }
} }
board[bestCol][getFirstEmptyRow(bestCol)] = aiP;
leds[getIdx(7, 0)] = CRGB::Black; if ((gameState == DEMO || blunder_enabled) && random(100) < 20)
{
int rCol = random(0, 7);
if (getFirstEmptyRow(rCol) != -1)
bestCol = rCol;
}
finalize:
current_look_ahead = originalPly;
moveDiscToCol(activeCol, bestCol, aiP, 100);
delay(450);
animateDrop(bestCol, aiP);
}
// --- Web Portal & Setup ---
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>";
html += "<h1>Connect 4 Admin</h1><div class='card'><form action='/save' method='POST'>";
html += "Base AI Ply (1-10):<input type='number' name='ply' value='" + String(current_look_ahead) + "'>";
html += "Brightness (5-255):<input type='number' name='br' value='" + String(current_brightness) + "'>";
html += "Idle Timeout (Sec):<input type='number' name='idle' value='" + String(current_idle_timeout_ms / 1000) + "'>";
html += "Enable Blunders: <input type='checkbox' name='blunder' " + String(blunder_enabled ? "checked" : "") + "><br>";
html += "Evolution Mode: <input type='checkbox' name='evolve' " + String(progressive_difficulty ? "checked" : "") + "><br><br>";
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;font-weight:bold;'></form></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"))
{
uint32_t s = server.arg("idle").toInt();
current_idle_timeout_ms = s * 1000;
prefs.putUInt("idle", s);
}
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);
} }
// --- Menu UI with Restored Serifs ---
void showMenu() void showMenu()
{ {
isDemoOver = false; isDemoOver = false;
@@ -279,42 +406,6 @@ void showMenu()
FastLED.show(); FastLED.show();
} }
// --- Web Portal ---
void handleRoot()
{
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<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>";
html += "<h1>Connect 4 Admin</h1><div class='card'><form action='/save' method='POST'>";
html += "AI Ply (1-10):<input type='number' name='ply' value='" + String(current_look_ahead) + "'>";
html += "Brightness (5-255):<input type='number' name='br' value='" + String(current_brightness) + "'>";
html += "Idle Timeout (Sec):<input type='number' name='idle' value='" + String(current_idle_timeout_ms / 1000) + "'>";
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;font-weight:bold;'></form></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"))
{
uint32_t s = server.arg("idle").toInt();
current_idle_timeout_ms = s * 1000;
prefs.putUInt("idle", s);
}
server.sendHeader("Location", "/");
server.send(303);
}
void setup() void setup()
{ {
Serial.begin(115200); Serial.begin(115200);
@@ -322,29 +413,20 @@ void setup()
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; 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.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(current_brightness); FastLED.setBrightness(current_brightness);
pinMode(ENC_SW, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP);
WiFi.disconnect(true); // Clear old settings WiFi.disconnect(true);
WiFi.mode(WIFI_AP); // Force Access Point mode WiFi.mode(WIFI_AP);
delay(100); // Give the radio a moment to reset delay(100);
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
// SSID, Password (MUST be 8+ chars), Channel, Hidden (0=No), Max Clients
if (WiFi.softAP("Connect4-Config", WIFI_PASSWORD, 1, 0, 4))
{
Serial.println("WPA2 AP Started Successfully");
}
else
{
Serial.println("AP Failed - Check if WIFI_PASSWORD is at least 8 characters!");
}
server.on("/", handleRoot); server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave); server.on("/save", HTTP_POST, handleSave);
server.begin(); server.begin();
lastActivityTime = millis(); lastActivityTime = millis();
showMenu(); showMenu();
} }
@@ -355,7 +437,6 @@ void loop()
long newPos = myEnc.read() / SENSITIVITY; long newPos = myEnc.read() / SENSITIVITY;
bool pressed = (digitalRead(ENC_SW) == LOW); bool pressed = (digitalRead(ENC_SW) == LOW);
// Escape Demo / Interrupt
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500))) if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
{ {
if (gameState == DEMO || isDemoOver) if (gameState == DEMO || isDemoOver)
@@ -366,7 +447,7 @@ void loop()
FastLED.show(); FastLED.show();
delay(30); delay(30);
} }
delay(2000); delay(1000);
gameState = MENU; gameState = MENU;
memset(board, 0, sizeof(board)); memset(board, 0, sizeof(board));
showMenu(); showMenu();
@@ -405,6 +486,7 @@ void loop()
gameState = DEMO; gameState = DEMO;
memset(board, 0, sizeof(board)); memset(board, 0, sizeof(board));
currentPlayer = 1; currentPlayer = 1;
demoPly = random(3, 7);
} }
} }
else if (gameState == PLAYING) else if (gameState == PLAYING)
@@ -422,9 +504,7 @@ void loop()
int row = getFirstEmptyRow(activeCol); int row = getFirstEmptyRow(activeCol);
if (row != -1) if (row != -1)
{ {
board[activeCol][row] = currentPlayer; animateDrop(activeCol, currentPlayer);
renderBoard();
FastLED.show();
if (scanBoard(currentPlayer)) if (scanBoard(currentPlayer))
gameState = FINISHED_WIN; gameState = FINISHED_WIN;
else if (isBoardFull()) else if (isBoardFull())
@@ -435,8 +515,6 @@ void loop()
{ {
int8_t aiP = (menuMode == 0) ? 2 : 1; int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove(aiP); performAiMove(aiP);
renderBoard();
FastLED.show();
if (scanBoard(aiP)) if (scanBoard(aiP))
{ {
currentPlayer = aiP; currentPlayer = aiP;
@@ -456,7 +534,6 @@ void loop()
} }
else if (gameState == DEMO) else if (gameState == DEMO)
{ {
// No idle timeout check here to prevent premature restarts
renderBoard(); renderBoard();
FastLED.show(); FastLED.show();
delay(600); delay(600);
@@ -480,7 +557,6 @@ void loop()
} }
else else
{ {
// Monitor for Idle in Win screen to return to Demo
if (!isDemoOver && (millis() - lastActivityTime > current_idle_timeout_ms)) if (!isDemoOver && (millis() - lastActivityTime > current_idle_timeout_ms))
{ {
memset(board, 0, sizeof(board)); memset(board, 0, sizeof(board));
@@ -488,7 +564,6 @@ void loop()
currentPlayer = 1; currentPlayer = 1;
return; return;
} }
static uint32_t lastFlash = 0; static uint32_t lastFlash = 0;
static bool toggle = true; static bool toggle = true;
if (millis() - lastFlash > 300) if (millis() - lastFlash > 300)
@@ -515,12 +590,12 @@ void loop()
} }
FastLED.show(); FastLED.show();
} }
// Restart Demo loop if it was a demo game if (isDemoOver && (millis() - demoResetTimer > 15000))
if (isDemoOver && (millis() - demoResetTimer > 30000))
{ {
memset(board, 0, sizeof(board)); memset(board, 0, sizeof(board));
gameState = DEMO; gameState = DEMO;
isDemoOver = false; isDemoOver = false;
demoPly = random(3, 7);
} }
if (pressed) if (pressed)
{ {