Issue #1 — Winner's life total now captured: - Parse verbose Forge log for life-total-change events - Track last-known life per seat via 3 regex patterns - Winner's final life set from log data (not left at default 40) - Losers who died to mill/poison retain their actual life total Issue #2 — Win condition classification formalized: - New WinCondition enum (schema/WinCondition.java) with 8 categories - Centralized Forge loss-reason-string → enum mapping (regex-based) - UNKNOWN fallback with logging for unrecognized loss reasons - All classification now goes through WinCondition.classify() Issue #3 — JVM startup overhead reduced: - Added -XX:+UseSerialGC (lighter GC for batch processes) - Added -XX:TieredStopAtLevel=1 (faster JIT, less compilation) - ForgeWorker stub class for future persistent batch mode - Estimated reduction: ~2-3s → ~1-1.5s per game startup Issue #4 — Single-threaded throughput optimization: - New CLI flags: --ai-simplified, --ai-think-time, --benchmark - JVM flags passed through to Forge subprocess - Benchmark mode: run 1 game before batch, report wall/Forge/overhead times - API support: aiSimplified, aiThinkTimeMs in StartRequest Issue #5 — Threading, progress, and backpressure: - --threads 0 or -1 auto-detects optimal count from CPU topology - detectOptimalThreads() estimates physical cores (excludes HT) - ProgressCallback interface for real-time sims/sec reporting - Per-game progress emitted as [PROGRESS] lines, parsed by lab_api.py - --max-queue flag for graceful backpressure (limits concurrent JVMs) - Final throughput summary printed at batch completion - StatusResponse now includes simsPerSec field Also: .gitignore restored, lab_api.py updated with all new API fields
196 lines
7.3 KiB
Java
196 lines
7.3 KiB
Java
package commanderailab.bridge;
|
|
|
|
import commanderailab.schema.BatchResult;
|
|
import commanderailab.schema.BatchResult.*;
|
|
import commanderailab.schema.JsonExporter;
|
|
import commanderailab.stats.StatsAggregator;
|
|
import commanderailab.ai.AiPolicy;
|
|
import commanderailab.ai.ForgeBuiltinPolicy;
|
|
import commanderailab.batch.BatchRunner;
|
|
import commanderailab.batch.MultiThreadBatchRunner;
|
|
|
|
import com.google.gson.Gson;
|
|
import com.google.gson.GsonBuilder;
|
|
|
|
import java.time.Instant;
|
|
import java.util.*;
|
|
|
|
/**
|
|
* WebApiBridge — HTTP/WebSocket API layer for the web UI.
|
|
*
|
|
* This class provides methods that can be called from a web server
|
|
* (e.g., Javalin, Spring, or the existing Python FastAPI server)
|
|
* to trigger batch runs and retrieve results.
|
|
*/
|
|
public class WebApiBridge {
|
|
|
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
|
|
private final String forgeJarPath;
|
|
private final String forgeWorkDir;
|
|
private volatile BatchResult lastResult;
|
|
private volatile boolean isRunning;
|
|
private volatile int progress;
|
|
private volatile double currentSimsPerSec;
|
|
private String policyServerUrl = "http://localhost:8080";
|
|
|
|
// Issue #4/#5: Performance settings
|
|
private boolean aiSimplified = false;
|
|
private int aiThinkTimeMs = -1;
|
|
private int maxQueueDepth = -1;
|
|
|
|
public WebApiBridge(String forgeJarPath, String forgeWorkDir) {
|
|
this.forgeJarPath = forgeJarPath;
|
|
this.forgeWorkDir = forgeWorkDir;
|
|
this.isRunning = false;
|
|
this.progress = 0;
|
|
this.currentSimsPerSec = 0.0;
|
|
}
|
|
|
|
/**
|
|
* Configure AI optimization settings (Issue #4).
|
|
*/
|
|
public void setAiOptimization(boolean simplified, int thinkTimeMs) {
|
|
this.aiSimplified = simplified;
|
|
this.aiThinkTimeMs = thinkTimeMs;
|
|
}
|
|
|
|
/**
|
|
* Set max concurrent Forge processes for backpressure (Issue #5).
|
|
*/
|
|
public void setMaxQueueDepth(int maxDepth) {
|
|
this.maxQueueDepth = maxDepth;
|
|
}
|
|
|
|
/**
|
|
* Start a batch run (backward-compatible, Forge AI only).
|
|
*/
|
|
public String startBatchAsync(String deck1, String deck2, String deck3,
|
|
int numGames, int threads, Long seed) {
|
|
return startBatchAsync(deck1, deck2, deck3, numGames, threads, seed, false, "midrange");
|
|
}
|
|
|
|
/**
|
|
* Start a batch with learned policy support.
|
|
*
|
|
* @param useLearnedPolicy If true, attempts to use the trained neural network policy
|
|
* @param policyStyle Playstyle for the learned policy ("aggro", "control", "midrange", "combo")
|
|
*/
|
|
public String startBatchAsync(String deck1, String deck2, String deck3,
|
|
int numGames, int threads, Long seed,
|
|
boolean useLearnedPolicy, String policyStyle) {
|
|
String batchId = UUID.randomUUID().toString();
|
|
|
|
Thread batchThread = new Thread(() -> {
|
|
try {
|
|
isRunning = true;
|
|
progress = 0;
|
|
|
|
List<DeckInfo> decks = buildDecks(deck1, deck2, deck3);
|
|
AiPolicy policy;
|
|
|
|
if (useLearnedPolicy) {
|
|
commanderailab.ml.LearnedPolicy learned =
|
|
new commanderailab.ml.LearnedPolicy(
|
|
policyServerUrl,
|
|
policyStyle != null ? policyStyle : "midrange",
|
|
true
|
|
);
|
|
if (learned.connect()) {
|
|
policy = learned;
|
|
System.out.println("[ML] Using learned policy for batch " + batchId);
|
|
} else {
|
|
System.out.println("[ML] Learned policy unavailable, using Forge AI");
|
|
policy = new ForgeBuiltinPolicy();
|
|
}
|
|
} else {
|
|
policy = new ForgeBuiltinPolicy();
|
|
}
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
List<GameResult> games;
|
|
|
|
if (threads > 1) {
|
|
MultiThreadBatchRunner runner = new MultiThreadBatchRunner(
|
|
forgeJarPath, forgeWorkDir, decks, policy, threads);
|
|
runner.setAiOptimization(aiSimplified, aiThinkTimeMs);
|
|
if (maxQueueDepth > 0) runner.setMaxQueueDepth(maxQueueDepth);
|
|
// Issue #5: Progress callback for real-time sims/sec
|
|
runner.setProgressCallback((completed, total, pct, simsPerSec, lastResult) -> {
|
|
progress = pct;
|
|
currentSimsPerSec = simsPerSec;
|
|
});
|
|
games = runner.runBatch(numGames, seed);
|
|
} else {
|
|
BatchRunner runner = new BatchRunner(
|
|
forgeJarPath, forgeWorkDir, decks, policy);
|
|
runner.setAiOptimization(aiSimplified, aiThinkTimeMs);
|
|
// Issue #5: Progress callback
|
|
runner.setProgressCallback((completed, total, pct, simsPerSec, lastResult) -> {
|
|
progress = pct;
|
|
currentSimsPerSec = simsPerSec;
|
|
});
|
|
games = runner.runBatchSingleThread(numGames, seed);
|
|
}
|
|
|
|
long elapsed = System.currentTimeMillis() - startTime;
|
|
Summary summary = StatsAggregator.computeSummary(games, decks, elapsed);
|
|
|
|
BatchResult result = new BatchResult();
|
|
result.metadata = new Metadata();
|
|
result.metadata.batchId = batchId;
|
|
result.metadata.timestamp = Instant.now().toString();
|
|
result.metadata.totalGames = numGames;
|
|
result.metadata.completedGames = games.size();
|
|
result.metadata.engineVersion = "forge-2.0.12-SNAPSHOT";
|
|
result.metadata.masterSeed = seed;
|
|
result.metadata.threads = threads;
|
|
result.metadata.elapsedMs = elapsed;
|
|
result.decks = decks;
|
|
result.games = games;
|
|
result.summary = summary;
|
|
|
|
lastResult = result;
|
|
progress = 100;
|
|
|
|
} catch (Exception e) {
|
|
System.err.println("Batch run failed: " + e.getMessage());
|
|
e.printStackTrace();
|
|
} finally {
|
|
isRunning = false;
|
|
}
|
|
}, "batch-runner-" + batchId);
|
|
|
|
batchThread.setDaemon(true);
|
|
batchThread.start();
|
|
|
|
return batchId;
|
|
}
|
|
|
|
public int getProgress() { return progress; }
|
|
public boolean isRunning() { return isRunning; }
|
|
public double getCurrentSimsPerSec() { return currentSimsPerSec; }
|
|
|
|
public String getLastResultJson() {
|
|
if (lastResult == null) return "{}";
|
|
return JsonExporter.toJson(lastResult);
|
|
}
|
|
|
|
public BatchResult getLastResult() { return lastResult; }
|
|
|
|
private List<DeckInfo> buildDecks(String... deckNames) {
|
|
List<DeckInfo> decks = new ArrayList<>();
|
|
for (int i = 0; i < deckNames.length; i++) {
|
|
DeckInfo d = new DeckInfo();
|
|
d.seatIndex = i;
|
|
d.deckFile = deckNames[i];
|
|
d.deckName = deckNames[i].replace(".dck", "");
|
|
d.commanderName = d.deckName;
|
|
d.colorIdentity = List.of();
|
|
d.cardCount = 100;
|
|
decks.add(d);
|
|
}
|
|
return decks;
|
|
}
|
|
}
|