Files
mtg-builder/docs/plans/2026-03-03-deck-builder-implementation.md
Tuan-Dat Tran 31a4995bbe 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
2026-03-03 23:23:44 +01:00

14 KiB

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

#!/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

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:

### 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

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

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

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

# 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

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?