diff --git a/.gitignore b/.gitignore index 89cc49c..457f27a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +.vscode/settings.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 369420d..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "cSpell.words": [ - "espressif", - "fastled", - "lolin", - "microcontroller", - "paulstoffregen" - ] -} \ No newline at end of file diff --git a/Background information.md b/Background information.md new file mode 100644 index 0000000..7f37826 --- /dev/null +++ b/Background information.md @@ -0,0 +1,72 @@ +# 📑 Technical Specification: Connect 4 AI Logic + +## 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._ + +--- + +## 2. Positional Evaluation + +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. + +### Scoring Heuristics + +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. + +--- + +## 3. Determining the Value of a "Half-Move" + +A "half-move" is a single disc placement by one player. Its value is determined via the **Minimax Algorithm** with **Alpha-Beta Pruning**. + +### The Recursive Search Process + +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. + +### Optimization: Alpha-Beta Pruning + +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. + +--- + +## 5. Summary of Logic Execution + +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. + +--- + +## References + +- [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) diff --git a/README.md b/README.md index 0d81643..df3d6e0 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,86 @@ -# Connect Four: ESP32-C3 LED Edition +# đŸ•šī¸ Connect 4 AI: Master Edition (v2.0) -A hardware-based Connect Four game featuring an 8x8 NeoPixel matrix, a strategic Minimax AI, and a dynamic "Attract Mode" for public display. - -## How the Program Works - -The program is built as a **Finite State Machine (FSM)**. -It manages the game flow by transitioning between distinct states based on user input, game outcomes, or inactivity timers. - -### 1. Game States - -- **Menu**: Displays stylized Roman numerals (**I** or **II**) to select between Single Player or Two Player modes. -- **Game Play**: The main loop handles the real-time gravity of falling discs, encoder tracking for column selection, and the hand-off between the human and the AI. -- **Game Over**: Triggered when a win or draw is detected. It "locks" the board, dims the background discs to 15% intensity, and flashes the winning line. -- **Demo Mode**: Triggered after 60 seconds of inactivity. The AI plays against itself to act as a visual "attract mode." - -### 2. Win Detection Logic - -To ensure 100% accuracy, the program performs a synchronous, multi-directional scan of the 7x6 grid after every single move. -It checks for four matching non-zero values in the following patterns: - -- **Horizontal**: `[column] [row]` to `[column + 3] [row]` -- **Vertical**: `[column] [row]` to `[column] [row + 3]` -- **Diagonal Up**: `[column] [row]` to `[column + 3] [row + 3]` -- **Diagonal Down**: `[column] [row]` to `[column + 3] [row - 3]` +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. --- -## The AI: Strategic Minimax +## 🛠 Hardware Configuration -The computer opponent uses the **Minimax Algorithm**, a classic artificial intelligence method for zero-sum games. +### 🔌 Pin Mapping (Lolin C3 Mini) -### 1. Look-Ahead (Depth Search) +| 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) | -The AI does not just look at the current board; it simulates the game **6 to 8 moves into the future**. -It explores a "tree" of possibilities: _"If I play here, and the player plays there, then I can play here..."_ +### 📐 Physical Layout -### 2. Alpha-Beta Pruning +The project is optimized for an 8x8 NeoPixel Matrix (65mm x 67mm). -Because searching millions of possibilities would be too slow for a microcontroller, we use **Alpha-Beta Pruning**. -This allows the AI to "prune" (ignore) branches of the game tree that are mathematically -guaranteed to be worse than moves it has already found, significantly speeding up the calculation. - -### 3. Immediate Threat Reaction - -To prevent the AI from being "distracted" by deep strategies while missing a simple win or loss, -we implemented a high-priority **Reaction Scanner**: - -- **Kill Move**: If the AI can win in exactly one move, it takes it immediately. -- **Block Move**: If the player is one move away from winning (3-in-a-row), the AI identifies the threat and blocks it regardless of the Minimax score. - -### 4. Controlled Randomness (Demo Mode) - -To keep the Demo Mode interesting for spectators, the AI has a 25% chance to ignore the "perfect" move and pick a random column. -This ensures that every demo game is unique and not a repetitive loop of the same strategy. +- **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. --- -## Technical Specifications +## 🧠 Advanced AI & Logic Features -### Hardware Pins (Lolin C3 Mini) +### 1. Progressive Difficulty (Evolution Mode) -| Component | Pin | Function | -| :---------- | :-- | :--------------------------------------- | -| **LED_PIN** | 4 | WS2812B NeoPixel Data | -| **ENC_A** | 0 | Rotary Encoder Phase A | -| **ENC_B** | 1 | Rotary Encoder Phase B | -| **ENC_SW** | 2 | Switch (Includes 50ms Software Debounce) | +To keep the game challenging and the CPU efficient, the AI search depth (Ply) scales as the board fills. -### NeoPixel Grid Layout +- **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. -The 8x8 matrix is mapped as follows: +### 2. Intelligent Win Detection & Flashing -- **Play Area**: 7 columns (0-6) by 6 rows (0-5). -- **Boundaries**: Row 1 and Column 7 are lit in **Blue** to mark the board limits. -- **Indicators**: The top-right pixel (7,0) pulses in the computer's color while it is "thinking." -- **Glowing Frame**: During Demo mode, the blue borders pulse with a white "glow" effect using a `beat8` sine wave to indicate autonomous play. +The win-engine has been refactored to prevent "color ghosting." + +- **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. 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. --- -## Controls & Interaction +## 📖 Code Architecture & Modules -- **Rotate Encoder**: Move the cursor (top row) to select a column. -- **Press Encoder Button**: Drop a disc. -- **Full Column Warning**: If you attempt to play in a full column, the selection disc will blink rapidly, and the move will be ignored. -- **Reset**: After a game ends, press the button once to return to the Menu. +### 🔄 State Machine -## Build Flags (platformio.ini) +The core loop manages five distinct states: -Tweak the game performance without changing the source code: +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. -- `IDLE_TIMEOUT`: Time (ms) before Demo Mode starts. -- `DEMO_RESET_PAUSE`: Delay (ms) between games in Demo Mode. -- `DEBOUNCE_DELAY`: Sensitivity of the encoder button. -- `BRIGHTNESS`: Global brightness of the NeoPixels. +### 🌐 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. +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. diff --git a/app.db b/app.db deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.cpp b/src/main.cpp index 97ebe83..7df2ca5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ const int COLS = 7; const int ROWS = 6; +// --- Configuration & Globals --- CRGB leds[NUM_LEDS]; Encoder myEnc(ENC_A, ENC_B); WebServer server(80); @@ -28,52 +29,47 @@ 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 activeCol = 3; long oldEncPos = -999; uint32_t lastActivityTime = 0; uint32_t demoResetTimer = 0; bool isDemoOver = false; +uint8_t demoPly = 4; -// Web-Configurable Parameters (Stored in Flash) uint8_t current_look_ahead; uint8_t current_brightness; uint32_t current_idle_timeout_ms; +bool blunder_enabled = false; +bool progressive_difficulty = false; -// Thinking Animation Helpers uint8_t aiBrightness = 0; bool aiFadeUp = true; -// --- Helper Functions --- -int getIdx(int x, int y) { return (y * 8) + x; } +// --- Function Prototypes --- +int getIdx(int x, int y); +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); +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); +void showMenu(); +int getDynamicPly(); -void updateThinkingLED(int8_t p) -{ - static uint32_t lastCycle = 0; - if (millis() - lastCycle < 20) - return; - lastCycle = millis(); - if (aiFadeUp) - { - aiBrightness += 15; - if (aiBrightness >= 240) - aiFadeUp = false; - } - else - { - aiBrightness -= 15; - if (aiBrightness <= 15) - aiFadeUp = true; - } - CRGB compColor = (p == 1) ? CRGB::Yellow : CRGB::Red; - leds[getIdx(7, 0)] = compColor.nscale8(aiBrightness); - FastLED.show(); -} +// --- Utility & Rendering --- + +int getIdx(int x, int y) { return (y * 8) + x; } void drawStaticUI() { FastLED.clear(); CRGB borderColor = CRGB::Blue; - if (gameState == DEMO || (gameState >= 2 && isDemoOver)) + if (gameState == DEMO || gameState >= 2) { uint8_t glow = beat8(15); borderColor = blend(CRGB::Blue, CRGB::White, glow / 4); @@ -109,6 +105,80 @@ int getFirstEmptyRow(int col) return -1; } +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); +} + +// --- Visuals & Animations --- + +void updateThinkingVisuals(int8_t p, int8_t col) +{ + static uint32_t lastCycle = 0; + if (millis() - lastCycle < 25) + return; + lastCycle = millis(); + if (aiFadeUp) + { + aiBrightness += 15; + if (aiBrightness >= 240) + aiFadeUp = false; + } + else + { + aiBrightness -= 15; + if (aiBrightness <= 15) + aiFadeUp = true; + } + 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); + FastLED.show(); +} + +void animateDrop(int col, int player) +{ + int targetRow = getFirstEmptyRow(col); + if (targetRow == -1) + return; + for (int r = 5; r >= targetRow; r--) + { + renderBoard(); + leds[getIdx(col, 7 - r)] = (player == 1) ? CRGB::Yellow : CRGB::Red; + FastLED.show(); + delay(max(20, 80 - (5 - r) * 15)); + } + board[col][targetRow] = player; + renderBoard(); + FastLED.show(); +} + +void moveDiscToCol(int startCol, int targetCol, int player, int speed) +{ + int current = startCol; + CRGB pColor = (player == 1) ? CRGB::Yellow : CRGB::Red; + while (current != targetCol) + { + leds[getIdx(current, 0)] = CRGB::Black; + current += (targetCol > current) ? 1 : -1; + renderBoard(); + leds[getIdx(current, 0)] = pColor; + FastLED.show(); + delay(speed); + } + activeCol = targetCol; +} + +// --- AI Engine --- + bool isBoardFull() { for (int c = 0; c < COLS; c++) @@ -117,74 +187,85 @@ bool isBoardFull() return true; } -bool scanBoard(int8_t p) +int8_t scanBoard() { memset(winMask, 0, sizeof(winMask)); - bool found = false; - auto check = [&](int c, int r, int dc, int dr) + auto check = [&](int column, int row, int columnOffset, int rowOffset) { - if (board[c][r] == p && board[c + dc][r + dr] == p && board[c + 2 * dc][r + 2 * dr] == p && board[c + 3 * dc][r + 3 * dr] == p) + 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) { - for (int i = 0; i < 4; i++) - winMask[getIdx(c + i * dc, 7 - (r + i * dr))] = true; - return true; + for (int index = 0; index < 4; index++) + winMask[getIdx(column + index * columnOffset, 7 - (row + index * rowOffset))] = true; + return postion; } - return false; + return (int8_t)0; }; - for (int r = 0; r < 6; r++) - for (int c = 0; c < 4; c++) - if (check(c, r, 1, 0)) - found = true; - for (int r = 0; r < 3; r++) - for (int c = 0; c < 7; c++) - if (check(c, r, 0, 1)) - found = true; - for (int r = 0; r < 3; r++) - for (int c = 0; c < 4; c++) - if (check(c, r, 1, 1)) - found = true; - for (int r = 3; r < 6; r++) - for (int c = 0; c < 4; c++) - if (check(c, r, 1, -1)) - found = true; - return found; + for (int row = 0; row < 6; row++) + for (int column = 0; column < 4; column++) + { + int8_t res = check(column, row, 1, 0); + if (res) + return res; + } + for (int row = 0; row < 3; row++) + for (int column = 0; column < 7; column++) + { + int8_t res = check(column, row, 0, 1); + if (res) + return res; + } + for (int row = 0; row < 3; row++) + for (int column = 0; column < 4; column++) + { + int8_t res = check(column, row, 1, 1); + if (res) + return res; + } + for (int row = 3; row < 6; row++) + for (int column = 0; column < 4; column++) + { + int8_t res = check(column, row, 1, -1); + if (res) + return res; + } + return 0; } -// --- 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 aiPlayer, int8_t humanPlayer, int8_t rootCol) { if (depth >= current_look_ahead - 1) - updateThinkingLED(aiP); + updateThinkingVisuals(aiPlayer, rootCol); else yield(); - if (scanBoard(aiP)) + // Check for wins within minimax + int8_t win = scanBoard(); + if (win == aiPlayer) return 1000 + depth; - if (scanBoard(huP)) + if (win == humanPlayer) return -1000 - depth; if (depth == 0 || isBoardFull()) return 0; int order[] = {3, 2, 4, 1, 5, 0, 6}; int best = isMax ? -2000 : 2000; - for (int c : order) + for (int column : order) { - int r = getFirstEmptyRow(c); - if (r != -1) + int row = getFirstEmptyRow(column); + if (row != -1) { - board[c][r] = isMax ? aiP : huP; - int val = minimax(depth - 1, alpha, beta, !isMax, aiP, huP); - board[c][r] = 0; + board[column][row] = isMax ? aiPlayer : humanPlayer; + int val = minimax(depth - 1, alpha, beta, !isMax, aiPlayer, humanPlayer, (depth == current_look_ahead ? column : rootCol)); + board[column][row] = 0; if (isMax) { - if (val > best) - best = val; + best = max(best, val); alpha = max(alpha, best); } else { - if (val < best) - best = val; + best = min(best, val); beta = min(beta, best); } if (beta <= alpha) @@ -194,54 +275,109 @@ int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP) return best; } -void performAiMove(int8_t aiP) +void performAiMove(int8_t aiPlayer) { - int8_t huP = (aiP == 1) ? 2 : 1; - aiBrightness = 0; - aiFadeUp = true; - for (int c = 0; c < COLS; c++) - { - int r = getFirstEmptyRow(c); - if (r != -1) - { - board[c][r] = aiP; - if (scanBoard(aiP)) - { - leds[getIdx(7, 0)] = CRGB::Black; - return; - } - board[c][r] = huP; - if (current_look_ahead >= 2 && scanBoard(huP)) - { - board[c][r] = aiP; - leds[getIdx(7, 0)] = CRGB::Black; - return; - } - board[c][r] = 0; - } - } + int humanPlayer = (aiPlayer == 1) ? 2 : 1; int bestScore = -30000; int bestCol = 3; - for (int c : {3, 2, 4, 1, 5, 0, 6}) + int originalPly = current_look_ahead; + current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly(); + + for (int column = 0; column < COLS; column++) { - int r = getFirstEmptyRow(c); - if (r != -1) + int row = getFirstEmptyRow(column); + if (row != -1) { - board[c][r] = aiP; - int score = minimax(current_look_ahead, -30000, 30000, false, aiP, huP); - board[c][r] = 0; + board[column][row] = aiPlayer; + if (scanBoard() == 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; + } + board[column][row] = 0; + } + } + for (int column : {3, 2, 4, 1, 5, 0, 6}) + { + int row = getFirstEmptyRow(column); + if (row != -1) + { + board[column][row] = aiPlayer; + int score = minimax(current_look_ahead, -30000, 30000, false, aiPlayer, humanPlayer, column); + board[column][row] = 0; if (score > bestScore) { bestScore = score; - bestCol = c; + bestCol = column; } } } - board[bestCol][getFirstEmptyRow(bestCol)] = aiP; - leds[getIdx(7, 0)] = CRGB::Black; + 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 --- + +void handleRoot() +{ + String html = "" + ""; + html += "

