burrowdweller commited on
Commit
6e205a6
·
verified ·
1 Parent(s): d55daad

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. browser/entry.js +310 -26
  2. browser/manifest.json +33 -3
browser/entry.js CHANGED
@@ -3607,6 +3607,44 @@ function moveToIndex(fromName, toName, promoLetter) {
3607
  // algorithm that isn't finalized yet would mean re-porting it again
3608
  // later. Default behavior here (plain PUCT) matches Python's default
3609
  // config exactly; nothing is being weakened, just left for later.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3610
  class MCTSConfig {
3611
  constructor({ nSimulations = 800, timeBudgetMs = null, // anytime: stop on whichever hits first
3612
  cPuct = 1.5, batchSize = 1, // >1 enables virtual-loss leaf batching
@@ -3647,6 +3685,19 @@ function wdlToScore(wdlProbs) {
3647
  function isDraw(chess) {
3648
  return chess.isGameOver() && !chess.isCheckmate();
3649
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
3650
  class MCTS {
3651
  /**
3652
  * @param {{evaluateBatch(chessInstances): Promise<{policyLogits: Float32Array[], wdlLogits: Float32Array[]}>}} evaluator
@@ -3655,41 +3706,72 @@ class MCTS {
3655
  constructor(evaluator, config) {
3656
  this.evaluator = evaluator;
3657
  this.config = config || new MCTSConfig();
 
 
3658
  }
3659
  /**
3660
  * Run search from `chess`'s current position. Returns the winning move's
3661
  * SAN string (the most-visited root child). Mutates `chess` internally
3662
  * via move()/undo() during simulation but always leaves it at the
3663
  * original position when it returns.
 
 
 
 
 
 
 
3664
  */
3665
- async search(chess) {
3666
  const cfg = this.config;
 
3667
  const root = new Node(1.0);
3668
- const startTime = Date.now();
 
 
 
 
 
 
 
 
 
 
3669
  let sim = 0;
3670
  if (cfg.batchSize <= 1) {
3671
  while (sim < cfg.nSimulations) {
3672
- if (cfg.timeBudgetMs !== null && Date.now() - startTime >= cfg.timeBudgetMs) {
3673
  break;
3674
  }
3675
  await this._simulate(root, chess);
3676
  sim++;
 
 
3677
  }
3678
  }
3679
  else {
3680
  // Bootstrap root expansion exactly once, matching serial search.
3681
- if (sim < cfg.nSimulations && (cfg.timeBudgetMs === null || Date.now() - startTime < cfg.timeBudgetMs)) {
3682
  const value = await this._expand(root, chess);
3683
  this._backup([root], value);
3684
  sim++;
 
3685
  }
3686
  while (sim < cfg.nSimulations) {
3687
- if (cfg.timeBudgetMs !== null && Date.now() - startTime >= cfg.timeBudgetMs) {
3688
  break;
3689
  }
3690
- const roundBudget = Math.min(cfg.batchSize, cfg.nSimulations - sim);
3691
- const completed = await this._runBatchedRound(root, chess, roundBudget, startTime);
 
 
 
 
 
 
3692
  sim += completed;
 
 
3693
  if (completed === 0)
3694
  break;
3695
  }
@@ -3716,6 +3798,21 @@ class MCTS {
3716
  }
3717
  return bestEdge.san;
3718
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3719
  // -- internals -----------------------------------------------------
3720
  async _simulate(root, chess) {
3721
  let node = root;
@@ -3741,12 +3838,13 @@ class MCTS {
3741
  chess.undo();
3742
  }
3743
  }
3744
- async _runBatchedRound(root, chess, roundBudget, startTime) {
 
3745
  const pending = new Map();
3746
  const order = [];
3747
  let completed = 0;
3748
  for (let i = 0; i < roundBudget; i++) {
3749
- if (this.config.timeBudgetMs !== null && Date.now() - startTime >= this.config.timeBudgetMs) {
3750
  break;
3751
  }
3752
  const result = this._selectLeafVL(root, chess);
@@ -3772,6 +3870,9 @@ class MCTS {
3772
  completed += this._finishFreshEntry(entries[i], policyLogits[i], wdlLogits[i]);
3773
  }
3774
  }
 
 
 
3775
  return completed;
3776
  }
3777
  _selectLeafVL(root, chess) {
@@ -3949,18 +4050,40 @@ const OPENING_BOOK = Object.freeze({ "1nbqkbnr/1ppppppp/8/8/8/1P2P3/1BPP1PPP/rN1
3949
  //
3950
  // Contract (rules technical appendix, schema chess-gpt-package-v1):
3951
  // export async function loadPackage({ artifacts, config, ort })
3952
- // -> { newGame({ random }) -> { chooseMove({ history, legalMoves }) -> SAN,
3953
  // dispose() },
3954
  // dispose() }
3955
  //
 
 
 
 
 
 
 
 
 
 
 
3956
  // Because we only ever return a member of `legalMoves`, this can never lose
3957
  // on an illegal/invalid move -- if anything above goes wrong for a specific
3958
  // position, we fall back to legalMoves[0] rather than throw.
3959
- // ~10s/move budget per the tournament rules; 9.2s leaves margin for
3960
- // encoding/dispatch overhead around each simulation's model call.
3961
- // Overridable via loadPackage's `config` (e.g. { timeBudgetMs: 500 } for
3962
- // fast test iteration) -- see test_smoke.mjs.
 
 
3963
  const DEFAULT_TIME_BUDGET_MS = 9200;
 
 
 
 
 
 
 
 
 
3964
  const MAX_SIMULATIONS = 100000; // effectively unbounded; time budget governs
3965
  // Virtual-loss leaf batching (chessdb-4ru): a batchSize sweep (1/2/4/8/
3966
  // 16/32) against the real model via onnxruntime-node
@@ -3981,9 +4104,153 @@ function emitEvent(onEvent, event) {
3981
  // Diagnostics must never change engine behavior.
3982
  }
3983
  }
3984
- function clockMs() {
3985
  return typeof performance !== "undefined" ? performance.now() : Date.now();
3986
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3987
  export function normalizeBookFen(fen) {
3988
  return fen.split(" ").slice(0, 4).join(" ");
3989
  }
@@ -4064,13 +4331,25 @@ export async function loadPackage({ artifacts, ort, config }) {
4064
  async newGame( /* { random } */) {
4065
  const mcts = new MCTS(evaluator, new MCTSConfig({ nSimulations, timeBudgetMs, batchSize: MCTS_BATCH_SIZE }));
4066
  return {
4067
- async chooseMove({ history, legalMoves }) {
4068
  if (!legalMoves || legalMoves.length === 0) {
4069
  throw new Error("chooseMove called with no legalMoves");
4070
  }
4071
- const startedAt = clockMs();
 
 
 
 
 
 
 
 
 
 
 
 
4072
  try {
4073
- const chess = new Chess();
4074
  for (const san of history) {
4075
  chess.move(san);
4076
  }
@@ -4080,6 +4359,7 @@ export async function loadPackage({ artifacts, ort, config }) {
4080
  history: [...history],
4081
  fen: chess.fen(),
4082
  legalMoves: [...legalMoves],
 
4083
  });
4084
  const bookMove = history.length <= 9
4085
  ? bookMoveSan(chess, OPENING_BOOK, legalMoves)
@@ -4089,12 +4369,16 @@ export async function loadPackage({ artifacts, ort, config }) {
4089
  type: "book_hit",
4090
  ply: history.length,
4091
  move: bookMove,
4092
- elapsedMs: clockMs() - startedAt,
4093
  });
4094
- return bookMove;
 
 
 
 
4095
  }
4096
- const chosen = await mcts.search(chess);
4097
- const elapsedMs = clockMs() - startedAt;
4098
  // mcts.search() draws its move from chess.js's own legal-move
4099
  // list at the current position, which should always match the
4100
  // runner-provided legalMoves -- but if the runner's SAN
@@ -4110,7 +4394,7 @@ export async function loadPackage({ artifacts, ort, config }) {
4110
  move: fallback,
4111
  elapsedMs,
4112
  });
4113
- return fallback;
4114
  }
4115
  emitEvent(onEvent, {
4116
  type: "mcts_move",
@@ -4119,7 +4403,7 @@ export async function loadPackage({ artifacts, ort, config }) {
4119
  elapsedMs,
4120
  simCount: mcts.lastSimCount,
4121
  });
4122
- return chosen;
4123
  }
4124
  catch (error) {
4125
  const fallback = legalMoves[0];
@@ -4128,9 +4412,9 @@ export async function loadPackage({ artifacts, ort, config }) {
4128
  reason: "exception",
4129
  error: String(error instanceof Error ? error.message : error),
4130
  move: fallback,
4131
- elapsedMs: clockMs() - startedAt,
4132
  });
4133
- return fallback;
4134
  }
4135
  },
4136
  async dispose() { },
 
3607
  // algorithm that isn't finalized yet would mean re-porting it again
3608
  // later. Default behavior here (plain PUCT) matches Python's default
3609
  // config exactly; nothing is being weakened, just left for later.
3610
+ const ADAPTIVE_BATCH_SAFETY = 1.25;
3611
+ const ADAPTIVE_BATCH_GUARD_MS = 2;
3612
+ const ADAPTIVE_BATCH_EMA_ALPHA = 0.25;
3613
+ function clockMs() {
3614
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
3615
+ return performance.now();
3616
+ }
3617
+ return Date.now();
3618
+ }
3619
+ /**
3620
+ * Choose the largest halving batch that conservatively fits the remaining
3621
+ * budget. `referenceBatchMs` is an observed duration for
3622
+ * `referenceBatchSize`; lower sizes use a linear estimate plus safety margin.
3623
+ * A null reference preserves the configured batch for cold-start behavior.
3624
+ */
3625
+ function adaptiveBatchSizeForBudget(maxBatchSize, remainingMs, referenceBatchMs, referenceBatchSize = maxBatchSize) {
3626
+ if (!Number.isInteger(maxBatchSize) || maxBatchSize <= 0)
3627
+ return 0;
3628
+ if (remainingMs === Infinity)
3629
+ return maxBatchSize;
3630
+ if (!Number.isFinite(remainingMs) || remainingMs <= 0)
3631
+ return 0;
3632
+ if (!Number.isFinite(referenceBatchMs)
3633
+ || referenceBatchMs <= 0
3634
+ || !Number.isInteger(referenceBatchSize)
3635
+ || referenceBatchSize <= 0) {
3636
+ return maxBatchSize;
3637
+ }
3638
+ for (let candidate = maxBatchSize; candidate >= 1; candidate = Math.floor(candidate / 2)) {
3639
+ const estimatedMs = referenceBatchMs * (candidate / referenceBatchSize);
3640
+ const requiredMs = estimatedMs * ADAPTIVE_BATCH_SAFETY + ADAPTIVE_BATCH_GUARD_MS;
3641
+ if (remainingMs >= requiredMs)
3642
+ return candidate;
3643
+ if (candidate === 1)
3644
+ break;
3645
+ }
3646
+ return 0;
3647
+ }
3648
  class MCTSConfig {
3649
  constructor({ nSimulations = 800, timeBudgetMs = null, // anytime: stop on whichever hits first
3650
  cPuct = 1.5, batchSize = 1, // >1 enables virtual-loss leaf batching
 
3685
  function isDraw(chess) {
3686
  return chess.isGameOver() && !chess.isCheckmate();
3687
  }
3688
+ function rootSnapshot(root, simCount) {
3689
+ return {
3690
+ simCount,
3691
+ candidates: Array.from(root.children.values(), (edge) => ({
3692
+ san: edge.san,
3693
+ from: edge.from,
3694
+ to: edge.to,
3695
+ promotion: edge.promotion,
3696
+ visits: edge.node.visitCount,
3697
+ prior: edge.node.prior,
3698
+ })),
3699
+ };
3700
+ }
3701
  class MCTS {
3702
  /**
3703
  * @param {{evaluateBatch(chessInstances): Promise<{policyLogits: Float32Array[], wdlLogits: Float32Array[]}>}} evaluator
 
3706
  constructor(evaluator, config) {
3707
  this.evaluator = evaluator;
3708
  this.config = config || new MCTSConfig();
3709
+ this.batchDurationMs = null;
3710
+ this.batchDurationSamples = 0;
3711
  }
3712
  /**
3713
  * Run search from `chess`'s current position. Returns the winning move's
3714
  * SAN string (the most-visited root child). Mutates `chess` internally
3715
  * via move()/undo() during simulation but always leaves it at the
3716
  * original position when it returns.
3717
+ *
3718
+ * `timeBudgetMs` (optional, second-argument object) overrides
3719
+ * `this.config.timeBudgetMs` for this one call only -- lets a caller that
3720
+ * creates one long-lived MCTS instance per game (entry.js does, so root
3721
+ * statistics reset per move but the instance itself persists) still honor
3722
+ * a per-move budget that can change move to move (e.g. a tournament
3723
+ * runner's per-move time limit).
3724
  */
3725
+ async search(chess, { timeBudgetMs, onRootUpdate } = {}) {
3726
  const cfg = this.config;
3727
+ const effectiveTimeBudgetMs = timeBudgetMs !== undefined ? timeBudgetMs : cfg.timeBudgetMs;
3728
  const root = new Node(1.0);
3729
+ const notifyRoot = (simCount) => {
3730
+ if (typeof onRootUpdate !== "function" || root.children.size === 0)
3731
+ return;
3732
+ try {
3733
+ onRootUpdate(rootSnapshot(root, simCount));
3734
+ }
3735
+ catch {
3736
+ // Optional diagnostics must never change search behavior.
3737
+ }
3738
+ };
3739
+ const startTime = clockMs();
3740
  let sim = 0;
3741
  if (cfg.batchSize <= 1) {
3742
  while (sim < cfg.nSimulations) {
3743
+ if (effectiveTimeBudgetMs !== null && clockMs() - startTime >= effectiveTimeBudgetMs) {
3744
  break;
3745
  }
3746
  await this._simulate(root, chess);
3747
  sim++;
3748
+ if (sim === 1 || sim % 16 === 0)
3749
+ notifyRoot(sim);
3750
  }
3751
  }
3752
  else {
3753
  // Bootstrap root expansion exactly once, matching serial search.
3754
+ if (sim < cfg.nSimulations && (effectiveTimeBudgetMs === null || clockMs() - startTime < effectiveTimeBudgetMs)) {
3755
  const value = await this._expand(root, chess);
3756
  this._backup([root], value);
3757
  sim++;
3758
+ notifyRoot(sim);
3759
  }
3760
  while (sim < cfg.nSimulations) {
3761
+ if (effectiveTimeBudgetMs !== null && clockMs() - startTime >= effectiveTimeBudgetMs) {
3762
  break;
3763
  }
3764
+ const maxRoundBudget = Math.min(cfg.batchSize, cfg.nSimulations - sim);
3765
+ const remainingMs = effectiveTimeBudgetMs === null
3766
+ ? Infinity
3767
+ : effectiveTimeBudgetMs - (clockMs() - startTime);
3768
+ const roundBudget = this._chooseAdaptiveBatchSize(remainingMs, maxRoundBudget);
3769
+ if (roundBudget === 0)
3770
+ break;
3771
+ const completed = await this._runBatchedRound(root, chess, roundBudget, startTime, effectiveTimeBudgetMs);
3772
  sim += completed;
3773
+ if (completed > 0)
3774
+ notifyRoot(sim);
3775
  if (completed === 0)
3776
  break;
3777
  }
 
3798
  }
3799
  return bestEdge.san;
3800
  }
3801
+ _recordDefaultBatchTiming(elapsedMs) {
3802
+ if (!Number.isFinite(elapsedMs) || elapsedMs <= 0)
3803
+ return;
3804
+ if (this.batchDurationMs === null) {
3805
+ this.batchDurationMs = elapsedMs;
3806
+ }
3807
+ else {
3808
+ this.batchDurationMs = ((1 - ADAPTIVE_BATCH_EMA_ALPHA) * this.batchDurationMs
3809
+ + ADAPTIVE_BATCH_EMA_ALPHA * elapsedMs);
3810
+ }
3811
+ this.batchDurationSamples++;
3812
+ }
3813
+ _chooseAdaptiveBatchSize(remainingMs, maxBatchSize = this.config.batchSize) {
3814
+ return adaptiveBatchSizeForBudget(maxBatchSize, remainingMs, this.batchDurationMs, this.config.batchSize);
3815
+ }
3816
  // -- internals -----------------------------------------------------
3817
  async _simulate(root, chess) {
3818
  let node = root;
 
3838
  chess.undo();
3839
  }
3840
  }
3841
+ async _runBatchedRound(root, chess, roundBudget, startTime, timeBudgetMs) {
3842
+ const roundStart = clockMs();
3843
  const pending = new Map();
3844
  const order = [];
3845
  let completed = 0;
3846
  for (let i = 0; i < roundBudget; i++) {
3847
+ if (timeBudgetMs !== null && clockMs() - startTime >= timeBudgetMs) {
3848
  break;
3849
  }
3850
  const result = this._selectLeafVL(root, chess);
 
3870
  completed += this._finishFreshEntry(entries[i], policyLogits[i], wdlLogits[i]);
3871
  }
3872
  }
3873
+ if (order.length === this.config.batchSize) {
3874
+ this._recordDefaultBatchTiming(clockMs() - roundStart);
3875
+ }
3876
  return completed;
3877
  }
3878
  _selectLeafVL(root, chess) {
 
4050
  //
4051
  // Contract (rules technical appendix, schema chess-gpt-package-v1):
4052
  // export async function loadPackage({ artifacts, config, ort })
4053
+ // -> { newGame({ random }) -> { chooseMove({ history, legalMoves, moveTimeLimitMs }) -> SAN,
4054
  // dispose() },
4055
  // dispose() }
4056
  //
4057
+ // moveTimeLimitMs (added to the rules 2026-07-31): the runner's wall-clock
4058
+ // budget in ms for this move. A submission MAY use it and MAY ignore it;
4059
+ // this one uses it to cap MCTS's per-move search so we stay under it (the
4060
+ // runner allows a 1.25x grace period before terminating the worker as a
4061
+ // loss, but that grace is not extra thinking time and must not be relied
4062
+ // on -- see MOVE_TIME_LIMIT_MARGIN below). Packages published before this
4063
+ // revision remain valid: the field is additive, and chooseMove falls back
4064
+ // to the fixed DEFAULT_TIME_BUDGET_MS/config.timeBudgetMs budget below
4065
+ // when it's absent (also how local tests/benchmarks that don't pass it
4066
+ // keep working unchanged).
4067
+ //
4068
  // Because we only ever return a member of `legalMoves`, this can never lose
4069
  // on an illegal/invalid move -- if anything above goes wrong for a specific
4070
  // position, we fall back to legalMoves[0] rather than throw.
4071
+ // Fallback budget when the runner doesn't supply moveTimeLimitMs on
4072
+ // chooseMove (older runner, or a local test/benchmark harness that omits
4073
+ // it -- see test_smoke.mjs). Also overridable via loadPackage's `config`
4074
+ // (e.g. { timeBudgetMs: 500 } for fast test iteration); config.timeBudgetMs
4075
+ // wins over this default but NOT over a real per-move moveTimeLimitMs,
4076
+ // which is the live constraint whenever the runner provides one.
4077
  const DEFAULT_TIME_BUDGET_MS = 9200;
4078
+ // Fraction of the runner's per-move moveTimeLimitMs actually budgeted to
4079
+ // MCTS search, leaving margin for encoding/dispatch overhead around each
4080
+ // simulation's model call before the runner's own clock runs out. Matches
4081
+ // the historical 9.2s-of-10s margin (DEFAULT_TIME_BUDGET_MS above, from
4082
+ // when the budget was a fixed ~10s/move) so this doesn't change behavior
4083
+ // for the moveTimeLimitMs=10000 case. Deliberately stays inside
4084
+ // moveTimeLimitMs itself, not the runner's 1.25x grace period -- that
4085
+ // grace is for worker-teardown latency, not additional thinking time.
4086
+ const MOVE_TIME_LIMIT_MARGIN = 0.92;
4087
  const MAX_SIMULATIONS = 100000; // effectively unbounded; time budget governs
4088
  // Virtual-loss leaf batching (chessdb-4ru): a batchSize sweep (1/2/4/8/
4089
  // 16/32) against the real model via onnxruntime-node
 
4104
  // Diagnostics must never change engine behavior.
4105
  }
4106
  }
4107
+ function entryClockMs() {
4108
  return typeof performance !== "undefined" ? performance.now() : Date.now();
4109
  }
4110
+ const MAX_CANDIDATES = 4;
4111
+ const MIN_REFRESH_MS = 150;
4112
+ const MAX_COMMANDS_PER_MOVE = 56;
4113
+ const ORDINARY_COMMAND_LIMIT = MAX_COMMANDS_PER_MOVE - 1;
4114
+ const CANDIDATE_FADE_MS = 900;
4115
+ const FINAL_FADE_MS = 1500;
4116
+ function selectCandidateArrows(snapshot, legalMoves) {
4117
+ const legal = new Set(Array.isArray(legalMoves) ? legalMoves : []);
4118
+ const candidates = Array.isArray(snapshot?.candidates) ? snapshot.candidates : [];
4119
+ const ranked = candidates
4120
+ .filter((candidate) => candidate && legal.has(candidate.san))
4121
+ .sort((a, b) => {
4122
+ const aVisits = Number.isFinite(a.visits) ? a.visits : 0;
4123
+ const bVisits = Number.isFinite(b.visits) ? b.visits : 0;
4124
+ const aHasVisits = aVisits > 0 ? 1 : 0;
4125
+ const bHasVisits = bVisits > 0 ? 1 : 0;
4126
+ return bHasVisits - aHasVisits
4127
+ || bVisits - aVisits
4128
+ || (Number.isFinite(b.prior) ? b.prior : 0) - (Number.isFinite(a.prior) ? a.prior : 0)
4129
+ || String(a.san).localeCompare(String(b.san));
4130
+ });
4131
+ const selected = [];
4132
+ const keys = new Set();
4133
+ for (const candidate of ranked) {
4134
+ if (typeof candidate.from !== "string" || typeof candidate.to !== "string")
4135
+ continue;
4136
+ const key = `${candidate.from}-${candidate.to}`;
4137
+ if (keys.has(key))
4138
+ continue;
4139
+ keys.add(key);
4140
+ selected.push({
4141
+ san: candidate.san,
4142
+ from: candidate.from,
4143
+ to: candidate.to,
4144
+ });
4145
+ if (selected.length === MAX_CANDIDATES)
4146
+ break;
4147
+ }
4148
+ return selected.map((candidate, rank) => ({
4149
+ ...candidate,
4150
+ intensity: selected.length > 1 ? 1 - 0.6 * rank / (selected.length - 1) : 1,
4151
+ }));
4152
+ }
4153
+ function createThinkingDisplay(thinking) {
4154
+ const emit = typeof thinking?.emit === "function" ? thinking.emit.bind(thinking) : null;
4155
+ const displayed = new Map();
4156
+ let ordinaryCommands = 0;
4157
+ let lastUpdateAt = -Infinity;
4158
+ const safeEmit = (command) => {
4159
+ if (!emit)
4160
+ return;
4161
+ try {
4162
+ emit(command);
4163
+ }
4164
+ catch {
4165
+ // Display diagnostics must never change move selection.
4166
+ }
4167
+ };
4168
+ return {
4169
+ enabled: Boolean(emit),
4170
+ start() {
4171
+ displayed.clear();
4172
+ ordinaryCommands = 1;
4173
+ lastUpdateAt = -Infinity;
4174
+ safeEmit({ type: "clearAll" });
4175
+ },
4176
+ update(snapshot, legalMoves) {
4177
+ const now = entryClockMs();
4178
+ if (now - lastUpdateAt < MIN_REFRESH_MS)
4179
+ return;
4180
+ lastUpdateAt = now;
4181
+ const selected = selectCandidateArrows(snapshot, legalMoves);
4182
+ const next = new Map(selected.map((arrow) => [
4183
+ `${arrow.from}-${arrow.to}`,
4184
+ arrow,
4185
+ ]));
4186
+ const diff = [];
4187
+ for (const [key, arrow] of displayed) {
4188
+ const replacement = next.get(key);
4189
+ if (!replacement) {
4190
+ diff.push({ type: "clearArrow", from: arrow.from, to: arrow.to, fadeMs: CANDIDATE_FADE_MS });
4191
+ }
4192
+ else if (replacement.intensity !== arrow.intensity) {
4193
+ diff.push({
4194
+ type: "drawArrow",
4195
+ from: replacement.from,
4196
+ to: replacement.to,
4197
+ intensity: replacement.intensity,
4198
+ fadeMs: CANDIDATE_FADE_MS,
4199
+ });
4200
+ }
4201
+ }
4202
+ for (const [key, arrow] of next) {
4203
+ if (!displayed.has(key)) {
4204
+ diff.push({
4205
+ type: "drawArrow",
4206
+ from: arrow.from,
4207
+ to: arrow.to,
4208
+ intensity: arrow.intensity,
4209
+ fadeMs: CANDIDATE_FADE_MS,
4210
+ });
4211
+ }
4212
+ }
4213
+ if (ordinaryCommands + diff.length > ORDINARY_COMMAND_LIMIT)
4214
+ return;
4215
+ for (const command of diff) {
4216
+ safeEmit(command);
4217
+ ordinaryCommands++;
4218
+ }
4219
+ displayed.clear();
4220
+ for (const [key, arrow] of next)
4221
+ displayed.set(key, arrow);
4222
+ },
4223
+ commit(move, position) {
4224
+ const san = typeof move === "string" ? move : move?.san;
4225
+ if (ordinaryCommands + displayed.size <= ORDINARY_COMMAND_LIMIT) {
4226
+ for (const arrow of displayed.values()) {
4227
+ safeEmit({
4228
+ type: "clearArrow",
4229
+ from: arrow.from,
4230
+ to: arrow.to,
4231
+ fadeMs: CANDIDATE_FADE_MS,
4232
+ });
4233
+ ordinaryCommands++;
4234
+ }
4235
+ displayed.clear();
4236
+ }
4237
+ const verbose = position && typeof position.moves === "function"
4238
+ ? position.moves({ verbose: true }).find((candidate) => candidate.san === san)
4239
+ : null;
4240
+ if (verbose) {
4241
+ safeEmit({
4242
+ type: "drawArrow",
4243
+ from: verbose.from,
4244
+ to: verbose.to,
4245
+ intensity: 1,
4246
+ fadeMs: FINAL_FADE_MS,
4247
+ });
4248
+ }
4249
+ return san;
4250
+ },
4251
+ };
4252
+ }
4253
+ export { selectCandidateArrows };
4254
  export function normalizeBookFen(fen) {
4255
  return fen.split(" ").slice(0, 4).join(" ");
4256
  }
 
4331
  async newGame( /* { random } */) {
4332
  const mcts = new MCTS(evaluator, new MCTSConfig({ nSimulations, timeBudgetMs, batchSize: MCTS_BATCH_SIZE }));
4333
  return {
4334
+ async chooseMove({ history, legalMoves, moveTimeLimitMs, thinking }) {
4335
  if (!legalMoves || legalMoves.length === 0) {
4336
  throw new Error("chooseMove called with no legalMoves");
4337
  }
4338
+ const startedAt = entryClockMs();
4339
+ const display = createThinkingDisplay(thinking);
4340
+ display.start();
4341
+ // moveTimeLimitMs is the runner's live per-move budget when
4342
+ // provided; margin it down so search itself stops with room to
4343
+ // encode/return before the runner's own clock (and definitely
4344
+ // before the 1.25x grace teardown) expires. Falls back to the
4345
+ // fixed timeBudgetMs (config override or DEFAULT_TIME_BUDGET_MS)
4346
+ // when the runner doesn't supply it.
4347
+ const searchTimeBudgetMs = Number.isFinite(moveTimeLimitMs) && moveTimeLimitMs > 0
4348
+ ? moveTimeLimitMs * MOVE_TIME_LIMIT_MARGIN
4349
+ : timeBudgetMs;
4350
+ let chess;
4351
  try {
4352
+ chess = new Chess();
4353
  for (const san of history) {
4354
  chess.move(san);
4355
  }
 
4359
  history: [...history],
4360
  fen: chess.fen(),
4361
  legalMoves: [...legalMoves],
4362
+ searchTimeBudgetMs,
4363
  });
4364
  const bookMove = history.length <= 9
4365
  ? bookMoveSan(chess, OPENING_BOOK, legalMoves)
 
4369
  type: "book_hit",
4370
  ply: history.length,
4371
  move: bookMove,
4372
+ elapsedMs: entryClockMs() - startedAt,
4373
  });
4374
+ return display.commit(bookMove, chess);
4375
+ }
4376
+ const searchOptions = { timeBudgetMs: searchTimeBudgetMs };
4377
+ if (display.enabled) {
4378
+ searchOptions.onRootUpdate = (snapshot) => display.update(snapshot, legalMoves);
4379
  }
4380
+ const chosen = await mcts.search(chess, searchOptions);
4381
+ const elapsedMs = entryClockMs() - startedAt;
4382
  // mcts.search() draws its move from chess.js's own legal-move
4383
  // list at the current position, which should always match the
4384
  // runner-provided legalMoves -- but if the runner's SAN
 
4394
  move: fallback,
4395
  elapsedMs,
4396
  });
4397
+ return display.commit(fallback, chess);
4398
  }
4399
  emitEvent(onEvent, {
4400
  type: "mcts_move",
 
4403
  elapsedMs,
4404
  simCount: mcts.lastSimCount,
4405
  });
4406
+ return display.commit(chosen, chess);
4407
  }
4408
  catch (error) {
4409
  const fallback = legalMoves[0];
 
4412
  reason: "exception",
4413
  error: String(error instanceof Error ? error.message : error),
4414
  move: fallback,
4415
+ elapsedMs: entryClockMs() - startedAt,
4416
  });
4417
+ return display.commit(fallback, chess);
4418
  }
4419
  },
4420
  async dispose() { },
browser/manifest.json CHANGED
@@ -3,8 +3,8 @@
3
  "name": "minichess-gpt-student-v1",
4
  "entrypoint": {
5
  "path": "entry.js",
6
- "bytes": 1004943,
7
- "sha256": "080962ccd9b71b7cf7cb8c1793e21ad27737e59002b1b9823bded14b54f6e88e"
8
  },
9
  "artifacts": {
10
  "model": {
@@ -13,5 +13,35 @@
13
  "sha256": "aad1bed67c2f3517d30f914bafe9d57f6c7f84194150daa5322f7081f3a1df0f"
14
  }
15
  },
16
- "config": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
 
3
  "name": "minichess-gpt-student-v1",
4
  "entrypoint": {
5
  "path": "entry.js",
6
+ "bytes": 1017367,
7
+ "sha256": "ff7875c9f06f6eda265dde2cd2f86811b4a449079c0c1e8fdc1a3d845bc73e8a"
8
  },
9
  "artifacts": {
10
  "model": {
 
13
  "sha256": "aad1bed67c2f3517d30f914bafe9d57f6c7f84194150daa5322f7081f3a1df0f"
14
  }
15
  },
16
+ "config": {
17
+ "release": {
18
+ "variant": "student-v1",
19
+ "modelSha256": "aad1bed67c2f3517d30f914bafe9d57f6c7f84194150daa5322f7081f3a1df0f",
20
+ "modelBytes": 5551177,
21
+ "sourceCommit": "cdedb20d8348148d9c02ec5ba6fe0d01958da81c",
22
+ "modelPath": "runs/student_distillation/student-v1/student.onnx",
23
+ "modelSource": "registry",
24
+ "modelContract": {
25
+ "validated": true,
26
+ "publishable": true,
27
+ "inputName": "tokens",
28
+ "inputType": "int64",
29
+ "sequenceLength": 69,
30
+ "outputShapes": {
31
+ "policy": [
32
+ 1,
33
+ 4272
34
+ ],
35
+ "wdl": [
36
+ 1,
37
+ 3
38
+ ],
39
+ "uncertainty": [
40
+ 1,
41
+ 1
42
+ ]
43
+ }
44
+ }
45
+ }
46
+ }
47
  }