fix: auto-populate all CFG paths from environment variables

CFG.forge_dir, precon_dir, forge_decks_dir, forge_jar, and lab_jar
were only set via CLI args. When the server starts via uvicorn directly
(no CLI), all paths stayed empty and forge_card_loader could not find
the cardsfolder, causing 0 lands in every deck.

Fix: read FORGE_DIR, FORGE_JAR, PRECON_DIR, PRECON_DECKS_DIR,
FORGE_DECKS_DIR, and LAB_JAR from environment at import time, mirroring
the existing ANTHROPIC_API_KEY / XIMILAR_API_KEY pattern.
This commit is contained in:
KoalaTrapLord committed 2026-04-03 04:56:44 -04:00
1 parent 1fc99a5d4a
commit d6863b6d7e
1 file changed
+139 -131
+139 -131
View File
@@ -1,131 +1,139 @@
"""Runtime configuration, state classes, and static data for Commander AI Lab.""" """Runtime configuration, state classes, and static data for Commander AI Lab."""
from __future__ import annotations from __future__ import annotations
import json import json
import logging import logging
import os import os
import subprocess import subprocess
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
log = logging.getLogger("commander_ai_lab.api") log = logging.getLogger("commander_ai_lab.api")
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# Configuration # Configuration
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
class Config: class Config:
"""Runtime configuration -- set via CLI args or env vars.""" """Runtime configuration -- set via CLI args or env vars."""
forge_jar: str = "" forge_jar: str = ""
forge_dir: str = "" forge_dir: str = ""
forge_decks_dir: str = "" forge_decks_dir: str = ""
lab_jar: str = "" lab_jar: str = ""
precon_dir: str = "" precon_dir: str = ""
results_dir: str = "results" results_dir: str = "results"
port: int = 8080 port: int = 8080
ximilar_api_key: str = "" ximilar_api_key: str = ""
pplx_api_key: str = "" pplx_api_key: str = ""
anthropic_api_key: str = "" anthropic_api_key: str = ""
CFG = Config() CFG = Config()
# Pre-populate from env so ASGI mode (uvicorn lab_api:app) works without main()
CFG.anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "") # Pre-populate from environment so uvicorn/ASGI mode works without CLI args.
# CLI args (lab_api.py main()) will override these if provided.
CFG.anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
def validate_forge_config() -> None: CFG.ximilar_api_key = os.environ.get("XIMILAR_API_KEY", "")
"""Warn clearly at startup if FORGE_DIR is misconfigured.""" CFG.pplx_api_key = os.environ.get("PPLX_API_KEY", "")
if not CFG.forge_dir: CFG.forge_jar = os.environ.get("FORGE_JAR", "")
log.warning("FORGE_DIR not set -- Forge card enrichment disabled. " CFG.forge_dir = os.environ.get("FORGE_DIR", "")
"Set FORGE_DIR to your forge-repo root or forge-gui directory.") CFG.lab_jar = os.environ.get("LAB_JAR", "")
return # PRECON_DIR / PRECON_DECKS_DIR are aliases -- prefer PRECON_DIR
base = Path(CFG.forge_dir) CFG.precon_dir = os.environ.get("PRECON_DIR") or os.environ.get("PRECON_DECKS_DIR", "")
a = base / "forge-gui" / "res" / "cardsfolder" CFG.forge_decks_dir = os.environ.get("FORGE_DECKS_DIR", "")
b = base / "res" / "cardsfolder"
if not a.is_dir() and not b.is_dir():
log.warning( def validate_forge_config() -> None:
"FORGE_DIR=%s is set but cardsfolder not found at:\n" """Warn clearly at startup if FORGE_DIR is misconfigured."""
" %s\n %s\n" if not CFG.forge_dir:
"Check that FORGE_DIR points to forge-repo root or forge-gui/.", log.warning("FORGE_DIR not set -- Forge card enrichment disabled. "
CFG.forge_dir, a, b "Set FORGE_DIR to your forge-repo root or forge-gui directory.")
) return
else: base = Path(CFG.forge_dir)
resolved = a if a.is_dir() else b a = base / "forge-gui" / "res" / "cardsfolder"
log.info("Forge cardsfolder confirmed at %s", resolved) b = base / "res" / "cardsfolder"
CFG.ximilar_api_key = os.environ.get("XIMILAR_API_KEY", "") if not a.is_dir() and not b.is_dir():
CFG.pplx_api_key = os.environ.get("PPLX_API_KEY", "") log.warning(
"FORGE_DIR=%s is set but cardsfolder not found at:\n"
" %s\n %s\n"
# ══════════════════════════════════════════════════════════════ "Check that FORGE_DIR points to forge-repo root or forge-gui/.",
# In-Memory State CFG.forge_dir, a, b
# ══════════════════════════════════════════════════════════════ )
class BatchState: else:
def __init__(self, batch_id: str, total_games: int, threads: int): resolved = a if a.is_dir() else b
self.batch_id = batch_id log.info("Forge cardsfolder confirmed at %s", resolved)
self.total_games = total_games
self.threads = threads
self.completed_games = 0 # ══════════════════════════════════════════════════════════════
self.running = True # In-Memory State
self.start_time = datetime.now() # ══════════════════════════════════════════════════════════════
self.elapsed_ms = 0 class BatchState:
self.result_path: Optional[str] = None def __init__(self, batch_id: str, total_games: int, threads: int):
self.error: Optional[str] = None self.batch_id = batch_id
self.process: Optional[subprocess.Popen] = None self.total_games = total_games
self.log_lines: list = [] self.threads = threads
self.sims_per_sec: float = 0.0 self.completed_games = 0
self.running = True
self.start_time = datetime.now()
import asyncio as _asyncio self.elapsed_ms = 0
active_batches: dict[str, BatchState] = {} self.result_path: Optional[str] = None
active_batches_lock = _asyncio.Lock() self.error: Optional[str] = None
self.process: Optional[subprocess.Popen] = None
self.log_lines: list = []
COMMANDER_META: dict = {} self.sims_per_sec: float = 0.0
BUILTIN_COMMANDERS: dict = {
"Edgar Markov": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["W","B","R"]}], import asyncio as _asyncio
"Atraxa, Praetors' Voice": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["W","U","B","G"]}], active_batches: dict[str, BatchState] = {}
"Korvold, Fae-Cursed King": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["B","R","G"]}], active_batches_lock = _asyncio.Lock()
"Muldrotha, the Gravetide": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["U","B","G"]}],
"The Ur-Dragon": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["W","U","B","R","G"]}],
"Yuriko, the Tiger's Shadow": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["U","B"]}], COMMANDER_META: dict = {}
"Krenko, Mob Boss": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["R"]}],
"Meren of Clan Nel Toth": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["B","G"]}], BUILTIN_COMMANDERS: dict = {
"Prossh, Skyraider of Kher": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["B","R","G"]}], "Edgar Markov": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["W","B","R"]}],
"Kaalia of the Vast": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["W","B","R"]}], "Atraxa, Praetors' Voice": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["W","U","B","G"]}],
"Talrand, Sky Summoner": [{"source": "edhrec", "archetype": "control", "colorIdentity": ["U"]}], "Korvold, Fae-Cursed King": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["B","R","G"]}],
"Omnath, Locus of Creation": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["W","U","R","G"]}], "Muldrotha, the Gravetide": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["U","B","G"]}],
"Teysa Karlov": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["W","B"]}], "The Ur-Dragon": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["W","U","B","R","G"]}],
"Lathril, Blade of the Elves": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["B","G"]}], "Yuriko, the Tiger's Shadow": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["U","B"]}],
"Breya, Etherium Shaper": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["W","U","B","R"]}], "Krenko, Mob Boss": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["R"]}],
} "Meren of Clan Nel Toth": [{"source": "edhrec", "archetype": "midrange", "colorIdentity": ["B","G"]}],
"Prossh, Skyraider of Kher": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["B","R","G"]}],
"Kaalia of the Vast": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["W","B","R"]}],
def load_commander_meta() -> None: "Talrand, Sky Summoner": [{"source": "edhrec", "archetype": "control", "colorIdentity": ["U"]}],
"""Load commander meta mapping from file or fall back to builtins.""" "Omnath, Locus of Creation": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["W","U","R","G"]}],
meta_path = Path(__file__).parent.parent / "commander-meta.json" "Teysa Karlov": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["W","B"]}],
if meta_path.exists(): "Lathril, Blade of the Elves": [{"source": "edhrec", "archetype": "aggro", "colorIdentity": ["B","G"]}],
try: "Breya, Etherium Shaper": [{"source": "edhrec", "archetype": "combo", "colorIdentity": ["W","U","B","R"]}],
with open(meta_path, "r", encoding="utf-8") as f: }
COMMANDER_META.clear()
COMMANDER_META.update(json.load(f))
log.info(f" Meta: Loaded {len(COMMANDER_META)} commanders from {meta_path}") def load_commander_meta() -> None:
return """Load commander meta mapping from file or fall back to builtins."""
except Exception as e: meta_path = Path(__file__).parent.parent / "commander-meta.json"
log.warning(f" WARNING: Failed to load commander-meta.json: {e}") if meta_path.exists():
COMMANDER_META.clear() try:
COMMANDER_META.update(BUILTIN_COMMANDERS) with open(meta_path, "r", encoding="utf-8") as f:
log.info(f" Meta: {len(COMMANDER_META)} built-in commanders") COMMANDER_META.clear()
COMMANDER_META.update(json.load(f))
log.info(f" Meta: Loaded {len(COMMANDER_META)} commanders from {meta_path}")
# ══════════════════════════════════════════════════════════════ return
# AI Profiles except Exception as e:
# ══════════════════════════════════════════════════════════════ log.warning(f" WARNING: Failed to load commander-meta.json: {e}")
AI_PROFILES = { COMMANDER_META.clear()
"default": {"name": "default", "description": "Balanced \u2014 Forge's default AI behavior", "aggression": 0.5, "cardAdvantage": 0.5, "removalPriority": 0.5, "boardPresence": 0.5, "comboPriority": 0.3, "patience": 0.5}, COMMANDER_META.update(BUILTIN_COMMANDERS)
"aggro": {"name": "aggro", "description": "Aggressive \u2014 attacks early, prioritizes damage", "aggression": 0.9, "cardAdvantage": 0.3, "removalPriority": 0.3, "boardPresence": 0.8, "comboPriority": 0.1, "patience": 0.1}, log.info(f" Meta: {len(COMMANDER_META)} built-in commanders")
"control": {"name": "control", "description": "Control \u2014 defensive, removal-heavy, card advantage", "aggression": 0.2, "cardAdvantage": 0.9, "removalPriority": 0.9, "boardPresence": 0.3, "comboPriority": 0.4, "patience": 0.9},
"combo": {"name": "combo", "description": "Combo \u2014 ramps, digs for pieces, assembles combos", "aggression": 0.2, "cardAdvantage": 0.8, "removalPriority": 0.4, "boardPresence": 0.3, "comboPriority": 0.95, "patience": 0.7},
"midrange": {"name": "midrange", "description": "Midrange \u2014 flexible, strong board presence, value-oriented", "aggression": 0.5, "cardAdvantage": 0.6, "removalPriority": 0.6, "boardPresence": 0.7, "comboPriority": 0.3, "patience": 0.5}, # ══════════════════════════════════════════════════════════════
} # AI Profiles
# ══════════════════════════════════════════════════════════════
AI_PROFILES = {
"default": {"name": "default", "description": "Balanced \u2014 Forge's default AI behavior", "aggression": 0.5, "cardAdvantage": 0.5, "removalPriority": 0.5, "boardPresence": 0.5, "comboPriority": 0.3, "patience": 0.5},
"aggro": {"name": "aggro", "description": "Aggressive \u2014 attacks early, prioritizes damage", "aggression": 0.9, "cardAdvantage": 0.3, "removalPriority": 0.3, "boardPresence": 0.8, "comboPriority": 0.1, "patience": 0.1},
"control": {"name": "control", "description": "Control \u2014 defensive, removal-heavy, card advantage", "aggression": 0.2, "cardAdvantage": 0.9, "removalPriority": 0.9, "boardPresence": 0.3, "comboPriority": 0.4, "patience": 0.9},
"combo": {"name": "combo", "description": "Combo \u2014 ramps, digs for pieces, assembles combos", "aggression": 0.2, "cardAdvantage": 0.8, "removalPriority": 0.4, "boardPresence": 0.3, "comboPriority": 0.95, "patience": 0.7},
"midrange": {"name": "midrange", "description": "Midrange \u2014 flexible, strong board presence, value-oriented", "aggression": 0.5, "cardAdvantage": 0.6, "removalPriority": 0.6, "boardPresence": 0.7, "comboPriority": 0.3, "patience": 0.5},
}