mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat: add supplemental theme catalog tooling, additional theme selection, and custom theme selection
This commit is contained in:
parent
3a1b011dbc
commit
9428e09cef
39 changed files with 3643 additions and 198 deletions
|
|
@ -10,6 +10,13 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||
|
||||
from deck_builder.builder import DeckBuilder
|
||||
from deck_builder import builder_constants as bc
|
||||
from deck_builder.theme_resolution import (
|
||||
ThemeResolutionInfo,
|
||||
clean_theme_inputs,
|
||||
normalize_theme_match_mode,
|
||||
parse_theme_list,
|
||||
resolve_additional_theme_inputs,
|
||||
)
|
||||
from file_setup.setup import initial_setup
|
||||
from tagging import tagger
|
||||
from exceptions import CommanderValidationError
|
||||
|
|
@ -81,6 +88,7 @@ def _tokenize_commander_name(value: Any) -> List[str]:
|
|||
return [token for token in re.split(r"[^a-z0-9]+", normalized) if token]
|
||||
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _load_commander_name_lookup() -> Tuple[set[str], Tuple[str, ...]]:
|
||||
builder = DeckBuilder(
|
||||
|
|
@ -193,6 +201,10 @@ def run(
|
|||
allow_illegal: bool = False,
|
||||
fuzzy_matching: bool = True,
|
||||
seed: Optional[int | str] = None,
|
||||
additional_themes: Optional[List[str]] = None,
|
||||
theme_match_mode: str = "permissive",
|
||||
user_theme_resolution: Optional[ThemeResolutionInfo] = None,
|
||||
user_theme_weight: Optional[float] = None,
|
||||
) -> DeckBuilder:
|
||||
"""Run a scripted non-interactive deck build and return the DeckBuilder instance."""
|
||||
trimmed_commander = (command_name or "").strip()
|
||||
|
|
@ -274,6 +286,34 @@ def run(
|
|||
builder.fuzzy_matching = fuzzy_matching # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
normalized_theme_mode = normalize_theme_match_mode(theme_match_mode)
|
||||
theme_resolution = user_theme_resolution
|
||||
if theme_resolution is None:
|
||||
theme_resolution = resolve_additional_theme_inputs(
|
||||
additional_themes or [],
|
||||
normalized_theme_mode,
|
||||
)
|
||||
else:
|
||||
if theme_resolution.mode != normalized_theme_mode:
|
||||
theme_resolution = resolve_additional_theme_inputs(
|
||||
theme_resolution.requested,
|
||||
normalized_theme_mode,
|
||||
)
|
||||
|
||||
try:
|
||||
builder.theme_match_mode = theme_resolution.mode # type: ignore[attr-defined]
|
||||
builder.theme_catalog_version = theme_resolution.catalog_version # type: ignore[attr-defined]
|
||||
builder.user_theme_requested = list(theme_resolution.requested) # type: ignore[attr-defined]
|
||||
builder.user_theme_resolved = list(theme_resolution.resolved) # type: ignore[attr-defined]
|
||||
builder.user_theme_matches = list(theme_resolution.matches) # type: ignore[attr-defined]
|
||||
builder.user_theme_unresolved = list(theme_resolution.unresolved) # type: ignore[attr-defined]
|
||||
builder.user_theme_fuzzy_corrections = dict(theme_resolution.fuzzy_corrections) # type: ignore[attr-defined]
|
||||
builder.user_theme_resolution = theme_resolution # type: ignore[attr-defined]
|
||||
if user_theme_weight is not None:
|
||||
builder.user_theme_weight = float(user_theme_weight) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If ideal_counts are provided (from JSON), use them as the current defaults
|
||||
# so the step 2 prompts will show these values and our blank entries will accept them.
|
||||
|
|
@ -1207,6 +1247,32 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|||
include_group.add_argument("--fuzzy-matching", metavar="BOOL", type=_parse_bool, default=None,
|
||||
help="Enable fuzzy card name matching (bool: true/false/1/0)")
|
||||
|
||||
theme_group = p.add_argument_group(
|
||||
"Additional Themes",
|
||||
"Supplement commander themes with catalog-backed user inputs",
|
||||
)
|
||||
theme_group.add_argument(
|
||||
"--additional-themes",
|
||||
metavar="THEMES",
|
||||
type=parse_theme_list,
|
||||
default=None,
|
||||
help="Additional theme names (comma or semicolon separated)",
|
||||
)
|
||||
theme_group.add_argument(
|
||||
"--theme-match-mode",
|
||||
metavar="MODE",
|
||||
choices=["strict", "permissive"],
|
||||
default=None,
|
||||
help="Theme resolution strategy (strict requires all matches)",
|
||||
)
|
||||
theme_group.add_argument(
|
||||
"--user-theme-weight",
|
||||
metavar="FLOAT",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Weight multiplier applied to supplemental themes (default 1.0)",
|
||||
)
|
||||
|
||||
# Random mode configuration (parity with web random builder)
|
||||
random_group = p.add_argument_group(
|
||||
"Random Mode",
|
||||
|
|
@ -1428,6 +1494,9 @@ def _main() -> int:
|
|||
resolved_primary_choice = args.primary_choice
|
||||
resolved_secondary_choice = args.secondary_choice
|
||||
resolved_tertiary_choice = args.tertiary_choice
|
||||
primary_tag_name: Optional[str] = None
|
||||
secondary_tag_name: Optional[str] = None
|
||||
tertiary_tag_name: Optional[str] = None
|
||||
|
||||
try:
|
||||
# Collect tag names from CLI, JSON, and environment (CLI takes precedence)
|
||||
|
|
@ -1511,6 +1580,69 @@ def _main() -> int:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
additional_themes_json: List[str] = []
|
||||
try:
|
||||
collected: List[str] = []
|
||||
for key in ("additional_themes", "userThemes"):
|
||||
raw_value = json_cfg.get(key)
|
||||
if isinstance(raw_value, list):
|
||||
collected.extend(raw_value)
|
||||
if collected:
|
||||
additional_themes_json = clean_theme_inputs(collected)
|
||||
except Exception:
|
||||
additional_themes_json = []
|
||||
|
||||
cli_additional_themes: List[str] = []
|
||||
if hasattr(args, "additional_themes") and args.additional_themes:
|
||||
if isinstance(args.additional_themes, list):
|
||||
cli_additional_themes = clean_theme_inputs(args.additional_themes)
|
||||
else:
|
||||
cli_additional_themes = parse_theme_list(str(args.additional_themes))
|
||||
|
||||
env_additional_themes = parse_theme_list(os.getenv("DECK_ADDITIONAL_THEMES"))
|
||||
|
||||
additional_theme_inputs = (
|
||||
cli_additional_themes
|
||||
or env_additional_themes
|
||||
or additional_themes_json
|
||||
)
|
||||
|
||||
theme_mode_value = getattr(args, "theme_match_mode", None)
|
||||
if not theme_mode_value:
|
||||
theme_mode_value = os.getenv("THEME_MATCH_MODE")
|
||||
if not theme_mode_value:
|
||||
theme_mode_value = json_cfg.get("theme_match_mode") or json_cfg.get("themeMatchMode")
|
||||
normalized_theme_mode = normalize_theme_match_mode(theme_mode_value)
|
||||
|
||||
weight_value: Optional[float]
|
||||
if hasattr(args, "user_theme_weight") and args.user_theme_weight is not None:
|
||||
weight_value = args.user_theme_weight
|
||||
else:
|
||||
cfg_weight = json_cfg.get("user_theme_weight")
|
||||
if cfg_weight is not None:
|
||||
try:
|
||||
weight_value = float(cfg_weight)
|
||||
except Exception:
|
||||
weight_value = None
|
||||
else:
|
||||
weight_value = None
|
||||
|
||||
commander_tag_names = [
|
||||
str(tag)
|
||||
for tag in (primary_tag_name, secondary_tag_name, tertiary_tag_name)
|
||||
if isinstance(tag, str) and tag and str(tag).strip()
|
||||
]
|
||||
|
||||
try:
|
||||
theme_resolution = resolve_additional_theme_inputs(
|
||||
additional_theme_inputs,
|
||||
normalized_theme_mode,
|
||||
commander_tags=commander_tag_names,
|
||||
)
|
||||
except ValueError as exc:
|
||||
print(str(exc))
|
||||
return 2
|
||||
|
||||
resolved = {
|
||||
"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"]),
|
||||
|
|
@ -1536,18 +1668,45 @@ def _main() -> int:
|
|||
"enforcement_mode": args.enforcement_mode or json_cfg.get("enforcement_mode", "warn"),
|
||||
"allow_illegal": args.allow_illegal if args.allow_illegal is not None else bool(json_cfg.get("allow_illegal", False)),
|
||||
"fuzzy_matching": args.fuzzy_matching if args.fuzzy_matching is not None else bool(json_cfg.get("fuzzy_matching", True)),
|
||||
"additional_themes": list(theme_resolution.requested),
|
||||
"theme_match_mode": theme_resolution.mode,
|
||||
"user_theme_weight": weight_value,
|
||||
}
|
||||
|
||||
if args.dry_run:
|
||||
print(json.dumps(resolved, indent=2))
|
||||
preview = dict(resolved)
|
||||
preview["additional_themes_resolved"] = list(theme_resolution.resolved)
|
||||
preview["additional_themes_unresolved"] = list(theme_resolution.unresolved)
|
||||
preview["theme_catalog_version"] = theme_resolution.catalog_version
|
||||
preview["fuzzy_corrections"] = dict(theme_resolution.fuzzy_corrections)
|
||||
preview["user_theme_weight"] = weight_value
|
||||
print(json.dumps(preview, indent=2))
|
||||
return 0
|
||||
|
||||
if not str(resolved.get("command_name", "")).strip():
|
||||
print("Error: commander is required. Provide --commander or a JSON config with a 'commander' field.")
|
||||
return 2
|
||||
|
||||
if theme_resolution.requested:
|
||||
if theme_resolution.fuzzy_corrections:
|
||||
print("Fuzzy theme corrections applied:")
|
||||
for original, corrected in theme_resolution.fuzzy_corrections.items():
|
||||
print(f" • {original} → {corrected}")
|
||||
if theme_resolution.unresolved and theme_resolution.mode != "strict":
|
||||
print("Warning: unresolved additional themes (permissive mode):")
|
||||
for item in theme_resolution.unresolved:
|
||||
suggestion_text = ", ".join(
|
||||
f"{s['theme']} ({s['score']:.1f})" for s in item.get("suggestions", [])
|
||||
)
|
||||
if suggestion_text:
|
||||
print(f" • {item['input']} → suggestions: {suggestion_text}")
|
||||
else:
|
||||
print(f" • {item['input']} (no suggestions)")
|
||||
|
||||
try:
|
||||
run(**resolved)
|
||||
run_kwargs = dict(resolved)
|
||||
run_kwargs["user_theme_resolution"] = theme_resolution
|
||||
run(**run_kwargs)
|
||||
except CommanderValidationError as exc:
|
||||
print(str(exc))
|
||||
return 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue