"""Bestemshe Perfect-Play Explorer. A custom HTML/JS front end (multilingual, keyboard-driven, rewritable history) talks to the C++ tablebase oracle through a hidden Gradio bridge. Keeping the UI client-side lets us do global keyboard handling, arrow navigation, clickable history and language switching that Gradio's own components cannot express. All user-facing copy, flags and language names live in i18n.py. """ import json import os import subprocess import gradio as gr from i18n import LANGUAGES, TRANSLATIONS # CPU-only app. If the Space is provisioned on ZeroGPU hardware its startup # check demands a @spaces.GPU function; this dummy probe (never called) satisfies # it while all real work runs on the always-available CPU. try: import spaces @spaces.GPU def _zerogpu_probe(): return None except ImportError: pass QUERY_BIN = os.environ.get("BESTEMSHE_QUERY_BIN", "./query") DATA_DIR = os.environ.get("BESTEMSHE_DATA_DIR", "layers/compressed") TABLEBASE_DATASET = os.environ.get("BESTEMSHE_DATASET", "ansarzeinulla/bestemshe-tablebase") def ensure_tablebase(): """The 8.96GB tablebase lives in a dataset repo (Space repos are capped at 1GB).""" if os.path.isdir(DATA_DIR) and any(f.endswith(".bin") for f in os.listdir(DATA_DIR)): return from huggingface_hub import snapshot_download print(f"[setup] downloading tablebase from {TABLEBASE_DATASET} ...") snapshot_download(repo_id=TABLEBASE_DATASET, repo_type="dataset", local_dir=".") print("[setup] tablebase ready.") def ensure_query_binary(): """On HF Gradio Spaces there is no Docker build step: compile at startup.""" if os.path.isfile(QUERY_BIN) and os.access(QUERY_BIN, os.X_OK): return print("[setup] compiling query CLI...") subprocess.run( ["g++", "-std=c++17", "-O3", "-DNDEBUG", "query.cpp", "-o", "query", "-lzstd"], check=True, ) print("[setup] query CLI ready.") def parse_fields(text): parts = text.split() if len(parts) != 12: raise ValueError("Enter 12 numbers.") try: vals = [int(p) for p in parts] except ValueError: raise ValueError("All values must be whole numbers.") if any(v < 0 or v > 50 for v in vals): raise ValueError("Each number must be between 0 and 50.") if sum(vals) != 50: raise ValueError(f"The stones must total 50 (yours total {sum(vals)}).") if vals[0] % 2 or vals[1] % 2: raise ValueError("Kazan totals must be even.") if vals[0] > 24 or vals[1] > 24: raise ValueError("A kazan of 26+ means the game is already over.") return vals def run_query(state_str): env = dict(os.environ, BESTEMSHE_DATA_DIR=DATA_DIR) proc = subprocess.run( [QUERY_BIN] + state_str.split(), capture_output=True, text=True, timeout=60, env=env, ) return json.loads(proc.stdout) def oracle_bridge(payload): """payload = 'nonce|K1 K2 p0..p9'. Returns JSON string with the same nonce.""" nonce, _, state = payload.partition("|") try: parse_fields(state) data = run_query(state) if "error" in data: return json.dumps({"nonce": nonce, "ok": False, "error": data["error"]}) return json.dumps({"nonce": nonce, "ok": True, "data": data}) except ValueError as e: return json.dumps({"nonce": nonce, "ok": False, "error": str(e)}) except Exception as e: # noqa: BLE001 return json.dumps({"nonce": nonce, "ok": False, "error": str(e)}) # --------------------------------------------------------------------------- # # Front end # --------------------------------------------------------------------------- # LANG_OPTIONS = "".join( f'' for l in LANGUAGES ) INDEX_MARKUP = r"""

