File size: 16,661 Bytes
acd049c
1f46af1
acd049c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1caf272
 
 
 
 
 
acd049c
 
 
 
 
1caf272
 
 
 
 
acd049c
 
 
 
 
 
 
 
46b1fd8
 
 
 
 
 
 
 
 
 
acd049c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1caf272
acd049c
 
 
 
 
 
 
 
 
 
 
1caf272
3706fe1
acd049c
1caf272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acd049c
 
 
 
 
 
 
1caf272
 
 
 
 
 
acd049c
 
1caf272
 
 
acd049c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1caf272
 
 
acd049c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46b1fd8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acd049c
 
 
1f46af1
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<!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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");

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;
  // drop any in-progress game: rules and replays no longer match
  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>