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:
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?
|
||||
Reference in New Issue
Block a user