Spaces:
Paused
Paused
Download test/duelCore.test.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/test/duelCore.test.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/test/duelCore.test.js
-
curl -L -o duelCore.test.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/test/duelCore.test.js
12.3 kB
| /* ============================================================ | |
| Guards for tools/duelCore.js — the fairness-critical "run one duel match" primitive extracted | |
| out of tools/ailab.js (docs/competitions-and-elo.md Phase 1) so a browser Worker can import it | |
| in a later stage without pulling in ailab.js's CLI/printing/search/leaderboard code. | |
| tools/ailab.js's own test/ailab.test.js already exercises pinnedDuelDials/the match runner | |
| EXHAUSTIVELY through runDuel/runSwappedDuel/runDuelBrackets — fairness across sides, difficulty | |
| pinning, determinism, override isolation — and stays the safety net proving the extraction | |
| didn't change any of that observable behaviour. This file adds direct, ISOLATED coverage of the | |
| primitive itself, imported straight from tools/duelCore.js, never through tools/ailab.js. | |
| ============================================================ */ | |
| import { test } from "node:test"; | |
| import assert from "node:assert/strict"; | |
| import { readFileSync } from "node:fs"; | |
| import { pinnedDuelDials, duelSeed, runDuelMatch } from "../tools/duelCore.js"; | |
| import { DIFFICULTY_OPTIONS } from "../engine/aiDifficulty.js"; | |
| import { STRATEGIES } from "../engine/aiStrategy.js"; | |
| /* ---------- pinnedDuelDials ---------- */ | |
| test("pinnedDuelDials reads apm/micro straight off the named DIFFICULTY_OPTIONS row", () => { | |
| for (const opt of DIFFICULTY_OPTIONS) { | |
| assert.deepEqual(pinnedDuelDials(opt.mult), { difficulty: opt.mult, apm: opt.aiApm, micro: !!opt.aiMicro }, | |
| `difficulty "${opt.mult}" must echo its own row's dials`); | |
| } | |
| }); | |
| test("pinnedDuelDials normalizes an unknown/typo'd difficulty to medium's dials AND label", () => { | |
| const medium = DIFFICULTY_OPTIONS.find(o => o.mult === "medium"); | |
| const dials = pinnedDuelDials("totallyBogusTypo"); | |
| assert.deepEqual(dials, { difficulty: "medium", apm: medium.aiApm, micro: !!medium.aiMicro }, | |
| "an unrecognised difficulty must fall back to medium's own dials, and REPORT medium -- not echo the garbage input"); | |
| }); | |
| /* ---------- duelSeed ---------- */ | |
| test("duelSeed is deterministic: identical inputs produce the identical seed", () => { | |
| const a = duelSeed(1, "ferros", "medium", "Alpha", "Beta", 0); | |
| const b = duelSeed(1, "ferros", "medium", "Alpha", "Beta", 0); | |
| assert.equal(a, b); | |
| assert.equal(typeof a, "number"); | |
| }); | |
| test("duelSeed sorts the two candidate names into the hash, so a pair draws the identical map regardless of argument order", () => { | |
| const forward = duelSeed(1, "ferros", "medium", "Alpha", "Beta", 0); | |
| const backward = duelSeed(1, "ferros", "medium", "Beta", "Alpha", 0); | |
| assert.equal(forward, backward, "duelSeed(a,b,...) and duelSeed(b,a,...) must draw the same map"); | |
| }); | |
| test("duelSeed genuinely varies with world, difficulty, replicate, and base seed", () => { | |
| const base = duelSeed(1, "ferros", "medium", "Alpha", "Beta", 0); | |
| assert.notEqual(base, duelSeed(1, "korrath", "medium", "Alpha", "Beta", 0), "world must vary the seed"); | |
| assert.notEqual(base, duelSeed(1, "ferros", "hard", "Alpha", "Beta", 0), "difficulty must vary the seed"); | |
| assert.notEqual(base, duelSeed(1, "ferros", "medium", "Alpha", "Beta", 1), "replicate must vary the seed"); | |
| assert.notEqual(base, duelSeed(2, "ferros", "medium", "Alpha", "Beta", 0), "the base seed must vary the seed"); | |
| assert.notEqual(base, duelSeed(1, "ferros", "medium", "Alpha", "Gamma", 0), "the actual pair must vary the seed"); | |
| }); | |
| /* ---------- duelSeed's seedKey escape hatch (docs/ai-evolution-design.md §8.1) ---------- */ | |
| test("omitting seedKey leaves duelSeed byte-for-byte the derivation it has always been", () => { | |
| // The whole point of the parameter is that it is invisible unless asked for: every existing | |
| // caller (runDuel without an evolutionary opts, every recorded competition row, every saved | |
| // --json duel result) must keep drawing the identical maps. | |
| for (const [world, diff, rep] of [["ferros", "medium", 0], ["korrath", "hard", 3], ["vesper", "easy", 1]]) { | |
| assert.equal(duelSeed(1, world, diff, "Alpha", "Beta", rep), | |
| duelSeed(1, world, diff, "Alpha", "Beta", rep, undefined), | |
| "an explicit undefined seedKey must be identical to omitting it"); | |
| } | |
| }); | |
| test("seedKey replaces the NAMES in the hash, so differently-named pairs share one map set", () => { | |
| // This is the property an evolutionary run needs: generated names (g3i7 vs g2i4) would otherwise | |
| // hash to their own maps every generation, and "the child beat its parent's number" would be | |
| // partly a statement about map luck rather than about the genome. | |
| const a = duelSeed(1, "ferros", "medium", "g3i7", "g2i4", 0, "evolve"); | |
| const b = duelSeed(1, "ferros", "medium", "totally", "different", 0, "evolve"); | |
| assert.equal(a, b, "with a seedKey pinned, the candidate names must not reach the map draw"); | |
| assert.notEqual(a, duelSeed(1, "ferros", "medium", "g3i7", "g2i4", 0), | |
| "and it must actually differ from the unpinned derivation, or it is doing nothing"); | |
| // Everything else still varies exactly as before — the key replaces the names, nothing else. | |
| assert.notEqual(a, duelSeed(1, "korrath", "medium", "g3i7", "g2i4", 0, "evolve")); | |
| assert.notEqual(a, duelSeed(1, "ferros", "hard", "g3i7", "g2i4", 0, "evolve")); | |
| assert.notEqual(a, duelSeed(1, "ferros", "medium", "g3i7", "g2i4", 1, "evolve")); | |
| assert.notEqual(a, duelSeed(2, "ferros", "medium", "g3i7", "g2i4", 0, "evolve")); | |
| assert.notEqual(a, duelSeed(1, "ferros", "medium", "g3i7", "g2i4", 0, "evolve2")); | |
| }); | |
| /* ---------- runDuelMatch (formerly ailab.js's own private duelRun) ---------- */ | |
| const dials = pinnedDuelDials("medium"); | |
| test("runDuelMatch returns exactly the row shape every ailab.js consumer relies on -- no more, no fewer fields", () => { | |
| const row = runDuelMatch({ | |
| world: "ferros", seed: duelSeed(7, "ferros", "medium", "A", "B", 0), dials, | |
| aName: "A", aStrategy: "default", bName: "B", bStrategy: "aggressive", minutes: 5, | |
| }); | |
| const expectedFields = [ | |
| "world", "seed", "difficulty", "swapAsym", "aName", "aStrategy", "bName", "bStrategy", | |
| "aDifficulty", "bDifficulty", "aApm", "bApm", "aMicro", "bMicro", | |
| "winner", "winReason", "time", "aScore", "bScore", "margin", | |
| ]; | |
| assert.deepEqual(Object.keys(row).sort(), [...expectedFields].sort(), | |
| "runDuelMatch's row shape must not change AT ALL -- every existing ailab.js consumer depends on this exact set of fields"); | |
| }); | |
| test("runDuelMatch is deterministic — same inputs, byte-identical row", () => { | |
| const cfg = { | |
| world: "ferros", seed: duelSeed(7, "ferros", "medium", "A", "B", 0), dials, | |
| aName: "A", aStrategy: "default", bName: "B", bStrategy: "aggressive", minutes: 5, | |
| }; | |
| assert.equal(JSON.stringify(runDuelMatch(cfg)), JSON.stringify(runDuelMatch(cfg)), | |
| "two identical calls diverged: something reads a clock or an unseeded pick"); | |
| }); | |
| test("runDuelMatch pins the SAME dials object's apm/micro/difficulty into both sides, read back off the real controllers", () => { | |
| for (const difficulty of ["easy", "hard"]) { | |
| const d = pinnedDuelDials(difficulty); | |
| const dial = DIFFICULTY_OPTIONS.find(o => o.mult === difficulty); | |
| const row = runDuelMatch({ | |
| world: "ferros", seed: duelSeed(3, "ferros", difficulty, "A", "B", 0), dials: d, | |
| aName: "A", aStrategy: "default", bName: "B", bStrategy: "economic", minutes: 5, | |
| }); | |
| assert.equal(row.difficulty, difficulty); | |
| assert.equal(row.aDifficulty, difficulty); | |
| assert.equal(row.bDifficulty, difficulty); | |
| assert.equal(row.aApm, dial.aiApm, "candidate A's own controller must carry this difficulty's aiApm"); | |
| assert.equal(row.bApm, dial.aiApm, "candidate B's own controller must carry the SAME aiApm as A, not its own"); | |
| assert.equal(row.aMicro, !!dial.aiMicro); | |
| assert.equal(row.bMicro, !!dial.aiMicro); | |
| } | |
| }); | |
| test("runDuelMatch reports swapAsym back exactly as given", () => { | |
| const cfg = { | |
| world: "ferros", seed: duelSeed(7, "ferros", "medium", "A", "B", 0), dials, | |
| aName: "A", aStrategy: "default", bName: "B", bStrategy: "aggressive", minutes: 5, | |
| }; | |
| assert.equal(runDuelMatch({ ...cfg, swapAsym: false }).swapAsym, false); | |
| assert.equal(runDuelMatch({ ...cfg, swapAsym: true }).swapAsym, true); | |
| assert.equal(runDuelMatch(cfg).swapAsym, false, "swapAsym must default falsy when omitted, coerced to a real boolean"); | |
| }); | |
| test("runDuelMatch actually resolves a fight: a strong candidate beats a defenceless one", () => { | |
| // neverInitiates + a standing-army cap of ZERO is absolute defencelessness in skirmish (no | |
| // desperation timeout) -- the same fixture shape test/ailab.test.js's own duel tests use, here | |
| // exercised directly against the primitive with a single match rather than a whole worlds x | |
| // seeds matrix (that exhaustive coverage already lives in test/ailab.test.js via runDuel). | |
| STRATEGIES.labDuelCoreCrippled = { name: "labDuelCoreCrippled", desc: "test", neverInitiates: true, standingArmyCap: 0 }; | |
| const row = runDuelMatch({ | |
| world: "korrath", seed: duelSeed(7, "korrath", "medium", "normal", "crippled", 0), dials, | |
| aName: "normal", aStrategy: "default", bName: "crippled", bStrategy: "labDuelCoreCrippled", minutes: 20, | |
| }); | |
| assert.equal(row.winner, "a", `the normal candidate must win (got winner=${row.winner}, reason=${row.winReason})`); | |
| assert.equal(row.winReason, "elimination", "a defenceless base should lose to a genuine assault before the clock"); | |
| assert.ok(row.margin > 0, "the winning side's score margin must be positive"); | |
| }); | |
| /* ---------- D3 (docs/competitions-and-elo.md): aArchetype/bArchetype -- the per-entrant archetype | |
| override, threaded through runDuelMatch's own config into createSelfPlayState's two controllers. | |
| The row shape itself must stay UNCHANGED (archetype is an input dial here, never a reported | |
| output field -- see the exact-shape test above), so this proves the override reaches the sim | |
| some other way: two otherwise-identical runs that disagree only on archetype must diverge. ---------- */ | |
| test("runDuelMatch's aArchetype/bArchetype reach the simulated match without changing the row's own field set", () => { | |
| const cfg = { | |
| world: "ferros", seed: duelSeed(9, "ferros", "medium", "A", "B", 0), dials, | |
| aName: "A", aStrategy: "default", bName: "B", bStrategy: "default", minutes: 8, | |
| }; | |
| const baseline = runDuelMatch(cfg); | |
| const withArchetypes = runDuelMatch({ ...cfg, aArchetype: "rusher", bArchetype: "technologist" }); | |
| assert.deepEqual(Object.keys(withArchetypes).sort(), Object.keys(baseline).sort(), | |
| "supplying an archetype must not add or remove ANY field from the row -- it's an input dial, not an output"); | |
| assert.notEqual(JSON.stringify(baseline), JSON.stringify(withArchetypes), | |
| "two otherwise-identical matches that disagree only on archetype must actually simulate differently -- " + | |
| "proof the override genuinely reaches the created states, not just accepted and ignored"); | |
| }); | |
| test("runDuelMatch treats a missing/null aArchetype/bArchetype as byte-identical to omitting the option entirely", () => { | |
| const cfg = { | |
| world: "ferros", seed: duelSeed(9, "ferros", "medium", "A", "B", 0), dials, | |
| aName: "A", aStrategy: "default", bName: "B", bStrategy: "default", minutes: 8, | |
| }; | |
| const omitted = runDuelMatch(cfg); | |
| const explicitNull = runDuelMatch({ ...cfg, aArchetype: null, bArchetype: undefined }); | |
| assert.equal(JSON.stringify(omitted), JSON.stringify(explicitNull)); | |
| }); | |
| /* ---------- browser importability (docs/competitions-and-elo.md Phase 1: "it will be imported by | |
| a Worker next stage") -- same idiom test/ai-selfplay.test.js already uses for tools/selfplay.js, | |
| pointed at this file instead. ---------- */ | |
| test("tools/duelCore.js has no bare `document` or `window` reference anywhere in its source", () => { | |
| const src = readFileSync(new URL("../tools/duelCore.js", import.meta.url), "utf8"); | |
| const FORBIDDEN = /\bdocument\b|\bwindow\b/; | |
| const offenders = []; | |
| src.split("\n").forEach((line, i) => { | |
| if (FORBIDDEN.test(line)) offenders.push(`${i + 1}: ${line.trim()}`); | |
| }); | |
| assert.deepEqual(offenders, [], | |
| "tools/duelCore.js must have zero `document`/`window` references so a browser Worker can import it cleanly:\n" | |
| + offenders.join("\n")); | |
| }); | |