summaryrefslogtreecommitdiff
path: root/2026/games_submissions/gbjk
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-07-17 03:10:43 -0400
committerLLLL Colonq <llll@colonq>2026-07-17 03:10:43 -0400
commit07a36f2b38a5cba402ebc5158710f5a85f443456 (patch)
treef28855384eadc73f49cc1a707a73896f87bc5530 /2026/games_submissions/gbjk
parent951b316773265052fbc5fa546bbcf31bef277062 (diff)
Post-stream update
Diffstat (limited to '2026/games_submissions/gbjk')
-rw-r--r--2026/games_submissions/gbjk/LICENSE21
-rw-r--r--2026/games_submissions/gbjk/index.html456
-rw-r--r--2026/games_submissions/gbjk/src/.gitignore24
-rw-r--r--2026/games_submissions/gbjk/src/index.html12
-rw-r--r--2026/games_submissions/gbjk/src/package.json16
-rw-r--r--2026/games_submissions/gbjk/src/pnpm-lock.yaml568
-rw-r--r--2026/games_submissions/gbjk/src/src/main.ts33
-rw-r--r--2026/games_submissions/gbjk/src/src/setup.ts389
-rw-r--r--2026/games_submissions/gbjk/src/src/style.css103
-rw-r--r--2026/games_submissions/gbjk/src/tsconfig.json23
-rw-r--r--2026/games_submissions/gbjk/src/vite.config.ts9
11 files changed, 1654 insertions, 0 deletions
diff --git a/2026/games_submissions/gbjk/LICENSE b/2026/games_submissions/gbjk/LICENSE
new file mode 100644
index 0000000..14fac91
--- /dev/null
+++ b/2026/games_submissions/gbjk/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026
+
+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/gbjk/index.html b/2026/games_submissions/gbjk/index.html
new file mode 100644
index 0000000..0e43bab
--- /dev/null
+++ b/2026/games_submissions/gbjk/index.html
@@ -0,0 +1,456 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>tortis</title>
+ <script type="module" crossorigin>//#region \0vite/modulepreload-polyfill.js
+(function polyfill() {
+ const relList = document.createElement("link").relList;
+ if (relList && relList.supports && relList.supports("modulepreload")) return;
+ for (const link of document.querySelectorAll("link[rel=\"modulepreload\"]")) processPreload(link);
+ new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (mutation.type !== "childList") continue;
+ for (const node of mutation.addedNodes) if (node.tagName === "LINK" && node.rel === "modulepreload") processPreload(node);
+ }
+ }).observe(document, {
+ childList: true,
+ subtree: true
+ });
+ function getFetchOpts(link) {
+ const fetchOpts = {};
+ if (link.integrity) fetchOpts.integrity = link.integrity;
+ if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;
+ if (link.crossOrigin === "use-credentials") fetchOpts.credentials = "include";
+ else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";
+ else fetchOpts.credentials = "same-origin";
+ return fetchOpts;
+ }
+ function processPreload(link) {
+ if (link.ep) return;
+ link.ep = true;
+ const fetchOpts = getFetchOpts(link);
+ fetch(link.href, fetchOpts);
+ }
+})();
+//#endregion
+//#region src/setup.ts
+var WIDTH = 160;
+var HEIGHT = 240;
+var GRID_HEIGHT = HEIGHT - 10;
+var ROWS = 15;
+var COLS = 10;
+var GRID_WIDTH = GRID_HEIGHT / ROWS * 10;
+var GRID_X = 5;
+var GRID_Y = 5;
+var MARGIN = 2;
+var CELL_WIDTH = GRID_HEIGHT / ROWS - MARGIN;
+var FALLTIME = 600;
+var MIN_FALLTIME = 300;
+var rotation = 0;
+var board = [];
+function resetBoard() {
+ board.length = 0;
+ for (let height = 0; height < ROWS; height++) {
+ const row = [];
+ for (let width = 0; width < COLS; width++) row.push("void");
+ board.push(row);
+ }
+}
+resetBoard();
+function random(elements) {
+ return elements[Math.floor(Math.random() * elements.length)];
+}
+var addedShapeNumber = 1;
+function addSquares() {
+ const color = random([
+ "fallingRed",
+ "fallingGreen",
+ "fallingBlue"
+ ]);
+ let w = COLS / 2;
+ let h = 1;
+ for (let i = 0; i < 4; i++) {
+ board[h][w] = color;
+ if (i === 3) break;
+ let move = null;
+ let j = 0;
+ for (; j < 20; j++) {
+ move = addedShapeNumber % 6 === 0 ? "down" : random([
+ "up",
+ "down",
+ "left",
+ "right"
+ ]);
+ if (move === "up" && (h === 0 || board[h - 1][w] !== "void")) continue;
+ if (move === "down" && (h === ROWS - 1 || board[h + 1][w] !== "void")) continue;
+ if (move === "left" && (w === 0 || board[h][w - 1] !== "void")) continue;
+ if (move === "right" && (w === COLS - 1 || board[h][w + 1] !== "void")) continue;
+ break;
+ }
+ if (j === 20) return false;
+ switch (move) {
+ case "up":
+ h--;
+ break;
+ case "down":
+ h++;
+ break;
+ case "left":
+ w--;
+ break;
+ case "right":
+ w++;
+ break;
+ }
+ }
+ addedShapeNumber++;
+ return true;
+}
+function clearFilledRows() {
+ let lastClearedRow = ROWS;
+ let clearedRows = 0;
+ for (let row = ROWS - 1; row >= 0; row--) if (!board[row].some((cell) => cell === "void" || cell === "fallingBlue" || cell === "fallingGreen" || cell === "fallingRed")) {
+ for (let col = 0; col < COLS; col++) board[row][col] = "void";
+ lastClearedRow = row;
+ clearedRows++;
+ } else if (lastClearedRow !== ROWS) for (let col = 0; col < COLS; col++) switch (board[row][col]) {
+ case "red":
+ board[row][col] = "fallingRed";
+ break;
+ case "green":
+ board[row][col] = "fallingGreen";
+ break;
+ case "blue":
+ board[row][col] = "fallingBlue";
+ break;
+ }
+ return clearedRows;
+}
+function setup(elements) {
+ const ctx = elements.main.getContext("2d");
+ let gameRunning = false;
+ let animationFrameId = null;
+ let lastFrame = 0;
+ let fallTime = FALLTIME;
+ let speedUpPressed = false;
+ elements.left.addEventListener("click", () => moveFalling("left"));
+ elements.right.addEventListener("click", () => moveFalling("right"));
+ elements.rotateLeft.addEventListener("click", () => {
+ if (!gameRunning) return;
+ rotation -= 30 * Math.random();
+ elements.main.style.transform = `rotate(${rotation}deg)`;
+ });
+ elements.rotateRight.addEventListener("click", () => {
+ if (!gameRunning) return;
+ rotation += 30 * Math.random();
+ elements.main.style.transform = `rotate(${rotation}deg)`;
+ });
+ elements.speedUp.addEventListener("mousedown", () => speedUpPressed = gameRunning);
+ elements.speedUp.addEventListener("mouseup", () => speedUpPressed = false);
+ window.addEventListener("message", (ev) => {
+ const message = ev.data;
+ if (!isStartMessage(message)) return;
+ startGame(message.difficulty);
+ });
+ window.parent.postMessage({ op: "ready" }, "*");
+ draw();
+ function isStartMessage(message) {
+ if (typeof message !== "object" || message === null) return false;
+ const candidate = message;
+ return candidate.op === "start" && typeof candidate.difficulty === "number" && Number.isFinite(candidate.difficulty);
+ }
+ function resetGame(difficulty) {
+ resetBoard();
+ addedShapeNumber = 1;
+ fallTime = Math.max(MIN_FALLTIME, FALLTIME - Math.max(0, difficulty) * 5);
+ rotation = 0;
+ elements.main.style.transform = "";
+ speedUpPressed = false;
+ lastFrame = 0;
+ }
+ function startGame(difficulty) {
+ if (animationFrameId !== null) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ resetGame(difficulty);
+ gameRunning = true;
+ window.parent.postMessage({
+ op: "started",
+ verb: "catch!"
+ }, "*");
+ animationFrameId = requestAnimationFrame(gameLoop);
+ }
+ function finishGame(win) {
+ if (!gameRunning) return;
+ gameRunning = false;
+ if (animationFrameId !== null) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ window.parent.postMessage({
+ op: "done",
+ win
+ }, "*");
+ resetGame(0);
+ draw();
+ }
+ function moveFalling(move) {
+ if (!gameRunning) return;
+ switch (move) {
+ case "left":
+ for (let row = 0; row < ROWS; row++) for (let col = 0; col < COLS - 1; col++) if (board[row][col] === "void") switch (board[row][col + 1]) {
+ case "fallingRed":
+ case "fallingGreen":
+ case "fallingBlue":
+ board[row][col] = board[row][col + 1];
+ board[row][col + 1] = "void";
+ }
+ break;
+ case "right":
+ for (let row = 0; row < ROWS; row++) for (let col = COLS - 1; col > 0; col--) if (board[row][col] === "void") switch (board[row][col - 1]) {
+ case "fallingRed":
+ case "fallingGreen":
+ case "fallingBlue":
+ board[row][col] = board[row][col - 1];
+ board[row][col - 1] = "void";
+ }
+ break;
+ }
+ }
+ function physics() {
+ let somethingFell = false;
+ for (let row = ROWS - 1; row >= 1; row--) for (let col = 0; col < COLS; col++) {
+ let space = board[row][col];
+ let above = board[row - 1][col];
+ switch (above) {
+ case "fallingRed":
+ case "fallingGreen":
+ case "fallingBlue": if (space === "void") {
+ somethingFell = true;
+ board[row][col] = board[row - 1][col];
+ board[row - 1][col] = "void";
+ } else switch (above) {
+ case "fallingRed":
+ board[row - 1][col] = "red";
+ break;
+ case "fallingGreen":
+ board[row - 1][col] = "green";
+ break;
+ case "fallingBlue":
+ board[row - 1][col] = "blue";
+ break;
+ }
+ }
+ }
+ for (let col = 0; col < COLS; col++) switch (board[ROWS - 1][col]) {
+ case "fallingRed":
+ board[ROWS - 1][col] = "red";
+ break;
+ case "fallingGreen":
+ board[ROWS - 1][col] = "green";
+ break;
+ case "fallingBlue":
+ board[ROWS - 1][col] = "blue";
+ break;
+ }
+ return somethingFell;
+ }
+ function draw() {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ ctx.fillStyle = "black";
+ ctx.fillRect(GRID_X, GRID_Y, GRID_WIDTH, GRID_HEIGHT);
+ for (let h = 0; h < ROWS; h++) for (let w = 0; w < COLS; w++) {
+ switch (board[h][w]) {
+ case "void":
+ ctx.fillStyle = "#2e2e2e";
+ break;
+ case "red":
+ ctx.fillStyle = "#994444";
+ break;
+ case "green":
+ ctx.fillStyle = "#449944";
+ break;
+ case "blue":
+ ctx.fillStyle = "#444499";
+ break;
+ case "fallingRed":
+ ctx.fillStyle = "#bb3333";
+ break;
+ case "fallingGreen":
+ ctx.fillStyle = "#33bb33";
+ break;
+ case "fallingBlue":
+ ctx.fillStyle = "#3333bb";
+ break;
+ }
+ const x = 8 + w * CELL_WIDTH + (w - 1) * MARGIN;
+ const y = 8 + h * CELL_WIDTH + (h - 1) * MARGIN;
+ ctx.fillRect(x, y, CELL_WIDTH, CELL_WIDTH);
+ }
+ }
+ function gameLoop(time) {
+ animationFrameId = null;
+ if (!gameRunning) return;
+ if (time - lastFrame > (speedUpPressed ? fallTime / 10 : fallTime)) {
+ lastFrame = time;
+ if (!physics()) {
+ const clearedRows = clearFilledRows();
+ if (clearedRows >= 4) {
+ finishGame(true);
+ return;
+ } else if (clearedRows === 0) {
+ if (!addSquares()) {
+ finishGame(false);
+ return;
+ }
+ }
+ }
+ }
+ draw();
+ animationFrameId = requestAnimationFrame(gameLoop);
+ }
+}
+//#endregion
+//#region src/main.ts
+document.querySelector("#app").innerHTML = `
+ <div class="layout">
+
+ <div class="game">
+ <div class="title-bar">
+ <h1>Tortis</h1>
+ <p>(4row clear to win)</p>
+ </div>
+
+ <canvas height="240" width="160" id="main"></canvas>
+ </div>
+
+ <ul>
+ <button id="move-left">move left</button>
+ <button id="rotate-left">rotate left</button>
+ <button id="move-right">move right</button>
+ <button id="rotate-right">rotate right</button>
+ <button id="speed-up">speed up</button>
+ </ul>
+ </div>
+`;
+setup({
+ main: document.querySelector("#main"),
+ left: document.querySelector("#move-left"),
+ right: document.querySelector("#move-right"),
+ rotateLeft: document.querySelector("#rotate-left"),
+ rotateRight: document.querySelector("#rotate-right"),
+ speedUp: document.querySelector("#speed-up")
+});
+//#endregion</script>
+ <style rel="stylesheet" crossorigin>* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+ min-height: 100%;
+}
+
+body {
+ margin: 0;
+ color: darksalmon;
+ background-color: #2e2e2e;
+ font-family: monospace;
+ overflow-x: hidden;
+}
+
+button {
+ min-width: 0;
+ font: inherit;
+ font-size: .4rem;
+ line-height: 1.1;
+ padding: .15rem;
+ color: darksalmon;
+ background-color: #2e2e2e;
+ border-color: salmon;
+ cursor: pointer;
+ white-space: normal;
+ overflow-wrap: anywhere;
+
+ z-index: 9999;
+}
+
+button:hover {
+ background-color: darkslategray;
+}
+
+button:active {
+ background-color: slategray;
+}
+
+.layout {
+ margin: auto;
+ display: flex;
+ min-height: 100dvh;
+ width: 100%;
+ padding: .15rem;
+ gap: .2rem;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.game {
+ min-width: 0;
+}
+
+.title-bar {
+ display: flex;
+ align-items: baseline;
+ gap: .25rem;
+ margin-bottom: .2rem;
+}
+
+h1,
+p {
+ margin: 0;
+}
+
+h1 {
+ font-size: .6rem;
+ line-height: 1;
+}
+
+p {
+ font-size: .4rem;
+ line-height: 1.1;
+}
+
+canvas {
+ display: block;
+ aspect-ratio: 2 / 3;
+ border-radius: 6px;
+ border: 1px solid darksalmon;
+ box-shadow: 0px 0px 4px darksalmon;
+ height: 80vmin;
+ width: auto;
+
+ transition: transform 100ms;
+ margin: auto;
+}
+
+ul {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ gap: .15rem;
+ width: min(6rem, 42vw);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+/*$vite$:1*/</style>
+ </head>
+ <body>
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/2026/games_submissions/gbjk/src/.gitignore b/2026/games_submissions/gbjk/src/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/2026/games_submissions/gbjk/src/index.html b/2026/games_submissions/gbjk/src/index.html
new file mode 100644
index 0000000..c68a552
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>tortis</title>
+ </head>
+ <body>
+ <div id="app"></div>
+ <script type="module" src="/src/main.ts"></script>
+ </body>
+</html>
diff --git a/2026/games_submissions/gbjk/src/package.json b/2026/games_submissions/gbjk/src/package.json
new file mode 100644
index 0000000..a5e9656
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "tortis",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "6.0.2",
+ "vite": "8.0.12",
+ "vite-plugin-singlefile": "2.3.3"
+ }
+}
diff --git a/2026/games_submissions/gbjk/src/pnpm-lock.yaml b/2026/games_submissions/gbjk/src/pnpm-lock.yaml
new file mode 100644
index 0000000..1087420
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/pnpm-lock.yaml
@@ -0,0 +1,568 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ devDependencies:
+ typescript:
+ specifier: ~6.0.2
+ version: 6.0.3
+ vite:
+ specifier: ^8.0.12
+ version: 8.0.16
+ vite-plugin-singlefile:
+ specifier: ^2.3.3
+ version: 2.3.3(vite@8.0.16)
+
+packages:
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@napi-rs/wasm-runtime@1.1.5':
+ resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.133.0':
+ resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
+
+ '@rolldown/binding-android-arm64@1.0.3':
+ resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.3':
+ resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.3':
+ resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.3':
+ resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.3':
+ resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.3':
+ resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.3':
+ resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.3':
+ resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.3':
+ resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.3':
+ resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.3':
+ resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.3':
+ resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.3':
+ resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.3':
+ resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.3':
+ resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@tybys/wasm-util@0.10.2':
+ resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ nanoid@3.3.13:
+ resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.2:
+ resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ rolldown@1.0.3:
+ resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ vite-plugin-singlefile@2.3.3:
+ resolution: {integrity: sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==}
+ engines: {node: '>18.0.0'}
+ peerDependencies:
+ rollup: ^4.59.0
+ vite: ^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ vite@8.0.16:
+ resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.18
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+snapshots:
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.2
+ optional: true
+
+ '@oxc-project/types@0.133.0': {}
+
+ '@rolldown/binding-android-arm64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.3':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.3':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.3':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.3':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@tybys/wasm-util@0.10.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ detect-libc@2.1.2: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ fsevents@2.3.3:
+ optional: true
+
+ is-number@7.0.0: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.2
+
+ nanoid@3.3.13: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.2: {}
+
+ picomatch@4.0.4: {}
+
+ postcss@8.5.15:
+ dependencies:
+ nanoid: 3.3.13
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ rolldown@1.0.3:
+ dependencies:
+ '@oxc-project/types': 0.133.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.3
+ '@rolldown/binding-darwin-arm64': 1.0.3
+ '@rolldown/binding-darwin-x64': 1.0.3
+ '@rolldown/binding-freebsd-x64': 1.0.3
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.3
+ '@rolldown/binding-linux-arm64-gnu': 1.0.3
+ '@rolldown/binding-linux-arm64-musl': 1.0.3
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.3
+ '@rolldown/binding-linux-s390x-gnu': 1.0.3
+ '@rolldown/binding-linux-x64-gnu': 1.0.3
+ '@rolldown/binding-linux-x64-musl': 1.0.3
+ '@rolldown/binding-openharmony-arm64': 1.0.3
+ '@rolldown/binding-wasm32-wasi': 1.0.3
+ '@rolldown/binding-win32-arm64-msvc': 1.0.3
+ '@rolldown/binding-win32-x64-msvc': 1.0.3
+
+ source-map-js@1.2.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@6.0.3: {}
+
+ vite-plugin-singlefile@2.3.3(vite@8.0.16):
+ dependencies:
+ micromatch: 4.0.8
+ vite: 8.0.16
+
+ vite@8.0.16:
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rolldown: 1.0.3
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ fsevents: 2.3.3
diff --git a/2026/games_submissions/gbjk/src/src/main.ts b/2026/games_submissions/gbjk/src/src/main.ts
new file mode 100644
index 0000000..8752b31
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/src/main.ts
@@ -0,0 +1,33 @@
+import { setup } from "./setup";
+import "./style.css";
+
+document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
+ <div class="layout">
+
+ <div class="game">
+ <div class="title-bar">
+ <h1>Tortis</h1>
+ <p>(4row clear to win)</p>
+ </div>
+
+ <canvas height="240" width="160" id="main"></canvas>
+ </div>
+
+ <ul>
+ <button id="move-left">move left</button>
+ <button id="rotate-left">rotate left</button>
+ <button id="move-right">move right</button>
+ <button id="rotate-right">rotate right</button>
+ <button id="speed-up">speed up</button>
+ </ul>
+ </div>
+`;
+
+setup({
+ main: document.querySelector<HTMLCanvasElement>("#main")!,
+ left: document.querySelector<HTMLButtonElement>("#move-left")!,
+ right: document.querySelector<HTMLButtonElement>("#move-right")!,
+ rotateLeft: document.querySelector<HTMLButtonElement>("#rotate-left")!,
+ rotateRight: document.querySelector<HTMLButtonElement>("#rotate-right")!,
+ speedUp: document.querySelector<HTMLButtonElement>("#speed-up")!,
+});
diff --git a/2026/games_submissions/gbjk/src/src/setup.ts b/2026/games_submissions/gbjk/src/src/setup.ts
new file mode 100644
index 0000000..9d0614b
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/src/setup.ts
@@ -0,0 +1,389 @@
+export type Elements = {
+ main: HTMLCanvasElement;
+ right: HTMLButtonElement;
+ left: HTMLButtonElement;
+ rotateRight: HTMLButtonElement;
+ rotateLeft: HTMLButtonElement;
+ speedUp: HTMLButtonElement;
+};
+
+const WIDTH = 160;
+const HEIGHT = 240;
+
+const GRID_HEIGHT = HEIGHT - 10;
+const ROWS = 15;
+const COLS = 10;
+const GRID_WIDTH = (GRID_HEIGHT / ROWS) * 10;
+const GRID_X = 5;
+const GRID_Y = 5;
+const MARGIN = 2;
+const CELL_WIDTH = GRID_HEIGHT / ROWS - MARGIN;
+const FALLTIME = 600;
+const MIN_FALLTIME = 300;
+
+let rotation = 0;
+
+type Block =
+ | "void"
+ | "red"
+ | "green"
+ | "blue"
+ | "fallingRed"
+ | "fallingGreen"
+ | "fallingBlue";
+
+const board: Block[][] = [];
+
+function resetBoard() {
+ board.length = 0;
+
+ for (let height = 0; height < ROWS; height++) {
+ const row: Block[] = [];
+ for (let width = 0; width < COLS; width++) {
+ row.push("void");
+ }
+ board.push(row);
+ }
+}
+
+resetBoard();
+
+function random<T>(elements: T[]): T {
+ return elements[Math.floor(Math.random() * elements.length)];
+}
+
+let addedShapeNumber = 1;
+function addSquares(): boolean {
+ const color = random(["fallingRed", "fallingGreen", "fallingBlue"] as const);
+ let w = COLS / 2;
+ let h = 1;
+ for (let i = 0; i < 4; i++) {
+ board[h][w] = color;
+
+ if (i === 3) break;
+
+ let move: "up" | "down" | "left" | "right" | null = null;
+ let j = 0;
+ for (; j < 20; j++) {
+ move =
+ addedShapeNumber % 6 === 0
+ ? "down"
+ : random(["up", "down", "left", "right"] as const);
+
+ if (move === "up" && (h === 0 || board[h - 1][w] !== "void")) continue;
+ if (move === "down" && (h === ROWS - 1 || board[h + 1][w] !== "void"))
+ continue;
+
+ if (move === "left" && (w === 0 || board[h][w - 1] !== "void")) continue;
+ if (move === "right" && (w === COLS - 1 || board[h][w + 1] !== "void"))
+ continue;
+
+ break;
+ }
+ if (j === 20) {
+ return false;
+ }
+
+ switch (move) {
+ case "up":
+ h--;
+ break;
+ case "down":
+ h++;
+ break;
+ case "left":
+ w--;
+ break;
+ case "right":
+ w++;
+ break;
+ }
+ }
+
+ addedShapeNumber++;
+ return true;
+}
+
+function clearFilledRows(): number {
+ let lastClearedRow = ROWS;
+ let clearedRows = 0;
+
+ for (let row = ROWS - 1; row >= 0; row--) {
+ if (
+ !board[row].some(
+ (cell) =>
+ cell === "void" ||
+ cell === "fallingBlue" ||
+ cell === "fallingGreen" ||
+ cell === "fallingRed",
+ )
+ ) {
+ for (let col = 0; col < COLS; col++) {
+ board[row][col] = "void";
+ }
+ lastClearedRow = row;
+ clearedRows++;
+ } else if (lastClearedRow !== ROWS) {
+ for (let col = 0; col < COLS; col++) {
+ switch (board[row][col]) {
+ case "red":
+ board[row][col] = "fallingRed";
+ break;
+ case "green":
+ board[row][col] = "fallingGreen";
+ break;
+ case "blue":
+ board[row][col] = "fallingBlue";
+ break;
+ }
+ }
+ }
+ }
+
+ return clearedRows;
+}
+
+export function setup(elements: Elements) {
+ const ctx = elements.main.getContext("2d")!;
+
+ let gameRunning = false;
+ let animationFrameId: number | null = null;
+ let lastFrame = 0;
+ let fallTime = FALLTIME;
+
+ let speedUpPressed = false;
+
+ elements.left.addEventListener("click", () => moveFalling("left"));
+ elements.right.addEventListener("click", () => moveFalling("right"));
+
+ elements.rotateLeft.addEventListener("click", () => {
+ if (!gameRunning) return;
+
+ rotation -= 30 * Math.random();
+ elements.main.style.transform = `rotate(${rotation}deg)`;
+ });
+ elements.rotateRight.addEventListener("click", () => {
+ if (!gameRunning) return;
+
+ rotation += 30 * Math.random();
+ elements.main.style.transform = `rotate(${rotation}deg)`;
+ });
+
+ elements.speedUp.addEventListener(
+ "mousedown",
+ () => (speedUpPressed = gameRunning),
+ );
+ elements.speedUp.addEventListener("mouseup", () => (speedUpPressed = false));
+
+ window.addEventListener("message", (ev: MessageEvent<unknown>) => {
+ const message = ev.data;
+ if (!isStartMessage(message)) return;
+
+ startGame(message.difficulty);
+ });
+
+ window.parent.postMessage({ op: "ready" }, "*");
+
+ draw();
+
+ function isStartMessage(
+ message: unknown,
+ ): message is { op: "start"; difficulty: number } {
+ if (typeof message !== "object" || message === null) return false;
+
+ const candidate = message as { op?: unknown; difficulty?: unknown };
+ return (
+ candidate.op === "start" &&
+ typeof candidate.difficulty === "number" &&
+ Number.isFinite(candidate.difficulty)
+ );
+ }
+
+ function resetGame(difficulty: number) {
+ resetBoard();
+ addedShapeNumber = 1;
+ fallTime = Math.max(MIN_FALLTIME, FALLTIME - Math.max(0, difficulty) * 5);
+ rotation = 0;
+ elements.main.style.transform = "";
+ speedUpPressed = false;
+ lastFrame = 0;
+ }
+
+ function startGame(difficulty: number) {
+ if (animationFrameId !== null) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+
+ resetGame(difficulty);
+ gameRunning = true;
+ window.parent.postMessage({ op: "started", verb: "catch!" }, "*");
+ animationFrameId = requestAnimationFrame(gameLoop);
+ }
+
+ function finishGame(win: boolean) {
+ if (!gameRunning) return;
+
+ gameRunning = false;
+ if (animationFrameId !== null) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+
+ window.parent.postMessage({ op: "done", win }, "*");
+ resetGame(0);
+ draw();
+ }
+
+ function moveFalling(move: "left" | "right") {
+ if (!gameRunning) return;
+
+ switch (move) {
+ case "left":
+ for (let row = 0; row < ROWS; row++) {
+ for (let col = 0; col < COLS - 1; col++) {
+ if (board[row][col] === "void") {
+ switch (board[row][col + 1]) {
+ case "fallingRed":
+ case "fallingGreen":
+ case "fallingBlue":
+ board[row][col] = board[row][col + 1];
+ board[row][col + 1] = "void";
+ }
+ }
+ }
+ }
+ break;
+ case "right":
+ for (let row = 0; row < ROWS; row++) {
+ for (let col = COLS - 1; col > 0; col--) {
+ if (board[row][col] === "void") {
+ switch (board[row][col - 1]) {
+ case "fallingRed":
+ case "fallingGreen":
+ case "fallingBlue":
+ board[row][col] = board[row][col - 1];
+ board[row][col - 1] = "void";
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ function physics(): boolean {
+ let somethingFell = false;
+
+ for (let row = ROWS - 1; row >= 1; row--) {
+ for (let col = 0; col < COLS; col++) {
+ let space = board[row][col];
+ let above = board[row - 1][col];
+ switch (above) {
+ case "fallingRed":
+ case "fallingGreen":
+ case "fallingBlue":
+ if (space === "void") {
+ somethingFell = true;
+ board[row][col] = board[row - 1][col];
+ board[row - 1][col] = "void";
+ } else {
+ switch (above) {
+ case "fallingRed":
+ board[row - 1][col] = "red";
+ break;
+ case "fallingGreen":
+ board[row - 1][col] = "green";
+ break;
+ case "fallingBlue":
+ board[row - 1][col] = "blue";
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ for (let col = 0; col < COLS; col++) {
+ switch (board[ROWS - 1][col]) {
+ case "fallingRed":
+ board[ROWS - 1][col] = "red";
+ break;
+ case "fallingGreen":
+ board[ROWS - 1][col] = "green";
+ break;
+ case "fallingBlue":
+ board[ROWS - 1][col] = "blue";
+ break;
+ }
+ }
+
+ return somethingFell;
+ }
+
+ function draw() {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
+
+ ctx.fillStyle = "black";
+ ctx.fillRect(GRID_X, GRID_Y, GRID_WIDTH, GRID_HEIGHT);
+
+ for (let h = 0; h < ROWS; h++) {
+ for (let w = 0; w < COLS; w++) {
+ switch (board[h][w]) {
+ case "void":
+ ctx.fillStyle = "#2e2e2e";
+ break;
+ case "red":
+ ctx.fillStyle = "#994444";
+ break;
+ case "green":
+ ctx.fillStyle = "#449944";
+ break;
+ case "blue":
+ ctx.fillStyle = "#444499";
+ break;
+ case "fallingRed":
+ ctx.fillStyle = "#bb3333";
+ break;
+ case "fallingGreen":
+ ctx.fillStyle = "#33bb33";
+ break;
+ case "fallingBlue":
+ ctx.fillStyle = "#3333bb";
+ break;
+ }
+
+ const x = GRID_X + MARGIN * 1.5 + w * CELL_WIDTH + (w - 1) * MARGIN;
+ const y = GRID_Y + MARGIN * 1.5 + h * CELL_WIDTH + (h - 1) * MARGIN;
+
+ ctx.fillRect(x, y, CELL_WIDTH, CELL_WIDTH);
+ }
+ }
+ }
+
+ function gameLoop(time: number) {
+ animationFrameId = null;
+
+ if (!gameRunning) return;
+
+ if (time - lastFrame > (speedUpPressed ? fallTime / 10 : fallTime)) {
+ lastFrame = time;
+ const somethingFell = physics();
+ if (!somethingFell) {
+ const clearedRows = clearFilledRows();
+ if (clearedRows >= 4) {
+ finishGame(true);
+ return;
+ } else if (clearedRows === 0) {
+ if (!addSquares()) {
+ finishGame(false);
+ return;
+ }
+ }
+ }
+ }
+
+ draw();
+ animationFrameId = requestAnimationFrame(gameLoop);
+ }
+}
diff --git a/2026/games_submissions/gbjk/src/src/style.css b/2026/games_submissions/gbjk/src/src/style.css
new file mode 100644
index 0000000..56e07d1
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/src/style.css
@@ -0,0 +1,103 @@
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+ min-height: 100%;
+}
+
+body {
+ margin: 0;
+ color: darksalmon;
+ background-color: #2e2e2e;
+ font-family: monospace;
+ overflow-x: hidden;
+}
+
+button {
+ min-width: 0;
+ font: inherit;
+ font-size: .4rem;
+ line-height: 1.1;
+ padding: .15rem;
+ color: darksalmon;
+ background-color: #2e2e2e;
+ border-color: salmon;
+ cursor: pointer;
+ white-space: normal;
+ overflow-wrap: anywhere;
+
+ z-index: 9999;
+}
+
+button:hover {
+ background-color: darkslategray;
+}
+
+button:active {
+ background-color: slategray;
+}
+
+.layout {
+ margin: auto;
+ display: flex;
+ min-height: 100dvh;
+ width: 100%;
+ padding: .15rem;
+ gap: .2rem;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.game {
+ min-width: 0;
+}
+
+.title-bar {
+ display: flex;
+ align-items: baseline;
+ gap: .25rem;
+ margin-bottom: .2rem;
+}
+
+h1,
+p {
+ margin: 0;
+}
+
+h1 {
+ font-size: .6rem;
+ line-height: 1;
+}
+
+p {
+ font-size: .4rem;
+ line-height: 1.1;
+}
+
+canvas {
+ display: block;
+ aspect-ratio: 2 / 3;
+ border-radius: 6px;
+ border: 1px solid darksalmon;
+ box-shadow: 0px 0px 4px darksalmon;
+ height: 80vmin;
+ width: auto;
+
+ transition: transform 100ms;
+ margin: auto;
+}
+
+ul {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ gap: .15rem;
+ width: min(6rem, 42vw);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
diff --git a/2026/games_submissions/gbjk/src/tsconfig.json b/2026/games_submissions/gbjk/src/tsconfig.json
new file mode 100644
index 0000000..1ab38c8
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "es2023",
+ "module": "esnext",
+ "lib": ["ES2023", "DOM"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/2026/games_submissions/gbjk/src/vite.config.ts b/2026/games_submissions/gbjk/src/vite.config.ts
new file mode 100644
index 0000000..c9036bd
--- /dev/null
+++ b/2026/games_submissions/gbjk/src/vite.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vite";
+import { viteSingleFile } from "vite-plugin-singlefile";
+
+export default defineConfig({
+ plugins: [viteSingleFile()],
+ build: {
+ minify: false,
+ },
+});