Release v1.1.0: headless runner + tagging updates (Discard Matters, Freerunning, Craft, Spree, Explore/Map, Rad, Energy/Resource Engine, Spawn/Scion)

This commit is contained in:
matt 2025-08-22 16:32:39 -07:00
parent 36abbaa1dd
commit 99005c19f8
23 changed files with 1330 additions and 420 deletions

View file

@ -8,6 +8,7 @@ from __future__ import annotations
# Standard library imports
import sys
from pathlib import Path
import json
from typing import NoReturn
# Local imports
@ -26,12 +27,7 @@ logger.addHandler(logging_util.stream_handler)
builder = DeckBuilder()
def run_menu() -> NoReturn:
"""Launch directly into the deck builder after ensuring data files exist.
Creates required directories, ensures card CSVs are present (running setup
and tagging if needed), then starts the full deck build flow. Exits when done.
"""
def _ensure_data_ready() -> None:
logger.info("Starting MTG Python Deckbuilder")
Path('csv_files').mkdir(parents=True, exist_ok=True)
Path('deck_files').mkdir(parents=True, exist_ok=True)
@ -47,6 +43,9 @@ def run_menu() -> NoReturn:
logger.info("Initial setup and tagging completed.")
except Exception as e:
logger.error(f"Failed ensuring CSVs are ready: {e}")
def _interactive_loop() -> None:
while True:
try:
# Fresh builder instance for each deck to avoid state carryover
@ -54,15 +53,157 @@ def run_menu() -> NoReturn:
except Exception as e:
logger.error(f"Unexpected error in deck builder: {e}")
# Prompt to build another deck or quit
# Prompt to build another deck or return to main menu
try:
resp = input("\nBuild another deck? (y/n): ").strip().lower()
except KeyboardInterrupt:
resp = 'n'
print("")
if resp not in ('y', 'yes'):
break
def run_menu() -> NoReturn:
"""Launch directly into the deck builder after ensuring data files exist.
Creates required directories, ensures card CSVs are present (running setup
and tagging if needed), then starts the full deck build flow. Exits when done.
"""
_ensure_data_ready()
# Auto headless mode for container runs (no menu prompt)
auto_mode = os.getenv('DECK_MODE', '').strip().lower()
if auto_mode in ("headless", "noninteractive", "auto"):
try:
from headless_runner import _main as headless_main
headless_main()
except Exception as e:
logger.error(f"Headless run failed: {e}")
logger.info("Exiting application")
sys.exit(0)
# Menu-driven selection
def _run_headless_with_config(selected_config: str | None) -> None:
"""Run headless runner, optionally forcing a specific config path for this invocation."""
try:
from headless_runner import _main as headless_main
# Temporarily override DECK_CONFIG for this run if provided
prev_cfg = os.environ.get('DECK_CONFIG')
try:
if selected_config:
os.environ['DECK_CONFIG'] = selected_config
headless_main()
finally:
if selected_config is not None:
if prev_cfg is not None:
os.environ['DECK_CONFIG'] = prev_cfg
else:
os.environ.pop('DECK_CONFIG', None)
except Exception as e:
logger.error(f"Headless run failed: {e}")
def _headless_submenu() -> None:
"""Submenu to choose a JSON config and run the headless builder.
Behavior:
- If DECK_CONFIG points to a file, run it immediately.
- Else, search for *.json in (DECK_CONFIG as dir) or /app/config or ./config.
- If one file is found, run it immediately.
- If multiple files, list them for selection.
- If none, fall back to running headless using env/CLI/defaults.
"""
cfg_target = os.getenv('DECK_CONFIG')
# Case 1: DECK_CONFIG is an explicit file
if cfg_target and os.path.isfile(cfg_target):
print(f"\nRunning headless with config: {cfg_target}")
_run_headless_with_config(cfg_target)
return
# Determine directory to scan for JSON configs
if cfg_target and os.path.isdir(cfg_target):
cfg_dir = cfg_target
elif os.path.isdir('/app/config'):
cfg_dir = '/app/config'
else:
cfg_dir = 'config'
try:
p = Path(cfg_dir)
files = sorted([str(fp) for fp in p.glob('*.json')]) if p.exists() else []
except Exception:
files = []
# No configs found: run headless with current env/CLI/defaults
if not files:
print("\nNo JSON configs found in '" + cfg_dir + "'. Running headless with env/CLI/defaults...")
_run_headless_with_config(None)
return
# Single config: run automatically
if len(files) == 1:
print(f"\nFound one JSON config: {files[0]}\nRunning it now...")
_run_headless_with_config(files[0])
return
# Multiple configs: list and select
def _config_label(p: str) -> str:
try:
with open(p, 'r', encoding='utf-8') as fh:
data = json.load(fh)
cmd = str(data.get('commander') or '').strip() or 'Unknown Commander'
themes = [t for t in [data.get('primary_tag'), data.get('secondary_tag'), data.get('tertiary_tag')] if isinstance(t, str) and t.strip()]
name = os.path.basename(p).lower()
if name == 'deck.json':
return 'Default'
return f"{cmd} - {', '.join(themes)}" if themes else cmd
except Exception:
return p
print("\nAvailable JSON configs:")
labels = [_config_label(f) for f in files]
for idx, label in enumerate(labels, start=1):
print(f" {idx}) {label}")
print(" 0) Back to main menu")
while True:
try:
sel = input("Select a config to run [0]: ").strip() or '0'
except KeyboardInterrupt:
print("")
sel = '0'
if sel == '0':
return
try:
i = int(sel)
if 1 <= i <= len(files):
_run_headless_with_config(files[i - 1])
return
except ValueError:
pass
print("Invalid selection. Try again.")
while True:
print("\n==== MTG Deckbuilder ====")
print("1) Interactive deck build")
print("2) Headless (env/JSON-configured) run")
print(" - Will auto-run a single config if found, or let you choose from many")
print("q) Quit")
try:
choice = input("Select an option [1]: ").strip().lower() or '1'
except KeyboardInterrupt:
print("")
choice = 'q'
if choice in ('1', 'i', 'interactive'):
_interactive_loop()
# loop returns to main menu
elif choice in ('2', 'h', 'headless', 'noninteractive'):
_headless_submenu()
# after one headless run, return to menu
elif choice in ('q', 'quit', 'exit'):
logger.info("Exiting application")
sys.exit(0)
else:
print("Invalid selection. Please try again.")
if __name__ == "__main__":
run_menu()