mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-21 20:40:47 +02:00
feat(owned-cards): add owned-only workflow, multi-file parsing, and recommendations export\n\n- Prompt to use only owned cards (gated by presence of lists)\n- Support .txt/.csv owned lists, multi-select; commander exempt\n- Owned-only filtering + add guard; recommendations CSV/TXT when incomplete\n- CSV Owned column when not owned-only\n- Docs and Docker updated (owned_cards + config mounts)\n- CI: Windows EXE on tag; Docker Hub tag fix (no major.minor)\n- Changelog added; RELEASE_NOTES.md ignored
This commit is contained in:
parent
5f922835a6
commit
acfb29cafb
16 changed files with 480 additions and 261 deletions
9
.github/workflows/dockerhub-publish.yml
vendored
9
.github/workflows/dockerhub-publish.yml
vendored
|
@ -53,7 +53,6 @@ jobs:
|
|||
mwisnowski/mtg-python-deckbuilder
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
org.opencontainers.image.title=MTG Python Deckbuilder
|
||||
|
@ -70,11 +69,3 @@ jobs:
|
|||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Update Docker Hub description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: mwisnowski/mtg-python-deckbuilder
|
||||
readme-filepath: ./RELEASE_NOTES_TEMPLATE.md
|
||||
short-description: "CLI MTG Commander deckbuilder with smart tagging, headless mode, CSV/TXT exports, Docker-ready."
|
||||
|
|
40
.github/workflows/github-release.yml
vendored
40
.github/workflows/github-release.yml
vendored
|
@ -7,8 +7,40 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Build Windows EXE
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: powershell
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel setuptools
|
||||
if (Test-Path 'requirements.txt') { pip install -r requirements.txt }
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Build executable (PyInstaller)
|
||||
shell: powershell
|
||||
run: |
|
||||
pyinstaller --onefile --name mtg-deckbuilder code/main.py
|
||||
if (!(Test-Path dist/mtg-deckbuilder.exe)) { throw 'Build failed: dist/mtg-deckbuilder.exe not found' }
|
||||
|
||||
- name: Upload artifact (Windows EXE)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mtg-deckbuilder-windows
|
||||
path: dist/mtg-deckbuilder.exe
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-windows
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
@ -32,11 +64,19 @@ jobs:
|
|||
echo "version=$VERSION_REF" >> $GITHUB_OUTPUT
|
||||
echo "notes_file=RELEASE_NOTES.md" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: mtg-deckbuilder-windows
|
||||
path: artifacts
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.notes.outputs.version }}
|
||||
name: ${{ steps.notes.outputs.version }}
|
||||
body_path: ${{ steps.notes.outputs.notes_file }}
|
||||
files: |
|
||||
artifacts/mtg-deckbuilder.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -12,7 +12,5 @@ build/
|
|||
csv_files/
|
||||
dist/
|
||||
logs/
|
||||
test_determinism.py
|
||||
test.py
|
||||
deterministic_test.py
|
||||
!config/deck.json
|
||||
!config/deck.json
|
||||
RELEASE_NOTES.md
|
36
CHANGELOG.md
Normal file
36
CHANGELOG.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
This format follows Keep a Changelog principles and aims for Semantic Versioning.
|
||||
|
||||
## How we version
|
||||
- Semantic Versioning: MAJOR.MINOR.PATCH (e.g., v1.2.3). Pre-releases use -alpha/-beta/-rc.
|
||||
- Tags are created as `vX.Y.Z` on the default branch; releases and Docker images use that exact version and `latest`.
|
||||
- Change entries prefer the Keep a Changelog types: Added, Changed, Fixed, Removed, Deprecated, Security.
|
||||
- Link PRs/issues inline when helpful, e.g., (#123) or [#123]. Reference-style links at the bottom are encouraged for readability.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Owned-cards workflow:
|
||||
- Prompt (only if lists exist) to "Use only owned cards?"
|
||||
- Support multiple file selection; parse `.txt` (1 per line) and `.csv` (any `name` column)
|
||||
- Owned-only mode filters the pool to owned names; commander exempt
|
||||
- Recommendations export when owned-only deck is incomplete (~1.5× missing) to `deck_files/[stem]_recommendations.csv` and `.txt`
|
||||
- CSV export includes an `Owned` column when not using owned-only
|
||||
- Windows EXE build via PyInstaller is produced on tag and attached to GitHub Releases
|
||||
|
||||
### Changed
|
||||
- Rename folder from `card_library` to `owned_cards` (env override: `OWNED_CARDS_DIR`; back-compat respected)
|
||||
- Docker assets and docs updated:
|
||||
- New volume mounts: `./owned_cards:/app/owned_cards` and `./config:/app/config`
|
||||
- Compose and helper scripts updated accordingly
|
||||
- Release notes source is `RELEASE_NOTES_TEMPLATE.md`; `RELEASE_NOTES.md` ignored
|
||||
|
||||
### Fixed
|
||||
- Docker Hub workflow no longer publishes a `major.minor` tag (e.g., `1.1`); only full semver (e.g., `1.2.3`) and `latest`
|
||||
|
||||
---
|
||||
|
||||
For prior releases, see the GitHub Releases page.
|
|
@ -16,6 +16,8 @@ docker run -it --rm `
|
|||
-v "${PWD}/deck_files:/app/deck_files" `
|
||||
-v "${PWD}/logs:/app/logs" `
|
||||
-v "${PWD}/csv_files:/app/csv_files" `
|
||||
-v "${PWD}/owned_cards:/app/owned_cards" `
|
||||
-v "${PWD}/config:/app/config" `
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
```
|
||||
|
||||
|
@ -23,6 +25,7 @@ docker run -it --rm `
|
|||
- `/app/deck_files` ↔ `./deck_files`
|
||||
- `/app/logs` ↔ `./logs`
|
||||
- `/app/csv_files` ↔ `./csv_files`
|
||||
- `/app/owned_cards` ↔ `./owned_cards` (owned cards lists: .txt/.csv)
|
||||
- Optional: `/app/config` ↔ `./config` (JSON configs for headless)
|
||||
|
||||
## Interactive vs headless
|
||||
|
@ -52,6 +55,8 @@ docker run -it --rm `
|
|||
-v "${PWD}/deck_files:/app/deck_files" `
|
||||
-v "${PWD}/logs:/app/logs" `
|
||||
-v "${PWD}/csv_files:/app/csv_files" `
|
||||
-v "${PWD}/owned_cards:/app/owned_cards" `
|
||||
-v "${PWD}/config:/app/config" `
|
||||
mtg-deckbuilder
|
||||
```
|
||||
|
||||
|
@ -59,6 +64,7 @@ docker run -it --rm `
|
|||
- No prompts? Use `docker compose run --rm` (not `up`) or add `-it` to `docker run`
|
||||
- Files not saving? Verify volume mounts and that folders exist
|
||||
- Headless not picking config? Ensure `./config` is mounted to `/app/config` and `DECK_CONFIG` points to a JSON file
|
||||
- Owned-cards prompt not seeing files? Ensure `./owned_cards` is mounted to `/app/owned_cards`
|
||||
|
||||
## Tips
|
||||
- Use `docker compose run`, not `up`, for interactive mode
|
||||
|
|
|
@ -23,12 +23,13 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||
COPY code/ ./code/
|
||||
COPY mypy.ini .
|
||||
COPY config/ ./config/
|
||||
RUN mkdir -p owned_cards
|
||||
|
||||
# Create necessary directories as mount points
|
||||
RUN mkdir -p deck_files logs csv_files config
|
||||
|
||||
# Create volumes for persistent data
|
||||
VOLUME ["/app/deck_files", "/app/logs", "/app/csv_files", "/app/config"]
|
||||
VOLUME ["/app/deck_files", "/app/logs", "/app/csv_files", "/app/config", "/app/owned_cards"]
|
||||
|
||||
# Create symbolic links BEFORE changing working directory
|
||||
# These will point to the mounted volumes
|
||||
|
@ -36,10 +37,11 @@ RUN cd /app/code && \
|
|||
ln -sf /app/deck_files ./deck_files && \
|
||||
ln -sf /app/logs ./logs && \
|
||||
ln -sf /app/csv_files ./csv_files && \
|
||||
ln -sf /app/config ./config
|
||||
ln -sf /app/config ./config && \
|
||||
ln -sf /app/owned_cards ./owned_cards
|
||||
|
||||
# Verify symbolic links were created
|
||||
RUN cd /app/code && ls -la deck_files logs csv_files config
|
||||
RUN cd /app/code && ls -la deck_files logs csv_files config owned_cards
|
||||
|
||||
# Set the working directory to code for proper imports
|
||||
WORKDIR /app/code
|
||||
|
|
BIN
README.md
BIN
README.md
Binary file not shown.
205
RELEASE_NOTES.md
205
RELEASE_NOTES.md
|
@ -1,205 +0,0 @@
|
|||
# MTG Python Deckbuilder v1.1.2 Release Notes
|
||||
|
||||
Small update focused on reliability and polish.
|
||||
|
||||
## Fixes & Improvements
|
||||
- Headless: simplified flow and removed project-specific defaults; JSON export remains opt-in in headless.
|
||||
- Config: ensured correct precedence (CLI > env > JSON > defaults) and improved tag selection by mapping tag names to indices stepwise; respected `bracket_level`.
|
||||
- Data freshness: auto-refreshes card data if missing or older than 7 days and enforces re-tagging when needed via a `.tagging_complete.json` flag.
|
||||
- Tagging: fixed Explore/Map pattern error by treating "+1/+1 counter" as a literal; minor stability tweaks.
|
||||
- Docker: image now ships a default `config/` and docs/scripts clarify mounting `./config` for headless.
|
||||
|
||||
---
|
||||
|
||||
# MTG Python Deckbuilder v1.1.0 Release Notes
|
||||
|
||||
Note: Future releases will generate this file from `RELEASE_NOTES_TEMPLATE.md` automatically in CI.
|
||||
|
||||
## Highlights
|
||||
- Headless mode via submenu in the main menu (auto-runs single config; lists multiple as "Commander - Theme1, Theme2, Theme3"; `deck.json` shows as "Default")
|
||||
- Config precedence: CLI > env > JSON > defaults; honors `ideal_counts` in JSON
|
||||
- Exports: CSV/TXT always; JSON run-config only for interactive runs (headless skips it)
|
||||
- Docs simplified: concise README and Docker guide; PowerShell examples included
|
||||
|
||||
## Docker
|
||||
- Single service with persistent volumes:
|
||||
- /app/deck_files
|
||||
- /app/logs
|
||||
- /app/csv_files
|
||||
- Optional: /app/config for JSON configs
|
||||
|
||||
### Quick Start (PowerShell)
|
||||
```powershell
|
||||
# From Docker Hub
|
||||
docker run -it --rm `
|
||||
-v "${PWD}/deck_files:/app/deck_files" `
|
||||
-v "${PWD}/logs:/app/logs" `
|
||||
-v "${PWD}/csv_files:/app/csv_files" `
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
|
||||
# From source with Compose
|
||||
docker compose build
|
||||
docker compose run --rm mtg-deckbuilder
|
||||
|
||||
# Headless (optional)
|
||||
docker compose run --rm -e DECK_MODE=headless mtg-deckbuilder
|
||||
# With JSON config
|
||||
docker compose run --rm -e DECK_MODE=headless -e DECK_CONFIG=/app/config/deck.json mtg-deckbuilder
|
||||
```
|
||||
|
||||
## Changes
|
||||
- Added headless runner and headless submenu
|
||||
- Suppressed JSON run-config export for headless runs
|
||||
- `ideal_counts` in JSON now honored by prompts; only `fetch_count` tracked for lands
|
||||
- Documentation trimmed and updated; added sample config with ideal_counts
|
||||
|
||||
### Tagging updates
|
||||
- New: Discard Matters theme – detects your discard effects and triggers; includes Madness and Blood creators; Loot/Connive/Cycling/Blood also add Discard Matters.
|
||||
- New taggers:
|
||||
- Freerunning → adds Freerunning and Cost Reduction.
|
||||
- Craft → adds Transform; conditionally Artifacts Matter, Exile Matters, Graveyard Matters.
|
||||
- Spree → adds Modal and Cost Scaling.
|
||||
- Explore/Map → adds Card Selection; Explore may add +1/+1 Counters; Map adds Tokens Matter.
|
||||
- Rad counters → adds Rad Counters.
|
||||
- Exile Matters expanded to cover Warp and Time Counters/Time Travel/Vanishing.
|
||||
- Energy enriched to also tag Resource Engine.
|
||||
- Eldrazi Spawn/Scion creators now tag Aristocrats and Ramp (replacing prior Sacrifice Fodder mapping).
|
||||
|
||||
## Known Issues
|
||||
- First run downloads card data (takes a few minutes)
|
||||
- Use `docker compose run --rm` (not `up`) for interactive sessions
|
||||
- Ensure volumes are mounted to persist files outside the container
|
||||
|
||||
---
|
||||
|
||||
# MTG Python Deckbuilder v1.0.0 Release Notes
|
||||
|
||||
## 🎉 Initial Release
|
||||
|
||||
This is the first stable release of the MTG Python Deckbuilder - a comprehensive command-line tool for building and analyzing Magic: The Gathering Commander/EDH decks.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
### Core Functionality
|
||||
- **Deck Building**: Create and manage Commander/EDH decks with intelligent card suggestions
|
||||
- **Theme Detection**: Automatically identify and suggest cards based on deck themes and strategies
|
||||
- **Color Identity Support**: Filter cards based on Commander color identity rules
|
||||
- **CSV File Management**: Efficient storage and retrieval of card data
|
||||
- **Card Database**: Comprehensive MTG card database with regular updates
|
||||
- **Instant Export**: Completed deck lists are automatically displayed for easy copy/paste to online deck builders like Moxfield
|
||||
|
||||
### Setup & Management
|
||||
- **Initial Setup**: Automated download and processing of MTG card data
|
||||
- **CSV File Tagging**: Automatically tag cards with themes and strategies
|
||||
- **Commander Validation**: Verify commander legality and format compliance
|
||||
|
||||
### Planned Features
|
||||
- **Price Checking**: From the initial unpolished build I have plans to leverage Scrython for price information (using cheapest print)
|
||||
- **Deck Value**: From the price checking, there's plans to track the deck value, assign a max deck value, and a max per card value
|
||||
- **Non-Singleton Cards**: Also from an unpolished build there's remnants for adding and tracking cards you can have multiple copies of (i.e. Nazgul or Hare Apparent) and use these as a "Hidden" theme
|
||||
- **Further Tag Refinment**: I'm sure there's some missing themes or mis tags, there's honestly far too many cards for me to want to read through and make sure everything is correct, but this will be an evolving project
|
||||
|
||||
## 🐳 Docker Support
|
||||
|
||||
### Easy Deployment
|
||||
- **Cross-platform**: Works on Windows, macOS, and Linux
|
||||
- **No Python Required**: Run without installing Python locally
|
||||
- **File Persistence**: Your decks and data persist between container runs
|
||||
- **Interactive Terminal**: Full menu and keyboard interaction support
|
||||
|
||||
### Quick Start
|
||||
```bash
|
||||
# Linux/macOS
|
||||
./quick-start.sh
|
||||
|
||||
# Windows PowerShell
|
||||
.\run-docker.ps1 compose
|
||||
```
|
||||
|
||||
## 📦 Installation Options
|
||||
|
||||
### Option 1: Docker Hub (Easiest)
|
||||
```bash
|
||||
# Create a directory for your decks
|
||||
mkdir mtg-decks && cd mtg-decks
|
||||
|
||||
# Run directly from Docker Hub
|
||||
docker run -it --rm \
|
||||
-v "$(pwd)/deck_files":/app/deck_files \
|
||||
-v "$(pwd)/logs":/app/logs \
|
||||
-v "$(pwd)/csv_files":/app/csv_files \
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
```
|
||||
|
||||
**Windows Docker Desktop Users**: See `WINDOWS_DOCKER_GUIDE.md` for detailed Windows-specific instructions including GUI setup.
|
||||
|
||||
### Option 2: Docker from Source (Recommended for Development)
|
||||
1. Clone the repository
|
||||
2. Ensure Docker is installed
|
||||
3. Run `./quick-start.sh` (Linux/macOS) or `.\run-docker.ps1 compose` (Windows)
|
||||
|
||||
### Option 3: From Source
|
||||
```bash
|
||||
git clone https://github.com/mwisnowski/mtg_python_deckbuilder.git
|
||||
cd mtg_python_deckbuilder
|
||||
pip install -r requirements.txt
|
||||
python code/main.py
|
||||
```
|
||||
|
||||
## 🗂️ File Structure
|
||||
|
||||
After running, you'll have:
|
||||
```
|
||||
mtg_python_deckbuilder/
|
||||
├── deck_files/ # Your saved decks (CSV and TXT files)
|
||||
├── logs/ # Application logs
|
||||
├── csv_files/ # Card database files
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🔧 System Requirements
|
||||
|
||||
- **Docker**: Latest version recommended
|
||||
- **Python**: 3.11+ (if running from source)
|
||||
- **Memory**: 2GB+ RAM for card database processing
|
||||
- **Storage**: 500MB+ for card data and decks
|
||||
|
||||
## 📋 Dependencies
|
||||
|
||||
### Core Dependencies
|
||||
- pandas >= 1.5.0
|
||||
- inquirer >= 3.1.3
|
||||
- scrython >= 1.10.0
|
||||
- numpy >= 1.24.0
|
||||
- requests >= 2.31.0
|
||||
|
||||
### Development Dependencies
|
||||
- mypy >= 1.3.0
|
||||
- pandas-stubs >= 2.0.0
|
||||
- pytest >= 8.0.0
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
- Initial setup requires internet connection for card data download
|
||||
- Large card database may take time to process on first run
|
||||
- File permissions may show as 'root' when using Docker (normal behavior)
|
||||
|
||||
## 🔄 Breaking Changes
|
||||
|
||||
N/A - Initial release
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- MTG JSON for comprehensive card data
|
||||
- The Python community for excellent libraries
|
||||
- Magic: The Gathering players and deck builders
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/mwisnowski/mtg_python_deckbuilder/issues)
|
||||
- **Documentation**: See README.md and DOCKER.md
|
||||
- **Docker Help**: `./run-docker.sh help`
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: This is the initial release
|
|
@ -1,19 +1,24 @@
|
|||
# MTG Python Deckbuilder ${VERSION}
|
||||
|
||||
## Highlights
|
||||
- Owned cards: prompt after commander to "Use only owned cards?"; supports `.txt`/`.csv` lists in `owned_cards/`.
|
||||
- Owned-only builds filter the pool by your lists; if the deck can't reach 100, it remains incomplete and notes it.
|
||||
- Recommendations: on incomplete owned-only builds, exports `deck_files/[stem]_recommendations.csv` and `.txt` with ~1.5× missing cards, and prints a short notice.
|
||||
- Owned column: when not using owned-only, owned cards are marked with an `Owned` column in the final CSV.
|
||||
- Headless support: run non-interactively or via the menu's headless submenu.
|
||||
- Config precedence: CLI > env > JSON > defaults; `ideal_counts` in JSON are honored.
|
||||
- Exports: CSV/TXT always; JSON run-config is exported for interactive runs. In headless, JSON export is opt-in via `HEADLESS_EXPORT_JSON`.
|
||||
- Power bracket: set interactively or via `bracket_level` (env: `DECK_BRACKET_LEVEL`).
|
||||
- Data freshness: auto-refreshes `cards.csv` if missing or older than 7 days and re-tags when needed using `.tagging_complete.json`.
|
||||
- Docker: ships a default `config/` in the image; mount `./config` to `/app/config` to use your own.
|
||||
- Docker: mount `./owned_cards` to `/app/owned_cards` to enable owned-cards features; `./config` to `/app/config` for JSON configs.
|
||||
|
||||
## Docker
|
||||
- Single service; persistent volumes:
|
||||
- /app/deck_files
|
||||
- /app/logs
|
||||
- /app/csv_files
|
||||
- Optional: /app/config (mount `./config` for JSON configs)
|
||||
- /app/owned_cards
|
||||
- /app/config (mount `./config` for JSON configs)
|
||||
|
||||
### Quick Start
|
||||
```powershell
|
||||
|
@ -22,6 +27,8 @@ docker run -it --rm `
|
|||
-v "${PWD}/deck_files:/app/deck_files" `
|
||||
-v "${PWD}/logs:/app/logs" `
|
||||
-v "${PWD}/csv_files:/app/csv_files" `
|
||||
-v "${PWD}/owned_cards:/app/owned_cards" `
|
||||
-v "${PWD}/config:/app/config" `
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
|
||||
# From source with Compose
|
||||
|
@ -35,11 +42,9 @@ docker compose run --rm -e DECK_MODE=headless -e DECK_CONFIG=/app/config/deck.js
|
|||
```
|
||||
|
||||
## Changes
|
||||
- Simplified headless runner and integrated a headless submenu in the main menu.
|
||||
- JSON export policy: headless runs skip JSON export by default; opt in with `HEADLESS_EXPORT_JSON`.
|
||||
- Correct config precedence applied consistently; tag name-to-index mapping improved for multi-step selection; `bracket_level` respected.
|
||||
- Data freshness enforcement with 7-day refresh and tagging completion flag.
|
||||
- Documentation and Docker usage clarified; default `config/` now included in the image.
|
||||
- Added owned-cards workflow, CSV Owned column, and recommendations export when owned-only builds are incomplete.
|
||||
- Docker assets updated to include `/app/owned_cards` volume and mount examples.
|
||||
- Windows release workflow now attaches a PyInstaller-built EXE to GitHub Releases.
|
||||
|
||||
### Tagging updates
|
||||
- Explore/Map: fixed a pattern issue by treating "+1/+1 counter" as a literal; Explore adds Card Selection and may add +1/+1 Counters; Map adds Card Selection and Tokens Matter.
|
||||
|
|
|
@ -36,6 +36,8 @@ docker run -it --rm `
|
|||
-v "${PWD}/deck_files:/app/deck_files" `
|
||||
-v "${PWD}/logs:/app/logs" `
|
||||
-v "${PWD}/csv_files:/app/csv_files" `
|
||||
-v "${PWD}/owned_cards:/app/owned_cards" `
|
||||
-v "${PWD}/config:/app/config" `
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
```
|
||||
|
||||
|
@ -50,6 +52,8 @@ docker run -it --rm ^
|
|||
-v "%cd%\deck_files:/app/deck_files" ^
|
||||
-v "%cd%\logs:/app/logs" ^
|
||||
-v "%cd%\csv_files:/app/csv_files" ^
|
||||
-v "%cd%\owned_cards:/app/owned_cards" ^
|
||||
-v "%cd%\config:/app/config" ^
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
```
|
||||
|
||||
|
@ -69,7 +73,9 @@ Create these folders on your computer:
|
|||
C:\mtg-decks\
|
||||
├── deck_files\
|
||||
├── logs\
|
||||
└── csv_files\
|
||||
├── csv_files\
|
||||
└── owned_cards\
|
||||
└── config\
|
||||
```
|
||||
|
||||
### Step 3: Run Container
|
||||
|
@ -145,24 +151,3 @@ C:\mtg-decks\
|
|||
├── deck_files\ # Your completed decks (.csv and .txt files)
|
||||
│ ├── Atraxa_Superfriends_20250821.csv
|
||||
│ ├── Atraxa_Superfriends_20250821.txt
|
||||
# Windows Quick Start (Docker)
|
||||
|
||||
Prerequisite: Docker Desktop running.
|
||||
|
||||
## Run (one command)
|
||||
```powershell
|
||||
$base = "C:\mtg-decks"; New-Item -ItemType Directory -Force -Path "$base\deck_files","$base\logs","$base\csv_files" | Out-Null; docker run -it --rm -v "$base\deck_files:/app/deck_files" -v "$base\logs:/app/logs" -v "$base\csv_files:/app/csv_files" mwisnowski/mtg-python-deckbuilder:latest
|
||||
```
|
||||
|
||||
Files saved to:
|
||||
- Decks: C:\mtg-decks\deck_files
|
||||
- Logs: C:\mtg-decks\logs
|
||||
- Card data: C:\mtg-decks\csv_files
|
||||
-v "${baseDir}\logs:/app/logs" `
|
||||
-v "${baseDir}\csv_files:/app/csv_files" `
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
|
||||
Write-Host "Session ended. Files saved in: $baseDir" -ForegroundColor Green
|
||||
```
|
||||
|
||||
Then run with: `powershell -ExecutionPolicy Bypass -File setup-mtg-deckbuilder.ps1`
|
||||
|
|
|
@ -120,6 +120,12 @@ class DeckBuilder(
|
|||
if hasattr(self, 'run_reporting_phase'):
|
||||
self.run_reporting_phase()
|
||||
if hasattr(self, 'export_decklist_csv'):
|
||||
# If user opted out of owned-only, silently load all owned files for marking
|
||||
try:
|
||||
if not self.use_owned_only and not self.owned_card_names:
|
||||
self._load_all_owned_silent()
|
||||
except Exception:
|
||||
pass
|
||||
csv_path = self.export_decklist_csv()
|
||||
try:
|
||||
import os as _os
|
||||
|
@ -127,6 +133,15 @@ class DeckBuilder(
|
|||
txt_path = self.export_decklist_text(filename=base + '.txt') # type: ignore[attr-defined]
|
||||
# Display the text file contents for easy copy/paste to online deck builders
|
||||
self._display_txt_contents(txt_path)
|
||||
# If owned-only build is incomplete, generate recommendations
|
||||
try:
|
||||
total_cards = sum(int(v.get('Count', 1)) for v in self.card_library.values())
|
||||
if self.use_owned_only and total_cards < 100:
|
||||
missing = 100 - total_cards
|
||||
rec_limit = int(math.ceil(1.5 * float(missing)))
|
||||
self._generate_recommendations(base_stem=base, limit=rec_limit)
|
||||
except Exception:
|
||||
pass
|
||||
# Also export a matching JSON config for replay (interactive builds only)
|
||||
if not getattr(self, 'headless', False):
|
||||
try:
|
||||
|
@ -153,6 +168,14 @@ class DeckBuilder(
|
|||
pass
|
||||
except Exception:
|
||||
logger.warning("Plaintext export failed (non-fatal)")
|
||||
# If owned-only and deck not complete, print a note
|
||||
try:
|
||||
if self.use_owned_only:
|
||||
total_cards = sum(int(v.get('Count', 1)) for v in self.card_library.values())
|
||||
if total_cards < 100:
|
||||
self.output_func(f"Note: deck is incomplete ({total_cards}/100). Not enough owned cards to fill the deck.")
|
||||
except Exception:
|
||||
pass
|
||||
end_ts = datetime.datetime.now()
|
||||
logger.info(f"=== Deck Build: COMPLETE in {(end_ts - start_ts).total_seconds():.2f}s ===")
|
||||
except KeyboardInterrupt:
|
||||
|
@ -186,8 +209,8 @@ class DeckBuilder(
|
|||
self.output_func("Ready for copy/paste to Moxfield, EDHREC, or other deck builders")
|
||||
self.output_func(f"{separator}")
|
||||
self.output_func(contents)
|
||||
self.output_func(f"{separator}")
|
||||
self.output_func(f"Deck list also saved to: {txt_path}")
|
||||
# self.output_func(f"{separator}")
|
||||
# self.output_func(f"Deck list also saved to: {txt_path}")
|
||||
self.output_func(f"{separator}\n")
|
||||
|
||||
except Exception as e:
|
||||
|
@ -237,6 +260,10 @@ class DeckBuilder(
|
|||
_commander_df: Optional[pd.DataFrame] = None
|
||||
_combined_cards_df: Optional[pd.DataFrame] = None
|
||||
_full_cards_df: Optional[pd.DataFrame] = None # immutable snapshot of original combined pool
|
||||
# Owned-cards mode
|
||||
use_owned_only: bool = False
|
||||
owned_card_names: set[str] = field(default_factory=set)
|
||||
owned_files_selected: List[str] = field(default_factory=list)
|
||||
|
||||
# Deck library (cards added so far) mapping name->record
|
||||
card_library: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
|
@ -299,6 +326,289 @@ class DeckBuilder(
|
|||
m()
|
||||
logger.info(f"Land Step {step}: complete (current land count {self._current_land_count() if hasattr(self, '_current_land_count') else 'n/a'})")
|
||||
|
||||
def _generate_recommendations(self, base_stem: str, limit: int):
|
||||
"""Silently build a full (non-owned-filtered) deck with same choices and export top recommendations.
|
||||
|
||||
- Uses same commander, tags, bracket, and ideal_counts.
|
||||
- Excludes any cards already in this deck's library.
|
||||
- Exports CSV and TXT to deck_files with suffix _recommendations.
|
||||
"""
|
||||
try:
|
||||
# Nothing to recommend if limit <= 0 or no commander
|
||||
if limit <= 0 or not self.commander_row is not None:
|
||||
return
|
||||
# Prepare a quiet builder
|
||||
def _silent_out(_msg: str) -> None:
|
||||
return None
|
||||
def _silent_in(_prompt: str) -> str:
|
||||
return ""
|
||||
rec = DeckBuilder(input_func=_silent_in, output_func=_silent_out, log_outputs=False, headless=True)
|
||||
# Carry over selections
|
||||
rec.commander_name = self.commander_name
|
||||
rec.commander_row = self.commander_row
|
||||
rec.commander_tags = list(self.commander_tags)
|
||||
rec.primary_tag = self.primary_tag
|
||||
rec.secondary_tag = self.secondary_tag
|
||||
rec.tertiary_tag = self.tertiary_tag
|
||||
rec.selected_tags = list(self.selected_tags)
|
||||
rec.bracket_definition = self.bracket_definition
|
||||
rec.bracket_level = self.bracket_level
|
||||
rec.bracket_name = self.bracket_name
|
||||
rec.bracket_limits = dict(self.bracket_limits) if self.bracket_limits else {}
|
||||
rec.ideal_counts = dict(self.ideal_counts) if self.ideal_counts else {}
|
||||
# Initialize commander dict (also adds commander to library)
|
||||
try:
|
||||
if rec.commander_row is not None:
|
||||
rec._initialize_commander_dict(rec.commander_row)
|
||||
except Exception:
|
||||
pass
|
||||
# Build on full pool (owned-only disabled by default)
|
||||
rec.determine_color_identity()
|
||||
rec.setup_dataframes()
|
||||
# Ensure bracket applied and counts present
|
||||
try:
|
||||
rec.run_deck_build_step1()
|
||||
except Exception:
|
||||
pass
|
||||
# Run the content-adding phases silently
|
||||
try:
|
||||
rec._run_land_build_steps()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(rec, 'add_creatures_phase'):
|
||||
rec.add_creatures_phase()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(rec, 'add_spells_phase'):
|
||||
rec.add_spells_phase()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(rec, 'post_spell_land_adjust'):
|
||||
rec.post_spell_land_adjust()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Build recommendation subset excluding already-chosen names
|
||||
chosen = set(self.card_library.keys())
|
||||
rec_items = []
|
||||
for nm, info in rec.card_library.items():
|
||||
if nm not in chosen:
|
||||
rec_items.append((nm, info))
|
||||
if not rec_items:
|
||||
return
|
||||
# Cap to requested limit
|
||||
rec_subset: Dict[str, Dict[str, Any]] = {}
|
||||
for nm, info in rec_items[:max(0, int(limit))]:
|
||||
rec_subset[nm] = info
|
||||
|
||||
# Temporarily export subset using the recommendation builder's context/snapshots
|
||||
original_lib = rec.card_library
|
||||
try:
|
||||
rec.card_library = rec_subset
|
||||
# Export CSV and TXT with suffix
|
||||
rec.export_decklist_csv(directory='deck_files', filename=base_stem + '_recommendations.csv', suppress_output=True) # type: ignore[attr-defined]
|
||||
rec.export_decklist_text(directory='deck_files', filename=base_stem + '_recommendations.txt', suppress_output=True) # type: ignore[attr-defined]
|
||||
finally:
|
||||
rec.card_library = original_lib
|
||||
# Notify user succinctly
|
||||
try:
|
||||
self.output_func(f"Recommended but unowned cards in deck_files/{base_stem}_recommendations.csv")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as _e:
|
||||
try:
|
||||
self.output_func(f"Failed to generate recommendations: {_e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---------------------------
|
||||
# Owned Cards Helpers
|
||||
# ---------------------------
|
||||
def _card_library_dir(self) -> str:
|
||||
"""Return folder to read owned cards from, preferring 'owned_cards'.
|
||||
|
||||
Precedence:
|
||||
- OWNED_CARDS_DIR env var
|
||||
- CARD_LIBRARY_DIR env var (back-compat)
|
||||
- 'owned_cards' if exists
|
||||
- 'card_library' if exists (back-compat)
|
||||
- default 'owned_cards'
|
||||
"""
|
||||
try:
|
||||
import os as _os
|
||||
# Env overrides
|
||||
env_dir = _os.getenv('OWNED_CARDS_DIR') or _os.getenv('CARD_LIBRARY_DIR')
|
||||
if env_dir:
|
||||
return env_dir
|
||||
# Prefer new name
|
||||
if _os.path.isdir('owned_cards'):
|
||||
return 'owned_cards'
|
||||
if _os.path.isdir('card_library'):
|
||||
return 'card_library'
|
||||
return 'owned_cards'
|
||||
except Exception:
|
||||
return 'owned_cards'
|
||||
|
||||
def _find_owned_files(self) -> List[str]:
|
||||
import os as _os
|
||||
folder = self._card_library_dir()
|
||||
try:
|
||||
entries = []
|
||||
if _os.path.isdir(folder):
|
||||
for name in _os.listdir(folder):
|
||||
p = _os.path.join(folder, name)
|
||||
if _os.path.isfile(p) and name.lower().endswith(('.txt', '.csv')):
|
||||
entries.append(p)
|
||||
return sorted(entries)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _parse_owned_line(self, line: str) -> Optional[str]:
|
||||
s = (line or '').strip()
|
||||
if not s or s.startswith('#') or s.startswith('//'):
|
||||
return None
|
||||
parts = s.split()
|
||||
if len(parts) >= 2 and (parts[0].isdigit() or (parts[0].lower().endswith('x') and parts[0][:-1].isdigit())):
|
||||
s = ' '.join(parts[1:])
|
||||
return s.strip() or None
|
||||
|
||||
def _read_txt_owned(self, path: str) -> List[str]:
|
||||
out: List[str] = []
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
n = self._parse_owned_line(line)
|
||||
if n:
|
||||
out.append(n)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
def _read_csv_owned(self, path: str) -> List[str]:
|
||||
import csv as _csv
|
||||
names: List[str] = []
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8', errors='ignore', newline='') as f:
|
||||
try:
|
||||
reader = _csv.DictReader(f)
|
||||
headers = [h.strip() for h in (reader.fieldnames or [])]
|
||||
candidates = [c for c in ('name', 'card', 'Card', 'card_name', 'Card Name') if c in headers]
|
||||
if candidates:
|
||||
key = candidates[0]
|
||||
for row in reader:
|
||||
val = (row.get(key) or '').strip()
|
||||
if val:
|
||||
names.append(val)
|
||||
else:
|
||||
f.seek(0)
|
||||
reader2 = _csv.reader(f)
|
||||
for row in reader2:
|
||||
if not row:
|
||||
continue
|
||||
val = (row[0] or '').strip()
|
||||
if val and val.lower() not in ('name', 'card', 'card name'):
|
||||
names.append(val)
|
||||
except Exception:
|
||||
# Fallback plain reader
|
||||
f.seek(0)
|
||||
for line in f:
|
||||
if line.strip():
|
||||
names.append(line.strip())
|
||||
except Exception:
|
||||
pass
|
||||
return names
|
||||
|
||||
def _load_owned_from_files(self, files: List[str]) -> set[str]:
|
||||
names: List[str] = []
|
||||
for p in files:
|
||||
pl = p.lower()
|
||||
try:
|
||||
if pl.endswith('.txt'):
|
||||
names.extend(self._read_txt_owned(p))
|
||||
elif pl.endswith('.csv'):
|
||||
names.extend(self._read_csv_owned(p))
|
||||
except Exception:
|
||||
continue
|
||||
clean = {n.strip() for n in names if isinstance(n, str) and n.strip()}
|
||||
return clean
|
||||
|
||||
def _prompt_use_owned_cards(self):
|
||||
# Quick existence check: only prompt if any owned files are present
|
||||
files = self._find_owned_files()
|
||||
if not files:
|
||||
# No owned lists present; skip prompting entirely
|
||||
return
|
||||
resp = self.input_func("Use only owned cards? (y/N): ").strip().lower()
|
||||
self.use_owned_only = (resp in ('y', 'yes'))
|
||||
if not self.use_owned_only:
|
||||
return
|
||||
self.output_func("Select owned card files by number (comma-separated), or press Enter to use all:")
|
||||
for i, p in enumerate(files):
|
||||
try:
|
||||
base = p.replace('\\', '/').split('/')[-1]
|
||||
except Exception:
|
||||
base = p
|
||||
self.output_func(f" [{i}] {base}")
|
||||
raw = self.input_func("Selection: ").strip()
|
||||
selected: List[str] = []
|
||||
if not raw:
|
||||
selected = files
|
||||
else:
|
||||
seen = set()
|
||||
for tok in raw.split(','):
|
||||
tok = tok.strip()
|
||||
if tok.isdigit():
|
||||
idx = int(tok)
|
||||
if 0 <= idx < len(files) and idx not in seen:
|
||||
selected.append(files[idx])
|
||||
seen.add(idx)
|
||||
if not selected:
|
||||
self.output_func("No valid selections; using all owned files.")
|
||||
selected = files
|
||||
self.owned_files_selected = selected
|
||||
self.owned_card_names = self._load_owned_from_files(selected)
|
||||
self.output_func(f"Owned cards loaded: {len(self.owned_card_names)} unique names from {len(selected)} file(s).")
|
||||
|
||||
# Public helper for headless/tests: enable/disable owned-only and optionally preload files
|
||||
def set_owned_mode(self, owned_only: bool, files: Optional[List[str]] = None):
|
||||
self.use_owned_only = bool(owned_only)
|
||||
if not self.use_owned_only:
|
||||
self.owned_card_names = set()
|
||||
self.owned_files_selected = []
|
||||
return
|
||||
if files is None:
|
||||
return
|
||||
# Normalize to existing files
|
||||
valid: List[str] = []
|
||||
for p in files:
|
||||
try:
|
||||
if os.path.isfile(p) and p.lower().endswith(('.txt', '.csv')):
|
||||
valid.append(p)
|
||||
else:
|
||||
# try relative to card_library
|
||||
alt = os.path.join(self._card_library_dir(), p)
|
||||
if os.path.isfile(alt) and alt.lower().endswith(('.txt', '.csv')):
|
||||
valid.append(alt)
|
||||
except Exception:
|
||||
continue
|
||||
if valid:
|
||||
self.owned_files_selected = valid
|
||||
self.owned_card_names = self._load_owned_from_files(valid)
|
||||
|
||||
# Internal helper: when user opts out, silently load all owned files for CSV flagging
|
||||
def _load_all_owned_silent(self):
|
||||
try:
|
||||
files = self._find_owned_files()
|
||||
if not files:
|
||||
return
|
||||
self.owned_files_selected = files
|
||||
self.owned_card_names = self._load_owned_from_files(files)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---------------------------
|
||||
# RNG Initialization
|
||||
# ---------------------------
|
||||
|
@ -613,6 +923,24 @@ class DeckBuilder(
|
|||
# Drop duplicate rows by 'name' if column exists
|
||||
if 'name' in combined.columns:
|
||||
combined = combined.drop_duplicates(subset='name', keep='first')
|
||||
# If owned-only mode, filter combined pool to owned names (case-insensitive)
|
||||
if self.use_owned_only:
|
||||
try:
|
||||
owned_lower = {n.lower() for n in self.owned_card_names}
|
||||
name_col = None
|
||||
if 'name' in combined.columns:
|
||||
name_col = 'name'
|
||||
elif 'Card Name' in combined.columns:
|
||||
name_col = 'Card Name'
|
||||
if name_col is not None:
|
||||
mask = combined[name_col].astype(str).str.lower().isin(owned_lower)
|
||||
prev = len(combined)
|
||||
combined = combined[mask].copy()
|
||||
self.output_func(f"Owned-only mode: filtered card pool from {prev} to {len(combined)} records.")
|
||||
else:
|
||||
self.output_func("Owned-only mode: no recognizable name column to filter on; skipping filter.")
|
||||
except Exception as _e:
|
||||
self.output_func(f"Owned-only mode: failed to filter combined pool: {_e}")
|
||||
self._combined_cards_df = combined
|
||||
# Preserve original snapshot for enrichment across subsequent removals
|
||||
if self._full_cards_df is None:
|
||||
|
@ -639,6 +967,15 @@ class DeckBuilder(
|
|||
|
||||
Stores minimal metadata; duplicates increment Count. Basic lands allowed unlimited.
|
||||
"""
|
||||
# In owned-only mode, block adding cards not in owned list (except the commander itself)
|
||||
try:
|
||||
if getattr(self, 'use_owned_only', False) and not is_commander:
|
||||
owned = getattr(self, 'owned_card_names', set()) or set()
|
||||
if owned and card_name.lower() not in {n.lower() for n in owned}:
|
||||
# Silently skip non-owned additions
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if creature_types is None:
|
||||
creature_types = []
|
||||
if tags is None:
|
||||
|
@ -802,6 +1139,12 @@ class DeckBuilder(
|
|||
if self.files_to_load:
|
||||
file_list = ", ".join(f"{stem}_cards.csv" for stem in self.files_to_load)
|
||||
self.output_func(f"Card Pool Files: {file_list}")
|
||||
# Owned-only status
|
||||
if getattr(self, 'use_owned_only', False):
|
||||
try:
|
||||
self.output_func(f"Owned-only mode: {len(self.owned_card_names)} cards from {len(self.owned_files_selected)} file(s)")
|
||||
except Exception:
|
||||
self.output_func("Owned-only mode: enabled")
|
||||
if self.selected_tags:
|
||||
self.output_func("Chosen Tags:")
|
||||
if self.primary_tag:
|
||||
|
@ -822,6 +1165,11 @@ class DeckBuilder(
|
|||
def run_initial_setup(self):
|
||||
self.choose_commander()
|
||||
self.select_commander_tags()
|
||||
# Ask if user wants to limit pool to owned cards and gather selection
|
||||
try:
|
||||
self._prompt_use_owned_cards()
|
||||
except Exception as e:
|
||||
self.output_func(f"Owned-cards prompt failed (continuing without): {e}")
|
||||
# New: color identity & card pool loading
|
||||
try:
|
||||
self.determine_color_identity()
|
||||
|
|
|
@ -166,7 +166,7 @@ class ReportingMixin:
|
|||
|
||||
headers = [
|
||||
"Name","Count","Type","ManaCost","ManaValue","Colors","Power","Toughness",
|
||||
"Role","SubRole","AddedBy","TriggerTag","Synergy","Tags","Text"
|
||||
"Role","SubRole","AddedBy","TriggerTag","Synergy","Tags","Text","Owned"
|
||||
]
|
||||
|
||||
# Precedence list for sorting
|
||||
|
@ -200,6 +200,13 @@ class ReportingMixin:
|
|||
|
||||
rows: List[tuple] = [] # (sort_key, row_data)
|
||||
|
||||
# Prepare owned lookup if available
|
||||
owned_set_lower = set()
|
||||
try:
|
||||
owned_set_lower = {n.lower() for n in (getattr(self, 'owned_card_names', set()) or set())}
|
||||
except Exception:
|
||||
owned_set_lower = set()
|
||||
|
||||
for name, info in self.card_library.items():
|
||||
base_type = info.get('Card Type') or info.get('Type','')
|
||||
base_mc = info.get('Mana Cost','')
|
||||
|
@ -250,6 +257,7 @@ class ReportingMixin:
|
|||
cat = classify(base_type, name)
|
||||
prec = precedence_index.get(cat, 999)
|
||||
# Alphabetical within category (no mana value sorting)
|
||||
owned_flag = 'Y' if (name.lower() in owned_set_lower) else ''
|
||||
rows.append(((prec, name.lower()), [
|
||||
name,
|
||||
info.get('Count',1),
|
||||
|
@ -265,7 +273,8 @@ class ReportingMixin:
|
|||
info.get('TriggerTag') or '',
|
||||
info.get('Synergy') if info.get('Synergy') is not None else '',
|
||||
tags_join,
|
||||
text_field[:800] if isinstance(text_field, str) else str(text_field)[:800]
|
||||
text_field[:800] if isinstance(text_field, str) else str(text_field)[:800],
|
||||
owned_flag
|
||||
]))
|
||||
# Now sort (category precedence, then alphabetical name)
|
||||
rows.sort(key=lambda x: x[0])
|
||||
|
|
|
@ -8,16 +8,17 @@ services:
|
|||
- ${PWD}/deck_files:/app/deck_files
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/csv_files:/app/csv_files
|
||||
# Optional: mount a config directory for headless JSON
|
||||
# Optional: mount a config directory for headless JSON and owned cards
|
||||
- ${PWD}/config:/app/config
|
||||
- ${PWD}/owned_cards:/app/owned_cards
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- TERM=xterm-256color
|
||||
- DEBIAN_FRONTEND=noninteractive
|
||||
# Set DECK_MODE=headless to auto-run non-interactive mode on start
|
||||
# - DECK_MODE=headless
|
||||
# Optional headless configuration (examples):
|
||||
# - DECK_CONFIG=/app/config/deck.json
|
||||
# - DECK_COMMANDER=Pantlaza
|
||||
# Set DECK_MODE=headless to auto-run non-interactive mode on start
|
||||
# - DECK_MODE=headless
|
||||
# Optional headless configuration (examples):
|
||||
# - DECK_CONFIG=/app/config/deck.json
|
||||
# - DECK_COMMANDER=Pantlaza
|
||||
# Ensure proper cleanup
|
||||
restart: "no"
|
||||
|
|
|
@ -7,6 +7,7 @@ if not exist "deck_files" mkdir deck_files
|
|||
if not exist "logs" mkdir logs
|
||||
if not exist "csv_files" mkdir csv_files
|
||||
if not exist "config" mkdir config
|
||||
if not exist "owned_cards" mkdir owned_cards
|
||||
|
||||
echo Starting MTG Python Deckbuilder from Docker Hub...
|
||||
echo Your files will be saved in the current directory:
|
||||
|
@ -21,6 +22,7 @@ docker run -it --rm ^
|
|||
-v "%cd%\deck_files:/app/deck_files" ^
|
||||
-v "%cd%\logs:/app/logs" ^
|
||||
-v "%cd%\csv_files:/app/csv_files" ^
|
||||
-v "%cd%\owned_cards:/app/owned_cards" ^
|
||||
-v "%cd%\config:/app/config" ^
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ echo "==========================================="
|
|||
|
||||
# Create directories if they don't exist
|
||||
mkdir -p deck_files logs csv_files
|
||||
mkdir -p config
|
||||
mkdir -p config owned_cards
|
||||
|
||||
echo "Starting MTG Python Deckbuilder from Docker Hub..."
|
||||
echo "Your files will be saved in the current directory:"
|
||||
|
@ -20,6 +20,7 @@ docker run -it --rm \
|
|||
-v "$(pwd)/deck_files":/app/deck_files \
|
||||
-v "$(pwd)/logs":/app/logs \
|
||||
-v "$(pwd)/csv_files":/app/csv_files \
|
||||
-v "$(pwd)/owned_cards":/app/owned_cards \
|
||||
-v "$(pwd)/config":/app/config \
|
||||
mwisnowski/mtg-python-deckbuilder:latest
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue