techaaditya commited on
Commit
ea88929
·
verified ·
1 Parent(s): 1f0caba

Restore original Space files and editable Bot API URL

Browse files
Files changed (4) hide show
  1. README.md +18 -12
  2. bagh_engine.zip +2 -2
  3. index.html +32 -71
  4. pyodide_boot.js +12 -25
README.md CHANGED
@@ -11,18 +11,24 @@ short_description: Play Baghchal in-browser vs a tablebase-checked engine
11
 
12
  # Baghchal — Tigers & Goats
13
 
14
- The default opponent is **hosted Venom**, served over HTTPS from the Oracle VM:
 
 
15
 
16
- - Bot API: https://venom.129.159.173.183.sslip.io/
17
- - Health: https://venom.129.159.173.183.sslip.io/health
 
 
18
 
19
- Venom uses full-game WDL, distance to win, trap density and phase evaluation.
20
- The board requests Venom explicitly and checks that replies identify the selected bot.
21
- If the connection fails, the move stops with a visible error. It never silently plays a
22
- browser engine in place of Venom. The old HTTP VM URL is upgraded to HTTPS on this page
23
- because browsers block HTTP API requests from HTTPS pages.
24
 
25
- To use browser engines offline, clear the Bot API field and select a local engine.
26
- Python search bots run through Pyodide; they do not have the hosted solved database.
27
- The API response panel uses the configured hosted bot, or an explicitly local Python
28
- preview when no API URL is configured. This static Space itself is not a bot API endpoint.
 
 
 
11
 
12
  # Baghchal — Tigers & Goats
13
 
14
+ Play the Nepali board game against a real engine. Everything runs **in your browser**: legal-move
15
+ generation, the OBX board notation, and the engine ladder from `pebble` (random) up to `phantom`
16
+ (PVS + LMR search).
17
 
18
+ **How strong is `phantom`?** Baghchal is a solved game — it is a draw with best play — so every
19
+ move can be checked against the truth. Over 400 sampled positions `phantom` plays a value-optimal
20
+ move **90.3%** of the time *on positions where the choice actually matters*: the ones where at
21
+ most half the legal moves hold the game value. A uniformly random player scores **20.6%** there.
22
 
23
+ That qualifier is the whole point. The unrestricted "agreement with perfect play" figure is
24
+ **96.0%** for `phantom` — but **64.1%** for a bot playing at random, because in a drawn game
25
+ almost any move preserves the draw. Quoting the unrestricted number alone makes a listless engine
26
+ look nearly perfect, so both are published here. (Reproduce with `python -m bagh_bench.agreement
27
+ --states 400 --time-ms 300 --bots pebble,shepherd,hunter,stalker,phantom,venom`.)
28
 
29
+ There is no server. This is a *static* Space; the Python engine is CPython compiled to WebAssembly
30
+ via [Pyodide](https://pyodide.org), loaded on your first bot move. If Pyodide cannot start, the
31
+ board falls back to a lighter JavaScript engine so the game always works.
32
+
33
+ Set a **Bot API** URL in the side panel to use a hosted engine instead — that is the only way to
34
+ reach the *perfect* tablebase bot, which needs an 8.7 GB solved database no browser can hold.
bagh_engine.zip CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9c541fe2641f44236f14ce033b2064e688942deae2445b8da2bf646ac282cc87
3
- size 1022769
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:04e2bf1382a2e42bf4aed0a7b299638890d1dffa392294e702b614aee8a4f10a
3
+ size 1021420
index.html CHANGED
@@ -277,9 +277,8 @@
277
  </div>
278
  <div class="ctl-line">
279
  <label for="serverUrl" title="Optional: point at a running bagh-server for named bots + exact tablebase / venom analysis">Bot&nbsp;API</label>
280
- <input type="text" id="serverUrl" value="http://129.159.173.183:8080" placeholder="http://129.159.173.183:8080">
281
  </div>
282
- <div id="apiStatus" class="src" role="status" aria-live="polite">Connecting to Bot API…</div>
283
  </div>
284
 
285
  <div class="card">
@@ -595,7 +594,7 @@ let analyzeTimer = null;
595
  function analyze() {
596
  refreshPlacementHints();
597
  render();
598
- const url = serverBase();
599
  clearTimeout(analyzeTimer);
600
  analyzeTimer = setTimeout(() => (url ? analyzeServer(url) : analyzeLocal()), 60);
601
  }
@@ -644,8 +643,8 @@ async function analyzeServer(url) {
644
  setEvalBar(top ? (top.value === "WIN" ? 9999 : top.value === "LOSS" ? -9999 : 0) * (state.turn === "T" ? 1 : -1) : 0);
645
  document.getElementById("pv").textContent = "server analysis — see ranking";
646
  } catch (e) {
 
647
  analyzeLocal();
648
- src.textContent = "· local analysis only (Bot API unreachable)";
649
  }
650
  }
