[add] Web config and ply 1
This commit is contained in:
@@ -15,6 +15,10 @@ build_flags =
|
|||||||
-D IDLE_TIMEOUT=45000
|
-D IDLE_TIMEOUT=45000
|
||||||
-D DEMO_RESET_PAUSE=20000
|
-D DEMO_RESET_PAUSE=20000
|
||||||
-D DEBOUNCE_DELAY=50
|
-D DEBOUNCE_DELAY=50
|
||||||
|
-D DEFAULT_LOOK_AHEAD=8
|
||||||
|
-D DEFAULT_BRIGHTNESS=25
|
||||||
|
-D DEFAULT_IDLE_TIMEOUT=45
|
||||||
|
-D WIFI_PASSWORD=\"youlose4\"
|
||||||
lib_deps =
|
lib_deps =
|
||||||
fastled/FastLED @ ^3.6.0
|
fastled/FastLED @ ^3.6.0
|
||||||
paulstoffregen/Encoder @ ^1.4.4
|
paulstoffregen/Encoder @ ^1.4.4
|
||||||
+87
-25
@@ -1,14 +1,18 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <FastLED.h>
|
#include <FastLED.h>
|
||||||
#include <Encoder.h>
|
#include <Encoder.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
#define NUM_LEDS 64
|
#define NUM_LEDS 64
|
||||||
const int COLS = 7;
|
const int COLS = 7;
|
||||||
const int ROWS = 6;
|
const int ROWS = 6;
|
||||||
const int LOOK_AHEAD = 8;
|
|
||||||
|
|
||||||
CRGB leds[NUM_LEDS];
|
CRGB leds[NUM_LEDS];
|
||||||
Encoder myEnc(ENC_A, ENC_B);
|
Encoder myEnc(ENC_A, ENC_B);
|
||||||
|
WebServer server(80);
|
||||||
|
Preferences prefs;
|
||||||
|
|
||||||
int8_t board[COLS][ROWS];
|
int8_t board[COLS][ROWS];
|
||||||
bool winMask[NUM_LEDS];
|
bool winMask[NUM_LEDS];
|
||||||
@@ -30,20 +34,24 @@ uint32_t lastActivityTime = 0;
|
|||||||
uint32_t demoResetTimer = 0;
|
uint32_t demoResetTimer = 0;
|
||||||
bool isDemoOver = false;
|
bool isDemoOver = false;
|
||||||
|
|
||||||
// Thinking Animation Variables
|
// Web-Configurable Parameters (Stored in Flash)
|
||||||
|
uint8_t current_look_ahead;
|
||||||
|
uint8_t current_brightness;
|
||||||
|
uint32_t current_idle_timeout_ms;
|
||||||
|
|
||||||
|
// Thinking Animation Helpers
|
||||||
uint8_t aiBrightness = 0;
|
uint8_t aiBrightness = 0;
|
||||||
bool aiFadeUp = true;
|
bool aiFadeUp = true;
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
int getIdx(int x, int y) { return (y * 8) + x; }
|
int getIdx(int x, int y) { return (y * 8) + x; }
|
||||||
|
|
||||||
// RESTORED: Thinking Animation Function
|
|
||||||
void updateThinkingLED(int8_t p)
|
void updateThinkingLED(int8_t p)
|
||||||
{
|
{
|
||||||
static uint32_t lastCycle = 0;
|
static uint32_t lastCycle = 0;
|
||||||
if (millis() - lastCycle < 20)
|
if (millis() - lastCycle < 20)
|
||||||
return;
|
return;
|
||||||
lastCycle = millis();
|
lastCycle = millis();
|
||||||
|
|
||||||
if (aiFadeUp)
|
if (aiFadeUp)
|
||||||
{
|
{
|
||||||
aiBrightness += 15;
|
aiBrightness += 15;
|
||||||
@@ -56,7 +64,6 @@ void updateThinkingLED(int8_t p)
|
|||||||
if (aiBrightness <= 15)
|
if (aiBrightness <= 15)
|
||||||
aiFadeUp = true;
|
aiFadeUp = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CRGB compColor = (p == 1) ? CRGB::Yellow : CRGB::Red;
|
CRGB compColor = (p == 1) ? CRGB::Yellow : CRGB::Red;
|
||||||
leds[getIdx(7, 0)] = compColor.nscale8(aiBrightness);
|
leds[getIdx(7, 0)] = compColor.nscale8(aiBrightness);
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
@@ -143,10 +150,10 @@ bool scanBoard(int8_t p)
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- AI Engine ---
|
||||||
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP)
|
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP)
|
||||||
{
|
{
|
||||||
// Show thinking animation during deep recursion
|
if (depth >= current_look_ahead - 1)
|
||||||
if (depth == LOOK_AHEAD || depth == LOOK_AHEAD - 1)
|
|
||||||
updateThinkingLED(aiP);
|
updateThinkingLED(aiP);
|
||||||
else
|
else
|
||||||
yield();
|
yield();
|
||||||
@@ -191,8 +198,7 @@ void performAiMove(int8_t aiP)
|
|||||||
{
|
{
|
||||||
int8_t huP = (aiP == 1) ? 2 : 1;
|
int8_t huP = (aiP == 1) ? 2 : 1;
|
||||||
aiBrightness = 0;
|
aiBrightness = 0;
|
||||||
aiFadeUp = true; // Reset animation
|
aiFadeUp = true;
|
||||||
|
|
||||||
for (int c = 0; c < COLS; c++)
|
for (int c = 0; c < COLS; c++)
|
||||||
{
|
{
|
||||||
int r = getFirstEmptyRow(c);
|
int r = getFirstEmptyRow(c);
|
||||||
@@ -205,7 +211,7 @@ void performAiMove(int8_t aiP)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
board[c][r] = huP;
|
board[c][r] = huP;
|
||||||
if (scanBoard(huP))
|
if (current_look_ahead >= 2 && scanBoard(huP))
|
||||||
{
|
{
|
||||||
board[c][r] = aiP;
|
board[c][r] = aiP;
|
||||||
leds[getIdx(7, 0)] = CRGB::Black;
|
leds[getIdx(7, 0)] = CRGB::Black;
|
||||||
@@ -214,7 +220,6 @@ void performAiMove(int8_t aiP)
|
|||||||
board[c][r] = 0;
|
board[c][r] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int bestScore = -30000;
|
int bestScore = -30000;
|
||||||
int bestCol = 3;
|
int bestCol = 3;
|
||||||
for (int c : {3, 2, 4, 1, 5, 0, 6})
|
for (int c : {3, 2, 4, 1, 5, 0, 6})
|
||||||
@@ -223,7 +228,7 @@ void performAiMove(int8_t aiP)
|
|||||||
if (r != -1)
|
if (r != -1)
|
||||||
{
|
{
|
||||||
board[c][r] = aiP;
|
board[c][r] = aiP;
|
||||||
int score = minimax(LOOK_AHEAD, -30000, 30000, false, aiP, huP);
|
int score = minimax(current_look_ahead, -30000, 30000, false, aiP, huP);
|
||||||
board[c][r] = 0;
|
board[c][r] = 0;
|
||||||
if (score > bestScore)
|
if (score > bestScore)
|
||||||
{
|
{
|
||||||
@@ -233,9 +238,10 @@ void performAiMove(int8_t aiP)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
board[bestCol][getFirstEmptyRow(bestCol)] = aiP;
|
board[bestCol][getFirstEmptyRow(bestCol)] = aiP;
|
||||||
leds[getIdx(7, 0)] = CRGB::Black; // Clear thinking LED
|
leds[getIdx(7, 0)] = CRGB::Black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Menu UI with Restored Serifs ---
|
||||||
void showMenu()
|
void showMenu()
|
||||||
{
|
{
|
||||||
isDemoOver = false;
|
isDemoOver = false;
|
||||||
@@ -273,20 +279,73 @@ void showMenu()
|
|||||||
FastLED.show();
|
FastLED.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Web Portal ---
|
||||||
|
void handleRoot()
|
||||||
|
{
|
||||||
|
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>";
|
||||||
|
html += "<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 += "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 += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;font-weight:bold;'></form></div></body></html>";
|
||||||
|
server.send(200, "text/html", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSave()
|
||||||
|
{
|
||||||
|
if (server.hasArg("ply"))
|
||||||
|
{
|
||||||
|
current_look_ahead = server.arg("ply").toInt();
|
||||||
|
prefs.putUChar("ply", current_look_ahead);
|
||||||
|
}
|
||||||
|
if (server.hasArg("br"))
|
||||||
|
{
|
||||||
|
current_brightness = server.arg("br").toInt();
|
||||||
|
FastLED.setBrightness(current_brightness);
|
||||||
|
prefs.putUChar("br", current_brightness);
|
||||||
|
}
|
||||||
|
if (server.hasArg("idle"))
|
||||||
|
{
|
||||||
|
uint32_t s = server.arg("idle").toInt();
|
||||||
|
current_idle_timeout_ms = s * 1000;
|
||||||
|
prefs.putUInt("idle", s);
|
||||||
|
}
|
||||||
|
server.sendHeader("Location", "/");
|
||||||
|
server.send(303);
|
||||||
|
}
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
prefs.begin("c4-game", false);
|
||||||
|
current_look_ahead = prefs.getUChar("ply", 8);
|
||||||
|
current_brightness = prefs.getUChar("br", 25);
|
||||||
|
current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000;
|
||||||
|
|
||||||
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
|
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
|
||||||
FastLED.setBrightness(BRIGHTNESS);
|
FastLED.setBrightness(current_brightness);
|
||||||
pinMode(ENC_SW, INPUT_PULLUP);
|
pinMode(ENC_SW, INPUT_PULLUP);
|
||||||
|
|
||||||
|
WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
|
||||||
|
WiFi.softAP("Connect4-Config", WIFI_PASSWORD, 1, 0, 4); // SSID, Pass, Channel 1, Hidden 0, Max Connections 4
|
||||||
|
WiFi.softAP("Connect4-Config");
|
||||||
|
|
||||||
|
server.on("/", handleRoot);
|
||||||
|
server.on("/save", HTTP_POST, handleSave);
|
||||||
|
server.begin();
|
||||||
|
|
||||||
lastActivityTime = millis();
|
lastActivityTime = millis();
|
||||||
showMenu();
|
showMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
{
|
{
|
||||||
|
server.handleClient();
|
||||||
long newPos = myEnc.read() / SENSITIVITY;
|
long newPos = myEnc.read() / SENSITIVITY;
|
||||||
bool pressed = (digitalRead(ENC_SW) == LOW);
|
bool pressed = (digitalRead(ENC_SW) == LOW);
|
||||||
|
|
||||||
|
// Escape Demo / Interrupt
|
||||||
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
|
if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
|
||||||
{
|
{
|
||||||
if (gameState == DEMO || isDemoOver)
|
if (gameState == DEMO || isDemoOver)
|
||||||
@@ -297,9 +356,7 @@ void loop()
|
|||||||
FastLED.show();
|
FastLED.show();
|
||||||
delay(30);
|
delay(30);
|
||||||
}
|
}
|
||||||
FastLED.clear();
|
delay(2000);
|
||||||
FastLED.show();
|
|
||||||
delay(500);
|
|
||||||
gameState = MENU;
|
gameState = MENU;
|
||||||
memset(board, 0, sizeof(board));
|
memset(board, 0, sizeof(board));
|
||||||
showMenu();
|
showMenu();
|
||||||
@@ -333,7 +390,7 @@ void loop()
|
|||||||
}
|
}
|
||||||
delay(300);
|
delay(300);
|
||||||
}
|
}
|
||||||
if (millis() - lastActivityTime > IDLE_TIMEOUT)
|
if (millis() - lastActivityTime > current_idle_timeout_ms)
|
||||||
{
|
{
|
||||||
gameState = DEMO;
|
gameState = DEMO;
|
||||||
memset(board, 0, sizeof(board));
|
memset(board, 0, sizeof(board));
|
||||||
@@ -359,13 +416,9 @@ void loop()
|
|||||||
renderBoard();
|
renderBoard();
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
if (scanBoard(currentPlayer))
|
if (scanBoard(currentPlayer))
|
||||||
{
|
|
||||||
gameState = FINISHED_WIN;
|
gameState = FINISHED_WIN;
|
||||||
}
|
|
||||||
else if (isBoardFull())
|
else if (isBoardFull())
|
||||||
{
|
|
||||||
gameState = FINISHED_DRAW;
|
gameState = FINISHED_DRAW;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (menuMode < 2)
|
if (menuMode < 2)
|
||||||
@@ -380,10 +433,8 @@ void loop()
|
|||||||
gameState = FINISHED_WIN;
|
gameState = FINISHED_WIN;
|
||||||
}
|
}
|
||||||
else if (isBoardFull())
|
else if (isBoardFull())
|
||||||
{
|
|
||||||
gameState = FINISHED_DRAW;
|
gameState = FINISHED_DRAW;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
currentPlayer = (currentPlayer == 1) ? 2 : 1;
|
currentPlayer = (currentPlayer == 1) ? 2 : 1;
|
||||||
@@ -395,6 +446,7 @@ void loop()
|
|||||||
}
|
}
|
||||||
else if (gameState == DEMO)
|
else if (gameState == DEMO)
|
||||||
{
|
{
|
||||||
|
// No idle timeout check here to prevent premature restarts
|
||||||
renderBoard();
|
renderBoard();
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
delay(600);
|
delay(600);
|
||||||
@@ -418,6 +470,15 @@ void loop()
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Monitor for Idle in Win screen to return to Demo
|
||||||
|
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)
|
||||||
@@ -444,7 +505,8 @@ void loop()
|
|||||||
}
|
}
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
}
|
}
|
||||||
if (isDemoOver && (millis() - demoResetTimer > DEMO_RESET_PAUSE))
|
// Restart Demo loop if it was a demo game
|
||||||
|
if (isDemoOver && (millis() - demoResetTimer > 30000))
|
||||||
{
|
{
|
||||||
memset(board, 0, sizeof(board));
|
memset(board, 0, sizeof(board));
|
||||||
gameState = DEMO;
|
gameState = DEMO;
|
||||||
|
|||||||
Reference in New Issue
Block a user