File size: 4,406 Bytes
7547388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
(() => {
  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();
})();