""".replace("__LANG_OPTIONS__", LANG_OPTIONS) INDEX_JS = r""" () => { if (window.__bbInit) return; window.__bbInit = true; const DEFAULT_FEN = "5,5,5,5,5/5,5,5,5,5 0,0 w 1"; const I18N = __I18N_JSON__; const SITE = "https://huggingface.co/spaces/ansarzeinulla/Bestemshe-God-Algorithm"; let lang = "EN"; let initialFen = DEFAULT_FEN; let line = []; // nodes {state, ply, moveIn, key, moves, gameOver, winner} let played = []; // notations, aligned to line transitions let cursor = 0; let busy = false; const t = (k) => (I18N[lang][k] !== undefined ? I18N[lang][k] : I18N.EN[k]); // Two roles by side index: 0 = the first player (moves first), 1 = the follower. // Names are translated, so winners are tracked by side index, not by name. const nameFor = (side) => (side === 0 ? t("nameFirst") : t("nameSecond")); // ---- FEN <-> internal state -------------------------------------------- # // Internal state is mover-relative "K1 K2 p0..p9" (K1/p0-4 = side to move). // FEN is colour-absolute: w1..w5/b1..b5 wKazan,bKazan side moveNo (White=Bastaushi). function absView(state, ply) { const v = state.split(" ").map(Number); const kM = v[0], kO = v[1], b = v.slice(2); if (ply % 2 === 0) return { bk: kM, kk: kO, bp: b.slice(0, 5), kp: b.slice(5, 10), moverBottom: true }; return { bk: kO, kk: kM, bp: b.slice(5, 10), kp: b.slice(0, 5), moverBottom: false }; } function nodeToFen(node) { const av = absView(node.state, node.ply); const side = node.ply % 2 === 0 ? "w" : "b"; const moveNo = Math.floor(node.ply / 2) + 1; return av.bp.join(",") + "/" + av.kp.join(",") + " " + av.bk + "," + av.kk + " " + side + " " + moveNo; } // Returns {state, ply} or {error}. function fenToInternal(fen) { try { const parts = fen.trim().split(/\s+/); if (parts.length < 3) return { error: "bad" }; const cells = parts[0].split("/"); if (cells.length !== 2) return { error: "bad" }; const white = cells[0].split(",").map(Number); const black = cells[1].split(",").map(Number); const kaz = parts[1].split(",").map(Number); if (white.length !== 5 || black.length !== 5 || kaz.length !== 2) return { error: "bad" }; const all = white.concat(black, kaz); if (all.some((n) => !Number.isInteger(n) || n < 0)) return { error: "bad" }; const side = (parts[2] || "w").toLowerCase(); const moveNo = parts[3] !== undefined ? parseInt(parts[3], 10) : 1; const wk = kaz[0], bk = kaz[1]; let state, ply; if (side === "b") { // Black (Kostaushi) to move state = bk + " " + wk + " " + black.concat(white).join(" "); ply = (Math.max(1, moveNo) - 1) * 2 + 1; } else { // White (Bastaushi) to move state = wk + " " + bk + " " + white.concat(black).join(" "); ply = (Math.max(1, moveNo) - 1) * 2; } return { state, ply }; } catch (e) { return { error: "bad" }; } } // ---- oracle bridge ----------------------------------------------------- # function setNative(el, val) { const set = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; set.call(el, val); el.dispatchEvent(new Event("input", { bubbles: true })); } function oracleOnce(state) { return new Promise((resolve, reject) => { const nonce = Math.random().toString(36).slice(2); const inp = document.querySelector("#oracle_in textarea"); const out = document.querySelector("#oracle_out textarea"); const btn = document.querySelector("#oracle_btn button") || document.querySelector("#oracle_btn"); if (!inp || !out || !btn) { reject("bridge not ready"); return; } let done = false; const poll = setInterval(() => { try { const j = JSON.parse(out.value); if (j.nonce === nonce) { done = true; clearInterval(poll); resolve(j); } } catch (e) {} }, 70); setTimeout(() => { if (!done) { clearInterval(poll); reject("timeout"); } }, 6000); setNative(inp, nonce + "|" + state); btn.click(); }); } async function oracle(state) { let lastErr; for (let attempt = 0; attempt < 4; attempt++) { try { return await oracleOnce(state); } catch (e) { lastErr = e; await new Promise((r) => setTimeout(r, 250)); } } throw lastErr; } async function makeNode(state, ply, moveIn) { const node = { state, ply, moveIn, key: (ply % 2) + ":" + state, moves: [], gameOver: null, winner: null }; const res = await oracle(state); if (!res.ok) { node.error = res.error; return node; } node.moves = res.data.moves; if (node.moves.length === 0) { node.gameOver = "nomove"; node.winner = (ply + 1) % 2; } return node; } async function newGame(fen) { const parsed = fenToInternal(fen); if (parsed.error) return "bad"; const n0 = await makeNode(parsed.state, parsed.ply, null); if (n0.error) return n0.error; initialFen = fen; line = [n0]; played = []; cursor = 0; render(); return null; } async function playMove(node, m) { if (busy) return; busy = true; try { line = line.slice(0, cursor + 1); played = played.slice(0, cursor); const notation = "" + m.from + m.to + (m.capture ? "+" : ""); const cs = m.child.K1 + " " + m.child.K2 + " " + m.child.board.join(" "); const cp = node.ply + 1; let child; if (m.terminal) { child = { state: cs, ply: cp, moveIn: notation, key: (cp % 2) + ":" + cs, moves: [], gameOver: "win", winner: node.ply % 2 }; } else { // Loop detection: we keep every position (side-to-move + board) of the // current line; if this child repeats one of them, it is an infinite-loop draw. const repeat = line.some((nd) => nd.key === (cp % 2) + ":" + cs); child = await makeNode(cs, cp, notation); child.moveIn = notation; if (repeat) { child.gameOver = "draw"; child.winner = null; child.moves = []; } } line.push(child); played.push(notation); cursor = line.length - 1; render(); if (child.gameOver) announceEnd(child); } finally { busy = false; } } function optimal(node) { const rank = { WIN: 2, DRAW: 1, LOSS: 0 }; let best = -1; node.moves.forEach((m) => { best = Math.max(best, rank[m.result]); }); return node.moves.filter((m) => rank[m.result] === best); } async function inputIndex(i) { const node = line[cursor]; if (!node || node.gameOver || node.error) return; if (i === 0) { // random OPTIMAL move const b = optimal(node); if (b.length) await playMove(node, b[Math.floor(Math.random() * b.length)]); return; } if (i === 9) { // random move — optimality ignored if (node.moves.length) await playMove(node, node.moves[Math.floor(Math.random() * node.moves.length)]); return; } const m = node.moves.find((x) => x.from === i); if (m) await playMove(node, m); } function go(idx) { cursor = Math.max(0, Math.min(line.length - 1, idx)); render(); } // ---- PGN --------------------------------------------------------------- # function resultToken() { const last = line[line.length - 1]; if (last.gameOver === "draw") return "1/2-1/2"; if (last.gameOver === "win" || last.gameOver === "nomove") return last.winner === 0 ? "1-0" : "0-1"; return "*"; } function pgnMoves() { let out = []; for (let i = 0; i < played.length; i += 2) { out.push((i / 2 + 1) + ". " + played.slice(i, i + 2).join(" ")); } let body = out.join(" "); const last = line[line.length - 1]; if (last.gameOver === "draw") body += " {Loop is reached}"; const res = resultToken(); if (res !== "*") body += " " + res; return body; } function downloadPgn() { const d = new Date(); const date = d.getFullYear() + "." + String(d.getMonth() + 1).padStart(2, "0") + "." + String(d.getDate()).padStart(2, "0"); const header = '[Event "Single Play Versus God"]\n' + '[Site "' + SITE + '"]\n' + '[Date "' + date + '"]\n' + '[White "Player"]\n' + '[Black "God"]\n' + '[Result "' + resultToken() + '"]\n' + '[PlyCount "' + played.length + '"]\n' + '[Annotator "God"]\n' + '[Mode "online"]\n'; const pgn = header + "\n" + pgnMoves() + "\n"; const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([pgn], { type: "text/plain" })); a.download = "bestemshe.pgn"; a.click(); } // ---- rendering --------------------------------------------------------- # // Colour-absolute view of a node (White = Bastaushi, Black = Kostaushi). function colorView(node) { const v = node.state.split(" ").map(Number); const k1 = v[0], k2 = v[1], p = v.slice(2); const whiteToMove = node.ply % 2 === 0; let wk, bk, wp, bp; if (whiteToMove) { wk = k1; bk = k2; wp = p.slice(0, 5); bp = p.slice(5, 10); } else { bk = k1; wk = k2; bp = p.slice(0, 5); wp = p.slice(5, 10); } return { wk, bk, wp, bp, whiteToMove }; } // A cell is exactly 2 stones wide and 5 stones tall — 10 visible slots. // Stones fill row by row, left column first (an odd stone sits on the left). // `side` sets the growth direction: black grows downward (rows top→bottom), // white grows upward (rows bottom→top). Once a slot is full, further stones // stack a new layer on top of it — shown as a small dark dot in the stone's // centre rather than a second circle. Slot i holds ceil((n-i)/10) layers. function cellStack(n, side) { let rows = []; for (let r = 0; r < 5; r++) { let s = ""; for (let c = 0; c < 2; c++) { const i = r * 2 + c; const layers = n > i ? Math.ceil((n - i) / 10) : 0; if (layers <= 0) s += ''; else s += '' + (layers >= 2 ? '' : "") + ""; } rows.push('
' + s + "
"); } if (side === "white") rows.reverse(); return '
' + rows.join("") + "
"; } // A kazan holds stones in columns of 2, filled column by column (left→right). // Vertical gravity (top for black, bottom for white) is handled in CSS. function kazanStack(n) { let cols = []; const ncols = Math.ceil(n / 2); for (let c = 0; c < ncols; c++) { const inCol = Math.min(2, n - c * 2); let b = ""; for (let k = 0; k < inCol; k++) b += ''; cols.push('
' + b + "
"); } return cols.join(""); } function cellHtml(count, pit, isMover, move, side) { const cls = "bb-cell" + (isMover && move ? " mv " + move.result : ""); const data = isMover && move ? ' data-act="pit" data-i="' + pit + '"' : ""; return '
" + cellStack(count, side) + "
"; } // Each cell is 2 stones wide; the five cells fill the board's width, so the // stone size is whatever makes 2 of them fit snugly inside one cell. Recomputed // on every render and on resize (keeps stones as large as the space allows). function sizeBoard() { const board = document.getElementById("bb-board"); const wrap = document.getElementById("bb-wrap"); if (!board || !wrap) return; const cs = getComputedStyle(board); const inner = board.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); const cellGap = 8, cellPad = 6, colGap = 6; // must match the CSS const cellOuter = (inner - 4 * cellGap) / 5; let bs = Math.floor((cellOuter - 2 * cellPad - colGap) / 2); // largest that fits the width bs = Math.max(14, Math.min(120, bs)); wrap.style.setProperty("--bs", bs + "px"); // Then measure the real board height and shrink until it fits the device viewport. for (let i = 0; i < 5; i++) { const avail = window.innerHeight - board.getBoundingClientRect().top - 12; const h = board.offsetHeight; if (h <= avail || bs <= 14) break; bs = Math.max(14, Math.floor(bs * avail / h)); wrap.style.setProperty("--bs", bs + "px"); } } window.addEventListener("resize", sizeBoard); function announceEnd(node) { const titleEl = document.getElementById("bb-end-title"); const msgEl = document.getElementById("bb-end-msg"); if (node.gameOver === "draw") { titleEl.textContent = "🔁 " + t("drawLoop"); msgEl.textContent = t("loopHint"); } else { titleEl.textContent = "🏆 " + nameFor(node.winner) + " " + t("wins"); msgEl.textContent = ""; } document.getElementById("bb-end").classList.add("open"); } function render() { const node = line[cursor]; const cv = colorView(node); const moverMoves = new Map(); if (!node.gameOver && !node.error) node.moves.forEach((m) => moverMoves.set(m.from, m)); // Fixed board: Black (Kostaushi) on top, White (Bastaushi) on the bottom. // Each player numbers pits 1..5 from their own left, so Black's pits read // right-to-left across the top; White's read left-to-right along the bottom. let blackCells = "", blackNums = "", whiteCells = "", whiteNums = ""; for (let c = 1; c <= 5; c++) { const bp = 6 - c; // black pit at visual column c const bMover = !cv.whiteToMove; blackCells += cellHtml(cv.bp[bp - 1], bp, bMover, moverMoves.get(bp), "black"); blackNums += '
' + cv.bp[bp - 1] + "
"; const wp = c; // white pit at visual column c const wMover = cv.whiteToMove; whiteCells += cellHtml(cv.wp[wp - 1], wp, wMover, moverMoves.get(wp), "white"); whiteNums += '
' + cv.wp[wp - 1] + "
"; } const blackTurn = cv.whiteToMove ? "" : " turn"; const whiteTurn = cv.whiteToMove ? " turn" : ""; document.getElementById("bb-board").innerHTML = '
' + '' + cv.bk + '' + '
' + kazanStack(cv.bk) + "
" + '
' + blackNums + "
" + '
' + blackCells + "
" + '
' + whiteCells + "
" + '
' + whiteNums + "
" + '
' + '' + cv.wk + '' + '
' + kazanStack(cv.wk) + "
"; sizeBoard(); let hist = ""; for (let i = 0; i < played.length; i += 2) { hist += '
' + (i / 2 + 1) + "."; for (let j = i; j < i + 2; j++) { hist += (j < played.length) ? '' + played[j] + "" : ""; } hist += "
"; } const histEl = document.getElementById("bb-hist"); histEl.innerHTML = hist; document.getElementById("bb-hist-title").textContent = t("history"); const onEl = histEl.querySelector(".bb-mv.on"); if (onEl) onEl.scrollIntoView({ block: "nearest" }); else histEl.scrollTop = histEl.scrollHeight; // toolbar tooltips + modal labels document.getElementById("bb-btn-new").title = t("newGame"); document.getElementById("bb-btn-setup").title = t("setup"); document.getElementById("bb-btn-help").title = t("help"); document.getElementById("bb-btn-pgn").title = t("pgn"); document.getElementById("bb-setup-title").textContent = t("setup"); document.getElementById("bb-fen-help").innerHTML = t("fenHelp"); document.getElementById("bb-setup-apply").textContent = t("apply"); document.getElementById("bb-setup-close").textContent = t("close"); document.getElementById("bb-help-title").textContent = t("help"); document.getElementById("bb-help-intro").innerHTML = t("helpIntro"); document.getElementById("bb-help-keys").innerHTML = t("keys").map((k) => "" + k[0] + "" + k[1] + "").join(""); document.getElementById("bb-help-extra").textContent = t("helpExtra"); document.getElementById("bb-help-credit").textContent = t("authors"); document.getElementById("bb-help-close").textContent = t("close"); document.getElementById("bb-end-close").textContent = t("close"); document.getElementById("bb-end-pgn").textContent = t("pgn"); } function anyModalOpen() { return document.querySelector(".bb-overlay.open") !== null; } // ---- events ------------------------------------------------------------ # document.getElementById("bb-langsel").addEventListener("change", (e) => { lang = e.target.value; render(); }); document.getElementById("bb-toggle").addEventListener("click", () => { const cols = document.getElementById("bb-cols"); const hidden = cols.classList.toggle("side-hidden"); document.getElementById("bb-toggle").textContent = hidden ? "⇤" : "⇥"; sizeBoard(); // board width changed — rescale stones }); document.getElementById("bb-wrap").addEventListener("click", async (e) => { const el = e.target.closest("[data-act]"); if (!el) return; const act = el.dataset.act; if (act === "new") { await newGame(DEFAULT_FEN); } else if (act === "pgn") { downloadPgn(); } else if (act === "open") { const m = document.getElementById(el.dataset.modal); if (el.dataset.modal === "bb-setup") { document.getElementById("bb-fen").value = nodeToFen(line[cursor]); document.getElementById("bb-setup-err").textContent = ""; } m.classList.add("open"); } else if (act === "close") { document.getElementById(el.dataset.modal).classList.remove("open"); } else if (act === "apply") { const fen = document.getElementById("bb-fen").value.trim(); const err = await newGame(fen); if (err) document.getElementById("bb-setup-err").textContent = t("fenHelp"); else document.getElementById("bb-setup").classList.remove("open"); } else if (act === "pit") { await inputIndex(parseInt(el.dataset.i, 10)); } else if (act === "hist") { go(parseInt(el.dataset.idx, 10) + 1); } }); document.querySelectorAll(".bb-overlay").forEach((o) => o.addEventListener("click", (e) => { if (e.target === o) o.classList.remove("open"); })); // Kazakh keyboard number row maps to the same digit inputs (item 5.2). const KZ_DIGIT = { "«": "1", "ә": "2", "і": "3", "ң": "4", "ғ": "5", "ұ": "9", "қ": "0" }; // WASD navigation, case-insensitive, plus the letters those keys type on a // Kazakh/Russian layout: W→ц, A→ф, S→ы, D→в (items 5.3 / 5.4). const NAV = { w: "up", a: "left", s: "down", d: "right", "ц": "up", "ф": "left", "ы": "down", "в": "right" }; document.addEventListener("keydown", (e) => { if (anyModalOpen()) { if (e.key === "Escape") document.querySelectorAll(".bb-overlay.open").forEach((o) => o.classList.remove("open")); return; } const tag = (document.activeElement && document.activeElement.tagName) || ""; if (tag === "TEXTAREA" || tag === "INPUT" || tag === "SELECT") return; let key = e.key; if (KZ_DIGIT[key] !== undefined) key = KZ_DIGIT[key]; // normalise Kazakh row to digits const nav = NAV[key.toLowerCase()]; // WASD / Kazakh nav (case-insensitive) if ((key >= "0" && key <= "5") || key === "9") { inputIndex(parseInt(key, 10)); e.preventDefault(); } else if (e.key === "ArrowRight" || nav === "right") { go(cursor + 1); e.preventDefault(); } else if (e.key === "ArrowLeft" || nav === "left") { go(cursor - 1); e.preventDefault(); } else if (e.key === "ArrowUp" || nav === "up") { go(0); e.preventDefault(); } else if (e.key === "ArrowDown" || nav === "down") { go(line.length - 1); e.preventDefault(); } }); const boot = setInterval(() => { if (document.querySelector("#oracle_in textarea") && document.querySelector("#oracle_btn")) { clearInterval(boot); newGame(DEFAULT_FEN); } }, 100); } """.replace("__I18N_JSON__", json.dumps(TRANSLATIONS, ensure_ascii=False)) ensure_query_binary() ensure_tablebase() with gr.Blocks(title="Bestemshe — Perfect-Play Explorer", fill_width=True) as demo: gr.HTML(INDEX_MARKUP) inp = gr.Textbox(elem_id="oracle_in") out = gr.Textbox(elem_id="oracle_out") btn = gr.Button("bridge", elem_id="oracle_btn") btn.click(oracle_bridge, inp, out) demo.load(None, None, None, js=INDEX_JS) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))