|
53 | 53 | codeCell: document.getElementById("codeCell"), |
54 | 54 | runCode: document.getElementById("runCodeButton"), |
55 | 55 | resetCode: document.getElementById("resetCodeButton"), |
| 56 | + challengePanel: document.getElementById("challengePanel"), |
| 57 | + challengeBoard: document.getElementById("challengeBoard"), |
| 58 | + scoreButton: document.getElementById("scoreButton"), |
56 | 59 | reset: document.getElementById("resetButton"), |
57 | 60 | step: document.getElementById("stepButton"), |
58 | 61 | run: document.getElementById("runButton"), |
|
105 | 108 | elements.resetCode.addEventListener("click", () => { |
106 | 109 | resetEditedAgent(); |
107 | 110 | }); |
| 111 | + elements.scoreButton.addEventListener("click", () => { |
| 112 | + runChallenge(); |
| 113 | + }); |
108 | 114 | elements.replay.addEventListener("input", () => { |
109 | 115 | state.replayIndex = clampReplayIndex(elements.replay.value); |
110 | 116 | render(); |
|
197 | 203 | setRealStatus("running real Python: " + sourcePath(scenario)); |
198 | 204 | if (scenario === "pickretry") { |
199 | 205 | populateDefaultAgentSource(); |
| 206 | + if (!elements.challengeBoard.textContent.trim()) { |
| 207 | + renderChallengeMessage( |
| 208 | + 'Edit the agent above, then "Score ' + CHALLENGE_SEEDS + ' seeds" to see if you beat it.' |
| 209 | + ); |
| 210 | + } |
200 | 211 | } |
201 | 212 | } catch (error) { |
202 | 213 | if (scenario === "pickretry") { |
|
460 | 471 | } |
461 | 472 | } |
462 | 473 |
|
| 474 | + // --- "Beat the robot" challenge --------------------------------------------- |
| 475 | + // Scores the edited agent vs. the shipped one across many seeds. Single-seed |
| 476 | + // scoring would reward overfitting; robustness across seeds is the real test. |
| 477 | + const CHALLENGE_SEEDS = 10; |
| 478 | + const CHALLENGE_DRIVER = [ |
| 479 | + "import json, os, sys, importlib.util", |
| 480 | + "cwd = os.getcwd()", |
| 481 | + "if cwd not in sys.path:", |
| 482 | + " sys.path.insert(0, cwd)", |
| 483 | + "class _NoMatplotlib:", |
| 484 | + " def find_spec(self, name, path=None, target=None):", |
| 485 | + " if name == 'matplotlib' or name.startswith('matplotlib.'):", |
| 486 | + " raise ImportError('matplotlib is intentionally unavailable on the headless browser path')", |
| 487 | + " return None", |
| 488 | + "sys.meta_path.insert(0, _NoMatplotlib())", |
| 489 | + "path = os.path.join(cwd, 'examples', 'manipulation', '01_pick_and_retry.py')", |
| 490 | + "spec = importlib.util.spec_from_file_location('pick_and_retry', path)", |
| 491 | + "mod = importlib.util.module_from_spec(spec)", |
| 492 | + "spec.loader.exec_module(mod)", |
| 493 | + "from pir.viz.playground_trace import score_pick_and_retry", |
| 494 | + "seeds = list(range(" + CHALLENGE_SEEDS + "))", |
| 495 | + "baseline = score_pick_and_retry(mod.run_agent, mod.PickAndRetryAgent, seeds=seeds)", |
| 496 | + "src = USER_SRC", |
| 497 | + "if src and src.strip():", |
| 498 | + " ns = {}", |
| 499 | + " exec(src, ns)", |
| 500 | + " Agent = ns.get('PickAndRetryAgent')", |
| 501 | + " if Agent is None:", |
| 502 | + " raise ValueError('Your code must define a class named PickAndRetryAgent')", |
| 503 | + " you = score_pick_and_retry(mod.run_agent, Agent, seeds=seeds)", |
| 504 | + "else:", |
| 505 | + " you = baseline", |
| 506 | + "json.dumps({'seeds': seeds, 'baseline': baseline, 'you': you})", |
| 507 | + ].join("\n"); |
| 508 | + |
| 509 | + async function runChallenge() { |
| 510 | + stopRun(); |
| 511 | + elements.scoreButton.disabled = true; |
| 512 | + renderChallengeMessage("scoring " + CHALLENGE_SEEDS + " seeds for both agents…"); |
| 513 | + setRealStatus("running the challenge…"); |
| 514 | + try { |
| 515 | + const pyodide = await ensurePyodide(); |
| 516 | + pyodide.globals.set("USER_SRC", elements.codeCell.value || ""); |
| 517 | + const result = JSON.parse(await pyodide.runPythonAsync(CHALLENGE_DRIVER)); |
| 518 | + renderScoreboard(result); |
| 519 | + setRealStatus("challenge scored across " + CHALLENGE_SEEDS + " seeds"); |
| 520 | + } catch (error) { |
| 521 | + renderChallengeMessage(""); |
| 522 | + setRealStatus("challenge failed: " + error, true); |
| 523 | + } finally { |
| 524 | + elements.scoreButton.disabled = false; |
| 525 | + } |
| 526 | + } |
| 527 | + |
| 528 | + function renderChallengeMessage(message) { |
| 529 | + elements.challengeBoard.textContent = ""; |
| 530 | + if (!message) { |
| 531 | + return; |
| 532 | + } |
| 533 | + const note = document.createElement("p"); |
| 534 | + note.className = "challenge-empty"; |
| 535 | + note.textContent = message; |
| 536 | + elements.challengeBoard.appendChild(note); |
| 537 | + } |
| 538 | + |
| 539 | + function challengeVerdict(you, base) { |
| 540 | + const same = |
| 541 | + you.success_rate === base.success_rate && |
| 542 | + you.mean_reward === base.mean_reward && |
| 543 | + you.mean_steps === base.mean_steps; |
| 544 | + if (same) { |
| 545 | + return { cls: "tie", text: "You're running the shipped agent — edit it and score again to try to beat it." }; |
| 546 | + } |
| 547 | + // Higher success rate wins; then higher reward; then fewer steps. |
| 548 | + let youWin; |
| 549 | + if (you.success_rate !== base.success_rate) { |
| 550 | + youWin = you.success_rate > base.success_rate; |
| 551 | + } else if (you.mean_reward !== base.mean_reward) { |
| 552 | + youWin = you.mean_reward > base.mean_reward; |
| 553 | + } else { |
| 554 | + youWin = you.mean_steps < base.mean_steps; |
| 555 | + } |
| 556 | + return youWin |
| 557 | + ? { cls: "win", text: "🏆 You beat the shipped agent across " + CHALLENGE_SEEDS + " seeds!" } |
| 558 | + : { cls: "lose", text: "Shipped agent still wins — make your policy more robust across seeds." }; |
| 559 | + } |
| 560 | + |
| 561 | + function renderScoreboard(result) { |
| 562 | + const you = result.you; |
| 563 | + const base = result.baseline; |
| 564 | + elements.challengeBoard.textContent = ""; |
| 565 | + |
| 566 | + const verdict = challengeVerdict(you, base); |
| 567 | + const banner = document.createElement("div"); |
| 568 | + banner.className = "challenge-verdict " + verdict.cls; |
| 569 | + banner.textContent = verdict.text; |
| 570 | + elements.challengeBoard.appendChild(banner); |
| 571 | + |
| 572 | + // metric label, key, and whether higher is better |
| 573 | + const rows = [ |
| 574 | + ["success rate", "success_rate", true, (v) => Math.round(v * 100) + "%"], |
| 575 | + ["mean reward", "mean_reward", true, (v) => v.toFixed(2)], |
| 576 | + ["mean steps", "mean_steps", false, (v) => v.toFixed(1)], |
| 577 | + ["mean retries", "mean_retries", false, (v) => v.toFixed(1)], |
| 578 | + ["mean grasp_miss", "mean_grasp_miss", false, (v) => v.toFixed(1)], |
| 579 | + ]; |
| 580 | + |
| 581 | + const table = document.createElement("table"); |
| 582 | + table.className = "challenge-table"; |
| 583 | + const thead = document.createElement("thead"); |
| 584 | + const headRow = document.createElement("tr"); |
| 585 | + ["metric", "baseline", "you"].forEach((label) => { |
| 586 | + const th = document.createElement("th"); |
| 587 | + th.textContent = label; |
| 588 | + headRow.appendChild(th); |
| 589 | + }); |
| 590 | + thead.appendChild(headRow); |
| 591 | + table.appendChild(thead); |
| 592 | + |
| 593 | + const tbody = document.createElement("tbody"); |
| 594 | + rows.forEach(([label, key, higherBetter, fmt]) => { |
| 595 | + const tr = document.createElement("tr"); |
| 596 | + const name = document.createElement("td"); |
| 597 | + name.textContent = label; |
| 598 | + tr.appendChild(name); |
| 599 | + |
| 600 | + const baseCell = document.createElement("td"); |
| 601 | + baseCell.textContent = fmt(base[key]); |
| 602 | + const youCell = document.createElement("td"); |
| 603 | + youCell.textContent = fmt(you[key]); |
| 604 | + |
| 605 | + if (base[key] !== you[key]) { |
| 606 | + const youBetter = higherBetter ? you[key] > base[key] : you[key] < base[key]; |
| 607 | + (youBetter ? youCell : baseCell).classList.add("better"); |
| 608 | + } |
| 609 | + tr.appendChild(baseCell); |
| 610 | + tr.appendChild(youCell); |
| 611 | + tbody.appendChild(tr); |
| 612 | + }); |
| 613 | + table.appendChild(tbody); |
| 614 | + elements.challengeBoard.appendChild(table); |
| 615 | + } |
| 616 | + |
463 | 617 | function stepOnce() { |
464 | 618 | if (state.index >= state.config.steps.length) { |
465 | 619 | render(); |
|
955 | 1109 | } |
956 | 1110 | elements.answer.disabled = state.scenario === "pickretry"; |
957 | 1111 | elements.codePanel.hidden = state.scenario !== "pickretry"; |
| 1112 | + elements.challengePanel.hidden = state.scenario !== "pickretry"; |
958 | 1113 |
|
959 | 1114 | renderReplay(replayIndex); |
960 | 1115 | renderCompare(); |
|
0 commit comments