Compare commits

..

2 Commits

2 changed files with 96 additions and 72 deletions
+1
View File
@@ -11,6 +11,7 @@ build_flags =
-D ENC_B=1 -D ENC_B=1
-D ENC_SW=2 -D ENC_SW=2
-D SENSITIVITY=4 -D SENSITIVITY=4
-D SHOW_BORDER=0
-D BRIGHTNESS=25 -D BRIGHTNESS=25
-D IDLE_TIMEOUT=45000 -D IDLE_TIMEOUT=45000
-D DEMO_RESET_PAUSE=20000 -D DEMO_RESET_PAUSE=20000
+95 -72
View File
@@ -5,6 +5,11 @@
#include <WebServer.h> #include <WebServer.h>
#include <Preferences.h> #include <Preferences.h>
// Build Flag Default (can be overridden in platformio.ini)
#ifndef SHOW_BORDER
#define SHOW_BORDER 1
#endif
#define NUM_LEDS 64 #define NUM_LEDS 64
const int COLS = 7; const int COLS = 7;
const int ROWS = 6; const int ROWS = 6;
@@ -29,7 +34,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 winnerPlayer = 0;
int8_t activeCol = 3; int8_t activeCol = 3;
long oldEncPos = -999; long oldEncPos = -999;
uint32_t lastActivityTime = 0; uint32_t lastActivityTime = 0;
@@ -52,12 +57,12 @@ void drawStaticUI();
void renderBoard(); void renderBoard();
int getFirstEmptyRow(int col); int getFirstEmptyRow(int col);
bool isBoardFull(); bool isBoardFull();
int8_t scanBoard(); // Changed to return the winner ID int8_t scanBoard();
void updateThinkingVisuals(int8_t p, int8_t col); void updateThinkingVisuals(int8_t playerColor, int8_t column);
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);
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 aiPlayer, int8_t humanPlayer, int8_t rootCol);
void performAiMove(int8_t aiP); void performAiMove(int8_t aiPlayer);
void showMenu(); void showMenu();
int getDynamicPly(); int getDynamicPly();
@@ -68,6 +73,7 @@ int getIdx(int x, int y) { return (y * 8) + x; }
void drawStaticUI() void drawStaticUI()
{ {
FastLED.clear(); FastLED.clear();
#if SHOW_BORDER == 1
CRGB borderColor = CRGB::Blue; CRGB borderColor = CRGB::Blue;
if (gameState == DEMO || gameState >= 2) if (gameState == DEMO || gameState >= 2)
{ {
@@ -78,29 +84,30 @@ void drawStaticUI()
leds[getIdx(x, 1)] = borderColor; leds[getIdx(x, 1)] = borderColor;
for (int y = 1; y < 8; y++) for (int y = 1; y < 8; y++)
leds[getIdx(7, y)] = borderColor; leds[getIdx(7, y)] = borderColor;
#endif
} }
void renderBoard() void renderBoard()
{ {
drawStaticUI(); 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) if (board[column][row] == 1)
leds[getIdx(c, 7 - r)] = CRGB::Yellow; leds[getIdx(column, 7 - row)] = CRGB::Yellow;
if (board[c][r] == 2) if (board[column][row] == 2)
leds[getIdx(c, 7 - r)] = CRGB::Red; leds[getIdx(column, 7 - row)] = CRGB::Red;
} }
} }
} }
int getFirstEmptyRow(int col) int getFirstEmptyRow(int col)
{ {
for (int r = 0; r < ROWS; r++) for (int row = 0; row < ROWS; row++)
{ {
if (board[col][r] == 0) if (board[col][row] == 0)
return r; return row;
} }
return -1; return -1;
} }
@@ -109,17 +116,17 @@ int getDynamicPly()
{ {
if (!progressive_difficulty && gameState != DEMO) if (!progressive_difficulty && gameState != DEMO)
return current_look_ahead; return current_look_ahead;
int count = 0; int occupiedCount = 0;
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] != 0) if (board[column][row] != 0)
count++; occupiedCount++;
return constrain(current_look_ahead + (count / 7), 1, 10); return constrain(current_look_ahead + (occupiedCount / 7), 1, 10);
} }
// --- Visuals & Animations --- // --- Visuals & Animations ---
void updateThinkingVisuals(int8_t p, int8_t col) void updateThinkingVisuals(int8_t playerColor, int8_t column)
{ {
static uint32_t lastCycle = 0; static uint32_t lastCycle = 0;
if (millis() - lastCycle < 25) if (millis() - lastCycle < 25)
@@ -139,8 +146,8 @@ void updateThinkingVisuals(int8_t p, int8_t col)
} }
for (int x = 0; x < COLS; x++) for (int x = 0; x < COLS; x++)
leds[getIdx(x, 0)] = CRGB::Black; leds[getIdx(x, 0)] = CRGB::Black;
CRGB aiColor = (p == 1) ? CRGB::Yellow : CRGB::Red; CRGB aiColor = (playerColor == 1) ? CRGB::Yellow : CRGB::Red;
leds[getIdx(col, 0)] = aiColor.nscale8(aiBrightness); leds[getIdx(column, 0)] = aiColor.nscale8(aiBrightness);
FastLED.show(); FastLED.show();
} }
@@ -149,12 +156,12 @@ void animateDrop(int col, int player)
int targetRow = getFirstEmptyRow(col); int targetRow = getFirstEmptyRow(col);
if (targetRow == -1) if (targetRow == -1)
return; return;
for (int r = 5; r >= targetRow; r--) for (int row = 5; row >= targetRow; row--)
{ {
renderBoard(); 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(); FastLED.show();
delay(max(20, 80 - (5 - r) * 15)); delay(max(20, 80 - (5 - row) * 15));
} }
board[col][targetRow] = player; board[col][targetRow] = player;
renderBoard(); renderBoard();
@@ -181,8 +188,8 @@ void moveDiscToCol(int startCol, int targetCol, int player, int speed)
bool isBoardFull() bool isBoardFull()
{ {
for (int c = 0; c < COLS; c++) for (int column = 0; column < COLS; column++)
if (board[c][5] == 0) if (board[column][5] == 0)
return false; return false;
return true; return true;
} }
@@ -190,44 +197,48 @@ bool isBoardFull()
int8_t scanBoard() int8_t scanBoard()
{ {
memset(winMask, 0, sizeof(winMask)); 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]; int8_t playerAtPos = board[col][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) if (playerAtPos != 0 &&
board[col + dCol][row + dRow] == playerAtPos &&
board[col + 2 * dCol][row + 2 * dRow] == playerAtPos &&
board[col + 3 * dCol][row + 3 * dRow] == playerAtPos)
{ {
for (int index = 0; index < 4; index++) for (int i = 0; i < 4; i++)
winMask[getIdx(column + index * columnOffset, 7 - (row + index * rowOffset))] = true; winMask[getIdx(col + i * dCol, 7 - (row + i * dRow))] = true;
return postion; return playerAtPos;
} }
return (int8_t)0; return (int8_t)0;
}; };
for (int row = 0; row < 6; row++) for (int row = 0; row < 6; row++)
for (int column = 0; column < 4; column++) for (int col = 0; col < 4; col++)
{ {
int8_t res = check(column, row, 1, 0); int8_t result = checkMatch(col, row, 1, 0);
if (res) if (result)
return res; return result;
} }
for (int row = 0; row < 3; row++) for (int row = 0; row < 3; row++)
for (int column = 0; column < 7; column++) for (int col = 0; col < 7; col++)
{ {
int8_t res = check(column, row, 0, 1); int8_t result = checkMatch(col, row, 0, 1);
if (res) if (result)
return res; return result;
} }
for (int row = 0; row < 3; row++) for (int row = 0; row < 3; row++)
for (int column = 0; column < 4; column++) for (int col = 0; col < 4; col++)
{ {
int8_t res = check(column, row, 1, 1); int8_t result = checkMatch(col, row, 1, 1);
if (res) if (result)
return res; return result;
} }
for (int row = 3; row < 6; row++) for (int row = 3; row < 6; row++)
for (int column = 0; column < 4; column++) for (int col = 0; col < 4; col++)
{ {
int8_t res = check(column, row, 1, -1); int8_t result = checkMatch(col, row, 1, -1);
if (res) if (result)
return res; return result;
} }
return 0; return 0;
} }
@@ -239,40 +250,40 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t
else else
yield(); yield();
// Check for wins within minimax // Check winner via temporary scan (logic check only)
int8_t win = scanBoard(); int8_t winner = scanBoard();
if (win == aiPlayer) if (winner == aiPlayer)
return 1000 + depth; return 1000 + depth;
if (win == humanPlayer) if (winner == humanPlayer)
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 colOrder[] = {3, 2, 4, 1, 5, 0, 6};
int best = isMax ? -2000 : 2000; int bestScore = isMax ? -2000 : 2000;
for (int column : order) for (int column : colOrder)
{ {
int row = getFirstEmptyRow(column); int row = getFirstEmptyRow(column);
if (row != -1) if (row != -1)
{ {
board[column][row] = isMax ? aiPlayer : humanPlayer; 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; board[column][row] = 0;
if (isMax) if (isMax)
{ {
best = max(best, val); bestScore = max(bestScore, score);
alpha = max(alpha, best); alpha = max(alpha, bestScore);
} }
else else
{ {
best = min(best, val); bestScore = min(bestScore, score);
beta = min(beta, best); beta = min(beta, bestScore);
} }
if (beta <= alpha) if (beta <= alpha)
break; break;
} }
} }
return best; return bestScore;
} }
void performAiMove(int8_t aiPlayer) void performAiMove(int8_t aiPlayer)
@@ -283,6 +294,7 @@ void performAiMove(int8_t aiPlayer)
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
for (int column = 0; column < COLS; column++) for (int column = 0; column < COLS; column++)
{ {
int row = getFirstEmptyRow(column); int row = getFirstEmptyRow(column);
@@ -293,18 +305,19 @@ void performAiMove(int8_t aiPlayer)
{ {
board[column][row] = 0; board[column][row] = 0;
bestCol = column; bestCol = column;
goto finalize; goto finalizeMove;
} }
board[column][row] = humanPlayer; board[column][row] = humanPlayer;
if (current_look_ahead >= 2 && scanBoard() == humanPlayer) if (current_look_ahead >= 2 && scanBoard() == humanPlayer)
{ {
board[column][row] = 0; board[column][row] = 0;
bestCol = column; bestCol = column;
goto finalize; goto finalizeMove;
} }
board[column][row] = 0; board[column][row] = 0;
} }
} }
for (int column : {3, 2, 4, 1, 5, 0, 6}) for (int column : {3, 2, 4, 1, 5, 0, 6})
{ {
int row = getFirstEmptyRow(column); int row = getFirstEmptyRow(column);
@@ -320,13 +333,15 @@ void performAiMove(int8_t aiPlayer)
} }
} }
} }
if ((gameState == DEMO || blunder_enabled) && random(100) < 20) if ((gameState == DEMO || blunder_enabled) && random(100) < 20)
{ {
int rCol = random(0, 7); int randomColumn = random(0, 7);
if (getFirstEmptyRow(rCol) != -1) if (getFirstEmptyRow(randomColumn) != -1)
bestCol = rCol; bestCol = randomColumn;
} }
finalize:
finalizeMove:
current_look_ahead = originalPly; current_look_ahead = originalPly;
moveDiscToCol(activeCol, bestCol, aiPlayer, 100); moveDiscToCol(activeCol, bestCol, aiPlayer, 100);
delay(450); delay(450);
@@ -382,10 +397,12 @@ void showMenu()
{ {
isDemoOver = false; isDemoOver = false;
FastLED.clear(); FastLED.clear();
#if SHOW_BORDER == 1
for (int x = 0; x < 7; x++) for (int x = 0; x < 7; x++)
leds[getIdx(x, 1)] = CRGB::Blue; leds[getIdx(x, 1)] = CRGB::Blue;
for (int y = 1; y < 8; y++) for (int y = 1; y < 8; y++)
leds[getIdx(7, y)] = CRGB::Blue; leds[getIdx(7, y)] = CRGB::Blue;
#endif
if (menuMode < 2) if (menuMode < 2)
{ {
CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
@@ -442,6 +459,7 @@ 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)
@@ -503,12 +521,14 @@ void loop()
{ {
activeCol = (newPos % 7 + 7) % 7; activeCol = (newPos % 7 + 7) % 7;
oldEncPos = newPos; oldEncPos = newPos;
lastActivityTime = millis();
} }
renderBoard(); renderBoard();
leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red; leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red;
FastLED.show(); FastLED.show();
if (pressed) if (pressed)
{ {
lastActivityTime = millis();
int row = getFirstEmptyRow(activeCol); int row = getFirstEmptyRow(activeCol);
if (row != -1) if (row != -1)
{ {
@@ -528,8 +548,9 @@ void loop()
{ {
if (menuMode < 2) if (menuMode < 2)
{ {
int8_t aiPlayer = (menuMode == 0) ? 2 : 1; int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove(aiPlayer); performAiMove(aiP);
lastActivityTime = millis(); // Reset after AI thinking
winnerPlayer = scanBoard(); winnerPlayer = scanBoard();
if (winnerPlayer != 0) if (winnerPlayer != 0)
{ {
@@ -574,7 +595,7 @@ void loop()
} }
} }
else else
{ // FINISHED state { // 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)
@@ -584,8 +605,10 @@ void loop()
renderBoard(); renderBoard();
for (int i = 0; i < NUM_LEDS; i++) for (int i = 0; i < NUM_LEDS; i++)
{ {
#if SHOW_BORDER == 1
if (leds[i] == CRGB::Blue) if (leds[i] == CRGB::Blue)
continue; continue;
#endif
if (gameState == FINISHED_WIN) if (gameState == FINISHED_WIN)
{ {
if (winMask[i]) if (winMask[i])