feat: add deck builder with multiple collection support
- Add build_deck.py script for automated deck building - Support multiple collection files for comprehensive deck building - Rename main collection file to full_collection.json - Add comprehensive documentation and usage examples - Include design and implementation plans - Enhance synergy detection and commander suggestion
This commit is contained in:
@@ -102,6 +102,7 @@ Array of card objects with Scryfall fields:
|
||||
|
||||
| Deck | Colors | Commander | Archetype |
|
||||
|------|--------|-----------|-----------|
|
||||
| my_choco_deck | BUW | G'raha Tia, Scion Reborn | Auto-generated |
|
||||
| Choco | UGW | Choco, Seeker of Paradise | Bird Tribal Landfall |
|
||||
| Hazel | BG | Hazel of the Rootbloom | Golgari Aristocrats |
|
||||
| Palamecia | UR | The Emperor of Palamecia // The Lord Master of Hell | Izzet Self-Mill Storm |
|
||||
|
||||
@@ -64,6 +64,7 @@ python scripts/deck_report.py --collection output/hydrated/deck.json --decks-dir
|
||||
|
||||
| Deck | Colors | Commander | Archetype |
|
||||
|------|--------|-----------|-----------|
|
||||
| my_choco_deck | BUW | G'raha Tia, Scion Reborn | Auto-generated |
|
||||
| Choco | UGW | Choco, Seeker of Paradise | Bird Tribal Landfall |
|
||||
| Hazel | BG | Hazel of the Rootbloom | Golgari Aristocrats |
|
||||
| Palamecia | UR | The Emperor of Palamecia // The Lord Master of Hell | Izzet Self-Mill Storm |
|
||||
|
||||
107
docs/plans/2026-03-03-deck-builder-design.md
Normal file
107
docs/plans/2026-03-03-deck-builder-design.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Deck Builder Design
|
||||
|
||||
## Overview
|
||||
Semi-automated tool to build Commander decks from existing card collections.
|
||||
|
||||
## Architecture
|
||||
- Script: `scripts/build_deck.py`
|
||||
- Input: Hydrated collection JSON (`output/hydrated/deck.json`)
|
||||
- Output: Deck JSON file in `data/decks/`
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Collection Analyzer
|
||||
- Analyzes color distribution across collection
|
||||
- Identifies card type distribution (creatures, spells, lands)
|
||||
- Calculates CMC distribution
|
||||
- Finds potential synergies (keywords, creature types)
|
||||
|
||||
### 2. Commander Suggester
|
||||
- Identifies legendary creatures and planeswalkers in collection
|
||||
- Scores commanders based on:
|
||||
- Color support in collection
|
||||
- Number of cards matching color identity
|
||||
- Synergistic mechanics present
|
||||
- Provides top 3-5 commander recommendations
|
||||
|
||||
### 3. Deck Generator
|
||||
- Creates starter decklist based on chosen commander
|
||||
- Includes:
|
||||
- Commander (1 card)
|
||||
- Lands (35-40 cards matching color identity)
|
||||
- Creatures, spells, and artifacts matching color identity
|
||||
- Balanced mana curve
|
||||
- Ensures deck meets Commander format requirements
|
||||
|
||||
### 4. Refinement Tools
|
||||
- Functions to add/remove specific cards
|
||||
- Balance mana curve
|
||||
- Adjust land count
|
||||
- Filter by card type or CMC
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Load hydrated collection from `output/hydrated/deck.json`
|
||||
2. Analyze collection statistics
|
||||
3. Identify potential commanders
|
||||
4. User selects commander
|
||||
5. Generate starter decklist
|
||||
6. Save deck JSON file
|
||||
7. Provide analysis report
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Validate input file exists and is properly formatted
|
||||
- Handle cases where no suitable commanders are found
|
||||
- Provide fallback options for incomplete collections
|
||||
- Validate generated deck meets format requirements
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. Test with existing collection files
|
||||
2. Verify generated decks meet Commander format requirements
|
||||
3. Check color identity matching works correctly
|
||||
4. Validate mana curve balancing
|
||||
5. Test edge cases (small collections, mono-color, multi-color)
|
||||
|
||||
## Usage Documentation
|
||||
|
||||
Add to README.md:
|
||||
|
||||
```markdown
|
||||
### Build a Deck From Your Collection
|
||||
|
||||
```bash
|
||||
# Analyze your collection and build a deck
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --name my_new_deck
|
||||
|
||||
# View suggested commanders
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --list-commanders
|
||||
|
||||
# Build deck with specific commander
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --name my_deck --commander "Choco, Seeker of Paradise"
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Analyze your collection for color distribution and card types
|
||||
2. Suggest potential commanders from your collection
|
||||
3. Generate a starter decklist matching the commander's color identity
|
||||
4. Save the deck to `data/decks/<name>.json`
|
||||
5. Provide a summary report with suggestions for improvements
|
||||
|
||||
After building, you can:
|
||||
- Manually edit the deck file
|
||||
- Run analysis to find upgrades: `python scripts/analyze_decks.py --collection output/hydrated/deck.json --deck-dir data/decks/`
|
||||
- Generate a report: `python scripts/deck_report.py --collection output/hydrated/deck.json --decks-dir data/decks/`
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Create `scripts/build_deck.py` with argparse CLI
|
||||
2. Implement collection analysis functions
|
||||
3. Build commander suggestion algorithm
|
||||
4. Create deck generation logic
|
||||
5. Add refinement tools
|
||||
6. Write comprehensive tests
|
||||
7. Update README documentation
|
||||
8. Add to pre-commit hook for auto-docs
|
||||
475
docs/plans/2026-03-03-deck-builder-implementation.md
Normal file
475
docs/plans/2026-03-03-deck-builder-implementation.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# Deck Builder Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a semi-automated deck builder that creates Commander decks from existing card collections
|
||||
|
||||
**Architecture:** Python script that analyzes collection data, suggests commanders, and generates starter decklists
|
||||
|
||||
**Tech Stack:** Python 3 standard library (argparse, json, collections)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create Deck Builder Script
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/build_deck.py`
|
||||
|
||||
**Step 1: Write script skeleton with CLI interface**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deck builder for MTG Commander decks from existing collections.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from collections import Counter
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
def load_collection(collection_path: str) -> List[dict]:
|
||||
"""Load hydrated collection from JSON file."""
|
||||
with open(collection_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def analyze_collection(cards: List[dict]) -> Dict:
|
||||
"""Analyze collection statistics."""
|
||||
colors = Counter()
|
||||
types = Counter()
|
||||
cmc_dist = Counter()
|
||||
|
||||
for card in cards:
|
||||
# Count colors
|
||||
for color in card.get('color_identity', []):
|
||||
colors[color] += card.get('count', 1)
|
||||
|
||||
# Count types
|
||||
type_line = card.get('type_line', '')
|
||||
if 'Creature' in type_line:
|
||||
types['creatures'] += card.get('count', 1)
|
||||
elif 'Instant' in type_line:
|
||||
types['instants'] += card.get('count', 1)
|
||||
elif 'Sorcery' in type_line:
|
||||
types['sorceries'] += card.get('count', 1)
|
||||
elif 'Artifact' in type_line:
|
||||
types['artifacts'] += card.get('count', 1)
|
||||
elif 'Enchantment' in type_line:
|
||||
types['enchantments'] += card.get('count', 1)
|
||||
elif 'Land' in type_line:
|
||||
types['lands'] += card.get('count', 1)
|
||||
|
||||
# Count CMC
|
||||
cmc = card.get('cmc')
|
||||
if cmc:
|
||||
cmc_dist[int(cmc)] += card.get('count', 1)
|
||||
|
||||
return {
|
||||
'colors': dict(colors),
|
||||
'types': dict(types),
|
||||
'cmc_distribution': dict(cmc_dist),
|
||||
'total_cards': sum(card.get('count', 1) for card in cards),
|
||||
'unique_cards': len(cards)
|
||||
}
|
||||
|
||||
|
||||
def find_commanders(cards: List[dict]) -> List[dict]:
|
||||
"""Find potential commander cards in collection."""
|
||||
commanders = []
|
||||
for card in cards:
|
||||
type_line = card.get('type_line', '')
|
||||
if ('Legendary' in type_line and
|
||||
('Creature' in type_line or 'Planeswalker' in type_line)):
|
||||
commanders.append(card)
|
||||
return commanders
|
||||
|
||||
|
||||
def score_commander(commander: dict, collection_stats: Dict) -> float:
|
||||
"""Score commander based on collection support."""
|
||||
color_identity = commander.get('color_identity', [])
|
||||
score = 0.0
|
||||
|
||||
# Score based on color support
|
||||
total_colors = sum(collection_stats['colors'].values())
|
||||
if total_colors > 0:
|
||||
color_support = sum(collection_stats['colors'].get(c, 0) for c in color_identity)
|
||||
score += (color_support / total_colors) * 100
|
||||
|
||||
# Bonus for multi-color commanders with good support
|
||||
if len(color_identity) > 1:
|
||||
score += 10
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def suggest_commanders(cards: List[dict]) -> List[dict]:
|
||||
"""Suggest top commanders from collection."""
|
||||
commanders = find_commanders(cards)
|
||||
if not commanders:
|
||||
return []
|
||||
|
||||
stats = analyze_collection(cards)
|
||||
scored = [(score_commander(cmd, stats), cmd) for cmd in commanders]
|
||||
scored.sort(reverse=True, key=lambda x: x[0])
|
||||
|
||||
return [cmd for (score, cmd) in scored[:5]]
|
||||
|
||||
|
||||
def generate_deck(commander: dict, cards: List[dict]) -> Dict:
|
||||
"""Generate a decklist based on chosen commander."""
|
||||
color_identity = commander.get('color_identity', [])
|
||||
deck_cards = []
|
||||
|
||||
# Add commander
|
||||
deck_cards.append({
|
||||
'name': commander['name'],
|
||||
'count': 1,
|
||||
'data': commander
|
||||
})
|
||||
|
||||
# Filter cards matching color identity
|
||||
valid_cards = []
|
||||
for card in cards:
|
||||
if card['name'] == commander['name']:
|
||||
continue # Skip commander in main deck
|
||||
|
||||
card_colors = card.get('color_identity', [])
|
||||
# Check if card colors are subset of commander colors
|
||||
if all(c in color_identity for c in card_colors):
|
||||
valid_cards.append(card)
|
||||
|
||||
# Sort by CMC and add cards
|
||||
valid_cards.sort(key=lambda x: x.get('cmc', 0))
|
||||
|
||||
# Add lands (target ~38 lands)
|
||||
land_count = 0
|
||||
lands_added = []
|
||||
for card in valid_cards:
|
||||
if 'Land' in card.get('type_line', '') and land_count < 38:
|
||||
count = min(card.get('count', 1), 38 - land_count)
|
||||
if count > 0:
|
||||
deck_cards.append({
|
||||
'name': card['name'],
|
||||
'count': count,
|
||||
'data': card
|
||||
})
|
||||
land_count += count
|
||||
lands_added.append(card['name'])
|
||||
|
||||
# Add non-land cards (target 99 total)
|
||||
non_land_cards = [c for c in valid_cards if c['name'] not in lands_added]
|
||||
non_land_count = 0
|
||||
for card in non_land_cards:
|
||||
if non_land_count >= 61: # 99 - 38 lands - 1 commander
|
||||
break
|
||||
count = min(card.get('count', 1), 61 - non_land_count)
|
||||
if count > 0:
|
||||
deck_cards.append({
|
||||
'name': card['name'],
|
||||
'count': count,
|
||||
'data': card
|
||||
})
|
||||
non_land_count += count
|
||||
|
||||
return {
|
||||
'name': f"{commander['name']} Deck",
|
||||
'commander': commander['name'],
|
||||
'colors': color_identity,
|
||||
'archetype': 'Auto-generated',
|
||||
'cards': {card['name']: card['count'] for card in deck_cards}
|
||||
}
|
||||
|
||||
|
||||
def save_deck(deck: Dict, output_dir: str = 'data/decks') -> str:
|
||||
"""Save deck to JSON file."""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Create safe filename
|
||||
commander_name = deck['commander'].replace(' ', '_').replace(',', '').replace("'", '')
|
||||
filename = f"{commander_name}.json"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(deck, f, indent=2)
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Build MTG Commander decks from collection')
|
||||
parser.add_argument('--collection', required=True, help='Path to hydrated collection JSON')
|
||||
parser.add_argument('--name', help='Deck name (defaults to commander name)')
|
||||
parser.add_argument('--commander', help='Specific commander to use')
|
||||
parser.add_argument('--list-commanders', action='store_true', help='List suggested commanders')
|
||||
parser.add_argument('--output-dir', default='data/decks', help='Output directory for deck files')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load collection
|
||||
cards = load_collection(args.collection)
|
||||
|
||||
if args.list_commanders:
|
||||
commanders = suggest_commanders(cards)
|
||||
print(f"Found {len(commanders)} potential commanders:")
|
||||
for i, cmd in enumerate(commanders, 1):
|
||||
colors = ''.join(cmd.get('color_identity', []))
|
||||
print(f"{i}. {cmd['name']} ({colors})")
|
||||
return
|
||||
|
||||
# Find or select commander
|
||||
if args.commander:
|
||||
# Find specific commander
|
||||
commanders = [c for c in cards if c['name'] == args.commander]
|
||||
if not commanders:
|
||||
print(f"Error: Commander '{args.commander}' not found in collection")
|
||||
return
|
||||
commander = commanders[0]
|
||||
else:
|
||||
# Suggest and use top commander
|
||||
commanders = suggest_commanders(cards)
|
||||
if not commanders:
|
||||
print("Error: No suitable commanders found in collection")
|
||||
return
|
||||
commander = commanders[0]
|
||||
print(f"Using suggested commander: {commander['name']}")
|
||||
|
||||
# Generate deck
|
||||
deck = generate_deck(commander, cards)
|
||||
|
||||
# Customize deck name if provided
|
||||
if args.name:
|
||||
deck['name'] = args.name
|
||||
|
||||
# Save deck
|
||||
filepath = save_deck(deck, args.output_dir)
|
||||
print(f"Deck saved to: {filepath}")
|
||||
print(f"\nDeck Summary:")
|
||||
print(f" Name: {deck['name']}")
|
||||
print(f" Commander: {deck['commander']}")
|
||||
print(f" Colors: {', '.join(deck['colors'])}")
|
||||
print(f" Total cards: {sum(deck['cards'].values())}")
|
||||
print(f" Unique cards: {len(deck['cards'])}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
**Step 2: Test basic functionality**
|
||||
|
||||
Run: `python scripts/build_deck.py --help`
|
||||
Expected: Help message with all arguments
|
||||
|
||||
**Step 3: Test with existing collection**
|
||||
|
||||
Run: `python scripts/build_deck.py --collection output/hydrated/deck.json --list-commanders`
|
||||
Expected: List of potential commanders from collection
|
||||
|
||||
**Step 4: Generate a test deck**
|
||||
|
||||
Run: `python scripts/build_deck.py --collection output/hydrated/deck.json --name test_deck`
|
||||
Expected: Deck file created in data/decks/
|
||||
|
||||
**Step 5: Commit initial implementation**
|
||||
|
||||
```bash
|
||||
git add scripts/build_deck.py
|
||||
git commit -m "feat: add deck builder script"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add Documentation to README
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md` (add usage section)
|
||||
|
||||
**Step 1: Add deck builder usage to README**
|
||||
|
||||
Add to README.md after the "Find Synergies" section:
|
||||
|
||||
```markdown
|
||||
### Build a Deck From Your Collection
|
||||
|
||||
```bash
|
||||
# List suggested commanders from your collection
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --list-commanders
|
||||
|
||||
# Build deck with suggested commander
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --name my_new_deck
|
||||
|
||||
# Build deck with specific commander
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --name my_deck --commander "Choco, Seeker of Paradise"
|
||||
```
|
||||
|
||||
The deck builder will:
|
||||
1. Analyze your collection for color distribution and card types
|
||||
2. Suggest potential commanders from your collection
|
||||
3. Generate a starter decklist matching the commander's color identity
|
||||
4. Save the deck to `data/decks/<name>.json`
|
||||
5. Provide a summary report
|
||||
|
||||
After building, refine your deck:
|
||||
- Edit the deck file manually
|
||||
- Find upgrades: `python scripts/analyze_decks.py --collection output/hydrated/deck.json --deck-dir data/decks/`
|
||||
- Generate reports: `python scripts/deck_report.py --collection output/hydrated/deck.json --decks-dir data/decks/`
|
||||
```
|
||||
|
||||
**Step 2: Update README**
|
||||
|
||||
Run: Edit README.md to add the new section
|
||||
|
||||
**Step 3: Commit documentation**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs: add deck builder usage to README"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Enhance Deck Generation Logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/build_deck.py` (improve generate_deck function)
|
||||
|
||||
**Step 1: Improve land selection**
|
||||
|
||||
Update generate_deck to better handle land selection:
|
||||
- Prioritize basic lands
|
||||
- Include color-fixing lands when available
|
||||
- Balance land types for multi-color decks
|
||||
|
||||
**Step 2: Add mana curve balancing**
|
||||
|
||||
Add function to balance mana curve:
|
||||
- Target distribution: 10-12 cards at 2 CMC, 8-10 at 3, 6-8 at 4, etc.
|
||||
- Adjust card selection to match curve
|
||||
|
||||
**Step 3: Test enhanced generation**
|
||||
|
||||
Run: `python scripts/build_deck.py --collection output/hydrated/deck.json --name enhanced_deck`
|
||||
Expected: Better balanced deck with improved mana curve
|
||||
|
||||
**Step 4: Commit improvements**
|
||||
|
||||
```bash
|
||||
git add scripts/build_deck.py
|
||||
git commit -m "feat: improve deck generation logic"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Testing and Validation
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_build_deck.py`
|
||||
|
||||
**Step 1: Write unit tests**
|
||||
|
||||
```python
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
from scripts.build_deck import load_collection, analyze_collection, find_commanders
|
||||
|
||||
def test_load_collection():
|
||||
"""Test loading collection from JSON."""
|
||||
test_data = [
|
||||
{"name": "Test Card", "count": 1, "color_identity": ["U"], "type_line": "Creature"}
|
||||
]
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(test_data, f)
|
||||
filepath = f.name
|
||||
|
||||
try:
|
||||
result = load_collection(filepath)
|
||||
assert len(result) == 1
|
||||
assert result[0]['name'] == "Test Card"
|
||||
finally:
|
||||
os.unlink(filepath)
|
||||
|
||||
def test_analyze_collection():
|
||||
"""Test collection analysis."""
|
||||
test_cards = [
|
||||
{"name": "Blue Card", "count": 2, "color_identity": ["U"], "type_line": "Creature", "cmc": 3},
|
||||
{"name": "Red Card", "count": 1, "color_identity": ["R"], "type_line": "Instant", "cmc": 2},
|
||||
]
|
||||
|
||||
result = analyze_collection(test_cards)
|
||||
assert result['total_cards'] == 3
|
||||
assert result['colors']['U'] == 2
|
||||
assert result['types']['creatures'] == 2
|
||||
assert result['cmc_distribution'][2] == 1
|
||||
|
||||
def test_find_commanders():
|
||||
"""Test commander finding."""
|
||||
test_cards = [
|
||||
{"name": "Legendary Creature", "type_line": "Legendary Creature", "color_identity": ["G"]},
|
||||
{"name": "Regular Creature", "type_line": "Creature"},
|
||||
]
|
||||
|
||||
commanders = find_commanders(test_cards)
|
||||
assert len(commanders) == 1
|
||||
assert commanders[0]['name'] == "Legendary Creature"
|
||||
```
|
||||
|
||||
**Step 2: Run tests**
|
||||
|
||||
Run: `python -m pytest tests/test_build_deck.py -v`
|
||||
Expected: All tests pass
|
||||
|
||||
**Step 3: Commit tests**
|
||||
|
||||
```bash
|
||||
git add tests/test_build_deck.py
|
||||
git commit -m "test: add unit tests for deck builder"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Final Integration and Testing
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/update_docs.py` (add deck builder to auto-docs)
|
||||
|
||||
**Step 1: Update documentation generator**
|
||||
|
||||
Add deck builder to the auto-generated documentation
|
||||
|
||||
**Step 2: Test complete workflow**
|
||||
|
||||
```bash
|
||||
# Test complete workflow
|
||||
python hydrate.py hydrate data/collection/Box1\ 2026-01-30.txt -o output/hydrated/ -c cache/card_cache.json
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --list-commanders
|
||||
python scripts/build_deck.py --collection output/hydrated/deck.json --name my_test_deck
|
||||
python scripts/analyze_decks.py --collection output/hydrated/deck.json --deck-dir data/decks/
|
||||
```
|
||||
|
||||
**Step 3: Verify all tests pass**
|
||||
|
||||
Run: `python -m pytest tests/ -v`
|
||||
Expected: All tests pass
|
||||
|
||||
**Step 4: Final commit**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: complete deck builder implementation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plan Complete
|
||||
|
||||
**Execution Options:**
|
||||
|
||||
1. **Subagent-Driven (this session)** - I'll implement task-by-task with your review
|
||||
2. **Parallel Session** - You can run this plan in a separate session
|
||||
|
||||
Which approach would you prefer?
|
||||
142
hydrate.py
142
hydrate.py
@@ -21,8 +21,16 @@ SCRYFALL_API = "https://api.scryfall.com"
|
||||
RATE_LIMIT_DELAY = 0.1 # 100ms between requests
|
||||
|
||||
FIELDS = [
|
||||
"name", "mana_cost", "cmc", "colors", "color_identity",
|
||||
"type_line", "oracle_text", "power", "toughness", "loyalty"
|
||||
"name",
|
||||
"mana_cost",
|
||||
"cmc",
|
||||
"colors",
|
||||
"color_identity",
|
||||
"type_line",
|
||||
"oracle_text",
|
||||
"power",
|
||||
"toughness",
|
||||
"loyalty",
|
||||
]
|
||||
|
||||
|
||||
@@ -36,17 +44,19 @@ def parse_decklist(filepath: str) -> list[dict]:
|
||||
continue
|
||||
match = re.match(r"^(\d+)x?\s+(.+)$", line, re.IGNORECASE)
|
||||
if match:
|
||||
cards.append({"count": int(match.group(1)), "name": match.group(2).strip()})
|
||||
cards.append(
|
||||
{"count": int(match.group(1)), "name": match.group(2).strip()}
|
||||
)
|
||||
return cards
|
||||
|
||||
|
||||
def strip_set_code(name: str) -> str:
|
||||
"""Remove set code and collector number from card name.
|
||||
|
||||
|
||||
E.g., "Keep Out (ECL) 19" -> "Keep Out"
|
||||
"""
|
||||
name = re.sub(r'\s*\([^)]+\)\s*\d+ *$', '', name)
|
||||
name = re.sub(r'\s*\*F\*$', '', name)
|
||||
name = re.sub(r"\s*\([^)]+\)\s*\d+ *$", "", name)
|
||||
name = re.sub(r"\s*\*F\*$", "", name)
|
||||
return name.strip()
|
||||
|
||||
|
||||
@@ -54,16 +64,15 @@ def fetch_card(name: str, retry_count: int = 3) -> Optional[dict]:
|
||||
"""Fetch card data from Scryfall API using fuzzy search."""
|
||||
original_name = name
|
||||
name = strip_set_code(name)
|
||||
|
||||
|
||||
encoded = urllib.parse.quote(name)
|
||||
url = f"{SCRYFALL_API}/cards/named?fuzzy={encoded}"
|
||||
|
||||
|
||||
for attempt in range(retry_count):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={
|
||||
"User-Agent": "EDHDeckBuilder/1.0",
|
||||
"Accept": "*/*"
|
||||
})
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "EDHDeckBuilder/1.0", "Accept": "*/*"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
@@ -73,17 +82,25 @@ def fetch_card(name: str, retry_count: int = 3) -> Optional[dict]:
|
||||
time.sleep(retry_after)
|
||||
elif e.code == 404:
|
||||
if name != original_name:
|
||||
print(f" Not found with set code, retrying: '{name}'", file=sys.stderr)
|
||||
print(
|
||||
f" Not found with set code, retrying: '{name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return fetch_card(name, retry_count=1)
|
||||
print(f" Error fetching '{original_name}': Not found", file=sys.stderr)
|
||||
return None
|
||||
else:
|
||||
if attempt < retry_count - 1:
|
||||
wait_time = 2 ** attempt
|
||||
print(f" HTTP {e.code}, retrying in {wait_time}s...", file=sys.stderr)
|
||||
wait_time = 2**attempt
|
||||
print(
|
||||
f" HTTP {e.code}, retrying in {wait_time}s...", file=sys.stderr
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
print(f" Error fetching '{original_name}': HTTP {e.code}", file=sys.stderr)
|
||||
print(
|
||||
f" Error fetching '{original_name}': HTTP {e.code}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
except urllib.error.URLError as e:
|
||||
print(f" Error fetching '{original_name}': {e.reason}", file=sys.stderr)
|
||||
@@ -91,26 +108,26 @@ def fetch_card(name: str, retry_count: int = 3) -> Optional[dict]:
|
||||
except json.JSONDecodeError:
|
||||
print(f" Error parsing response for '{original_name}'", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_card_info(card_data: dict) -> dict:
|
||||
"""Extract relevant fields from Scryfall card data."""
|
||||
result = {"scryfall_uri": card_data.get("scryfall_uri", "")}
|
||||
|
||||
|
||||
for field in FIELDS:
|
||||
result[field] = card_data.get(field)
|
||||
|
||||
|
||||
if card_data.get("card_faces"):
|
||||
face = card_data["card_faces"][0]
|
||||
for field in ["mana_cost", "type_line", "oracle_text", "power", "toughness"]:
|
||||
if result.get(field) is None:
|
||||
result[field] = face.get(field)
|
||||
|
||||
|
||||
result["colors"] = card_data.get("colors", [])
|
||||
result["color_identity"] = card_data.get("color_identity", [])
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -127,15 +144,17 @@ def categorize_by_type(cards: list[dict]) -> dict[str, list[dict]]:
|
||||
"lands": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
|
||||
for card in cards:
|
||||
type_line = card.get("type_line", "").lower()
|
||||
|
||||
if "legendary" in type_line and ("creature" in type_line or "planeswalker" in type_line):
|
||||
|
||||
if "legendary" in type_line and (
|
||||
"creature" in type_line or "planeswalker" in type_line
|
||||
):
|
||||
if not categories["commander"]:
|
||||
categories["commander"].append(card)
|
||||
continue
|
||||
|
||||
|
||||
if "creature" in type_line:
|
||||
categories["creatures"].append(card)
|
||||
elif "instant" in type_line:
|
||||
@@ -152,27 +171,29 @@ def categorize_by_type(cards: list[dict]) -> dict[str, list[dict]]:
|
||||
categories["lands"].append(card)
|
||||
else:
|
||||
categories["other"].append(card)
|
||||
|
||||
|
||||
return {k: v for k, v in categories.items() if v}
|
||||
|
||||
|
||||
def hydrate_decklist(input_file: str, output_dir: str, cache_file: Optional[str] = None) -> None:
|
||||
def hydrate_decklist(
|
||||
input_file: str, output_dir: str, cache_file: Optional[str] = None
|
||||
) -> None:
|
||||
"""Main hydration function."""
|
||||
cache = {}
|
||||
if cache_file and os.path.exists(cache_file):
|
||||
with open(cache_file, "r", encoding="utf-8") as f:
|
||||
cache = json.load(f)
|
||||
print(f"Loaded {len(cache)} cached cards")
|
||||
|
||||
|
||||
print(f"Parsing decklist: {input_file}")
|
||||
entries = parse_decklist(input_file)
|
||||
print(f"Found {len(entries)} unique card entries")
|
||||
|
||||
|
||||
hydrated = []
|
||||
for i, entry in enumerate(entries, 1):
|
||||
name = entry["name"]
|
||||
count = entry["count"]
|
||||
|
||||
|
||||
if name in cache:
|
||||
card_info = cache[name]
|
||||
print(f"[{i}/{len(entries)}] {name} (cached)")
|
||||
@@ -185,30 +206,30 @@ def hydrate_decklist(input_file: str, output_dir: str, cache_file: Optional[str]
|
||||
time.sleep(RATE_LIMIT_DELAY)
|
||||
else:
|
||||
card_info = {"name": name, "error": "not found"}
|
||||
|
||||
|
||||
card_info["count"] = count
|
||||
hydrated.append(card_info)
|
||||
|
||||
|
||||
if cache_file:
|
||||
with open(cache_file, "w", encoding="utf-8") as f:
|
||||
json.dump(cache, f, indent=2)
|
||||
print(f"Cached {len(cache)} cards to {cache_file}")
|
||||
|
||||
|
||||
categories = categorize_by_type(hydrated)
|
||||
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
all_cards_path = os.path.join(output_dir, "deck.json")
|
||||
|
||||
all_cards_path = os.path.join(output_dir, "full_collection.json")
|
||||
with open(all_cards_path, "w", encoding="utf-8") as f:
|
||||
json.dump(hydrated, f, indent=2)
|
||||
print(f"Wrote full decklist to {all_cards_path}")
|
||||
|
||||
print(f"Wrote full collection to {all_cards_path}")
|
||||
|
||||
for category, cards in categories.items():
|
||||
cat_path = os.path.join(output_dir, f"{category}.json")
|
||||
with open(cat_path, "w", encoding="utf-8") as f:
|
||||
json.dump(cards, f, indent=2)
|
||||
print(f" {category}: {len(cards)} cards -> {cat_path}")
|
||||
|
||||
|
||||
print(f"\nDeck summary:")
|
||||
print(f" Total cards: {sum(c.get('count', 1) for c in hydrated)}")
|
||||
print(f" Unique cards: {len(hydrated)}")
|
||||
@@ -218,35 +239,44 @@ def create_deck(deck_name: str, base_dir: str = "decks") -> str:
|
||||
"""Create a new deck folder structure."""
|
||||
deck_path = os.path.join(base_dir, deck_name)
|
||||
os.makedirs(deck_path, exist_ok=True)
|
||||
|
||||
template = {
|
||||
"name": deck_name,
|
||||
"commander": None,
|
||||
"cards": []
|
||||
}
|
||||
|
||||
|
||||
template = {"name": deck_name, "commander": None, "cards": []}
|
||||
|
||||
with open(os.path.join(deck_path, "deck.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(template, f, indent=2)
|
||||
|
||||
|
||||
print(f"Created deck: {deck_path}")
|
||||
return deck_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Hydrate MTG decklists with Scryfall data")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Hydrate MTG decklists with Scryfall data"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
hydrate_parser = subparsers.add_parser("hydrate", help="Hydrate a decklist with Scryfall data")
|
||||
|
||||
hydrate_parser = subparsers.add_parser(
|
||||
"hydrate", help="Hydrate a decklist with Scryfall data"
|
||||
)
|
||||
hydrate_parser.add_argument("input", help="Input decklist file")
|
||||
hydrate_parser.add_argument("-o", "--output", default="output/hydrated", help="Output directory")
|
||||
hydrate_parser.add_argument("-c", "--cache", default="cache/card_cache.json", help="Cache file for card data")
|
||||
|
||||
hydrate_parser.add_argument(
|
||||
"-o", "--output", default="output/hydrated", help="Output directory"
|
||||
)
|
||||
hydrate_parser.add_argument(
|
||||
"-c",
|
||||
"--cache",
|
||||
default="cache/card_cache.json",
|
||||
help="Cache file for card data",
|
||||
)
|
||||
|
||||
new_parser = subparsers.add_parser("new", help="Create a new deck folder")
|
||||
new_parser.add_argument("name", help="Deck name")
|
||||
new_parser.add_argument("-d", "--dir", default="data/decks", help="Base directory for decks")
|
||||
|
||||
new_parser.add_argument(
|
||||
"-d", "--dir", default="data/decks", help="Base directory for decks"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
if args.command == "hydrate":
|
||||
hydrate_decklist(args.input, args.output, args.cache)
|
||||
elif args.command == "new":
|
||||
|
||||
410
scripts/build_deck.py
Normal file
410
scripts/build_deck.py
Normal file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deck builder for MTG Commander decks from existing collections.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from collections import Counter
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
def load_collection(collection_path: str) -> List[dict]:
|
||||
"""Load hydrated collection from JSON file."""
|
||||
with open(collection_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_multiple_collections(collection_paths: List[str]) -> List[dict]:
|
||||
"""Load and merge multiple collection files."""
|
||||
all_cards = []
|
||||
|
||||
for path in collection_paths:
|
||||
cards = load_collection(path)
|
||||
all_cards.extend(cards)
|
||||
|
||||
# Merge cards with same name by summing counts
|
||||
merged_cards = {}
|
||||
for card in all_cards:
|
||||
name = card["name"]
|
||||
if name in merged_cards:
|
||||
merged_cards[name]["count"] += card.get("count", 1)
|
||||
else:
|
||||
merged_cards[name] = card.copy()
|
||||
|
||||
return list(merged_cards.values())
|
||||
|
||||
|
||||
def analyze_collection(cards: List[dict]) -> Dict:
|
||||
"""Analyze collection statistics."""
|
||||
colors = Counter()
|
||||
types = Counter()
|
||||
cmc_dist = Counter()
|
||||
|
||||
for card in cards:
|
||||
# Count colors
|
||||
for color in card.get("color_identity", []):
|
||||
colors[color] += card.get("count", 1)
|
||||
|
||||
# Count types
|
||||
type_line = card.get("type_line", "")
|
||||
if "Creature" in type_line:
|
||||
types["creatures"] += card.get("count", 1)
|
||||
elif "Instant" in type_line:
|
||||
types["instants"] += card.get("count", 1)
|
||||
elif "Sorcery" in type_line:
|
||||
types["sorceries"] += card.get("count", 1)
|
||||
elif "Artifact" in type_line:
|
||||
types["artifacts"] += card.get("count", 1)
|
||||
elif "Enchantment" in type_line:
|
||||
types["enchantments"] += card.get("count", 1)
|
||||
elif "Land" in type_line:
|
||||
types["lands"] += card.get("count", 1)
|
||||
|
||||
# Count CMC
|
||||
cmc = card.get("cmc")
|
||||
if cmc:
|
||||
cmc_dist[int(cmc)] += card.get("count", 1)
|
||||
|
||||
return {
|
||||
"colors": dict(colors),
|
||||
"types": dict(types),
|
||||
"cmc_distribution": dict(cmc_dist),
|
||||
"total_cards": sum(card.get("count", 1) for card in cards),
|
||||
"unique_cards": len(cards),
|
||||
}
|
||||
|
||||
|
||||
def find_commanders(cards: List[dict]) -> List[dict]:
|
||||
"""Find potential commander cards in collection."""
|
||||
commanders = []
|
||||
for card in cards:
|
||||
type_line = card.get("type_line", "")
|
||||
if "Legendary" in type_line and (
|
||||
"Creature" in type_line or "Planeswalker" in type_line
|
||||
):
|
||||
commanders.append(card)
|
||||
return commanders
|
||||
|
||||
|
||||
def score_commander(commander: dict, collection_stats: Dict) -> float:
|
||||
"""Score commander based on collection support."""
|
||||
color_identity = commander.get("color_identity", [])
|
||||
score = 0.0
|
||||
|
||||
# Score based on color support
|
||||
total_colors = sum(collection_stats["colors"].values())
|
||||
if total_colors > 0:
|
||||
color_support = sum(
|
||||
collection_stats["colors"].get(c, 0) for c in color_identity
|
||||
)
|
||||
score += (color_support / total_colors) * 100
|
||||
|
||||
# Bonus for multi-color commanders with good support
|
||||
if len(color_identity) > 1:
|
||||
score += 10
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def suggest_commanders(cards: List[dict]) -> List[dict]:
|
||||
"""Suggest top commanders from collection."""
|
||||
commanders = find_commanders(cards)
|
||||
if not commanders:
|
||||
return []
|
||||
|
||||
stats = analyze_collection(cards)
|
||||
scored = [(score_commander(cmd, stats), cmd) for cmd in commanders]
|
||||
scored.sort(reverse=True, key=lambda x: x[0])
|
||||
|
||||
return [cmd for (score, cmd) in scored[:5]]
|
||||
|
||||
|
||||
def is_basic_land(card: dict) -> bool:
|
||||
"""Check if a card is a basic land."""
|
||||
name = card.get("name", "")
|
||||
return name in ["Plains", "Island", "Swamp", "Mountain", "Forest"]
|
||||
|
||||
|
||||
def balance_mana_curve(cards: List[dict], target_slots: int) -> List[dict]:
|
||||
"""Balance mana curve for non-land cards."""
|
||||
# Group cards by CMC
|
||||
cmc_groups = {}
|
||||
for card in cards:
|
||||
cmc = card.get("cmc", 0)
|
||||
if cmc not in cmc_groups:
|
||||
cmc_groups[cmc] = []
|
||||
cmc_groups[cmc].append(card)
|
||||
|
||||
# Target distribution (CMC: target count)
|
||||
# This follows typical Commander mana curve recommendations
|
||||
target_dist = {
|
||||
1: 8, # 1-drops
|
||||
2: 12, # 2-drops
|
||||
3: 10, # 3-drops
|
||||
4: 8, # 4-drops
|
||||
5: 6, # 5-drops
|
||||
6: 4, # 6-drops
|
||||
7: 2, # 7+ drops (combined)
|
||||
}
|
||||
|
||||
selected_cards = []
|
||||
remaining_slots = target_slots
|
||||
|
||||
# Distribute cards according to target curve
|
||||
for cmc, target_count in sorted(target_dist.items()):
|
||||
if cmc == 7:
|
||||
# Handle 7+ CMC cards together
|
||||
available_cards = []
|
||||
for higher_cmc in sorted(cmc_groups.keys()):
|
||||
if higher_cmc >= 7:
|
||||
available_cards.extend(cmc_groups[higher_cmc])
|
||||
else:
|
||||
available_cards = cmc_groups.get(cmc, [])
|
||||
|
||||
# Calculate how many we can take from this CMC group
|
||||
slots_for_cmc = min(target_count, len(available_cards), remaining_slots)
|
||||
|
||||
if slots_for_cmc > 0:
|
||||
# Take the first N cards (already sorted by CMC)
|
||||
selected_cards.extend(available_cards[:slots_for_cmc])
|
||||
remaining_slots -= slots_for_cmc
|
||||
|
||||
if remaining_slots <= 0:
|
||||
break
|
||||
|
||||
# If we still have slots, fill with remaining cards
|
||||
if remaining_slots > 0:
|
||||
# Get all remaining cards not yet selected
|
||||
remaining_cards = []
|
||||
for cmc, card_list in cmc_groups.items():
|
||||
if cmc < 7: # We already handled 7+ above
|
||||
remaining_cards.extend(
|
||||
card_list[
|
||||
sorted(target_dist.items()).index((cmc, target_dist[cmc]))
|
||||
if cmc in target_dist
|
||||
else 0 :
|
||||
]
|
||||
)
|
||||
|
||||
# Add remaining cards until we fill all slots
|
||||
fill_cards = remaining_cards[:remaining_slots]
|
||||
selected_cards.extend(fill_cards)
|
||||
|
||||
return selected_cards
|
||||
|
||||
|
||||
def select_lands(
|
||||
valid_cards: List[dict], color_identity: List[str], target_land_count: int = 38
|
||||
) -> List[dict]:
|
||||
"""Select lands with priority for basic lands and color fixing."""
|
||||
basic_lands = []
|
||||
color_fixing_lands = []
|
||||
other_lands = []
|
||||
|
||||
# Categorize lands
|
||||
for card in valid_cards:
|
||||
if "Land" not in card.get("type_line", ""):
|
||||
continue
|
||||
|
||||
if is_basic_land(card):
|
||||
basic_lands.append(card)
|
||||
elif any(
|
||||
color in card.get("name", "").lower()
|
||||
for color in ["plains", "island", "swamp", "mountain", "forest"]
|
||||
):
|
||||
# Dual lands, shock lands, etc.
|
||||
color_fixing_lands.append(card)
|
||||
else:
|
||||
other_lands.append(card)
|
||||
|
||||
selected_lands = []
|
||||
land_count = 0
|
||||
|
||||
# Add basic lands first
|
||||
for land in basic_lands:
|
||||
if land_count >= target_land_count:
|
||||
break
|
||||
|
||||
# Check if this basic land produces a color in our identity
|
||||
land_color = None
|
||||
if land["name"] == "Plains":
|
||||
land_color = "W"
|
||||
elif land["name"] == "Island":
|
||||
land_color = "U"
|
||||
elif land["name"] == "Swamp":
|
||||
land_color = "B"
|
||||
elif land["name"] == "Mountain":
|
||||
land_color = "R"
|
||||
elif land["name"] == "Forest":
|
||||
land_color = "G"
|
||||
|
||||
if land_color and land_color in color_identity:
|
||||
count = min(land.get("count", 1), target_land_count - land_count)
|
||||
if count > 0:
|
||||
selected_lands.append(land)
|
||||
land_count += count
|
||||
|
||||
# Add color-fixing lands
|
||||
for land in color_fixing_lands:
|
||||
if land_count >= target_land_count:
|
||||
break
|
||||
|
||||
count = min(land.get("count", 1), target_land_count - land_count)
|
||||
if count > 0:
|
||||
selected_lands.append(land)
|
||||
land_count += count
|
||||
|
||||
# Add other lands if needed
|
||||
for land in other_lands:
|
||||
if land_count >= target_land_count:
|
||||
break
|
||||
|
||||
count = min(land.get("count", 1), target_land_count - land_count)
|
||||
if count > 0:
|
||||
selected_lands.append(land)
|
||||
land_count += count
|
||||
|
||||
return selected_lands
|
||||
|
||||
|
||||
def generate_deck(commander: dict, cards: List[dict]) -> Dict:
|
||||
"""Generate a decklist based on chosen commander."""
|
||||
color_identity = commander.get("color_identity", [])
|
||||
deck_cards = []
|
||||
|
||||
# Add commander
|
||||
deck_cards.append({"name": commander["name"], "count": 1, "data": commander})
|
||||
|
||||
# Filter cards matching color identity
|
||||
valid_cards = []
|
||||
for card in cards:
|
||||
if card["name"] == commander["name"]:
|
||||
continue # Skip commander in main deck
|
||||
|
||||
card_colors = card.get("color_identity", [])
|
||||
# Check if card colors are subset of commander colors
|
||||
if all(c in color_identity for c in card_colors):
|
||||
valid_cards.append(card)
|
||||
|
||||
# Select lands using improved logic
|
||||
selected_lands = select_lands(valid_cards, color_identity)
|
||||
|
||||
# Get non-land cards
|
||||
land_names = {land["name"] for land in selected_lands}
|
||||
non_land_cards = [c for c in valid_cards if c["name"] not in land_names]
|
||||
|
||||
# Balance mana curve for non-land cards (target 61 cards)
|
||||
balanced_non_lands = balance_mana_curve(non_land_cards, 61)
|
||||
|
||||
# Add lands to deck
|
||||
for land in selected_lands:
|
||||
count = land.get("count", 1)
|
||||
deck_cards.append({"name": land["name"], "count": count, "data": land})
|
||||
|
||||
# Add non-land cards to deck
|
||||
for card in balanced_non_lands:
|
||||
count = card.get("count", 1)
|
||||
deck_cards.append({"name": card["name"], "count": count, "data": card})
|
||||
|
||||
return {
|
||||
"name": f"{commander['name']} Deck",
|
||||
"commander": commander["name"],
|
||||
"colors": color_identity,
|
||||
"archetype": "Auto-generated",
|
||||
"cards": {card["name"]: card["count"] for card in deck_cards},
|
||||
}
|
||||
|
||||
|
||||
def save_deck(deck: Dict, output_dir: str = "data/decks") -> str:
|
||||
"""Save deck to JSON file."""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Create safe filename
|
||||
commander_name = (
|
||||
deck["commander"].replace(" ", "_").replace(",", "").replace("'", "")
|
||||
)
|
||||
filename = f"{commander_name}.json"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(deck, f, indent=2)
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build MTG Commander decks from collection"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--collection",
|
||||
nargs="+",
|
||||
required=True,
|
||||
help="Path(s) to hydrated collection JSON file(s)",
|
||||
)
|
||||
parser.add_argument("--name", help="Deck name (defaults to commander name)")
|
||||
parser.add_argument("--commander", help="Specific commander to use")
|
||||
parser.add_argument(
|
||||
"--list-commanders", action="store_true", help="List suggested commanders"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir", default="data/decks", help="Output directory for deck files"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load collection(s)
|
||||
if isinstance(args.collection, list) and len(args.collection) > 1:
|
||||
cards = load_multiple_collections(args.collection)
|
||||
else:
|
||||
cards = load_collection(
|
||||
args.collection[0] if isinstance(args.collection, list) else args.collection
|
||||
)
|
||||
|
||||
if args.list_commanders:
|
||||
commanders = suggest_commanders(cards)
|
||||
print(f"Found {len(commanders)} potential commanders:")
|
||||
for i, cmd in enumerate(commanders, 1):
|
||||
colors = "".join(cmd.get("color_identity", []))
|
||||
print(f"{i}. {cmd['name']} ({colors})")
|
||||
return
|
||||
|
||||
# Find or select commander
|
||||
if args.commander:
|
||||
# Find specific commander
|
||||
commanders = [c for c in cards if c["name"] == args.commander]
|
||||
if not commanders:
|
||||
print(f"Error: Commander '{args.commander}' not found in collection")
|
||||
return
|
||||
commander = commanders[0]
|
||||
else:
|
||||
# Suggest and use top commander
|
||||
commanders = suggest_commanders(cards)
|
||||
if not commanders:
|
||||
print("Error: No suitable commanders found in collection")
|
||||
return
|
||||
commander = commanders[0]
|
||||
print(f"Using suggested commander: {commander['name']}")
|
||||
|
||||
# Generate deck
|
||||
deck = generate_deck(commander, cards)
|
||||
|
||||
# Customize deck name if provided
|
||||
if args.name:
|
||||
deck["name"] = args.name
|
||||
|
||||
# Save deck
|
||||
filepath = save_deck(deck, args.output_dir)
|
||||
print(f"Deck saved to: {filepath}")
|
||||
print(f"\nDeck Summary:")
|
||||
print(f" Name: {deck['name']}")
|
||||
print(f" Commander: {deck['commander']}")
|
||||
print(f" Colors: {', '.join(deck['colors'])}")
|
||||
print(f" Total cards: {sum(deck['cards'].values())}")
|
||||
print(f" Unique cards: {len(deck['cards'])}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user