Files
commander-ai-lab/routes/precon.py
T
Commander AI Lab 7750b43494 fix: deck loading — wire startGameWithDecks, fetch .dck content, parse [sections]
Three fixes that prevented precon decks from loading into actual gameplay:

1. ui.js: window.startGame() now calls startGameWithDecks() when
   setupDeckChoices has entries (the 3-tab selector stores choices there).
   Previously it called initGame() which looked for old deck-sel-{i}
   dropdowns that no longer exist.

2. engine.js: loadPreconForPlayer() now fetches .dck file content from
   the backend (/api/lab/precons/deck?fileName=...) when precon.decklist
   is empty. Falls back to fetching precon-decks/{file} directly.
   PRECON_DATA entries only have metadata (name, commander, set) — the
   actual card list lives in the .dck files.

3. engine.js: parseDeckList() now handles .dck bracket-style section
   headers like [Commander], [Main], [metadata]. Previously only matched
   bare text like 'COMMANDER:' without brackets. Also skips [metadata]
   lines (e.g. 'Name=Elven Empire').

4. routes/precon.py: Added GET /api/lab/precons/deck?fileName=... endpoint
   to serve raw .dck file content for the LAN client.
2026-04-03 13:45:15 +00:00

131 lines
4.3 KiB
Python

"""
routes/precon.py
================
Precon deck management endpoints:
GET /api/lab/precons
POST /api/lab/precons/install
POST /api/lab/precons/install-batch
POST /api/lab/precons/refresh
"""
from __future__ import annotations
import os
import shutil
from pathlib import Path
from fastapi import APIRouter, HTTPException
from models.state import CFG
from services.precon_service import download_precon_database, PRECON_INDEX
from services.logging import log
from services.precon_service import _get_precon_dir
router = APIRouter(tags=["precon"])
@router.get("/api/lab/precons")
async def list_precons():
"""List all available precon decks."""
return {"precons": PRECON_INDEX} # reads current value every call
@router.get("/api/lab/precons/deck")
async def get_precon_deck(fileName: str):
"""Return the raw decklist content of a precon .dck file.
Query: ?fileName=Elven_Empire.dck
"""
if not fileName:
raise HTTPException(400, "fileName query parameter is required")
# Sanitize: only allow .dck files, no path traversal
if '..' in fileName or '/' in fileName or '\\' in fileName:
raise HTTPException(400, "Invalid fileName")
if not fileName.endswith('.dck'):
raise HTTPException(400, "Only .dck files are supported")
src = _get_precon_dir() / fileName
if not src.exists():
raise HTTPException(404, f"Precon not found: {fileName}")
try:
content = src.read_text(encoding='utf-8')
except OSError as e:
raise HTTPException(500, f"Failed to read deck file: {e}")
return {"fileName": fileName, "decklist": content}
@router.post("/api/lab/precons/install")
async def install_precon(req: dict):
"""Install a precon deck to the Forge decks directory.
Body: {"fileName": "Elven_Empire.dck"} or {"name": "Elven_Empire.dck"}
"""
file_name = req.get("fileName", "") or req.get("name", "")
if not file_name:
raise HTTPException(400, "fileName is required")
src = _get_precon_dir() / file_name
if not src.exists():
raise HTTPException(404, f"Precon not found: {file_name}")
decks_dir = CFG.forge_decks_dir
if not decks_dir or not os.path.isdir(decks_dir):
raise HTTPException(500, f"Forge decks directory not found: {decks_dir}")
dst = Path(decks_dir) / file_name
try:
shutil.copy2(str(src), str(dst))
except OSError as e:
log.error(f"Failed to copy deck file {file_name}: {e}")
raise HTTPException(500, f"Failed to copy deck file: {e}")
deck_name = file_name.replace(".dck", "")
return {
"installed": True,
"deckName": deck_name,
"destination": str(dst),
"message": f"Installed {deck_name} to Forge decks",
}
@router.post("/api/lab/precons/install-batch")
async def install_precons_batch(req: dict):
"""Install multiple precon decks at once.
Body: {"fileNames": ["Elven_Empire.dck", ...]} or {"names": ["Elven_Empire.dck", ...]}
"""
file_names = req.get("fileNames", []) or req.get("names", [])
if not file_names:
raise HTTPException(400, "fileNames list is required")
decks_dir = CFG.forge_decks_dir
if not decks_dir or not os.path.isdir(decks_dir):
raise HTTPException(500, f"Forge decks directory not found: {decks_dir}")
results = []
for file_name in file_names:
src = _get_precon_dir() / file_name
if not src.exists():
results.append({"fileName": file_name, "installed": False, "error": "not found"})
continue
dst = Path(decks_dir) / file_name
try:
shutil.copy2(str(src), str(dst))
deck_name = file_name.replace(".dck", "")
results.append({"fileName": file_name, "installed": True, "deckName": deck_name})
except OSError as e:
log.error(f"Failed to copy deck file {file_name}: {e}")
results.append({"fileName": file_name, "installed": False, "error": str(e)})
return {"results": results}
@router.post("/api/lab/precons/refresh")
async def refresh_precons():
"""Force re-download of all Commander precon decks from GitHub."""
result = download_precon_database(force=True)
if result.get("error"):
raise HTTPException(502, result["error"])
return {
"message": f"Downloaded {result['downloaded']} Commander precon decks",
"total": result.get("total", 0),
}