Compare commits

...

17 Commits

Author SHA1 Message Date
seppedl fa757598be [refactor] Code, build flags and documentation. 2026-03-15 15:30:58 +01:00
seppedl 52c84301dc [fix] Encoder sensitivity. Extend delay after win. 2026-03-12 12:59:39 +01:00
seppedl c73d6e3686 [fix] Interrupt demo when button is pressed. 2026-03-10 08:29:13 +01:00
seppedl 0aa1802cce [update] Documentation 2026-03-09 10:43:46 +01:00
seppedl 0994c11f0b [fix] AI strategy detect win first, killer instinct and button press. Although not 100% okay as button is not always detected during 'thinking" 2026-03-09 10:38:42 +01:00
seppedl a6e0bd0489 [fix] Skip win animation if AI vs Player wins. Logical error in timong loops. 2026-03-09 10:16:56 +01:00
seppedl 117d078efc [add] Option to hide blue borders. 2026-03-09 10:15:49 +01:00
seppedl 8edfda2b21 [refactor] Renamed all single letter variables to more meaningfull names. 2026-03-07 11:30:14 +01:00
seppedl 6c8802509f [add] Background information on gameplay. 2026-03-07 11:15:46 +01:00
seppedl 7921ffe7e9 [cleanup] Extra files removed. 2026-03-07 11:02:49 +01:00
seppedl 5075b568ba [cleanup] Extra files removed. 2026-03-07 11:00:43 +01:00
seppedl 5238fbf0f5 [fix] Demo draw state no longer flashing and adding switch to demo if player(s) abandon game. 2026-03-07 08:30:42 +01:00
seppedl 73981c95c5 [refactor] Progressive difficulty, blunder logic and documentation. 2026-03-06 22:14:25 +01:00
seppedl 8e338b5e1c [add] Wifi password 2026-03-06 17:13:56 +01:00
seppedl 8fb373f5d6 [add] Web config and ply 1 2026-03-06 17:07:10 +01:00
seppedl 917cec34e4 [add] Autoplay and improve gameplay 2026-03-06 13:40:46 +01:00
seppedl 45e06009a9 [update] Documentation. 2026-03-06 09:53:53 +01:00
10 changed files with 720 additions and 546 deletions
+2
View File
@@ -3,3 +3,5 @@
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
.vscode/settings.json
CLAUDE.md
-1
View File
@@ -1 +0,0 @@
3.14
+117
View File
@@ -0,0 +1,117 @@
# Connect 4 AI: How the Computer Thinks
## 1. The Virtual Board
The computer doesn't see colored discs on a grid. It sees a table of numbers:
- **0** = Empty space
- **1** = Yellow disc
- **2** = Red disc
The board has 7 columns and 6 rows. After every move, a scan function checks all directions (horizontal, vertical, and both diagonals) to see if anyone has four in a row.
## 2. What is a "Ply"?
A **ply** is one move by one player. If the AI is set to ply 6, it looks 6 individual moves into the future. Since players alternate turns, ply 6 means the AI considers 3 of its own moves and 3 of the opponent's moves.
More plies = stronger play, but takes longer to calculate. On the ESP32-C3, ply 4 is nearly instant, ply 6 takes about a second, and ply 8-10 can take several seconds. The AI shows a pulsing light while it is thinking.
## 3. The Minimax Strategy
### The basic idea
Imagine you are playing Connect 4 against a friend. Before you drop your disc, you think: "If I put my disc here, what will my friend do? And then what would I do after that?"
That is exactly what the computer does, except it checks **every** possible move, not just a few.
### Two players, two goals
The AI calls the two players **Max** (itself) and **Min** (you):
- **Max** wants the highest possible score (the AI winning).
- **Min** wants the lowest possible score (you winning).
The AI assumes you will always make your best move. It doesn't hope you'll make a mistake.
### A simple example
Imagine there are only 3 columns left and the AI can look 2 moves ahead. It builds a tree like this:
```
AI's turn (Max - pick the highest)
/ | \
col 2 col 3 col 4
/ \ / \ / \
Your turn (Min - pick the lowest)
... ... ... ... ... ...
+5 -3 +2 +8 -1 +4
```
1. After column 2: you would pick the move scoring -3 (lowest = best for you).
2. After column 3: you would pick the move scoring +2.
3. After column 4: you would pick the move scoring -1.
The AI compares -3, +2, and -1, and picks column 3 because +2 is the best it can guarantee.
### Scoring
The AI assigns scores to board positions:
- **+1000 or more:** The AI wins. A faster win gets a higher score, so the AI goes for the quickest victory.
- **-1000 or less:** The opponent wins. A faster loss gets a more negative score, so the AI fights hardest against immediate threats.
- **0:** Nobody has won and the search depth ran out. The position is neutral.
This scoring is why the AI has "killer instinct" - it doesn't just try to win, it tries to win as fast as possible.
## 4. Alpha-Beta Pruning: The Smart Shortcut
### The problem
Looking ahead 8 plies in Connect 4 means exploring millions of board positions. Even a fast microcontroller can't check them all in a reasonable time.
### The solution
**Alpha-Beta pruning** is a way to skip branches of the tree that can't possibly change the final decision.
Think of it like shopping for a birthday present. You visit Shop A and find a nice toy for 10 euros. Then you go to Shop B. The first item you see costs 15 euros, and you notice everything else in Shop B is even more expensive. You don't need to check every item in Shop B - you already know Shop A is better. You leave Shop B and save time.
The AI does the same thing:
- **Alpha** is the best score the AI (Max) has found so far. Think of it as "I already know I can do at least this well."
- **Beta** is the best score the opponent (Min) has found so far. Think of it as "The opponent already knows they can limit me to at most this."
When the AI is exploring a branch and discovers that the score can never beat what it already has (beta <= alpha), it **prunes** (cuts off) that entire branch. It skips all remaining moves in that branch because they can't change the outcome.
### How much does it help?
In practice, pruning lets the AI skip 50-90% of the positions it would otherwise need to check. This is why the column order matters - the AI checks the center column first (column 3), then works outward. Good moves tend to be near the center, so checking them first leads to better pruning and faster search.
## 5. The Three-Phase Move Strategy
Before running the expensive minimax search, the AI takes two quick shortcuts:
1. **Can I win right now?** The AI tries placing its disc in each column. If any column completes four in a row, it takes that move immediately. No need to think further.
2. **Can my opponent win next turn?** The AI checks if the opponent could win by playing in any column. If so, it blocks that column. Missing this would be a fatal mistake.
3. **Deep search.** Only if there are no immediate wins or threats does the AI run the full minimax search with alpha-beta pruning.
This three-phase approach makes the AI both fast (instant reactions to obvious moves) and smart (deep strategic thinking when needed).
## 6. Demo Mode: Asymmetric Skill
In demo mode, two AI players play against each other. To make the games interesting (rather than always ending in a draw), each player is randomly assigned a different search depth. One player might look 5 moves ahead while the other only looks 3 moves ahead. The stronger player can find winning setups that the weaker one misses, leading to exciting games with real winners. Who gets the advantage is randomized each game.
## 7. Responsive Controls
The ESP32-C3 is a single-core processor. When the AI is thinking, it could block all input for several seconds. Two techniques keep the game responsive:
1. **Mid-search button checks:** During the minimax search, the AI periodically checks whether the player has pressed the button. If so, it immediately abandons the search.
2. **Abort flag:** A global flag (`abortAi`) propagates through all levels of the recursive search. Once set, every level of the search returns immediately, unwinding the entire calculation in microseconds.
## Further Reading
- [Connect Four - Mathematical Solution (Wikipedia)](https://en.wikipedia.org/wiki/Connect_Four#Mathematical_solution)
- [Minimax Algorithm (Wikipedia)](https://en.wikipedia.org/wiki/Minimax)
- [Alpha-Beta Pruning (Wikipedia)](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning)
+112 -26
View File
@@ -1,38 +1,124 @@
# Connect Four
# Connect 4 - ESP32
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 Connect 4 game with AI for the ESP32-C3 (Lolin C3 Mini), using an 8x8 NeoPixel LED matrix and rotary encoder.
## Technical specifications
## Hardware
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.
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
### Pin Mapping (Lolin C3 Mini)
| 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/Abort (SW) |
### LED Matrix Layout
The 8x8 matrix (64 LEDs) is used as follows:
```
Row 0: [ col0 ] [ col1 ] [ col2 ] [ col3 ] [ col4 ] [ col5 ] [ col6 ] [ indicator ]
Row 1: [ ---- ] [ ---- ] [ ---- ] [ ---- ] [ ---- ] [ ---- ] [ ---- ] [ border ]
Row 2-7:[ game board: 7 columns x 6 rows ] [ border ]
```
The grid is managed using the FastLED.h library and is connected to:
```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
- **Row 0 (columns 0-6):** Interaction row. Shows the current column selection cursor and AI thinking animation (pulsing disc).
- **Row 0 (column 7):** Game mode indicator LED (dim). Yellow = player is yellow vs AI, Red = player is red vs AI, Blue = two player, Off = demo.
- **Row 1 + Column 7:** Blue border frame (toggleable via `SHOW_BORDER` build flag). Glows softly during demo and finished states.
- **Rows 2-7, Columns 0-6:** The 7x6 game board. Yellow and Red discs.
- **LED index formula:** `index = (y * 8) + x`
## Game Modes
Use the rotary encoder to select a mode, press the button to start:
1. **Player vs AI (Yellow)** - Player plays yellow (first move), AI plays red.
2. **Player vs AI (Red)** - AI plays yellow (first move), player plays red.
3. **Two Player** - Two humans alternate turns. Yellow goes first.
### Demo Mode
When idle (no input for the configured timeout), the board enters demo mode where two AI players play against each other automatically. To make games more interesting, the two demo players are assigned different skill levels (asymmetric ply depths), so games frequently end in a win rather than a draw. Press the button or turn the encoder to exit demo mode and return to the menu.
### Animations
- **Disc drop:** Discs fall from the top with accelerating speed.
- **Column movement:** Discs slide across the top row to the selected column.
- **AI thinking:** The disc in the selected column pulses while the AI calculates.
- **Win:** The winning four discs flash while the rest of the board dims. Displayed for 30 seconds.
- **Draw:** All discs blink on and off.
## WiFi Admin Interface
The ESP32 creates a WiFi access point:
- **Network:** `Connect4-Config`
- **Password:** Configured via `WIFI_PASSWORD` build flag (default: `youlose4`)
- **Admin page:** Connect to the network and open `http://192.168.4.1`
### Settings (via web interface)
| Setting | Description |
| :--------------- | :------------------------------------------------------- |
| **Base AI Ply** | Search depth for the AI (1-10). Higher = stronger. |
| **Brightness** | LED brightness (0-255). |
| **Idle Timeout** | Seconds of inactivity before demo mode starts. |
| **Blunders** | Reserved for future use. |
| **Evolution** | Progressive difficulty: AI gets stronger as game goes on.|
Settings are saved to flash (NVS) and persist across reboots.
### Game Log
The web interface shows a log of the last N games (configurable via `MAX_GAME_LOG` build flag). Each entry shows the game type, AI level, winner, and the sequence of columns played. Games where a player beats the computer are highlighted in red. The game log is persisted to flash and survives reboots.
## Build & Upload
Requires [PlatformIO](https://platformio.org/) CLI or VS Code with PlatformIO extension.
```bash
# Build
pio run
# Upload to board
pio run --target upload
# Monitor serial output
pio device monitor
```
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.
### Dependencies
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.
- [FastLED](https://github.com/FastLED/FastLED) >= 3.6.0 - LED control
- [Encoder](https://github.com/PaulStoffregen/Encoder) >= 1.4.4 - Rotary encoder input
- Preferences (built-in) - Persistent settings storage
- WiFi / WebServer (built-in) - Admin interface
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.
### Build Flags
The game has four states:
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.
2. Game play: Players are playing the game until one of the players connects four or they achieve a draw.
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.
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.
All configurable parameters are defined as `-D` flags in `platformio.ini`:
The program initializes the SerialPrint output (baud 115200) and outputs useful (debugging) information regarding the game state and selections.
| Flag | Default | Description |
| :--------------------- | :------ | :--------------------------------------------- |
| `LED_PIN` | `4` | GPIO pin for NeoPixel data line |
| `ENC_A` | `0` | GPIO pin for encoder CLK |
| `ENC_B` | `1` | GPIO pin for encoder DT |
| `ENC_SW` | `2` | GPIO pin for encoder button |
| `SENSITIVITY` | `4` | Encoder steps per detent (higher = less sensitive) |
| `SHOW_BORDER` | `1` | Show blue border frame (0 = off, 1 = on) |
| `DEFAULT_LOOK_AHEAD` | `8` | Default AI search depth (plies) |
| `DEFAULT_BRIGHTNESS` | `25` | Default LED brightness (0-255) |
| `DEFAULT_IDLE_TIMEOUT` | `45` | Seconds before demo mode activates |
| `MAX_GAME_LOG` | `5` | Number of games stored in the game log |
| `WIFI_PASSWORD` | `youlose4` | Password for the WiFi access point |
## Project Structure
```
src/main.cpp Single-file application (all game logic, AI, LED, web server)
platformio.ini Build configuration, pin mappings, and tunable parameters
README.md This file - technical and practical information
Background information.md How the AI works (suitable for all ages)
CLAUDE.md AI assistant project context
```
View File
-6
View File
@@ -1,6 +0,0 @@
def main():
print("Hello from connect-four!")
if __name__ == "__main__":
main()
+10 -1
View File
@@ -9,9 +9,18 @@ build_flags =
-D LED_PIN=4
-D ENC_A=0
-D ENC_B=1
-D ENC_SW=6
-D ENC_SW=2
-D SENSITIVITY=4
-D SHOW_BORDER=0
-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 MAX_GAME_LOG=100
-D WIFI_PASSWORD=\"youlose4\"
lib_deps =
fastled/FastLED @ ^3.6.0
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",
]
+470 -192
View File
@@ -1,278 +1,556 @@
#include <Arduino.h>
#include <FastLED.h>
#include <Encoder.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#ifndef SHOW_BORDER
#define SHOW_BORDER 1
#endif
#ifndef SENSITIVITY
#define SENSITIVITY 4
#endif
#define LED_PIN 4
#define ENC_A 0
#define ENC_B 1
#define ENC_SW 2
#define NUM_LEDS 64
#ifndef MAX_GAME_LOG
#define MAX_GAME_LOG 5
#endif
const int COLS = 7;
const int ROWS = 6;
const int LOOK_AHEAD = 6; // Depth 6 is very stable and tough for C3
const int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
CRGB leds[NUM_LEDS];
Encoder *myEnc;
Encoder myEnc(ENC_A, ENC_B);
WebServer server(80);
Preferences prefs;
int8_t board[COLS][ROWS];
bool winMask[NUM_LEDS];
enum State { MENU, PLAYING, FINISHED_WIN, FINISHED_DRAW };
enum State { MENU, PLAYING, AI_TURN, FINISHED_WIN, FINISHED_DRAW, DEMO };
State gameState = MENU;
int8_t menuMode = 0; // 0: P1-Yellow, 1: P1-Red, 2: PvP
int8_t currentPlayer = 1; // 1: Yellow, 2: Red
int8_t menuMode = 0;
int8_t currentPlayer = 1;
int8_t winnerPlayer = 0;
int8_t activeCol = 3;
long oldEncPos = -999;
uint32_t lastActivityTime = 0;
uint32_t demoResetTimer = 0;
uint32_t globalInputCooldown = 0;
uint8_t demoPly[2] = {4, 4};
bool abortAi = false;
bool lastButtonState = HIGH;
uint8_t currentLookAhead = 6;
uint8_t currentBrightness = 30;
uint32_t currentIdleTimeoutMs = 60000;
bool blunderEnabled = false;
bool progressiveDifficulty = false;
uint8_t aiBrightness = 0;
bool aiFadeUp = true;
// --- Helper Functions ---
struct GameEntry {
char type;
uint8_t level;
char winner;
String moves;
};
GameEntry gameLog[MAX_GAME_LOG];
uint8_t gameLogCount = 0;
String currentMoves = "";
int8_t gameMenuMode = 0;
uint8_t gameLevel = 0;
// --- Prototypes ---
CRGB playerColor(int8_t player);
int getIdx(int x, int y);
void resetBoard();
void drawBorder(CRGB color);
void logGame(int8_t winner);
void saveGameLog();
void loadGameLog();
void drawStaticUI();
void renderBoard();
void showMenu();
int getFirstEmptyRow(int col);
bool isBoardFull();
int getDynamicPly();
int8_t scanBoard();
bool checkGameEnd();
void updateThinkingVisuals(int8_t pColor, 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 aiP, int8_t huP, int8_t rootCol);
void performAiMove(int8_t aiP);
void randomizeDemoPlies();
void handleRoot();
void handleSave();
void handleMenu(long newPos, bool pressed);
void handlePlaying(long newPos, bool pressed);
void handleAiTurn();
void handleDemo();
void handleFinished();
// --- Helpers ---
CRGB playerColor(int8_t player) {
return (player == 1) ? CRGB::Yellow : CRGB::Red;
}
int getIdx(int x, int y) { return (y * 8) + x; }
void resetBoard() {
memset(board, 0, sizeof(board));
winnerPlayer = 0;
}
void drawBorder(CRGB color) {
for (int x = 0; x < 7; x++) leds[getIdx(x, 1)] = color;
for (int y = 1; y < 8; y++) leds[getIdx(7, y)] = color;
}
void logGame(int8_t winner) {
char type = (gameMenuMode == 0) ? 'Y' : (gameMenuMode == 1) ? 'R' : '2';
char winChar = (winner == 1) ? 'Y' : (winner == 2) ? 'R' : 'D';
GameEntry entry = { type, gameLevel, winChar, currentMoves };
if (gameLogCount < MAX_GAME_LOG) { gameLog[gameLogCount++] = entry; }
else { for (int i = 0; i < MAX_GAME_LOG - 1; i++) gameLog[i] = gameLog[i + 1]; gameLog[MAX_GAME_LOG - 1] = entry; }
saveGameLog();
}
void saveGameLog() {
prefs.putUChar("glc", gameLogCount);
for (int i = 0; i < gameLogCount; i++) {
String val = String(gameLog[i].type) + ":" + String(gameLog[i].level) + ":" + String(gameLog[i].winner) + ":" + gameLog[i].moves;
prefs.putString(("g" + String(i)).c_str(), val);
}
}
void loadGameLog() {
gameLogCount = prefs.getUChar("glc", 0);
if (gameLogCount > MAX_GAME_LOG) gameLogCount = MAX_GAME_LOG;
for (int i = 0; i < gameLogCount; i++) {
String val = prefs.getString(("g" + String(i)).c_str(), "");
if (val.length() < 5) { gameLogCount = i; break; }
gameLog[i].type = val.charAt(0);
int sep1 = val.indexOf(':', 2);
gameLog[i].level = val.substring(2, sep1).toInt();
gameLog[i].winner = val.charAt(sep1 + 1);
gameLog[i].moves = val.substring(sep1 + 3);
}
}
void randomizeDemoPlies() {
uint8_t strong = random(4, 6);
uint8_t weak = random(2, 4);
if (random(2) == 0) { demoPly[0] = strong; demoPly[1] = weak; }
else { demoPly[0] = weak; demoPly[1] = strong; }
}
// --- Display ---
void drawStaticUI() {
FastLED.clear();
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;
#if SHOW_BORDER == 1
CRGB borderColor = CRGB::Blue;
if (gameState == DEMO || gameState == FINISHED_WIN || gameState == FINISHED_DRAW) {
uint8_t glow = beat8(15);
borderColor = blend(CRGB::Blue, CRGB::White, glow / 4);
}
drawBorder(borderColor);
#endif
}
void renderBoard() {
drawStaticUI();
for(int c=0; c<COLS; c++) {
for(int r=0; r<ROWS; r++) {
if(board[c][r] == 1) leds[getIdx(c, 7-r)] = CRGB::Yellow;
if(board[c][r] == 2) leds[getIdx(c, 7-r)] = CRGB::Red;
if (gameState == PLAYING || gameState == AI_TURN) {
CRGB indicator = (menuMode == 0) ? CRGB::Yellow : (menuMode == 1) ? CRGB::Red : CRGB::Blue;
indicator.nscale8(25);
leds[getIdx(7, 0)] = indicator;
}
for (int c = 0; c < COLS; c++) {
for (int r = 0; r < ROWS; r++) {
if (board[c][r] != 0) leds[getIdx(c, 7 - r)] = playerColor(board[c][r]);
}
}
}
void showMenu() {
FastLED.clear();
#if SHOW_BORDER == 1
drawBorder(CRGB::Blue);
#endif
CRGB pCol = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
if (menuMode < 2) {
for (int y = 3; y <= 6; y++) leds[getIdx(3, y)] = pCol;
leds[getIdx(2, 3)] = pCol; leds[getIdx(4, 3)] = pCol;
leds[getIdx(2, 6)] = pCol; leds[getIdx(4, 6)] = pCol;
} 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();
}
// --- Game logic ---
int getFirstEmptyRow(int col) {
for (int r = 0; r < ROWS; r++) { if (board[col][r] == 0) return r; }
for (int r = 0; r < ROWS; r++) if (board[col][r] == 0) return r;
return -1;
}
bool isBoardFull() {
for (int c = 0; c < COLS; c++) if (board[c][5] == 0) return false;
for (int c = 0; c < COLS; c++) if (board[c][ROWS - 1] == 0) return false;
return true;
}
// Scans board and fills winMask if 4+ connected
bool scanBoard(int8_t p) {
bool found = false;
memset(winMask, 0, sizeof(winMask));
auto checkLine = [&](int x, int y, int dx, int dy) {
int getDynamicPly() {
if (!progressiveDifficulty && gameState != DEMO) return currentLookAhead;
int count = 0;
for (int i = 0; i < 7; i++) {
int nx = x + i * dx; int ny = y + i * dy;
if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS && board[nx][ny] == p) {
count++;
} else {
if (count >= 4) {
for (int j = 1; j <= count; j++)
winMask[getIdx(nx - j * dx, 7 - (ny - j * dy))] = true;
found = true;
}
count = 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 i = -5; i < 7; i++) { checkLine(i, 0, 1, 1); checkLine(i, 5, 1, -1); }
return found;
for (int c = 0; c < COLS; c++) for (int r = 0; r < ROWS; r++) if (board[c][r] != 0) count++;
return constrain(currentLookAhead + (count / 7), 1, 10);
}
// --- AI Thinking Visualization ---
void updateThinkingLED() {
int8_t scanBoard() {
memset(winMask, 0, sizeof(winMask));
auto check = [&](int c, int r, int dc, int dr) {
int8_t p = board[c][r];
if (p != 0 && board[c+dc][r+dr] == p && board[c+2*dc][r+2*dr] == p && board[c+3*dc][r+3*dr] == p) {
for (int i = 0; i < 4; i++) winMask[getIdx(c+i*dc, 7-(r+i*dr))] = true;
return p;
}
return (int8_t)0;
};
for (int r=0; r<6; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,0); if(res) return res; }
for (int r=0; r<3; r++) for (int c=0; c<7; c++) { int8_t res = check(c,r,0,1); if(res) return res; }
for (int r=0; r<3; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,1); if(res) return res; }
for (int r=3; r<6; r++) for (int c=0; c<4; c++) { int8_t res = check(c,r,1,-1); if(res) return res; }
return 0;
}
bool checkGameEnd() {
winnerPlayer = scanBoard();
if (winnerPlayer != 0) {
if (gameState != DEMO) logGame(winnerPlayer);
gameState = FINISHED_WIN;
demoResetTimer = millis();
lastActivityTime = millis();
return true;
}
if (isBoardFull()) {
if (gameState != DEMO) logGame(0);
gameState = FINISHED_DRAW;
demoResetTimer = millis();
lastActivityTime = millis();
return true;
}
return false;
}
// --- Animation ---
void updateThinkingVisuals(int8_t pColor, int8_t column) {
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; }
// Pulse in the computer's color
CRGB compColor = (menuMode == 0) ? CRGB::Red : CRGB::Yellow;
leds[getIdx(7, 0)] = compColor.nscale8(aiBrightness);
if (aiFadeUp) { aiBrightness += 25; if (aiBrightness >= 230) aiFadeUp = false; }
else { aiBrightness -= 25; if (aiBrightness <= 25) aiFadeUp = true; }
for (int x = 0; x < COLS; x++) leds[getIdx(x, 0)] = CRGB::Black;
CRGB aiColor = playerColor(pColor);
aiColor.nscale8(aiBrightness);
leds[getIdx(column, 0)] = aiColor;
FastLED.show();
}
// --- Minimax Logic ---
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP) {
// Check wins within the simulation
if (scanBoard(aiP)) return 1000 + depth;
if (scanBoard(huP)) return -1000 - depth;
if (depth == 0 || isBoardFull()) return 0;
int order[] = {3, 2, 4, 1, 5, 0, 6};
if (isMax) {
int maxEval = -2000;
for (int c : order) {
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = aiP;
int eval = minimax(depth - 1, alpha, beta, false, aiP, huP);
board[c][r] = 0;
maxEval = max(maxEval, eval);
alpha = max(alpha, eval);
if (beta <= alpha) break;
}
}
return maxEval;
} else {
int minEval = 2000;
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;
}
}
return minEval;
void animateDrop(int col, int player) {
int targetRow = getFirstEmptyRow(col);
if (targetRow == -1) return;
if (gameState != DEMO) currentMoves += String(col);
for (int r = 5; r >= targetRow; r--) {
renderBoard();
leds[getIdx(col, 7 - r)] = playerColor(player);
FastLED.show();
delay(max(10, 60 - (5 - r) * 10));
}
board[col][targetRow] = player;
}
void performAiMove() {
int8_t aiP = (menuMode == 0) ? 2 : 1; // AI is Red if player chose Yellow
int8_t huP = (menuMode == 0) ? 1 : 2;
void moveDiscToCol(int startCol, int targetCol, int player, int speed) {
int current = startCol;
CRGB colr = playerColor(player);
while (current != targetCol && !abortAi) {
if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { abortAi = true; break; }
leds[getIdx(current, 0)] = CRGB::Black;
current += (targetCol > current) ? 1 : -1;
renderBoard();
leds[getIdx(current, 0)] = colr;
FastLED.show();
delay(speed);
}
activeCol = targetCol;
}
aiBrightness = 0; aiFadeUp = true;
int bestScore = -30000;
int bestCol = -1;
// --- AI ---
// 1. Immediate Win/Block Check
for(int c=0; c<COLS; c++) {
int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP, int8_t rootCol) {
if (gameState == DEMO && digitalRead(ENC_SW) == LOW) { abortAi = true; return 0; }
if (depth >= currentLookAhead - 1) updateThinkingVisuals(aiP, rootCol);
else yield();
if (abortAi) return 0;
int8_t win = scanBoard();
if (win == aiP) return 1000 + depth;
if (win == huP) return -1000 - depth;
if (depth == 0 || isBoardFull()) return 0;
int best = isMax ? -10000 : 10000;
for (int c : colOrder) {
if (abortAi) return 0;
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(scanBoard(huP)) { board[c][r] = aiP; leds[getIdx(7, 0)] = CRGB::Black; return; }
if (r != -1) {
board[c][r] = isMax ? aiP : huP;
int score = minimax(depth - 1, alpha, beta, !isMax, aiP, huP, (depth == currentLookAhead ? c : rootCol));
board[c][r] = 0;
if (isMax) { if (score > best) best = score; if (best > alpha) alpha = best; }
else { if (score < best) best = score; if (best < beta) beta = best; }
if (beta <= alpha) break;
}
}
return best;
}
void performAiMove(int8_t aiP) {
abortAi = false;
int huP = (aiP == 1) ? 2 : 1;
int bestScore = -30000; int bestCol = 3;
int originalPly = currentLookAhead;
currentLookAhead = (gameState == DEMO) ? demoPly[aiP - 1] : getDynamicPly();
for (int c = 0; c < COLS; c++) {
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
board[c][r] = huP; if (scanBoard() == huP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
board[c][r] = 0;
}
}
// 2. Recursive Search
for (int c : {3, 2, 4, 1, 5, 0, 6}) {
for (int c : colOrder) {
if (abortAi) goto finalizeMove;
int r = getFirstEmptyRow(c);
if (r != -1) {
board[c][r] = aiP;
int score = minimax(LOOK_AHEAD, -30000, 30000, false, aiP, huP);
int score = minimax(currentLookAhead, -30000, 30000, false, aiP, huP, c);
board[c][r] = 0;
updateThinkingLED(); // Visual feedback
if (score > bestScore) { bestScore = score; bestCol = c; }
}
}
if (bestCol != -1) board[bestCol][getFirstEmptyRow(bestCol)] = aiP;
leds[getIdx(7, 0)] = CRGB::Black;
finalizeMove:
currentLookAhead = originalPly;
if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } }
}
void showMenu() {
drawStaticUI();
if (menuMode < 2) {
CRGB p1Col = (menuMode == 1) ? CRGB::Red : CRGB::Yellow;
for(int y=3; y<=6; y++) leds[getIdx(3, y)] = p1Col;
leds[getIdx(2,3)] = p1Col; leds[getIdx(4,3)] = p1Col;
leds[getIdx(2,6)] = p1Col; leds[getIdx(4,6)] = p1Col;
// --- Web handlers ---
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;}"
"table{width:100%;} th,td{padding:4px;}"
"</style></head><body>"
"<h1>Connect 4 Admin</h1>"
"<div class='card'><form action='/save' method='POST'>";
html += "Base AI Ply:<input type='number' name='ply' value='" + String(currentLookAhead) + "'>";
html += "Brightness:<input type='number' name='br' value='" + String(currentBrightness) + "'>";
html += "Idle Timeout (s):<input type='number' name='idle' value='" + String(currentIdleTimeoutMs / 1000) + "'>";
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunderEnabled ? "checked" : "") + "><br>";
html += "Evolution: <input type='checkbox' name='evolve' " + String(progressiveDifficulty ? "checked" : "") + "><br><br>";
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;'>";
html += "</form></div>";
html += "<div class='card' style='margin-top:15px;text-align:left;'><h3 style='text-align:center;'>Game Log</h3>";
if (gameLogCount == 0) {
html += "<p style='text-align:center;'>No games played yet.</p>";
} 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(1, 6)] = CRGB::Yellow;
leds[getIdx(5, 3)] = CRGB::Red; leds[getIdx(5, 6)] = CRGB::Red;
leds[getIdx(3, 3)] = CRGB::Red; leds[getIdx(3, 6)] = CRGB::Yellow;
html += "<table><tr><th>Type</th><th>Lvl</th><th>Winner</th><th>Moves</th></tr>";
for (int i = gameLogCount - 1; i >= 0; i--) {
bool playerWon = gameLog[i].type != '2' && gameLog[i].type == gameLog[i].winner;
html += playerWon ? "<tr style='color:#ff4444;'>" : "<tr>";
html += "<td>" + String(gameLog[i].type) + "</td>";
html += "<td>" + String(gameLog[i].level) + "</td>";
html += "<td>" + String(gameLog[i].winner) + "</td>";
html += "<td>" + gameLog[i].moves + "</td></tr>";
}
html += "</table>";
}
html += "</div></body></html>";
server.send(200, "text/html", html);
}
void handleSave() {
if (server.hasArg("ply")) { currentLookAhead = server.arg("ply").toInt(); prefs.putUChar("ply", currentLookAhead); }
if (server.hasArg("br")) { currentBrightness = server.arg("br").toInt(); FastLED.setBrightness(currentBrightness); prefs.putUChar("br", currentBrightness); }
if (server.hasArg("idle")) { currentIdleTimeoutMs = server.arg("idle").toInt() * 1000; prefs.putUInt("idle", currentIdleTimeoutMs / 1000); }
blunderEnabled = server.hasArg("blunder"); prefs.putBool("blunder", blunderEnabled);
progressiveDifficulty = server.hasArg("evolve"); prefs.putBool("evolve", progressiveDifficulty);
server.sendHeader("Location", "/"); server.send(303);
}
// --- State handlers ---
void handleMenu(long newPos, bool pressed) {
if (millis() > globalInputCooldown) {
if (newPos != oldEncPos) { menuMode = (newPos % 3 + 3) % 3; oldEncPos = newPos; showMenu(); }
if (pressed) {
resetBoard();
currentMoves = "";
gameMenuMode = menuMode;
gameLevel = currentLookAhead;
currentPlayer = 1;
if (menuMode == 1) gameState = AI_TURN;
else gameState = PLAYING;
globalInputCooldown = millis() + 500;
}
}
}
void handlePlaying(long newPos, bool pressed) {
if (newPos != oldEncPos) { activeCol = (newPos % 7 + 7) % 7; oldEncPos = newPos; lastActivityTime = millis(); }
renderBoard();
leds[getIdx(activeCol, 0)] = playerColor(currentPlayer);
FastLED.show();
if (pressed) {
int row = getFirstEmptyRow(activeCol);
if (row != -1) {
animateDrop(activeCol, currentPlayer);
if (!checkGameEnd()) {
if (menuMode < 2) gameState = AI_TURN;
else currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
lastActivityTime = millis();
}
}
}
void handleAiTurn() {
int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove(aiP);
if (abortAi) { gameState = MENU; showMenu(); return; }
if (!checkGameEnd()) {
gameState = PLAYING;
currentPlayer = (aiP == 1) ? 2 : 1;
}
lastActivityTime = millis();
}
void handleDemo() {
renderBoard(); FastLED.show(); delay(300);
performAiMove(currentPlayer);
if (abortAi) { gameState = MENU; showMenu(); globalInputCooldown = millis() + 600; lastButtonState = LOW; return; }
if (!checkGameEnd()) {
currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
}
void handleFinished() {
static uint32_t lastFlash = 0;
static bool toggle = true;
if (millis() - lastFlash > 300) {
lastFlash = millis();
toggle = !toggle;
renderBoard();
for (int i = 0; i < NUM_LEDS; i++) {
#if SHOW_BORDER == 1
if (leds[i] == CRGB::Blue) continue;
#endif
if (gameState == FINISHED_WIN) {
if (winMask[i]) leds[i] = toggle ? playerColor(winnerPlayer) : CRGB::Black;
else { CRGB c = leds[i]; c.nscale8(60); leds[i] = c; }
} else if (gameState == FINISHED_DRAW) {
if (!toggle) leds[i] = CRGB::Black;
}
}
FastLED.show();
}
if (millis() - demoResetTimer > 30000) {
resetBoard();
randomizeDemoPlies();
gameState = DEMO;
demoResetTimer = 0;
lastActivityTime = millis();
}
}
// --- Main ---
void setup() {
Serial.begin(115200);
myEnc = new Encoder(ENC_A, ENC_B);
prefs.begin("c4-game", false);
currentLookAhead = prefs.getUChar("ply", 8);
currentBrightness = prefs.getUChar("br", 25);
currentIdleTimeoutMs = prefs.getUInt("idle", 60) * 1000;
blunderEnabled = prefs.getBool("blunder", false);
progressiveDifficulty = prefs.getBool("evolve", false);
loadGameLog();
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.setBrightness(currentBrightness);
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();
}
void loop() {
long newPos = myEnc->read() / SENSITIVITY;
bool pressed = (digitalRead(ENC_SW) == LOW);
server.handleClient();
long rawPos = myEnc.read();
long newPos = rawPos / SENSITIVITY;
bool currentButton = digitalRead(ENC_SW);
bool pressed = false;
if (currentButton == LOW && lastButtonState == HIGH) { if (millis() > globalInputCooldown) pressed = true; }
lastButtonState = currentButton;
if (gameState == MENU) {
if (newPos != oldEncPos) {
menuMode = (newPos % 3 + 3) % 3;
oldEncPos = newPos;
showMenu();
// Interrupt: return to menu from finished/demo states
if ((newPos != oldEncPos || pressed) && (gameState == FINISHED_WIN || gameState == FINISHED_DRAW || gameState == DEMO)) {
abortAi = true;
resetBoard();
for (int i = 0; i < 10; i++) { fadeToBlackBy(leds, NUM_LEDS, 50); FastLED.show(); delay(15); }
gameState = MENU; showMenu(); oldEncPos = newPos; lastActivityTime = millis();
globalInputCooldown = millis() + 600;
return;
}
if (pressed) {
memset(board, 0, sizeof(board));
gameState = PLAYING;
// If Single Player RED, computer (1/Yellow) starts
if (menuMode == 1) {
// Idle timeout: enter demo mode
if (gameState != DEMO && gameState != FINISHED_WIN && gameState != FINISHED_DRAW) {
if (millis() - lastActivityTime > currentIdleTimeoutMs) {
resetBoard();
randomizeDemoPlies();
gameState = DEMO;
currentPlayer = 1;
renderBoard(); FastLED.show();
performAiMove();
currentPlayer = 2; // Set back to player
} else {
currentPlayer = 1; // Human starts
}
delay(300);
return;
}
}
else if (gameState == PLAYING) {
if (newPos != oldEncPos) {
activeCol = (newPos % 7 + 7) % 7;
oldEncPos = newPos;
}
renderBoard();
leds[getIdx(activeCol, 0)] = (currentPlayer == 1) ? CRGB::Yellow : CRGB::Red;
FastLED.show();
if (pressed) {
int row = getFirstEmptyRow(activeCol);
if (row != -1) {
board[activeCol][row] = currentPlayer;
renderBoard(); FastLED.show();
// 1. Check if the move just made ended the game
if (scanBoard(currentPlayer)) {
gameState = FINISHED_WIN;
} else if (isBoardFull()) {
gameState = FINISHED_DRAW;
} else {
// 2. Handle Turn Switching
if (menuMode < 2) { // Single Player
int8_t aiP = (menuMode == 0) ? 2 : 1;
performAiMove();
if (scanBoard(aiP)) {
currentPlayer = aiP; // For the flashing color
gameState = FINISHED_WIN;
} else if (isBoardFull()) {
gameState = FINISHED_DRAW;
}
} else { // PvP
currentPlayer = (currentPlayer == 1) ? 2 : 1;
}
}
} else {
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 {
static unsigned long lastFlash = 0;
static bool toggle = true;
if (millis() - lastFlash > 300) {
lastFlash = millis(); toggle = !toggle;
renderBoard();
for (int i = 0; i < NUM_LEDS; i++) {
if (gameState == FINISHED_WIN) {
if (winMask[i]) leds[i] = toggle ? (currentPlayer == 1 ? CRGB::Yellow : CRGB::Red) : CRGB::Black;
else if (leds[i] && leds[i] != CRGB::Blue) leds[i].nscale8(40);
} else {
if (leds[i] && leds[i] != CRGB::Blue) leds[i] = toggle ? leds[i] : CRGB::Black;
}
}
FastLED.show();
}
if (pressed) { gameState = MENU; showMenu(); delay(300); }
switch (gameState) {
case MENU: handleMenu(newPos, pressed); break;
case PLAYING: handlePlaying(newPos, pressed); break;
case AI_TURN: handleAiTurn(); break;
case DEMO: handleDemo(); break;
case FINISHED_WIN:
case FINISHED_DRAW: handleFinished(); break;
}
}
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" },
]