mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
feat(cli): add type indicators, ideal count args, and theme name support
Enhanced CLI with type-safe help text, 8 ideal count flags (--land-count, etc), and theme selection by name (--primary-tag)
This commit is contained in:
parent
cfcc01db85
commit
abea242c16
6 changed files with 588 additions and 79 deletions
|
@ -29,6 +29,11 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
|
||||||
- Engine integration with include injection after lands, before creatures/spells with ordering tests
|
- Engine integration with include injection after lands, before creatures/spells with ordering tests
|
||||||
- Exclude re-entry prevention ensuring blocked cards cannot re-enter via downstream heuristics
|
- Exclude re-entry prevention ensuring blocked cards cannot re-enter via downstream heuristics
|
||||||
- Web UI enhancement with two-column layout, chips/tag UI, and real-time validation
|
- Web UI enhancement with two-column layout, chips/tag UI, and real-time validation
|
||||||
|
- **CLI enhancement: Enhanced help text with type indicators** - All CLI arguments now show expected value types (PATH, NAME, INT, BOOL) and organized into logical groups
|
||||||
|
- **CLI enhancement: Ideal count arguments** - New CLI flags for deck composition: `--ramp-count`, `--land-count`, `--basic-land-count`, `--creature-count`, `--removal-count`, `--wipe-count`, `--card-advantage-count`, `--protection-count`
|
||||||
|
- **CLI enhancement: Theme tag name support** - Theme selection by name instead of index: `--primary-tag`, `--secondary-tag`, `--tertiary-tag` as alternatives to numeric choices
|
||||||
|
- **CLI enhancement: Include/exclude CLI support** - Full CLI parity for include/exclude with `--include-cards`, `--exclude-cards`, `--enforcement-mode`, `--allow-illegal`, `--fuzzy-matching`
|
||||||
|
- **CLI enhancement: Console summary printing** - Detailed include/exclude summary output for headless builds with diagnostics and validation results
|
||||||
- Enhanced fuzzy matching with 300+ Commander-legal card knowledge base and popular/iconic card prioritization
|
- Enhanced fuzzy matching with 300+ Commander-legal card knowledge base and popular/iconic card prioritization
|
||||||
- Card constants refactored to dedicated `builder_constants.py` with functional organization
|
- Card constants refactored to dedicated `builder_constants.py` with functional organization
|
||||||
- Fuzzy match confirmation modal with dark theme support and card preview functionality
|
- Fuzzy match confirmation modal with dark theme support and card preview functionality
|
||||||
|
|
BIN
README.md
BIN
README.md
Binary file not shown.
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
- **Include/Exclude Cards Feature Complete**: Full implementation with enhanced web UI, intelligent fuzzy matching, and performance optimization. Users can now specify must-include and must-exclude cards with comprehensive card knowledge base and excellent performance.
|
- **Include/Exclude Cards Feature Complete**: Full implementation with enhanced web UI, intelligent fuzzy matching, and performance optimization. Users can now specify must-include and must-exclude cards with comprehensive card knowledge base and excellent performance.
|
||||||
|
- **Enhanced CLI with Type Safety**: Comprehensive CLI enhancement with type indicators, ideal count arguments, and theme tag name support making headless operation more user-friendly and discoverable.
|
||||||
|
- **Theme Tag Name Selection**: Intelligent theme selection by name instead of index numbers, automatically resolving to correct choices accounting for selection ordering.
|
||||||
- **Enhanced Fuzzy Matching**: Advanced algorithm with 300+ Commander-legal card knowledge base, popular/iconic card prioritization, and dark theme confirmation modal for optimal user experience.
|
- **Enhanced Fuzzy Matching**: Advanced algorithm with 300+ Commander-legal card knowledge base, popular/iconic card prioritization, and dark theme confirmation modal for optimal user experience.
|
||||||
- **Mobile Responsive Design**: Optimized mobile experience with bottom-floating build controls, two-column grid layout, and horizontal scrolling prevention for improved thumb navigation.
|
- **Mobile Responsive Design**: Optimized mobile experience with bottom-floating build controls, two-column grid layout, and horizontal scrolling prevention for improved thumb navigation.
|
||||||
- **Enhanced Visual Validation**: List size validation UI with warning icons (⚠️ over-limit, ⚡ approaching limit) and color coding providing clear feedback on usage limits.
|
- **Enhanced Visual Validation**: List size validation UI with warning icons (⚠️ over-limit, ⚡ approaching limit) and color coding providing clear feedback on usage limits.
|
||||||
|
@ -9,6 +11,13 @@
|
||||||
- **Dual Architecture Support**: Seamless functionality across both web interface (staging system) and CLI (direct build) with proper include injection timing.
|
- **Dual Architecture Support**: Seamless functionality across both web interface (staging system) and CLI (direct build) with proper include injection timing.
|
||||||
|
|
||||||
## What's new
|
## What's new
|
||||||
|
- **Enhanced CLI Experience**
|
||||||
|
- Type-safe help text with value indicators (PATH, NAME, INT, BOOL) and organized argument groups
|
||||||
|
- Ideal count CLI arguments: `--ramp-count`, `--land-count`, `--creature-count`, etc. for deck composition control
|
||||||
|
- Theme tag name support: `--primary-tag "Airbending"` instead of `--primary-choice 1` with intelligent resolution
|
||||||
|
- Include/exclude CLI parity: `--include-cards`, `--exclude-cards` with semicolon support for comma-containing card names
|
||||||
|
- Console summary output with detailed diagnostics and validation results for headless builds
|
||||||
|
- Priority system: CLI > JSON Config > Environment Variables > Defaults
|
||||||
- **Enhanced Visual Validation**
|
- **Enhanced Visual Validation**
|
||||||
- List size validation UI with visual warning system using icons and color coding
|
- List size validation UI with visual warning system using icons and color coding
|
||||||
- Live validation badges showing count/limit status with clear visual indicators
|
- Live validation badges showing count/limit status with clear visual indicators
|
||||||
|
|
|
@ -4,11 +4,7 @@ import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from deck_builder.builder import DeckBuilder
|
from deck_builder.builder import DeckBuilder
|
||||||
from deck_builder import builder_constants as bc
|
from deck_builder import builder_constants as bc
|
||||||
from file_setup.setup import initial_setup
|
from file_setup.setup import initial_setup
|
||||||
|
@ -207,7 +203,97 @@ def run(
|
||||||
def _should_export_json_headless() -> bool:
|
def _should_export_json_headless() -> bool:
|
||||||
return os.getenv('HEADLESS_EXPORT_JSON', '').strip().lower() in {'1','true','yes','on'}
|
return os.getenv('HEADLESS_EXPORT_JSON', '').strip().lower() in {'1','true','yes','on'}
|
||||||
|
|
||||||
|
def _print_include_exclude_summary(builder: DeckBuilder) -> None:
|
||||||
|
"""Print include/exclude summary to console (M4: Extended summary printing)."""
|
||||||
|
if not hasattr(builder, 'include_exclude_diagnostics') or not builder.include_exclude_diagnostics:
|
||||||
|
return
|
||||||
|
|
||||||
|
diagnostics = builder.include_exclude_diagnostics
|
||||||
|
|
||||||
|
# Skip if no include/exclude activity
|
||||||
|
if not any([
|
||||||
|
diagnostics.get('include_cards'),
|
||||||
|
diagnostics.get('exclude_cards'),
|
||||||
|
diagnostics.get('include_added'),
|
||||||
|
diagnostics.get('excluded_removed')
|
||||||
|
]):
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("INCLUDE/EXCLUDE SUMMARY")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Include cards impact
|
||||||
|
include_cards = diagnostics.get('include_cards', [])
|
||||||
|
if include_cards:
|
||||||
|
print(f"\n✓ Must Include Cards ({len(include_cards)}):")
|
||||||
|
|
||||||
|
include_added = diagnostics.get('include_added', [])
|
||||||
|
if include_added:
|
||||||
|
print(f" ✓ Successfully Added ({len(include_added)}):")
|
||||||
|
for card in include_added:
|
||||||
|
print(f" • {card}")
|
||||||
|
|
||||||
|
missing_includes = diagnostics.get('missing_includes', [])
|
||||||
|
if missing_includes:
|
||||||
|
print(f" ⚠ Could Not Include ({len(missing_includes)}):")
|
||||||
|
for card in missing_includes:
|
||||||
|
print(f" • {card}")
|
||||||
|
|
||||||
|
# Exclude cards impact
|
||||||
|
exclude_cards = diagnostics.get('exclude_cards', [])
|
||||||
|
if exclude_cards:
|
||||||
|
print(f"\n✗ Must Exclude Cards ({len(exclude_cards)}):")
|
||||||
|
|
||||||
|
excluded_removed = diagnostics.get('excluded_removed', [])
|
||||||
|
if excluded_removed:
|
||||||
|
print(f" ✓ Successfully Excluded ({len(excluded_removed)}):")
|
||||||
|
for card in excluded_removed:
|
||||||
|
print(f" • {card}")
|
||||||
|
|
||||||
|
print(" Patterns:")
|
||||||
|
for pattern in exclude_cards:
|
||||||
|
print(f" • {pattern}")
|
||||||
|
|
||||||
|
# Validation issues
|
||||||
|
issues = []
|
||||||
|
fuzzy_corrections = diagnostics.get('fuzzy_corrections', {})
|
||||||
|
if fuzzy_corrections:
|
||||||
|
issues.append(f"Fuzzy Matched ({len(fuzzy_corrections)})")
|
||||||
|
|
||||||
|
duplicates = diagnostics.get('duplicates_collapsed', {})
|
||||||
|
if duplicates:
|
||||||
|
issues.append(f"Duplicates Collapsed ({len(duplicates)})")
|
||||||
|
|
||||||
|
illegal_dropped = diagnostics.get('illegal_dropped', [])
|
||||||
|
if illegal_dropped:
|
||||||
|
issues.append(f"Illegal Cards Dropped ({len(illegal_dropped)})")
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
print("\n⚠ Validation Issues:")
|
||||||
|
|
||||||
|
if fuzzy_corrections:
|
||||||
|
print(" ⚡ Fuzzy Matched:")
|
||||||
|
for original, corrected in fuzzy_corrections.items():
|
||||||
|
print(f" • {original} → {corrected}")
|
||||||
|
|
||||||
|
if duplicates:
|
||||||
|
print(" Duplicates Collapsed:")
|
||||||
|
for card, count in duplicates.items():
|
||||||
|
print(f" • {card} ({count}x)")
|
||||||
|
|
||||||
|
if illegal_dropped:
|
||||||
|
print(" Illegal Cards Dropped:")
|
||||||
|
for card in illegal_dropped:
|
||||||
|
print(f" • {card}")
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
||||||
def _export_outputs(builder: DeckBuilder) -> None:
|
def _export_outputs(builder: DeckBuilder) -> None:
|
||||||
|
# M4: Print include/exclude summary to console
|
||||||
|
_print_include_exclude_summary(builder)
|
||||||
|
|
||||||
csv_path: Optional[str] = None
|
csv_path: Optional[str] = None
|
||||||
try:
|
try:
|
||||||
csv_path = builder.export_decklist_csv() if hasattr(builder, "export_decklist_csv") else None
|
csv_path = builder.export_decklist_csv() if hasattr(builder, "export_decklist_csv") else None
|
||||||
|
@ -252,6 +338,24 @@ def _parse_bool(val: Optional[str | bool | int]) -> Optional[bool]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_card_list(val: Optional[str]) -> List[str]:
|
||||||
|
"""Parse comma or semicolon-separated card list from CLI argument."""
|
||||||
|
if not val:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Support semicolon separation for card names with commas
|
||||||
|
if ';' in val:
|
||||||
|
return [card.strip() for card in val.split(';') if card.strip()]
|
||||||
|
|
||||||
|
# Use the intelligent parsing for comma-separated (handles card names with commas)
|
||||||
|
try:
|
||||||
|
from deck_builder.include_exclude_utils import parse_card_list_input
|
||||||
|
return parse_card_list_input(val)
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to simple comma split if import fails
|
||||||
|
return [card.strip() for card in val.split(',') if card.strip()]
|
||||||
|
|
||||||
|
|
||||||
def _parse_opt_int(val: Optional[str | int]) -> Optional[int]:
|
def _parse_opt_int(val: Optional[str | int]) -> Optional[int]:
|
||||||
if val is None:
|
if val is None:
|
||||||
return None
|
return None
|
||||||
|
@ -278,27 +382,94 @@ def _load_json_config(path: Optional[str]) -> Dict[str, Any]:
|
||||||
|
|
||||||
def _build_arg_parser() -> argparse.ArgumentParser:
|
def _build_arg_parser() -> argparse.ArgumentParser:
|
||||||
p = argparse.ArgumentParser(description="Headless deck builder runner")
|
p = argparse.ArgumentParser(description="Headless deck builder runner")
|
||||||
p.add_argument("--config", default=os.getenv("DECK_CONFIG"), help="Path to JSON config file")
|
p.add_argument("--config", metavar="PATH", default=os.getenv("DECK_CONFIG"),
|
||||||
p.add_argument("--commander", default=None)
|
help="Path to JSON config file (string)")
|
||||||
p.add_argument("--primary-choice", type=int, default=None)
|
p.add_argument("--commander", metavar="NAME", default=None,
|
||||||
p.add_argument("--secondary-choice", type=_parse_opt_int, default=None)
|
help="Commander name to search for (string)")
|
||||||
p.add_argument("--tertiary-choice", type=_parse_opt_int, default=None)
|
p.add_argument("--primary-choice", metavar="INT", type=int, default=None,
|
||||||
p.add_argument("--bracket-level", type=int, default=None)
|
help="Primary theme tag choice number (integer)")
|
||||||
p.add_argument("--add-lands", type=_parse_bool, default=None)
|
p.add_argument("--secondary-choice", metavar="INT", type=_parse_opt_int, default=None,
|
||||||
p.add_argument("--fetch-count", type=_parse_opt_int, default=None)
|
help="Secondary theme tag choice number (integer, optional)")
|
||||||
p.add_argument("--dual-count", type=_parse_opt_int, default=None)
|
p.add_argument("--tertiary-choice", metavar="INT", type=_parse_opt_int, default=None,
|
||||||
p.add_argument("--triple-count", type=_parse_opt_int, default=None)
|
help="Tertiary theme tag choice number (integer, optional)")
|
||||||
p.add_argument("--utility-count", type=_parse_opt_int, default=None)
|
p.add_argument("--primary-tag", metavar="NAME", default=None,
|
||||||
# no seed support
|
help="Primary theme tag name (string, alternative to --primary-choice)")
|
||||||
# Booleans
|
p.add_argument("--secondary-tag", metavar="NAME", default=None,
|
||||||
p.add_argument("--add-creatures", type=_parse_bool, default=None)
|
help="Secondary theme tag name (string, alternative to --secondary-choice)")
|
||||||
p.add_argument("--add-non-creature-spells", type=_parse_bool, default=None)
|
p.add_argument("--tertiary-tag", metavar="NAME", default=None,
|
||||||
p.add_argument("--add-ramp", type=_parse_bool, default=None)
|
help="Tertiary theme tag name (string, alternative to --tertiary-choice)")
|
||||||
p.add_argument("--add-removal", type=_parse_bool, default=None)
|
p.add_argument("--bracket-level", metavar="1-5", type=int, default=None,
|
||||||
p.add_argument("--add-wipes", type=_parse_bool, default=None)
|
help="Power bracket level 1-5 (integer)")
|
||||||
p.add_argument("--add-card-advantage", type=_parse_bool, default=None)
|
|
||||||
p.add_argument("--add-protection", type=_parse_bool, default=None)
|
# Ideal count arguments - new feature!
|
||||||
p.add_argument("--dry-run", action="store_true", help="Print resolved config and exit")
|
ideal_group = p.add_argument_group("Ideal Deck Composition",
|
||||||
|
"Override default target counts for deck categories")
|
||||||
|
ideal_group.add_argument("--ramp-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target number of ramp spells (integer, default: 8)")
|
||||||
|
ideal_group.add_argument("--land-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target total number of lands (integer, default: 35)")
|
||||||
|
ideal_group.add_argument("--basic-land-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Minimum number of basic lands (integer, default: 15)")
|
||||||
|
ideal_group.add_argument("--creature-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target number of creatures (integer, default: 25)")
|
||||||
|
ideal_group.add_argument("--removal-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target number of spot removal spells (integer, default: 10)")
|
||||||
|
ideal_group.add_argument("--wipe-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target number of board wipes (integer, default: 2)")
|
||||||
|
ideal_group.add_argument("--card-advantage-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target number of card advantage pieces (integer, default: 10)")
|
||||||
|
ideal_group.add_argument("--protection-count", metavar="INT", type=int, default=None,
|
||||||
|
help="Target number of protection spells (integer, default: 8)")
|
||||||
|
|
||||||
|
# Land-specific counts
|
||||||
|
land_group = p.add_argument_group("Land Configuration",
|
||||||
|
"Control specific land type counts and options")
|
||||||
|
land_group.add_argument("--add-lands", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Whether to add lands (bool: true/false/1/0)")
|
||||||
|
land_group.add_argument("--fetch-count", metavar="INT", type=_parse_opt_int, default=None,
|
||||||
|
help="Number of fetch lands to include (integer, optional)")
|
||||||
|
land_group.add_argument("--dual-count", metavar="INT", type=_parse_opt_int, default=None,
|
||||||
|
help="Number of dual lands to include (integer, optional)")
|
||||||
|
land_group.add_argument("--triple-count", metavar="INT", type=_parse_opt_int, default=None,
|
||||||
|
help="Number of triple lands to include (integer, optional)")
|
||||||
|
land_group.add_argument("--utility-count", metavar="INT", type=_parse_opt_int, default=None,
|
||||||
|
help="Number of utility lands to include (integer, optional)")
|
||||||
|
|
||||||
|
# Card type toggles
|
||||||
|
toggle_group = p.add_argument_group("Card Type Toggles",
|
||||||
|
"Enable/disable adding specific card types")
|
||||||
|
toggle_group.add_argument("--add-creatures", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add creatures to deck (bool: true/false/1/0)")
|
||||||
|
toggle_group.add_argument("--add-non-creature-spells", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add non-creature spells to deck (bool: true/false/1/0)")
|
||||||
|
toggle_group.add_argument("--add-ramp", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add ramp spells to deck (bool: true/false/1/0)")
|
||||||
|
toggle_group.add_argument("--add-removal", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add removal spells to deck (bool: true/false/1/0)")
|
||||||
|
toggle_group.add_argument("--add-wipes", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add board wipes to deck (bool: true/false/1/0)")
|
||||||
|
toggle_group.add_argument("--add-card-advantage", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add card advantage pieces to deck (bool: true/false/1/0)")
|
||||||
|
toggle_group.add_argument("--add-protection", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Add protection spells to deck (bool: true/false/1/0)")
|
||||||
|
|
||||||
|
# Include/Exclude configuration
|
||||||
|
include_group = p.add_argument_group("Include/Exclude Cards",
|
||||||
|
"Force include or exclude specific cards")
|
||||||
|
include_group.add_argument("--include-cards", metavar="CARDS",
|
||||||
|
help='Cards to force include (string: comma-separated, max 10). For cards with commas in names like "Krenko, Mob Boss", use semicolons or JSON config.')
|
||||||
|
include_group.add_argument("--exclude-cards", metavar="CARDS",
|
||||||
|
help='Cards to exclude from deck (string: comma-separated, max 15). For cards with commas in names like "Krenko, Mob Boss", use semicolons or JSON config.')
|
||||||
|
include_group.add_argument("--enforcement-mode", metavar="MODE", choices=["warn", "strict"], default=None,
|
||||||
|
help="How to handle missing includes (string: warn=continue, strict=abort)")
|
||||||
|
include_group.add_argument("--allow-illegal", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Allow illegal cards in includes/excludes (bool: true/false/1/0)")
|
||||||
|
include_group.add_argument("--fuzzy-matching", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
|
help="Enable fuzzy card name matching (bool: true/false/1/0)")
|
||||||
|
|
||||||
|
# Utility
|
||||||
|
p.add_argument("--dry-run", action="store_true",
|
||||||
|
help="Print resolved configuration and exit without building")
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
@ -375,6 +546,27 @@ def _main() -> int:
|
||||||
except Exception:
|
except Exception:
|
||||||
ideal_counts_json = {}
|
ideal_counts_json = {}
|
||||||
|
|
||||||
|
# Build ideal_counts dict from CLI args, JSON, or defaults
|
||||||
|
ideal_counts_resolved = {}
|
||||||
|
ideal_mappings = [
|
||||||
|
("ramp_count", "ramp", 8),
|
||||||
|
("land_count", "lands", 35),
|
||||||
|
("basic_land_count", "basic_lands", 15),
|
||||||
|
("creature_count", "creatures", 25),
|
||||||
|
("removal_count", "removal", 10),
|
||||||
|
("wipe_count", "wipes", 2),
|
||||||
|
("card_advantage_count", "card_advantage", 10),
|
||||||
|
("protection_count", "protection", 8),
|
||||||
|
]
|
||||||
|
|
||||||
|
for cli_key, json_key, default_val in ideal_mappings:
|
||||||
|
cli_val = getattr(args, cli_key, None)
|
||||||
|
if cli_val is not None:
|
||||||
|
ideal_counts_resolved[json_key] = cli_val
|
||||||
|
elif json_key in ideal_counts_json:
|
||||||
|
ideal_counts_resolved[json_key] = ideal_counts_json[json_key]
|
||||||
|
# Don't set defaults here - let the builder use its own defaults
|
||||||
|
|
||||||
# Pull include/exclude configuration from JSON (M1: Config + Validation + Persistence)
|
# Pull include/exclude configuration from JSON (M1: Config + Validation + Persistence)
|
||||||
include_cards_json = []
|
include_cards_json = []
|
||||||
exclude_cards_json = []
|
exclude_cards_json = []
|
||||||
|
@ -386,6 +578,97 @@ def _main() -> int:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# M4: Parse CLI include/exclude card lists
|
||||||
|
cli_include_cards = _parse_card_list(args.include_cards) if hasattr(args, 'include_cards') else []
|
||||||
|
cli_exclude_cards = _parse_card_list(args.exclude_cards) if hasattr(args, 'exclude_cards') else []
|
||||||
|
|
||||||
|
# Resolve tag names to indices BEFORE building resolved dict (so they can override defaults)
|
||||||
|
resolved_primary_choice = args.primary_choice
|
||||||
|
resolved_secondary_choice = args.secondary_choice
|
||||||
|
resolved_tertiary_choice = args.tertiary_choice
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Collect tag names from CLI, JSON, and environment (CLI takes precedence)
|
||||||
|
primary_tag_name = (
|
||||||
|
args.primary_tag or
|
||||||
|
(str(os.getenv("DECK_PRIMARY_TAG") or "").strip()) or
|
||||||
|
str(json_cfg.get("primary_tag", "")).strip()
|
||||||
|
)
|
||||||
|
secondary_tag_name = (
|
||||||
|
args.secondary_tag or
|
||||||
|
(str(os.getenv("DECK_SECONDARY_TAG") or "").strip()) or
|
||||||
|
str(json_cfg.get("secondary_tag", "")).strip()
|
||||||
|
)
|
||||||
|
tertiary_tag_name = (
|
||||||
|
args.tertiary_tag or
|
||||||
|
(str(os.getenv("DECK_TERTIARY_TAG") or "").strip()) or
|
||||||
|
str(json_cfg.get("tertiary_tag", "")).strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
tag_names = [t for t in [primary_tag_name, secondary_tag_name, tertiary_tag_name] if t]
|
||||||
|
if tag_names:
|
||||||
|
# Load commander name to resolve tags
|
||||||
|
commander_name = _resolve_value(args.commander, "DECK_COMMANDER", json_cfg, "commander", "")
|
||||||
|
if commander_name:
|
||||||
|
try:
|
||||||
|
# Load commander tags to compute indices
|
||||||
|
tmp = DeckBuilder()
|
||||||
|
df = tmp.load_commander_data()
|
||||||
|
row = df[df["name"] == commander_name]
|
||||||
|
if not row.empty:
|
||||||
|
original = list(dict.fromkeys(row.iloc[0].get("themeTags", []) or []))
|
||||||
|
|
||||||
|
# Step 1: primary from original
|
||||||
|
if primary_tag_name:
|
||||||
|
for i, t in enumerate(original, start=1):
|
||||||
|
if str(t).strip().lower() == primary_tag_name.strip().lower():
|
||||||
|
resolved_primary_choice = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Step 2: secondary from remaining after primary
|
||||||
|
if secondary_tag_name:
|
||||||
|
if resolved_primary_choice is not None:
|
||||||
|
# Create remaining list after removing primary choice
|
||||||
|
remaining_1 = [t for j, t in enumerate(original, start=1) if j != resolved_primary_choice]
|
||||||
|
for i2, t in enumerate(remaining_1, start=1):
|
||||||
|
if str(t).strip().lower() == secondary_tag_name.strip().lower():
|
||||||
|
resolved_secondary_choice = i2
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# If no primary set, secondary maps directly to original list
|
||||||
|
for i, t in enumerate(original, start=1):
|
||||||
|
if str(t).strip().lower() == secondary_tag_name.strip().lower():
|
||||||
|
resolved_secondary_choice = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Step 3: tertiary from remaining after primary+secondary
|
||||||
|
if tertiary_tag_name:
|
||||||
|
if resolved_primary_choice is not None and resolved_secondary_choice is not None:
|
||||||
|
# reconstruct remaining after removing primary then secondary as displayed
|
||||||
|
remaining_1 = [t for j, t in enumerate(original, start=1) if j != resolved_primary_choice]
|
||||||
|
remaining_2 = [t for j, t in enumerate(remaining_1, start=1) if j != resolved_secondary_choice]
|
||||||
|
for i3, t in enumerate(remaining_2, start=1):
|
||||||
|
if str(t).strip().lower() == tertiary_tag_name.strip().lower():
|
||||||
|
resolved_tertiary_choice = i3
|
||||||
|
break
|
||||||
|
elif resolved_primary_choice is not None:
|
||||||
|
# Only primary set, tertiary from remaining after primary
|
||||||
|
remaining_1 = [t for j, t in enumerate(original, start=1) if j != resolved_primary_choice]
|
||||||
|
for i, t in enumerate(remaining_1, start=1):
|
||||||
|
if str(t).strip().lower() == tertiary_tag_name.strip().lower():
|
||||||
|
resolved_tertiary_choice = i
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# No primary or secondary set, tertiary maps directly to original list
|
||||||
|
for i, t in enumerate(original, start=1):
|
||||||
|
if str(t).strip().lower() == tertiary_tag_name.strip().lower():
|
||||||
|
resolved_tertiary_choice = i
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
resolved = {
|
resolved = {
|
||||||
"command_name": _resolve_value(args.commander, "DECK_COMMANDER", json_cfg, "commander", defaults["command_name"]),
|
"command_name": _resolve_value(args.commander, "DECK_COMMANDER", json_cfg, "commander", defaults["command_name"]),
|
||||||
"add_creatures": _resolve_value(args.add_creatures, "DECK_ADD_CREATURES", json_cfg, "add_creatures", defaults["add_creatures"]),
|
"add_creatures": _resolve_value(args.add_creatures, "DECK_ADD_CREATURES", json_cfg, "add_creatures", defaults["add_creatures"]),
|
||||||
|
@ -395,72 +678,28 @@ def _main() -> int:
|
||||||
"add_wipes": _resolve_value(args.add_wipes, "DECK_ADD_WIPES", json_cfg, "add_wipes", defaults["add_wipes"]),
|
"add_wipes": _resolve_value(args.add_wipes, "DECK_ADD_WIPES", json_cfg, "add_wipes", defaults["add_wipes"]),
|
||||||
"add_card_advantage": _resolve_value(args.add_card_advantage, "DECK_ADD_CARD_ADVANTAGE", json_cfg, "add_card_advantage", defaults["add_card_advantage"]),
|
"add_card_advantage": _resolve_value(args.add_card_advantage, "DECK_ADD_CARD_ADVANTAGE", json_cfg, "add_card_advantage", defaults["add_card_advantage"]),
|
||||||
"add_protection": _resolve_value(args.add_protection, "DECK_ADD_PROTECTION", json_cfg, "add_protection", defaults["add_protection"]),
|
"add_protection": _resolve_value(args.add_protection, "DECK_ADD_PROTECTION", json_cfg, "add_protection", defaults["add_protection"]),
|
||||||
"primary_choice": _resolve_value(args.primary_choice, "DECK_PRIMARY_CHOICE", json_cfg, "primary_choice", defaults["primary_choice"]),
|
"primary_choice": _resolve_value(resolved_primary_choice, "DECK_PRIMARY_CHOICE", json_cfg, "primary_choice", defaults["primary_choice"]),
|
||||||
"secondary_choice": _resolve_value(args.secondary_choice, "DECK_SECONDARY_CHOICE", json_cfg, "secondary_choice", defaults["secondary_choice"]),
|
"secondary_choice": _resolve_value(resolved_secondary_choice, "DECK_SECONDARY_CHOICE", json_cfg, "secondary_choice", defaults["secondary_choice"]),
|
||||||
"tertiary_choice": _resolve_value(args.tertiary_choice, "DECK_TERTIARY_CHOICE", json_cfg, "tertiary_choice", defaults["tertiary_choice"]),
|
"tertiary_choice": _resolve_value(resolved_tertiary_choice, "DECK_TERTIARY_CHOICE", json_cfg, "tertiary_choice", defaults["tertiary_choice"]),
|
||||||
"bracket_level": _resolve_value(args.bracket_level, "DECK_BRACKET_LEVEL", json_cfg, "bracket_level", None),
|
"bracket_level": _resolve_value(args.bracket_level, "DECK_BRACKET_LEVEL", json_cfg, "bracket_level", None),
|
||||||
"add_lands": _resolve_value(args.add_lands, "DECK_ADD_LANDS", json_cfg, "add_lands", defaults["add_lands"]),
|
"add_lands": _resolve_value(args.add_lands, "DECK_ADD_LANDS", json_cfg, "add_lands", defaults["add_lands"]),
|
||||||
"fetch_count": _resolve_value(args.fetch_count, "DECK_FETCH_COUNT", json_cfg, "fetch_count", defaults["fetch_count"]),
|
"fetch_count": _resolve_value(args.fetch_count, "DECK_FETCH_COUNT", json_cfg, "fetch_count", defaults["fetch_count"]),
|
||||||
"dual_count": _resolve_value(args.dual_count, "DECK_DUAL_COUNT", json_cfg, "dual_count", defaults["dual_count"]),
|
"dual_count": _resolve_value(args.dual_count, "DECK_DUAL_COUNT", json_cfg, "dual_count", defaults["dual_count"]),
|
||||||
"triple_count": _resolve_value(args.triple_count, "DECK_TRIPLE_COUNT", json_cfg, "triple_count", defaults["triple_count"]),
|
"triple_count": _resolve_value(args.triple_count, "DECK_TRIPLE_COUNT", json_cfg, "triple_count", defaults["triple_count"]),
|
||||||
"utility_count": _resolve_value(args.utility_count, "DECK_UTILITY_COUNT", json_cfg, "utility_count", defaults["utility_count"]),
|
"utility_count": _resolve_value(args.utility_count, "DECK_UTILITY_COUNT", json_cfg, "utility_count", defaults["utility_count"]),
|
||||||
"ideal_counts": ideal_counts_json,
|
"ideal_counts": ideal_counts_resolved,
|
||||||
# Include/Exclude configuration (M1: Config + Validation + Persistence)
|
# M4: Include/Exclude configuration (CLI + JSON + Env priority)
|
||||||
"include_cards": include_cards_json,
|
"include_cards": cli_include_cards or include_cards_json,
|
||||||
"exclude_cards": exclude_cards_json,
|
"exclude_cards": cli_exclude_cards or exclude_cards_json,
|
||||||
"enforcement_mode": json_cfg.get("enforcement_mode", "warn"),
|
"enforcement_mode": args.enforcement_mode or json_cfg.get("enforcement_mode", "warn"),
|
||||||
"allow_illegal": bool(json_cfg.get("allow_illegal", False)),
|
"allow_illegal": args.allow_illegal if args.allow_illegal is not None else bool(json_cfg.get("allow_illegal", False)),
|
||||||
"fuzzy_matching": bool(json_cfg.get("fuzzy_matching", True)),
|
"fuzzy_matching": args.fuzzy_matching if args.fuzzy_matching is not None else bool(json_cfg.get("fuzzy_matching", True)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print(json.dumps(resolved, indent=2))
|
print(json.dumps(resolved, indent=2))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Optional: map tag names from JSON/env to numeric indices for this commander
|
|
||||||
try:
|
|
||||||
primary_tag_name = (str(os.getenv("DECK_PRIMARY_TAG") or "").strip()) or str(json_cfg.get("primary_tag", "")).strip()
|
|
||||||
secondary_tag_name = (str(os.getenv("DECK_SECONDARY_TAG") or "").strip()) or str(json_cfg.get("secondary_tag", "")).strip()
|
|
||||||
tertiary_tag_name = (str(os.getenv("DECK_TERTIARY_TAG") or "").strip()) or str(json_cfg.get("tertiary_tag", "")).strip()
|
|
||||||
tag_names = [t for t in [primary_tag_name, secondary_tag_name, tertiary_tag_name] if t]
|
|
||||||
if tag_names:
|
|
||||||
try:
|
|
||||||
# Load commander tags to compute indices
|
|
||||||
tmp = DeckBuilder()
|
|
||||||
df = tmp.load_commander_data()
|
|
||||||
row = df[df["name"] == resolved["command_name"]]
|
|
||||||
if not row.empty:
|
|
||||||
original = list(dict.fromkeys(row.iloc[0].get("themeTags", []) or []))
|
|
||||||
# Step 1: primary from original
|
|
||||||
if primary_tag_name:
|
|
||||||
for i, t in enumerate(original, start=1):
|
|
||||||
if str(t).strip().lower() == primary_tag_name.strip().lower():
|
|
||||||
resolved["primary_choice"] = i
|
|
||||||
break
|
|
||||||
# Step 2: secondary from remaining after primary
|
|
||||||
if secondary_tag_name:
|
|
||||||
primary_idx = resolved.get("primary_choice")
|
|
||||||
remaining_1 = [t for j, t in enumerate(original, start=1) if j != primary_idx]
|
|
||||||
for i2, t in enumerate(remaining_1, start=1):
|
|
||||||
if str(t).strip().lower() == secondary_tag_name.strip().lower():
|
|
||||||
resolved["secondary_choice"] = i2
|
|
||||||
break
|
|
||||||
# Step 3: tertiary from remaining after primary+secondary
|
|
||||||
if tertiary_tag_name and resolved.get("secondary_choice") is not None:
|
|
||||||
primary_idx = resolved.get("primary_choice")
|
|
||||||
secondary_idx = resolved.get("secondary_choice")
|
|
||||||
# reconstruct remaining after removing primary then secondary as displayed
|
|
||||||
remaining_1 = [t for j, t in enumerate(original, start=1) if j != primary_idx]
|
|
||||||
remaining_2 = [t for j, t in enumerate(remaining_1, start=1) if j != secondary_idx]
|
|
||||||
for i3, t in enumerate(remaining_2, start=1):
|
|
||||||
if str(t).strip().lower() == tertiary_tag_name.strip().lower():
|
|
||||||
resolved["tertiary_choice"] = i3
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not str(resolved.get("command_name", "")).strip():
|
if not str(resolved.get("command_name", "")).strip():
|
||||||
print("Error: commander is required. Provide --commander or a JSON config with a 'commander' field.")
|
print("Error: commander is required. Provide --commander or a JSON config with a 'commander' field.")
|
||||||
return 2
|
return 2
|
||||||
|
|
137
code/tests/test_cli_include_exclude.py
Normal file
137
code/tests/test_cli_include_exclude.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
"""
|
||||||
|
Test CLI include/exclude functionality (M4: CLI Parity).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIIncludeExclude:
|
||||||
|
"""Test CLI include/exclude argument parsing and functionality."""
|
||||||
|
|
||||||
|
def test_cli_argument_parsing(self):
|
||||||
|
"""Test that CLI arguments are properly parsed."""
|
||||||
|
# Test help output includes new arguments
|
||||||
|
result = subprocess.run(
|
||||||
|
['python', 'code/headless_runner.py', '--help'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=Path(__file__).parent.parent.parent
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
help_text = result.stdout
|
||||||
|
assert '--include-cards' in help_text
|
||||||
|
assert '--exclude-cards' in help_text
|
||||||
|
assert '--enforcement-mode' in help_text
|
||||||
|
assert '--allow-illegal' in help_text
|
||||||
|
assert '--fuzzy-matching' in help_text
|
||||||
|
assert 'semicolons' in help_text # Check for comma warning
|
||||||
|
|
||||||
|
def test_cli_dry_run_with_include_exclude(self):
|
||||||
|
"""Test dry run output includes include/exclude configuration."""
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'code/headless_runner.py',
|
||||||
|
'--commander', 'Krenko, Mob Boss',
|
||||||
|
'--include-cards', 'Sol Ring;Lightning Bolt',
|
||||||
|
'--exclude-cards', 'Chaos Orb',
|
||||||
|
'--enforcement-mode', 'strict',
|
||||||
|
'--dry-run'
|
||||||
|
], capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
# Parse the JSON output
|
||||||
|
config = json.loads(result.stdout)
|
||||||
|
|
||||||
|
assert config['command_name'] == 'Krenko, Mob Boss'
|
||||||
|
assert config['include_cards'] == ['Sol Ring', 'Lightning Bolt']
|
||||||
|
assert config['exclude_cards'] == ['Chaos Orb']
|
||||||
|
assert config['enforcement_mode'] == 'strict'
|
||||||
|
|
||||||
|
def test_cli_semicolon_parsing(self):
|
||||||
|
"""Test semicolon separation for card names with commas."""
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'code/headless_runner.py',
|
||||||
|
'--include-cards', 'Krenko, Mob Boss;Jace, the Mind Sculptor',
|
||||||
|
'--exclude-cards', 'Teferi, Hero of Dominaria',
|
||||||
|
'--dry-run'
|
||||||
|
], capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
config = json.loads(result.stdout)
|
||||||
|
assert config['include_cards'] == ['Krenko, Mob Boss', 'Jace, the Mind Sculptor']
|
||||||
|
assert config['exclude_cards'] == ['Teferi, Hero of Dominaria']
|
||||||
|
|
||||||
|
def test_cli_comma_parsing_simple_names(self):
|
||||||
|
"""Test comma separation for simple card names without commas."""
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'code/headless_runner.py',
|
||||||
|
'--include-cards', 'Sol Ring,Lightning Bolt,Counterspell',
|
||||||
|
'--exclude-cards', 'Island,Mountain',
|
||||||
|
'--dry-run'
|
||||||
|
], capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
config = json.loads(result.stdout)
|
||||||
|
assert config['include_cards'] == ['Sol Ring', 'Lightning Bolt', 'Counterspell']
|
||||||
|
assert config['exclude_cards'] == ['Island', 'Mountain']
|
||||||
|
|
||||||
|
def test_cli_json_priority(self):
|
||||||
|
"""Test that CLI arguments override JSON config values."""
|
||||||
|
# Create a temporary JSON config
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump({
|
||||||
|
'commander': 'Atraxa, Praetors\' Voice',
|
||||||
|
'include_cards': ['Doubling Season'],
|
||||||
|
'exclude_cards': ['Winter Orb'],
|
||||||
|
'enforcement_mode': 'warn'
|
||||||
|
}, f, indent=2)
|
||||||
|
temp_config = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'code/headless_runner.py',
|
||||||
|
'--config', temp_config,
|
||||||
|
'--include-cards', 'Sol Ring', # Override JSON
|
||||||
|
'--enforcement-mode', 'strict', # Override JSON
|
||||||
|
'--dry-run'
|
||||||
|
], capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
config = json.loads(result.stdout)
|
||||||
|
# CLI should override JSON
|
||||||
|
assert config['include_cards'] == ['Sol Ring'] # CLI override
|
||||||
|
assert config['exclude_cards'] == ['Winter Orb'] # From JSON (no CLI override)
|
||||||
|
assert config['enforcement_mode'] == 'strict' # CLI override
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_config)
|
||||||
|
|
||||||
|
def test_cli_empty_values(self):
|
||||||
|
"""Test handling of empty/missing include/exclude values."""
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'code/headless_runner.py',
|
||||||
|
'--commander', 'Krenko, Mob Boss',
|
||||||
|
'--dry-run'
|
||||||
|
], capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
config = json.loads(result.stdout)
|
||||||
|
assert config['include_cards'] == []
|
||||||
|
assert config['exclude_cards'] == []
|
||||||
|
assert config['enforcement_mode'] == 'warn' # Default
|
||||||
|
assert config['allow_illegal'] is False # Default
|
||||||
|
assert config['fuzzy_matching'] is True # Default
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
pytest.main([__file__])
|
119
test_cli_ideal_counts.py
Normal file
119
test_cli_ideal_counts.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick test script to verify CLI ideal count functionality works correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def test_cli_ideal_counts():
|
||||||
|
"""Test that CLI ideal count arguments work correctly."""
|
||||||
|
print("Testing CLI ideal count arguments...")
|
||||||
|
|
||||||
|
# Test dry-run with various ideal count CLI args
|
||||||
|
cmd = [
|
||||||
|
"python", "code/headless_runner.py",
|
||||||
|
"--commander", "Aang, Airbending Master",
|
||||||
|
"--creature-count", "30",
|
||||||
|
"--land-count", "37",
|
||||||
|
"--ramp-count", "10",
|
||||||
|
"--removal-count", "12",
|
||||||
|
"--basic-land-count", "18",
|
||||||
|
"--dry-run"
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd=".")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ Command failed: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = json.loads(result.stdout)
|
||||||
|
ideal_counts = config.get("ideal_counts", {})
|
||||||
|
|
||||||
|
# Verify CLI args took effect
|
||||||
|
expected = {
|
||||||
|
"creatures": 30,
|
||||||
|
"lands": 37,
|
||||||
|
"ramp": 10,
|
||||||
|
"removal": 12,
|
||||||
|
"basic_lands": 18
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expected_val in expected.items():
|
||||||
|
actual_val = ideal_counts.get(key)
|
||||||
|
if actual_val != expected_val:
|
||||||
|
print(f"❌ {key}: expected {expected_val}, got {actual_val}")
|
||||||
|
return False
|
||||||
|
print(f"✅ {key}: {actual_val}")
|
||||||
|
|
||||||
|
print("✅ All CLI ideal count arguments working correctly!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"❌ Failed to parse JSON output: {e}")
|
||||||
|
print(f"Output was: {result.stdout}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_help_contains_types():
|
||||||
|
"""Test that help text shows value types."""
|
||||||
|
print("\nTesting help text contains type information...")
|
||||||
|
|
||||||
|
cmd = ["python", "code/headless_runner.py", "--help"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd=".")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ Help command failed: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
help_text = result.stdout
|
||||||
|
|
||||||
|
# Check for type indicators
|
||||||
|
type_indicators = [
|
||||||
|
"PATH", "NAME", "INT", "BOOL", "CARDS", "MODE", "1-5"
|
||||||
|
]
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for indicator in type_indicators:
|
||||||
|
if indicator not in help_text:
|
||||||
|
missing.append(indicator)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(f"❌ Missing type indicators: {missing}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for organized sections
|
||||||
|
sections = [
|
||||||
|
"Ideal Deck Composition:",
|
||||||
|
"Land Configuration:",
|
||||||
|
"Card Type Toggles:",
|
||||||
|
"Include/Exclude Cards:"
|
||||||
|
]
|
||||||
|
|
||||||
|
missing_sections = []
|
||||||
|
for section in sections:
|
||||||
|
if section not in help_text:
|
||||||
|
missing_sections.append(section)
|
||||||
|
|
||||||
|
if missing_sections:
|
||||||
|
print(f"❌ Missing help sections: {missing_sections}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Help text contains proper type information and sections!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
success = True
|
||||||
|
success &= test_cli_ideal_counts()
|
||||||
|
success &= test_help_contains_types()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n🎉 All tests passed! CLI ideal count functionality working correctly.")
|
||||||
|
else:
|
||||||
|
print("\n❌ Some tests failed.")
|
||||||
|
|
||||||
|
exit(0 if success else 1)
|
Loading…
Add table
Add a link
Reference in a new issue