Compare commits
2 Commits
8edfda2b21
...
a6e0bd0489
| Author | SHA1 | Date | |
|---|---|---|---|
| a6e0bd0489 | |||
| 117d078efc |
@@ -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
@@ -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])
|
||||||
|
|||||||
Reference in New Issue
Block a user