651
  function renderMoves(view) {
@@ -668,7 +667,14 @@ function renderMoves(view) {
668
 
669
  // ---------- controls ----------
670
  document.getElementById("btnEngine").addEventListener("click", () => {
671
- doOneBotMove(document.getElementById("playBot").value || (serverBase() ? "venom" : LOCAL));
 
 
 
 
 
 
 
672
  });
673
  document.getElementById("btnUndo").addEventListener("click", () => {
674
  if (history.length) { state = history.pop(); lastMove = null; selected = null; destMap.clear(); analyze(); }
@@ -710,46 +716,25 @@ let botThinking = false;
710
  let watchRunning = false;
711
 
712
  const $ = id => document.getElementById(id);
713
- const SECURE_BOT_API = "https://venom.129.159.173.183.sslip.io";
714
- const serverBase = () => {
715
- let val = $("serverUrl").value.trim().replace(/\/$/, "");
716
- if (window.location.protocol === "https:" && /^http:\/\/129\.159\.173\.183:8080$/i.test(val)) {
717
- val = SECURE_BOT_API;
718
- $("serverUrl").value = val;
719
- }
720
- return val;
721
- };
722
- function resolveMoveStr(s, text) {
723
- if (typeof text !== "string") return null;
724
- const token = text.trim().replace(/^m/i, "");
725
- for (const m of legalMoves(s)) {
726
- const wire = m.kind === "place" ? coord(m.dst)
727
- : coord(m.src) + coord(m.dst) + (m.kind === "capture" ? "(" + coord(m.over) + ")" : "");
728
- if (moveStr(m) === token || wire === token) return m;
729
- }
730
- return null;
731
- }
732
 
733
- // Hosted selections must answer as the requested bot. Connection failures never play locally.
734
  async function botChooseMove(botName) {
735
  const base = serverBase();
736
  if (base && botName && botName !== LOCAL) {
 
737
  const res = await fetch(base + "/move", {
738
  method: "POST", headers: { "Content-Type": "application/json" },
739
- body: JSON.stringify({ obx: serialize(state), bot: botName, time_ms: 1000 }),
740
- signal: AbortSignal.timeout(15000)
741
  });
742
- if (!res.ok) throw new Error("Bot API returned HTTP " + res.status);
743
- const data = await res.json();
744
- if (data.bot !== botName || (["venom", "oracle"].includes(botName) && data.source !== "tablebase")) {
745
- throw new Error("Bot API did not return the selected " + botName + " policy");
746
  }
747
- const m = resolveMoveStr(state, data.algebraic) || resolveMoveStr(state, data.move);
748
- if (!m) throw new Error("Bot API returned no legal move");
749
- return m;
750
  }
751
- if (botName && botName !== LOCAL) throw new Error("Set a Bot API URL to use " + botName);
752
- // Only an explicit Local selection runs the JavaScript engine.
753
  const depth = +$("depth").value;
754
  const rows = rankMoves(state, depth);
755
  if (!rows.length) return null;
@@ -760,21 +745,11 @@ async function botChooseMove(botName) {
760
  async function doOneBotMove(botName) {
761
  if (winner(state) || botThinking) return false;
762
  botThinking = true; render();
763
- const position = serialize(state);
764
- try {
765
- const m = await botChooseMove(botName);
766
- if (!m || serialize(state) !== position) return false;
767
- botThinking = false;
768
- doMove(m);
769
- return true;
770
- } catch (e) {
771
- setPlayStatus("Bot move stopped: " + e.message + ". No substitute engine was played.");
772
- const status = $("apiStatus");
773
- if (status) status.textContent = "Bot connection failed: " + e.message;
774
- return false;
775
- } finally {
776
- botThinking = false;
777
- }
778
  }
779
 
780
  // Called at the end of every move. Drives the opponent in 'play' mode.
@@ -802,8 +777,7 @@ function updateWatchBtn() {
802
  }
803
  function setPlayStatus(msg, thinking) {
804
  const el = $("playStatus");
805
- if (thinking) el.innerHTML = `<span class="think"><span class="spin"></span>${msg}</span>`;
806
- else el.textContent = msg;
807
  }
808
 
809
  function setMode(m) {
@@ -818,25 +792,12 @@ function setMode(m) {
818
  async function fetchBots() {
819
  const selects = [$("playBot"), $("tigerBot"), $("goatBot")];
820
  let opts = [{ name: LOCAL, title: "Local engine (in-browser)" }];
821
- let serverDefault = "venom";
822
  const base = serverBase();
823
  if (base) {
824
- opts = opts.concat(["venom", "oracle", "phantom"].map(name => ({name, title: name + " (hosted)"})));
825
  try {
826
- const r = await fetch(base + "/bots", {signal: AbortSignal.timeout(15000)});
827
- if (!r.ok) throw new Error("HTTP " + r.status);
828
- if (r.ok) {
829
- const d = await r.json();
830
- if (d.default) serverDefault = d.default;
831
- opts = opts.slice(0, 1).concat(d.bots.map(b => ({
832
- name: b.name,
833
- title: (b.title || b.name) + (b.algorithm ? ` — ${b.algorithm}` : (b.desc ? ` — ${b.desc}` : ""))
834
- })));
835
- }
836
- if ($("apiStatus")) $("apiStatus").textContent = "Connected to Bot API · default " + serverDefault;
837
- } catch (e) {
838
- if ($("apiStatus")) $("apiStatus").textContent = "Bot API unreachable. Hosted moves are paused; check the connection.";
839
- }
840
  }
841
  for (const sel of selects) {
842
  const cur = sel.value;
@@ -844,8 +805,8 @@ async function fetchBots() {
844
  for (const o of opts) { const el = document.createElement("option"); el.value = o.name; el.textContent = o.title; sel.appendChild(el); }
845
  if (cur && opts.some(o => o.name === cur)) sel.value = cur;
846
  }
847
- // Default to the server's default bot (Venom) or strongest available.
848
- const strongest = (serverDefault && opts.some(o => o.name === serverDefault)) ? serverDefault : opts[opts.length - 1].name;
849
  if (!$("playBot").value || $("playBot").value === LOCAL) $("playBot").value = strongest;
850
  if (!$("tigerBot").value || $("tigerBot").value === LOCAL) $("tigerBot").value = strongest;
851
  if (!$("goatBot").value || $("goatBot").value === LOCAL) $("goatBot").value = strongest;
 
277
  </div>
278
  <div class="ctl-line">
279
  <label for="serverUrl" title="Optional: point at a running bagh-server for named bots + exact tablebase / venom analysis">Bot&nbsp;API</label>
280
+ <input type="text" id="serverUrl" placeholder="http://127.0.0.1:8080 (optional)">
281
  </div>
 
282
  </div>
283
 
284
  <div class="card">
 
594
  function analyze() {
595
  refreshPlacementHints();
596
  render();
597
+ const url = document.getElementById("serverUrl").value.trim();
598
  clearTimeout(analyzeTimer);
599
  analyzeTimer = setTimeout(() => (url ? analyzeServer(url) : analyzeLocal()), 60);
600
  }
 
643
  setEvalBar(top ? (top.value === "WIN" ? 9999 : top.value === "LOSS" ? -9999 : 0) * (state.turn === "T" ? 1 : -1) : 0);
644
  document.getElementById("pv").textContent = "server analysis — see ranking";
645
  } catch (e) {
646
+ src.textContent = "· local eval (server unreachable)";
647
  analyzeLocal();
 
648
  }
649
  }
650
  function renderMoves(view) {
 
667
 
668
  // ---------- controls ----------
669
  document.getElementById("btnEngine").addEventListener("click", () => {
670
+ if (winner(state)) return;
671
+ const depth = +document.getElementById("depth").value;
672
+ const rows = rankMoves(state, depth);
673
+ if (rows.length) {
674
+ const best = rows[0].val;
675
+ const ties = rows.filter(r => r.val === best);
676
+ doMove(ties[Math.floor(Math.random() * ties.length)].m);
677
+ }
678
  });
679
  document.getElementById("btnUndo").addEventListener("click", () => {
680
  if (history.length) { state = history.pop(); lastMove = null; selected = null; destMap.clear(); analyze(); }
 
716
  let watchRunning = false;
717
 
718
  const $ = id => document.getElementById(id);
719
+ const serverBase = () => $("serverUrl").value.trim().replace(/\/$/, "");
720
+ function resolveMoveStr(s, text) { for (const m of legalMoves(s)) if (moveStr(m) === text) return m; return null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
 
722
+ // Ask a bot for a move: the named server bot if a Bot API is set, else the local JS engine.
723
  async function botChooseMove(botName) {
724
  const base = serverBase();
725
  if (base && botName && botName !== LOCAL) {
726
+ try {
727
  const res = await fetch(base + "/move", {
728
  method: "POST", headers: { "Content-Type": "application/json" },
729
+ body: JSON.stringify({ obx: serialize(state), bot: botName, time_ms: 1000 })
 
730
  });
731
+ if (res.ok) {
732
+ const data = await res.json();
733
+ if (data.move) { const m = resolveMoveStr(state, data.move); if (m) return m; }
 
734
  }
735
+ } catch (e) { /* fall through to local engine */ }
 
 
736
  }
737
+ // Local fallback: depth-limited negamax, random among ties.
 
738
  const depth = +$("depth").value;
739
  const rows = rankMoves(state, depth);
740
  if (!rows.length) return null;
 
745
  async function doOneBotMove(botName) {
746
  if (winner(state) || botThinking) return false;
747
  botThinking = true; render();
748
+ const m = await botChooseMove(botName);
749
+ botThinking = false;
750
+ if (!m) return false;
751
+ doMove(m); // doMove -> render + analyze + scheduleAuto
752
+ return true;
 
 
 
 
 
 
 
 
 
 
753
  }
754
 
755
  // Called at the end of every move. Drives the opponent in 'play' mode.
 
777
  }
778
  function setPlayStatus(msg, thinking) {
779
  const el = $("playStatus");
780
+ el.innerHTML = thinking ? `<span class="think"><span class="spin"></span>${msg}</span>` : msg;
 
781
  }
782
 
783
  function setMode(m) {
 
792
  async function fetchBots() {
793
  const selects = [$("playBot"), $("tigerBot"), $("goatBot")];
794
  let opts = [{ name: LOCAL, title: "Local engine (in-browser)" }];
 
795
  const base = serverBase();
796
  if (base) {
 
797
  try {
798
+ const r = await fetch(base + "/bots");
799
+ if (r.ok) { const d = await r.json(); opts = opts.concat(d.bots.map(b => ({ name: b.name, title: `${b.title} — ${b.algorithm}` }))); }
800
+ } catch (e) { /* server offline: keep Local only */ }
 
 
 
 
 
 
 
 
 
 
 
801
  }
802
  for (const sel of selects) {
803
  const cur = sel.value;
 
805
  for (const o of opts) { const el = document.createElement("option"); el.value = o.name; el.textContent = o.title; sel.appendChild(el); }
806
  if (cur && opts.some(o => o.name === cur)) sel.value = cur;
807
  }
808
+ // Default to the strongest available bot (last in the roster) for opponents.
809
+ const strongest = opts[opts.length - 1].name;
810
  if (!$("playBot").value || $("playBot").value === LOCAL) $("playBot").value = strongest;
811
  if (!$("tigerBot").value || $("tigerBot").value === LOCAL) $("tigerBot").value = strongest;
812
  if (!$("goatBot").value || $("goatBot").value === LOCAL) $("goatBot").value = strongest;
pyodide_boot.js CHANGED
@@ -33,7 +33,7 @@
33
  { name: "shepherd", title: "Shepherd - greedy", hint: "One-ply greedy heuristic (70.2%)" },
34
  { name: "hunter", title: "Hunter - negamax", hint: "Negamax, depth 3 (80.9%)" },
35
  { name: "stalker", title: "Stalker - TT search", hint: "Alpha-beta + transposition table (89.4%)" },
36
- { name: "phantom", title: "Phantom - local PVS", hint: "Browser search; does not use the hosted Venom tablebase" },
37
  ];
38
  var PY_NAMES = PY_BOTS.map(function (b) { return b.name; });
39
 
@@ -147,13 +147,11 @@
147
  if (m) return m;
148
  }
149
  } catch (err) {
150
- console.warn("[bagh] Python engine unavailable:", err);
151
- status("Python engine unavailable - move stopped", "bad");
152
  ready = Promise.reject(err); // do not retry the boot on every move
153
  ready.catch(function () {});
154
- throw err;
155
  }
156
- throw new Error("Local Python bot returned no legal move");
157
  }
158
  return original(botName);
159
  };
@@ -171,8 +169,9 @@
171
  card.className = "card";
172
  card.innerHTML =
173
  '<h2>API response <span style="opacity:.6;font-weight:400">· preview</span></h2>' +
174
- '<div class="src" style="margin-bottom:8px">Uses your configured Bot API and selected bot.' +
175
- ' With no API URL, this is an explicitly local Python preview.</div>' +
 
176
  '<textarea id="apiObx" rows="2" spellcheck="false" style="width:100%;min-width:0;' +
177
  'box-sizing:border-box;font-family:var(--mono);font-size:11.5px;padding:7px 9px;' +
178
  'border-radius:7px;border:1px solid var(--panel-2);background:var(--bg);color:var(--ink)">' +
@@ -188,36 +187,24 @@
188
  var out = document.getElementById("apiOut");
189
  var note = document.getElementById("apiNote");
190
  var bot = (document.getElementById("playBot") || {}).value;
 
191
  out.textContent = "thinking...";
192
  note.textContent = "";
193
  try {
 
194
  var obx = document.getElementById("apiObx").value;
195
  var t0 = performance.now();
196
- var base = serverBase();
197
- if (base) {
198
- var response = await fetch(base + "/move", {
199
- method: "POST", headers: {"Content-Type": "application/json"},
200
- body: JSON.stringify({obx: obx, bot: bot, time_ms: THINK_MS}),
201
- signal: AbortSignal.timeout(15000)
202
- });
203
- if (!response.ok) throw new Error("Bot API HTTP " + response.status);
204
- out.textContent = JSON.stringify(await response.json(), null, 2);
205
- } else {
206
- if (PY_NAMES.indexOf(bot) === -1) throw new Error("Select a local Python bot for an offline preview");
207
- if (!ready) ready = boot();
208
- var py = await ready;
209
- out.textContent = py.globals.get("bagh_api")(obx, bot, THINK_MS);
210
- }
211
- note.textContent = bot + (base ? " · hosted · " : " · local · ") + Math.round(performance.now() - t0) + "ms";
212
  } catch (err) {
213
- out.textContent = "Request failed: " + err;
214
  }
215
  });
216
  }
217
 
218
  // Start downloading Pyodide now rather than on the first move: it is ~10 MB, and paying that
219
  // mid-game would look like the engine hanging. Failures are handled in botChooseMove.
220
- if (!serverBase() && !ready) {
221
  ready = boot();
222
  ready.catch(function () {});
223
  }
 
33
  { name: "shepherd", title: "Shepherd - greedy", hint: "One-ply greedy heuristic (70.2%)" },
34
  { name: "hunter", title: "Hunter - negamax", hint: "Negamax, depth 3 (80.9%)" },
35
  { name: "stalker", title: "Stalker - TT search", hint: "Alpha-beta + transposition table (89.4%)" },
36
+ { name: "phantom", title: "Phantom - PVS (96%)", hint: "PVS + LMR; 95.7% optimal on critical positions" },
37
  ];
38
  var PY_NAMES = PY_BOTS.map(function (b) { return b.name; });
39
 
 
147
  if (m) return m;
148
  }
149
  } catch (err) {
150
+ console.warn("[bagh] Python engine unavailable, using the JS fallback:", err);
151
+ status("Python engine unavailable - using JS fallback", "bad");
152
  ready = Promise.reject(err); // do not retry the boot on every move
153
  ready.catch(function () {});
 
154
  }
 
155
  }
156
  return original(botName);
157
  };
 
169
  card.className = "card";
170
  card.innerHTML =
171
  '<h2>API response <span style="opacity:.6;font-weight:400">· preview</span></h2>' +
172
+ '<div class="src" style="margin-bottom:8px">The exact JSON the Bot API returns, produced by' +
173
+ ' the server\'s own code running here in your browser. A static Space cannot be called by' +
174
+ ' another site — point baghchal.net at a hosted API.</div>' +
175
  '<textarea id="apiObx" rows="2" spellcheck="false" style="width:100%;min-width:0;' +
176
  'box-sizing:border-box;font-family:var(--mono);font-size:11.5px;padding:7px 9px;' +
177
  'border-radius:7px;border:1px solid var(--panel-2);background:var(--bg);color:var(--ink)">' +
 
187
  var out = document.getElementById("apiOut");
188
  var note = document.getElementById("apiNote");
189
  var bot = (document.getElementById("playBot") || {}).value;
190
+ if (PY_NAMES.indexOf(bot) === -1) bot = "phantom";
191
  out.textContent = "thinking...";
192
  note.textContent = "";
193
  try {
194
+ var py = await ready;
195
  var obx = document.getElementById("apiObx").value;
196
  var t0 = performance.now();
197
+ out.textContent = py.globals.get("bagh_api")(obx, bot, THINK_MS);
198
+ note.textContent = bot + " · " + Math.round(performance.now() - t0) + "ms";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  } catch (err) {
200
+ out.textContent = "Python engine unavailable: " + err;
201
  }
202
  });
203
  }
204
 
205
  // Start downloading Pyodide now rather than on the first move: it is ~10 MB, and paying that
206
  // mid-game would look like the engine hanging. Failures are handled in botChooseMove.
207
+ if (!ready) {
208
  ready = boot();
209
  ready.catch(function () {});
210
  }