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:
1 parent
1fc99a5d4a
commit
d6863b6d7e
1 file changed
+139
-131
+139
-131
@@ -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},
|
||||||
|
}
|
||||||
Reference in new issue
Block a user