Initial commit

This commit is contained in:
KoalaTrapLord committed 2026-03-10 12:13:56 -04:00
commit be55a02860
402 files changed
+64931

No files matched your search

@@ -0,0 +1,268 @@
package commanderailab;
import commanderailab.ai.*;
import commanderailab.schema.*;
import commanderailab.schema.BatchResult.*;
import commanderailab.stats.StatsAggregator;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Commander AI Lab core components.
* These tests validate data structures, stats computation, and JSON serialization
* without requiring the Forge engine to be installed.
*/
class CommanderAiLabTest {
// ── AI Policy Tests ──────────────────────────────────────
@Test
void testForgeBuiltinPolicyDefaults() {
ForgeBuiltinPolicy policy = new ForgeBuiltinPolicy();
assertEquals("forge-builtin", policy.getName());
assertTrue(policy.usesForgeBuiltinAi());
assertEquals(0.25, policy.getCardAdvantageWeight(), 0.001);
assertEquals(0.30, policy.getTempoWeight(), 0.001);
assertEquals(0.25, policy.getThreatScoringWeight(), 0.001);
assertEquals(0.20, policy.getCombatWeight(), 0.001);
}
@Test
void testCustomPolicyWeights() {
ForgeBuiltinPolicy policy = new ForgeBuiltinPolicy(0.4, 0.3, 0.2, 0.1);
assertEquals(0.4, policy.getCardAdvantageWeight(), 0.001);
assertEquals(0.3, policy.getTempoWeight(), 0.001);
}
// ── Stats Aggregation Tests ──────────────────────────────
@Test
void testStatsComputationSingleGame() {
List<DeckInfo> decks = createTestDecks();
List<GameResult> games = new ArrayList<>();
games.add(createGameResult(0, 0, 8, "combat_damage", new int[]{20, -5, 0}, new int[]{0, 1, 0}));
Summary summary = StatsAggregator.computeSummary(games, decks, 1000);
assertNotNull(summary);
assertEquals(3, summary.perDeck.size());
// Seat 0 won
DeckSummary s0 = summary.perDeck.get(0);
assertEquals(1, s0.wins);
assertEquals(0, s0.losses);
assertEquals(1.0, s0.winRate, 0.001);
assertNotNull(s0.avgTurnsToWin);
assertEquals(8.0, s0.avgTurnsToWin, 0.001);
// Seat 1 lost
DeckSummary s1 = summary.perDeck.get(1);
assertEquals(0, s1.wins);
assertEquals(1, s1.losses);
assertEquals(0.0, s1.winRate, 0.001);
assertNull(s1.avgTurnsToWin); // No wins
assertEquals(1.0, s1.avgMulligans, 0.001); // Had 1 mulligan
}
@Test
void testStatsComputationMultipleGames() {
List<DeckInfo> decks = createTestDecks();
List<GameResult> games = new ArrayList<>();
// Game 0: Seat 0 wins in 8 turns
games.add(createGameResult(0, 0, 8, "combat_damage", new int[]{20, 0, 5}, new int[]{0, 0, 0}));
// Game 1: Seat 1 wins in 12 turns
games.add(createGameResult(1, 1, 12, "commander_damage", new int[]{0, 30, 10}, new int[]{1, 0, 1}));
// Game 2: Seat 2 wins in 6 turns
games.add(createGameResult(2, 2, 6, "combo_alt_win", new int[]{40, 40, 40}, new int[]{0, 0, 0}));
// Game 3: Seat 0 wins in 10 turns
games.add(createGameResult(3, 0, 10, "combat_damage", new int[]{15, -3, 0}, new int[]{0, 0, 0}));
Summary summary = StatsAggregator.computeSummary(games, decks, 5000);
// Seat 0: 2 wins / 4 games = 50%
assertEquals(2, summary.perDeck.get(0).wins);
assertEquals(0.5, summary.perDeck.get(0).winRate, 0.001);
assertEquals(9.0, summary.perDeck.get(0).avgTurnsToWin, 0.001); // (8+10)/2
// Seat 1: 1 win / 4 games = 25%
assertEquals(1, summary.perDeck.get(1).wins);
assertEquals(0.25, summary.perDeck.get(1).winRate, 0.001);
// Seat 2: 1 win / 4 games = 25%
assertEquals(1, summary.perDeck.get(2).wins);
// Average turns across all games: (8+12+6+10)/4 = 9.0
assertEquals(9.0, summary.avgGameTurns, 0.001);
}
@Test
void testStatsWithDraws() {
List<DeckInfo> decks = createTestDecks();
List<GameResult> games = new ArrayList<>();
// Draw game (winningSeat = null)
GameResult draw = createGameResult(0, null, 50, "timeout", new int[]{10, 10, 10}, new int[]{0, 0, 0});
games.add(draw);
Summary summary = StatsAggregator.computeSummary(games, decks, 1000);
for (DeckSummary ds : summary.perDeck) {
assertEquals(0, ds.wins);
assertEquals(0, ds.losses);
assertEquals(1, ds.draws);
assertEquals(0.0, ds.winRate, 0.001);
assertNull(ds.avgTurnsToWin);
}
}
@Test
void testWinConditionBreakdown() {
List<DeckInfo> decks = createTestDecks();
List<GameResult> games = new ArrayList<>();
games.add(createGameResult(0, 0, 8, "combat_damage", new int[]{20, 0, 0}, new int[]{0, 0, 0}));
games.add(createGameResult(1, 0, 10, "commander_damage", new int[]{15, 0, 0}, new int[]{0, 0, 0}));
games.add(createGameResult(2, 0, 6, "combo_alt_win", new int[]{40, 0, 0}, new int[]{0, 0, 0}));
Summary summary = StatsAggregator.computeSummary(games, decks, 1000);
WinConditionBreakdown b = summary.perDeck.get(0).winConditionBreakdown;
assertEquals(1, b.combat_damage);
assertEquals(1, b.commander_damage);
assertEquals(1, b.combo_alt_win);
assertEquals(0, b.life_drain);
}
// ── Merge Stats Tests ────────────────────────────────────
@Test
void testMergeStats() {
List<GameResult> worker1 = new ArrayList<>();
worker1.add(createGameResult(0, 0, 8, "combat_damage", new int[]{20, 0, 0}, new int[]{0, 0, 0}));
worker1.add(createGameResult(1, 1, 10, "commander_damage", new int[]{0, 20, 0}, new int[]{0, 0, 0}));
List<GameResult> worker2 = new ArrayList<>();
worker2.add(createGameResult(2, 2, 6, "combo_alt_win", new int[]{0, 0, 40}, new int[]{0, 0, 0}));
List<List<GameResult>> workers = List.of(worker1, worker2);
List<GameResult> merged = StatsAggregator.mergeStats(workers);
assertEquals(3, merged.size());
// Verify re-indexing
assertEquals(0, merged.get(0).gameIndex);
assertEquals(1, merged.get(1).gameIndex);
assertEquals(2, merged.get(2).gameIndex);
}
// ── JSON Serialization Tests ─────────────────────────────
@Test
void testJsonRoundTrip() {
BatchResult original = createTestBatchResult();
String json = JsonExporter.toJson(original);
assertNotNull(json);
assertTrue(json.contains("\"schemaVersion\": \"1.0.0\""));
assertTrue(json.contains("\"format\": \"commander\""));
assertTrue(json.contains("\"podSize\": 3"));
// Validate structure
assertTrue(JsonExporter.validateBasicStructure(json));
}
@Test
void testJsonContainsAllRequiredKeys() {
BatchResult result = createTestBatchResult();
String json = JsonExporter.toJson(result);
assertTrue(json.contains("\"metadata\""));
assertTrue(json.contains("\"decks\""));
assertTrue(json.contains("\"games\""));
assertTrue(json.contains("\"summary\""));
assertTrue(json.contains("\"batchId\""));
assertTrue(json.contains("\"timestamp\""));
assertTrue(json.contains("\"winRate\""));
assertTrue(json.contains("\"avgTurnsToWin\""));
assertTrue(json.contains("\"avgMulligans\""));
assertTrue(json.contains("\"winConditionBreakdown\""));
}
@Test
void testJsonValidationRejectsInvalid() {
assertFalse(JsonExporter.validateBasicStructure("{}"));
assertFalse(JsonExporter.validateBasicStructure("not json"));
assertFalse(JsonExporter.validateBasicStructure("{\"metadata\": {}}"));
}
// ── Helper Methods ───────────────────────────────────────
private List<DeckInfo> createTestDecks() {
List<DeckInfo> decks = new ArrayList<>();
String[] names = {"Atraxa_Superfriends", "Korvold_Aristocrats", "Muldrotha_Value"};
String[] commanders = {"Atraxa, Praetors' Voice", "Korvold, Fae-Cursed King", "Muldrotha, the Gravetide"};
for (int i = 0; i < 3; i++) {
DeckInfo d = new DeckInfo();
d.seatIndex = i;
d.deckName = names[i];
d.commanderName = commanders[i];
d.deckFile = names[i] + ".dck";
d.colorIdentity = List.of("W", "U", "B", "G");
d.cardCount = 100;
decks.add(d);
}
return decks;
}
private GameResult createGameResult(int gameIndex, Integer winningSeat, int turns,
String winCondition, int[] lives, int[] mulligans) {
GameResult g = new GameResult();
g.gameIndex = gameIndex;
g.winningSeat = winningSeat;
g.totalTurns = turns;
g.winCondition = winCondition;
g.gameSeed = 42 + gameIndex;
g.elapsedMs = 1000 + gameIndex * 500;
g.playerResults = new ArrayList<>();
for (int i = 0; i < 3; i++) {
PlayerResult pr = new PlayerResult();
pr.seatIndex = i;
pr.finalLife = lives[i];
pr.mulligans = mulligans[i];
pr.isWinner = (winningSeat != null && winningSeat == i);
pr.commanderDamageDealt = 0;
pr.commanderCasts = 0;
pr.landsPlayed = turns / 2;
pr.spellsCast = turns;
pr.creaturesDestroyed = 0;
g.playerResults.add(pr);
}
return g;
}
private BatchResult createTestBatchResult() {
List<DeckInfo> decks = createTestDecks();
List<GameResult> games = new ArrayList<>();
games.add(createGameResult(0, 0, 8, "combat_damage", new int[]{20, 0, 5}, new int[]{0, 1, 0}));
games.add(createGameResult(1, 1, 12, "commander_damage", new int[]{0, 30, 10}, new int[]{1, 0, 1}));
games.add(createGameResult(2, 2, 6, "combo_alt_win", new int[]{40, 40, 40}, new int[]{0, 0, 0}));
Summary summary = StatsAggregator.computeSummary(games, decks, 3000);
BatchResult result = new BatchResult();
result.metadata = new Metadata();
result.metadata.batchId = "test-batch-001";
result.metadata.timestamp = "2026-03-06T22:00:00Z";
result.metadata.totalGames = 3;
result.metadata.completedGames = 3;
result.metadata.engineVersion = "forge-1.6.64-SNAPSHOT";
result.metadata.masterSeed = 42L;
result.metadata.threads = 1;
result.metadata.elapsedMs = 3000;
result.decks = decks;
result.games = games;
result.summary = summary;
return result;
}
}