[fix] AI killer instinct, border toggle, and animation timing.
This commit is contained in:
+46
-49
@@ -1,72 +1,69 @@
|
||||
# 📑 Technical Specification: Connect 4 AI Logic
|
||||
# 🕹️ Connect 4 AI: How the Brain Works
|
||||
|
||||
## 1. Board Representation: The 2D Grid Model
|
||||
|
||||
The game state is maintained in a 2D array of signed integers: `int8_t board[COLS][ROWS]`. This structure mirrors the physical dimensions of a standard Connect 4 rack.
|
||||
|
||||
### Data Structure
|
||||
|
||||
- **Dimensions:** 7 columns (X) by 6 rows (Y).
|
||||
- **Mapping:** \* `0`: Null/Empty slot.
|
||||
- `1`: Player 1 (Yellow / Human).
|
||||
- `2`: Player 2 (Red / AI).
|
||||
- **Hardware Translation:** To drive the 8x8 NeoPixel matrix, the 2D coordinates are flattened into a 1D index using the specific mapping:
|
||||
$$Index = (y \times 8) + x$$
|
||||
_Note: The 8th column ($x=7$) is ignored by the game logic and reserved for UI borders._
|
||||
To create a competitive Connect 4 experience on a small microcontroller, the game uses a mix of mathematical strategy and "shortcuts" to play like a human master.
|
||||
|
||||
---
|
||||
|
||||
## 2. Positional Evaluation
|
||||
## 1. The Virtual Board
|
||||
|
||||
Since Connect 4 has a state-space complexity of approximately $4.5 \times 10^{12}$, the ESP32 cannot calculate every possible outcome to the end of the game from the first move. Instead, it uses a **Heuristic Evaluation Function** to score board positions.
|
||||
The computer sees the board as a grid of numbers. It uses a **7-column by 6-row** map where:
|
||||
|
||||
### Scoring Heuristics
|
||||
- **0** = Empty space
|
||||
- **1** = Yellow (Human Player)
|
||||
- **2** = Red (Computer AI)
|
||||
|
||||
1. **Terminal Victory:** Any move that results in a 4-in-a-row is valued at $+1000$ (for AI) or $-1000$ (for Human).
|
||||
2. **Temporal Weighting:** To ensure the AI chooses the _fastest_ path to victory and the _longest_ path to defeat, the score is adjusted by the search depth:
|
||||
- **AI Win:** $1000 + depth$
|
||||
- **Human Win:** $-1000 - depth$
|
||||
3. **Column Geometry:** The AI inherently values central columns higher than edges. This is not explicitly hardcoded in the score but emerges from the search logic: a disc in column 3 can be part of horizontal, vertical, and diagonal win lines in both directions, making it mathematically more valuable.
|
||||
Every time a disc is dropped, a "Scan" function checks every possible direction (horizontal, vertical, and diagonal) to see if anyone has reached four in a row.
|
||||
|
||||
---
|
||||
|
||||
## 3. Determining the Value of a "Half-Move"
|
||||
## 2. Thinking Ahead (The "What If?" Engine)
|
||||
|
||||
A "half-move" is a single disc placement by one player. Its value is determined via the **Minimax Algorithm** with **Alpha-Beta Pruning**.
|
||||
The AI doesn't just look at the current board; it plays out thousands of "What if?" scenarios in its head.
|
||||
|
||||
### The Recursive Search Process
|
||||
### The Minimax Strategy
|
||||
|
||||
The AI simulates a move (a "branch") and then recursively simulates the opponent's best possible responses. The value of a move is the "minimized" maximum score possible from that branch.
|
||||
The AI uses a strategy called **Minimax**. It assumes that you will play your absolute best move, and it tries to find the move that leaves you with the worst possible outcome. It "maximizes" its own advantage while "minimizing" yours.
|
||||
|
||||
### Optimization: Alpha-Beta Pruning
|
||||
### Alpha-Beta Pruning (The Shortcut)
|
||||
|
||||
To prevent the ESP32-C3 from timing out, the engine "prunes" branches that are mathematically guaranteed to be worse than previously explored paths.
|
||||
|
||||
- **Alpha ($\alpha$):** The best score the AI (Maximizer) can guarantee.
|
||||
- **Beta ($\beta$):** The best score the Human (Minimizer) can guarantee.
|
||||
- **The Cut-off:** If at any point $\beta \leq \alpha$, the branch is abandoned.
|
||||
|
||||
### 4. Dynamic Move Ordering
|
||||
|
||||
The efficiency of the value determination is heavily reliant on **Move Ordering**. By evaluating the most promising columns first (starting from the center), the AI finds a high "Alpha" value quickly.
|
||||
|
||||
- **Search Order:** `3 -> 2 -> 4 -> 1 -> 5 -> 0 -> 6`
|
||||
This ordering allows the Alpha-Beta pruning to discard up to 90% of the possible moves in the outer columns without calculating them, significantly reducing the "Thinking" time on the microcontroller.
|
||||
Calculating every possible move in Connect 4 would take hours. To make the AI fast, it uses **Pruning**. If the AI starts calculating a move and realizes it’s definitely worse than a move it already found, it "prunes" (deletes) that entire branch of thought and moves on. This allows it to ignore up to 90% of useless moves.
|
||||
|
||||
---
|
||||
|
||||
## 5. Summary of Logic Execution
|
||||
## 3. Scoring System (The AI’s Instincts)
|
||||
|
||||
1. **Generate** all valid moves for the current board state.
|
||||
2. **Order** moves starting from the center column.
|
||||
3. **Execute** Minimax recursion for each move up to the current **Ply**.
|
||||
4. **Prune** branches that cannot mathematically improve the current best option.
|
||||
5. **Return** the move with the highest heuristic value.
|
||||
Since the computer can't always see to the very end of the game, it uses a scoring system to guess which positions are strongest:
|
||||
|
||||
- **Speed Matters:** The AI is rewarded more for a win that happens soon than a win that takes a long time. This gives it a "killer instinct" to end the game as quickly as possible.
|
||||
- **The Center is King:** The AI is programmed to prefer the middle column. Mathematically, the center column is involved in the most possible winning combinations, so the AI fights to control it early in the game.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
## 4. Being Responsive (Interrupt Handling)
|
||||
|
||||
- [Information on how to analyze Connect-four](https://www.google.com/search?q=https://en.wikipedia.org/wiki/Connect_Four%23Mathematical_solution)
|
||||
- [How does minimax work](https://en.wikipedia.org/wiki/Minimax)
|
||||
- [What is aplpha-beta pruning](ttps://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning)
|
||||
Your game runs on an **ESP32-C3**, which is a single-tasking processor. Normally, if the AI spends 2 seconds thinking, the buttons would feel "broken" or "frozen" until it finishes.
|
||||
|
||||
We solve this with two clever tricks:
|
||||
|
||||
1. **Mid-Thought Checks:** Every few milliseconds of calculation, the AI "pauses" for a microsecond to see if you have pressed the button.
|
||||
2. **Instant Exit:** If it detects you pressed the button while it was thinking, it abandons all calculations immediately and jumps back to the main menu.
|
||||
|
||||
---
|
||||
|
||||
## 5. Summary of an AI Move
|
||||
|
||||
When it is the computer's turn, it follows these steps in a split second:
|
||||
|
||||
1. **Check for Lethal:** Can I win right now? If yes, take it.
|
||||
2. **Check for Danger:** Can the human win on their next move? If yes, block it.
|
||||
3. **Search:** Look at the middle columns first, then the edges.
|
||||
4. **Prune:** Throw away bad moves immediately to save time.
|
||||
5. **Act:** Choose the move that leads to the quickest victory.
|
||||
|
||||
---
|
||||
|
||||
## 📚 References & Further Reading
|
||||
|
||||
- [The Mathematical Solution to Connect 4](https://en.wikipedia.org/wiki/Connect_Four#Mathematical_solution)
|
||||
- [How the Minimax Algorithm Works](https://en.wikipedia.org/wiki/Minimax)
|
||||
- [Understanding Alpha-Beta Pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning)
|
||||
|
||||
@@ -1,86 +1,50 @@
|
||||
# 🕹️ Connect 4 AI: Master Edition (v2.0)
|
||||
# 🕹️ Connect 4 AI: Grandmaster Edition (v2.5)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
A high-performance Connect 4 implementation for the ESP32-C3 (RISC-V). This version features a "Killer Instinct" AI, human-like animations, and a real-time interrupt system.
|
||||
|
||||
## 🛠 Hardware Configuration
|
||||
|
||||
### 🔌 Pin Mapping (Lolin C3 Mini)
|
||||
|
||||
| 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) |
|
||||
| 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/Abort (SW) |
|
||||
|
||||
### 📐 Physical Layout
|
||||
|
||||
The project is optimized for an 8x8 NeoPixel Matrix (65mm x 67mm).
|
||||
|
||||
- **Row 0:** Interaction & AI Decision Visualization.
|
||||
- **Row 1:** Static Blue UI border.
|
||||
- **Rows 2-7:** Active $7 \times 6$ game board.
|
||||
- **Status Column:** Far right column (Index 7) manages UI framing and "Glow" effects.
|
||||
- **Game Board:** 7 Columns x 6 Rows.
|
||||
- **Top Row (Row 0):** Interaction row (Selection & AI Thinking pulse).
|
||||
- **UI Border:** Row 1 and Column 7 (Blue frame, toggleable via `SHOW_BORDER`).
|
||||
- **Coordinate Formula:** $Index = (y \times 8) + x$
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Advanced AI & Logic Features
|
||||
## 🧠 Advanced AI Features
|
||||
|
||||
### 1. Progressive Difficulty (Evolution Mode)
|
||||
### 1. Offense-Priority Strategy
|
||||
|
||||
To keep the game challenging and the CPU efficient, the AI search depth (Ply) scales as the board fills.
|
||||
The AI follows a strict 3-phase move evaluation:
|
||||
|
||||
- **Formula:** $DynamicPly = BasePly + \lfloor \frac{DiscsOnBoard}{7} \rfloor$
|
||||
- **Benefit:** The AI is "casual" in the opening but becomes a "Grandmaster" in the endgame when tactical precision is vital.
|
||||
1. **Lethal:** If the AI can connect four this turn, it takes the win immediately.
|
||||
2. **Defensive:** If the human player has a lethal move, the AI blocks it.
|
||||
3. **Strategic:** If no immediate wins exist, it runs a deep Minimax search.
|
||||
|
||||
### 2. Intelligent Win Detection & Flashing
|
||||
### 2. High-Priority Interrupts
|
||||
|
||||
The win-engine has been refactored to prevent "color ghosting."
|
||||
The AI's single-core RISC-V processor is kept responsive via an "Abort Flag." Pressing the button or turning the encoder during an AI calculation (Demo or Playing) immediately kills the recursion and returns the user to the Menu.
|
||||
|
||||
- **Winner Locking:** The `scanBoard()` function returns the specific ID of the winner (1 for Yellow, 2 for Red).
|
||||
- **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. Evolution Mode
|
||||
|
||||
### 3. Smart Watchdog (Tiered Timeout)
|
||||
|
||||
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.
|
||||
AI difficulty scales dynamically: $DynamicPly = BasePly + \lfloor \frac{DiscsOnBoard}{7} \rfloor$.
|
||||
|
||||
---
|
||||
|
||||
## 📖 Code Architecture & Modules
|
||||
## 🛠 Installation & Build
|
||||
|
||||
### 🔄 State Machine
|
||||
|
||||
The core loop manages five distinct states:
|
||||
|
||||
1. **MENU:** Mode selection and board reset.
|
||||
2. **PLAYING:** Active turn-based logic with gravity-accelerated drop animations.
|
||||
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.
|
||||
|
||||
### 🌐 Web Administration Portal
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Installation
|
||||
|
||||
1. **Environment:** Use VS Code with the **PlatformIO** extension.
|
||||
1. **Environment:** VS Code with PlatformIO.
|
||||
2. **Dependencies:** `FastLED`, `Encoder`, `Preferences`.
|
||||
3. **Build Flag:** Define your WiFi password in `platformio.ini`: `-D WIFI_PASSWORD=\"your_password\"`.
|
||||
4. **Flash:** Upload to your ESP32-C3 and enjoy the ultimate desktop Connect 4 experience.
|
||||
3. **Build Flags:** - `-D SHOW_BORDER=1` (Enables blue frame)
|
||||
- `-D SHOW_BORDER=0` (Full-screen board mode)
|
||||
|
||||
@@ -11,6 +11,7 @@ build_flags =
|
||||
-D ENC_B=1
|
||||
-D ENC_SW=2
|
||||
-D SENSITIVITY=4
|
||||
-D SHOW_BORDER=0
|
||||
-D BRIGHTNESS=25
|
||||
-D IDLE_TIMEOUT=45000
|
||||
-D DEMO_RESET_PAUSE=20000
|
||||
|
||||
+194
-158
@@ -5,11 +5,14 @@
|
||||
#include <WebServer.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
#ifndef SHOW_BORDER
|
||||
#define SHOW_BORDER 1
|
||||
#endif
|
||||
|
||||
#define NUM_LEDS 64
|
||||
const int COLS = 7;
|
||||
const int ROWS = 6;
|
||||
|
||||
// --- Configuration & Globals ---
|
||||
CRGB leds[NUM_LEDS];
|
||||
Encoder myEnc(ENC_A, ENC_B);
|
||||
WebServer server(80);
|
||||
@@ -29,13 +32,14 @@ State gameState = MENU;
|
||||
|
||||
int8_t menuMode = 0;
|
||||
int8_t currentPlayer = 1;
|
||||
int8_t winnerPlayer = 0; // Tracks who actually won for the flashing effect
|
||||
int8_t winnerPlayer = 0;
|
||||
int8_t activeCol = 3;
|
||||
long oldEncPos = -999;
|
||||
uint32_t lastActivityTime = 0;
|
||||
uint32_t demoResetTimer = 0;
|
||||
bool isDemoOver = false;
|
||||
uint8_t demoPly = 4;
|
||||
bool abortAi = false;
|
||||
|
||||
uint8_t current_look_ahead;
|
||||
uint8_t current_brightness;
|
||||
@@ -52,22 +56,21 @@ void drawStaticUI();
|
||||
void renderBoard();
|
||||
int getFirstEmptyRow(int col);
|
||||
bool isBoardFull();
|
||||
int8_t scanBoard(); // Changed to return the winner ID
|
||||
void updateThinkingVisuals(int8_t p, int8_t col);
|
||||
int8_t scanBoard();
|
||||
void updateThinkingVisuals(int8_t playerColor, int8_t column);
|
||||
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);
|
||||
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t humanPlayer, int8_t rootCol);
|
||||
void performAiMove(int8_t aiPlayer);
|
||||
void showMenu();
|
||||
int getDynamicPly();
|
||||
|
||||
// --- Utility & Rendering ---
|
||||
|
||||
int getIdx(int x, int y) { return (y * 8) + x; }
|
||||
|
||||
void drawStaticUI()
|
||||
{
|
||||
FastLED.clear();
|
||||
#if SHOW_BORDER == 1
|
||||
CRGB borderColor = CRGB::Blue;
|
||||
if (gameState == DEMO || gameState >= 2)
|
||||
{
|
||||
@@ -78,48 +81,57 @@ void drawStaticUI()
|
||||
leds[getIdx(x, 1)] = borderColor;
|
||||
for (int y = 1; y < 8; y++)
|
||||
leds[getIdx(7, y)] = borderColor;
|
||||
#endif
|
||||
}
|
||||
|
||||
void renderBoard()
|
||||
{
|
||||
drawStaticUI();
|
||||
for (int c = 0; c < COLS; c++)
|
||||
for (int column = 0; column < COLS; column++)
|
||||
{
|
||||
for (int r = 0; r < ROWS; r++)
|
||||
for (int row = 0; row < ROWS; row++)
|
||||
{
|
||||
if (board[c][r] == 1)
|
||||
leds[getIdx(c, 7 - r)] = CRGB::Yellow;
|
||||
if (board[c][r] == 2)
|
||||
leds[getIdx(c, 7 - r)] = CRGB::Red;
|
||||
if (board[column][row] == 1)
|
||||
leds[getIdx(column, 7 - row)] = CRGB::Yellow;
|
||||
if (board[column][row] == 2)
|
||||
leds[getIdx(column, 7 - row)] = CRGB::Red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getFirstEmptyRow(int col)
|
||||
{
|
||||
for (int r = 0; r < ROWS; r++)
|
||||
for (int row = 0; row < ROWS; row++)
|
||||
{
|
||||
if (board[col][r] == 0)
|
||||
return r;
|
||||
if (board[col][row] == 0)
|
||||
return row;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool isBoardFull()
|
||||
{
|
||||
for (int column = 0; column < COLS; column++)
|
||||
{
|
||||
if (board[column][ROWS - 1] == 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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++;
|
||||
return constrain(current_look_ahead + (count / 7), 1, 10);
|
||||
int occupiedCount = 0;
|
||||
for (int column = 0; column < COLS; column++)
|
||||
for (int row = 0; row < ROWS; row++)
|
||||
if (board[column][row] != 0)
|
||||
occupiedCount++;
|
||||
return constrain(current_look_ahead + (occupiedCount / 7), 1, 10);
|
||||
}
|
||||
|
||||
// --- Visuals & Animations ---
|
||||
|
||||
void updateThinkingVisuals(int8_t p, int8_t col)
|
||||
void updateThinkingVisuals(int8_t playerColor, int8_t column)
|
||||
{
|
||||
static uint32_t lastCycle = 0;
|
||||
if (millis() - lastCycle < 25)
|
||||
@@ -139,8 +151,8 @@ void updateThinkingVisuals(int8_t p, int8_t col)
|
||||
}
|
||||
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);
|
||||
CRGB aiColor = (playerColor == 1) ? CRGB::Yellow : CRGB::Red;
|
||||
leds[getIdx(column, 0)] = aiColor.nscale8(aiBrightness);
|
||||
FastLED.show();
|
||||
}
|
||||
|
||||
@@ -149,12 +161,12 @@ void animateDrop(int col, int player)
|
||||
int targetRow = getFirstEmptyRow(col);
|
||||
if (targetRow == -1)
|
||||
return;
|
||||
for (int r = 5; r >= targetRow; r--)
|
||||
for (int row = 5; row >= targetRow; row--)
|
||||
{
|
||||
renderBoard();
|
||||
leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red;
|
||||
leds[getIdx(col, 7 - row)] = (player == 1) ? CRGB::Yellow : CRGB::Red;
|
||||
FastLED.show();
|
||||
delay(max(20, 80 - (5 - r) * 15));
|
||||
delay(max(20, 80 - (5 - row) * 15));
|
||||
}
|
||||
board[col][targetRow] = player;
|
||||
renderBoard();
|
||||
@@ -165,7 +177,7 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed)
|
||||
{
|
||||
int current = startCol;
|
||||
CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red;
|
||||
while (current != targetCol)
|
||||
while (current != targetCol && !abortAi)
|
||||
{
|
||||
leds[getIdx(current, 0)] = CRGB::Black;
|
||||
current += (targetCol > current) ? 1 : -1;
|
||||
@@ -173,59 +185,52 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed)
|
||||
leds[getIdx(current, 0)] = pColor;
|
||||
FastLED.show();
|
||||
delay(speed);
|
||||
if (digitalRead(ENC_SW) == LOW)
|
||||
abortAi = true;
|
||||
}
|
||||
activeCol = targetCol;
|
||||
}
|
||||
|
||||
// --- AI Engine ---
|
||||
|
||||
bool isBoardFull()
|
||||
{
|
||||
for (int c = 0; c < COLS; c++)
|
||||
if (board[c][5] == 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int8_t scanBoard()
|
||||
{
|
||||
memset(winMask, 0, sizeof(winMask));
|
||||
auto check = [&](int column, int row, int columnOffset, int rowOffset)
|
||||
auto checkMatch = [&](int col, int row, int dCol, int dRow)
|
||||
{
|
||||
int8_t postion = board[column][row];
|
||||
if (postion != 0 && board[column + columnOffset][row + rowOffset] == postion && board[column + 2 * columnOffset][row + 2 * rowOffset] == postion && board[column + 3 * columnOffset][row + 3 * rowOffset] == postion)
|
||||
int8_t pAtPos = board[col][row];
|
||||
if (pAtPos != 0 && board[col + dCol][row + dRow] == pAtPos &&
|
||||
board[col + 2 * dCol][row + 2 * dRow] == pAtPos && board[col + 3 * dCol][row + 3 * dRow] == pAtPos)
|
||||
{
|
||||
for (int index = 0; index < 4; index++)
|
||||
winMask[getIdx(column + index * columnOffset, 7 - (row + index * rowOffset))] = true;
|
||||
return postion;
|
||||
for (int i = 0; i < 4; i++)
|
||||
winMask[getIdx(col + i * dCol, 7 - (row + i * dRow))] = true;
|
||||
return pAtPos;
|
||||
}
|
||||
return (int8_t)0;
|
||||
};
|
||||
for (int row = 0; row < 6; row++)
|
||||
for (int column = 0; column < 4; column++)
|
||||
for (int r = 0; r < 6; r++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int8_t res = check(column, row, 1, 0);
|
||||
int8_t res = checkMatch(c, r, 1, 0);
|
||||
if (res)
|
||||
return res;
|
||||
}
|
||||
for (int row = 0; row < 3; row++)
|
||||
for (int column = 0; column < 7; column++)
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 7; c++)
|
||||
{
|
||||
int8_t res = check(column, row, 0, 1);
|
||||
int8_t res = checkMatch(c, r, 0, 1);
|
||||
if (res)
|
||||
return res;
|
||||
}
|
||||
for (int row = 0; row < 3; row++)
|
||||
for (int column = 0; column < 4; column++)
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int8_t res = check(column, row, 1, 1);
|
||||
int8_t res = checkMatch(c, r, 1, 1);
|
||||
if (res)
|
||||
return res;
|
||||
}
|
||||
for (int row = 3; row < 6; row++)
|
||||
for (int column = 0; column < 4; column++)
|
||||
for (int r = 3; r < 6; r++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int8_t res = check(column, row, 1, -1);
|
||||
int8_t res = checkMatch(c, r, 1, -1);
|
||||
if (res)
|
||||
return res;
|
||||
}
|
||||
@@ -234,55 +239,69 @@ int8_t scanBoard()
|
||||
|
||||
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t humanPlayer, int8_t rootCol)
|
||||
{
|
||||
if (depth % 2 == 0)
|
||||
{
|
||||
if (digitalRead(ENC_SW) == LOW)
|
||||
{
|
||||
abortAi = true;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (depth >= current_look_ahead - 1)
|
||||
updateThinkingVisuals(aiPlayer, rootCol);
|
||||
else
|
||||
yield();
|
||||
if (abortAi)
|
||||
return 0;
|
||||
|
||||
// Check for wins within minimax
|
||||
int8_t win = scanBoard();
|
||||
if (win == aiPlayer)
|
||||
return 1000 + depth;
|
||||
if (win == humanPlayer)
|
||||
return -1000 - depth;
|
||||
int8_t winner = scanBoard();
|
||||
if (winner == aiPlayer)
|
||||
return 1000 + depth; // Win sooner is better
|
||||
if (winner == humanPlayer)
|
||||
return -1000 - depth; // Lose later is better
|
||||
if (depth == 0 || isBoardFull())
|
||||
return 0;
|
||||
|
||||
int order[] = {3, 2, 4, 1, 5, 0, 6};
|
||||
int best = isMax ? -2000 : 2000;
|
||||
for (int column : order)
|
||||
int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
|
||||
int bestScore = isMax ? -10000 : 10000;
|
||||
|
||||
for (int column : colOrder)
|
||||
{
|
||||
if (abortAi)
|
||||
return 0;
|
||||
int row = getFirstEmptyRow(column);
|
||||
if (row != -1)
|
||||
{
|
||||
board[column][row] = isMax ? aiPlayer : humanPlayer;
|
||||
int val = minimax(depth - 1, alpha, beta, !isMax, aiPlayer, humanPlayer, (depth == current_look_ahead ? column : rootCol));
|
||||
int score = minimax(depth - 1, alpha, beta, !isMax, aiPlayer, humanPlayer, (depth == current_look_ahead ? column : rootCol));
|
||||
board[column][row] = 0;
|
||||
if (isMax)
|
||||
{
|
||||
best = max(best, val);
|
||||
alpha = max(alpha, best);
|
||||
bestScore = max(bestScore, score);
|
||||
alpha = max(alpha, bestScore);
|
||||
}
|
||||
else
|
||||
{
|
||||
best = min(best, val);
|
||||
beta = min(beta, best);
|
||||
bestScore = min(bestScore, score);
|
||||
beta = min(beta, bestScore);
|
||||
}
|
||||
if (beta <= alpha)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
return bestScore;
|
||||
}
|
||||
|
||||
void performAiMove(int8_t aiPlayer)
|
||||
{
|
||||
abortAi = false;
|
||||
int humanPlayer = (aiPlayer == 1) ? 2 : 1;
|
||||
int bestScore = -30000;
|
||||
int bestCol = 3;
|
||||
int originalPly = current_look_ahead;
|
||||
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
|
||||
|
||||
// PHASE 1: Immediate Win Check (OFFENSE)
|
||||
for (int column = 0; column < COLS; column++)
|
||||
{
|
||||
int row = getFirstEmptyRow(column);
|
||||
@@ -293,20 +312,34 @@ void performAiMove(int8_t aiPlayer)
|
||||
{
|
||||
board[column][row] = 0;
|
||||
bestCol = column;
|
||||
goto finalize;
|
||||
}
|
||||
board[column][row] = humanPlayer;
|
||||
if (current_look_ahead >= 2 && scanBoard() == humanPlayer)
|
||||
{
|
||||
board[column][row] = 0;
|
||||
bestCol = column;
|
||||
goto finalize;
|
||||
goto finalizeMove; // TAKE THE WIN IMMEDIATELY
|
||||
}
|
||||
board[column][row] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 2: Immediate Block Check (DEFENSE)
|
||||
for (int column = 0; column < COLS; column++)
|
||||
{
|
||||
int row = getFirstEmptyRow(column);
|
||||
if (row != -1)
|
||||
{
|
||||
board[column][row] = humanPlayer;
|
||||
if (scanBoard() == humanPlayer)
|
||||
{
|
||||
board[column][row] = 0;
|
||||
bestCol = column;
|
||||
goto finalizeMove; // MUST BLOCK
|
||||
}
|
||||
board[column][row] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 3: Minimax Look-ahead
|
||||
for (int column : {3, 2, 4, 1, 5, 0, 6})
|
||||
{
|
||||
if (abortAi)
|
||||
goto finalizeMove;
|
||||
int row = getFirstEmptyRow(column);
|
||||
if (row != -1)
|
||||
{
|
||||
@@ -320,34 +353,29 @@ void performAiMove(int8_t aiPlayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
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, aiPlayer, 100);
|
||||
delay(450);
|
||||
animateDrop(bestCol, aiPlayer);
|
||||
}
|
||||
|
||||
// --- Web Portal ---
|
||||
if ((gameState == DEMO || blunder_enabled) && random(100) < 20 && !abortAi)
|
||||
{
|
||||
int randomColumn = random(0, 7);
|
||||
if (getFirstEmptyRow(randomColumn) != -1)
|
||||
bestCol = randomColumn;
|
||||
}
|
||||
|
||||
finalizeMove:
|
||||
current_look_ahead = originalPly;
|
||||
if (!abortAi)
|
||||
{
|
||||
moveDiscToCol(activeCol, bestCol, aiPlayer, 100);
|
||||
delay(450);
|
||||
animateDrop(bestCol, aiPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
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>";
|
||||
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'><style>body{font-family:sans-serif;background:#121212;color:white;text-align:center;} .card{background:#222;padding:25px;border-radius:15px;display:inline-block;margin-top:20px;} input{width:100%;padding:10px;margin:10px 0;border-radius:5px;border:none;}</style></head><body><h1>Connect 4 Admin</h1><div class='card'><form action='/save' method='POST'>";
|
||||
html += "Base AI Ply:<input type='number' name='ply' value='" + String(current_look_ahead) + "'>Brightness:<input type='number' name='br' value='" + String(current_brightness) + "'>Idle Timeout (s):<input type='number' name='idle' value='" + String(current_idle_timeout_ms / 1000) + "'>";
|
||||
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunder_enabled ? "checked" : "") + "><br>Evolution: <input type='checkbox' name='evolve' " + String(progressive_difficulty ? "checked" : "") + "><br><br><input type='submit' value='Save' style='background:#28a745;color:white;'></form></div></body></html>";
|
||||
server.send(200, "text/html", html);
|
||||
}
|
||||
|
||||
@@ -366,9 +394,8 @@ void handleSave()
|
||||
}
|
||||
if (server.hasArg("idle"))
|
||||
{
|
||||
uint32_t s = server.arg("idle").toInt();
|
||||
current_idle_timeout_ms = s * 1000;
|
||||
prefs.putUInt("idle", s);
|
||||
current_idle_timeout_ms = server.arg("idle").toInt() * 1000;
|
||||
prefs.putUInt("idle", current_idle_timeout_ms / 1000);
|
||||
}
|
||||
blunder_enabled = server.hasArg("blunder");
|
||||
prefs.putBool("blunder", blunder_enabled);
|
||||
@@ -382,19 +409,21 @@ void showMenu()
|
||||
{
|
||||
isDemoOver = false;
|
||||
FastLED.clear();
|
||||
#if SHOW_BORDER == 1
|
||||
for (int x = 0; x < 7; x++)
|
||||
leds[getIdx(x, 1)] = CRGB::Blue;
|
||||
for (int y = 1; y < 8; y++)
|
||||
leds[getIdx(7, y)] = CRGB::Blue;
|
||||
#endif
|
||||
if (menuMode < 2)
|
||||
{
|
||||
CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
|
||||
CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
|
||||
for (int y = 3; y <= 6; y++)
|
||||
leds[getIdx(3, y)] = p1Col;
|
||||
leds[getIdx(2, 3)] = p1Col;
|
||||
leds[getIdx(4, 3)] = p1Col;
|
||||
leds[getIdx(2, 6)] = p1Col;
|
||||
leds[getIdx(4, 6)] = p1Col;
|
||||
leds[getIdx(3, y)] = pCol;
|
||||
leds[getIdx(2, 3)] = pCol;
|
||||
leds[getIdx(4, 3)] = pCol;
|
||||
leds[getIdx(2, 6)] = pCol;
|
||||
leds[getIdx(4, 6)] = pCol;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -424,7 +453,6 @@ void setup()
|
||||
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);
|
||||
@@ -446,18 +474,21 @@ void loop()
|
||||
{
|
||||
if (gameState >= 2 || gameState == DEMO)
|
||||
{
|
||||
for (int index = 0; index < 10; index++)
|
||||
{
|
||||
fadeToBlackBy(leds, NUM_LEDS, 32);
|
||||
FastLED.show();
|
||||
delay(30);
|
||||
}
|
||||
delay(500);
|
||||
gameState = MENU;
|
||||
abortAi = true;
|
||||
memset(board, 0, sizeof(board));
|
||||
winnerPlayer = 0;
|
||||
demoResetTimer = 0;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
fadeToBlackBy(leds, NUM_LEDS, 40);
|
||||
FastLED.show();
|
||||
delay(20);
|
||||
}
|
||||
gameState = MENU;
|
||||
showMenu();
|
||||
lastActivityTime = millis();
|
||||
oldEncPos = newPos;
|
||||
lastActivityTime = millis();
|
||||
delay(300);
|
||||
return;
|
||||
}
|
||||
lastActivityTime = millis();
|
||||
@@ -503,12 +534,14 @@ void loop()
|
||||
{
|
||||
activeCol = (newPos % 7 + 7) % 7;
|
||||
oldEncPos = newPos;
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
renderBoard();
|
||||
leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red;
|
||||
FastLED.show();
|
||||
if (pressed)
|
||||
{
|
||||
lastActivityTime = millis();
|
||||
int row = getFirstEmptyRow(activeCol);
|
||||
if (row != -1)
|
||||
{
|
||||
@@ -528,18 +561,22 @@ void loop()
|
||||
{
|
||||
if (menuMode < 2)
|
||||
{
|
||||
int8_t aiPlayer = (menuMode == 0) ? 2 : 1;
|
||||
performAiMove(aiPlayer);
|
||||
winnerPlayer = scanBoard();
|
||||
if (winnerPlayer != 0)
|
||||
int8_t aiP = (menuMode == 0) ? 2 : 1;
|
||||
performAiMove(aiP);
|
||||
lastActivityTime = millis();
|
||||
if (!abortAi)
|
||||
{
|
||||
gameState = FINISHED_WIN;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
else if (isBoardFull())
|
||||
{
|
||||
gameState = FINISHED_DRAW;
|
||||
demoResetTimer = millis();
|
||||
winnerPlayer = scanBoard();
|
||||
if (winnerPlayer != 0)
|
||||
{
|
||||
gameState = FINISHED_WIN;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
else if (isBoardFull())
|
||||
{
|
||||
gameState = FINISHED_DRAW;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -557,24 +594,27 @@ void loop()
|
||||
FastLED.show();
|
||||
delay(600);
|
||||
performAiMove(currentPlayer);
|
||||
winnerPlayer = scanBoard();
|
||||
if (winnerPlayer != 0)
|
||||
if (!abortAi)
|
||||
{
|
||||
gameState = FINISHED_WIN;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
else if (isBoardFull())
|
||||
{
|
||||
gameState = FINISHED_DRAW;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPlayer = (currentPlayer == 1) ? 2 : 1;
|
||||
winnerPlayer = scanBoard();
|
||||
if (winnerPlayer != 0)
|
||||
{
|
||||
gameState = FINISHED_WIN;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
else if (isBoardFull())
|
||||
{
|
||||
gameState = FINISHED_DRAW;
|
||||
demoResetTimer = millis();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPlayer = (currentPlayer == 1) ? 2 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // FINISHED state
|
||||
{
|
||||
static uint32_t lastFlash = 0;
|
||||
static bool toggle = true;
|
||||
if (millis() - lastFlash > 300)
|
||||
@@ -584,8 +624,10 @@ void loop()
|
||||
renderBoard();
|
||||
for (int i = 0; i < NUM_LEDS; i++)
|
||||
{
|
||||
#if SHOW_BORDER == 1
|
||||
if (leds[i] == CRGB::Blue)
|
||||
continue;
|
||||
#endif
|
||||
if (gameState == FINISHED_WIN)
|
||||
{
|
||||
if (winMask[i])
|
||||
@@ -608,11 +650,5 @@ void loop()
|
||||
demoResetTimer = 0;
|
||||
demoPly = random(3, 7);
|
||||
}
|
||||
if (pressed)
|
||||
{
|
||||
gameState = MENU;
|
||||
showMenu();
|
||||
delay(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user