Compare commits

...

12 Commits

10 changed files with 698 additions and 524 deletions
+1
View File
@@ -3,3 +3,4 @@
.vscode/c_cpp_properties.json .vscode/c_cpp_properties.json
.vscode/launch.json .vscode/launch.json
.vscode/ipch .vscode/ipch
.vscode/settings.json
-1
View File
@@ -1 +0,0 @@
3.14
+72
View File
@@ -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)
+76 -28
View File
@@ -1,38 +1,86 @@
# Connect Four # 🕹️ Connect 4 AI: Master Edition (v2.0)
Connect Four is a two player game played on a 7 by 6 grid. Each player has a color: one player is red, and the other player is yellow. The first player starts Connect Four by dropping one of their yellow discs into the (center) column of an empty game board. The two players then alternate turns dropping one of their discs at a time into an unfilled column, until the second player, with red discs, achieves a diagonal four in a row, and wins the game. If the board fills up before either player achieves four in a row, then the game is a draw. 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.
## Technical specifications ---
The game board is an 8 x 8 NeoPixel grid. To mark the limits of the board, the top row is off, in the row below the pixels are blue, and in the rightmost column's pixels are blue from the row below the empty row until the bottom row. ## 🛠 Hardware Configuration
The game consists of a rotary encoder, using the Encoder.h library connected to pins:
```c++
#define ENCODER_A 2
#define ENCODER_B 3
#define ENCODER_SW 4
```
The grid is managed using the FastLED.h library and is connected to: ### 🔌 Pin Mapping (Lolin C3 Mini)
```c++
#define LED_PIN 6
#define LED_WIDTH 8
#define LED_HEIGHT 8
#define NUM_LEDS (LED_WIDTH * LED_HEIGHT)
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
```
Players play by turning the rotary encoder to choose a column by rotating the encoder. When they press the button, the disc is dropped and falls to the lowest free position in the grid. If a column is full and a player tries to drop a disc, the disc at the top (column selection row) blinks, indicating that no disc can be dropped. If the player rotates the encoder to a non-full column, a disc can be played. | 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) |
A win is when one of the players achieves four connecting discs of the same color: horizontal, vertical, or diagonal. If a player wins, all the discs on the board a dimmed to 15% intensity, and the winning four discs are blinking at high intensity. ### 📐 Physical Layout
If the board is full and none of the players win, a draw, then all the discs on the board are dimmed, and a blinking animation indicates a draw. The project is optimized for an 8x8 NeoPixel Matrix (65mm x 67mm).
The game has four states: - **Row 0:** Interaction & AI Decision Visualization.
1. Menu: Here the players choose the type of game they want to play, 1 or 2 player. This is indicated on the board by a single yellow vertical bar in the center column (single player versus computer) or two vertical bars: two player game. Rotating the encoder switches between game modes, and pressing the button selects the game mode. - **Row 1:** Static Blue UI border.
2. Game play: Players are playing the game until one of the players connects four or they achieve a draw. - **Rows 2-7:** Active $7 \times 6$ game board.
3. Game over: One of the players connects four, or they achieve a draw. If the rotary encoder button is pressed, this state switches to the Menu state. - **Status Column:** Far right column (Index 7) manages UI framing and "Glow" effects.
4. Demo mode: The computer plays against itself. This mode is automatically triggered if there is no input for 60 seconds. If the rotary encoder is turned or pushed, the demo mode exits and the game returns to the menu state.
The program initializes the SerialPrint output (baud 115200) and outputs useful (debugging) information regarding the game state and selections. ---
## 🧠 Advanced AI & Logic Features
### 1. Progressive Difficulty (Evolution Mode)
To keep the game challenging and the CPU efficient, the AI search depth (Ply) scales as the board fills.
- **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.
### 2. Intelligent Win Detection & Flashing
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.
---
## 📖 Code Architecture & Modules
### 🔄 State Machine
The core loop manages five distinct states:
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.
### 🌐 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.
View File
-6
View File
@@ -1,6 +0,0 @@
def main():
print("Hello from connect-four!")
if __name__ == "__main__":
main()
+9 -1
View File
@@ -9,9 +9,17 @@ build_flags =
-D LED_PIN=4 -D LED_PIN=4
-D ENC_A=0 -D ENC_A=0
-D ENC_B=1 -D ENC_B=1
-D ENC_SW=6 -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 DEMO_RESET_PAUSE=20000
-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
-9
View File
@@ -1,9 +0,0 @@
[project]
name = "connect-four"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"esptool>=5.2.0",
]
+540 -177
View File
@@ -1,278 +1,641 @@
#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>
// 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;
const int LOOK_AHEAD = 6; // Depth 6 is very stable and tough for C3
// --- Configuration & Globals ---
CRGB leds[NUM_LEDS]; CRGB leds[NUM_LEDS];
Encoder *myEnc; 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];
enum State { MENU, PLAYING, FINISHED_WIN, FINISHED_DRAW }; enum State
{
MENU,
PLAYING,
FINISHED_WIN,
FINISHED_DRAW,
DEMO
};
State gameState = MENU; State gameState = MENU;
int8_t menuMode = 0; // 0: P1-Yellow, 1: P1-Red, 2: PvP int8_t menuMode = 0;
int8_t currentPlayer = 1; // 1: Yellow, 2: Red int8_t currentPlayer = 1;
int8_t winnerPlayer = 0;
int8_t activeCol = 3; int8_t activeCol = 3;
long oldEncPos = -999; long oldEncPos = -999;
uint32_t lastActivityTime = 0;
uint32_t demoResetTimer = 0;
bool isDemoOver = false;
uint8_t demoPly = 4;
uint8_t current_look_ahead;
uint8_t current_brightness;
uint32_t current_idle_timeout_ms;
bool blunder_enabled = false;
bool progressive_difficulty = false;
uint8_t aiBrightness = 0; uint8_t aiBrightness = 0;
bool aiFadeUp = true; bool aiFadeUp = true;
// --- Helper Functions --- // --- Function Prototypes ---
int getIdx(int x, int y);
void drawStaticUI();
void renderBoard();
int getFirstEmptyRow(int col);
bool isBoardFull();
int8_t scanBoard();
void updateThinkingVisuals(int8_t playerColor, int8_t column);
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 aiPlayer, int8_t humanPlayer, int8_t rootCol);
void performAiMove(int8_t aiPlayer);
void showMenu();
int getDynamicPly();
// --- Utility & Rendering ---
int getIdx(int x, int y) { return (y * 8) + x; } int getIdx(int x, int y) { return (y * 8) + x; }
void drawStaticUI() { void drawStaticUI()
{
FastLED.clear(); FastLED.clear();
for(int x = 0; x < 7; x++) leds[getIdx(x, 1)] = CRGB::Blue; #if SHOW_BORDER == 1
for(int y = 1; y < 8; y++) leds[getIdx(7, y)] = CRGB::Blue; CRGB borderColor = CRGB::Blue;
if (gameState == DEMO || gameState >= 2)
{
uint8_t glow = beat8(15);
borderColor = blend(CRGB::Blue, CRGB::White, glow / 4);
}
for (int x = 0; x < 7; x++)
leds[getIdx(x, 1)] = borderColor;
for (int y = 1; y < 8; y++)
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++) { {
if(board[c][r] == 1) leds[getIdx(c, 7-r)] = CRGB::Yellow; for (int row = 0; row < ROWS; row++)
if(board[c][r] == 2) leds[getIdx(c, 7-r)] = CRGB::Red; {
if (board[column][row] == 1)
leds[getIdx(column, 7 - row)] = CRGB::Yellow;
if (board[column][row] == 2)
leds[getIdx(column, 7 - row)] = CRGB::Red;
} }
} }
} }
int getFirstEmptyRow(int col) { int getFirstEmptyRow(int col)
for (int r = 0; r < ROWS; r++) { if (board[col][r] == 0) return r; } {
for (int row = 0; row < ROWS; row++)
{
if (board[col][row] == 0)
return row;
}
return -1; return -1;
} }
bool isBoardFull() { int getDynamicPly()
for (int c = 0; c < COLS; c++) if (board[c][5] == 0) return false; {
if (!progressive_difficulty && gameState != DEMO)
return current_look_ahead;
int occupiedCount = 0;
for (int column = 0; column < COLS; column++)
for (int row = 0; row < ROWS; row++)
if (board[column][row] != 0)
occupiedCount++;
return constrain(current_look_ahead + (occupiedCount / 7), 1, 10);
}
// --- Visuals & Animations ---
void updateThinkingVisuals(int8_t playerColor, int8_t column)
{
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 = (playerColor == 1) ? CRGB::Yellow : CRGB::Red;
leds[getIdx(column, 0)] = aiColor.nscale8(aiBrightness);
FastLED.show();
}
void animateDrop(int col, int player)
{
int targetRow = getFirstEmptyRow(col);
if (targetRow == -1)
return;
for (int row = 5; row >= targetRow; row--)
{
renderBoard();
leds[getIdx(col, 7 - row)] = (player == 1) ? CRGB::Yellow : CRGB::Red;
FastLED.show();
delay(max(20, 80 - (5 - row) * 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 column = 0; column < COLS; column++)
if (board[column][5] == 0)
return false;
return true; return true;
} }
// Scans board and fills winMask if 4+ connected int8_t scanBoard()
bool scanBoard(int8_t p) { {
bool found = false;
memset(winMask, 0, sizeof(winMask)); memset(winMask, 0, sizeof(winMask));
auto checkLine = [&](int x, int y, int dx, int dy) { auto checkMatch = [&](int col, int row, int dCol, int dRow)
int count = 0; {
for (int i = 0; i < 7; i++) { int8_t playerAtPos = board[col][row];
int nx = x + i * dx; int ny = y + i * dy; if (playerAtPos != 0 &&
if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && board[nx][ny] == p) { board[col + dCol][row + dRow] == playerAtPos &&
count++; board[col + 2 * dCol][row + 2 * dRow] == playerAtPos &&
} else { board[col + 3 * dCol][row + 3 * dRow] == playerAtPos)
if (count >= 4) { {
for (int j = 1; j <= count; j++) for (int i = 0; i < 4; i++)
winMask[getIdx(nx - j * dx, 7 - (ny - j * dy))] = true; winMask[getIdx(col + i * dCol, 7 - (row + i * dRow))] = true;
found = true; return playerAtPos;
}
count = 0;
}
} }
return (int8_t)0;
}; };
for (int i = 0; i < ROWS; i++) checkLine(0, i, 1, 0);
for (int i = 0; i < COLS; i++) checkLine(i, 0, 0, 1); for (int row = 0; row < 6; row++)
for (int i = -5; i < 7; i++) { checkLine(i, 0, 1, 1); checkLine(i, 5, 1, -1); } for (int col = 0; col < 4; col++)
return found; {
int8_t result = checkMatch(col, row, 1, 0);
if (result)
return result;
}
for (int row = 0; row < 3; row++)
for (int col = 0; col < 7; col++)
{
int8_t result = checkMatch(col, row, 0, 1);
if (result)
return result;
}
for (int row = 0; row < 3; row++)
for (int col = 0; col < 4; col++)
{
int8_t result = checkMatch(col, row, 1, 1);
if (result)
return result;
}
for (int row = 3; row < 6; row++)
for (int col = 0; col < 4; col++)
{
int8_t result = checkMatch(col, row, 1, -1);
if (result)
return result;
}
return 0;
} }
// --- AI Thinking Visualization --- int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiPlayer, int8_t humanPlayer, int8_t rootCol)
void updateThinkingLED() { {
static uint32_t lastCycle = 0; if (depth >= current_look_ahead - 1)
if (millis() - lastCycle < 20) return; updateThinkingVisuals(aiPlayer, rootCol);
lastCycle = millis(); else
if (aiFadeUp) { aiBrightness += 15; if (aiBrightness >= 240) aiFadeUp = false; } yield();
else { aiBrightness -= 15; if (aiBrightness <= 15) aiFadeUp = true; }
// Pulse in the computer's color
CRGB compColor = (menuMode == 0) ? CRGB::Red : CRGB::Yellow;
leds[getIdx(7, 0)] = compColor.nscale8(aiBrightness);
FastLED.show();
}
// --- Minimax Logic --- // Check winner via temporary scan (logic check only)
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP) { int8_t winner = scanBoard();
// Check wins within the simulation if (winner == aiPlayer)
if (scanBoard(aiP)) return 1000 + depth; return 1000 + depth;
if (scanBoard(huP)) return -1000 - depth; if (winner == humanPlayer)
if (depth == 0 || isBoardFull()) return 0; return -1000 - depth;
if (depth == 0 || isBoardFull())
return 0;
int order[] = {3, 2, 4, 1, 5, 0, 6}; int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
if (isMax) { int bestScore = isMax ? -2000 : 2000;
int maxEval = -2000; for (int column : colOrder)
for (int c : order) { {
int r = getFirstEmptyRow(c); int row = getFirstEmptyRow(column);
if (r != -1) { if (row != -1)
board[c][r] = aiP; {
int eval = minimax(depth - 1, alpha, beta, false, aiP, huP); board[column][row] = isMax ? aiPlayer : humanPlayer;
board[c][r] = 0; int score = minimax(depth - 1, alpha, beta, !isMax, aiPlayer, humanPlayer, (depth == current_look_ahead ? column : rootCol));
maxEval = max(maxEval, eval); board[column][row] = 0;
alpha = max(alpha, eval); if (isMax)
if (beta <= alpha) break; {
bestScore = max(bestScore, score);
alpha = max(alpha, bestScore);
} }
} else
return maxEval; {
} else { bestScore = min(bestScore, score);
int minEval = 2000; beta = min(beta, bestScore);
for (int c : order) {
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = huP;
int eval = minimax(depth - 1, alpha, beta, true, aiP, huP);
board[c][r] = 0;
minEval = min(minEval, eval);
beta = min(beta, eval);
if (beta <= alpha) break;
} }
if (beta <= alpha)
break;
} }
return minEval;
} }
return bestScore;
} }
void performAiMove() { void performAiMove(int8_t aiPlayer)
int8_t aiP = (menuMode == 0) ? 2 : 1; // AI is Red if player chose Yellow {
int8_t huP = (menuMode == 0) ? 1 : 2; int humanPlayer = (aiPlayer == 1) ? 2 : 1;
aiBrightness = 0; aiFadeUp = true;
int bestScore = -30000; int bestScore = -30000;
int bestCol = -1; int bestCol = 3;
int originalPly = current_look_ahead;
current_look_ahead = (gameState == DEMO) ? demoPly : getDynamicPly();
// 1. Immediate Win/Block Check // Instant win/block logic
for(int c=0; c<COLS; c++) { for (int column = 0; column < COLS; column++)
int r = getFirstEmptyRow(c); {
if(r != -1) { int row = getFirstEmptyRow(column);
board[c][r] = aiP; if(scanBoard(aiP)) { leds[getIdx(7, 0)] = CRGB::Black; return; } if (row != -1)
board[c][r] = huP; if(scanBoard(huP)) { board[c][r] = aiP; leds[getIdx(7, 0)] = CRGB::Black; return; } {
board[c][r] = 0; board[column][row] = aiPlayer;
if (scanBoard() == aiPlayer)
{
board[column][row] = 0;
bestCol = column;
goto finalizeMove;
}
board[column][row] = humanPlayer;
if (current_look_ahead >= 2 && scanBoard() == humanPlayer)
{
board[column][row] = 0;
bestCol = column;
goto finalizeMove;
}
board[column][row] = 0;
} }
} }
// 2. Recursive Search for (int column : {3, 2, 4, 1, 5, 0, 6})
for (int c : {3, 2, 4, 1, 5, 0, 6}) { {
int r = getFirstEmptyRow(c); int row = getFirstEmptyRow(column);
if (r != -1) { if (row != -1)
board[c][r] = aiP; {
int score = minimax(LOOK_AHEAD, -30000, 30000, false, aiP, huP); board[column][row] = aiPlayer;
board[c][r] = 0; int score = minimax(current_look_ahead, -30000, 30000, false, aiPlayer, humanPlayer, column);
updateThinkingLED(); // Visual feedback board[column][row] = 0;
if (score > bestScore) { bestScore = score; bestCol = c; } if (score > bestScore)
{
bestScore = score;
bestCol = column;
}
} }
} }
if (bestCol != -1) board[bestCol][getFirstEmptyRow(bestCol)] = aiP;
leds[getIdx(7, 0)] = CRGB::Black; if ((gameState == DEMO || blunder_enabled) && random(100) < 20)
{
int randomColumn = random(0, 7);
if (getFirstEmptyRow(randomColumn) != -1)
bestCol = randomColumn;
}
finalizeMove:
current_look_ahead = originalPly;
moveDiscToCol(activeCol, bestCol, aiPlayer, 100);
delay(450);
animateDrop(bestCol, aiPlayer);
} }
void showMenu() { // --- Web Portal ---
drawStaticUI();
if (menuMode < 2) { void handleRoot()
{
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>"
"<style>body{font-family:sans-serif;background:#121212;color:white;text-align:center;}"
" .card{background:#222;padding:25px;border-radius:15px;display:inline-block;margin-top:20px;}"
" input{width:100%;padding:10px;margin:10px 0;border-radius:5px;border:none;}</style></head><body>";
html += "<h1>Connect 4 Admin</h1><div class='card'><form action='/save' method='POST'>";
html += "Base AI Ply (1-10):<input type='number' name='ply' value='" + String(current_look_ahead) + "'>";
html += "Brightness (5-255):<input type='number' name='br' value='" + String(current_brightness) + "'>";
html += "Idle Timeout (Sec):<input type='number' name='idle' value='" + String(current_idle_timeout_ms / 1000) + "'>";
html += "Enable Blunders: <input type='checkbox' name='blunder' " + String(blunder_enabled ? "checked" : "") + "><br>";
html += "Evolution Mode: <input type='checkbox' name='evolve' " + String(progressive_difficulty ? "checked" : "") + "><br><br>";
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;font-weight:bold;'></form></div></body></html>";
server.send(200, "text/html", html);
}
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);
}
void showMenu()
{
isDemoOver = false;
FastLED.clear();
#if SHOW_BORDER == 1
for (int x = 0; x < 7; x++)
leds[getIdx(x, 1)] = CRGB::Blue;
for (int y = 1; y < 8; y++)
leds[getIdx(7, y)] = CRGB::Blue;
#endif
if (menuMode < 2)
{
CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow; CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
for(int y=3; y<=6; y++) leds[getIdx(3, y)] = p1Col; for (int y = 3; y <= 6; y++)
leds[getIdx(2,3)] = p1Col; leds[getIdx(4,3)] = p1Col; leds[getIdx(3, y)] = p1Col;
leds[getIdx(2,6)] = p1Col; leds[getIdx(4,6)] = p1Col; leds[getIdx(2, 3)] = p1Col;
} else { leds[getIdx(4, 3)] = p1Col;
for(int y=3; y<=6; y++) { leds[getIdx(2, y)] = CRGB::Yellow; leds[getIdx(4, y)] = CRGB::Red; } leds[getIdx(2, 6)] = p1Col;
leds[getIdx(1, 3)] = CRGB::Yellow; leds[getIdx(1, 6)] = CRGB::Yellow; leds[getIdx(4, 6)] = p1Col;
leds[getIdx(5, 3)] = CRGB::Red; leds[getIdx(5, 6)] = CRGB::Red; }
leds[getIdx(3, 3)] = CRGB::Red; leds[getIdx(3, 6)] = CRGB::Yellow; else
{
for (int y = 3; y <= 6; y++)
{
leds[getIdx(2, y)] = CRGB::Yellow;
leds[getIdx(4, y)] = CRGB::Red;
}
leds[getIdx(1, 3)] = CRGB::Yellow;
leds[getIdx(3, 3)] = CRGB::Yellow;
leds[getIdx(1, 6)] = CRGB::Yellow;
leds[getIdx(3, 6)] = CRGB::Yellow;
leds[getIdx(3, 3)] = CRGB::Red;
leds[getIdx(5, 3)] = CRGB::Red;
leds[getIdx(3, 6)] = CRGB::Red;
leds[getIdx(5, 6)] = CRGB::Red;
} }
FastLED.show(); FastLED.show();
} }
void setup() { void setup()
{
Serial.begin(115200); Serial.begin(115200);
myEnc = new Encoder(ENC_A, ENC_B); 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;
blunder_enabled = prefs.getBool("blunder", false);
progressive_difficulty = prefs.getBool("evolve", false);
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS); FastLED.setBrightness(current_brightness);
pinMode(ENC_SW, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP);
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.begin();
lastActivityTime = millis();
showMenu(); showMenu();
} }
void loop() { void loop()
long newPos = myEnc->read() / SENSITIVITY; {
server.handleClient();
long newPos = myEnc.read() / SENSITIVITY;
bool pressed = (digitalRead(ENC_SW) == LOW); bool pressed = (digitalRead(ENC_SW) == LOW);
if (gameState == MENU) { // Activity check
if (newPos != oldEncPos) { if (newPos != oldEncPos || (pressed && (millis() - lastActivityTime > 500)))
{
if (gameState >= 2 || gameState == DEMO)
{
for (int index = 0; index < 10; index++)
{
fadeToBlackBy(leds, NUM_LEDS, 32);
FastLED.show();
delay(30);
}
delay(500);
gameState = MENU;
memset(board, 0, sizeof(board));
showMenu();
lastActivityTime = millis();
oldEncPos = newPos;
return;
}
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)
{
menuMode = (newPos % 3 + 3) % 3; menuMode = (newPos % 3 + 3) % 3;
oldEncPos = newPos; oldEncPos = newPos;
showMenu(); showMenu();
} }
if (pressed) { if (pressed)
{
memset(board, 0, sizeof(board)); memset(board, 0, sizeof(board));
gameState = PLAYING; gameState = PLAYING;
// If Single Player RED, computer (1/Yellow) starts if (menuMode == 1)
if (menuMode == 1) { {
performAiMove(1);
currentPlayer = 2;
}
else
{
currentPlayer = 1; currentPlayer = 1;
renderBoard(); FastLED.show();
performAiMove();
currentPlayer = 2; // Set back to player
} else {
currentPlayer = 1; // Human starts
} }
delay(300); delay(300);
} }
} }
else if (gameState == PLAYING) { else if (gameState == PLAYING)
if (newPos != oldEncPos) { {
if (newPos != oldEncPos)
{
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)
board[activeCol][row] = currentPlayer; {
renderBoard(); FastLED.show(); animateDrop(activeCol, currentPlayer);
winnerPlayer = scanBoard();
// 1. Check if the move just made ended the game if (winnerPlayer != 0)
if (scanBoard(currentPlayer)) { {
gameState = FINISHED_WIN; gameState = FINISHED_WIN;
} else if (isBoardFull()) { demoResetTimer = millis();
}
else if (isBoardFull())
{
gameState = FINISHED_DRAW; gameState = FINISHED_DRAW;
} else { demoResetTimer = millis();
// 2. Handle Turn Switching }
if (menuMode < 2) { // Single Player else
{
if (menuMode < 2)
{
int8_t aiP = (menuMode == 0) ? 2 : 1; int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove(); performAiMove(aiP);
if (scanBoard(aiP)) { lastActivityTime = millis(); // Reset after AI thinking
currentPlayer = aiP; // For the flashing color winnerPlayer = scanBoard();
if (winnerPlayer != 0)
{
gameState = FINISHED_WIN; gameState = FINISHED_WIN;
} else if (isBoardFull()) { demoResetTimer = millis();
gameState = FINISHED_DRAW;
} }
} else { // PvP else if (isBoardFull())
{
gameState = FINISHED_DRAW;
demoResetTimer = millis();
}
}
else
{
currentPlayer = (currentPlayer == 1) ? 2 : 1; currentPlayer = (currentPlayer == 1) ? 2 : 1;
} }
} }
} else { delay(300);
for(int i=0; i<3; i++) {
leds[getIdx(activeCol, 0)] = CRGB::Black; FastLED.show(); delay(80);
leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red; FastLED.show(); delay(80);
}
} }
delay(300);
} }
} }
else { else if (gameState == DEMO)
static unsigned long lastFlash = 0; {
renderBoard();
FastLED.show();
delay(600);
performAiMove(currentPlayer);
winnerPlayer = scanBoard();
if (winnerPlayer != 0)
{
gameState = FINISHED_WIN;
demoResetTimer = millis();
}
else if (isBoardFull())
{
gameState = FINISHED_DRAW;
demoResetTimer = millis();
}
else
{
currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
}
else
{ // FINISHED state (WIN/DRAW)
static uint32_t lastFlash = 0;
static bool toggle = true; static bool toggle = true;
if (millis() - lastFlash > 300) { if (millis() - lastFlash > 300)
lastFlash = millis(); toggle = !toggle; {
renderBoard(); lastFlash = millis();
for (int i = 0; i < NUM_LEDS; i++) { toggle = !toggle;
if (gameState == FINISHED_WIN) { renderBoard();
if (winMask[i]) leds[i] = toggle ? (currentPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black; for (int i = 0; i < NUM_LEDS; i++)
else if (leds[i] && leds[i] != CRGB::Blue) leds[i].nscale8(40); {
} else { #if SHOW_BORDER == 1
if (leds[i] && leds[i] != CRGB::Blue) leds[i] = toggle ? leds[i] : CRGB::Black; if (leds[i] == CRGB::Blue)
continue;
#endif
if (gameState == FINISHED_WIN)
{
if (winMask[i])
leds[i] = toggle ? (winnerPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black;
else
leds[i].nscale8(60);
}
else if (gameState == FINISHED_DRAW)
{
if (!toggle)
leds[i] = CRGB::Black;
} }
} }
FastLED.show(); FastLED.show();
} }
if (pressed) { gameState = MENU; showMenu(); delay(300); } if (millis() - demoResetTimer > 15000)
{
memset(board, 0, sizeof(board));
gameState = DEMO;
demoResetTimer = 0;
demoPly = random(3, 7);
}
if (pressed)
{
gameState = MENU;
showMenu();
delay(300);
}
} }
} }
Generated
-302
View File
@@ -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" },
]