[fix] AI strategy detect win first, killer instinct and button press. Although not 100% okay as button is not always detected during 'thinking"

This commit is contained in:
2026-03-09 10:38:42 +01:00
parent a6e0bd0489
commit 0994c11f0b
+112 -99
View File
@@ -5,7 +5,6 @@
#include <WebServer.h> #include <WebServer.h>
#include <Preferences.h> #include <Preferences.h>
// Build Flag Default (can be overridden in platformio.ini)
#ifndef SHOW_BORDER #ifndef SHOW_BORDER
#define SHOW_BORDER 1 #define SHOW_BORDER 1
#endif #endif
@@ -14,7 +13,6 @@
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);
@@ -41,6 +39,7 @@ uint32_t lastActivityTime = 0;
uint32_t demoResetTimer = 0; uint32_t demoResetTimer = 0;
bool isDemoOver = false; bool isDemoOver = false;
uint8_t demoPly = 4; uint8_t demoPly = 4;
bool abortAi = false;
uint8_t current_look_ahead; uint8_t current_look_ahead;
uint8_t current_brightness; uint8_t current_brightness;
@@ -66,8 +65,6 @@ void performAiMove(int8_t aiPlayer);
void showMenu(); void showMenu();
int getDynamicPly(); int getDynamicPly();
// --- Utility & Rendering ---
int getIdx(int x, int y) { return (y * 8) + x; } int getIdx(int x, int y) { return (y * 8) + x; }
void drawStaticUI() void drawStaticUI()
@@ -112,6 +109,16 @@ int getFirstEmptyRow(int col)
return -1; return -1;
} }
bool isBoardFull()
{
for (int column = 0; column < COLS; column++)
{
if (board[column][ROWS - 1] == 0)
return false;
}
return true;
}
int getDynamicPly() int getDynamicPly()
{ {
if (!progressive_difficulty && gameState != DEMO) if (!progressive_difficulty && gameState != DEMO)
@@ -124,8 +131,6 @@ int getDynamicPly()
return constrain(current_look_ahead + (occupiedCount / 7), 1, 10); return constrain(current_look_ahead + (occupiedCount / 7), 1, 10);
} }
// --- Visuals & Animations ---
void updateThinkingVisuals(int8_t playerColor, int8_t column) void updateThinkingVisuals(int8_t playerColor, int8_t column)
{ {
static uint32_t lastCycle = 0; static uint32_t lastCycle = 0;
@@ -172,7 +177,7 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed)
{ {
int current = startCol; int current = startCol;
CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red; CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red;
while (current != targetCol) while (current != targetCol && !abortAi)
{ {
leds[getIdx(current, 0)] = CRGB::Black; leds[getIdx(current, 0)] = CRGB::Black;
current += (targetCol > current) ? 1 : -1; current += (targetCol > current) ? 1 : -1;
@@ -180,89 +185,90 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed)
leds[getIdx(current, 0)] = pColor; leds[getIdx(current, 0)] = pColor;
FastLED.show(); FastLED.show();
delay(speed); delay(speed);
if (digitalRead(ENC_SW) == LOW)
abortAi = true;
} }
activeCol = targetCol; activeCol = targetCol;
} }
// --- AI Engine ---
bool isBoardFull()
{
for (int column = 0; column < COLS; column++)
if (board[column][5] == 0)
return false;
return true;
}
int8_t scanBoard() int8_t scanBoard()
{ {
memset(winMask, 0, sizeof(winMask)); memset(winMask, 0, sizeof(winMask));
auto checkMatch = [&](int col, int row, int dCol, int dRow) auto checkMatch = [&](int col, int row, int dCol, int dRow)
{ {
int8_t playerAtPos = board[col][row]; int8_t pAtPos = board[col][row];
if (playerAtPos != 0 && if (pAtPos != 0 && board[col + dCol][row + dRow] == pAtPos &&
board[col + dCol][row + dRow] == playerAtPos && board[col + 2 * dCol][row + 2 * dRow] == pAtPos && board[col + 3 * dCol][row + 3 * dRow] == pAtPos)
board[col + 2 * dCol][row + 2 * dRow] == playerAtPos &&
board[col + 3 * dCol][row + 3 * dRow] == playerAtPos)
{ {
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
winMask[getIdx(col + i * dCol, 7 - (row + i * dRow))] = true; winMask[getIdx(col + i * dCol, 7 - (row + i * dRow))] = true;
return playerAtPos; return pAtPos;
} }
return (int8_t)0; return (int8_t)0;
}; };
for (int r = 0; r < 6; r++)
for (int row = 0; row < 6; row++) for (int c = 0; c < 4; c++)
for (int col = 0; col < 4; col++)
{ {
int8_t result = checkMatch(col, row, 1, 0); int8_t res = checkMatch(c, r, 1, 0);
if (result) if (res)
return result; return res;
} }
for (int row = 0; row < 3; row++) for (int r = 0; r < 3; r++)
for (int col = 0; col < 7; col++) for (int c = 0; c < 7; c++)
{ {
int8_t result = checkMatch(col, row, 0, 1); int8_t res = checkMatch(c, r, 0, 1);
if (result) if (res)
return result; return res;
} }
for (int row = 0; row < 3; row++) for (int r = 0; r < 3; r++)
for (int col = 0; col < 4; col++) for (int c = 0; c < 4; c++)
{ {
int8_t result = checkMatch(col, row, 1, 1); int8_t res = checkMatch(c, r, 1, 1);
if (result) if (res)
return result; return res;
} }
for (int row = 3; row < 6; row++) for (int r = 3; r < 6; r++)
for (int col = 0; col < 4; col++) for (int c = 0; c < 4; c++)
{ {
int8_t result = checkMatch(col, row, 1, -1); int8_t res = checkMatch(c, r, 1, -1);
if (result) if (res)
return result; return res;
} }
return 0; return 0;
} }
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t humanPlayer, int8_t rootCol) 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) if (depth >= current_look_ahead - 1)
updateThinkingVisuals(aiPlayer, rootCol); updateThinkingVisuals(aiPlayer, rootCol);
else else
yield(); yield();
if (abortAi)
return 0;
// Check winner via temporary scan (logic check only)
int8_t winner = scanBoard(); int8_t winner = scanBoard();
if (winner == aiPlayer) if (winner == aiPlayer)
return 1000 + depth; return 1000 + depth; // Win sooner is better
if (winner == humanPlayer) if (winner == humanPlayer)
return -1000 - depth; return -1000 - depth; // Lose later is better
if (depth == 0 || isBoardFull()) if (depth == 0 || isBoardFull())
return 0; return 0;
int colOrder[] = {3, 2, 4, 1, 5, 0, 6}; int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
int bestScore = isMax ? -2000 : 2000; int bestScore = isMax ? -10000 : 10000;
for (int column : colOrder) for (int column : colOrder)
{ {
if (abortAi)
return 0;
int row = getFirstEmptyRow(column); int row = getFirstEmptyRow(column);
if (row != -1) if (row != -1)
{ {
@@ -288,13 +294,14 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t
void performAiMove(int8_t aiPlayer) void performAiMove(int8_t aiPlayer)
{ {
abortAi = false;
int humanPlayer = (aiPlayer == 1) ? 2 : 1; int humanPlayer = (aiPlayer == 1) ? 2 : 1;
int bestScore = -30000; int bestScore = -30000;
int bestCol = 3; int bestCol = 3;
int originalPly = current_look_ahead; int originalPly = current_look_ahead;
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly(); current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
// Instant win/block logic // PHASE 1: Immediate Win Check (OFFENSE)
for (int column = 0; column < COLS; column++) for (int column = 0; column < COLS; column++)
{ {
int row = getFirstEmptyRow(column); int row = getFirstEmptyRow(column);
@@ -305,21 +312,34 @@ void performAiMove(int8_t aiPlayer)
{ {
board[column][row] = 0; board[column][row] = 0;
bestCol = column; bestCol = column;
goto finalizeMove; goto finalizeMove; // TAKE THE WIN IMMEDIATELY
}
board[column][row] = humanPlayer;
if (current_look_ahead >= 2 && scanBoard() == humanPlayer)
{
board[column][row] = 0;
bestCol = column;
goto finalizeMove;
} }
board[column][row] = 0; 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}) for (int column : {3, 2, 4, 1, 5, 0, 6})
{ {
if (abortAi)
goto finalizeMove;
int row = getFirstEmptyRow(column); int row = getFirstEmptyRow(column);
if (row != -1) if (row != -1)
{ {
@@ -334,7 +354,7 @@ void performAiMove(int8_t aiPlayer)
} }
} }
if ((gameState == DEMO || blunder_enabled) && random(100) < 20) if ((gameState == DEMO || blunder_enabled) && random(100) < 20 && !abortAi)
{ {
int randomColumn = random(0, 7); int randomColumn = random(0, 7);
if (getFirstEmptyRow(randomColumn) != -1) if (getFirstEmptyRow(randomColumn) != -1)
@@ -343,26 +363,19 @@ void performAiMove(int8_t aiPlayer)
finalizeMove: finalizeMove:
current_look_ahead = originalPly; current_look_ahead = originalPly;
if (!abortAi)
{
moveDiscToCol(activeCol, bestCol, aiPlayer, 100); moveDiscToCol(activeCol, bestCol, aiPlayer, 100);
delay(450); delay(450);
animateDrop(bestCol, aiPlayer); animateDrop(bestCol, aiPlayer);
} }
}
// --- Web Portal ---
void handleRoot() void handleRoot()
{ {
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>" 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'>";
"<style>body{font-family:sans-serif;background:#121212;color:white;text-align:center;}" 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) + "'>";
" .card{background:#222;padding:25px;border-radius:15px;display:inline-block;margin-top:20px;}" 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>";
" 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); server.send(200, "text/html", html);
} }
@@ -381,9 +394,8 @@ void handleSave()
} }
if (server.hasArg("idle")) if (server.hasArg("idle"))
{ {
uint32_t s = server.arg("idle").toInt(); current_idle_timeout_ms = server.arg("idle").toInt() * 1000;
current_idle_timeout_ms = s * 1000; prefs.putUInt("idle", current_idle_timeout_ms / 1000);
prefs.putUInt("idle", s);
} }
blunder_enabled = server.hasArg("blunder"); blunder_enabled = server.hasArg("blunder");
prefs.putBool("blunder", blunder_enabled); prefs.putBool("blunder", blunder_enabled);
@@ -405,13 +417,13 @@ void showMenu()
#endif #endif
if (menuMode < 2) 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++) for (int y = 3; y <= 6; y++)
leds[getIdx(3, y)] = p1Col; leds[getIdx(3, y)] = pCol;
leds[getIdx(2, 3)] = p1Col; leds[getIdx(2, 3)] = pCol;
leds[getIdx(4, 3)] = p1Col; leds[getIdx(4, 3)] = pCol;
leds[getIdx(2, 6)] = p1Col; leds[getIdx(2, 6)] = pCol;
leds[getIdx(4, 6)] = p1Col; leds[getIdx(4, 6)] = pCol;
} }
else else
{ {
@@ -441,7 +453,6 @@ void setup()
current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000; current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000;
blunder_enabled = prefs.getBool("blunder", false); blunder_enabled = prefs.getBool("blunder", false);
progressive_difficulty = prefs.getBool("evolve", 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);
@@ -459,23 +470,25 @@ void loop()
long newPos = myEnc.read() / SENSITIVITY; long newPos = myEnc.read() / SENSITIVITY;
bool pressed = (digitalRead(ENC_SW) == LOW); bool pressed = (digitalRead(ENC_SW) == LOW);
// Activity check
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500))) if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
{ {
if (gameState >= 2 || gameState == DEMO) if (gameState >= 2 || gameState == DEMO)
{ {
for (int index = 0; index < 10; index++) abortAi = true;
{
fadeToBlackBy(leds, NUM_LEDS, 32);
FastLED.show();
delay(30);
}
delay(500);
gameState = MENU;
memset(board, 0, sizeof(board)); 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(); showMenu();
lastActivityTime = millis();
oldEncPos = newPos; oldEncPos = newPos;
lastActivityTime = millis();
delay(300);
return; return;
} }
lastActivityTime = millis(); lastActivityTime = millis();
@@ -550,7 +563,9 @@ void loop()
{ {
int8_t aiP = (menuMode == 0) ? 2 : 1; int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove(aiP); performAiMove(aiP);
lastActivityTime = millis(); // Reset after AI thinking lastActivityTime = millis();
if (!abortAi)
{
winnerPlayer = scanBoard(); winnerPlayer = scanBoard();
if (winnerPlayer != 0) if (winnerPlayer != 0)
{ {
@@ -563,6 +578,7 @@ void loop()
demoResetTimer = millis(); demoResetTimer = millis();
} }
} }
}
else else
{ {
currentPlayer = (currentPlayer == 1) ? 2 : 1; currentPlayer = (currentPlayer == 1) ? 2 : 1;
@@ -578,6 +594,8 @@ void loop()
FastLED.show(); FastLED.show();
delay(600); delay(600);
performAiMove(currentPlayer); performAiMove(currentPlayer);
if (!abortAi)
{
winnerPlayer = scanBoard(); winnerPlayer = scanBoard();
if (winnerPlayer != 0) if (winnerPlayer != 0)
{ {
@@ -594,8 +612,9 @@ void loop()
currentPlayer = (currentPlayer == 1) ? 2 : 1; currentPlayer = (currentPlayer == 1) ? 2 : 1;
} }
} }
}
else else
{ // FINISHED state (WIN/DRAW) {
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)
@@ -631,11 +650,5 @@ void loop()
demoResetTimer = 0; demoResetTimer = 0;
demoPly = random(3, 7); demoPly = random(3, 7);
} }
if (pressed)
{
gameState = MENU;
showMenu();
delay(300);
}
} }
} }