Spaces:
Running
Running
Download pyodide_boot.js from techaaditya/BaghchalAI: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/techaaditya/BaghchalAI/resolve/main/pyodide_boot.js
- Command line
-
hf download hf://spaces/techaaditya/BaghchalAI/pyodide_boot.js
-
curl -L -o pyodide_boot.js https://huggingface.co/spaces/techaaditya/BaghchalAI/resolve/main/pyodide_boot.js
10.9 kB
| // 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); } | |
| })(); | |