(() => { const canvas = document.getElementById("canvas"); const message = document.getElementById("runnerMessage"); const PYODIDE_URL = "/classroom-assets/vendor/pyodide/"; let pyodide = null; let busy = false; let outputDecoders = {}; function tell(type, details = {}) { window.parent.postMessage({ type, ...details }, "*"); } function showMessage(text) { message.textContent = text; message.hidden = false; } function hideMessage() { message.hidden = true; } function setStream(channel) { return { isatty: false, write(bytes) { const decoder = outputDecoders[channel]; const text = decoder.decode(bytes, { stream: true }); if (text) tell("output", { runId: window.currentRunId, text }); return bytes.length; }, }; } async function initialize() { try { if (typeof loadPyodide !== "function") throw new Error("Pyodide 主程式沒有載入"); pyodide = await loadPyodide({ indexURL: PYODIDE_URL }); pyodide.canvas.setCanvas2D(canvas); pyodide._api._skip_unwind_fatal_error = true; tell("ready"); showMessage("Pygame 已就緒,按下「執行」開始遊戲。"); } catch (error) { const text = `瀏覽器版 Pygame 載入失敗:${String(error)}`; showMessage(text); tell("load_error", { text }); } } async function run(files, runId) { if (!pyodide || busy) return; busy = true; window.currentRunId = runId; outputDecoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; pyodide.setStdout(setStream("stdout")); pyodide.setStderr(setStream("stderr")); pyodide.setStdin({ stdin: () => { throw new Error("瀏覽器版 Pygame 不支援 input();請改用 pygame 的鍵盤或滑鼠事件。"); } }); showMessage("正在載入 Pygame…"); tell("status", { runId, label: "載入 Pygame 套件…" }); try { await pyodide.loadPackage("pygame-ce"); pyodide.runPython(` import os, shutil, sys root = "/home/pyodide/classroom_pygame" shutil.rmtree(root, ignore_errors=True) os.makedirs(root, exist_ok=True) for name, module in list(sys.modules.items()): if str(getattr(module, "__file__", "")).startswith(root): del sys.modules[name] `); for (const [path, content] of Object.entries(files || {})) { if (path.startsWith("/") || path.split(/[\\/]/).includes("..")) continue; const fullPath = `/home/pyodide/classroom_pygame/${path}`; pyodide.FS.mkdirTree(fullPath.slice(0, fullPath.lastIndexOf("/"))); pyodide.FS.writeFile(fullPath, content, { encoding: "utf8" }); } pyodide.globals.set("classroom_entry", "main.py"); canvas.focus(); hideMessage(); tell("status", { runId, label: "遊戲執行中" }); const succeeded = await pyodide.runPythonAsync(` import asyncio, inspect, os, sys, traceback root = "/home/pyodide/classroom_pygame" os.chdir(root) if root not in sys.path: sys.path.insert(0, root) entry = os.path.join(root, classroom_entry) namespace = {"__name__": "__main__", "__file__": entry} try: with open(entry, "r", encoding="utf-8") as source: exec(compile(source.read(), entry, "exec"), namespace) main = namespace.get("main") if inspect.iscoroutinefunction(main): await main() True except BaseException: traceback.print_exc() raise finally: import pygame pygame.quit() `); for (const channel of ["stdout", "stderr"]) { const rest = outputDecoders[channel].decode(); if (rest) tell("output", { runId, text: rest }); } tell(succeeded ? "done" : "error", { runId }); showMessage(succeeded ? "遊戲已結束。按「執行」可重新開始。" : "遊戲發生錯誤,請查看文字輸出。" ); } catch (error) { for (const channel of ["stdout", "stderr"]) { const rest = outputDecoders[channel].decode(); if (rest) tell("output", { runId, text: rest }); } const text = String(error); tell("error", { runId, text }); showMessage("遊戲發生錯誤,請查看文字輸出。"); } finally { busy = false; } } window.addEventListener("message", event => { if (event.source !== window.parent || event.data?.type !== "run") return; run(event.data.files, event.data.runId); }); initialize(); })();