mtg_python_deckbuilder/code/main.py

209 lines
No EOL
7.6 KiB
Python

"""Command-line entrypoint for the MTG Python Deckbuilder.
Launches directly into the interactive deck builder. On first run (or if the
card database is missing), it automatically performs initial setup and tagging.
"""
from __future__ import annotations
# Standard library imports
import sys
from pathlib import Path
import json
from typing import NoReturn
# Local imports
from deck_builder import DeckBuilder
from file_setup.setup import initial_setup
from tagging import tagger
import logging_util
import os
from settings import CSV_DIRECTORY
# Create logger for this module
logger = logging_util.logging.getLogger(__name__)
logger.setLevel(logging_util.LOG_LEVEL)
logger.addHandler(logging_util.file_handler)
logger.addHandler(logging_util.stream_handler)
builder = DeckBuilder()
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)
Path('logs').mkdir(parents=True, exist_ok=True)
# Ensure required CSVs exist and are tagged before proceeding
try:
cards_path = os.path.join(CSV_DIRECTORY, 'cards.csv')
if not os.path.exists(cards_path):
logger.info("cards.csv not found. Running initial setup and tagging...")
initial_setup()
tagger.run_tagging()
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
DeckBuilder().build_deck_full()
except Exception as e:
logger.error(f"Unexpected error in deck builder: {e}")
# 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()