| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>Eleusis</title> |
| <script src="https://cdn.jsdelivr.net/pyodide/v0.27.3/full/pyodide.js"></script> |
| <style> |
| :root { --cream:#f5deb3; --mist:#9fbfb2; --ink:#e8e3d6; } |
| * { box-sizing: border-box; } |
| body { |
| margin:0; min-height:100vh; font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif; |
| background: linear-gradient(160deg,#0b3d2e 0%,#0a2b21 55%,#061a14 100%); color: var(--ink); |
| } |
| h1 { text-align:center; color:var(--cream); font-size:2.3em; margin:18px 0 2px; |
| text-shadow:0 0 18px rgba(245,222,179,.45); letter-spacing:.35em; } |
| .subtitle { text-align:center; color:var(--mist); margin:0 14px 18px; } |
| .wrap { display:flex; flex-wrap:wrap; gap:16px; max-width:1100px; margin:0 auto; padding:0 14px 40px; } |
| .panel { background:rgba(255,255,255,.05); border:1px solid rgba(245,222,179,.35); |
| border-radius:12px; padding:16px; flex:1 1 420px; } |
| .panel h3 { margin:2px 0 8px; color:var(--cream); font-weight:600; } |
| .chip { display:inline-block; background:#faf6ee; border:1px solid #b9ad93; border-radius:7px; |
| padding:4px 9px; margin:2px; font-weight:700; font-family:ui-monospace,monospace; |
| box-shadow:0 2px 4px rgba(0,0,0,.35); } |
| .red { color:#c0392b; } .blk { color:#1c2833; } |
| .rej { opacity:.55; transform:scale(.85); } |
| .hand button { background:#faf6ee; border:2px solid #b9ad93; border-radius:8px; padding:7px 11px; |
| margin:3px; font-weight:700; font-family:ui-monospace,monospace; font-size:1.02em; |
| cursor:pointer; } |
| .hand button.sel { border-color:#e67e22; outline:3px solid rgba(230,126,34,.45); } |
| .btnrow { display:flex; flex-wrap:wrap; gap:8px; margin:12px 0; } |
| .btn { border:none; border-radius:9px; padding:10px 14px; font-size:1em; cursor:pointer; |
| background:#28483d; color:var(--ink); border:1px solid rgba(245,222,179,.3); } |
| .btn.primary { background:#e67e22; color:#161616; font-weight:700; } |
| .btn:disabled { opacity:.4; cursor:default; } |
| textarea { width:100%; background:rgba(0,0,0,.35); color:#ffd9a0; border:1px solid rgba(245,222,179,.3); |
| border-radius:8px; padding:8px; font-family:ui-monospace,monospace; font-size:.95em; min-height:52px; } |
| .status { margin:10px 0; line-height:1.45; } |
| .status code, .log code, .modelview code { background:rgba(0,0,0,.35); color:#ffd9a0; padding:1px 5px; |
| border-radius:5px; font-size:.9em; white-space:pre-wrap; } |
| .log div { margin:3px 0; } |
| details { margin-top:10px; color:var(--mist); } |
| details code { color:#ffd9a0; } |
| .attempts button { background:rgba(0,0,0,.25); color:var(--ink); border:1px solid rgba(245,222,179,.25); |
| border-radius:7px; padding:6px 9px; margin:2px; cursor:pointer; } |
| .attempts button.sel { border-color:#e67e22; } |
| .modelview { margin-top:10px; line-height:1.5; } |
| .banner { background:rgba(230,126,34,.12); border:1px solid rgba(230,126,34,.5); border-radius:9px; |
| padding:10px 12px; margin:10px 0; } |
| a { color:#9fd3ff; } |
| #loading { text-align:center; padding:60px 20px; color:var(--mist); font-size:1.1em; } |
| .spin { display:inline-block; width:16px; height:16px; border:3px solid rgba(245,222,179,.3); |
| border-top-color:var(--cream); border-radius:50%; animation:s 1s linear infinite; vertical-align:-3px; margin-right:8px;} |
| @keyframes s { to { transform:rotate(360deg); } } |
| .warn { color:#e6b35a; font-size:.92em; } |
| .variants { display:flex; justify-content:center; gap:0; margin:0 auto 6px; width:fit-content; |
| border:1px solid rgba(245,222,179,.35); border-radius:10px; overflow:hidden; } |
| .variants button { background:rgba(255,255,255,.05); color:var(--mist); border:none; |
| padding:8px 16px; font-size:.98em; cursor:pointer; } |
| .variants button.sel { background:#e67e22; color:#161616; font-weight:700; } |
| .variantdesc { text-align:center; color:var(--mist); font-size:.92em; margin:0 14px 16px; } |
| </style> |
| </head> |
| <body> |
| <h1>β ELEUSIS β₯</h1> |
| <p class="subtitle">A hidden rule decides which cards are accepted. Discover it β and see whether you beat a 4B model trained on this game.</p> |
| <div class="variants" id="variants" style="display:none"> |
| <button id="var-hf" class="sel">π Benchmark rules</button> |
| <button id="var-small">π Simple rules</button> |
| </div> |
| <p class="variantdesc" id="variantdesc" style="display:none"></p> |
|
|
| <div id="loading"><span class="spin"></span>Loading the game engine (Python in your browser, ~10s first time)β¦</div> |
|
|
| <div class="wrap" id="ui" style="display:none"> |
| <div class="panel"> |
| <div id="board" class="status"><i style="color:var(--mist)">Press <b>New game</b> to draw a hidden rule.</i></div> |
| <div id="handlabel" style="color:var(--mist);margin:8px 0 2px">Your hand β pick a card to test</div> |
| <div class="hand" id="hand"></div> |
| <div id="nlrow" style="display:none"> |
| <div style="color:var(--mist);margin:12px 0 4px">Describe your rule guess in plain English:</div> |
| <div style="display:flex;gap:8px"> |
| <input id="nl" type="text" maxlength="300" placeholder="e.g. every card must be higher than the previous one" |
| style="flex:1;background:rgba(0,0,0,.35);color:var(--ink);border:1px solid rgba(245,222,179,.3);border-radius:8px;padding:8px;font-size:.95em" /> |
| <button class="btn" id="translate">β¨ Translate</button> |
| </div> |
| <div id="nlstatus" class="warn" style="margin:4px 0"></div> |
| </div> |
| <div style="color:var(--mist);margin:12px 0 4px">Rule guess as Python (free, checked every turn):</div> |
| <textarea id="guess" placeholder='e.g. card.color == "red" or not mainline or card.rank > mainline[-1].rank'></textarea> |
| <div class="btnrow"> |
| <button class="btn primary" id="newgame">π New game</button> |
| <button class="btn" id="play" disabled>βΆ Play selected card</button> |
| <button class="btn" id="noplay" disabled>β
No card playable</button> |
| <button class="btn" id="giveup" disabled>π³ Give up</button> |
| </div> |
| <div class="status" id="status"></div> |
| <div class="log" id="log"></div> |
| <details> |
| <summary>π Rule-guess cheatsheet</summary> |
| <p>Write a Python expression (or multi-line body with <code>return</code>) over:<br> |
| <code>card.rank</code> (A=1 β¦ K=13) Β· <code>card.suit</code> (<code>'hearts' 'diamonds' 'clubs' 'spades'</code>) Β· |
| <code>card.color</code> (<code>'red' 'black'</code>) Β· <code>card.is_face</code><br> |
| <code>mainline</code> β the accepted cards so far, oldestβnewest.</p> |
| <p>Examples: <code>card.rank % 2 == 0</code> Β· <code>card.suit == "spades"</code> Β· |
| <code>not mainline or card.color != mainline[-1].color</code></p> |
| <p>Guesses are judged by <i>behavioral equivalence</i> (same accepts/rejects on every probe board) β |
| the exact check the model faced.</p> |
| </details> |
| </div> |
|
|
| <div class="panel"> |
| <h3>π€ The model's game</h3> |
| <p style="color:var(--mist);margin:4px 0"><i>Same rule, same deal (starter, hand, draw order). Careful: revealing may spoil the rule.</i></p> |
| <div class="attempts" id="attempts"></div> |
| <div class="modelview" id="modelview"></div> |
| <p style="color:var(--mist);font-size:.92em" id="modelnote"></p> |
| </div> |
| </div> |
|
|
| <script> |
| const GLYPH = {H:"β₯", D:"β¦", C:"β£", S:"β "}; |
| const chip = (sym, rej=false) => { |
| const suit = sym.slice(-1), color = (suit==="H"||suit==="D") ? "red" : "blk"; |
| return `<span class="chip ${color}${rej?" rej":""}">${sym.slice(0,-1)}${GLYPH[suit]}</span>`; |
| }; |
| const esc = s => (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); |
| |
| let py, selected = null, gameOn = false, variant = "hf"; |
| const WORKER_URL = "https://pinference.arturomf94.workers.dev"; |
| |
| const VARIANT_INFO = { |
| hf: { |
| desc: "26 held-out benchmark rules Β· 10 turns Β· a played card is replaced from the deck.", |
| note: `Model: <a href="https://huggingface.co/nph4rd/Qwen3.5-4B-eleusis-rl4-consistency-step30" target="_blank"> |
| Qwen3.5-4B-eleusis-rl4-consistency-step30</a> β oracle SFT + GRPO with consistency-shaped rewards. |
| Its games are replays from the recorded 26Γ8 verification run (512 tokens/turn, temp 0.7), not live inference.`, |
| }, |
| small: { |
| desc: "6 held-out simple rules Γ 4 deals Β· 8 turns Β· no redraws β your hand shrinks as you play.", |
| note: `Model: a Qwen3.5-4B LoRA (rank 16) trained with RL in |
| <a href="https://app.primeintellect.ai/dashboard/training/r9gjcjph7qwgq0c44byyyfg9" target="_blank">this hosted run</a> |
| on the simple-rules variant. Its games are replays from the recorded step-90 eval |
| (24 deals Γ 8 attempts, 1024 tokens/turn, temp 0.7), not live inference.`, |
| }, |
| }; |
| |
| function setVariant(v) { |
| variant = v; |
| document.getElementById("var-hf").classList.toggle("sel", v === "hf"); |
| document.getElementById("var-small").classList.toggle("sel", v === "small"); |
| document.getElementById("variantdesc").innerHTML = VARIANT_INFO[v].desc; |
| document.getElementById("modelnote").innerHTML = VARIANT_INFO[v].note; |
| |
| gameOn = false; selected = null; |
| document.getElementById("board").innerHTML = |
| `<i style="color:var(--mist)">Press <b>New game</b> to draw a hidden rule.</i>`; |
| document.getElementById("hand").innerHTML = ""; |
| document.getElementById("status").innerHTML = ""; |
| document.getElementById("log").innerHTML = ""; |
| document.getElementById("attempts").innerHTML = ""; |
| document.getElementById("modelview").innerHTML = ""; |
| ["play","noplay","giveup"].forEach(id => document.getElementById(id).disabled = true); |
| } |
| |
| async function boot() { |
| py = await loadPyodide(); |
| const files = ["cards.py", "engine.py", "game.py"]; |
| for (const f of files) { |
| const src = await (await fetch("./" + f)).text(); |
| py.FS.writeFile(f, src); |
| } |
| for (const v of ["hf", "small"]) { |
| const games = await (await fetch(`./games_${v}.json`)).text(); |
| py.globals.set("_games_payload", games); |
| py.globals.set("_variant", v); |
| await py.runPythonAsync(`import game\ngame.load_games(_variant, _games_payload)`); |
| } |
| document.getElementById("loading").style.display = "none"; |
| document.getElementById("ui").style.display = "flex"; |
| document.getElementById("variants").style.display = "flex"; |
| document.getElementById("variantdesc").style.display = "block"; |
| setVariant("hf"); |
| } |
| |
| function call(expr) { return JSON.parse(py.runPython(expr)); } |
| |
| function renderHand(hand, enabled) { |
| const el = document.getElementById("hand"); |
| el.innerHTML = ""; |
| hand.forEach(sym => { |
| const b = document.createElement("button"); |
| const suit = sym.slice(-1); |
| b.innerHTML = `<span class="${(suit==="H"||suit==="D")?"red":"blk"}">${sym.slice(0,-1)}${GLYPH[suit]}</span>`; |
| b.disabled = !enabled; |
| b.onclick = () => { selected = sym; [...el.children].forEach(c => c.classList.remove("sel")); b.classList.add("sel"); }; |
| el.appendChild(b); |
| }); |
| selected = null; |
| } |
| |
| function render(st) { |
| gameOn = !st.over; |
| const potential = st.max_turns - st.turn; |
| let bh = `<b style="color:var(--mist)">Board:</b> `; |
| st.board.forEach(e => { bh += chip(e.card); e.rejects.forEach(r => bh += chip(r, true)); }); |
| bh += `<div>Turn <b>${st.turn}/${st.max_turns}</b> Β· score if you solve it this turn: <b>${potential}</b></div>`; |
| document.getElementById("board").innerHTML = bh; |
| renderHand(st.hand, gameOn); |
| ["play","noplay","giveup"].forEach(id => document.getElementById(id).disabled = !gameOn); |
| |
| let msg = st.message ? `<span class="warn">${esc(st.message)}</span>` : ""; |
| if (st.log.length) { |
| const last = st.log[st.log.length-1]; |
| if (last.guess && !last.guess_correct && !st.over) |
| msg += `<div class="warn">Your guess didn't match the hidden rule${last.guess_error ? " ("+esc(last.guess_error)+")" : ""} β keep refining.</div>`; |
| } |
| if (st.over) { |
| const mdl = `The model solved this rule in <b>${st.model_solved_count}/8</b> recorded attempts` + |
| (st.model_best_turns ? ` (best: ${st.model_best_turns} turns).` : "."); |
| msg = `<div class="banner">${st.solved |
| ? `π <b>Solved in ${st.turn} turns β score ${st.score}/${st.max_turns}</b>` |
| : `<b>Game over β the rule eluded you (score 0).</b>`}<br> |
| Hidden rule: <i>${esc(st.rule_label)}</i><br><code>${esc(st.rule_code)}</code><br>${mdl}</div>`; |
| } |
| document.getElementById("status").innerHTML = msg; |
| |
| document.getElementById("log").innerHTML = st.log.map((e,i) => { |
| const badge = {accepted:"β
accepted", rejected:"β rejected", |
| correct_no_play:"β correct no-play", missed_play:"β missed play (a card was playable)"}[e.result] || e.result; |
| const g = e.guess ? ` Β· guessed <code>${esc(e.guess)}</code> β ${e.guess_correct?"π CORRECT":"wrong"}` : ""; |
| return `<div>T${i+1}: ${e.card?chip(e.card):"<i>no card</i>"} β ${badge}${g}</div>`; |
| }).join(""); |
| |
| const at = document.getElementById("attempts"); |
| at.innerHTML = ""; |
| (st.model_attempts||[]).forEach((a,i) => { |
| const b = document.createElement("button"); |
| b.textContent = `Attempt ${i+1} ${a.solved?"β
":"β"}`; |
| b.onclick = () => { [...at.children].forEach(c=>c.classList.remove("sel")); b.classList.add("sel"); reveal(i); }; |
| at.appendChild(b); |
| }); |
| document.getElementById("modelview").innerHTML = |
| `<i style="color:var(--mist)">Select an attempt to reveal the model's full game.</i>`; |
| } |
| |
| function reveal(i) { |
| const ep = call(`game.reveal_attempt(${i})`); |
| let h = `<p>The model solved this rule <b>${ep.model_solved_count}/8</b> times. ` + |
| `<b>Attempt ${i+1}</b> β ${ep.solved ? "β
solved in "+ep.turns_used+" turns" : "β not solved"}.</p>`; |
| ep.turns.forEach((t,k) => { |
| const badge = {accepted:"β
", rejected:"β", correct_no_play:"β no-play", |
| missed_play:"β missed", invalid:"β invalid (hallucinated card)"}[t.result] || t.result; |
| const solved = t.solved_here ? " β <b>π hypothesis correct, game over</b>" : ""; |
| h += `<div style="margin:6px 0"><b>T${k+1}</b>: ${t.card?chip(t.card):"<i>no card</i>"} ${badge}${solved}<br> |
| <code>${esc(t.rule)}</code></div>`; |
| }); |
| document.getElementById("modelview").innerHTML = h; |
| } |
| |
| document.getElementById("newgame").onclick = () => render(call(`game.new_game("${variant}")`)); |
| document.getElementById("var-hf").onclick = () => setVariant("hf"); |
| document.getElementById("var-small").onclick = () => setVariant("small"); |
| document.getElementById("play").onclick = () => { |
| if (!selected) { document.getElementById("status").innerHTML = `<span class="warn">Pick a card first.</span>`; return; } |
| const g = document.getElementById("guess").value; |
| py.globals.set("_sel", selected); py.globals.set("_guess", g); |
| render(call("game.play_turn(_sel, _guess, False)")); |
| document.getElementById("guess").value = ""; |
| }; |
| document.getElementById("noplay").onclick = () => { |
| const g = document.getElementById("guess").value; |
| py.globals.set("_guess", g); |
| render(call('game.play_turn("", _guess, True)')); |
| document.getElementById("guess").value = ""; |
| }; |
| document.getElementById("giveup").onclick = () => render(call("game.give_up()")); |
| |
| if (WORKER_URL) document.getElementById("nlrow").style.display = "block"; |
| document.getElementById("translate").onclick = async () => { |
| const nl = document.getElementById("nl").value.trim(); |
| const st = document.getElementById("nlstatus"); |
| if (!nl) { st.textContent = "Describe the rule first."; return; } |
| st.textContent = "Translatingβ¦"; |
| try { |
| const r = await fetch(WORKER_URL, { method: "POST", |
| headers: { "content-type": "application/json" }, body: JSON.stringify({ text: nl }) }); |
| const data = await r.json(); |
| if (!r.ok || !data.predicate) throw new Error(data.error || "no result"); |
| py.globals.set("_cand", data.predicate); |
| const compiles = py.runPython( |
| "import engine\ntry:\n engine.compile_python_rule(_cand)\n _ok = True\nexcept Exception:\n _ok = False\n_ok"); |
| if (!compiles) { st.textContent = "The translation didn't compile β try rephrasing."; return; } |
| document.getElementById("guess").value = data.predicate; |
| st.textContent = "Translated β β review the Python below, then play a card (or no-play) to submit it."; |
| } catch (e) { |
| st.textContent = "Translation failed: " + e.message; |
| } |
| }; |
| |
| boot(); |
| </script> |
| </body> |
| </html> |
|
|