Connect 4 Admin

"; + html += "Base AI Ply (1-10):"; + html += "Brightness (5-255):"; + html += "Idle Timeout (Sec):"; + html += "Enable Blunders:
"; + html += "Evolution Mode:

"; + 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); + } + blunder_enabled = server.hasArg("blunder"); + prefs.putBool("blunder", blunder_enabled); + progressive_difficulty = server.hasArg("evolve"); + prefs.putBool("evolve", progressive_difficulty); + server.sendHeader("Location", "/"); + server.send(303); } -// --- Menu UI with Restored Serifs --- void showMenu() { isDemoOver = false; @@ -279,42 +415,6 @@ void showMenu() FastLED.show(); } -// --- Web Portal --- -void handleRoot() -{ - String html = ""; - html += ""; - html += "

Connect 4 Admin

"; - html += "AI Ply (1-10):"; - html += "Brightness (5-255):"; - html += "Idle Timeout (Sec):"; - 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() { Serial.begin(115200); @@ -322,29 +422,16 @@ void setup() current_look_ahead = prefs.getUChar("ply", 8); current_brightness = prefs.getUChar("br", 25); current_idle_timeout_ms = prefs.getUInt("idle", 60) * 1000; + blunder_enabled = prefs.getBool("blunder", false); + progressive_difficulty = prefs.getBool("evolve", false); FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(current_brightness); pinMode(ENC_SW, INPUT_PULLUP); - - WiFi.disconnect(true); // Clear old settings - WiFi.mode(WIFI_AP); // Force Access Point mode - delay(100); // Give the radio a moment to reset - - // SSID, Password (MUST be 8+ chars), Channel, Hidden (0=No), Max Clients - if (WiFi.softAP("Connect4-Config", WIFI_PASSWORD, 1, 0, 4)) - { - Serial.println("WPA2 AP Started Successfully"); - } - else - { - Serial.println("AP Failed - Check if WIFI_PASSWORD is at least 8 characters!"); - } - + WiFi.softAP("Connect4-Config", WIFI_PASSWORD); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.begin(); - lastActivityTime = millis(); showMenu(); } @@ -355,18 +442,17 @@ void loop() long newPos = myEnc.read() / SENSITIVITY; bool pressed = (digitalRead(ENC_SW) == LOW); - // Escape Demo / Interrupt if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500))) { - if (gameState == DEMO || isDemoOver) + if (gameState >= 2 || gameState == DEMO) { - for (int i = 0; i < 10; i++) + for (int index = 0; index < 10; index++) { fadeToBlackBy(leds, NUM_LEDS, 32); FastLED.show(); delay(30); } - delay(2000); + delay(500); gameState = MENU; memset(board, 0, sizeof(board)); showMenu(); @@ -377,6 +463,16 @@ void loop() lastActivityTime = millis(); } + uint32_t activeLimit = (gameState == PLAYING) ? (current_idle_timeout_ms * 2) : current_idle_timeout_ms; + if (gameState != DEMO && (gameState < 2) && (millis() - lastActivityTime > activeLimit)) + { + gameState = DEMO; + memset(board, 0, sizeof(board)); + currentPlayer = 1; + demoPly = random(3, 7); + return; + } + if (gameState == MENU) { if (newPos != oldEncPos) @@ -400,12 +496,6 @@ void loop() } delay(300); } - if (millis() - lastActivityTime > current_idle_timeout_ms) - { - gameState = DEMO; - memset(board, 0, sizeof(board)); - currentPlayer = 1; - } } else if (gameState == PLAYING) { @@ -422,28 +512,35 @@ void loop() int row = getFirstEmptyRow(activeCol); if (row != -1) { - board[activeCol][row] = currentPlayer; - renderBoard(); - FastLED.show(); - if (scanBoard(currentPlayer)) + animateDrop(activeCol, currentPlayer); + winnerPlayer = scanBoard(); + if (winnerPlayer != 0) + { gameState = FINISHED_WIN; + demoResetTimer = millis(); + } else if (isBoardFull()) + { gameState = FINISHED_DRAW; + demoResetTimer = millis(); + } else { if (menuMode < 2) { - int8_t aiP = (menuMode == 0) ? 2 : 1; - performAiMove(aiP); - renderBoard(); - FastLED.show(); - if (scanBoard(aiP)) + int8_t aiPlayer = (menuMode == 0) ? 2 : 1; + performAiMove(aiPlayer); + winnerPlayer = scanBoard(); + if (winnerPlayer != 0) { - currentPlayer = aiP; gameState = FINISHED_WIN; + demoResetTimer = millis(); } else if (isBoardFull()) + { gameState = FINISHED_DRAW; + demoResetTimer = millis(); + } } else { @@ -456,21 +553,19 @@ void loop() } else if (gameState == DEMO) { - // No idle timeout check here to prevent premature restarts renderBoard(); FastLED.show(); delay(600); performAiMove(currentPlayer); - if (scanBoard(currentPlayer)) + winnerPlayer = scanBoard(); + if (winnerPlayer != 0) { gameState = FINISHED_WIN; - isDemoOver = true; demoResetTimer = millis(); } else if (isBoardFull()) { gameState = FINISHED_DRAW; - isDemoOver = true; demoResetTimer = millis(); } else @@ -479,16 +574,7 @@ void loop() } } 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; - } - + { // FINISHED state static uint32_t lastFlash = 0; static bool toggle = true; if (millis() - lastFlash > 300) @@ -503,9 +589,9 @@ void loop() if (gameState == FINISHED_WIN) { if (winMask[i]) - leds[i] = toggle ? (currentPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; + leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; else - leds[i].nscale8(40); + leds[i].nscale8(60); } else if (gameState == FINISHED_DRAW) { @@ -515,12 +601,12 @@ void loop() } FastLED.show(); } - // Restart Demo loop if it was a demo game - if (isDemoOver && (millis() - demoResetTimer > 30000)) + if (millis() - demoResetTimer > 15000) { memset(board, 0, sizeof(board)); gameState = DEMO; - isDemoOver = false; + demoResetTimer = 0; + demoPly = random(3, 7); } if (pressed) { diff --git a/uv.lock b/uv.lock deleted file mode 100644 index d603d37..0000000 --- a/uv.lock +++ /dev/null @@ -1,302 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.14" - -[[package]] -name = "bitarray" -version = "3.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/06/92fdc84448d324ab8434b78e65caf4fb4c6c90b4f8ad9bdd4c8021bfaf1e/bitarray-3.8.0.tar.gz", hash = "sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d", size = 151991, upload-time = "2025-11-02T21:41:15.117Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/b0/411327a6c7f6b2bead64bb06fe60b92e0344957ec1ab0645d5ccc25fdafe/bitarray-3.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89", size = 148563, upload-time = "2025-11-02T21:40:01.006Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bc/ff80d97c627d774f879da0ea93223adb1267feab7e07d5c17580ffe6d632/bitarray-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310", size = 145422, upload-time = "2025-11-02T21:40:02.535Z" }, - { url = "https://files.pythonhosted.org/packages/66/e7/b4cb6c5689aacd0a32f3aa8a507155eaa33528c63de2f182b60843fbf700/bitarray-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c", size = 332852, upload-time = "2025-11-02T21:40:03.645Z" }, - { url = "https://files.pythonhosted.org/packages/e7/91/fbd1b047e3e2f4b65590f289c8151df1d203d75b005f5aae4e072fe77d76/bitarray-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d", size = 360801, upload-time = "2025-11-02T21:40:04.827Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4a/63064c593627bac8754fdafcb5343999c93ab2aeb27bcd9d270a010abea5/bitarray-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5", size = 371408, upload-time = "2025-11-02T21:40:05.985Z" }, - { url = "https://files.pythonhosted.org/packages/46/97/ddc07723767bdafd170f2ff6e173c940fa874192783ee464aa3c1dedf07d/bitarray-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2", size = 340033, upload-time = "2025-11-02T21:40:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1e/e1ea9f1146fd4af032817069ff118918d73e5de519854ce3860e2ed560ff/bitarray-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe", size = 330774, upload-time = "2025-11-02T21:40:08.496Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9f/8242296c124a48d1eab471fd0838aeb7ea9c6fd720302d99ab7855d3e6d3/bitarray-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11", size = 358337, upload-time = "2025-11-02T21:40:10.035Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6b/9095d75264c67d479f298c80802422464ce18c3cdd893252eeccf4997611/bitarray-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7", size = 355639, upload-time = "2025-11-02T21:40:11.485Z" }, - { url = "https://files.pythonhosted.org/packages/a0/af/c93c0ae5ef824136e90ac7ddf6cceccb1232f34240b2f55a922f874da9b4/bitarray-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860", size = 336999, upload-time = "2025-11-02T21:40:12.709Z" }, - { url = "https://files.pythonhosted.org/packages/81/0f/72c951f5997b2876355d5e671f78dd2362493254876675cf22dbd24389ae/bitarray-3.8.0-cp314-cp314-win32.whl", hash = "sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25", size = 142169, upload-time = "2025-11-02T21:40:14.031Z" }, - { url = "https://files.pythonhosted.org/packages/8a/55/ef1b4de8107bf13823da8756c20e1fbc9452228b4e837f46f6d9ddba3eb3/bitarray-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4", size = 148737, upload-time = "2025-11-02T21:40:15.436Z" }, - { url = "https://files.pythonhosted.org/packages/5f/26/bc0784136775024ac56cc67c0d6f9aa77a7770de7f82c3a7c9be11c217cd/bitarray-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e", size = 146083, upload-time = "2025-11-02T21:40:17.135Z" }, - { url = "https://files.pythonhosted.org/packages/6e/64/57984e64264bf43d93a1809e645972771566a2d0345f4896b041ce20b000/bitarray-3.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521", size = 149455, upload-time = "2025-11-02T21:40:18.558Z" }, - { url = "https://files.pythonhosted.org/packages/81/c0/0d5f2eaef1867f462f764bdb07d1e116c33a1bf052ea21889aefe4282f5b/bitarray-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa", size = 146491, upload-time = "2025-11-02T21:40:19.665Z" }, - { url = "https://files.pythonhosted.org/packages/65/c6/bc1261f7a8862c0c59220a484464739e52235fd1e2afcb24d7f7d3fb5702/bitarray-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8", size = 339721, upload-time = "2025-11-02T21:40:21.277Z" }, - { url = "https://files.pythonhosted.org/packages/81/d8/289ca55dd2939ea17b1108dc53bffc0fdc5160ba44f77502dfaae35d08c6/bitarray-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3", size = 367823, upload-time = "2025-11-02T21:40:22.463Z" }, - { url = "https://files.pythonhosted.org/packages/91/a2/61e7461ca9ac0fcb70f327a2e84b006996d2a840898e69037a39c87c6d06/bitarray-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df", size = 377341, upload-time = "2025-11-02T21:40:23.789Z" }, - { url = "https://files.pythonhosted.org/packages/6c/87/4a0c9c8bdb13916d443e04d8f8542eef9190f31425da3c17c3478c40173f/bitarray-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f", size = 344985, upload-time = "2025-11-02T21:40:25.261Z" }, - { url = "https://files.pythonhosted.org/packages/17/4c/ff9259b916efe53695b631772e5213699c738efc2471b5ffe273f4000994/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8", size = 336796, upload-time = "2025-11-02T21:40:26.942Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4b/51b2468bbddbade5e2f3b8d5db08282c5b309e8687b0f02f75a8b5ff559c/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8", size = 365085, upload-time = "2025-11-02T21:40:28.224Z" }, - { url = "https://files.pythonhosted.org/packages/bf/79/53473bfc2e052c6dbb628cdc1b156be621c77aaeb715918358b01574be55/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773", size = 361012, upload-time = "2025-11-02T21:40:29.635Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b1/242bf2e44bfc69e73fa2b954b425d761a8e632f78ea31008f1c3cfad0854/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9", size = 340644, upload-time = "2025-11-02T21:40:31.089Z" }, - { url = "https://files.pythonhosted.org/packages/cf/01/12e5ecf30a5de28a32485f226cad4b8a546845f65f755ce0365057ab1e92/bitarray-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149", size = 143630, upload-time = "2025-11-02T21:40:32.351Z" }, - { url = "https://files.pythonhosted.org/packages/b6/92/6b6ade587b08024a8a890b07724775d29da9cf7497be5c3cbe226185e463/bitarray-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e", size = 150250, upload-time = "2025-11-02T21:40:33.596Z" }, - { url = "https://files.pythonhosted.org/packages/ed/40/be3858ffed004e47e48a2cefecdbf9b950d41098b780f9dc3aa609a88351/bitarray-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f", size = 147015, upload-time = "2025-11-02T21:40:35.064Z" }, -] - -[[package]] -name = "bitstring" -version = "4.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bitarray" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/15/a8/a80c890db75d5bdd5314b5de02c4144c7de94fd0cefcae51acaeb14c6a3f/bitstring-4.3.1.tar.gz", hash = "sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a", size = 251426, upload-time = "2025-03-22T09:39:06.978Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/2d/174566b533755ddf8efb32a5503af61c756a983de379f8ad3aed6a982d38/bitstring-4.3.1-py3-none-any.whl", hash = "sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a", size = 71930, upload-time = "2025-03-22T09:39:05.163Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "connect-four" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "esptool" }, -] - -[package.metadata] -requires-dist = [{ name = "esptool", specifier = ">=5.2.0" }] - -[[package]] -name = "cryptography" -version = "46.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, -] - -[[package]] -name = "esptool" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bitstring" }, - { name = "click" }, - { name = "cryptography" }, - { name = "intelhex" }, - { name = "pyserial" }, - { name = "pyyaml" }, - { name = "reedsolo" }, - { name = "rich-click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/25/7b50d81a66f600a60f23258fa134201e97e854271b478ca4e21e9f694355/esptool-5.2.0.tar.gz", hash = "sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6", size = 463000, upload-time = "2026-02-18T16:10:52.641Z" } - -[[package]] -name = "intelhex" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/37/1e7522494557d342a24cb236e2aec5d078fac8ed03ad4b61372586406b01/intelhex-2.3.0.tar.gz", hash = "sha256:892b7361a719f4945237da8ccf754e9513db32f5628852785aea108dcd250093", size = 44513, upload-time = "2020-10-20T20:35:51.526Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/78/79461288da2b13ed0a13deb65c4ad1428acb674b95278fa9abf1cefe62a2/intelhex-2.3.0-py2.py3-none-any.whl", hash = "sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4", size = 50914, upload-time = "2020-10-20T20:35:50.162Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pyserial" -version = "3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "reedsolo" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/61/a67338cbecf370d464e71b10e9a31355f909d6937c3a8d6b17dd5d5beb5e/reedsolo-1.7.0.tar.gz", hash = "sha256:c1359f02742751afe0f1c0de9f0772cc113835aa2855d2db420ea24393c87732", size = 59723, upload-time = "2023-01-17T05:10:19.733Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/19/1bb346c0e581557c88946d2bb979b2bee8992e72314cfb418b5440e383db/reedsolo-1.7.0-py3-none-any.whl", hash = "sha256:2b6a3e402a1ee3e1eea3f932f81e6c0b7bbc615588074dca1dbbcdeb055002bd", size = 32360, upload-time = "2023-01-17T05:10:17.652Z" }, -] - -[[package]] -name = "rich" -version = "14.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, -] - -[[package]] -name = "rich-click" -version = "1.9.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/27/091e140ea834272188e63f8dd6faac1f5c687582b687197b3e0ec3c78ebf/rich_click-1.9.7.tar.gz", hash = "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc", size = 74838, upload-time = "2026-01-31T04:29:27.707Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491, upload-time = "2026-01-31T04:29:26.777Z" }, -]