(() => { let frame = null; let ready = false; let bootPromise = null; let resolveBoot = null; let rejectBoot = null; let timeout = null; let runId = 0; let activeRunId = 0; let onMessage = () => {}; function receive(event) { if (!frame || event.source !== frame.contentWindow) return; const message = event.data || {}; if (message.type === "ready") { ready = true; clearTimeout(timeout); timeout = null; resolveBoot?.(); resolveBoot = null; rejectBoot = null; bootPromise = null; return; } if (message.type === "load_error") { const error = new Error(message.text || "無法載入瀏覽器版 Pygame"); clearTimeout(timeout); timeout = null; rejectBoot?.(error); resolveBoot = null; rejectBoot = null; bootPromise = null; onMessage({ ...message, runId: activeRunId }); return; } if (message.runId && message.runId !== activeRunId) return; onMessage(message); } window.addEventListener("message", receive); window.ClassroomPygame = { attach(target, handler) { frame = target; onMessage = handler; }, isPygame(files) { return Object.entries(files || {}).some(([path, code]) => path.toLowerCase().endsWith(".py") && /^\s*(?:import\s+pygame(?:\s|,|$)|from\s+pygame(?:\.|\s+import\s))/m.test(code) ); }, async run(files) { const thisRun = ++runId; activeRunId = thisRun; frame.hidden = false; if (!ready) { if (!bootPromise) { bootPromise = new Promise((resolve, reject) => { resolveBoot = resolve; rejectBoot = reject; timeout = setTimeout(() => { const error = new Error("Pygame 載入逾時,請確認網路連線後重試。"); bootPromise = null; resolveBoot = null; rejectBoot = null; reject(error); }, 60000); frame.src = "/classroom-assets/pygame-runner.html?v=2026fall-1"; }); } await bootPromise; } if (thisRun !== runId) return; frame.contentWindow.postMessage({ type: "run", runId: thisRun, files }, "*"); }, stop() { ++runId; activeRunId = runId; ready = false; clearTimeout(timeout); timeout = null; if (rejectBoot) rejectBoot(new Error("Pygame 已停止")); resolveBoot = null; rejectBoot = null; bootPromise = null; if (frame) { frame.hidden = true; frame.src = "about:blank"; } }, }; })();