// 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 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