[refactor] Progressive difficulty, blunder logic and documentation.
This commit is contained in:
@@ -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.
|
||||
|
||||
## 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]`
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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**.
|
||||
It explores a "tree" of possibilities: _"If I play here, and the player plays there, then I can play here..."_
|
||||
### 📐 Physical Dimensions
|
||||
|
||||
### 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**.
|
||||
This allows the AI to "prune" (ignore) branches of the game tree that are mathematically
|
||||
guaranteed to be worse than moves it has already found, significantly speeding up the calculation.
|
||||
|
||||
### 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.
|
||||
- **Top Row (0):** Interaction and AI decision visualization.
|
||||
- **Game Board:** Standard $7 \times 6$ grid.
|
||||
- **UI Borders:** Fixed blue frame for visibility.
|
||||
|
||||
---
|
||||
|
||||
## Technical Specifications
|
||||
## 🧠 Advanced AI Features
|
||||
|
||||
### Hardware Pins (Lolin C3 Mini)
|
||||
### 1. Progressive Difficulty (Evolution Mode)
|
||||
|
||||
| Component | Pin | Function |
|
||||
| :---------- | :-- | :--------------------------------------- |
|
||||
| **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) |
|
||||
The AI search depth (Ply) increases as the board fills. This ensures the AI is fast in the opening and lethal in the endgame.
|
||||
|
||||
### 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).
|
||||
- **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."
|
||||
- **Glowing Frame**: During Demo mode, the blue borders pulse with a white "glow" effect using a `beat8` sine wave to indicate autonomous play.
|
||||
To avoid endless stalemate draws between high-level AIs, a "Blunder" logic is used.
|
||||
|
||||
- **Demo Mode:** Always active; 20% chance to make a suboptimal move.
|
||||
- **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.
|
||||
- **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.
|
||||
### 🔄 State Machine
|
||||
|
||||
## 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.
|
||||
- `DEMO_RESET_PAUSE`: Delay (ms) between games in Demo Mode.
|
||||
- `DEBOUNCE_DELAY`: Sensitivity of the encoder button.
|
||||
- `BRIGHTNESS`: Global brightness of the NeoPixels.
|
||||
### 🎨 Rendering & Mapping
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 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
@@ -9,6 +9,7 @@
|
||||
const int COLS = 7;
|
||||
const int ROWS = 6;
|
||||
|
||||
// --- Configuration & Globals ---
|
||||
CRGB leds[NUM_LEDS];
|
||||
Encoder myEnc(ENC_A, ENC_B);
|
||||
WebServer server(80);
|
||||
@@ -33,41 +34,36 @@ long oldEncPos = -999;
|
||||
uint32_t lastActivityTime = 0;
|
||||
uint32_t demoResetTimer = 0;
|
||||
bool isDemoOver = false;
|
||||
uint8_t demoPly = 4;
|
||||
|
||||
// Web-Configurable Parameters (Stored in Flash)
|
||||
// Configurable Parameters
|
||||
uint8_t current_look_ahead;
|
||||
uint8_t current_brightness;
|
||||
uint32_t current_idle_timeout_ms;
|
||||
bool blunder_enabled = false;
|
||||
bool progressive_difficulty = false;
|
||||
|
||||
// Thinking Animation Helpers
|
||||
uint8_t aiBrightness = 0;
|
||||
bool aiFadeUp = true;
|
||||
|
||||
// --- Helper Functions ---
|
||||
int getIdx(int x, int y) { return (y * 8) + x; }
|
||||
// --- Function Prototypes ---
|
||||
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)
|
||||
{
|
||||
static uint32_t lastCycle = 0;
|
||||
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();
|
||||
}
|
||||
// --- Utility & Rendering ---
|
||||
|
||||
int getIdx(int x, int y) { return (y * 8) + x; }
|
||||
|
||||
void drawStaticUI()
|
||||
{
|
||||
@@ -109,6 +105,85 @@ int getFirstEmptyRow(int col)
|
||||
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()
|
||||
{
|
||||
for (int c = 0; c < COLS; c++)
|
||||
@@ -150,21 +225,18 @@ bool scanBoard(int8_t p)
|
||||
return found;
|
||||
}
|
||||
|
||||
// --- AI Engine ---
|
||||
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP)
|
||||
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol)
|
||||
{
|
||||
if (depth >= current_look_ahead - 1)
|
||||
updateThinkingLED(aiP);
|
||||
updateThinkingVisuals(aiP, rootCol);
|
||||
else
|
||||
yield();
|
||||
|
||||
if (scanBoard(aiP))
|
||||
return 1000 + depth;
|
||||
if (scanBoard(huP))
|
||||
return -1000 - depth;
|
||||
if (depth == 0 || isBoardFull())
|
||||
return 0;
|
||||
|
||||
int order[] = {3, 2, 4, 1, 5, 0, 6};
|
||||
int best = isMax ? -2000 : 2000;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
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)
|
||||
{
|
||||
int8_t huP = (aiP == 1) ? 2 : 1;
|
||||
aiBrightness = 0;
|
||||
aiFadeUp = true;
|
||||
int huP = (aiP == 1) ? 2 : 1;
|
||||
int bestScore = -30000;
|
||||
int bestCol = 3;
|
||||
int originalPly = current_look_ahead;
|
||||
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
|
||||
|
||||
for (int c = 0; c < COLS; c++)
|
||||
{
|
||||
int r = getFirstEmptyRow(c);
|
||||
@@ -207,28 +282,28 @@ void performAiMove(int8_t aiP)
|
||||
board[c][r] = aiP;
|
||||
if (scanBoard(aiP))
|
||||
{
|
||||
leds[getIdx(7, 0)] = CRGB::Black;
|
||||
return;
|
||||
board[c][r] = 0;
|
||||
bestCol = c;
|
||||
goto finalize;
|
||||
}
|
||||
board[c][r] = huP;
|
||||
if (current_look_ahead >= 2 && scanBoard(huP))
|
||||
{
|
||||
board[c][r] = aiP;
|
||||
leds[getIdx(7, 0)] = CRGB::Black;
|
||||
return;
|
||||
board[c][r] = 0;
|
||||
bestCol = c;
|
||||
goto finalize;
|
||||
}
|
||||
board[c][r] = 0;
|
||||
}
|
||||
}
|
||||
int bestScore = -30000;
|
||||
int bestCol = 3;
|
||||
|
||||
for (int c : {3, 2, 4, 1, 5, 0, 6})
|
||||
{
|
||||
int r = getFirstEmptyRow(c);
|
||||
if (r != -1)
|
||||
{
|
||||
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;
|
||||
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()
|
||||
{
|
||||
isDemoOver = false;
|
||||
@@ -279,42 +406,6 @@ void showMenu()
|
||||
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()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
@@ -322,29 +413,20 @@ void setup()
|
||||
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.disconnect(true); // Clear old settings
|
||||
WiFi.mode(WIFI_AP); // Force Access Point mode
|
||||
delay(100); // Give the radio a moment to reset
|
||||
|
||||
// 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!");
|
||||
}
|
||||
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_AP);
|
||||
delay(100);
|
||||
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
|
||||
server.on("/", handleRoot);
|
||||
server.on("/save", HTTP_POST, handleSave);
|
||||
server.begin();
|
||||
|
||||
lastActivityTime = millis();
|
||||
showMenu();
|
||||
}
|
||||
@@ -355,7 +437,6 @@ void loop()
|
||||
long newPos = myEnc.read() / SENSITIVITY;
|
||||
bool pressed = (digitalRead(ENC_SW) == LOW);
|
||||
|
||||
// Escape Demo / Interrupt
|
||||
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
|
||||
{
|
||||
if (gameState == DEMO || isDemoOver)
|
||||
@@ -366,7 +447,7 @@ void loop()
|
||||
FastLED.show();
|
||||
delay(30);
|
||||
}
|
||||
delay(2000);
|
||||
delay(1000);
|
||||
gameState = MENU;
|
||||
memset(board, 0, sizeof(board));
|
||||
showMenu();
|
||||
@@ -405,6 +486,7 @@ void loop()
|
||||
gameState = DEMO;
|
||||
memset(board, 0, sizeof(board));
|
||||
currentPlayer = 1;
|
||||
demoPly = random(3, 7);
|
||||
}
|
||||
}
|
||||
else if (gameState == PLAYING)
|
||||
@@ -422,9 +504,7 @@ void loop()
|
||||
int row = getFirstEmptyRow(activeCol);
|
||||
if (row != -1)
|
||||
{
|
||||
board[activeCol][row] = currentPlayer;
|
||||
renderBoard();
|
||||
FastLED.show();
|
||||
animateDrop(activeCol, currentPlayer);
|
||||
if (scanBoard(currentPlayer))
|
||||
gameState = FINISHED_WIN;
|
||||
else if (isBoardFull())
|
||||
@@ -435,8 +515,6 @@ void loop()
|
||||
{
|
||||
int8_t aiP = (menuMode == 0) ? 2 : 1;
|
||||
performAiMove(aiP);
|
||||
renderBoard();
|
||||
FastLED.show();
|
||||
if (scanBoard(aiP))
|
||||
{
|
||||
currentPlayer = aiP;
|
||||
@@ -456,7 +534,6 @@ void loop()
|
||||
}
|
||||
else if (gameState == DEMO)
|
||||
{
|
||||
// No idle timeout check here to prevent premature restarts
|
||||
renderBoard();
|
||||
FastLED.show();
|
||||
delay(600);
|
||||
@@ -480,7 +557,6 @@ void loop()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Monitor for Idle in Win screen to return to Demo
|
||||
if (!isDemoOver && (millis() - lastActivityTime > current_idle_timeout_ms))
|
||||
{
|
||||
memset(board, 0, sizeof(board));
|
||||
@@ -488,7 +564,6 @@ void loop()
|
||||
currentPlayer = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t lastFlash = 0;
|
||||
static bool toggle = true;
|
||||
if (millis() - lastFlash > 300)
|
||||
@@ -515,12 +590,12 @@ void loop()
|
||||
}
|
||||
FastLED.show();
|
||||
}
|
||||
// Restart Demo loop if it was a demo game
|
||||
if (isDemoOver && (millis() - demoResetTimer > 30000))
|
||||
if (isDemoOver && (millis() - demoResetTimer > 15000))
|
||||
{
|
||||
memset(board, 0, sizeof(board));
|
||||
gameState = DEMO;
|
||||
isDemoOver = false;
|
||||
demoPly = random(3, 7);
|
||||
}
|
||||
if (pressed)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user