Preperly functional Docker instance created and working

This commit is contained in:
mwisnowski 2025-08-21 10:28:10 -07:00
parent ada2403c40
commit 661bf236d9
11 changed files with 189 additions and 4900 deletions

295
DOCKER.md
View file

@ -1,8 +1,10 @@
# Docker Usage Guide for MTG Deckbuilder
# Docker Guide for MTG Python Deckbuilder
## Quick Start (Recommended)
A comprehensive guide for running the MTG Python Deckbuilder in Docker containers with full file persistence and cross-platform support.
### Linux/Remote Host (Interactive Applications)
## 🚀 Quick Start
### Linux/macOS/Remote Host
```bash
# Make scripts executable (one time only)
chmod +x quick-start.sh run-docker-linux.sh
@ -10,42 +12,16 @@ chmod +x quick-start.sh run-docker-linux.sh
# Simplest method - just run this:
./quick-start.sh
# Or use the full script with more options:
# Or use the full script with options:
./run-docker-linux.sh compose
```
### Windows (PowerShell)
```powershell
# Run with Docker Compose
# Run with Docker Compose (recommended)
.\run-docker.ps1 compose
```
## Important: Interactive Applications & Docker Compose
**Your MTG Deckbuilder is an interactive application** that uses menus and requires keyboard input. This creates special requirements:
### ✅ What Works for Interactive Apps:
- `docker run -it` (manual)
- `docker-compose run` (recommended)
- `./quick-start.sh` (easiest)
### ❌ What Doesn't Work:
- `docker-compose up` (runs in background, no interaction)
- Running without `-it` flags
### Why the Difference?
- **`docker-compose up`**: Starts services in the background, doesn't attach to your terminal
- **`docker-compose run`**: Creates a new container and attaches to your terminal for interaction
## Manual Docker Commands
### Windows PowerShell
```powershell
# Build the image
docker build -t mtg-deckbuilder .
# Run with volume mounting for file persistence
# Or manual Docker run
docker run -it --rm `
-v "${PWD}/deck_files:/app/deck_files" `
-v "${PWD}/logs:/app/logs" `
@ -53,12 +29,83 @@ docker run -it --rm `
mtg-deckbuilder
```
### Linux/macOS/Git Bash
```bash
# Build the image
docker build -t mtg-deckbuilder .
## 📋 Prerequisites
# Run with volume mounting for file persistence
- **Docker** installed and running
- **Docker Compose** (usually included with Docker)
- Basic terminal/command line knowledge
## 🔧 Available Commands
### Quick Start Scripts
| Script | Platform | Description |
|--------|----------|-------------|
| `./quick-start.sh` | Linux/macOS | Simplest way to run the application |
| `.\run-docker.ps1 compose` | Windows | PowerShell equivalent |
### Full Featured Scripts
| Command | Description |
|---------|-------------|
| `./run-docker-linux.sh setup` | Create directories and check Docker installation |
| `./run-docker-linux.sh build` | Build the Docker image |
| `./run-docker-linux.sh compose` | Run with Docker Compose (recommended) |
| `./run-docker-linux.sh run` | Run with manual volume mounting |
| `./run-docker-linux.sh clean` | Remove containers and images |
## 🗂️ File Persistence
Your files are automatically saved to local directories that persist between runs:
```
mtg_python_deckbuilder/
├── deck_files/ # Your saved decks (CSV and TXT files)
├── logs/ # Application logs and debug info
├── csv_files/ # Card database and color-sorted files
└── ...
```
### How It Works
The Docker container uses **volume mounting** to map container directories to your local filesystem:
- Container path `/app/deck_files` ↔ Host path `./deck_files`
- Container path `/app/logs` ↔ Host path `./logs`
- Container path `/app/csv_files` ↔ Host path `./csv_files`
When the application saves files, they appear in your local directories and remain there after the container stops.
## 🎮 Interactive Application Requirements
The MTG Deckbuilder is an **interactive application** that uses menus and requires keyboard input.
### ✅ Commands That Work
- `docker compose run --rm mtg-deckbuilder`
- `docker run -it --rm mtg-deckbuilder`
- `./quick-start.sh`
- Helper scripts with `compose` command
### ❌ Commands That Don't Work
- `docker compose up` (runs in background, no interaction)
- `docker run` without `-it` flags
- Any command without proper TTY allocation
### Why the Difference?
- **`docker compose run`**: Creates new container with terminal attachment
- **`docker compose up`**: Starts service in background without terminal
## 🔨 Manual Docker Commands
### Build the Image
```bash
docker build -t mtg-deckbuilder .
```
### Run with Full Volume Mounting
**Linux/macOS:**
```bash
docker run -it --rm \
-v "$(pwd)/deck_files:/app/deck_files" \
-v "$(pwd)/logs:/app/logs" \
@ -66,97 +113,109 @@ docker run -it --rm \
mtg-deckbuilder
```
## File Persistence Explained
The key to saving your files is **volume mounting**. Here's what happens:
### Without Volume Mounting (Bad)
- Files are saved inside the container
- When container stops, files are lost forever
- Example: `docker run -it mtg-deckbuilder`
### With Volume Mounting (Good)
- Files are saved to your local directories
- Files persist between container runs
- Local directories are "mounted" into the container
- Example: `docker run -it -v "./deck_files:/app/deck_files" mtg-deckbuilder`
## Directory Structure After Running
After running the Docker container, you'll have these local directories:
```
mtg_python_deckbuilder/
├── deck_files/ # Your saved decks (CSV and TXT files)
├── logs/ # Application logs
├── csv_files/ # Card database files
└── ...
**Windows PowerShell:**
```powershell
docker run -it --rm `
-v "${PWD}/deck_files:/app/deck_files" `
-v "${PWD}/logs:/app/logs" `
-v "${PWD}/csv_files:/app/csv_files" `
mtg-deckbuilder
```
## Troubleshooting
## 📁 Docker Compose Files
### Files Still Not Saving?
The project includes two Docker Compose configurations:
1. **Check directory creation**: The helper scripts automatically create the needed directories
2. **Verify volume mounts**: Look for `-v` flags in your docker run command
3. **Check permissions**: Make sure you have write access to the local directories
### `docker-compose.yml` (Main)
- Standard configuration
- Container name: `mtg-deckbuilder-main`
- Use with: `docker compose run --rm mtg-deckbuilder`
### `docker-compose.interactive.yml` (Alternative)
- Identical functionality
- Container name: `mtg-deckbuilder-interactive`
- Use with: `docker compose -f docker compose.interactive.yml run --rm mtg-deckbuilder-interactive`
Both files provide the same functionality and file persistence.
## 🐛 Troubleshooting
### Files Not Saving?
1. **Check volume mounts**: Ensure you see `-v` flags in your docker command
2. **Verify directories exist**: Scripts automatically create needed directories
3. **Check permissions**: Ensure you have write access to the project directory
4. **Use correct command**: Use `docker compose run`, not `docker compose up`
### Application Won't Start Interactively?
1. **Use the right command**: `docker compose run --rm mtg-deckbuilder`
2. **Check TTY allocation**: Ensure `-it` flags are present in manual commands
3. **Avoid background mode**: Don't use `docker compose up` for interactive apps
### Permission Issues?
Files created by Docker may be owned by `root`. This is normal on Linux systems.
### Container Build Fails?
1. **Update Docker**: Ensure you have a recent version
2. **Clear cache**: Run `docker system prune -f`
3. **Check network**: Ensure Docker can download dependencies
### Starting Fresh
```powershell
# Windows - Clean up everything
.\run-docker.ps1 clean
# Or manually
docker-compose down
docker rmi mtg-deckbuilder
```
### Container Won't Start
1. Make sure Docker Desktop is running
2. Try rebuilding: `.\run-docker.ps1 build`
3. Check for port conflicts
4. Review Docker logs: `docker logs mtg-deckbuilder`
## Helper Script Commands
### Windows PowerShell
```powershell
.\run-docker.ps1 build # Build the Docker image
.\run-docker.ps1 run # Run with manual volume mounting
.\run-docker.ps1 compose # Run with Docker Compose (recommended)
.\run-docker.ps1 clean # Clean up containers and images
.\run-docker.ps1 help # Show help
```
### Linux/macOS
**Complete cleanup:**
```bash
./run-docker.sh build # Build the Docker image
./run-docker.sh run # Run with manual volume mounting
./run-docker.sh compose # Run with Docker Compose (recommended)
./run-docker.sh clean # Clean up containers and images
./run-docker.sh help # Show help
# Stop all containers
docker compose down
docker compose -f docker-compose.interactive.yml down
# Remove image
docker rmi mtg-deckbuilder
# Clean up system
docker system prune -f
# Rebuild
docker compose build
```
## Why Docker Compose is Recommended
## 🔍 Verifying Everything Works
Docker Compose offers several advantages:
After running the application:
1. **Simpler commands**: Just `docker-compose up`
2. **Configuration in file**: All settings stored in `docker-compose.yml`
3. **Automatic cleanup**: Containers are removed when stopped
4. **Consistent behavior**: Same setup every time
## Verifying File Persistence
After running the application and creating/saving files:
1. Exit the Docker container
2. Check your local directories:
```powershell
ls deck_files # Should show your saved deck files
ls logs # Should show log files
ls csv_files # Should show card database files
1. **Create or modify some data** (run setup, build a deck, etc.)
2. **Exit the container** (Ctrl+C or select Quit)
3. **Check your local directories**:
```bash
ls -la deck_files/ # Should show any decks you created
ls -la logs/ # Should show log files
ls -la csv_files/ # Should show card database files
```
3. Run the container again - your files should still be there!
4. **Run again** - your data should still be there!
## 🎯 Best Practices
1. **Use the quick-start script** for simplest experience
2. **Always use `docker compose run`** for interactive applications
3. **Keep your project directory organized** - files persist locally
4. **Regularly backup your `deck_files/`** if you create valuable decks
5. **Use `clean` commands** to free up disk space when needed
## 🌟 Benefits of Docker Approach
- ✅ **Consistent environment** across different machines
- ✅ **No Python installation required** on host system
- ✅ **Isolated dependencies** - won't conflict with other projects
- ✅ **Easy sharing** - others can run your setup instantly
- ✅ **Cross-platform** - works on Windows, macOS, and Linux
- ✅ **File persistence** - your work is saved locally
- ✅ **Easy cleanup** - remove everything with one command
---
**Need help?** Check the troubleshooting section above or refer to the helper script help:
```bash
./run-docker-linux.sh help
```

View file

@ -25,18 +25,22 @@ COPY csv_files/ ./csv_files/
COPY mypy.ini .
# Create necessary directories as mount points
RUN mkdir -p deck_files logs
RUN mkdir -p deck_files logs csv_files
# Create volumes for persistent data
VOLUME ["/app/deck_files", "/app/logs", "/app/csv_files"]
# Set the working directory to code for proper imports
WORKDIR /app/code
# Create symbolic links so the app can find the data directories
RUN ln -sf /app/deck_files ./deck_files && \
# Create symbolic links BEFORE changing working directory
RUN cd /app/code && \
ln -sf /app/deck_files ./deck_files && \
ln -sf /app/logs ./logs && \
ln -sf /app/csv_files ./csv_files
# Verify symbolic links were created
RUN cd /app/code && ls -la deck_files logs csv_files
# Set the working directory to code for proper imports
WORKDIR /app/code
# Run the application
CMD ["python", "main.py"]

View file

@ -364,7 +364,7 @@ LAND_REMOVAL_MAX_ATTEMPTS: Final[int] = 3
PROTECTED_LANDS: Final[List[str]] = BASIC_LANDS + [land['name'] for land in KINDRED_STAPLE_LANDS]
# Other defaults
DEFAULT_CREATURE_COUNT: Final[int] = 35 # Default number of creatures
DEFAULT_CREATURE_COUNT: Final[int] = 25 # Default number of creatures
DEFAULT_REMOVAL_COUNT: Final[int] = 10 # Default number of spot removal spells
DEFAULT_WIPES_COUNT: Final[int] = 2 # Default number of board wipes

View file

@ -1,7 +0,0 @@
from .builder import DeckBuilder
from .builder_utils import *
from .builder_constants import *
__all__ = [
'DeckBuilder',
]

View file

@ -1,437 +0,0 @@
from typing import Dict, List, Final, Tuple, Union, Callable
from settings import CARD_DATA_COLUMNS as CSV_REQUIRED_COLUMNS # unified
__all__ = [
'CSV_REQUIRED_COLUMNS'
]
import ast
# Commander selection configuration
# Format string for displaying duplicate cards in deck lists
FUZZY_MATCH_THRESHOLD: Final[int] = 90 # Threshold for fuzzy name matching
MAX_FUZZY_CHOICES: Final[int] = 5 # Maximum number of fuzzy match choices
# Commander-related constants
DUPLICATE_CARD_FORMAT: Final[str] = '{card_name} x {count}'
COMMANDER_CSV_PATH: Final[str] = 'csv_files/commander_cards.csv'
DECK_DIRECTORY = '../deck_files'
COMMANDER_CONVERTERS: Final[Dict[str, str]] = {'themeTags': ast.literal_eval, 'creatureTypes': ast.literal_eval} # CSV loading converters
COMMANDER_POWER_DEFAULT: Final[int] = 0
COMMANDER_TOUGHNESS_DEFAULT: Final[int] = 0
COMMANDER_MANA_VALUE_DEFAULT: Final[int] = 0
COMMANDER_TYPE_DEFAULT: Final[str] = ''
COMMANDER_TEXT_DEFAULT: Final[str] = ''
COMMANDER_MANA_COST_DEFAULT: Final[str] = ''
COMMANDER_COLOR_IDENTITY_DEFAULT: Final[str] = ''
COMMANDER_COLORS_DEFAULT: Final[List[str]] = []
COMMANDER_CREATURE_TYPES_DEFAULT: Final[str] = ''
COMMANDER_TAGS_DEFAULT: Final[List[str]] = []
COMMANDER_THEMES_DEFAULT: Final[List[str]] = []
CARD_TYPES = ['Artifact','Creature', 'Enchantment', 'Instant', 'Land', 'Planeswalker', 'Sorcery',
'Kindred', 'Dungeon', 'Battle']
# Basic mana colors
MANA_COLORS: Final[List[str]] = ['W', 'U', 'B', 'R', 'G']
# Mana pip patterns for each color
MANA_PIP_PATTERNS: Final[Dict[str, str]] = {
color: f'{{{color}}}' for color in MANA_COLORS
}
MONO_COLOR_MAP: Final[Dict[str, Tuple[str, List[str]]]] = {
'COLORLESS': ('Colorless', ['colorless']),
'W': ('White', ['colorless', 'white']),
'U': ('Blue', ['colorless', 'blue']),
'B': ('Black', ['colorless', 'black']),
'R': ('Red', ['colorless', 'red']),
'G': ('Green', ['colorless', 'green'])
}
DUAL_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = {
'B, G': ('Golgari: Black/Green', ['B', 'G', 'B, G'], ['colorless', 'black', 'green', 'golgari']),
'B, R': ('Rakdos: Black/Red', ['B', 'R', 'B, R'], ['colorless', 'black', 'red', 'rakdos']),
'B, U': ('Dimir: Black/Blue', ['B', 'U', 'B, U'], ['colorless', 'black', 'blue', 'dimir']),
'B, W': ('Orzhov: Black/White', ['B', 'W', 'B, W'], ['colorless', 'black', 'white', 'orzhov']),
'G, R': ('Gruul: Green/Red', ['G', 'R', 'G, R'], ['colorless', 'green', 'red', 'gruul']),
'G, U': ('Simic: Green/Blue', ['G', 'U', 'G, U'], ['colorless', 'green', 'blue', 'simic']),
'G, W': ('Selesnya: Green/White', ['G', 'W', 'G, W'], ['colorless', 'green', 'white', 'selesnya']),
'R, U': ('Izzet: Blue/Red', ['U', 'R', 'U, R'], ['colorless', 'blue', 'red', 'izzet']),
'U, W': ('Azorius: Blue/White', ['U', 'W', 'U, W'], ['colorless', 'blue', 'white', 'azorius']),
'R, W': ('Boros: Red/White', ['R', 'W', 'R, W'], ['colorless', 'red', 'white', 'boros'])
}
TRI_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = {
'B, G, U': ('Sultai: Black/Blue/Green', ['B', 'G', 'U', 'B, G', 'B, U', 'G, U', 'B, G, U'],
['colorless', 'black', 'blue', 'green', 'dimir', 'golgari', 'simic', 'sultai']),
'B, G, R': ('Jund: Black/Red/Green', ['B', 'G', 'R', 'B, G', 'B, R', 'G, R', 'B, G, R'],
['colorless', 'black', 'green', 'red', 'golgari', 'rakdos', 'gruul', 'jund']),
'B, G, W': ('Abzan: Black/Green/White', ['B', 'G', 'W', 'B, G', 'B, W', 'G, W', 'B, G, W'],
['colorless', 'black', 'green', 'white', 'golgari', 'orzhov', 'selesnya', 'abzan']),
'B, R, U': ('Grixis: Black/Blue/Red', ['B', 'R', 'U', 'B, R', 'B, U', 'R, U', 'B, R, U'],
['colorless', 'black', 'blue', 'red', 'dimir', 'rakdos', 'izzet', 'grixis']),
'B, R, W': ('Mardu: Black/Red/White', ['B', 'R', 'W', 'B, R', 'B, W', 'R, W', 'B, R, W'],
['colorless', 'black', 'red', 'white', 'rakdos', 'orzhov', 'boros', 'mardu']),
'B, U, W': ('Esper: Black/Blue/White', ['B', 'U', 'W', 'B, U', 'B, W', 'U, W', 'B, U, W'],
['colorless', 'black', 'blue', 'white', 'dimir', 'orzhov', 'azorius', 'esper']),
'G, R, U': ('Temur: Blue/Green/Red', ['G', 'R', 'U', 'G, R', 'G, U', 'R, U', 'G, R, U'],
['colorless', 'green', 'red', 'blue', 'simic', 'izzet', 'gruul', 'temur']),
'G, R, W': ('Naya: Green/Red/White', ['G', 'R', 'W', 'G, R', 'G, W', 'R, W', 'G, R, W'],
['colorless', 'green', 'red', 'white', 'gruul', 'selesnya', 'boros', 'naya']),
'G, U, W': ('Bant: Blue/Green/White', ['G', 'U', 'W', 'G, U', 'G, W', 'U, W', 'G, U, W'],
['colorless', 'green', 'blue', 'white', 'simic', 'azorius', 'selesnya', 'bant']),
'R, U, W': ('Jeskai: Blue/Red/White', ['R', 'U', 'W', 'R, U', 'U, W', 'R, W', 'R, U, W'],
['colorless', 'blue', 'red', 'white', 'izzet', 'azorius', 'boros', 'jeskai'])
}
OTHER_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = {
'B, G, R, U': ('Glint: Black/Blue/Green/Red',
['B', 'G', 'R', 'U', 'B, G', 'B, R', 'B, U', 'G, R', 'G, U', 'R, U', 'B, G, R',
'B, G, U', 'B, R, U', 'G, R, U', 'B, G, R, U'],
['colorless', 'black', 'blue', 'green', 'red', 'golgari', 'rakdos', 'dimir',
'gruul', 'simic', 'izzet', 'jund', 'sultai', 'grixis', 'temur', 'glint']),
'B, G, R, W': ('Dune: Black/Green/Red/White',
['B', 'G', 'R', 'W', 'B, G', 'B, R', 'B, W', 'G, R', 'G, W', 'R, W', 'B, G, R',
'B, G, W', 'B, R, W', 'G, R, W', 'B, G, R, W'],
['colorless', 'black', 'green', 'red', 'white', 'golgari', 'rakdos', 'orzhov',
'gruul', 'selesnya', 'boros', 'jund', 'abzan', 'mardu', 'naya', 'dune']),
'B, G, U, W': ('Witch: Black/Blue/Green/White',
['B', 'G', 'U', 'W', 'B, G', 'B, U', 'B, W', 'G, U', 'G, W', 'U, W', 'B, G, U',
'B, G, W', 'B, U, W', 'G, U, W', 'B, G, U, W'],
['colorless', 'black', 'blue', 'green', 'white', 'golgari', 'dimir', 'orzhov',
'simic', 'selesnya', 'azorius', 'sultai', 'abzan', 'esper', 'bant', 'witch']),
'B, R, U, W': ('Yore: Black/Blue/Red/White',
['B', 'R', 'U', 'W', 'B, R', 'B, U', 'B, W', 'R, U', 'R, W', 'U, W', 'B, R, U',
'B, R, W', 'B, U, W', 'R, U, W', 'B, R, U, W'],
['colorless', 'black', 'blue', 'red', 'white', 'rakdos', 'dimir', 'orzhov',
'izzet', 'boros', 'azorius', 'grixis', 'mardu', 'esper', 'jeskai', 'yore']),
'G, R, U, W': ('Ink: Blue/Green/Red/White',
['G', 'R', 'U', 'W', 'G, R', 'G, U', 'G, W', 'R, U', 'R, W', 'U, W', 'G, R, U',
'G, R, W', 'G, U, W', 'R, U, W', 'G, R, U, W'],
['colorless', 'blue', 'green', 'red', 'white', 'gruul', 'simic', 'selesnya',
'izzet', 'boros', 'azorius', 'temur', 'naya', 'bant', 'jeskai', 'ink']),
'B, G, R, U, W': ('WUBRG: All colors',
['B', 'G', 'R', 'U', 'W', 'B, G', 'B, R', 'B, U', 'B, W', 'G, R', 'G, U',
'G, W', 'R, U', 'R, W', 'U, W', 'B, G, R', 'B, G, U', 'B, G, W', 'B, R, U',
'B, R, W', 'B, U, W', 'G, R, U', 'G, R, W', 'G, U, W', 'R, U, W',
'B, G, R, U', 'B, G, R, W', 'B, G, U, W', 'B, R, U, W', 'G, R, U, W',
'B, G, R, U, W'],
['colorless', 'black', 'green', 'red', 'blue', 'white', 'golgari', 'rakdos',
'dimir', 'orzhov', 'gruul', 'simic', 'selesnya', 'izzet', 'boros', 'azorius',
'jund', 'sultai', 'abzan', 'grixis', 'mardu', 'esper', 'temur', 'naya',
'bant', 'jeskai', 'glint', 'dune', 'witch', 'yore', 'ink', 'wubrg'])
}
# Price checking configuration
DEFAULT_PRICE_DELAY: Final[float] = 0.1 # Delay between price checks in seconds
MAX_PRICE_CHECK_ATTEMPTS: Final[int] = 3 # Maximum attempts for price checking
PRICE_CACHE_SIZE: Final[int] = 128 # Size of price check LRU cache
PRICE_CHECK_TIMEOUT: Final[int] = 30 # Timeout for price check requests in seconds
PRICE_TOLERANCE_MULTIPLIER: Final[float] = 1.1 # Multiplier for price tolerance
DEFAULT_MAX_CARD_PRICE: Final[float] = 20.0 # Default maximum price per card
# Deck composition defaults
DEFAULT_RAMP_COUNT: Final[int] = 8 # Default number of ramp pieces
DEFAULT_LAND_COUNT: Final[int] = 35 # Default total land count
DEFAULT_BASIC_LAND_COUNT: Final[int] = 20 # Default minimum basic lands
DEFAULT_NON_BASIC_LAND_SLOTS: Final[int] = 10 # Default number of non-basic land slots to reserve
DEFAULT_BASICS_PER_COLOR: Final[int] = 5 # Default number of basic lands to add per color
# Miscellaneous land configuration
MISC_LAND_MIN_COUNT: Final[int] = 5 # Minimum number of miscellaneous lands to add
MISC_LAND_MAX_COUNT: Final[int] = 10 # Maximum number of miscellaneous lands to add
MISC_LAND_POOL_SIZE: Final[int] = 100 # Maximum size of initial land pool to select from
# Default fetch land count
FETCH_LAND_DEFAULT_COUNT: Final[int] = 3 # Default number of fetch lands to include
# Basic Lands
BASIC_LANDS = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest']
# Basic land mappings
COLOR_TO_BASIC_LAND: Final[Dict[str, str]] = {
'W': 'Plains',
'U': 'Island',
'B': 'Swamp',
'R': 'Mountain',
'G': 'Forest',
'C': 'Wastes'
}
# Dual land type mappings
DUAL_LAND_TYPE_MAP: Final[Dict[str, str]] = {
'azorius': 'Plains Island',
'dimir': 'Island Swamp',
'rakdos': 'Swamp Mountain',
'gruul': 'Mountain Forest',
'selesnya': 'Forest Plains',
'orzhov': 'Plains Swamp',
'golgari': 'Swamp Forest',
'simic': 'Forest Island',
'izzet': 'Island Mountain',
'boros': 'Mountain Plains'
}
# Triple land type mappings
TRIPLE_LAND_TYPE_MAP: Final[Dict[str, str]] = {
'bant': 'Forest Plains Island',
'esper': 'Plains Island Swamp',
'grixis': 'Island Swamp Mountain',
'jund': 'Swamp Mountain Forest',
'naya': 'Mountain Forest Plains',
'mardu': 'Mountain Plains Swamp',
'abzan': 'Plains Swamp Forest',
'sultai': 'Swamp Forest Island',
'temur': 'Forest Island Mountain',
'jeskai': 'Island Mountain Plains'
}
# Default preference for including dual lands
DEFAULT_DUAL_LAND_ENABLED: Final[bool] = True
# Default preference for including triple lands
DEFAULT_TRIPLE_LAND_ENABLED: Final[bool] = True
SNOW_COVERED_BASIC_LANDS: Final[Dict[str, str]] = {
'W': 'Snow-Covered Plains',
'U': 'Snow-Covered Island',
'B': 'Snow-Covered Swamp',
'G': 'Snow-Covered Forest'
}
SNOW_BASIC_LAND_MAPPING: Final[Dict[str, str]] = {
'W': 'Snow-Covered Plains',
'U': 'Snow-Covered Island',
'B': 'Snow-Covered Swamp',
'R': 'Snow-Covered Mountain',
'G': 'Snow-Covered Forest',
'C': 'Wastes' # Note: No snow-covered version exists for Wastes
}
# Generic fetch lands list
GENERIC_FETCH_LANDS: Final[List[str]] = [
'Evolving Wilds',
'Terramorphic Expanse',
'Shire Terrace',
'Escape Tunnel',
'Promising Vein',
'Myriad Landscape',
'Fabled Passage',
'Terminal Moraine',
'Prismatic Vista'
]
# Kindred land constants
KINDRED_STAPLE_LANDS: Final[List[Dict[str, str]]] = [
{
'name': 'Path of Ancestry',
'type': 'Land'
},
{
'name': 'Three Tree City',
'type': 'Legendary Land'
},
{'name': 'Cavern of Souls', 'type': 'Land'}
]
# Color-specific fetch land mappings
COLOR_TO_FETCH_LANDS: Final[Dict[str, List[str]]] = {
'W': [
'Flooded Strand',
'Windswept Heath',
'Marsh Flats',
'Arid Mesa',
'Brokers Hideout',
'Obscura Storefront',
'Cabaretti Courtyard'
],
'U': [
'Flooded Strand',
'Polluted Delta',
'Scalding Tarn',
'Misty Rainforest',
'Brokers Hideout',
'Obscura Storefront',
'Maestros Theater'
],
'B': [
'Polluted Delta',
'Bloodstained Mire',
'Marsh Flats',
'Verdant Catacombs',
'Obscura Storefront',
'Maestros Theater',
'Riveteers Overlook'
],
'R': [
'Bloodstained Mire',
'Wooded Foothills',
'Scalding Tarn',
'Arid Mesa',
'Maestros Theater',
'Riveteers Overlook',
'Cabaretti Courtyard'
],
'G': [
'Wooded Foothills',
'Windswept Heath',
'Verdant Catacombs',
'Misty Rainforest',
'Brokers Hideout',
'Riveteers Overlook',
'Cabaretti Courtyard'
]
}
# Staple land conditions mapping
STAPLE_LAND_CONDITIONS: Final[Dict[str, Callable[[List[str], List[str], int], bool]]] = {
'Reliquary Tower': lambda commander_tags, colors, commander_power: True, # Always include
'Ash Barrens': lambda commander_tags, colors, commander_power: 'Landfall' not in commander_tags,
'Command Tower': lambda commander_tags, colors, commander_power: len(colors) > 1,
'Exotic Orchard': lambda commander_tags, colors, commander_power: len(colors) > 1,
'War Room': lambda commander_tags, colors, commander_power: len(colors) <= 2,
'Rogue\'s Passage': lambda commander_tags, colors, commander_power: commander_power >= 5
}
# Constants for land removal functionality
LAND_REMOVAL_MAX_ATTEMPTS: Final[int] = 3
# Protected lands that cannot be removed during land removal process
PROTECTED_LANDS: Final[List[str]] = BASIC_LANDS + [land['name'] for land in KINDRED_STAPLE_LANDS]
# Other defaults
DEFAULT_CREATURE_COUNT: Final[int] = 25 # Default number of creatures
DEFAULT_REMOVAL_COUNT: Final[int] = 10 # Default number of spot removal spells
DEFAULT_WIPES_COUNT: Final[int] = 2 # Default number of board wipes
DEFAULT_CARD_ADVANTAGE_COUNT: Final[int] = 10 # Default number of card advantage pieces
DEFAULT_PROTECTION_COUNT: Final[int] = 8 # Default number of protection spells
# Deck composition prompts
DECK_COMPOSITION_PROMPTS: Final[Dict[str, str]] = {
'ramp': 'Enter desired number of ramp pieces (default: 8):',
'lands': 'Enter desired number of total lands (default: 35):',
'basic_lands': 'Enter minimum number of basic lands (default: 20):',
'creatures': 'Enter desired number of creatures (default: 25):',
'removal': 'Enter desired number of spot removal spells (default: 10):',
'wipes': 'Enter desired number of board wipes (default: 2):',
'card_advantage': 'Enter desired number of card advantage pieces (default: 10):',
'protection': 'Enter desired number of protection spells (default: 8):',
'max_deck_price': 'Enter maximum total deck price in dollars (default: 400.0):',
'max_card_price': 'Enter maximum price per card in dollars (default: 20.0):'
}
DEFAULT_MAX_DECK_PRICE: Final[float] = 400.0 # Default maximum total deck price
BATCH_PRICE_CHECK_SIZE: Final[int] = 50 # Number of cards to check prices for in one batch
# Constants for input validation
# Type aliases
CardName = str
CardType = str
ThemeTag = str
ColorIdentity = str
ColorList = List[str]
ColorInfo = Tuple[str, List[str], List[str]]
INPUT_VALIDATION = {
'max_attempts': 3,
'default_text_message': 'Please enter a valid text response.',
'default_number_message': 'Please enter a valid number.',
'default_confirm_message': 'Please enter Y/N or Yes/No.',
'default_choice_message': 'Please select a valid option from the list.'
}
QUESTION_TYPES = [
'Text',
'Number',
'Confirm',
'Choice'
]
# Constants for theme weight management and selection
# Multiplier for initial card pool size during theme-based selection
THEME_POOL_SIZE_MULTIPLIER: Final[float] = 2.0
# Bonus multiplier for cards that match multiple deck themes
THEME_PRIORITY_BONUS: Final[float] = 1.2
# Safety multiplier to avoid overshooting target counts
THEME_WEIGHT_MULTIPLIER: Final[float] = 0.9
THEME_WEIGHTS_DEFAULT: Final[Dict[str, float]] = {
'primary': 1.0,
'secondary': 0.6,
'tertiary': 0.3,
'hidden': 0.0
}
WEIGHT_ADJUSTMENT_FACTORS: Final[Dict[str, float]] = {
'kindred_primary': 1.5, # Boost for Kindred themes as primary
'kindred_secondary': 1.3, # Boost for Kindred themes as secondary
'kindred_tertiary': 1.2, # Boost for Kindred themes as tertiary
'theme_synergy': 1.2 # Boost for themes that work well together
}
DEFAULT_THEME_TAGS = [
'Aggro', 'Aristocrats', 'Artifacts Matter', 'Big Mana', 'Blink',
'Board Wipes', 'Burn', 'Cantrips', 'Card Draw', 'Clones',
'Combat Matters', 'Control', 'Counters Matter', 'Energy',
'Enter the Battlefield', 'Equipment', 'Exile Matters', 'Infect',
'Interaction', 'Lands Matter', 'Leave the Battlefield', 'Legends Matter',
'Life Matters', 'Mill', 'Monarch', 'Protection', 'Ramp', 'Reanimate',
'Removal', 'Sacrifice Matters', 'Spellslinger', 'Stax', 'Super Friends',
'Theft', 'Token Creation', 'Tokens Matter', 'Voltron', 'X Spells'
]
# CSV processing configuration
CSV_READ_TIMEOUT: Final[int] = 30 # Timeout in seconds for CSV read operations
CSV_PROCESSING_BATCH_SIZE: Final[int] = 1000 # Number of rows to process in each batch
# CSV validation configuration
CSV_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float]]]] = {
'name': {'type': ('str', 'object'), 'required': True, 'unique': True},
'edhrecRank': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 100000},
'manaValue': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 20},
'power': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'},
'toughness': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'}
}
# (CSV_REQUIRED_COLUMNS imported from settings to avoid duplication)
# DataFrame processing configuration
BATCH_SIZE: Final[int] = 1000 # Number of records to process at once
DATAFRAME_BATCH_SIZE: Final[int] = 500 # Batch size for DataFrame operations
TRANSFORM_BATCH_SIZE: Final[int] = 250 # Batch size for data transformations
CSV_DOWNLOAD_TIMEOUT: Final[int] = 30 # Timeout in seconds for CSV downloads
PROGRESS_UPDATE_INTERVAL: Final[int] = 100 # Number of records between progress updates
# DataFrame operation timeouts
DATAFRAME_READ_TIMEOUT: Final[int] = 30 # Timeout for DataFrame read operations
DATAFRAME_WRITE_TIMEOUT: Final[int] = 30 # Timeout for DataFrame write operations
DATAFRAME_TRANSFORM_TIMEOUT: Final[int] = 45 # Timeout for DataFrame transformations
DATAFRAME_VALIDATION_TIMEOUT: Final[int] = 20 # Timeout for DataFrame validation
# Required DataFrame columns
DATAFRAME_REQUIRED_COLUMNS: Final[List[str]] = [
'name', 'type', 'colorIdentity', 'manaValue', 'text',
'edhrecRank', 'themeTags', 'keywords'
]
# DataFrame validation rules
DATAFRAME_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = {
'name': {'type': ('str', 'object'), 'required': True, 'unique': True},
'edhrecRank': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 100000},
'manaValue': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 20},
'power': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'},
'toughness': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'},
'colorIdentity': {'type': ('str', 'object'), 'required': True},
'text': {'type': ('str', 'object'), 'required': False}
}
# Card type sorting order for organizing libraries
# This constant defines the order in which different card types should be sorted
# when organizing a deck library. The order is designed to group cards logically,
# starting with Planeswalkers and ending with Lands.
CARD_TYPE_SORT_ORDER: Final[List[str]] = [
'Planeswalker', 'Battle', 'Creature', 'Instant', 'Sorcery',
'Artifact', 'Enchantment', 'Land'
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,143 +0,0 @@
#!/bin/bash
# Diagnostic script to debug Docker volume mounting issues
echo "=== MTG Deckbuilder Volume Mount Diagnostics ==="
echo "Date: $(date)"
echo "User: $(whoami)"
echo "Working Directory: $(pwd)"
echo ""
# Check if Docker is working
echo "=== Docker Info ==="
docker --version
echo ""
# Check host directories
echo "=== Host Directory Check ==="
echo "Current directory contents:"
ls -la
echo ""
echo "Checking for data directories:"
for dir in deck_files logs csv_files; do
if [ -d "$dir" ]; then
echo "$dir exists"
echo " Permissions: $(ls -ld $dir | awk '{print $1, $3, $4}')"
echo " Contents: $(ls -la $dir | wc -l) items"
if [ "$(ls -A $dir)" ]; then
echo " Files: $(ls -A $dir | head -3)"
else
echo " (empty)"
fi
else
echo "$dir missing"
echo " Creating..."
mkdir -p "$dir"
chmod 755 "$dir"
fi
echo ""
done
# Test basic Docker volume mounting
echo "=== Docker Volume Mount Test ==="
echo "Testing if Docker can write to host directories..."
docker run --rm \
-v "$(pwd)/deck_files:/test/deck_files" \
-v "$(pwd)/logs:/test/logs" \
-v "$(pwd)/csv_files:/test/csv_files" \
alpine:latest /bin/sh -c "
echo 'Container test started'
echo 'Working directory: \$(pwd)'
echo 'Mount points:'
ls -la /test/
echo ''
echo 'Testing file creation:'
echo 'test-$(date +%s)' > /test/deck_files/docker-test.txt
echo 'test-$(date +%s)' > /test/logs/docker-test.log
echo 'test-$(date +%s)' > /test/csv_files/docker-test.csv
echo 'Files created in container'
ls -la /test/*/docker-test.*
"
echo ""
echo "=== Host File Check After Docker Test ==="
echo "Checking if files were created on host:"
for dir in deck_files logs csv_files; do
echo "$dir:"
if [ -f "$dir/docker-test.txt" ] || [ -f "$dir/docker-test.log" ] || [ -f "$dir/docker-test.csv" ]; then
ls -la "$dir"/docker-test.*
else
echo " No test files found"
fi
done
# Test with the actual MTG image
echo ""
echo "=== MTG Deckbuilder Container Test ==="
echo "Testing with actual MTG deckbuilder image..."
# First check if image exists
if docker images | grep -q mtg-deckbuilder; then
echo "MTG deckbuilder image found"
docker run --rm \
-v "$(pwd)/deck_files:/app/deck_files" \
-v "$(pwd)/logs:/app/logs" \
-v "$(pwd)/csv_files:/app/csv_files" \
mtg-deckbuilder /bin/bash -c "
echo 'MTG Container test'
echo 'Working directory: \$(pwd)'
echo 'Python path: \$(which python)'
echo 'App directory contents:'
ls -la /app/
echo ''
echo 'Mount point permissions:'
ls -la /app/deck_files /app/logs /app/csv_files
echo ''
echo 'Testing file creation:'
echo 'mtg-test-$(date +%s)' > /app/deck_files/mtg-test.txt
echo 'mtg-test-$(date +%s)' > /app/logs/mtg-test.log
echo 'mtg-test-$(date +%s)' > /app/csv_files/mtg-test.csv
echo 'MTG test files created'
# Try to run a quick Python test
cd /app/code
python -c 'import os; print(\"Python can access:\", os.listdir(\"/app\"))'
python -c '
import os
from pathlib import Path
print(\"Testing Path operations:\")
deck_path = Path(\"/app/deck_files\")
print(f\"Deck path exists: {deck_path.exists()}\")
print(f\"Deck path writable: {os.access(deck_path, os.W_OK)}\")
test_file = deck_path / \"python-test.txt\"
try:
test_file.write_text(\"Python test\")
print(f\"Python write successful: {test_file.read_text()}\")
except Exception as e:
print(f\"Python write failed: {e}\")
'
"
else
echo "MTG deckbuilder image not found. Building..."
docker build -t mtg-deckbuilder .
fi
echo ""
echo "=== Final Host Check ==="
echo "Files in host directories after all tests:"
for dir in deck_files logs csv_files; do
echo "$dir:"
ls -la "$dir"/ 2>/dev/null || echo " Directory empty or inaccessible"
echo ""
done
# Cleanup test files
echo "=== Cleanup ==="
echo "Removing test files..."
rm -f deck_files/docker-test.* logs/docker-test.* csv_files/docker-test.*
rm -f deck_files/mtg-test.* logs/mtg-test.* csv_files/mtg-test.*
rm -f deck_files/python-test.* logs/python-test.* csv_files/python-test.*
echo "Diagnostics complete!"

View file

@ -1,16 +0,0 @@
services:
mtg-deckbuilder-interactive:
build: .
container_name: mtg-deckbuilder-interactive
stdin_open: true
tty: true
volumes:
- ${PWD}/deck_files:/app/deck_files
- ${PWD}/logs:/app/logs
- ${PWD}/csv_files:/app/csv_files
environment:
- PYTHONUNBUFFERED=1
- TERM=xterm-256color
- DEBIAN_FRONTEND=noninteractive
# Don't restart automatically
restart: "no"

View file

@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
requires-python = ">=3.10"
requires-python = ">=3.13" # This is what it was built with anyway
dependencies = [
"pandas>=1.5.0",
"inquirer>=3.1.3",

View file

@ -1,32 +0,0 @@
#!/bin/bash
# Simple MTG Deckbuilder runner with proper interactivity
echo "MTG Deckbuilder - Quick Start"
echo "=============================="
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Create directories if they don't exist
echo -e "${GREEN}Setting up directories...${NC}"
mkdir -p deck_files logs csv_files
# Check which compose command is available
if command -v docker-compose &> /dev/null; then
COMPOSE_CMD="docker-compose"
elif docker compose version &> /dev/null; then
COMPOSE_CMD="docker compose"
else
echo "Error: Neither docker-compose nor 'docker compose' is available"
exit 1
fi
echo -e "${GREEN}Using: $COMPOSE_CMD${NC}"
echo -e "${YELLOW}Starting MTG Deckbuilder...${NC}"
echo "Press Ctrl+C to exit when done"
echo ""
# Run with the interactive compose file
$COMPOSE_CMD -f docker-compose.interactive.yml run --rm mtg-deckbuilder-interactive