Spaces:
Paused
Paused
Commit History
-
Close the guard gaps behind the last three bugs; withdraw one review finding
Claude +1: Claude Opus 4.8Unverified signatureFindings 4, 5 and 8 of docs/code-review-2026-08.md, plus a correction to 7. #8 tools/ ships to the browser but was outside the purity guard. boot.js imports tools/selfplay.js; competition.js, competitionWorker.js and playerFingerprint.js import tools/duelCore.js and tools/genome.js. So three "Node bench" files run in a browser Worker and decide the outcomes that become persisted Elo ratings, while the purity scan only ever walked engine/ and what engine/ imports — structurally identical code guarded on one side of a directory line and unguarded on the other. The review said moving the files was "simplest and better". Measuring first showed otherwise: 30+ prose references across engine/, README and docs would have had to move with them, for no safety gain. Extended the scan instead, and DERIVED the extra roots by walking out from index.html (plus the Worker entry, whose `new Worker(new URL(...))` edge no import walk can see) so a fourth file pulled into the app tomorrow is covered the…021c865
-
Phase 5 (replay + season): re-run any recorded match, and close a ladder
Claude +1: Claude Opus 4.8Unverified signaturedocs/competitions-and-elo.md Phase 5's remaining two bullets, which completes the plan — all six phases (0-5) have now landed, noted in a new closing section. REPLAY IS A RE-RUN, NOT A RECORDING. A stored history row already carries its world, its exact seed, its swapAsym half, its pinned difficulty and both entrants' strategies; the roster carries each entrant's archetype. That is the complete input to createSelfPlayState, so buildReplayConfig(row, ledger) rebuilds the match and startSpectatedMatch runs it through the spectator the previous stage built — same speed control, same Observer Mode, nothing captured, nothing stored. What gets rebuilt is the SEATING, not the labelling: a row is always reported A-relative but was played in one of the worker's two directions, so buildReplayConfig un-does flipRow's relabelling and restates the recorded outcome seat-relative, which is what lets spectatedMatchOutcome be compared to it directly. DETERMINISM HELD — BUT ONLY AFTER A REAL BUG, AND …090c049
-
Phase 2 (part 2): roster + persistent ladder wired into Quick Duel, archetype threaded end-to-end
Claude +1: Claude Sonnet 5Unverified signatureTurns Phase 1's one-off Quick Duel into the persistent competition mode docs/competitions-and-elo.md Phase 2 describes: Quick Duel / Roster / Standings, one coherent mode behind a small tab row, all three reading and writing the SAME competitionLedger.js ledger. THE KEY UX CHANGE: ad-hoc, free-typed entrants are retired. Each Quick Duel entrant is now either picked "From Roster" (a <select> of competitionLedger's current roster) or drafted "New Entrant" (name + strategy + archetype + faction) -- a new draft joins the roster, via addRosterEntry, the instant startDuel() actually commits to running (after buildJob's own validation passes, before the Worker spins up), never on a keystroke, so an abandoned draft never pollutes the roster. A finished duel's rows are folded into the ledger for the pinned difficulty bracket via recordCompetition alone -- no second, hand-rolled elo.js applySeries call in competition.js. The results view's old "Elo isn't saved" line is gone, replaced by "Elo up…628afc1 -
Phase 0: split tools/selfplay.js into a browser-safe core + tools/selfplay-cli.js
Claude +1: Claude Sonnet 5Unverified signaturedocs/competitions-and-elo.md's Phase 0: tools/selfplay.js ended with a module-scope `process.argv[1]` guard, which throws ReferenceError the moment the file is imported anywhere without a `process` global (a browser main thread, or a Worker) — the blocker every later phase in that doc depends on being gone. - tools/selfplay.js now keeps ONLY the pure core: createSelfPlayState, tickSelfPlay, runSelfPlayMatch, fingerprint, and the DT constant they share. Zero `process`/`document` references anywhere in the file, not even in a comment. Function bodies are byte-for-byte unchanged from before the split. - tools/selfplay-cli.js is the new Node CLI entry point (parseArgs, runCmd, USAGE, main, and the process.argv[1]-guarded invocation line), importing the four core functions from "./selfplay.js". `node tools/selfplay-cli.js run ...` behaves identically to the old `node tools/selfplay.js run ...` — same flags, same output. - tools/ailab.js's import of createSelfPlayState/runSe…022fcd2
-
Tier 2d: make the AI bench measure what it claims to measure
Claude +1: Claude Opus 5Unverified signatureThe side-swap could not detect the thing it exists to detect. Its header says "a side-symmetry bug would show up as those two disagreeing", but duelSeed hashed the candidate names in ORDER while runSwappedDuel calls runDuel(a,b) then runDuel(b,a) — so the two halves differed in seat AND map, and any disagreement was confounded with ordinary seed variance. Sorting the pair into the hash makes it a genuine paired comparison, which is what swapAsym (the map-side control) already was. No result test needed re-baselining: they compare two runs of the same call, not frozen values. Swiss standings ranked raw win totals across unequal match counts. A bye is scorable-neutral since e9ad1d0, but its recipient plays one fewer pairing — 16 fewer chances to win under the CLI defaults — so sorting on totals ranked "everyone who avoided a bye, then everyone who didn't": a 2W-2L candidate at 50% finished below a 3W-3L candidate at 50% purely for having played more. That is the mirror image of the bug …c3a176e
-
Make the AI controller owner-parametric: true two-sided self-play
Claude +1: Claude Sonnet 5Unverified signaturerunAI(state, dt, owner = "ai") and every AI phase module now resolve their controller, opponent, and fog grid from `owner` instead of hardcoding "ai"/ state.fogAI — 100% backward compatible (every pre-existing call site keeps the default and behaves byte-for-byte as before). A new, independently configured controller slot (state.playerAi, null by default) lets a second real AI drive the "player" seat in a skirmish, headless-only (tools/ selfplay.js) — nothing in setup.js/boot.js/the shipped game changed. Independent adversarial review (fairness lens + correctness lens) caught a real bug before this was accepted: Hard difficulty's economic edge (hardEdge) was seeded onto state.players.ai only, never a self-play "player" controller configured for Hard. Fixed via seedDifficultyEdge (engine/state.js), called for "player" once state.playerAi exists. engine/persist.js's save/load extension is purely additive (no SAVE_VERSION bump) — old saves round-trip unaffected. Co-Authored-By: Claude …916c3ad