feat: player runtime for the Phone library and That New Teacher
CI / frontend (push) Failing after 17s
CI / backend (push) Failing after 34s

Named stores (PyNamespace: dotted access, functions that see their own store first, restored after a
save is loaded), real generators run step by step (next, send, yield as an expression), len/bool through
__len__/__bool__, os/calendar/itertools/pygame_sdl2 helpers, Skip and ToggleLocalVariable actions,
text tags {glitch} and {wave} and plain display of explode/gradient/ruby, SnowBlossom and anim.Blink,
RoundedCorners as a transform, transforms listed in `at`, ATL choice weights that are flags, and
renpy.image_size from the converter's picture sizes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 22:39:16 +03:00
1 parent 83cc7fdd5c
commit 11f87df712
15 files changed
+457 -24

No files matched your search

+1 -1
View File
@@ -1 +1 @@
[{"op":"class","name":"Perk","bases":[],"body":[{"op":"def","name":"__init__","params":[{"n":"self","d":null,"kind":"p"},{"n":"key","d":null,"kind":"p"}],"body":[{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"key"},"v":{"k":"name","id":"key"}}],"decos":[]},{"op":"def","name":"__hash__","params":[{"n":"self","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"call","f":{"k":"name","id":"hash"},"a":[{"k":"attr","o":{"k":"name","id":"self"},"a":"key"}],"kw":{}}}],"decos":[]},{"op":"def","name":"__eq__","params":[{"n":"self","d":null,"kind":"p"},{"n":"other","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"cmp","l":{"k":"attr","o":{"k":"name","id":"self"},"a":"key"},"ops":["=="],"r":[{"k":"attr","o":{"k":"name","id":"other"},"a":"key"}]}}],"decos":[]}]},{"op":"class","name":"Bag","bases":[],"body":[{"op":"def","name":"__init__","params":[{"n":"self","d":null,"kind":"p"},{"n":"items","d":null,"kind":"p"}],"body":[{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"items"},"v":{"k":"name","id":"items"}}],"decos":[]},{"op":"def","name":"__iter__","params":[{"n":"self","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"call","f":{"k":"name","id":"iter"},"a":[{"k":"attr","o":{"k":"name","id":"self"},"a":"items"}],"kw":{}}}],"decos":[]},{"op":"def","name":"__getitem__","params":[{"n":"self","d":null,"kind":"p"},{"n":"i","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"sub","o":{"k":"attr","o":{"k":"name","id":"self"},"a":"items"},"i":{"k":"name","id":"i"}}}],"decos":[]},{"op":"def","name":"__contains__","params":[{"n":"self","d":null,"kind":"p"},{"n":"x","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"cmp","l":{"k":"name","id":"x"},"ops":["in"],"r":[{"k":"attr","o":{"k":"name","id":"self"},"a":"items"}]}}],"decos":[]}]},{"op":"set","t":{"k":"name","id":"table"},"v":{"k":"dict","v":[]}},{"op":"set","t":{"k":"sub","o":{"k":"name","id":"table"},"i":{"k":"call","f":{"k":"name","id":"Perk"},"a":[{"k":"const","v":"a"}],"kw":{}}},"v":{"k":"const","v":1}},{"op":"set","t":{"k":"sub","o":{"k":"name","id":"table"},"i":{"k":"call","f":{"k":"name","id":"Perk"},"a":[{"k":"const","v":"a"}],"kw":{}}},"v":{"k":"const","v":2}},{"op":"set","t":{"k":"name","id":"result_size"},"v":{"k":"call","f":{"k":"name","id":"len"},"a":[{"k":"name","id":"table"}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_value"},"v":{"k":"sub","o":{"k":"name","id":"table"},"i":{"k":"call","f":{"k":"name","id":"Perk"},"a":[{"k":"const","v":"a"}],"kw":{}}}},{"op":"set","t":{"k":"name","id":"bag"},"v":{"k":"call","f":{"k":"name","id":"Bag"},"a":[{"k":"list","v":[{"k":"const","v":3},{"k":"const","v":4},{"k":"const","v":5}]}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_loop"},"v":{"k":"comp","e":{"k":"bin","op":"*","l":{"k":"name","id":"x"},"r":{"k":"const","v":2}},"gens":[{"t":{"k":"name","id":"x"},"it":{"k":"name","id":"bag"},"if":[]}]}},{"op":"set","t":{"k":"name","id":"result_item"},"v":{"k":"sub","o":{"k":"name","id":"bag"},"i":{"k":"const","v":1}}},{"op":"set","t":{"k":"name","id":"result_in"},"v":{"k":"cmp","l":{"k":"const","v":4},"ops":["in"],"r":[{"k":"name","id":"bag"}]}},{"op":"set","t":{"k":"name","id":"result_out"},"v":{"k":"cmp","l":{"k":"const","v":9},"ops":["in"],"r":[{"k":"name","id":"bag"}]}},{"op":"def","name":"walk","params":[{"n":"values","d":null,"kind":"p"},{"n":"kind","d":null,"kind":"p"}],"body":[{"op":"set","t":{"k":"name","id":"seen"},"v":{"k":"list","v":[]}},{"op":"for","t":{"k":"name","id":"value"},"it":{"k":"name","id":"values"},"body":[{"op":"if","c":{"k":"call","f":{"k":"name","id":"isinstance"},"a":[{"k":"name","id":"value"},{"k":"name","id":"kind"}],"kw":{}},"then":[{"op":"yield","v":{"k":"name","id":"value"}}],"else":[{"op":"if","c":{"k":"call","f":{"k":"name","id":"isinstance"},"a":[{"k":"name","id":"value"},{"k":"name","id":"list"}],"kw":{}},"then":[{"op":"yield_from","v":{"k":"call","f":{"k":"name","id":"walk"},"a":[{"k":"name","id":"value"},{"k":"name","id":"kind"}],"kw":{}}}],"else":[]}]}],"else":[]}],"decos":[],"gen":true},{"op":"set","t":{"k":"name","id":"result_found"},"v":{"k":"call","f":{"k":"name","id":"list"},"a":[{"k":"call","f":{"k":"name","id":"walk"},"a":[{"k":"list","v":[{"k":"const","v":1},{"k":"const","v":"a"},{"k":"list","v":[{"k":"const","v":2},{"k":"const","v":"b"},{"k":"list","v":[{"k":"const","v":3}]}]},{"k":"const","v":4}]},{"k":"name","id":"int"}],"kw":{}}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_iter"},"v":{"k":"call","f":{"k":"name","id":"iter"},"a":[{"k":"list","v":[{"k":"const","v":7},{"k":"const","v":8}]}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_first"},"v":{"k":"call","f":{"k":"name","id":"next"},"a":[{"k":"name","id":"result_iter"}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_second"},"v":{"k":"call","f":{"k":"name","id":"next"},"a":[{"k":"name","id":"result_iter"}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_default"},"v":{"k":"call","f":{"k":"name","id":"next"},"a":[{"k":"name","id":"result_iter"},{"Line truncated
[{"op":"class","name":"Perk","bases":[],"body":[{"op":"def","name":"__init__","params":[{"n":"self","d":null,"kind":"p"},{"n":"key","d":null,"kind":"p"}],"body":[{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"key"},"v":{"k":"name","id":"key"}}],"decos":[]},{"op":"def","name":"__hash__","params":[{"n":"self","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"call","f":{"k":"name","id":"hash"},"a":[{"k":"attr","o":{"k":"name","id":"self"},"a":"key"}],"kw":{}}}],"decos":[]},{"op":"def","name":"__eq__","params":[{"n":"self","d":null,"kind":"p"},{"n":"other","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"cmp","l":{"k":"attr","o":{"k":"name","id":"self"},"a":"key"},"ops":["=="],"r":[{"k":"attr","o":{"k":"name","id":"other"},"a":"key"}]}}],"decos":[]}]},{"op":"class","name":"Bag","bases":[],"body":[{"op":"def","name":"__init__","params":[{"n":"self","d":null,"kind":"p"},{"n":"items","d":null,"kind":"p"}],"body":[{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"items"},"v":{"k":"name","id":"items"}}],"decos":[]},{"op":"def","name":"__iter__","params":[{"n":"self","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"call","f":{"k":"name","id":"iter"},"a":[{"k":"attr","o":{"k":"name","id":"self"},"a":"items"}],"kw":{}}}],"decos":[]},{"op":"def","name":"__getitem__","params":[{"n":"self","d":null,"kind":"p"},{"n":"i","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"sub","o":{"k":"attr","o":{"k":"name","id":"self"},"a":"items"},"i":{"k":"name","id":"i"}}}],"decos":[]},{"op":"def","name":"__contains__","params":[{"n":"self","d":null,"kind":"p"},{"n":"x","d":null,"kind":"p"}],"body":[{"op":"return","v":{"k":"cmp","l":{"k":"name","id":"x"},"ops":["in"],"r":[{"k":"attr","o":{"k":"name","id":"self"},"a":"items"}]}}],"decos":[]}]},{"op":"set","t":{"k":"name","id":"table"},"v":{"k":"dict","v":[]}},{"op":"set","t":{"k":"sub","o":{"k":"name","id":"table"},"i":{"k":"call","f":{"k":"name","id":"Perk"},"a":[{"k":"const","v":"a"}],"kw":{}}},"v":{"k":"const","v":1}},{"op":"set","t":{"k":"sub","o":{"k":"name","id":"table"},"i":{"k":"call","f":{"k":"name","id":"Perk"},"a":[{"k":"const","v":"a"}],"kw":{}}},"v":{"k":"const","v":2}},{"op":"set","t":{"k":"name","id":"result_size"},"v":{"k":"call","f":{"k":"name","id":"len"},"a":[{"k":"name","id":"table"}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_value"},"v":{"k":"sub","o":{"k":"name","id":"table"},"i":{"k":"call","f":{"k":"name","id":"Perk"},"a":[{"k":"const","v":"a"}],"kw":{}}}},{"op":"set","t":{"k":"name","id":"bag"},"v":{"k":"call","f":{"k":"name","id":"Bag"},"a":[{"k":"list","v":[{"k":"const","v":3},{"k":"const","v":4},{"k":"const","v":5}]}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_loop"},"v":{"k":"comp","e":{"k":"bin","op":"*","l":{"k":"name","id":"x"},"r":{"k":"const","v":2}},"gens":[{"t":{"k":"name","id":"x"},"it":{"k":"name","id":"bag"},"if":[]}]}},{"op":"set","t":{"k":"name","id":"result_item"},"v":{"k":"sub","o":{"k":"name","id":"bag"},"i":{"k":"const","v":1}}},{"op":"set","t":{"k":"name","id":"result_in"},"v":{"k":"cmp","l":{"k":"const","v":4},"ops":["in"],"r":[{"k":"name","id":"bag"}]}},{"op":"set","t":{"k":"name","id":"result_out"},"v":{"k":"cmp","l":{"k":"const","v":9},"ops":["in"],"r":[{"k":"name","id":"bag"}]}},{"op":"def","name":"walk","params":[{"n":"values","d":null,"kind":"p"},{"n":"kind","d":null,"kind":"p"}],"body":[{"op":"set","t":{"k":"name","id":"seen"},"v":{"k":"list","v":[]}},{"op":"for","t":{"k":"name","id":"value"},"it":{"k":"name","id":"values"},"body":[{"op":"if","c":{"k":"call","f":{"k":"name","id":"isinstance"},"a":[{"k":"name","id":"value"},{"k":"name","id":"kind"}],"kw":{}},"then":[{"op":"yield","v":{"k":"name","id":"value"}}],"else":[{"op":"if","c":{"k":"call","f":{"k":"name","id":"isinstance"},"a":[{"k":"name","id":"value"},{"k":"name","id":"list"}],"kw":{}},"then":[{"op":"yield_from","v":{"k":"call","f":{"k":"name","id":"walk"},"a":[{"k":"name","id":"value"},{"k":"name","id":"kind"}],"kw":{}}}],"else":[]}]}],"else":[]}],"decos":[],"gen":true},{"op":"set","t":{"k":"name","id":"result_found"},"v":{"k":"call","f":{"k":"name","id":"list"},"a":[{"k":"call","f":{"k":"name","id":"walk"},"a":[{"k":"list","v":[{"k":"const","v":1},{"k":"const","v":"a"},{"k":"list","v":[{"k":"const","v":2},{"k":"const","v":"b"},{"k":"list","v":[{"k":"const","v":3}]}]},{"k":"const","v":4}]},{"k":"name","id":"int"}],"kw":{}}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_iter"},"v":{"k":"call","f":{"k":"name","id":"iter"},"a":[{"k":"list","v":[{"k":"const","v":7},{"k":"const","v":8}]}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_first"},"v":{"k":"call","f":{"k":"name","id":"next"},"a":[{"k":"name","id":"result_iter"}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_second"},"v":{"k":"call","f":{"k":"name","id":"next"},"a":[{"k":"name","id":"result_iter"}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_default"},"v":{"k":"call","f":{"k":"name","id":"next"},"a":[{"k":"name","id":"result_iter"},{"Line truncated
+12
View File
@@ -49,3 +49,15 @@ result_iter = iter([7, 8])
result_first = next(result_iter)
result_second = next(result_iter)
result_default = next(result_iter, "done")
def coro():
got = yield 1
other = yield got + 1
yield other * 2
g = coro()
result_a = next(g)
result_b = g.send(10)
result_c = g.send(5)
+32
View File
@@ -8,6 +8,38 @@ const runtime = original ? JSON.parse(readFileSync(original, "utf8")) : null;
const click = (element) => element.onclick({ stopPropagation() {}, preventDefault() {} });
const enter = (engine, label) => engine.restart(() => engine.guardedJump(label));
test("MIST's actual movie Skip survives a queued redraw and returns before its timer", { skip: !runtime }, async () => {
const { engine, root, window } = makeEngine(runtime);
globalThis.HTMLVideoElement = window.HTMLVideoElement ?? class {};
const played = [], stopped = [];
engine.audio.play = (...args) => played.push(args);
engine.audio.stop = (...args) => stopped.push(args);
engine.audio.mediaOf = () => null;
try {
engine.newStore();
Object.assign(engine.store, { movielength: 63, movieplaying: "images/animations/movies/Intro_Movie.webm" });
const completed = engine.exec({ op: "call_screen", name: "custommoviescreen", args: [], kw: {} });
const button = [...root.querySelectorAll(".pg-sbtn")].find((element) => element.textContent === "Skip");
assert.ok(button);
assert.equal(button.classList.contains("pg-sdisabled"), false);
engine.screens.refresh();
window.dispatchEvent(new window.Event("pointerdown"));
await until(() => engine.store.movielength < 63);
assert.equal(button.isConnected, true);
window.dispatchEvent(new window.Event("pointerup"));
click(button);
await completed;
assert.equal(engine.store._return, 0);
assert.ok(engine.store.movielength > 60, "Skip returns without waiting for the movie timer");
assert.equal(root.querySelector(".pg-sbtn"), null);
assert.equal(played[0][0], "movie");
assert.equal(stopped.at(-1)[0], "movie");
} finally {
window.dispatchEvent(new window.Event("pointercancel"));
engine.destroy();
}
});
test("MIST's actual raise-hand loop accepts screen input and resumes after a save", { skip: !runtime }, async () => {
const { engine, root } = makeEngine(runtime);
try {
+27
View File
@@ -129,4 +129,31 @@ test("generator functions hand out what they yield; iter and next walk a sequenc
assert.equal(store.result_first, 7);
assert.equal(store.result_second, 8);
assert.equal(store.result_default, "done");
assert.deepEqual([store.result_a, store.result_b, store.result_c], [1, 11, 10]); // send() answers the yield
});
test("named stores keep their names apart, are reached through dotted paths, and survive a save", () => {
const { py, store } = runtime();
const call = (f, ...a) => ({ k: "call", f, a, kw: {} });
const name = (id) => ({ k: "name", id });
const attr = (o, a) => ({ k: "attr", o, a });
const c = (v) => ({ k: "const", v });
py.exec([{ op: "in_store", ns: "phone.chat", body: [
{ op: "set", t: name("counter"), v: c(0) },
{ op: "def", name: "bump", params: [], body: [
{ op: "global", names: ["counter"] },
{ op: "set", t: name("counter"), v: { k: "bin", op: "+", l: name("counter"), r: c(1) } },
{ op: "return", v: name("counter") }] },
] }]);
assert.equal(store.counter, undefined, "the name stays inside the store");
assert.equal(py.eval(call(attr(attr(name("phone"), "chat"), "bump"))), 1);
assert.equal(py.eval(call(attr(attr(name("phone"), "chat"), "bump"))), 2);
py.assignPath("phone.chat.mode", "night");
assert.equal(py.eval(attr(attr(name("phone"), "chat"), "mode")), "night");
const saved = JSON.parse(JSON.stringify({ phone: store.phone }));
assert.equal(saved.phone.a.chat.a.counter, 2);
assert.equal(saved.phone.a.chat.a.bump, undefined, "functions are rebuilt by init, not saved");
py.assignPath("phone.chat.counter", 99);
py.restoreNamespaces({ phone: decodeValue(saved).phone });
assert.equal(py.eval(attr(attr(name("phone"), "chat"), "counter")), 2);
});
+22
View File
@@ -294,3 +294,25 @@ test("a redraw keeps the playing video, and one held down button is not rebuilt
window.dispatchEvent(new window.Event("pointerup"));
await wait(30);
});
test("a redraw queued before pointerdown preserves the Return button until click", async () => {
const { manager } = setup();
const returned = [];
try {
manager.show("shop", ["skipped"], {}, { call: true, onReturn: (value) => returned.push(value) });
const button = buttons(manager.layer).find((element) => element.textContent === "Cancel");
manager.refresh(); // MIST's movie timer (or hover) queues the next frame.
window.dispatchEvent(new window.Event("pointerdown"));
await settle();
assert.equal(button.isConnected, true, "a queued redraw must also respect a held pointer");
window.dispatchEvent(new window.Event("pointerup"));
click(button);
assert.deepEqual(returned, [null]);
await settle();
assert.notEqual(buttons(manager.layer).find((element) => element.textContent === "Cancel"), button,
"the deferred redraw runs after the click");
} finally {
window.dispatchEvent(new window.Event("pointercancel"));
manager.destroy();
}
});