[fix] Demo draw state no longer flashing and adding switch to demo if player(s) abandon game.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 🕹️ Connect 4 AI: Master Edition
|
# 🕹️ Connect 4 AI: Master Edition (v2.0)
|
||||||
|
|
||||||
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.
|
A high-performance, feature-rich Connect 4 implementation for the ESP32-C3. This version features a "living" AI that evolves as you play, human-like movement animations, and a robust win-detection engine.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,68 +15,72 @@ A high-performance Connect 4 implementation for ESP32-C3 and 8x8 WS2812B matrice
|
|||||||
| **Rotary Encoder B** | `GPIO 1` | Directional DT |
|
| **Rotary Encoder B** | `GPIO 1` | Directional DT |
|
||||||
| **Encoder Button** | `GPIO 2` | Selection (SW) |
|
| **Encoder Button** | `GPIO 2` | Selection (SW) |
|
||||||
|
|
||||||
### 📐 Physical Dimensions
|
### 📐 Physical Layout
|
||||||
|
|
||||||
Designed for standard 8x8 matrix modules (approx. 65mm x 67mm).
|
The project is optimized for an 8x8 NeoPixel Matrix (65mm x 67mm).
|
||||||
|
|
||||||
- **Top Row (0):** Interaction and AI decision visualization.
|
- **Row 0:** Interaction & AI Decision Visualization.
|
||||||
- **Game Board:** Standard $7 \times 6$ grid.
|
- **Row 1:** Static Blue UI border.
|
||||||
- **UI Borders:** Fixed blue frame for visibility.
|
- **Rows 2-7:** Active $7 \times 6$ game board.
|
||||||
|
- **Status Column:** Far right column (Index 7) manages UI framing and "Glow" effects.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧠 Advanced AI Features
|
## 🧠 Advanced AI & Logic Features
|
||||||
|
|
||||||
### 1. Progressive Difficulty (Evolution Mode)
|
### 1. Progressive Difficulty (Evolution Mode)
|
||||||
|
|
||||||
The AI search depth (Ply) increases as the board fills. This ensures the AI is fast in the opening and lethal in the endgame.
|
To keep the game challenging and the CPU efficient, the AI search depth (Ply) scales as the board fills.
|
||||||
|
|
||||||
- **Formula:** $DynamicPly = BasePly + \lfloor \frac{DiscsOnBoard}{7} \rfloor$
|
- **Formula:** $DynamicPly = BasePly + \lfloor \frac{DiscsOnBoard}{7} \rfloor$
|
||||||
- **Benefit:** High-level tactical precision exactly when the game becomes critical.
|
- **Benefit:** The AI is "casual" in the opening but becomes a "Grandmaster" in the endgame when tactical precision is vital.
|
||||||
|
|
||||||
### 2. Strategic Blunder Injection
|
### 2. Intelligent Win Detection & Flashing
|
||||||
|
|
||||||
To avoid endless stalemate draws between high-level AIs, a "Blunder" logic is used.
|
The win-engine has been refactored to prevent "color ghosting."
|
||||||
|
|
||||||
- **Demo Mode:** Always active; 20% chance to make a suboptimal move.
|
- **Winner Locking:** The `scanBoard()` function returns the specific ID of the winner (1 for Yellow, 2 for Red).
|
||||||
- **Player Mode:** Toggleable via Web Portal to make the AI more "human."
|
- **Flashing Accuracy:** The final animation uses this ID to ensure the winning 4-in-a-row flashes in the **correct player's color**, regardless of whose turn it was when the game ended.
|
||||||
|
|
||||||
### 3. Alpha-Beta Pruning & Column Ordering
|
### 3. Smart Watchdog (Tiered Timeout)
|
||||||
|
|
||||||
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.
|
The game respects your "thinking time" by using a tiered idle-timeout system:
|
||||||
|
|
||||||
|
- **Menu/Finished State:** Standard timeout (e.g., 60s).
|
||||||
|
- **Playing State:** **Double Timeout** (e.g., 120s). This gives human players more time to analyze complex boards before the game auto-resets to Demo Mode.
|
||||||
|
|
||||||
|
### 4. Strategic Blunder Injection
|
||||||
|
|
||||||
|
To ensure Demo Mode doesn't end in an infinite loop of draws, a 20% "Blunder Chance" is injected. This forces the AI to occasionally make a human-like mistake, creating openings for a definitive winner.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 Code Architecture Details
|
## 📖 Code Architecture & Modules
|
||||||
|
|
||||||
### 🔄 State Machine
|
### 🔄 State Machine
|
||||||
|
|
||||||
The software cycles through states:
|
The core loop manages five distinct states:
|
||||||
|
|
||||||
- **MENU:** Select mode using the rotary encoder.
|
1. **MENU:** Mode selection and board reset.
|
||||||
- **PLAYING:** Manages player turns and the gravity-acceleration drop animation.
|
2. **PLAYING:** Active turn-based logic with gravity-accelerated drop animations.
|
||||||
- **DEMO:** Auto-starts after inactivity. Randomizes Ply (3-6) and enforces blunders to ensure definitive game results.
|
3. **FINISHED_WIN:** Locks the winner ID and flashes the winning segment.
|
||||||
|
4. **FINISHED_DRAW:** Blinks the entire board to signify a stalemate.
|
||||||
|
5. **DEMO:** Auto-plays with randomized difficulty (Ply 3-6) and mandatory blunder logic.
|
||||||
|
|
||||||
### 🎨 Rendering & Mapping
|
### 🌐 Web Administration Portal
|
||||||
|
|
||||||
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.
|
Accessible via the **"Connect4-Config"** AP at `192.168.4.1`.
|
||||||
|
|
||||||
|
- **Base Ply:** Sets the starting difficulty level.
|
||||||
|
- **Brightness:** Global LED intensity (0-255).
|
||||||
|
- **Evolution Toggle:** Turn on/off the progressive difficulty scaling.
|
||||||
|
- **Blunder Toggle:** Allow the AI to make mistakes during Human-vs-AI matches.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌐 Web Admin Portal
|
## 🛠 Installation
|
||||||
|
|
||||||
Connect to the **"Connect4-Config"** Access Point to adjust:
|
1. **Environment:** Use VS Code with the **PlatformIO** extension.
|
||||||
|
2. **Dependencies:** `FastLED`, `Encoder`, `Preferences`.
|
||||||
- **Base Ply:** Minimum search depth.
|
3. **Build Flag:** Define your WiFi password in `platformio.ini`: `-D WIFI_PASSWORD=\"your_password\"`.
|
||||||
- **Brightness:** Global LED intensity.
|
4. **Flash:** Upload to your ESP32-C3 and enjoy the ultimate desktop Connect 4 experience.
|
||||||
- **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.
|
|
||||||
|
|||||||
+79
-68
@@ -29,6 +29,7 @@ State gameState = MENU;
|
|||||||
|
|
||||||
int8_t menuMode = 0;
|
int8_t menuMode = 0;
|
||||||
int8_t currentPlayer = 1;
|
int8_t currentPlayer = 1;
|
||||||
|
int8_t winnerPlayer = 0; // Tracks who actually won for the flashing effect
|
||||||
int8_t activeCol = 3;
|
int8_t activeCol = 3;
|
||||||
long oldEncPos = -999;
|
long oldEncPos = -999;
|
||||||
uint32_t lastActivityTime = 0;
|
uint32_t lastActivityTime = 0;
|
||||||
@@ -36,7 +37,6 @@ uint32_t demoResetTimer = 0;
|
|||||||
bool isDemoOver = false;
|
bool isDemoOver = false;
|
||||||
uint8_t demoPly = 4;
|
uint8_t demoPly = 4;
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -52,7 +52,7 @@ void drawStaticUI();
|
|||||||
void renderBoard();
|
void renderBoard();
|
||||||
int getFirstEmptyRow(int col);
|
int getFirstEmptyRow(int col);
|
||||||
bool isBoardFull();
|
bool isBoardFull();
|
||||||
bool scanBoard(int8_t p);
|
int8_t scanBoard(); // Changed to return the winner ID
|
||||||
void updateThinkingVisuals(int8_t p, int8_t col);
|
void updateThinkingVisuals(int8_t p, int8_t col);
|
||||||
void animateDrop(int col, int player);
|
void animateDrop(int col, int player);
|
||||||
void moveDiscToCol(int startCol, int targetCol, int player, int speed);
|
void moveDiscToCol(int startCol, int targetCol, int player, int speed);
|
||||||
@@ -69,7 +69,7 @@ void drawStaticUI()
|
|||||||
{
|
{
|
||||||
FastLED.clear();
|
FastLED.clear();
|
||||||
CRGB borderColor = CRGB::Blue;
|
CRGB borderColor = CRGB::Blue;
|
||||||
if (gameState == DEMO || (gameState >= 2 && isDemoOver))
|
if (gameState == DEMO || gameState >= 2)
|
||||||
{
|
{
|
||||||
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);
|
||||||
@@ -114,8 +114,7 @@ int getDynamicPly()
|
|||||||
for (int r = 0; r < ROWS; r++)
|
for (int r = 0; r < ROWS; r++)
|
||||||
if (board[c][r] != 0)
|
if (board[c][r] != 0)
|
||||||
count++;
|
count++;
|
||||||
int evolution = count / 7;
|
return constrain(current_look_ahead + (count / 7), 1, 10);
|
||||||
return constrain(current_look_ahead + evolution, 1, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Visuals & Animations ---
|
// --- Visuals & Animations ---
|
||||||
@@ -143,7 +142,6 @@ void updateThinkingVisuals(int8_t p, int8_t col)
|
|||||||
CRGB aiColor = (p == 1) ? CRGB::Yellow : CRGB::Red;
|
CRGB aiColor = (p == 1) ? CRGB::Yellow : CRGB::Red;
|
||||||
leds[getIdx(col, 0)] = aiColor.nscale8(aiBrightness);
|
leds[getIdx(col, 0)] = aiColor.nscale8(aiBrightness);
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
activeCol = col;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void animateDrop(int col, int player)
|
void animateDrop(int col, int player)
|
||||||
@@ -151,15 +149,12 @@ void animateDrop(int col, int player)
|
|||||||
int targetRow = getFirstEmptyRow(col);
|
int targetRow = getFirstEmptyRow(col);
|
||||||
if (targetRow == -1)
|
if (targetRow == -1)
|
||||||
return;
|
return;
|
||||||
int currentDelay = 80;
|
|
||||||
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();
|
||||||
delay(currentDelay);
|
delay(max(20, 80 - (5 - r) * 15));
|
||||||
if (currentDelay > 20)
|
|
||||||
currentDelay -= 15;
|
|
||||||
}
|
}
|
||||||
board[col][targetRow] = player;
|
board[col][targetRow] = player;
|
||||||
renderBoard();
|
renderBoard();
|
||||||
@@ -192,37 +187,49 @@ bool isBoardFull()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool scanBoard(int8_t p)
|
int8_t scanBoard()
|
||||||
{
|
{
|
||||||
memset(winMask, 0, sizeof(winMask));
|
memset(winMask, 0, sizeof(winMask));
|
||||||
bool found = false;
|
|
||||||
auto check = [&](int c, int r, int dc, int dr)
|
auto check = [&](int c, int r, int dc, int dr)
|
||||||
{
|
{
|
||||||
if (board[c][r] == p && board[c + dc][r + dr] == p && board[c + 2 * dc][r + 2 * dr] == p && board[c + 3 * dc][r + 3 * dr] == p)
|
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)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
winMask[getIdx(c + i * dc, 7 - (r + i * dr))] = true;
|
winMask[getIdx(c + i * dc, 7 - (r + i * dr))] = true;
|
||||||
return true;
|
return p;
|
||||||
}
|
}
|
||||||
return false;
|
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++)
|
for (int c = 0; c < 4; c++)
|
||||||
if (check(c, r, 1, 0))
|
{
|
||||||
found = true;
|
int8_t res = check(c, r, 1, 0);
|
||||||
|
if (res)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
for (int r = 0; r < 3; r++)
|
for (int r = 0; r < 3; r++)
|
||||||
for (int c = 0; c < 7; c++)
|
for (int c = 0; c < 7; c++)
|
||||||
if (check(c, r, 0, 1))
|
{
|
||||||
found = true;
|
int8_t res = check(c, r, 0, 1);
|
||||||
|
if (res)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
for (int r = 0; r < 3; r++)
|
for (int r = 0; r < 3; r++)
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
if (check(c, r, 1, 1))
|
{
|
||||||
found = true;
|
int8_t res = check(c, r, 1, 1);
|
||||||
|
if (res)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
for (int r = 3; r < 6; r++)
|
for (int r = 3; r < 6; r++)
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
if (check(c, r, 1, -1))
|
{
|
||||||
found = true;
|
int8_t res = check(c, r, 1, -1);
|
||||||
return found;
|
if (res)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
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)
|
||||||
@@ -231,12 +238,16 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP,
|
|||||||
updateThinkingVisuals(aiP, rootCol);
|
updateThinkingVisuals(aiP, rootCol);
|
||||||
else
|
else
|
||||||
yield();
|
yield();
|
||||||
if (scanBoard(aiP))
|
|
||||||
|
// Check for wins within minimax
|
||||||
|
int8_t win = scanBoard();
|
||||||
|
if (win == aiP)
|
||||||
return 1000 + depth;
|
return 1000 + depth;
|
||||||
if (scanBoard(huP))
|
if (win == 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)
|
||||||
@@ -249,14 +260,12 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP,
|
|||||||
board[c][r] = 0;
|
board[c][r] = 0;
|
||||||
if (isMax)
|
if (isMax)
|
||||||
{
|
{
|
||||||
if (val > best)
|
best = max(best, val);
|
||||||
best = val;
|
|
||||||
alpha = max(alpha, best);
|
alpha = max(alpha, best);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (val < best)
|
best = min(best, val);
|
||||||
best = val;
|
|
||||||
beta = min(beta, best);
|
beta = min(beta, best);
|
||||||
}
|
}
|
||||||
if (beta <= alpha)
|
if (beta <= alpha)
|
||||||
@@ -280,14 +289,14 @@ void performAiMove(int8_t aiP)
|
|||||||
if (r != -1)
|
if (r != -1)
|
||||||
{
|
{
|
||||||
board[c][r] = aiP;
|
board[c][r] = aiP;
|
||||||
if (scanBoard(aiP))
|
if (scanBoard() == aiP)
|
||||||
{
|
{
|
||||||
board[c][r] = 0;
|
board[c][r] = 0;
|
||||||
bestCol = c;
|
bestCol = c;
|
||||||
goto finalize;
|
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] = 0;
|
board[c][r] = 0;
|
||||||
bestCol = c;
|
bestCol = c;
|
||||||
@@ -296,7 +305,6 @@ void performAiMove(int8_t aiP)
|
|||||||
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})
|
||||||
{
|
{
|
||||||
int r = getFirstEmptyRow(c);
|
int r = getFirstEmptyRow(c);
|
||||||
@@ -312,14 +320,12 @@ void performAiMove(int8_t aiP)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((gameState == DEMO || blunder_enabled) && random(100) < 20)
|
if ((gameState == DEMO || blunder_enabled) && random(100) < 20)
|
||||||
{
|
{
|
||||||
int rCol = random(0, 7);
|
int rCol = random(0, 7);
|
||||||
if (getFirstEmptyRow(rCol) != -1)
|
if (getFirstEmptyRow(rCol) != -1)
|
||||||
bestCol = rCol;
|
bestCol = rCol;
|
||||||
}
|
}
|
||||||
|
|
||||||
finalize:
|
finalize:
|
||||||
current_look_ahead = originalPly;
|
current_look_ahead = originalPly;
|
||||||
moveDiscToCol(activeCol, bestCol, aiP, 100);
|
moveDiscToCol(activeCol, bestCol, aiP, 100);
|
||||||
@@ -327,11 +333,14 @@ finalize:
|
|||||||
animateDrop(bestCol, aiP);
|
animateDrop(bestCol, aiP);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Web Portal & Setup ---
|
// --- Web Portal ---
|
||||||
|
|
||||||
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>";
|
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 += "<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 += "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 += "Brightness (5-255):<input type='number' name='br' value='" + String(current_brightness) + "'>";
|
||||||
@@ -419,10 +428,6 @@ void setup()
|
|||||||
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);
|
|
||||||
WiFi.mode(WIFI_AP);
|
|
||||||
delay(100);
|
|
||||||
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
|
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
|
||||||
server.on("/", handleRoot);
|
server.on("/", handleRoot);
|
||||||
server.on("/save", HTTP_POST, handleSave);
|
server.on("/save", HTTP_POST, handleSave);
|
||||||
@@ -439,7 +444,7 @@ void loop()
|
|||||||
|
|
||||||
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
|
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
|
||||||
{
|
{
|
||||||
if (gameState == DEMO || isDemoOver)
|
if (gameState >= 2 || gameState == DEMO)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@@ -447,7 +452,7 @@ void loop()
|
|||||||
FastLED.show();
|
FastLED.show();
|
||||||
delay(30);
|
delay(30);
|
||||||
}
|
}
|
||||||
delay(1000);
|
delay(500);
|
||||||
gameState = MENU;
|
gameState = MENU;
|
||||||
memset(board, 0, sizeof(board));
|
memset(board, 0, sizeof(board));
|
||||||
showMenu();
|
showMenu();
|
||||||
@@ -458,6 +463,16 @@ void loop()
|
|||||||
lastActivityTime = millis();
|
lastActivityTime = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t activeLimit = (gameState == PLAYING) ? (current_idle_timeout_ms * 2) : current_idle_timeout_ms;
|
||||||
|
if (gameState != DEMO && (gameState < 2) && (millis() - lastActivityTime > activeLimit))
|
||||||
|
{
|
||||||
|
gameState = DEMO;
|
||||||
|
memset(board, 0, sizeof(board));
|
||||||
|
currentPlayer = 1;
|
||||||
|
demoPly = random(3, 7);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (gameState == MENU)
|
if (gameState == MENU)
|
||||||
{
|
{
|
||||||
if (newPos != oldEncPos)
|
if (newPos != oldEncPos)
|
||||||
@@ -481,13 +496,6 @@ void loop()
|
|||||||
}
|
}
|
||||||
delay(300);
|
delay(300);
|
||||||
}
|
}
|
||||||
if (millis() - lastActivityTime > current_idle_timeout_ms)
|
|
||||||
{
|
|
||||||
gameState = DEMO;
|
|
||||||
memset(board, 0, sizeof(board));
|
|
||||||
currentPlayer = 1;
|
|
||||||
demoPly = random(3, 7);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (gameState == PLAYING)
|
else if (gameState == PLAYING)
|
||||||
{
|
{
|
||||||
@@ -505,23 +513,34 @@ void loop()
|
|||||||
if (row != -1)
|
if (row != -1)
|
||||||
{
|
{
|
||||||
animateDrop(activeCol, currentPlayer);
|
animateDrop(activeCol, currentPlayer);
|
||||||
if (scanBoard(currentPlayer))
|
winnerPlayer = scanBoard();
|
||||||
|
if (winnerPlayer != 0)
|
||||||
|
{
|
||||||
gameState = FINISHED_WIN;
|
gameState = FINISHED_WIN;
|
||||||
|
demoResetTimer = millis();
|
||||||
|
}
|
||||||
else if (isBoardFull())
|
else if (isBoardFull())
|
||||||
|
{
|
||||||
gameState = FINISHED_DRAW;
|
gameState = FINISHED_DRAW;
|
||||||
|
demoResetTimer = millis();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (menuMode < 2)
|
if (menuMode < 2)
|
||||||
{
|
{
|
||||||
int8_t aiP = (menuMode == 0) ? 2 : 1;
|
int8_t aiP = (menuMode == 0) ? 2 : 1;
|
||||||
performAiMove(aiP);
|
performAiMove(aiP);
|
||||||
if (scanBoard(aiP))
|
winnerPlayer = scanBoard();
|
||||||
|
if (winnerPlayer != 0)
|
||||||
{
|
{
|
||||||
currentPlayer = aiP;
|
|
||||||
gameState = FINISHED_WIN;
|
gameState = FINISHED_WIN;
|
||||||
|
demoResetTimer = millis();
|
||||||
}
|
}
|
||||||
else if (isBoardFull())
|
else if (isBoardFull())
|
||||||
|
{
|
||||||
gameState = FINISHED_DRAW;
|
gameState = FINISHED_DRAW;
|
||||||
|
demoResetTimer = millis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -538,16 +557,15 @@ void loop()
|
|||||||
FastLED.show();
|
FastLED.show();
|
||||||
delay(600);
|
delay(600);
|
||||||
performAiMove(currentPlayer);
|
performAiMove(currentPlayer);
|
||||||
if (scanBoard(currentPlayer))
|
winnerPlayer = scanBoard();
|
||||||
|
if (winnerPlayer != 0)
|
||||||
{
|
{
|
||||||
gameState = FINISHED_WIN;
|
gameState = FINISHED_WIN;
|
||||||
isDemoOver = true;
|
|
||||||
demoResetTimer = millis();
|
demoResetTimer = millis();
|
||||||
}
|
}
|
||||||
else if (isBoardFull())
|
else if (isBoardFull())
|
||||||
{
|
{
|
||||||
gameState = FINISHED_DRAW;
|
gameState = FINISHED_DRAW;
|
||||||
isDemoOver = true;
|
|
||||||
demoResetTimer = millis();
|
demoResetTimer = millis();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -556,14 +574,7 @@ void loop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{ // FINISHED state
|
||||||
if (!isDemoOver && (millis() - lastActivityTime > current_idle_timeout_ms))
|
|
||||||
{
|
|
||||||
memset(board, 0, sizeof(board));
|
|
||||||
gameState = DEMO;
|
|
||||||
currentPlayer = 1;
|
|
||||||
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)
|
||||||
@@ -578,9 +589,9 @@ void loop()
|
|||||||
if (gameState == FINISHED_WIN)
|
if (gameState == FINISHED_WIN)
|
||||||
{
|
{
|
||||||
if (winMask[i])
|
if (winMask[i])
|
||||||
leds[i] = toggle ? (currentPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black;
|
leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black;
|
||||||
else
|
else
|
||||||
leds[i].nscale8(40);
|
leds[i].nscale8(60);
|
||||||
}
|
}
|
||||||
else if (gameState == FINISHED_DRAW)
|
else if (gameState == FINISHED_DRAW)
|
||||||
{
|
{
|
||||||
@@ -590,11 +601,11 @@ void loop()
|
|||||||
}
|
}
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
}
|
}
|
||||||
if (isDemoOver && (millis() - demoResetTimer > 15000))
|
if (millis() - demoResetTimer > 15000)
|
||||||
{
|
{
|
||||||
memset(board, 0, sizeof(board));
|
memset(board, 0, sizeof(board));
|
||||||
gameState = DEMO;
|
gameState = DEMO;
|
||||||
isDemoOver = false;
|
demoResetTimer = 0;
|
||||||
demoPly = random(3, 7);
|
demoPly = random(3, 7);
|
||||||
}
|
}
|
||||||
if (pressed)
|
if (pressed)
|
||||||
|
|||||||
Reference in New Issue
Block a user