mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
chore: prep 2.3.1 docs and note Hero creature handling
This commit is contained in:
parent
2c4eb4ba23
commit
4f7d39acba
11 changed files with 1036 additions and 226 deletions
15
.env.example
15
.env.example
|
|
@ -13,7 +13,7 @@
|
||||||
# HOST=0.0.0.0 # Uvicorn bind host (only when APP_MODE=web).
|
# HOST=0.0.0.0 # Uvicorn bind host (only when APP_MODE=web).
|
||||||
# PORT=8080 # Uvicorn port.
|
# PORT=8080 # Uvicorn port.
|
||||||
# WORKERS=1 # Uvicorn worker count.
|
# WORKERS=1 # Uvicorn worker count.
|
||||||
APP_VERSION=v2.2.10 # Matches dockerhub compose.
|
APP_VERSION=v2.3.1 # Matches dockerhub compose.
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Theming
|
# Theming
|
||||||
|
|
@ -50,6 +50,19 @@ WEB_THEME_PICKER_DIAGNOSTICS=0 # 1=enable uncapped synergies, diagnostics f
|
||||||
# RANDOM_UI=1 # Show Surprise/Reroll/Share controls in UI
|
# RANDOM_UI=1 # Show Surprise/Reroll/Share controls in UI
|
||||||
# RANDOM_MAX_ATTEMPTS=5 # Cap retry attempts for constrained random builds
|
# RANDOM_MAX_ATTEMPTS=5 # Cap retry attempts for constrained random builds
|
||||||
# RANDOM_TIMEOUT_MS=5000 # Per-attempt timeout (ms)
|
# RANDOM_TIMEOUT_MS=5000 # Per-attempt timeout (ms)
|
||||||
|
# HEADLESS_RANDOM_MODE=1 # Force headless runner to invoke random flow instead of scripted build
|
||||||
|
# RANDOM_THEME=Treasure # Legacy single-theme alias (maps to primary theme if others unset)
|
||||||
|
# RANDOM_PRIMARY_THEME=Treasure # Primary theme slug (case-insensitive)
|
||||||
|
# RANDOM_SECONDARY_THEME=Artifacts # Secondary theme slug
|
||||||
|
# RANDOM_TERTIARY_THEME=Tokens # Tertiary theme slug
|
||||||
|
# RANDOM_AUTO_FILL=1 # Auto-fill missing secondary/tertiary slots
|
||||||
|
# RANDOM_AUTO_FILL_SECONDARY=1 # Explicit secondary auto-fill override (fallback from RANDOM_AUTO_FILL)
|
||||||
|
# RANDOM_AUTO_FILL_TERTIARY=1 # Explicit tertiary auto-fill override (fallback from RANDOM_AUTO_FILL)
|
||||||
|
# RANDOM_STRICT_THEME_MATCH=0 # Require strict commander theme matches
|
||||||
|
# RANDOM_CONSTRAINTS= # Inline JSON or path to JSON constraints for random selection
|
||||||
|
# RANDOM_CONSTRAINTS_PATH= # Alternate path-based constraints override (takes precedence if set)
|
||||||
|
# RANDOM_SEED= # Optional deterministic seed (int or string)
|
||||||
|
# RANDOM_OUTPUT_JSON=deck_files/random_build.json # Where to write random build metadata payload
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Automation & Performance (Web)
|
# Automation & Performance (Web)
|
||||||
|
|
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -14,10 +14,22 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- _No changes yet._
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- _No changes yet._
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- _No changes yet._
|
||||||
|
|
||||||
|
## [2.3.1] - 2025-09-29
|
||||||
|
### Added
|
||||||
|
- Headless runner parity: added `--random-mode` and accompanying `--random-*` flags to mirror the web Surprise/Reroll builder (multi-theme inputs, auto-fill overrides, deterministic seeds, constraints, and optional JSON payload export).
|
||||||
- Tests: added `test_headless_skips_owned_prompt_when_files_present` to guard the headless runner against regressions when owned card lists are present.
|
- Tests: added `test_headless_skips_owned_prompt_when_files_present` to guard the headless runner against regressions when owned card lists are present.
|
||||||
- Included the tiny `csv_files/testdata` fixture set so CI fast determinism tests have consistent sample data.
|
- Included the tiny `csv_files/testdata` fixture set so CI fast determinism tests have consistent sample data.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Configuration docs: docker compose manifests, `.env.example`, and README now enumerate all supported random-mode environment variables with sensible defaults and refreshed flag documentation for the headless runner.
|
||||||
- Owned Cards library tiles now use larger thumbnails and wider columns, and virtualization only activates when more than 800 cards are present to keep scrolling smooth.
|
- Owned Cards library tiles now use larger thumbnails and wider columns, and virtualization only activates when more than 800 cards are present to keep scrolling smooth.
|
||||||
- Theme catalog schema now accepts optional `id` values on entries so refreshed catalogs validate cleanly.
|
- Theme catalog schema now accepts optional `id` values on entries so refreshed catalogs validate cleanly.
|
||||||
- CI installs `httpx` with the rest of the web stack and runs pytest via `python -m pytest` so FastAPI tests resolve the local `code` package correctly.
|
- CI installs `httpx` with the rest of the web stack and runs pytest via `python -m pytest` so FastAPI tests resolve the local `code` package correctly.
|
||||||
|
|
@ -40,6 +52,7 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
|
||||||
- Duplicate overlap highlighting on desktop hover has been removed; theme pills now render once without stray bullets even when multiple overlaps are present.
|
- Duplicate overlap highlighting on desktop hover has been removed; theme pills now render once without stray bullets even when multiple overlaps are present.
|
||||||
- Headless runner no longer loops on the power bracket prompt when owned card files exist; scripted responses now auto-select defaults with optional `HEADLESS_USE_OWNED_ONLY` / `HEADLESS_OWNED_SELECTION` overrides for automation flows.
|
- Headless runner no longer loops on the power bracket prompt when owned card files exist; scripted responses now auto-select defaults with optional `HEADLESS_USE_OWNED_ONLY` / `HEADLESS_OWNED_SELECTION` overrides for automation flows.
|
||||||
- Regenerated `logs/perf/theme_preview_warm_baseline.json` to repair preview performance CI regressions caused by a malformed baseline file and verified the regression gate passes with the refreshed data.
|
- Regenerated `logs/perf/theme_preview_warm_baseline.json` to repair preview performance CI regressions caused by a malformed baseline file and verified the regression gate passes with the refreshed data.
|
||||||
|
- File setup now keeps cards with the Hero creature type; previously they were filtered out alongside the non-Commander-legal Hero card type.
|
||||||
|
|
||||||
## [2.3.0] - 2025-09-26
|
## [2.3.0] - 2025-09-26
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
BIN
README.md
BIN
README.md
Binary file not shown.
|
|
@ -6,6 +6,7 @@
|
||||||
- Fast-path catalog validation now tolerates empty synergy lists while still flagging missing fields or non-string entries.
|
- Fast-path catalog validation now tolerates empty synergy lists while still flagging missing fields or non-string entries.
|
||||||
- Committed deterministic CSV fixtures under `csv_files/testdata` so CI random-mode checks have a stable dataset.
|
- Committed deterministic CSV fixtures under `csv_files/testdata` so CI random-mode checks have a stable dataset.
|
||||||
- Owned Cards library tiles are larger, virtualization only kicks in for very large libraries, and popovers no longer show empty role pills.
|
- Owned Cards library tiles are larger, virtualization only kicks in for very large libraries, and popovers no longer show empty role pills.
|
||||||
|
- File setup now keeps Hero creature type cards instead of filtering them with the non-Commander-legal Hero card type.
|
||||||
- Random Mode fallback warning remains hidden when no theme filters are provided, keeping Surprise Me runs noise-free.
|
- Random Mode fallback warning remains hidden when no theme filters are provided, keeping Surprise Me runs noise-free.
|
||||||
- Hover previews regained the double-faced card flip button with state synced to the main tile, highlight only the themes that triggered inclusion, and present a mobile-friendly tap layout with centered positioning plus a close control.
|
- Hover previews regained the double-faced card flip button with state synced to the main tile, highlight only the themes that triggered inclusion, and present a mobile-friendly tap layout with centered positioning plus a close control.
|
||||||
- Deck summary text view exposes inline flip toggles for double-faced cards so counts and face switching stay in sync.
|
- Deck summary text view exposes inline flip toggles for double-faced cards so counts and face switching stay in sync.
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
- Added opt-in telemetry counters, reroll throttling safeguards, and structured diagnostics exports.
|
- Added opt-in telemetry counters, reroll throttling safeguards, and structured diagnostics exports.
|
||||||
- Expanded tooling, documentation, and QA coverage for theme governance, performance profiling, and seed history management.
|
- Expanded tooling, documentation, and QA coverage for theme governance, performance profiling, and seed history management.
|
||||||
- Hardened the headless runner against owned-card prompt loops with optional automation overrides and regression coverage.
|
- Hardened the headless runner against owned-card prompt loops with optional automation overrides and regression coverage.
|
||||||
|
- Headless runner random mode now mirrors the web UI configuration surface with CLI flag documentation, dry-run summaries, and JSON export parity.
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
### Multi-theme random builds
|
### Multi-theme random builds
|
||||||
|
|
@ -43,6 +45,7 @@
|
||||||
- Random theme exclusion catalog with reporting script and documentation, alongside a multi-theme performance profiler and regression guard.
|
- Random theme exclusion catalog with reporting script and documentation, alongside a multi-theme performance profiler and regression guard.
|
||||||
- Taxonomy snapshot tooling, splash penalty analytics, and governance documentation updated for strict alias and example enforcement.
|
- Taxonomy snapshot tooling, splash penalty analytics, and governance documentation updated for strict alias and example enforcement.
|
||||||
- README, CHANGELOG, and release notes refreshed to cover the random modes feature set.
|
- README, CHANGELOG, and release notes refreshed to cover the random modes feature set.
|
||||||
|
- Headless runner documentation now covers random mode CLI flags, env precedence, and parity with the web builder.
|
||||||
|
|
||||||
### Observability & QA
|
### Observability & QA
|
||||||
- Diagnostics badge polish, recent/favorite seeds panel, seed history API, and structured logging for random builds.
|
- Diagnostics badge polish, recent/favorite seeds panel, seed history API, and structured logging for random builds.
|
||||||
|
|
@ -55,6 +58,7 @@
|
||||||
- GitHub Actions now includes `httpx` in the default dependency install and executes pytest via `python -m` so FastAPI TestClient suites run without import errors.
|
- GitHub Actions now includes `httpx` in the default dependency install and executes pytest via `python -m` so FastAPI TestClient suites run without import errors.
|
||||||
- Fast path validator treats empty synergy arrays as acceptable and only warns on missing or malformed data, reducing noise during automated catalog generation.
|
- Fast path validator treats empty synergy arrays as acceptable and only warns on missing or malformed data, reducing noise during automated catalog generation.
|
||||||
- Tracked the tiny `csv_files/testdata` dataset in Git to guarantee fast determinism tests run against a consistent fixture set.
|
- Tracked the tiny `csv_files/testdata` dataset in Git to guarantee fast determinism tests run against a consistent fixture set.
|
||||||
|
- File setup retains cards tagged with the Hero creature type while continuing to skip the non-Commander-legal Hero card type.
|
||||||
|
|
||||||
## Detailed changes
|
## Detailed changes
|
||||||
### Added
|
### Added
|
||||||
|
|
@ -92,6 +96,7 @@
|
||||||
- Cache bust hooks now clear filter/preview caches on catalog refresh or tagging completion; metrics expose `preview_last_bust_at` and warm cache stats.
|
- Cache bust hooks now clear filter/preview caches on catalog refresh or tagging completion; metrics expose `preview_last_bust_at` and warm cache stats.
|
||||||
- Theme normalization standardizes terms (ETB → Enter the Battlefield, Pillow Fort → Pillowfort, etc.), with synergy output capped at five entries (curated > enforced > inferred ordering).
|
- Theme normalization standardizes terms (ETB → Enter the Battlefield, Pillow Fort → Pillowfort, etc.), with synergy output capped at five entries (curated > enforced > inferred ordering).
|
||||||
- README, CHANGELOG, and governance docs updated to reflect new workflows, taxonomy snapshots, and telemetry controls.
|
- README, CHANGELOG, and governance docs updated to reflect new workflows, taxonomy snapshots, and telemetry controls.
|
||||||
|
- Headless runner random mode uses shared configuration resolution, emits summary/payload artifacts, and exposes CLI flags matching the web UI.
|
||||||
- Theme catalog schema now allows optional `id` fields on entries so regenerated catalogs validate cleanly.
|
- Theme catalog schema now allows optional `id` fields on entries so regenerated catalogs validate cleanly.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
@ -106,10 +111,12 @@
|
||||||
- Corrected commander eligibility rules to restrict non-creature legendary permanents and honor “can be your commander” text.
|
- Corrected commander eligibility rules to restrict non-creature legendary permanents and honor “can be your commander” text.
|
||||||
- Refreshed `logs/perf/theme_preview_warm_baseline.json` to fix preview performance CI failures stemming from malformed baseline data.
|
- Refreshed `logs/perf/theme_preview_warm_baseline.json` to fix preview performance CI failures stemming from malformed baseline data.
|
||||||
- Prevented the headless runner from looping on bracket selection when owned card files exist by scripting prompt responses and exposing `HEADLESS_USE_OWNED_ONLY` / `HEADLESS_OWNED_SELECTION` overrides.
|
- Prevented the headless runner from looping on bracket selection when owned card files exist by scripting prompt responses and exposing `HEADLESS_USE_OWNED_ONLY` / `HEADLESS_OWNED_SELECTION` overrides.
|
||||||
|
- Fixed file setup filtering so Hero creature type cards remain available even though the Hero card type stays disallowed for Commander.
|
||||||
|
|
||||||
## Upgrade notes
|
## Upgrade notes
|
||||||
- Enable multi-theme random builds via existing Random Mode flags; strict matching persists automatically across UI, API, permalink, and export contexts.
|
- Enable multi-theme random builds via existing Random Mode flags; strict matching persists automatically across UI, API, permalink, and export contexts.
|
||||||
- Opt into telemetry by setting `RANDOM_TELEMETRY=1`; reroll throttle defaults are active but can be tuned through environment overrides.
|
- Opt into telemetry by setting `RANDOM_TELEMETRY=1`; reroll throttle defaults are active but can be tuned through environment overrides.
|
||||||
|
- Run headless random builds with `HEADLESS_RANDOM_MODE=1` or the new CLI `--random-*` flags to mirror Surprise Me behavior, optionally persisting outputs via `--random-output-json`.
|
||||||
- Refresh performance baselines with `code/scripts/check_random_theme_perf.py --update-baseline` when catalog changes materially affect timings.
|
- Refresh performance baselines with `code/scripts/check_random_theme_perf.py --update-baseline` when catalog changes materially affect timings.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,15 @@ BANNED_CARDS: List[str] = [
|
||||||
'Trade Secrets', 'Upheaval', "Yawgmoth's Bargain",
|
'Trade Secrets', 'Upheaval', "Yawgmoth's Bargain",
|
||||||
# Problematic / culturally sensitive or banned in other formats
|
# Problematic / culturally sensitive or banned in other formats
|
||||||
'Invoke Prejudice', 'Cleanse', 'Stone-Throwing Devils', 'Pradesh Gypsies',
|
'Invoke Prejudice', 'Cleanse', 'Stone-Throwing Devils', 'Pradesh Gypsies',
|
||||||
'Jihad', 'Imprison', 'Crusade'
|
'Jihad', 'Imprison', 'Crusade',
|
||||||
|
# Cards of the Hero type (non creature)
|
||||||
|
"The Protector", "The Hunter", "The Savant", "The Explorer",
|
||||||
|
"The Philosopher", "The Harvester", "The Tyrant", "The Vanquisher",
|
||||||
|
"The Avenger", "The Slayer", "The Warmonger", "The Destined",
|
||||||
|
"The Warrior", "The General", "The Provider", "The Champion",
|
||||||
|
# Hero Equipment
|
||||||
|
"Spear of the General", "Lash of the Tyrant", "Bow of the Hunter",
|
||||||
|
"Cloak of the Philosopher", "Axe of the Warmonger"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Constants for setup and CSV processing
|
# Constants for setup and CSV processing
|
||||||
|
|
@ -60,7 +68,6 @@ CARD_TYPES_TO_EXCLUDE: List[str] = [
|
||||||
'Phenomenon',
|
'Phenomenon',
|
||||||
'Stickers',
|
'Stickers',
|
||||||
'Attraction',
|
'Attraction',
|
||||||
'Hero',
|
|
||||||
'Contraption'
|
'Contraption'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ from __future__ import annotations
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, List, Optional
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -65,6 +66,25 @@ def _headless_list_owned_files() -> List[str]:
|
||||||
return []
|
return []
|
||||||
return sorted(entries)
|
return sorted(entries)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RandomRunConfig:
|
||||||
|
"""Runtime options for the headless random build flow."""
|
||||||
|
|
||||||
|
legacy_theme: Optional[str] = None
|
||||||
|
primary_theme: Optional[str] = None
|
||||||
|
secondary_theme: Optional[str] = None
|
||||||
|
tertiary_theme: Optional[str] = None
|
||||||
|
auto_fill_missing: bool = False
|
||||||
|
auto_fill_secondary: Optional[bool] = None
|
||||||
|
auto_fill_tertiary: Optional[bool] = None
|
||||||
|
strict_theme_match: bool = False
|
||||||
|
attempts: int = 5
|
||||||
|
timeout_ms: int = 5000
|
||||||
|
seed: Optional[int | str] = None
|
||||||
|
constraints: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
output_json: Optional[str] = None
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
command_name: str = "",
|
command_name: str = "",
|
||||||
add_creatures: bool = True,
|
add_creatures: bool = True,
|
||||||
|
|
@ -446,6 +466,574 @@ def _load_json_config(path: Optional[str]) -> Dict[str, Any]:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _load_constraints_spec(spec: Any) -> Dict[str, Any]:
|
||||||
|
"""Load random constraints from a dict, JSON string, or file path."""
|
||||||
|
|
||||||
|
if not spec:
|
||||||
|
return {}
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
return dict(spec)
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = str(spec).strip()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Treat existing file paths as JSON documents
|
||||||
|
if os.path.isfile(text):
|
||||||
|
try:
|
||||||
|
with open(text, "r", encoding="utf-8") as fh:
|
||||||
|
loaded = json.load(fh)
|
||||||
|
if isinstance(loaded, dict):
|
||||||
|
return loaded
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Warning: failed to load constraints from '{text}': {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Fallback: parse inline JSON
|
||||||
|
try:
|
||||||
|
parsed = json.loads(text)
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
return parsed
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Warning: failed to parse inline constraints '{text}': {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _try_convert_seed(value: Any) -> Optional[int | str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
text = str(value).strip()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(text)
|
||||||
|
except ValueError:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_pathish_target(target: str, seed: Any) -> str:
|
||||||
|
"""Return a concrete file path for an output target, creating directories as needed."""
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
raise ValueError("Empty output path provided")
|
||||||
|
|
||||||
|
normalized = target.strip()
|
||||||
|
if not normalized:
|
||||||
|
raise ValueError("Blank output path provided")
|
||||||
|
|
||||||
|
looks_dir = normalized.endswith(("/", "\\"))
|
||||||
|
if os.path.isdir(normalized) or looks_dir:
|
||||||
|
base_dir = normalized.rstrip("/\\") or "."
|
||||||
|
os.makedirs(base_dir, exist_ok=True)
|
||||||
|
seed_suffix = str(seed) if seed is not None else "latest"
|
||||||
|
filename = f"random_build_{seed_suffix}.json"
|
||||||
|
return os.path.join(base_dir, filename)
|
||||||
|
|
||||||
|
base_dir = os.path.dirname(normalized)
|
||||||
|
if base_dir:
|
||||||
|
os.makedirs(base_dir, exist_ok=True)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_random_bool(
|
||||||
|
cli_value: Optional[bool],
|
||||||
|
env_name: str,
|
||||||
|
random_section: Dict[str, Any],
|
||||||
|
json_key: str,
|
||||||
|
default: Optional[bool],
|
||||||
|
) -> Optional[bool]:
|
||||||
|
if cli_value is not None:
|
||||||
|
return bool(cli_value)
|
||||||
|
env_val = os.getenv(env_name)
|
||||||
|
result = _parse_bool(env_val) if env_val is not None else None
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
if json_key in random_section:
|
||||||
|
result = _parse_bool(random_section.get(json_key))
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_random_str(
|
||||||
|
cli_value: Optional[str],
|
||||||
|
env_name: str,
|
||||||
|
random_section: Dict[str, Any],
|
||||||
|
json_key: str,
|
||||||
|
default: Optional[str] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
candidates: Tuple[Any, ...] = (
|
||||||
|
cli_value,
|
||||||
|
os.getenv(env_name),
|
||||||
|
random_section.get(json_key),
|
||||||
|
default,
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text = str(candidate).strip()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_random_int(
|
||||||
|
cli_value: Optional[int],
|
||||||
|
env_name: str,
|
||||||
|
random_section: Dict[str, Any],
|
||||||
|
json_key: str,
|
||||||
|
default: int,
|
||||||
|
) -> int:
|
||||||
|
if cli_value is not None:
|
||||||
|
try:
|
||||||
|
return int(cli_value)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
env_val = os.getenv(env_name)
|
||||||
|
if env_val is not None and str(env_val).strip() != "":
|
||||||
|
try:
|
||||||
|
return int(float(str(env_val).strip()))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if json_key in random_section:
|
||||||
|
value = random_section.get(json_key)
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
if value:
|
||||||
|
return int(float(value))
|
||||||
|
elif value is not None:
|
||||||
|
return int(value)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_random_seed(cli_value: Optional[str], random_section: Dict[str, Any]) -> Optional[int | str]:
|
||||||
|
seed = _try_convert_seed(cli_value)
|
||||||
|
if seed is not None:
|
||||||
|
return seed
|
||||||
|
seed = _try_convert_seed(os.getenv("RANDOM_SEED"))
|
||||||
|
if seed is not None:
|
||||||
|
return seed
|
||||||
|
return _try_convert_seed(random_section.get("seed"))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_random_section(json_cfg: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
section = json_cfg.get("random")
|
||||||
|
if isinstance(section, dict):
|
||||||
|
return dict(section)
|
||||||
|
alt = json_cfg.get("random_config")
|
||||||
|
if isinstance(alt, dict):
|
||||||
|
return dict(alt)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _should_run_random_mode(args: argparse.Namespace, json_cfg: Dict[str, Any], random_section: Dict[str, Any]) -> bool:
|
||||||
|
if getattr(args, "random_mode", False):
|
||||||
|
return True
|
||||||
|
if _parse_bool(os.getenv("HEADLESS_RANDOM_MODE")):
|
||||||
|
return True
|
||||||
|
if (os.getenv("DECK_MODE") or "").strip().lower() == "random":
|
||||||
|
return True
|
||||||
|
if _parse_bool(json_cfg.get("random_mode")):
|
||||||
|
return True
|
||||||
|
if _parse_bool(random_section.get("enabled")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Detect CLI or env hints that imply random mode even without explicit flag
|
||||||
|
cli_indicators = (
|
||||||
|
getattr(args, "random_theme", None),
|
||||||
|
getattr(args, "random_primary_theme", None),
|
||||||
|
getattr(args, "random_secondary_theme", None),
|
||||||
|
getattr(args, "random_tertiary_theme", None),
|
||||||
|
getattr(args, "random_seed", None),
|
||||||
|
getattr(args, "random_auto_fill", None),
|
||||||
|
getattr(args, "random_auto_fill_secondary", None),
|
||||||
|
getattr(args, "random_auto_fill_tertiary", None),
|
||||||
|
getattr(args, "random_strict_theme_match", None),
|
||||||
|
getattr(args, "random_attempts", None),
|
||||||
|
getattr(args, "random_timeout_ms", None),
|
||||||
|
getattr(args, "random_constraints", None),
|
||||||
|
getattr(args, "random_output_json", None),
|
||||||
|
)
|
||||||
|
if any(value is not None for value in cli_indicators):
|
||||||
|
return True
|
||||||
|
|
||||||
|
for env_name in (
|
||||||
|
"RANDOM_THEME",
|
||||||
|
"RANDOM_PRIMARY_THEME",
|
||||||
|
"RANDOM_SECONDARY_THEME",
|
||||||
|
"RANDOM_TERTIARY_THEME",
|
||||||
|
"RANDOM_CONSTRAINTS",
|
||||||
|
"RANDOM_CONSTRAINTS_PATH",
|
||||||
|
"RANDOM_OUTPUT_JSON",
|
||||||
|
):
|
||||||
|
if os.getenv(env_name):
|
||||||
|
return True
|
||||||
|
|
||||||
|
noteworthy_keys = (
|
||||||
|
"theme",
|
||||||
|
"primary_theme",
|
||||||
|
"secondary_theme",
|
||||||
|
"tertiary_theme",
|
||||||
|
"seed",
|
||||||
|
"auto_fill",
|
||||||
|
"auto_fill_secondary",
|
||||||
|
"auto_fill_tertiary",
|
||||||
|
"strict_theme_match",
|
||||||
|
"attempts",
|
||||||
|
"timeout_ms",
|
||||||
|
"constraints",
|
||||||
|
"constraints_path",
|
||||||
|
"output_json",
|
||||||
|
)
|
||||||
|
if any(random_section.get(key) for key in noteworthy_keys):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_random_config(args: argparse.Namespace, json_cfg: Dict[str, Any]) -> Tuple[RandomRunConfig, Dict[str, Any]]:
|
||||||
|
random_section = _extract_random_section(json_cfg)
|
||||||
|
cfg = RandomRunConfig()
|
||||||
|
|
||||||
|
cfg.legacy_theme = _resolve_random_str(
|
||||||
|
getattr(args, "random_theme", None),
|
||||||
|
"RANDOM_THEME",
|
||||||
|
random_section,
|
||||||
|
"theme",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
cfg.primary_theme = _resolve_random_str(
|
||||||
|
getattr(args, "random_primary_theme", None),
|
||||||
|
"RANDOM_PRIMARY_THEME",
|
||||||
|
random_section,
|
||||||
|
"primary_theme",
|
||||||
|
cfg.legacy_theme,
|
||||||
|
)
|
||||||
|
cfg.secondary_theme = _resolve_random_str(
|
||||||
|
getattr(args, "random_secondary_theme", None),
|
||||||
|
"RANDOM_SECONDARY_THEME",
|
||||||
|
random_section,
|
||||||
|
"secondary_theme",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
cfg.tertiary_theme = _resolve_random_str(
|
||||||
|
getattr(args, "random_tertiary_theme", None),
|
||||||
|
"RANDOM_TERTIARY_THEME",
|
||||||
|
random_section,
|
||||||
|
"tertiary_theme",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
auto_fill_flag = _resolve_random_bool(
|
||||||
|
getattr(args, "random_auto_fill", None),
|
||||||
|
"RANDOM_AUTO_FILL",
|
||||||
|
random_section,
|
||||||
|
"auto_fill",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
cfg.auto_fill_missing = bool(auto_fill_flag)
|
||||||
|
cfg.auto_fill_secondary = _resolve_random_bool(
|
||||||
|
getattr(args, "random_auto_fill_secondary", None),
|
||||||
|
"RANDOM_AUTO_FILL_SECONDARY",
|
||||||
|
random_section,
|
||||||
|
"auto_fill_secondary",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
cfg.auto_fill_tertiary = _resolve_random_bool(
|
||||||
|
getattr(args, "random_auto_fill_tertiary", None),
|
||||||
|
"RANDOM_AUTO_FILL_TERTIARY",
|
||||||
|
random_section,
|
||||||
|
"auto_fill_tertiary",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg.strict_theme_match = bool(
|
||||||
|
_resolve_random_bool(
|
||||||
|
getattr(args, "random_strict_theme_match", None),
|
||||||
|
"RANDOM_STRICT_THEME_MATCH",
|
||||||
|
random_section,
|
||||||
|
"strict_theme_match",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg.attempts = max(
|
||||||
|
1,
|
||||||
|
_resolve_random_int(
|
||||||
|
getattr(args, "random_attempts", None),
|
||||||
|
"RANDOM_MAX_ATTEMPTS",
|
||||||
|
random_section,
|
||||||
|
"attempts",
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
cfg.timeout_ms = max(
|
||||||
|
100,
|
||||||
|
_resolve_random_int(
|
||||||
|
getattr(args, "random_timeout_ms", None),
|
||||||
|
"RANDOM_TIMEOUT_MS",
|
||||||
|
random_section,
|
||||||
|
"timeout_ms",
|
||||||
|
5000,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg.seed = _resolve_random_seed(getattr(args, "random_seed", None), random_section)
|
||||||
|
|
||||||
|
# Resolve constraints in precedence order: CLI > env JSON > env path > config dict > config path
|
||||||
|
constraints_candidates: Tuple[Any, ...] = (
|
||||||
|
getattr(args, "random_constraints", None),
|
||||||
|
os.getenv("RANDOM_CONSTRAINTS"),
|
||||||
|
os.getenv("RANDOM_CONSTRAINTS_PATH"),
|
||||||
|
random_section.get("constraints"),
|
||||||
|
random_section.get("constraints_path"),
|
||||||
|
)
|
||||||
|
for candidate in constraints_candidates:
|
||||||
|
loaded = _load_constraints_spec(candidate)
|
||||||
|
if loaded:
|
||||||
|
cfg.constraints = loaded
|
||||||
|
break
|
||||||
|
|
||||||
|
cfg.output_json = _resolve_random_str(
|
||||||
|
getattr(args, "random_output_json", None),
|
||||||
|
"RANDOM_OUTPUT_JSON",
|
||||||
|
random_section,
|
||||||
|
"output_json",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if cfg.primary_theme is None:
|
||||||
|
cfg.primary_theme = cfg.legacy_theme
|
||||||
|
if cfg.primary_theme and not cfg.legacy_theme:
|
||||||
|
cfg.legacy_theme = cfg.primary_theme
|
||||||
|
|
||||||
|
if cfg.auto_fill_missing:
|
||||||
|
if cfg.auto_fill_secondary is None:
|
||||||
|
cfg.auto_fill_secondary = True
|
||||||
|
if cfg.auto_fill_tertiary is None:
|
||||||
|
cfg.auto_fill_tertiary = True
|
||||||
|
|
||||||
|
return cfg, random_section
|
||||||
|
|
||||||
|
|
||||||
|
def _print_random_summary(result: Any, config: RandomRunConfig) -> None:
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("RANDOM MODE BUILD")
|
||||||
|
print("=" * 60)
|
||||||
|
commander = getattr(result, "commander", None) or "(unknown)"
|
||||||
|
print(f"Commander : {commander}")
|
||||||
|
seed_value = getattr(result, "seed", config.seed)
|
||||||
|
print(f"Seed : {seed_value}")
|
||||||
|
|
||||||
|
display_themes = list(getattr(result, "display_themes", []) or [])
|
||||||
|
if not display_themes:
|
||||||
|
primary = getattr(result, "primary_theme", config.primary_theme)
|
||||||
|
if primary:
|
||||||
|
display_themes.append(primary)
|
||||||
|
for extra in (
|
||||||
|
getattr(result, "secondary_theme", config.secondary_theme),
|
||||||
|
getattr(result, "tertiary_theme", config.tertiary_theme),
|
||||||
|
):
|
||||||
|
if extra:
|
||||||
|
display_themes.append(extra)
|
||||||
|
if display_themes:
|
||||||
|
print(f"Themes : {', '.join(display_themes)}")
|
||||||
|
else:
|
||||||
|
print("Themes : (none)")
|
||||||
|
|
||||||
|
fallback_kinds: List[str] = []
|
||||||
|
if getattr(result, "combo_fallback", False):
|
||||||
|
fallback_kinds.append("combo")
|
||||||
|
if getattr(result, "synergy_fallback", False):
|
||||||
|
fallback_kinds.append("synergy")
|
||||||
|
fallback_reason = getattr(result, "fallback_reason", None)
|
||||||
|
print(f"Fallback : {('/'.join(fallback_kinds)) if fallback_kinds else 'none'}")
|
||||||
|
if fallback_reason:
|
||||||
|
print(f"Fallback reason : {fallback_reason}")
|
||||||
|
|
||||||
|
auto_secondary = getattr(result, "auto_fill_secondary_enabled", config.auto_fill_secondary or False)
|
||||||
|
auto_tertiary = getattr(result, "auto_fill_tertiary_enabled", config.auto_fill_tertiary or False)
|
||||||
|
print(
|
||||||
|
"Auto-fill : secondary={} | tertiary={}".format(
|
||||||
|
"on" if auto_secondary else "off",
|
||||||
|
"on" if auto_tertiary else "off",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(f"Strict match : {'on' if config.strict_theme_match else 'off'}")
|
||||||
|
|
||||||
|
attempts_used = getattr(result, "attempts_tried", None)
|
||||||
|
if attempts_used is None:
|
||||||
|
attempts_used = config.attempts
|
||||||
|
print(f"Attempts used : {attempts_used} / {config.attempts}")
|
||||||
|
timeout_hit = getattr(result, "timeout_hit", False)
|
||||||
|
print(f"Timeout (ms) : {config.timeout_ms} (timeout_hit={timeout_hit})")
|
||||||
|
|
||||||
|
if config.constraints:
|
||||||
|
try:
|
||||||
|
print("Constraints :")
|
||||||
|
print(json.dumps(config.constraints, indent=2))
|
||||||
|
except Exception:
|
||||||
|
print(f"Constraints : {config.constraints}")
|
||||||
|
|
||||||
|
csv_path = getattr(result, "csv_path", None)
|
||||||
|
if csv_path:
|
||||||
|
print(f"Deck CSV : {csv_path}")
|
||||||
|
txt_path = getattr(result, "txt_path", None)
|
||||||
|
if txt_path:
|
||||||
|
print(f"Deck TXT : {txt_path}")
|
||||||
|
compliance = getattr(result, "compliance", None)
|
||||||
|
if compliance:
|
||||||
|
if isinstance(compliance, dict) and compliance.get("path"):
|
||||||
|
print(f"Compliance JSON : {compliance['path']}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
print("Compliance data :")
|
||||||
|
print(json.dumps(compliance, indent=2))
|
||||||
|
except Exception:
|
||||||
|
print(f"Compliance data : {compliance}")
|
||||||
|
|
||||||
|
summary = getattr(result, "summary", None)
|
||||||
|
if summary:
|
||||||
|
try:
|
||||||
|
rendered = json.dumps(summary, indent=2)
|
||||||
|
except Exception:
|
||||||
|
rendered = str(summary)
|
||||||
|
preview = rendered[:1000]
|
||||||
|
print("Summary preview :")
|
||||||
|
print(preview + ("..." if len(rendered) > len(preview) else ""))
|
||||||
|
|
||||||
|
decklist = getattr(result, "decklist", None)
|
||||||
|
if decklist:
|
||||||
|
try:
|
||||||
|
print(f"Decklist cards : {len(decklist)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_random_payload(config: RandomRunConfig, result: Any) -> None:
|
||||||
|
if not config.output_json:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
path = _resolve_pathish_target(config.output_json, getattr(result, "seed", config.seed))
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Warning: unable to resolve random output path '{config.output_json}': {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
seed_value = getattr(result, "seed", config.seed)
|
||||||
|
try:
|
||||||
|
normalized_seed = int(seed_value) if seed_value is not None else None
|
||||||
|
except Exception:
|
||||||
|
normalized_seed = seed_value
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"seed": normalized_seed,
|
||||||
|
"commander": getattr(result, "commander", None),
|
||||||
|
"themes": {
|
||||||
|
"primary": getattr(result, "primary_theme", config.primary_theme),
|
||||||
|
"secondary": getattr(result, "secondary_theme", config.secondary_theme),
|
||||||
|
"tertiary": getattr(result, "tertiary_theme", config.tertiary_theme),
|
||||||
|
"resolved": list(getattr(result, "resolved_themes", []) or []),
|
||||||
|
"display": list(getattr(result, "display_themes", []) or []),
|
||||||
|
"auto_filled": list(getattr(result, "auto_filled_themes", []) or []),
|
||||||
|
},
|
||||||
|
"strict_theme_match": bool(config.strict_theme_match),
|
||||||
|
"auto_fill": {
|
||||||
|
"missing": bool(config.auto_fill_missing),
|
||||||
|
"secondary": bool(getattr(result, "auto_fill_secondary_enabled", config.auto_fill_secondary or False)),
|
||||||
|
"tertiary": bool(getattr(result, "auto_fill_tertiary_enabled", config.auto_fill_tertiary or False)),
|
||||||
|
"applied": bool(getattr(result, "auto_fill_applied", False)),
|
||||||
|
},
|
||||||
|
"attempts": {
|
||||||
|
"configured": config.attempts,
|
||||||
|
"used": int(getattr(result, "attempts_tried", config.attempts) or config.attempts),
|
||||||
|
"timeout_ms": config.timeout_ms,
|
||||||
|
"timeout_hit": bool(getattr(result, "timeout_hit", False)),
|
||||||
|
"retries_exhausted": bool(getattr(result, "retries_exhausted", False)),
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"combo": bool(getattr(result, "combo_fallback", False)),
|
||||||
|
"synergy": bool(getattr(result, "synergy_fallback", False)),
|
||||||
|
"reason": getattr(result, "fallback_reason", None),
|
||||||
|
},
|
||||||
|
"constraints": config.constraints,
|
||||||
|
"csv_path": getattr(result, "csv_path", None),
|
||||||
|
"txt_path": getattr(result, "txt_path", None),
|
||||||
|
"compliance": getattr(result, "compliance", None),
|
||||||
|
"summary": getattr(result, "summary", None),
|
||||||
|
"decklist": getattr(result, "decklist", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(payload, fh, indent=2)
|
||||||
|
print(f"Random build payload written to {path}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Warning: failed to write random payload '{path}': {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_random_mode(config: RandomRunConfig) -> int:
|
||||||
|
try:
|
||||||
|
from deck_builder.random_entrypoint import (
|
||||||
|
RandomConstraintsImpossibleError,
|
||||||
|
RandomThemeNoMatchError,
|
||||||
|
build_random_full_deck,
|
||||||
|
) # type: ignore
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Random mode unavailable: {exc}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
timeout_ms = max(100, int(config.timeout_ms))
|
||||||
|
attempts = max(1, int(config.attempts))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = build_random_full_deck(
|
||||||
|
theme=config.legacy_theme,
|
||||||
|
constraints=config.constraints or None,
|
||||||
|
seed=config.seed,
|
||||||
|
attempts=attempts,
|
||||||
|
timeout_s=float(timeout_ms) / 1000.0,
|
||||||
|
primary_theme=config.primary_theme,
|
||||||
|
secondary_theme=config.secondary_theme,
|
||||||
|
tertiary_theme=config.tertiary_theme,
|
||||||
|
auto_fill_missing=config.auto_fill_missing,
|
||||||
|
auto_fill_secondary=config.auto_fill_secondary,
|
||||||
|
auto_fill_tertiary=config.auto_fill_tertiary,
|
||||||
|
strict_theme_match=config.strict_theme_match,
|
||||||
|
)
|
||||||
|
except RandomThemeNoMatchError as exc:
|
||||||
|
print(f"Random mode failed: strict theme match produced no results ({exc})")
|
||||||
|
return 3
|
||||||
|
except RandomConstraintsImpossibleError as exc:
|
||||||
|
print(f"Random mode constraints impossible: {exc}")
|
||||||
|
return 4
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Random mode encountered an unexpected error: {exc}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
_print_random_summary(result, config)
|
||||||
|
_write_random_payload(config, result)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
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", metavar="PATH", default=os.getenv("DECK_CONFIG"),
|
p.add_argument("--config", metavar="PATH", default=os.getenv("DECK_CONFIG"),
|
||||||
|
|
@ -533,6 +1121,101 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
||||||
include_group.add_argument("--fuzzy-matching", metavar="BOOL", type=_parse_bool, default=None,
|
include_group.add_argument("--fuzzy-matching", metavar="BOOL", type=_parse_bool, default=None,
|
||||||
help="Enable fuzzy card name matching (bool: true/false/1/0)")
|
help="Enable fuzzy card name matching (bool: true/false/1/0)")
|
||||||
|
|
||||||
|
# Random mode configuration (parity with web random builder)
|
||||||
|
random_group = p.add_argument_group(
|
||||||
|
"Random Mode",
|
||||||
|
"Generate decks using the random web builder flow",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-mode",
|
||||||
|
action="store_true",
|
||||||
|
help="Force random-mode build even if other inputs are provided",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-theme",
|
||||||
|
metavar="THEME",
|
||||||
|
default=None,
|
||||||
|
help="Legacy random theme (maps to primary theme if unspecified)",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-primary-theme",
|
||||||
|
metavar="THEME",
|
||||||
|
default=None,
|
||||||
|
help="Primary theme slug for random mode",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-secondary-theme",
|
||||||
|
metavar="THEME",
|
||||||
|
default=None,
|
||||||
|
help="Secondary theme slug for random mode",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-tertiary-theme",
|
||||||
|
metavar="THEME",
|
||||||
|
default=None,
|
||||||
|
help="Tertiary theme slug for random mode",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-auto-fill",
|
||||||
|
metavar="BOOL",
|
||||||
|
type=_parse_bool,
|
||||||
|
default=None,
|
||||||
|
help="Enable auto-fill assistance for missing theme slots",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-auto-fill-secondary",
|
||||||
|
metavar="BOOL",
|
||||||
|
type=_parse_bool,
|
||||||
|
default=None,
|
||||||
|
help="Enable auto-fill specifically for secondary theme",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-auto-fill-tertiary",
|
||||||
|
metavar="BOOL",
|
||||||
|
type=_parse_bool,
|
||||||
|
default=None,
|
||||||
|
help="Enable auto-fill specifically for tertiary theme",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-strict-theme-match",
|
||||||
|
metavar="BOOL",
|
||||||
|
type=_parse_bool,
|
||||||
|
default=None,
|
||||||
|
help="Require strict theme matches when selecting commanders",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-attempts",
|
||||||
|
metavar="INT",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Maximum attempts before giving up (default 5)",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-timeout-ms",
|
||||||
|
metavar="INT",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Timeout in milliseconds for theme search (default 5000)",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-seed",
|
||||||
|
metavar="SEED",
|
||||||
|
default=None,
|
||||||
|
help="Seed value for deterministic random builds",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-constraints",
|
||||||
|
metavar="JSON_OR_PATH",
|
||||||
|
default=None,
|
||||||
|
help="Random constraints as JSON or a path to a JSON file",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-output-json",
|
||||||
|
metavar="PATH",
|
||||||
|
default=None,
|
||||||
|
help="Write random build payload JSON to PATH (directory or file)",
|
||||||
|
)
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
p.add_argument("--dry-run", action="store_true",
|
p.add_argument("--dry-run", action="store_true",
|
||||||
help="Print resolved configuration and exit without building")
|
help="Print resolved configuration and exit without building")
|
||||||
|
|
@ -584,6 +1267,13 @@ def _main() -> int:
|
||||||
os.environ["DECK_CONFIG"] = chosen
|
os.environ["DECK_CONFIG"] = chosen
|
||||||
break
|
break
|
||||||
|
|
||||||
|
random_config, random_section = _resolve_random_config(args, json_cfg)
|
||||||
|
if _should_run_random_mode(args, json_cfg, random_section):
|
||||||
|
if args.dry_run:
|
||||||
|
print(json.dumps({"random_mode": True, "config": asdict(random_config)}, indent=2))
|
||||||
|
return 0
|
||||||
|
return _run_random_mode(random_config)
|
||||||
|
|
||||||
# Defaults mirror run() signature
|
# Defaults mirror run() signature
|
||||||
defaults = dict(
|
defaults = dict(
|
||||||
command_name="",
|
command_name="",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -51,6 +51,18 @@ services:
|
||||||
RANDOM_UI: "1" # 1=show Surprise/Theme/Reroll/Share controls in UI
|
RANDOM_UI: "1" # 1=show Surprise/Theme/Reroll/Share controls in UI
|
||||||
RANDOM_MAX_ATTEMPTS: "5" # cap retry attempts
|
RANDOM_MAX_ATTEMPTS: "5" # cap retry attempts
|
||||||
RANDOM_TIMEOUT_MS: "5000" # per-build timeout in ms
|
RANDOM_TIMEOUT_MS: "5000" # per-build timeout in ms
|
||||||
|
RANDOM_THEME: "" # optional legacy theme alias (maps to primary theme)
|
||||||
|
RANDOM_PRIMARY_THEME: "" # optional primary theme slug override
|
||||||
|
RANDOM_SECONDARY_THEME: "" # optional secondary theme slug override
|
||||||
|
RANDOM_TERTIARY_THEME: "" # optional tertiary theme slug override
|
||||||
|
RANDOM_AUTO_FILL: "0" # when 1, auto-fill missing secondary/tertiary themes
|
||||||
|
RANDOM_AUTO_FILL_SECONDARY: "" # override secondary auto-fill (blank inherits from RANDOM_AUTO_FILL)
|
||||||
|
RANDOM_AUTO_FILL_TERTIARY: "" # override tertiary auto-fill (blank inherits from RANDOM_AUTO_FILL)
|
||||||
|
RANDOM_STRICT_THEME_MATCH: "0" # require strict theme matches for commanders when 1
|
||||||
|
RANDOM_CONSTRAINTS: "" # inline JSON constraints for random builds (optional)
|
||||||
|
RANDOM_CONSTRAINTS_PATH: "" # path to JSON constraints file (takes precedence)
|
||||||
|
RANDOM_SEED: "" # deterministic random seed (int or string)
|
||||||
|
RANDOM_OUTPUT_JSON: "" # path or directory for random build output payload
|
||||||
# RANDOM_BUILD_SUPPRESS_INITIAL_EXPORT: "1" # (now defaults to 1 automatically for random builds; set to 0 to force legacy double-export behavior)
|
# RANDOM_BUILD_SUPPRESS_INITIAL_EXPORT: "1" # (now defaults to 1 automatically for random builds; set to 0 to force legacy double-export behavior)
|
||||||
|
|
||||||
# Theming
|
# Theming
|
||||||
|
|
@ -80,7 +92,7 @@ services:
|
||||||
# WEB_THEME_FILTER_PREWARM: "0"
|
# WEB_THEME_FILTER_PREWARM: "0"
|
||||||
WEB_AUTO_ENFORCE: "0" # 1=auto-run compliance export after builds
|
WEB_AUTO_ENFORCE: "0" # 1=auto-run compliance export after builds
|
||||||
WEB_CUSTOM_EXPORT_BASE: "" # Optional: custom base dir for deck export artifacts
|
WEB_CUSTOM_EXPORT_BASE: "" # Optional: custom base dir for deck export artifacts
|
||||||
APP_VERSION: "dev" # Displayed version label (set per release/tag)
|
APP_VERSION: "2.3.1" # Displayed version label (set per release/tag)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Misc / Land Selection (Step 7) Environment Tuning
|
# Misc / Land Selection (Step 7) Environment Tuning
|
||||||
|
|
@ -144,6 +156,7 @@ services:
|
||||||
# DECK_TRIPLE_COUNT: ""
|
# DECK_TRIPLE_COUNT: ""
|
||||||
# DECK_UTILITY_COUNT: ""
|
# DECK_UTILITY_COUNT: ""
|
||||||
# DECK_TAG_MODE: "AND" # AND|OR (if supported)
|
# DECK_TAG_MODE: "AND" # AND|OR (if supported)
|
||||||
|
# HEADLESS_RANDOM_MODE: "0" # 1=force headless random mode instead of scripted build
|
||||||
|
|
||||||
# Entrypoint knobs (only if you change the entrypoint behavior)
|
# Entrypoint knobs (only if you change the entrypoint behavior)
|
||||||
# APP_MODE: "web" # web|cli — selects uvicorn vs CLI
|
# APP_MODE: "web" # web|cli — selects uvicorn vs CLI
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,18 @@ services:
|
||||||
RANDOM_UI: "0" # 1=UI Surprise/Reroll controls
|
RANDOM_UI: "0" # 1=UI Surprise/Reroll controls
|
||||||
RANDOM_MAX_ATTEMPTS: "5" # Retry cap for constrained random builds
|
RANDOM_MAX_ATTEMPTS: "5" # Retry cap for constrained random builds
|
||||||
RANDOM_TIMEOUT_MS: "5000" # Per-attempt timeout (ms)
|
RANDOM_TIMEOUT_MS: "5000" # Per-attempt timeout (ms)
|
||||||
|
RANDOM_THEME: "" # optional legacy theme alias (maps to primary theme)
|
||||||
|
RANDOM_PRIMARY_THEME: "" # optional primary theme slug override
|
||||||
|
RANDOM_SECONDARY_THEME: "" # optional secondary theme slug override
|
||||||
|
RANDOM_TERTIARY_THEME: "" # optional tertiary theme slug override
|
||||||
|
RANDOM_AUTO_FILL: "0" # when 1, auto-fill missing secondary/tertiary themes
|
||||||
|
RANDOM_AUTO_FILL_SECONDARY: "" # override secondary auto-fill (blank inherits from RANDOM_AUTO_FILL)
|
||||||
|
RANDOM_AUTO_FILL_TERTIARY: "" # override tertiary auto-fill (blank inherits from RANDOM_AUTO_FILL)
|
||||||
|
RANDOM_STRICT_THEME_MATCH: "0" # require strict theme matches when 1
|
||||||
|
RANDOM_CONSTRAINTS: "" # inline JSON constraints for random builds (optional)
|
||||||
|
RANDOM_CONSTRAINTS_PATH: "" # path to JSON constraints file (takes precedence)
|
||||||
|
RANDOM_SEED: "" # deterministic random seed (int or string)
|
||||||
|
RANDOM_OUTPUT_JSON: "" # path or directory for random build payload export
|
||||||
|
|
||||||
# Theming
|
# Theming
|
||||||
THEME: "system" # system|light|dark default theme
|
THEME: "system" # system|light|dark default theme
|
||||||
|
|
@ -61,7 +73,7 @@ services:
|
||||||
# WEB_THEME_FILTER_PREWARM: "0"
|
# WEB_THEME_FILTER_PREWARM: "0"
|
||||||
WEB_AUTO_ENFORCE: "0" # 1=auto compliance JSON export after builds
|
WEB_AUTO_ENFORCE: "0" # 1=auto compliance JSON export after builds
|
||||||
WEB_CUSTOM_EXPORT_BASE: "" # Optional export base override
|
WEB_CUSTOM_EXPORT_BASE: "" # Optional export base override
|
||||||
APP_VERSION: "v2.2.10" # Displayed in footer/health
|
APP_VERSION: "v2.3.1" # Displayed in footer/health
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Misc Land Selection Tuning (Step 7)
|
# Misc Land Selection Tuning (Step 7)
|
||||||
|
|
@ -112,6 +124,7 @@ services:
|
||||||
# DECK_TRIPLE_COUNT: ""
|
# DECK_TRIPLE_COUNT: ""
|
||||||
# DECK_UTILITY_COUNT: ""
|
# DECK_UTILITY_COUNT: ""
|
||||||
# DECK_TAG_MODE: "AND"
|
# DECK_TAG_MODE: "AND"
|
||||||
|
# HEADLESS_RANDOM_MODE: "0" # 1=force headless random mode instead of scripted build
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Entrypoint / Server knobs
|
# Entrypoint / Server knobs
|
||||||
|
|
@ -158,7 +171,7 @@ services:
|
||||||
# THEME_PREVIEW_TTL_STEPS: "2,4,2,3,1" # step counts for band progression
|
# THEME_PREVIEW_TTL_STEPS: "2,4,2,3,1" # step counts for band progression
|
||||||
# Redis backend (optional)
|
# Redis backend (optional)
|
||||||
# THEME_PREVIEW_REDIS_URL: "redis://redis:6379/0"
|
# THEME_PREVIEW_REDIS_URL: "redis://redis:6379/0"
|
||||||
# THEME_PREVIEW_REDIS_DISABLE: "0" # 1=force disable redis even if URL is set
|
# THEME_PREVIEW_REDIS_DISABLE: "0" # 1=force disable redis even if URL is set
|
||||||
volumes:
|
volumes:
|
||||||
- ${PWD}/deck_files:/app/deck_files
|
- ${PWD}/deck_files:/app/deck_files
|
||||||
- ${PWD}/logs:/app/logs
|
- ${PWD}/logs:/app/logs
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mtg-deckbuilder"
|
name = "mtg-deckbuilder"
|
||||||
version = "2.3.0"
|
version = "2.3.1"
|
||||||
description = "A command-line tool for building and analyzing Magic: The Gathering decks"
|
description = "A command-line tool for building and analyzing Magic: The Gathering decks"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue