summaryrefslogtreecommitdiff
path: root/2026/games_submissions/liquidcake1
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-26 23:47:59 -0400
committerLLLL Colonq <llll@colonq>2026-08-26 23:47:59 -0400
commit0d2d8cd897e94bd94e0b3ae7c6ce9aa580a44a0a (patch)
treee9039d1c3aedca515d6cb499451d4b73aca4f5fd /2026/games_submissions/liquidcake1
parent242943e5b023ce40eb3be826c418a478d4672f78 (diff)
Add a_tension_span
Diffstat (limited to '2026/games_submissions/liquidcake1')
-rw-r--r--2026/games_submissions/liquidcake1/LICENSE21
-rw-r--r--2026/games_submissions/liquidcake1/funge.mjs42
-rw-r--r--2026/games_submissions/liquidcake1/index.html415
-rw-r--r--2026/games_submissions/liquidcake1/instructions.mjs265
-rw-r--r--2026/games_submissions/liquidcake1/interpreter.mjs449
-rw-r--r--2026/games_submissions/liquidcake1/jit.mjs259
-rw-r--r--2026/games_submissions/liquidcake1/queue.mjs30
-rw-r--r--2026/games_submissions/liquidcake1/wasm.mjs51
8 files changed, 1532 insertions, 0 deletions
diff --git a/2026/games_submissions/liquidcake1/LICENSE b/2026/games_submissions/liquidcake1/LICENSE
new file mode 100644
index 0000000..847e49b
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 liquidcake1
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/2026/games_submissions/liquidcake1/funge.mjs b/2026/games_submissions/liquidcake1/funge.mjs
new file mode 100644
index 0000000..a6fb723
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/funge.mjs
@@ -0,0 +1,42 @@
+let file_name = process.argv[2];
+
+import { readFileSync } from "fs";
+
+let field_s = readFileSync(file_name, "utf8");
+//console.log(field_s);
+
+let field = field_s.split("\n");
+field.pop();
+field = field.map( x => x.split("").map( c => c.charCodeAt(0) ) );
+//console.log(field);
+
+import { Interpreter } from "./interpreter.mjs";
+import { load_wasm } from "./wasm.mjs";
+import { overlay as SOCK_overlay } from "./overlays/SOCK.mjs";
+import { overlay as S_overlay } from "./overlays/S.mjs";
+import { overlay as WS_overlay } from "./overlays/WS.mjs";
+
+let interpreter = new Interpreter();
+
+interpreter.load_overlay("S", S_overlay);
+interpreter.load_overlay("SOCK", SOCK_overlay);
+interpreter.load_overlay("WS", WS_overlay);
+
+let instance = await load_wasm(interpreter);
+
+interpreter.field = field;
+interpreter.input_queue.push(["set_speed", 100]);
+//interpreter.input_queue.push(["set_speed", 90]);
+interpreter.input_queue.push(["pause"]);
+interpreter.input_queue.push(["unpause"]);
+interpreter.add_handler("char_out", function(arg) { process.stdout.write(arg); });
+//interpreter.add_handler("output_field", function(lines) { lines.forEach(function(line) {process.stdout.write(line + "\n"); })});
+interpreter.add_handler("terminate", function() { process.exit(0); });
+interpreter.go();
+process.stdin.on('data', function (chunk) {
+ chunk.forEach(x => interpreter.input_queue.push(["stdin", x]));
+});
+/*setTimeout(function () {
+ console.log("sending @");
+ interpreter.input_queue.push(["stdin", "@".charCodeAt(0)]);
+}, 2000);*/
diff --git a/2026/games_submissions/liquidcake1/index.html b/2026/games_submissions/liquidcake1/index.html
new file mode 100644
index 0000000..8bff36c
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/index.html
@@ -0,0 +1,415 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <script language="javascript" type="module">
+ import { Interpreter } from "./interpreter.mjs";
+ let interpreter = new Interpreter();
+ interpreter.add_handler("char_out", out);
+ interpreter.add_handler("error_occurred", oute);
+ interpreter.add_handler("cell_changed", handle_set_cell);
+ interpreter.add_handler("output_field", console.log);
+ interpreter.add_handler("thread_created", setup_thread_gui);
+ interpreter.add_handler("thread_paused", handle_thread_paused);
+ interpreter.add_handler("thread_state_updated", update_thread_state);
+ interpreter.add_handler("thread_state_synced", handle_thread_state_synced);
+ interpreter.add_handler("terminate", handle_thread_state_synced);
+ interpreter.add_handler("thread_dead", handle_thread_dead);
+ interpreter.max_loops = 1000;
+ window.befunge_interpreter = interpreter;
+ var start_time;
+ var end_time;
+ var difficulty;
+ async function handle_message(ev) {
+ console.log("handle_message", ev);
+ console.log("handle_message", ev.data);
+ console.log("handle_message", ev.data.op);
+ if (ev.data.op == "start") {
+ winner = loser = false;
+ console.log("Picking level");
+ await pick_level();
+ set_speed();
+ console.log("Picked level; explaining started.");
+ window.parent.postMessage({op: "started", verb: "catch!"});
+ difficulty = ev.data.difficulty;
+ let timeout;
+ if (difficulty < 10) {
+ timeout = 150000 / (10 + difficulty);
+ } else {
+ timeout = 5000 + 150000 / difficulty;
+ }
+ console.log("Starting... with difficulty ", difficulty, "which is", timeout);
+ start_time = new Date().getTime();
+ end_time = start_time + timeout;
+ check_time();
+ go();
+ let code = document.getElementById("code");
+ code.style.animation = "none";
+ window.setTimeout(function () {
+ code.style.animation = "";
+ }, 10);
+ console.log("Started.");
+ }
+ }
+ function check_time() {
+ if (winner) {
+ console.log("Check time: winner");
+ return;
+ }
+ let now = new Date().getTime();
+ let fraction_done = (now - start_time) / (end_time - start_time);
+ let bomb = document.getElementById("bomb");
+ if (fraction_done > 1) {
+ console.log("Check time: loser");
+ loser = true;
+ bomb.innerText = "💥";
+ interpreter.pause();
+ return;
+ }
+ let ticks = 10 - Math.ceil(fraction_done * 10);
+ bomb.innerText = "💣" + ".".repeat(ticks) + "💥";
+ window.setTimeout(check_time, 50);
+
+ }
+ async function load() {
+ document.getElementById("speed").addEventListener("input", set_speed);
+ document.addEventListener("mousemove", function (e) {
+ let follower = document.getElementById("follower");
+ follower.style.left = e.pageX + "px";
+ follower.style.top = e.pageY + "px";
+ });
+ set_speed();
+ window.addEventListener("message", handle_message);
+ window.parent.postMessage({op: "ready"});
+ }
+ let levels = [
+ '"!dlrow ,olleH",,,,,,,,,,,,,@',
+ '"H","e","l","l","o",","," "v\n @,"!","d","l","r","o","w",<',
+ '0>:2g:,"!"v\n ^_@#\\+1\\-<\nHello, world!',
+ ];
+ let current_level;
+ let removed;
+ let placed_row = -1;
+ let placed_col = -1;
+ function test_level(content) {
+ let test_int = new Interpreter();
+ let out = "";
+ let fulfil;
+ let test_stopping = false;
+ test_int.add_handler("char_out", function (c) { out += c; });
+ test_int.add_handler("thread_paused", function () { if (test_stopping) return; test_stopping = true; test_int.stop(); });
+ test_int.add_handler("thread_dead", function (thread) { console.log("dead!"); fulfil([out, thread.tick_count]); });
+ test_int.max_loops = 10000;
+ test_int.set_speed(100)
+ let lines = content.split("\n");
+ let width = 0;
+ for(let i=0; i<lines.length; i++) {
+ if (lines[i].length > width) {
+ width = lines[i].length;
+ }
+ }
+ for(let i=0; i<lines.length; i++) {
+ test_int.field.push([]);
+ for(let j=0; j<width; j++) {
+ if (j < lines[i].length) {
+ test_int.field[i].push(lines[i].charCodeAt(j))
+ } else {
+ test_int.field[i].push(32);
+ }
+ }
+ }
+ let p = new Promise(function(f) { fulfil = f; test_int.go(); });
+ return p;
+ }
+ function ticks_to_speed(ticks) {
+ let speed = Math.log(ticks / 100) / Math.log(1.1);
+ if (ticks < 100) {
+ speed -= 20;
+ }
+ return speed;
+ }
+ async function pick_level() {
+ let level_idx = Math.floor(Math.random() * levels.length);
+ const base_level = levels[level_idx];
+ let base_res = await test_level(base_level);
+ if (base_res[0] == "Hello, world!") {
+ console.log("Pre-edit validated!");
+ } else {
+ console.log("Ooops!");
+ }
+ let tries = 0;
+ while(tries < 100) {
+ tries += 1;
+ let idx = Math.floor(Math.random() * base_level.length);
+ removed = base_level[idx];
+ if (removed == "\n") {
+ continue;
+ }
+ let replace;
+ if (difficulty < 10) {
+ replace = " ";
+ } else {
+ replace = String.fromCharCode(Math.floor(Math.random() * 94) + 33);
+ }
+ current_level = base_level.slice(0, idx) + replace + base_level.slice(idx + 1);
+ console.log(idx);
+ let current_res = await test_level(current_level);
+ if (current_res[0] == "Hello, world!") {
+ console.log("Ooops! Try again!");
+ } else {
+ console.log("Post-edit validated!");
+ document.getElementById("to_place").innerText = removed;
+ document.getElementById("follower_cell").innerText = removed;
+ let ticks = current_res[1];
+ if (ticks > base_res[1] * 2) { ticks = base_res[1] * 2; }
+ document.getElementById("speed").value = ticks_to_speed(ticks);
+ if (interpreter.instructions_raw.hasOwnProperty(removed)) {
+ document.getElementById("place_description").innerText = interpreter.instructions_raw[removed].desc;
+ } else {
+ document.getElementById("place_description").innerText = "not an instruction";
+
+ }
+ console.log("level picked!");
+ return;
+ }
+ }
+ }
+ function set_speed(e) {
+ let raw_speed = document.getElementById("speed").value;
+ interpreter.set_speed(raw_speed);
+ }
+ function setup_thread_gui(thread) {
+ const this_thread = thread;
+ let thb = document.createElement("th");
+ let table = document.createElement("table");
+ let tr = document.createElement("tr");
+ let th = document.createElement("th");
+ tr.appendChild(thb);
+ tr.appendChild(th);
+ table.appendChild(tr);
+ let thread_info = document.createElement("p");
+ thread_info.appendChild(table);
+ document.getElementById("threads").appendChild(thread_info);
+ thread.info_elt = thread_info;
+ }
+ function update_thread_state(thread) {
+ let tr = thread.info_elt.children[0].children[0];
+ tr.children[1].innerText = `(${("" + thread.col).padStart(3, "\u00a0")},${("" + thread.row).padStart(3, "\u00a0")})`;
+ while(tr.children.length > thread.stack.length + 2) {
+ tr.removeChild(tr.children[tr.children.length - 1]);
+ }
+ while(tr.children.length < thread.stack.length + 2) {
+ tr.appendChild(document.createElement("td"));
+ }
+ thread.stack.forEach(function (val, idx) {
+ if (val > 32 && val < 127) {
+ val = String.fromCharCode(val);
+ }
+ tr.children[thread.stack.length - idx + 1].innerText = val;
+ });
+ }
+ let winner = false;
+ let loser = false;
+ let just_placed = false;
+ async function go() {
+ if (winner || loser) {
+ if (winner) {
+ console.log("We are winner!");
+ } else {
+ console.log("We are loser!");
+ }
+ window.parent.postMessage({op: "done", win: winner});
+ document.getElementById("out").classList.remove("right");
+ placed_col = placed_row = -1;
+ placed_td = undefined;
+ let table = document.getElementById("table");
+ while(table.children.length > 0) {
+ table.removeChild(table.children[table.children.length-1]);
+ interpreter.field.pop();
+ }
+ document.getElementById("out").innerText = "";
+ return;
+ }
+ if (document.getElementById("out").innerText == "Hello, world!") {
+ // WIN!
+ winner = true;
+ document.getElementById("out").classList.add("right");
+ placed_td.classList.add("right");
+ placed_td.classList.remove("wrong");
+ //console.log(dead_thread_ticks, ticks_to_speed(dead_thread_ticks / 10));
+ interpreter.set_speed(ticks_to_speed(dead_thread_ticks / 5));
+ } else if (just_placed) {
+ // Run once, super fast. We'll get back here immediately.
+ interpreter.set_speed(100);
+ just_placed = false;
+ load_level(current_level);
+ document.getElementById("out").innerText = "";
+ interpreter.go();
+ return;
+ } else {
+ set_speed();
+ }
+ document.getElementById("outlast").innerHTML = document.getElementById("out").innerHTML;
+ document.getElementById("out").innerText = "";
+ load_level(current_level);
+ interpreter.go();
+ }
+ function set_cell(col, row, val) {
+ interpreter.set_cell(col, row, val);
+ }
+ function handle_set_cell(col, row, val) {
+ let val_str = String.fromCharCode(val);
+ document.getElementById("table").children[row].children[col].innerText = val_str == " " ? "\u00a0" : val_str;
+ let title = `(${col},${row})=${val} (${val_str})`;
+ if (interpreter.instructions_raw[val_str] !== undefined)
+ title += ": " + interpreter.instructions_raw[val_str].desc;
+ document.getElementById("table").children[row].children[col].title = title;
+ }
+ function out(s) {
+ document.getElementById("out").appendChild(document.createTextNode(s));
+ }
+ function oute(s) {
+ if (s == "ERROR: Normal termination!" || s.startsWith("State was: ")) {
+ return;
+ }
+ let span = document.createElement("p");
+ span.className = "error";
+ span.innerText = s;
+ document.getElementById("out").appendChild(span);
+ }
+ let highlighted_cells = [];
+ function handle_thread_state_synced(threads) {
+ threads ||= [];
+ let new_highlighted_cells = threads.map(thread => document.getElementById("table").children[thread.row].children[thread.col]);
+ highlighted_cells.filter(x => !new_highlighted_cells.includes(x)).forEach(
+ cell => cell.classList.remove("active"));
+ highlighted_cells = new_highlighted_cells;
+ highlighted_cells.forEach(cell => cell.classList.add("active"));
+ }
+ let stopping;
+ function handle_thread_paused(paused_thread) {
+ if (stopping) {
+ // We'll unpause and then pause again...
+ return;
+ }
+ stopping = true;
+ interpreter.stop();
+ }
+ let dead_thread_ticks;
+ function handle_thread_dead(dead_thread) {
+ dead_thread_ticks = dead_thread.tick_count;
+ stopping = false;
+ document.getElementById("threads").removeChild(dead_thread.info_elt);
+ go();
+ }
+ let placed_td;
+ function click_cell(col, row, td) {
+ if (winner) {
+ return;
+ }
+ placed_col = col;
+ placed_row = row;
+ if (placed_td) {
+ placed_td.classList.remove("wrong");
+ }
+ placed_td = td;
+ td.classList.add("wrong");
+ set_cell(placed_col, placed_row, removed.charCodeAt(0));
+ just_placed = true;
+ interpreter.pause();
+ }
+ function load_level(content) {
+ const lines = content.split("\n");
+ const height = lines.length;
+ let width = 0;
+ for(let i=0; i<height; i++) {
+ if (lines[i].length > width) {
+ width = lines[i].length;
+ }
+ }
+ let table = document.getElementById("table");
+ while(table.children.length > height) {
+ table.removeChild(table.children[table.children.length-1]);
+ interpreter.field.pop();
+ }
+ while(table.children.length < height) {
+ table.appendChild(document.createElement("tr"));
+ interpreter.field.push([]);
+ }
+ for(let i=0; i<height; i++) {
+ let line;
+ if (i >= lines.length)
+ line = "";
+ else
+ line = lines[i];
+ let row = table.children[i];
+ while(row.children.length > width) {
+ row.removeChild(row.children[row.children.length-1]);
+ interpreter.field[i].pop();
+ }
+ while(row.children.length < width) {
+ const td = document.createElement("td");
+ const rown = i;
+ const coln = row.children.length;
+ td.addEventListener("click", function() { click_cell(coln, rown, td); });
+ row.appendChild(td);
+ interpreter.field[i].push(32);
+ }
+ for(let j=0; j<width; j++) {
+ let val;
+ if (j >= line.length)
+ val = " ";
+ else
+ val = line[j];
+ if (j == placed_col && i == placed_row) {
+ val = removed;
+ }
+ set_cell(j, i, val.charCodeAt(0));
+ }
+ }
+ }
+ window.addEventListener("load", load);
+ </script>
+<style>
+body { font-size: 4vh; }
+table { border: black solid 1px; padding: 0px; }
+th { font-family: monospace; font-weight: normal; }
+td { font-family: monospace; background: lightgrey; width: 1em; padding: 0px; text-align: center; }
+.out1 > p { display: inline-block; margin-top: 0; margin-bottom: 0; min-height: 1em; }
+.out1 > div { font-family: monospace; border: black solid 1px; display: inline-block; white-space-collapse: preserve; }
+.out1 > p:empty::before {
+ content: "";
+ display: inline-block;
+}
+.error { background-color: red; margin: 0px; }
+.active { background-color: lightpink; }
+.wrong { background-color: orange; }
+.right { background-color: green; }
+.follower { position: absolute; transform: translate(-50%, -50%); pointer-events: none; }
+body { cursor: none; }
+#threads { border: black solid 1px; padding: 0px; min-height: 2em;}
+#threads > p { margin: 0px; }
+@keyframes fadeOut {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+#code { position: absolute; font-weight: bold; font-size: 50vh; pointer-events: none; animation: fadeOut 2s ease-out forwards; background: white; pointer-events: none; }
+</style>
+ <title>Befunge</title>
+ </head>
+ <body>
+ <div id="code">Code!</div>
+ <div style="display: inline-block;"><big>Placing:</big></div><table style="display: inline-block;"><tr><td id="to_place"></td><th>Which does: <div id="place_description" style="display: inline-block;"></div></th></tr></table>
+ <div style="display: inline-block;">Speed <input type="range" min="-100" max="150" value="40" id="speed" style="height: 1em;"/></div>
+ <br/>
+ <div id="threads"></div>
+ <table id="table">
+ </table>
+ <div class="out1"><p>Output:</p><div id="out"></div></div>
+ <div class="out1"><p>Previous:</p><div id="outlast"></div></div>
+ <div class="out1"><p>Target:</p><div>Hello, world!</div></div>
+ <div id="follower" class="follower"><table><tr><td id="follower_cell">x</td></tr></table></div>
+ <div>Don't get exploded: <code id="bomb">💣......💥</code></div>
+ </body>
+</html>
diff --git a/2026/games_submissions/liquidcake1/instructions.mjs b/2026/games_submissions/liquidcake1/instructions.mjs
new file mode 100644
index 0000000..58c6aab
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/instructions.mjs
@@ -0,0 +1,265 @@
+export let instructions_raw = {
+ "+": {
+ impl: function (thread) { thread.stack.push(thread.pop() + thread.pop()); },
+ desc: "b a → b + a",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 2] += stack.pop();",
+ },
+ "-": {
+ impl: function (thread) { thread.stack.push(-thread.pop() + thread.pop()); },
+ desc: "b a → b - a",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 2] -= stack.pop();",
+ },
+ "*": {
+ impl: function (thread) { thread.stack.push(thread.pop() * thread.pop()); },
+ desc: "b a → b * a",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 2] *= stack.pop();",
+ },
+ "/": {
+ impl: function (thread) { let a = thread.pop(); let b = thread.pop(); thread.stack.push(Math.round(b / a)); },
+ desc: "b a → b // a",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 2] /= stack.pop();",
+ },
+ "%": {
+ impl: function (thread) { let a = thread.pop(); let b = thread.pop(); thread.stack.push(b % a); },
+ desc: "b a → b % a",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 2] %= stack.pop();",
+ },
+ "!": {
+ impl: function (thread) { thread.stack.push(thread.pop() == 0 ? 1 : 0); },
+ desc: "a → a == 0 ? 1 : 0",
+ can_jit: true,
+ stack_min: 1,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 1] = stack[stack.length - 1] ? 1 : 0;",
+ },
+ "`": {
+ impl: function (thread) { thread.stack.push(thread.pop() < thread.pop() ? 1 : 0); },
+ desc: "b a → b > a ? 1 : 0",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "stack[stack.length - 2] = stack.pop() < stack[stack.length - 1] ? 0 : 1;",
+ },
+ "<": {
+ impl: function (thread) { thread.cold = -1; thread.rowd = 0; },
+ desc: "() → (); go left",
+ stack_min: 0,
+ stack_return: 0,
+ unchecked_js_code: "",
+ can_jit: true,
+ },
+ ">": {
+ impl: function (thread) { thread.cold = 1; thread.rowd = 0; },
+ desc: "() → (); go right",
+ stack_min: 0,
+ stack_return: 0,
+ unchecked_js_code: "",
+ can_jit: true,
+ },
+ "^": {
+ impl: function (thread) { thread.cold = 0; thread.rowd = -1; },
+ desc: "() → (); go up",
+ stack_min: 0,
+ stack_return: 0,
+ unchecked_js_code: "",
+ can_jit: true,
+ },
+ "v": {
+ impl: function (thread) { thread.cold = 0; thread.rowd = 1; },
+ desc: "() → (); go down",
+ stack_min: 0,
+ stack_return: 0,
+ unchecked_js_code: "",
+ can_jit: true,
+ },
+ "?": {
+ impl: function (thread) { let x = Math.floor(Math.random() * 4); thread.cold = ((x % 2) * 2 - 1) * Math.floor(x / 2); thread.rowd = ((x % 2) * 2 - 1) * ( 1 - Math.floor(x / 2)); },
+ desc: "() → (); go random cardinal",
+ can_jit: false,
+ },
+ "_": {
+ impl: function (thread) { thread.rowd = 0; thread.cold = thread.pop() == 0 ? 1 : -1; },
+ desc: "a → (); a == 0 ? go right : go left",
+ can_jit: false,
+ },
+ "|": {
+ impl: function (thread) { thread.cold = 0; thread.rowd = thread.pop() == 0 ? 1 : -1; },
+ desc: "a → (); a == 0 ? go down : go up",
+ can_jit: false,
+ },
+ '"': {
+ impl: function (thread) { thread.mode = "string"; },
+ desc: "; toggle string mode",
+ can_jit: false, // TODO this should be OK, but we'll need JIT for string mode.
+ stack_min: 0,
+ stack_return: 0,
+ },
+ ":": {
+ impl: function (thread) { thread.stack.push(thread.stack[thread.stack.length-1]); },
+ desc: "a → a a",
+ can_jit: true,
+ stack_min: 1,
+ stack_return: 2,
+ unchecked_js_code: "stack.push(stack[stack.length-1]);",
+ },
+ "\\": {
+ impl: function (thread) { let a = thread.pop(); var b = thread.pop(); thread.stack.push(a, b); },
+ desc: "b a → a b",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 2,
+ unchecked_js_code: "stack.push(stack.pop(), stack.pop());",
+ },
+ "$": {
+ impl: function (thread) { thread.pop(); },
+ desc: "a → ()",
+ can_jit: true,
+ stack_min: 1,
+ stack_return: 0,
+ unchecked_js_code: "stack.pop();",
+ },
+ ".": {
+ impl: function (thread) { thread.interpreter.out(thread.pop() + " "); },
+ desc: "a → (); output a",
+ can_jit: false,
+ },
+ ",": {
+ impl: function (thread) { thread.interpreter.out(String.fromCharCode(thread.pop())); },
+ desc: "a → (); output chr(a)",
+ can_jit: false,
+ },
+ "#": {
+ impl: function (thread) { thread.col += thread.cold; thread.row += thread.rowd; },
+ desc: "; jump one cell",
+ can_jit: true,
+ stack_min: 0,
+ stack_return: 0,
+ unchecked_js_code: "",
+ },
+ "g": {
+ impl: function (thread) { let row = thread.pop(); var col = thread.pop(); thread.stack.push(thread.interpreter.get_field(thread, col, row)); },
+ desc: "col row → field[row][col]",
+ can_jit: true,
+ stack_min: 2,
+ stack_return: 1,
+ unchecked_js_code: "{let row = stack.pop(); stack[stack.length-1] = (thread.interpreter.get_field(thread, stack[stack.length-1], row));}",
+ },
+ "p": {
+ impl: function (thread) { let row = thread.pop(); var col = thread.pop(); var val = thread.pop(); thread.interpreter.set_field(thread, col, row, val); },
+ desc: "val col row → (); field[row][col] = val",
+ can_jit: true,
+ stack_min: 3,
+ stack_return: 0,
+ unchecked_js_code: function (thread_state) {
+ return `
+ {
+ let row = stack.pop();
+ let col = stack.pop();
+ thread.interpreter.set_field(thread, col, row, stack.pop());
+ for (let i=0; i<jit.path.length; i++) {
+ if (jit.path[i][0] == row && jit.path[i][1] == col) {
+ thread.row = ${thread_state.row} + ${thread_state.rowd};
+ thread.col = ${thread_state.col} + ${thread_state.cold};
+ thread.rowd = ${thread_state.rowd};
+ thread.cold = ${thread_state.cold};
+ return;
+ }
+ }
+ }`},
+ },
+ "t": {
+ impl: function (thread) { thread.interpreter.new_thread(thread.split_thread()); },
+ desc: "; create new thread in reverse direction",
+ can_jit: false,
+ },
+ // "&" input character TODO
+ "~": { // TODO handle EOF properly (push nothing)
+ impl: function (thread) {
+ if (thread.interpreter.stdin_queue.length > 0) {
+ console.log("Have data!");
+ thread.stack.push(thread.interpreter.stdin_queue.pop());
+ } else {
+ console.log("Have no data!");
+ return new Promise(r => thread.interpreter.stdin_waiters.push(r)).then(function (char_code) {thread.stack.push(char_code); thread.blocked = false;});
+ }
+ },
+ desc: "() → x; read a character from input",
+ can_jit: false,
+ },
+ "@": {
+ impl: function (thread) { thread.interpreter.output_error(thread, "Normal termination!"); thread.running = false; },
+ desc: "; stop current thread",
+ can_jit: false,
+ },
+ "]": {
+ impl: function (thread) { thread.interpreter.output_error(thread, "Debug!"); },
+ desc: "; dump thread debug",
+ can_jit: false,
+ },
+ " ": {
+ impl: function (thread) { },
+ desc: "; no-op",
+ can_jit: true,
+ stack_min: 0,
+ stack_return: 0,
+ unchecked_js_code: "",
+ },
+ "(": {
+ impl: function (thread) {
+ const n = thread.pop();
+ let x = 0;
+ for(let i=0; i<n; i++) {
+ x = x * 256 + thread.pop();
+ }
+ if (thread.interpreter.overlays[x]) {
+ // We don't support ")" anyway so for now don't
+ // need to know how to undo...
+ for(let [k, v] of Object.entries(thread.interpreter.overlays[x])) {
+ thread.overlays[k.charCodeAt(0)] = v.impl;
+ }
+ thread.stack.push(x);
+ thread.stack.push(1);
+ } else {
+ // Failure, reverse direction.
+ thread.cold *= -1;
+ thread.rowd *= -1;
+ }
+ },
+ desc: "xn ... x1 n → f=(x1 + x2*256 + ... + xn*256^(n-1)) 1 | (); load semantic f or reverse",
+ can_jit: false, // Varargs approach makes JIT hard!
+ },
+};
+
+// Add numbers 0-9
+for(let i=0; i<10; i++) {
+ const j = i; // Prevent any capture shenanigans.
+ instructions_raw[i] = {
+ impl: function (state) { state.stack.push(j); },
+ desc: `() → ${i}`,
+ can_jit: true,
+ stack_min: 0,
+ stack_return: 1,
+ unchecked_js_code: `stack.push(${i});`,
+ };
+}
+
+// Fast access instructions array.
+export let instructions = {};
+for(let [s, val] of Object.entries(instructions_raw)) {
+ instructions[s.charCodeAt(0)] = val.impl;
+}
diff --git a/2026/games_submissions/liquidcake1/interpreter.mjs b/2026/games_submissions/liquidcake1/interpreter.mjs
new file mode 100644
index 0000000..703b2d7
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/interpreter.mjs
@@ -0,0 +1,449 @@
+// JIT TODO:
+// * OK, it's probably about 6x faster with JIT than without, but we can go another 4-5x faster by loop unrolling.
+// * We don't bounds-check the stack before executing a JITted routine.
+// * We can probably do a lot better in speed by unrolling and aliasing the stack. Name top N stack vars a1 to aN,
+// then unpack the top N into those, then run code, then pack aN to a?.
+// * We don't have support for branches, even just the dumb "primary only, bail otherwise" variety.
+// * We don't ever remove JIT. We should destroy JIT when a cell is modified.
+// * If a cell is modified _DURING_ a JITted sequence, what do? If external, we can just claim it doesn't matter.
+// If the thread self-modified, we'll need a means to abort the JITted sequence early.
+// * There is no JIT visualisation.
+
+import { instructions, instructions_raw } from "./instructions.mjs";
+import { Queue } from "./queue.mjs";
+import { Jit } from "./jit.mjs";
+
+function gen_fingerprint(s) {
+ let x = 0;
+ for(let c of Array.from(s).map(x => x.charCodeAt(0))) {
+ x = x * 256 + c;
+ }
+ return x;
+}
+
+function deep_copy(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+function shallow_copy(obj) {
+ return Object.fromEntries(Object.entries(obj));
+}
+
+function shallowish_copy(obj, i) {
+ if (i > 0) {
+ return shallowish_copy(obj, i - 1);
+ } else {
+ return shallow_copy(obj);
+ }
+}
+
+class Thread {
+ running = true;
+ mode = "normal";
+ jit = null;
+ tick_count = 0;
+
+ constructor(interpreter, col, row, cold, rowd, stack, overlays) {
+ this.interpreter = interpreter;
+ this.col = col;
+ this.row = row;
+ this.cold = cold;
+ this.rowd = rowd;
+ this.stack = stack;
+ this.overlays = overlays || {};
+ }
+
+ split_thread() {
+ let thread = new Thread(
+ this.interpreter,
+ this.col,
+ this.row,
+ -this.cold,
+ -this.rowd,
+ Array.from(this.stack),
+ shallow_copy(this.overlays),
+ );
+ thread.tick_forwards();
+ thread.tick_count = this.tick_count; // Ensure new thread has equal priority to old thread.
+ return thread;
+ }
+
+ tick_forwards() {
+ this.col += this.cold;
+ this.row += this.rowd;
+ if (this.row == this.interpreter.field.length && this.rowd > 0) this.row = 0;
+ else if (this.row == -1 && this.rowd < 0) this.row = this.interpreter.field.length - 1;
+ if (this.col == this.interpreter.field[this.row].length && this.cold > 0) this.col = 0;
+ else if (this.col == -1 && this.cold < 0) this.col = this.interpreter.field[this.row].length - 1;
+ }
+
+ pop() {
+ if (this.stack.length > 0) {
+ return this.stack.pop();
+ } else {
+ this.interpreter.error(this, "Stack underflow");
+ return 0;
+ }
+ }
+}
+
+export class Interpreter {
+ max_loops = 1000000000000;
+ ticks = 0;
+ paused_wake = null;
+ paused_event = null;
+ slice_loops = 1;
+ slice_sleep = 20;
+ running = false;
+ threads = [];
+ events = {};
+ awake_sleep = null;
+ stats = [];
+ field = [];
+ instructions = shallow_copy(instructions);
+ instructions_raw = shallow_copy(instructions_raw);
+ overlays = {};
+ input_queue = new Queue(); // Queue things like: change speed, input, etc.
+ stdin_queue = new Queue(); // Chars from stdin.
+ stdin_waiters = [];
+ handlers = {};
+
+ constructor() {
+ this.jit = new Jit(this);
+ }
+
+ load_overlay(fingerprint, overlay) {
+ this.overlays[gen_fingerprint(fingerprint)] = overlay;
+ }
+
+ add_handler(event_name, f) {
+ if (this.handlers[event_name] === undefined) {
+ this.handlers[event_name] = [];
+ }
+ this.handlers[event_name].push(f);
+ }
+
+ trigger_event(event_name, ...args) {
+ //console.log(`Event: ${event_name} -- ${args}`);
+ let handlers = this.handlers[event_name];
+ if (handlers === undefined) {
+ //console.log(`Unhandled event: ${event_name} -- ${args}`);
+ } else {
+ handlers.forEach(f => f(...args));
+ }
+ }
+
+ set_speed(raw_speed) {
+ if (raw_speed > 0) {
+ this.slice_sleep = 1;
+ this.slice_loops = Math.floor(1.1 ** raw_speed);
+ } else {
+ this.slice_sleep = Math.floor(1.1 ** -raw_speed);
+ this.slice_loops = 1;
+ }
+ this.trigger_event("speed_changed", raw_speed, this.slice_sleep, this.slice_loops);
+ }
+
+ toggle_pause(e) {
+ if (this.paused_wake === null) {
+ this.pause();
+ } else {
+ this.unpause();
+ }
+ }
+
+ pause() {
+ let thisthis = this;
+ this.paused_event = new Promise(function (r) {
+ thisthis.paused_wake = r;
+ // We shouldn't be in the sleep loop if paused.
+ if (thisthis.awake_sleep != null) thisthis.awake_sleep();
+ });
+ // These really need outputting from the main loop, not here!
+ this.trigger_event("paused", true);
+ this.threads.forEach(function (thread) {
+ thisthis.trigger_event("thread_paused", thread, true);
+ });
+ }
+
+ unpause() {
+ let old_wake = this.paused_wake;
+ let thisthis = this;
+ this.paused_wake = null;
+ this.trigger_event("paused", false);
+ this.threads.forEach(function (thread) {
+ thisthis.trigger_event("thread_paused", thread, false)
+ });
+ if (old_wake)
+ old_wake();
+ }
+
+ async sleep_until_unpaused(all_blocked) { // TODO???
+ if (!this.paused_event)
+ await this.sleep(all_blocked ? 50 : this.slice_sleep);
+ if (this.paused_event) {
+ let paused_event = this.paused_event;
+ this.paused_event = null;
+ let unpause_data = await paused_event;
+ return unpause_data;
+ }
+ }
+
+ step(e) {
+ let old_wake = this.paused_wake;
+ this.pause();
+ if (old_wake !== null) {
+ old_wake({count: 1});
+ }
+ }
+
+ step_thread(thread) {
+ let old_wake = this.paused_wake;
+ this.pause();
+ if (old_wake !== null) {
+ old_wake({threads: [thread], count: 1});
+ }
+ }
+
+ async go() {
+ //console.log(this.threads);
+ this.stop();
+ if (this.running_promise != null) {
+ console.log("Waiting for last thread to exit");
+ await this.running_promise;
+ console.log("Last thread has exited");
+ }
+ let thread = new Thread(this, 0, 0, 1, 0, []);
+ this.new_thread(thread);
+ this.trigger_event("started");
+ this.main_loop();
+ }
+
+ new_thread(thread) {
+ this.threads.push(thread);
+ this.trigger_event("thread_created", thread);
+ }
+
+ stop() {
+ if (this.threads.length > 0) {
+ this.trigger_event("stopped");
+ }
+ this.threads.forEach(function (thread) { thread.running = false; });
+ this.unpause();
+ if (this.awake_sleep != null) this.awake_sleep();
+ }
+
+ check(state, col, row) {
+ if (row < 0 || row >= this.field.length) this.error(state, `Row ${row} is out of bounds`);
+ //else if (col < 0 || col >= this.field[row].length) this.error(state, `Col ${col} is out of bounds`);
+ else return true;
+ }
+
+ set_field(state, col, row, val) {
+ if (this.check(state, col, row)) {
+ this.set_cell(col, row, val);
+ } else {
+ console.log(`out of bounds access ${state} ${col} ${row} ${val}`);
+ }
+ }
+
+ set_cell(col, row, val) {
+ let val_str = String.fromCharCode(val);
+ this.field[row][col] = val;
+ let title = `(${col},${row})=${val} (${val_str})`;
+ if (this.instructions_raw[val_str] !== undefined)
+ title += ": " + this.instructions_raw[val_str].desc;
+ this.jit.cell_changed(row, col);
+ this.trigger_event("cell_changed", col, row, val);
+ }
+
+ get_field(state, col, row, val) {
+ if (this.check(state, col, row)) {
+ let val = this.field[row][col];
+ return val === undefined ? 0 : val;
+ }
+ }
+
+ out(s) {
+ this.trigger_event("char_out", s);
+ //console.log("Out: " + s);
+ }
+ oute(s) {
+ this.trigger_event("error_occurred", s);
+ //console.log("Err: " + s);
+ }
+
+ error(thread, message) {
+ thread.running = false;
+ this.output_error(thread, message);
+ }
+
+ output_error(thread, message) {
+ this.oute(`ERROR: ${message}`);
+ this.oute(`State was: row=${thread.row} col=${thread.col} stack=${thread.stack}`);
+ this.output_field();
+ }
+
+ output_field() {
+ let lines = [];
+ for(let row=0; row<this.field.length; row++) {
+ let s = "";
+ for(let col=0; col<this.field[row].length; col++) {
+ let c = this.field[row][col];
+ s += (c > 31 && c < 127 ? String.fromCharCode(this.field[row][col]) : "?");
+ }
+ lines.push(s);
+ }
+ this.trigger_event("output_field", lines);
+ }
+
+ async sleep(ms) {
+ await new Promise(r => { this.awake_sleep = r; setTimeout(r, ms) });
+ this.awake_sleep = null;
+ }
+
+ process_events() {
+ while (this.input_queue.length > 0) {
+ let item = this.input_queue.pop();
+ if (item[0] == "set_speed") {
+ this.set_speed(item[1]);
+ } else if (item[0] == "pause") {
+ this.pause();
+ } else if (item[0] == "unpause") {
+ this.unpause();
+ } else if (item[0] == "stdin") {
+ if (this.stdin_waiters.length > 0) {
+ this.stdin_waiters.shift()(item[1]);
+ } else {
+ this.stdin_queue.push(item[1]);
+ }
+ }
+ }
+ }
+
+ async main_loop() {
+ let ticks = 0;
+ this.running = true;
+ let running_wake = null;
+ this.running_promise = new Promise(r => { running_wake = r; });
+ let highlighted_cells = [];
+ let start_time = new Date().getTime();
+ let all_blocked = false;
+ while(this.threads.length > 0) {
+ this.process_events();
+ let slice_loops = this.slice_loops;
+ let limit_threads = null;
+ let need_sleep = true;
+
+ this.threads.forEach(thread => this.trigger_event("thread_state_updated", thread));
+ this.trigger_event("thread_state_synced", this.threads);
+
+ if (ticks > this.max_loops) {
+ this.ticks += ticks;
+ ticks = 0;
+ let end_time = new Date().getTime();
+ this.oute(`Ran out of ticks! (${ticks} > ${this.max_loops})`);
+ console.log(`${ticks} in ${end_time - start_time} is ${ticks / (end_time - start_time) / 1000} MHz`);
+ this.pause();
+ this.output_field();
+ }
+
+ let unpause_data = await this.sleep_until_unpaused(all_blocked);
+ if (unpause_data) {
+ console.log(`Unpaused; resetting ticks to 0 from ${ticks}`);
+ start_time = new Date().getTime();
+ if (unpause_data.count !== undefined) {
+ need_sleep = false;
+ slice_loops = unpause_data.count;
+ }
+ if (unpause_data.threads !== undefined)
+ limit_threads = unpause_data.threads;
+ }
+
+ let count = 0;
+ let dead_threads = [];
+ while(count < slice_loops && this.threads.length > 0) {
+ // This will _not_ iterate over newly-added threads.
+ all_blocked = true;
+ let to_iter = limit_threads !== null ? limit_threads : this.threads;
+ to_iter = to_iter.filter(thread => !thread.blocked);
+ if (to_iter.length == 0) {
+ break;
+ }
+ to_iter.forEach(function (thread) { if (!thread.waiter) { all_blocked = false; } });
+ let min_count = Math.min(...to_iter.map(x => x.tick_count));
+ to_iter.forEach((thread, i) => {
+ if (thread.tick_count == min_count) {
+ let tick_count = this.tick(thread, slice_loops - count, i);
+ count += tick_count;
+ thread.tick_count += tick_count;
+ }
+ });
+ let new_dead_threads = this.threads.filter(t => !t.running);
+ if (new_dead_threads) {
+ dead_threads.push(...new_dead_threads);
+ this.threads = this.threads.filter(t => t.running);
+ }
+ }
+
+ dead_threads.forEach(
+ dead_thread => this.trigger_event("thread_dead", dead_thread));
+
+ ticks += count;
+ }
+ running_wake();
+ //console.log(`exited main_loop with ticks ${ticks}`);
+ this.ticks += ticks;
+ let end_time = new Date().getTime();
+ //console.log(`${ticks} in ${end_time - start_time} is ${ticks / (end_time - start_time) / 1000} MHz`);
+ this.output_field();
+ this.trigger_event("terminate");
+ }
+
+ tick(thread, target_ticks, thread_num) {
+ if (thread.waiter) {
+ try {
+ if (thread.waiter(thread)) {
+ thread.waiter = null;
+ }
+ } catch (err) {
+ this.error(thread, "Exception caught during waiter: " + err.message);
+ //console.log(err.stack);
+ }
+ return 1;
+ }
+ if (thread.mode == "normal") {
+ let symbol = this.field[thread.row][thread.col] || 32;
+ let instruction = thread.overlays[symbol] || this.instructions[symbol];
+ //console.log(`Executing ${String.fromCharCode(symbol)} at ${thread.col},${thread.row} stack_length=${thread.stack.length} stack_tail=${thread.stack.slice(-10)}`);
+ //console.log("Executing " + String.fromCharCode(symbol));
+ let jit_return = this.jit.step_jit(thread, target_ticks);
+ if (jit_return > 0) {
+ return jit_return;
+ }
+ if (instruction == null) {
+ this.error(thread, "Invalid instruction: " + symbol);
+ } else {
+ try {
+ let ret = instruction(thread, thread_num);
+ if (ret && ret.constructor === Promise) {
+ thread.blocked = true;
+ }
+ } catch (err) {
+ this.error(thread, "Exception caught during " + symbol + ": " + err.message);
+ //console.log(err.stack);
+ }
+ }
+ } else if (thread.mode == "string") {
+ let symbol = this.field[thread.row][thread.col];
+ if (symbol == '"'.charCodeAt(0)) {
+ thread.mode = "normal";
+ } else {
+ thread.stack.push(symbol);
+ }
+ }
+ thread.tick_count += 1;
+ thread.tick_forwards();
+ return 1;
+ }
+}
diff --git a/2026/games_submissions/liquidcake1/jit.mjs b/2026/games_submissions/liquidcake1/jit.mjs
new file mode 100644
index 0000000..da0d2fb
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/jit.mjs
@@ -0,0 +1,259 @@
+// JIT TODO:
+// * OK, it's probably about 6x faster with JIT than without, but we can go another 4-5x faster by loop unrolling.
+// * We don't bounds-check the stack before executing a JITted routine.
+// * We can probably do a lot better in speed by unrolling and aliasing the stack. Name top N stack vars a1 to aN,
+// then unpack the top N into those, then run code, then pack aN to a?.
+// * We don't have support for branches, even just the dumb "primary only, bail otherwise" variety.
+// * We don't ever remove JIT. We should destroy JIT when a cell is modified.
+// * If a cell is modified _DURING_ a JITted sequence, what do? If external, we can just claim it doesn't matter.
+// If the thread self-modified, we'll need a means to abort the JITted sequence early.
+// * There is no JIT visualisation.
+
+function diagonalise_positive(row, col) {
+ /*
+ 0 1 3 6
+ 2 4 7
+ 5 8
+ 9
+ */
+ let shell = row + col;
+ let prev_shell_indices = shell * (shell + 1) / 2;
+ return prev_shell_indices + row;
+}
+
+function dediagonalise_positive(diagonal_index) {
+ // n (n + 1) / 2 = x;
+ let shell = Math.floor((Math.sqrt(8 * diagonal_index + 1) - 1) / 2);
+ let row_0_index = diagonalise_positive(0, shell);
+ let row = diagonal_index - row_0_index;
+ let col = shell - row;
+ return [row, col];
+}
+
+function diagonalise_any(row, col) {
+ /*
+ 35 21 11 23 39
+ 20 10 4 12 24
+ 9 3 0 1 5
+ 18 8 2 6 14
+ 31 17 7 15 27
+ */
+ let shell = Math.abs(row) + Math.abs(col);
+ if (shell == 0) return 0;
+ let prev_shell_indices = 2 * shell * (shell - 1);
+ if (row >= 0) {
+ return prev_shell_indices + shell + 1 - col;
+ } else {
+ return prev_shell_indices + 3 * shell + 1 + col;
+ }
+}
+
+function dediagonalise_any(diagonal_index) {
+ if (diagonal_index == 0) {
+ return [0, 0];
+ }
+ // cumulative past shell size is (shell-1)^2 + shell^2
+ // so 2s^2 - 2s + 1 = n
+ // (2 + (4-8(1-n))^0.5)/4 = ((2n-1)^0.5+1)/2
+ let shell = Math.floor((Math.sqrt(2*diagonal_index-1)+1)/2);
+ let row_0_pos_index = diagonalise_any(0, shell);
+ let col = shell - diagonal_index + row_0_pos_index;
+ if (col < -shell) {
+ col = -2 * shell - col;
+ }
+ let row = diagonal_index - row_0_pos_index;
+ if (row > shell) {
+ row = 2 * shell - row;
+ }
+ if (row < -shell) {
+ row = -2 * shell - row;
+ }
+ return [row, col];
+}
+
+function diagonalise_with_dir(row, col, rowd, cold) {
+ return 4 * diagonalise_positive(row, col) + diagonalise_any(rowd, cold);
+}
+
+function dediagonalise_with_dir(diagonal_index) {
+ let dir_diag = diagonal_index % 4;
+ let pos_diag = diagonal_index / 4;
+ return dediagonalise_any(pos_diag).concat(dediagonalise_positive(dir_diag));
+}
+
+class CellStats {
+ hit_count = 1;
+ jit_starts = [];
+ jit = null;
+}
+
+class JitPath {
+ path = [];
+ code = "";
+ stack_req = 0;
+ stack_delta = 0;
+ instruction_count = 0;
+ constructor() {
+ }
+}
+
+class JitFragment {
+ end_row = null;
+ end_col = null;
+ end_rowd = null;
+ end_cold = null;
+ instruction_count = null;
+ path = null;
+ stack_req = null;
+ code = null;
+ jit_starts = []; // Should just be the initial cell, but kept for consistency
+
+ constructor(thread, instruction_count, path, stack_req, code) {
+ // TODO: let [end_row, end_col, end_rowd, end_cold] = dediagonalise_with_dir(resting_place);
+ this.end_row = thread.row;
+ this.end_col = thread.col;
+ this.end_rowd = thread.rowd;
+ this.end_cold = thread.cold;
+ this.instruction_count = instruction_count;
+ this.path = path;
+ this.stack_req = stack_req;
+ this.code = code;
+ }
+
+ maybe_run_in_thread(thread, target_ticks) {
+ // TODO include this fragment in the JIT code itself.
+ // TODO should store and check thread fingerprints!
+ if (this.instruction_count > target_ticks || thread.stack.length < this.stack_req) {
+ // Checking requirements not met. Just interpret.
+ //console.log(`Not running JIT. ${this.instruction_count} > ${target_ticks} || ${thread.stack.length} < ${this.stack_req}`);
+ return 0;
+ }
+ try {
+ this.call(thread);
+ } catch (err) {
+ thread.interpreter.error(thread, `Exception caught in JIT: ${err.message}`);
+ // TODO: Stop thread?!
+ }
+ thread.tick_count += this.instruction_count;
+ thread.row = this.end_row;
+ thread.col = this.end_col;
+ thread.rowd = this.end_rowd;
+ thread.cold = this.end_cold;
+ return this.instruction_count;
+ }
+}
+
+export class Jit {
+ cell_stats = {};
+ path = null;
+ threshold = 10000000000;
+ interpreter = null;
+
+ constructor(interpreter) {
+ this.interpreter = interpreter;
+ }
+
+ step_jit(thread, target_ticks) {
+ let diagonal_index = diagonalise_with_dir(thread.row, thread.col, thread.rowd, thread.cold);
+ let jit_path = thread.jit_path;
+ if (jit_path === undefined) {
+ let cell_stats = this.cell_stats[diagonal_index];
+ // Run jitted code, if present, else increment count and possibly start a JIT follow.
+ if (!cell_stats) {
+ this.cell_stats[diagonal_index] = new CellStats();
+ return 0;
+ } else if (cell_stats.jit) {
+ return cell_stats.jit.maybe_run_in_thread(thread, target_ticks);
+ } else {
+ cell_stats.hit_count += 1;
+ if (cell_stats.hit_count < this.threshold) {
+ return 0;
+ }
+ //console.log(`loop found at ${diagonal_index}?`);
+ thread.jit_path = jit_path = new JitPath();
+ // Fall through to below now.
+ }
+ }
+ //console.log(`${jit_path}`);
+ if (jit_path.path.length > 0 && diagonal_index == jit_path.path[0]) {
+ // We completed a path.
+ let jit = this.complete_jit(thread);
+ // Run it, else we'll run the current instruction and then begin a new
+ // loop at the next cell, which will prevent evaluation of this one!
+ return jit.maybe_run_in_thread(thread, target_ticks);
+ }
+ // We're following a path.
+ // TODO this should not need to do string futzing.
+ let symbol = this.interpreter.field[thread.row][thread.col];
+ if (thread.overlays[symbol] !== undefined) {
+ // TODO
+ //console.log("Stopping JIT as we're entering an instruction from an overlay.");
+ this.complete_jit(thread);
+ return 0;
+ }
+ let raw_instruction = this.interpreter.instructions_raw[String.fromCharCode(symbol)];
+ if (raw_instruction === undefined) {
+ //console.log("Stopping JIT as we're entering an unknown instruction.");
+ this.complete_jit(thread);
+ return 0;
+ }
+ if (!raw_instruction.can_jit) {
+ //console.log("Stopping JIT as we're entering an instruction I don't understand.");
+ //console.log(jit_path);
+ this.complete_jit(thread);
+ return 0;
+ }
+ jit_path.code += `// ${String.fromCharCode(symbol)} at row ${thread.row}, col ${thread.col}, heading ${thread.rowd}, ${thread.cold}\n`;
+ let real_code = raw_instruction.unchecked_js_code;
+ if (typeof real_code == 'function') {
+ real_code = real_code(thread);
+ }
+ jit_path.code += `${real_code}\n`;
+ jit_path.stack_req = Math.max(jit_path.stack_req, -jit_path.stack_delta - raw_instruction.stack_min);
+ jit_path.stack_delta += raw_instruction.stack_return - raw_instruction.stack_min;
+ jit_path.path.push(diagonal_index);
+ jit_path.instruction_count += 1;
+ //console.log(jit_path);
+ return 0;
+ }
+
+ complete_jit(thread) {
+ let jit_path = thread.jit_path;
+ thread.jit_path = undefined;
+ if (jit_path.path.length == 0) {
+ //console.log("JIT path is empty.");
+ return;
+ }
+ console.log("Performing JIT compile, length " + jit_path.path.length);
+ //console.log(jit_path);
+ // TODO: jit_path.compile()
+ let code = `jit.call=function (thread) {\nlet stack = thread.stack;\n${jit_path.code}\n}`;
+ //console.log(code);
+ let jit = new JitFragment(thread, jit_path.instruction_count, jit_path.path, jit_path.stack_req, code);
+ //console.log(jit);
+ eval(code);
+ //console.log(jit);
+ let starting_place = jit_path.path[0];
+ this.cell_stats[jit_path.path[0]].jit = jit;
+ for(let i=0; i<jit_path.path.length; i++) {
+ this.cell_stats[jit_path.path[i]].jit_starts.push(starting_place);
+ }
+ // Drop out of follow mode, now.
+ return jit;
+ }
+
+ cell_changed(row, col) {
+ let dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
+ for(let i=0; i<4; i++) {
+ let rowd = dirs[i][0];
+ let cold = dirs[i][1];
+ let cell_diagonal_index = diagonalise_with_dir(row, col, rowd, cold);
+ let cell_stats = this.cell_stats[cell_diagonal_index];
+ if (cell_stats) {
+ for(let j=0; j<cell_stats.jit_starts.length; j++) {
+ this.cell_stats[cell_stats.jit_starts[j]] = undefined;
+ }
+ }
+ }
+ }
+}
diff --git a/2026/games_submissions/liquidcake1/queue.mjs b/2026/games_submissions/liquidcake1/queue.mjs
new file mode 100644
index 0000000..e6c3efe
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/queue.mjs
@@ -0,0 +1,30 @@
+export class Queue {
+ /* Structure is a (hopefully short) list of (longer) lists.
+ * Queue to the last list.
+ * Hold index into first list.
+ */
+ length = 0;
+ index = -1;
+ content = [[]];
+ max_len = 100;
+ push(x) {
+ let content = this.content;
+ if (content[content.length - 1].length > this.max_len) {
+ content.push([]);
+ }
+ content[content.length - 1].push(x);
+ this.length += 1;
+ }
+ pop() {
+ this.index += 1;
+ if (this.index >= this.content[0].length) {
+ if (this.content.length == 1) {
+ throw("empty");
+ }
+ this.content.shift();
+ this.index = 0;
+ }
+ this.length -= 1;
+ return this.content[0][this.index];
+ }
+}
diff --git a/2026/games_submissions/liquidcake1/wasm.mjs b/2026/games_submissions/liquidcake1/wasm.mjs
new file mode 100644
index 0000000..18bf547
--- /dev/null
+++ b/2026/games_submissions/liquidcake1/wasm.mjs
@@ -0,0 +1,51 @@
+export function load_wasm(interpreter) {
+ /*
+ (memory $memory 1)
+ (export "memory" (memory $memory))
+ (func (export "load_first_item_in_mem") (param) (result i32)
+ i32.const 0
+
+ ;; load first item in memory and return the result
+ i32.load
+ ;; store 10000 in the first location in memory
+ i32.const 0
+ i32.const 10000
+ i32.store
+ )
+ */
+ /*
+ (module
+(func $pop (import "my_namespace" "pop") (param i32) (result i32))
+(func $push (import "my_namespace" "push") (param i32 i32))
+(func (export "plus") (param i32)
+ local.get 0
+ local.get 0
+ call $pop
+ local.get 0
+ call $pop
+ i32.add
+ call $push
+))
+*/
+ //let wasm_string = "AGFzbQEAAAABBwFgAn9/AX8DAgEABwoBBmFkZFR3bwAACgkBBwAgACABagsACgRuYW1lAgMBAAA=";
+ let wasm_string = "AGFzbQEAAAABDwNgAX8Bf2ACf38AYAF/AAIoAgxteV9uYW1lc3BhY2UDcG9wAAAMbXlfbmFtZXNwYWNlBHB1c2gAAQMCAQIHCAEEcGx1cwACChEBDwAgACAAEAAgABAAahABCwAcBG5hbWUBDAIAA3BvcAEEcHVzaAIHAwAAAQACAA==";
+ // Fake minus version
+ //let wasm_string = "AGFzbQEAAAABDwNgAX8Bf2ACf38AYAF/AAIoAgxteV9uYW1lc3BhY2UDcG9wAAAMbXlfbmFtZXNwYWNlBHB1c2gAAQMCAQIHCAEEcGx1cwACChEBDwAgACAAEAAgABAAaxABCwAcBG5hbWUBDAIAA3BvcAEEcHVzaAIHAwAAAQACAA==";
+ let wasm_array = new TextEncoder().encode(atob(wasm_string))
+ const importObject = {
+ my_namespace: {
+ pop: threadNo => interpreter.threads[threadNo].pop(),
+ push: (threadNo, i) => interpreter.threads[threadNo].stack.push(i),
+ }
+ };
+ return WebAssembly.compile(wasm_array).then((mod) =>
+ WebAssembly.instantiate(mod, importObject).then(instance => {
+ patch_interpreter(interpreter, instance);
+ return instance;
+ })
+ );
+}
+function patch_interpreter(interpreter, instance) {
+ interpreter.instructions_raw["+"].impl = function (thread, thread_num) { instance.exports.plus(thread_num); };
+ interpreter.instructions["+".charCodeAt(0)] = function (thread, thread_num) { instance.exports.plus(thread_num); };
+}