- 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
475 lines
14 KiB
Markdown
475 lines
14 KiB
Markdown
# 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? |