Spaces:
Running
Running
Restore original Space files and editable Bot API URL
Browse files- README.md +18 -12
- bagh_engine.zip +2 -2
- index.html +32 -71
- 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 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 API</label>
|
| 280 |
-
<input type="text" id="serverUrl"
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 714 |
-
const
|
| 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 |
-
//
|
| 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 (
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
throw new Error("Bot API did not return the selected " + botName + " policy");
|
| 746 |
}
|
| 747 |
-
|
| 748 |
-
if (!m) throw new Error("Bot API returned no legal move");
|
| 749 |
-
return m;
|
| 750 |
}
|
| 751 |
-
|
| 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
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 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 |
-
|
| 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"
|
| 827 |
-
if (
|
| 828 |
-
|
| 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
|
| 848 |
-
const strongest =
|
| 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 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 -
|
| 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 -
|
| 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">
|
| 175 |
-
'
|
|
|
|
| 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 |
-
|
| 197 |
-
|
| 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 = "
|
| 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 (!
|
| 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 |
}
|