Spaces:
Running
Running
File size: 10,902 Bytes
a86c764 66073f0 a86c764 f454219 a86c764 66073f0 ea88929 a86c764 ea88929 a86c764 ea88929 a86c764 ea88929 a86c764 ea88929 a86c764 ea88929 a86c764 ea88929 a86c764 ea88929 a86c764 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | // Runs the REAL Python engines in the browser, on a Hugging Face *static* Space.
//
// A static Space serves files and runs nothing server-side -- but Pyodide (CPython compiled to
// WebAssembly) executes Python in the visitor's tab. Our engine ladder is pure standard library
// (no numpy, no C extensions), so it loads as-is from a zip; `phantom` here is the real PVS search,
// rather than the page's lighter JS negamax.
//
// Percentages below are agreement with the solved game on *critical* positions -- those where at
// most half the legal moves preserve the value. The unrestricted figure is not quoted because a
// random bot scores 73.5% on it (in a drawn game nearly any move holds the draw), so it flatters
// every engine and separates none of them. On critical positions random scores 27.7%.
//
// This file only *wraps* the page: it overrides botChooseMove to try Python first and defer to the
// original JS engine on any failure. If Pyodide is blocked, slow, or broken, the board still plays.
(function () {
var PYODIDE = "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js";
var THINK_MS = 1200;
var ready = null; // Promise<pyodide>
var badge;
// The page builds its bot list from the Bot API, so with no server configured it offers only
// "Local engine (in-browser)" -- the JS negamax. These are the Python engines we add in that
// case. This is the classical ladder, weakest to strongest, and it stops at phantom.
//
// Deliberately absent: oracle/venom, because without the 8.7 GB solve they silently degrade to
// search and listing them would promise perfect play the browser cannot give; and sage/zero,
// because they pull in the AlphaZero package, which no longer ships in the zip at all.
//
// Keep these SHORT. The side panel is a fixed 360px column, so a long label overflows the card.
// `hint` carries the detail as a hover tooltip, which costs no width.
var PY_BOTS = [
{ name: "pebble", title: "Pebble - random", hint: "Random legal move (27.7% on critical positions)" },
{ name: "shepherd", title: "Shepherd - greedy", hint: "One-ply greedy heuristic (70.2%)" },
{ name: "hunter", title: "Hunter - negamax", hint: "Negamax, depth 3 (80.9%)" },
{ name: "stalker", title: "Stalker - TT search", hint: "Alpha-beta + transposition table (89.4%)" },
{ name: "phantom", title: "Phantom - PVS (96%)", hint: "PVS + LMR; 95.7% optimal on critical positions" },
];
var PY_NAMES = PY_BOTS.map(function (b) { return b.name; });
function status(text, tone) {
if (!badge) {
badge = document.createElement("div");
badge.style.cssText =
"position:fixed;right:12px;bottom:12px;z-index:9999;padding:6px 10px;border-radius:6px;" +
"font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;pointer-events:none;" +
"background:rgba(20,20,24,.92);color:#e8e4d9;border:1px solid rgba(255,255,255,.14)";
document.body.appendChild(badge);
}
badge.textContent = text;
badge.style.color = tone === "bad" ? "#ffb4a2" : tone === "good" ? "#b8e0b0" : "#e8e4d9";
if (tone === "good") setTimeout(function () { badge.style.opacity = "0.55"; }, 2500);
}
function loadScript(src) {
return new Promise(function (res, rej) {
var s = document.createElement("script");
s.src = src; s.onload = res; s.onerror = function () { rej(new Error("load failed: " + src)); };
document.head.appendChild(s);
});
}
async function boot() {
status("loading Python engine...");
await loadScript(PYODIDE);
var py = await loadPyodide();
status("unpacking engines...");
var resp = await fetch("bagh_engine.zip");
if (!resp.ok) throw new Error("bagh_engine.zip missing (" + resp.status + ")");
await py.unpackArchive(await resp.arrayBuffer(), "zip");
py.runPython([
"import sys, random",
"sys.path.insert(0, '/home/pyodide')",
"from bagh_core.obx import parse",
"from bagh_engine import roster",
"_rng = random.Random()",
"_cache = {}",
"def bagh_choose(obx, bot, time_ms):",
" eng = _cache.get(bot)",
" if eng is None:",
" eng = _cache[bot] = roster.build(bot)",
" if getattr(eng, 'time_limit', None) is not None:",
" eng.time_limit = time_ms / 1000.0",
" st = parse(obx).state",
" if st.winner() is not None:",
" return ''",
" return str(eng.choose(st, _rng))",
// The API preview runs the deployed server's OWN response builder, so what it shows is the
// real contract rather than a re-implementation that could drift from it.
"import json as _json",
"from bagh_server.botapi import bot_reply",
"from bagh_server.server import BotService",
"_svc = None",
"def bagh_api(obx, bot, time_ms):",
" global _svc",
" if _svc is None:",
" _svc = BotService(roster.build(bot))",
" try:",
" payload = bot_reply(_svc, obx.strip(), side=None, time_ms=time_ms,",
" tiger_bot=bot, goat_bot=bot)",
" except Exception as exc:",
" return _json.dumps({'error': str(exc), 'obx': ''}, indent=2)",
" return _json.dumps(payload, indent=2)",
].join("\n"));
status("Python engine ready", "good");
return py;
}
// --- offer the Python engines in the bot pickers ------------------------------------------
var origFetchBots = window.fetchBots;
if (typeof origFetchBots === "function") {
window.fetchBots = async function () {
await origFetchBots.apply(this, arguments);
// A real Bot API supersedes us -- it may have the tablebase, which no browser can hold.
if ((typeof serverBase === "function") && serverBase()) return;
["playBot", "tigerBot", "goatBot"].forEach(function (id) {
var sel = document.getElementById(id);
if (!sel) return;
var had = sel.value;
PY_BOTS.forEach(function (b) {
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === b.name) return;
}
var el = document.createElement("option");
el.value = b.name; el.textContent = b.title; el.title = b.hint;
sel.appendChild(el);
});
// Default to the real engine rather than the JS fallback.
sel.value = (had && had !== LOCAL) ? had : "phantom";
});
};
window.fetchBots(); // the page already ran the original; re-run to append our options
}
var original = window.botChooseMove;
if (typeof original !== "function") return; // page changed shape; leave it alone
window.botChooseMove = async function (botName) {
// A configured Bot API always wins: it may have the tablebase, which the browser cannot.
var usingServer = (typeof serverBase === "function") && serverBase();
if (!usingServer && PY_NAMES.indexOf(botName) !== -1) {
try {
if (!ready) ready = boot();
var py = await ready;
var mv = py.globals.get("bagh_choose")(serialize(state), botName, THINK_MS);
if (mv) {
var m = resolveMoveStr(state, mv); // moveStr() matches Python's Move.__str__ exactly
if (m) return m;
}
} catch (err) {
console.warn("[bagh] Python engine unavailable, using the JS fallback:", err);
status("Python engine unavailable - using JS fallback", "bad");
ready = Promise.reject(err); // do not retry the boot on every move
ready.catch(function () {});
}
}
return original(botName);
};
// --- API response preview -------------------------------------------------------------------
// Runs the deployed server's own bot_reply() in the browser, so you can see the exact JSON the
// Bot API returns for a position. It is a PREVIEW, not an endpoint: a static Space serves files
// and runs no server, so baghchal.net cannot call this page -- it has to call a hosted API.
function apiPanel() {
// The side column is <aside class="panel">. Never fall back to .layout: it is the grid
// itself, so appending there would add a third column and break the page.
var host = document.querySelector("aside.panel");
if (!host) return;
var card = document.createElement("div");
card.className = "card";
card.innerHTML =
'<h2>API response <span style="opacity:.6;font-weight:400">· preview</span></h2>' +
'<div class="src" style="margin-bottom:8px">The exact JSON the Bot API returns, produced by' +
' the server\'s own code running here in your browser. A static Space cannot be called by' +
' another site — point baghchal.net at a hosted API.</div>' +
'<textarea id="apiObx" rows="2" spellcheck="false" style="width:100%;min-width:0;' +
'box-sizing:border-box;font-family:var(--mono);font-size:11.5px;padding:7px 9px;' +
'border-radius:7px;border:1px solid var(--panel-2);background:var(--bg);color:var(--ink)">' +
'TXXXT/XXXXX/XXXXX/XXXXX/TXXXT g @20 c0 -</textarea>' +
'<div class="ctl-line"><button id="apiSend" class="act primary" type="button">Send</button>' +
'<span id="apiNote" class="src" style="margin:0"></span></div>' +
'<pre id="apiOut" style="white-space:pre-wrap;word-break:break-word;overflow-x:auto;' +
'font-size:11.5px;margin-top:10px;padding:9px;border-radius:7px;background:var(--bg);' +
'border:1px solid var(--panel-2)">—</pre>';
host.appendChild(card);
document.getElementById("apiSend").addEventListener("click", async function () {
var out = document.getElementById("apiOut");
var note = document.getElementById("apiNote");
var bot = (document.getElementById("playBot") || {}).value;
if (PY_NAMES.indexOf(bot) === -1) bot = "phantom";
out.textContent = "thinking...";
note.textContent = "";
try {
var py = await ready;
var obx = document.getElementById("apiObx").value;
var t0 = performance.now();
out.textContent = py.globals.get("bagh_api")(obx, bot, THINK_MS);
note.textContent = bot + " · " + Math.round(performance.now() - t0) + "ms";
} catch (err) {
out.textContent = "Python engine unavailable: " + err;
}
});
}
// Start downloading Pyodide now rather than on the first move: it is ~10 MB, and paying that
// mid-game would look like the engine hanging. Failures are handled in botChooseMove.
if (!ready) {
ready = boot();
ready.catch(function () {});
}
try { apiPanel(); } catch (e) { console.warn("[bagh] API panel not added:", e); }
})();
|