[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.
|
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
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user