diff --git a/.env.example b/.env.example
index 156cbf7..947b890 100644
--- a/.env.example
+++ b/.env.example
@@ -23,4 +23,5 @@ FORGE_WORKING_DIR=D:/ForgeCommander/forge-repo/forge-gui
# PRECON_DECKS_DIR is kept below for backwards compat but is NOT used by the server.
PRECON_DIR=D:\ForgeCommander\commander-ai-lab\precon-decks
PRECON_DECKS_DIR=D:\ForgeCommander\commander-ai-lab\precon-decks
-FORGE_DECKS_DIR=D:\ForgeCommander\commander-ai-lab\precon-decks
+USER_DECKS_DIR=D:\ForgeCommander\commander-ai-lab\user-decks
+FORGE_DECKS_DIR=D:\ForgeCommander\commander-ai-lab\user-decks
diff --git a/.gitignore b/.gitignore
index 80ccef7..fc828ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,9 @@ embeddings/
checkpoints/
imported-decks/
+# User-created decks (generated at runtime, not committed)
+user-decks/*.dck
+
# OS
.DS_Store
Thumbs.db
diff --git a/coach/config.py b/coach/config.py
index 2485551..8729589 100644
--- a/coach/config.py
+++ b/coach/config.py
@@ -13,7 +13,7 @@ LAB_ROOT = Path(os.environ.get("COMMANDER_LAB_ROOT", Path(__file__).parent.paren
# ── Ollama (OpenAI-compatible API) ────────────────────────────
LLM_URL = os.environ.get("LLM_URL", "http://localhost:11434/v1")
-LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-oss:20b")
+LLM_MODEL = os.environ.get("LLM_MODEL", "claude-opus-4-20250514")
LLM_TIMEOUT = int(os.environ.get("LLM_TIMEOUT", "360")) # seconds
LLM_MAX_RETRIES = 3
diff --git a/lab_api.py b/lab_api.py
index 7ee13c4..e5b30cc 100644
--- a/lab_api.py
+++ b/lab_api.py
@@ -170,6 +170,7 @@ from pathlib import Path as _Path
_legacy_ui_dir = _Path(__file__).parent / "ui"
_lan_client_dir = _Path(__file__).parent / "lan-client"
_precon_decks_dir = _Path(__file__).parent / "precon-decks"
+_user_decks_dir = _Path(__file__).parent / "user-decks"
_spa_dir = _Path(__file__).parent / "frontend" / "commander-ai-lab-ui" / "dist"
# Mount LAN client at /lan (served before the root catch-all)
@@ -180,6 +181,10 @@ if _lan_client_dir.exists():
if _precon_decks_dir.exists():
app.mount("/precon-decks", StaticFiles(directory=str(_precon_decks_dir)), name="precon-decks")
+# Mount user-decks/ so exported .dck files are fetchable at /user-decks/{file}.dck
+if _user_decks_dir.exists():
+ app.mount("/user-decks", StaticFiles(directory=str(_user_decks_dir)), name="user-decks")
+
if _legacy_ui_dir.exists():
app.mount("/", StaticFiles(directory=str(_legacy_ui_dir), html=True), name="ui")
elif _spa_dir.exists():
diff --git a/lan-client/deckgenerator.html b/lan-client/deckgenerator.html
index ee89c05..8fcf180 100644
--- a/lan-client/deckgenerator.html
+++ b/lan-client/deckgenerator.html
@@ -146,11 +146,10 @@
-
+
+ claude-opus-4-20250514
@@ -200,7 +199,7 @@
-
Generating deck via Local AI...
+
Generating deck via Claude Opus 4...
-
-
+
+
diff --git a/lan-client/deckgenerator.js b/lan-client/deckgenerator.js
index 711cdf2..ab45b53 100644
--- a/lan-client/deckgenerator.js
+++ b/lan-client/deckgenerator.js
@@ -159,7 +159,7 @@ const DeckGenerator = (() => {
if (!state.v3Ready) {
// Warn but do not block — local/Ollama mode may still work
console.warn('V3 status: not initialized. Error:', data.error || 'unknown');
- toast('V3 generator not ready — check that the local AI model (gpt-oss:20b) is running via Ollama', 'warning');
+ toast('V3 generator not ready — check that the Anthropic API key is configured', 'warning');
}
} catch (e) {
console.warn('V3 status check failed:', e);
@@ -386,7 +386,7 @@ const DeckGenerator = (() => {
// ── Generate Deck ───────────────────────────────────────
async function generateDeck() {
clearLog();
- logMessage('Connecting to local AI model (gpt-oss:20b)...', 'info');
+ logMessage('Connecting to Claude AI...', 'info');
if (!state.commander) { toast('Select a commander first', 'error'); return; }
// Non-blocking warning only — local Ollama mode does not require v3Ready
if (!state.v3Ready) {
@@ -435,7 +435,7 @@ const DeckGenerator = (() => {
state.isLoading = loading;
$('dg-empty-state').style.display = 'none';
$('dg-loading').style.display = loading ? 'flex' : 'none';
- $('dg-results').style.display = loading ? 'none' : 'none';
+ $('dg-results').style.display = loading ? 'none' : 'block';
$('dg-preview-btn').disabled = loading || !state.commander;
if (text) $('dg-loading-text').textContent = text;
}
@@ -486,6 +486,7 @@ const DeckGenerator = (() => {
function renderStrategyBar(result) {
const bar = $('dg-strategy-bar');
+ if (!bar) return;
const bracket = result.bracket || {};
const archetype = result.archetype || '';
const strategy = result.strategy_summary || '';
@@ -504,6 +505,7 @@ const DeckGenerator = (() => {
function renderStatsBar(result) {
const stats = result.stats || {};
const container = $('dg-stats-bar');
+ if (!container) return;
const chips = [
chipHtml('Total', stats.total_cards || 0, ''),
@@ -539,6 +541,7 @@ const DeckGenerator = (() => {
function renderCardGroups(result) {
const container = $('dg-card-groups');
+ if (!container) return;
const cards = result.cards || [];
const commander = result.commander;
diff --git a/lan-client/engine.js b/lan-client/engine.js
index e35af64..c9d87b7 100644
--- a/lan-client/engine.js
+++ b/lan-client/engine.js
@@ -12556,32 +12556,6 @@ function renderDeckSelectionGrid() {
'AI Deck (auto-generated)
';
} else {
var idx = i;
- // Build saved decks options
- var savedDecks = (typeof getDecks === 'function') ? getDecks() : [];
- var savedDecksHtml = '';
- if (savedDecks.length === 0) {
- savedDecksHtml = 'No saved decks. Build one in Deck Builder first.
';
- } else {
- savedDecksHtml = '';
- for (var di = 0; di < savedDecks.length; di++) {
- var sd = savedDecks[di];
- var cardCount = 0;
- if (sd.cards) sd.cards.forEach(function(c) { cardCount += (c.quantity || 1); });
- if (sd.commander) cardCount++;
- var deckColors = '';
- if (sd.commander && sd.commander.colorIdentity && sd.commander.colorIdentity.length) {
- var manaSymbols = { W: '\u2600', U: '\uD83D\uDCA7', B: '\uD83D\uDC80', R: '\uD83D\uDD25', G: '\uD83C\uDF33' };
- sd.commander.colorIdentity.forEach(function(c) { deckColors += (manaSymbols[c] || c) + ' '; });
- }
- savedDecksHtml += '';
- }
- savedDecksHtml += '
';
- }
-
panel.innerHTML =
'
@@ -196,7 +195,7 @@
-
Generating deck via Local AI...
+
Generating deck via Claude Opus 4...
-
-
+
+
diff --git a/ui/deckgenerator.js b/ui/deckgenerator.js
index 3ce0dfa..71efc4c 100644
--- a/ui/deckgenerator.js
+++ b/ui/deckgenerator.js
@@ -159,7 +159,7 @@ const DeckGenerator = (() => {
if (!state.v3Ready) {
// Warn but do not block — local/Ollama mode may still work
console.warn('V3 status: not initialized. Error:', data.error || 'unknown');
- toast('V3 generator not ready — check that the local AI model (gpt-oss:20b) is running via Ollama', 'warning');
+ toast('V3 generator not ready — check that the Anthropic API key is configured', 'warning');
}
} catch (e) {
console.warn('V3 status check failed:', e);
@@ -386,7 +386,7 @@ const DeckGenerator = (() => {
// ── Generate Deck ───────────────────────────────────────
async function generateDeck() {
clearLog();
- logMessage('Connecting to local AI model (gpt-oss:20b)...', 'info');
+ logMessage('Connecting to Claude AI...', 'info');
if (!state.commander) { toast('Select a commander first', 'error'); return; }
// Non-blocking warning only — local Ollama mode does not require v3Ready
if (!state.v3Ready) {
@@ -435,7 +435,7 @@ const DeckGenerator = (() => {
state.isLoading = loading;
$('dg-empty-state').style.display = 'none';
$('dg-loading').style.display = loading ? 'flex' : 'none';
- $('dg-results').style.display = loading ? 'none' : 'none';
+ $('dg-results').style.display = loading ? 'none' : 'block';
$('dg-preview-btn').disabled = loading || !state.commander;
if (text) $('dg-loading-text').textContent = text;
}
@@ -454,6 +454,7 @@ const DeckGenerator = (() => {
function renderStrategyBar(result) {
const bar = $('dg-strategy-bar');
+ if (!bar) return;
const bracket = result.bracket || {};
const archetype = result.archetype || '';
const strategy = result.strategy_summary || '';
@@ -472,6 +473,7 @@ const DeckGenerator = (() => {
function renderStatsBar(result) {
const stats = result.stats || {};
const container = $('dg-stats-bar');
+ if (!container) return;
const chips = [
chipHtml('Total', stats.total_cards || 0, ''),
@@ -507,6 +509,7 @@ const DeckGenerator = (() => {
function renderCardGroups(result) {
const container = $('dg-card-groups');
+ if (!container) return;
const cards = result.cards || [];
const commander = result.commander;
diff --git a/ui/simulator.js b/ui/simulator.js
index 8cb5196..eba33cc 100644
--- a/ui/simulator.js
+++ b/ui/simulator.js
@@ -39,16 +39,41 @@
// ── Init: load decks ──
async function loadDecks() {
try {
- const res = await fetch('/api/decks');
+ const res = await fetch('/api/lab/decks');
if (!res.ok) return;
const data = await res.json();
- const decks = data.decks || data || [];
- decks.forEach(d => {
- const opt = document.createElement('option');
- opt.value = d.id;
- opt.textContent = d.name + (d.commander ? ' (' + d.commander + ')' : '');
- deckSelect.appendChild(opt);
- });
+ const decks = data.decks || [];
+
+ // Split into groups by source
+ const dbDecks = decks.filter(d => d.source === 'deckbuilder');
+ const forgeDecks = decks.filter(d => d.source === 'forge');
+
+ // My Decks — from Deck Builder (stored in SQLite)
+ if (dbDecks.length > 0) {
+ const group = document.createElement('optgroup');
+ group.label = '\u{1F0CF} My Decks (Deck Builder)';
+ dbDecks.forEach(d => {
+ const opt = document.createElement('option');
+ opt.value = 'db:' + d.deck_id;
+ opt.textContent = d.name + (d.commander ? ' (' + d.commander + ')' : '');
+ group.appendChild(opt);
+ });
+ deckSelect.appendChild(group);
+ }
+
+ // Forge .dck files (user-decks/, precon-decks/, sample-decks/)
+ if (forgeDecks.length > 0) {
+ const group = document.createElement('optgroup');
+ group.label = '\u{1F4C1} Deck Files (.dck)';
+ forgeDecks.forEach(d => {
+ const opt = document.createElement('option');
+ opt.value = d.name;
+ opt.textContent = d.name.replace(/_/g, ' ');
+ group.appendChild(opt);
+ });
+ deckSelect.appendChild(group);
+ }
+
} catch (e) {
console.warn('Could not load decks:', e);
}
@@ -165,18 +190,17 @@
// ── Run from deck ID ──
runBtn.addEventListener('click', async () => {
- const deckId = parseInt(deckSelect.value);
- if (!deckId) return;
+ const val = deckSelect.value;
+ if (!val) return;
- const numGames = parseInt(numGamesInput.value) || 10;
+ const numGames = parseInt(numGamesInput.value) || 10;
const recordLogs = recordLogsChk.checked;
const useDeepSeek = oppSelect.value === 'deepseek';
- // If DeepSeek selected but not connected, try to connect first
if (useDeepSeek && !dsConnected) {
await connectDeepSeek();
if (!dsConnected) {
- alert('Could not connect to DeepSeek. Make sure LM Studio is running at ' + dsApiBase.value);
+ alert('Could not connect to DeepSeek. Make sure Ollama is running at ' + dsApiBase.value);
return;
}
await applyDeepSeekConfig();
@@ -189,16 +213,35 @@
hideResults();
try {
- const endpoint = useDeepSeek ? '/api/sim/run-deepseek' : '/api/sim/run-from-deck';
+ let endpoint, body;
+
+ if (val.startsWith('db:')) {
+ // Deck Builder deck — auto-export to user-decks/ first
+ const deckId = parseInt(val.replace('db:', ''));
+
+ const exportRes = await fetch(`/api/decks/${deckId}/export-to-sim`, { method: 'POST' });
+ const exportData = await exportRes.json();
+ if (!exportData.success) throw new Error('Export failed: ' + (exportData.detail || 'unknown'));
+
+ endpoint = useDeepSeek ? '/api/sim/run-deepseek' : '/api/sim/run-from-deck';
+ body = { deckId, numGames, recordLogs };
+
+ } else {
+ // File-based deck (already a .dck)
+ endpoint = useDeepSeek ? '/api/sim/run-deepseek' : '/api/sim/run-from-deck';
+ body = { deckName: val, numGames, recordLogs };
+ }
+
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ deckId, numGames, recordLogs }),
+ body: JSON.stringify(body),
});
const data = await res.json();
if (data.error) throw new Error(data.error);
currentSimId = data.simId;
startPolling();
+
} catch (e) {
alert('Error: ' + e.message);
resetRunBtn();
diff --git a/user-decks/.gitkeep b/user-decks/.gitkeep
new file mode 100644
index 0000000..e69de29