summaryrefslogtreecommitdiff
path: root/2026/games_submissions/the0x539
diff options
context:
space:
mode:
Diffstat (limited to '2026/games_submissions/the0x539')
-rw-r--r--2026/games_submissions/the0x539/.gitignore1
-rwxr-xr-x2026/games_submissions/the0x539/MinecraftStandard.otfbin0 -> 159732 bytes
-rw-r--r--2026/games_submissions/the0x539/crafting.js98
-rw-r--r--2026/games_submissions/the0x539/data.js95
-rw-r--r--2026/games_submissions/the0x539/index.html26
-rw-r--r--2026/games_submissions/the0x539/interaction.js581
-rwxr-xr-x2026/games_submissions/the0x539/items/boots.pngbin0 -> 142 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/bow.pngbin0 -> 160 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/chest.pngbin0 -> 775 bytes
-rw-r--r--2026/games_submissions/the0x539/items/chestplate.pngbin0 -> 389 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/clock.gifbin0 -> 2153 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/cobble.pngbin0 -> 518 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/dust.pngbin0 -> 179 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/helmet.pngbin0 -> 134 bytes
-rw-r--r--2026/games_submissions/the0x539/items/hopper.pngbin0 -> 404 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/ingot.pngbin0 -> 156 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/leggings.pngbin0 -> 146 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/log.pngbin0 -> 3913 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/pickaxe.pngbin0 -> 174 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/piston.pngbin0 -> 635 bytes
-rw-r--r--2026/games_submissions/the0x539/items/plank.pngbin0 -> 4974 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/rod.pngbin0 -> 183 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/stick.pngbin0 -> 388 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/string.pngbin0 -> 162 bytes
-rwxr-xr-x2026/games_submissions/the0x539/items/tooltip-backdrop.pngbin0 -> 768 bytes
-rw-r--r--2026/games_submissions/the0x539/main.js196
-rw-r--r--2026/games_submissions/the0x539/screen.css168
-rwxr-xr-x2026/games_submissions/the0x539/sfx/big-ding.oggbin0 -> 15799 bytes
-rwxr-xr-x2026/games_submissions/the0x539/sfx/oof.oggbin0 -> 12546 bytes
-rwxr-xr-x2026/games_submissions/the0x539/sfx/small-ding.oggbin0 -> 7139 bytes
-rwxr-xr-x2026/games_submissions/the0x539/tooltip-backdrop.pngbin0 -> 710 bytes
-rw-r--r--2026/games_submissions/the0x539/ui.pngbin0 -> 764 bytes
32 files changed, 1165 insertions, 0 deletions
diff --git a/2026/games_submissions/the0x539/.gitignore b/2026/games_submissions/the0x539/.gitignore
new file mode 100644
index 0000000..845270b
--- /dev/null
+++ b/2026/games_submissions/the0x539/.gitignore
@@ -0,0 +1 @@
+harness
diff --git a/2026/games_submissions/the0x539/MinecraftStandard.otf b/2026/games_submissions/the0x539/MinecraftStandard.otf
new file mode 100755
index 0000000..8fa0b3e
--- /dev/null
+++ b/2026/games_submissions/the0x539/MinecraftStandard.otf
Binary files differ
diff --git a/2026/games_submissions/the0x539/crafting.js b/2026/games_submissions/the0x539/crafting.js
new file mode 100644
index 0000000..7a61cfc
--- /dev/null
+++ b/2026/games_submissions/the0x539/crafting.js
@@ -0,0 +1,98 @@
+import { recipes, amounts } from './data.js';
+
+export function getRecipeOutput() {
+ const input = readGrid();
+ if (input.length === 0) return null;
+
+ const mirroredInput = input.map(row => row.toReversed());
+
+ for (const [item, recipe] of Object.entries(recipes)) {
+ const isMatch = compare(input, recipe) || compare(mirroredInput, recipe);
+ if (!isMatch) continue;
+
+ const amount = amounts[item] ?? 1;
+ return [item, amount];
+ }
+
+ return null;
+}
+
+function last(arr) {
+ return arr[arr.length - 1];
+}
+
+function isSymmetric(grid) {
+ return grid.every(row => row[0] === last(row));
+}
+
+function readGrid() {
+ const cells = document.querySelectorAll('crafting-grid > inventory-cell');
+
+ const rows = [];
+ for (let y = 0; y < 3; y++) {
+ const row = [];
+ for (let x = 0; x < 3; x++) {
+ let item = null;
+ const stack = cells[3 * y + x].firstElementChild;
+ if (!!stack) {
+ item = stack.getAttribute('data-item');
+ }
+ row.push(item);
+ }
+ rows.push(row);
+ }
+
+ trimGrid(rows);
+
+ return rows;
+}
+
+function trimGrid(grid) {
+ if (grid.flat().every(v => v === null)) {
+ while (grid.length > 0) {
+ grid.pop();
+ }
+ return;
+ }
+
+ // it is now established that the grid is non-empty,
+ // so there will always be at least one row+col
+
+ // trim blank rows from bottom
+ while (last(grid).every(v => v === null)) {
+ grid.pop();
+ }
+
+ // trim blank rows from top
+ while (grid[0].every(v => v === null)) {
+ grid.shift();
+ }
+
+ // trim blank columns from right
+ while (grid.every(row => last(row) === null)) {
+ for (const row of grid) {
+ row.pop();
+ }
+ }
+
+ // trim blank columns from left
+ while (grid.every(row => row[0] === null)) {
+ for (const row of grid) {
+ row.shift();
+ }
+ }
+}
+
+function compare(a, b) {
+ if (a.length !== b.length) return false;
+
+ for (let y = 0; y < a.length; y++) {
+ if (a[y].length !== b[y].length) return false;
+
+ for (let x = 0; x < a[y].length; x++) {
+ if (a[y][x] !== b[y][x]) return false;
+ }
+ }
+
+ return true;
+}
diff --git a/2026/games_submissions/the0x539/data.js b/2026/games_submissions/the0x539/data.js
new file mode 100644
index 0000000..dba5d6d
--- /dev/null
+++ b/2026/games_submissions/the0x539/data.js
@@ -0,0 +1,95 @@
+const ingredients = {
+ i: 'ingot',
+ l: 'log',
+ p: 'plank',
+ s: 'stick',
+ d: 'dust',
+ c: 'cobble',
+ C: 'chest',
+ S: 'string',
+}
+
+function recipe(...shape) {
+ return shape.map(line => Array.from(line).map(ch => {
+ if (ch === ' ') {
+ return null;
+ } else {
+ const item = ingredients[ch];
+ console.assert(!!item);
+ return item;
+ }
+ }));
+}
+
+export const recipes = {
+ helmet: recipe(
+ 'iii',
+ 'i i',
+ ),
+ chestplate: recipe(
+ 'i i',
+ 'iii',
+ 'iii',
+ ),
+ leggings: recipe(
+ 'iii',
+ 'i i',
+ 'i i',
+ ),
+ boots: recipe(
+ 'i i',
+ 'i i',
+ ),
+ plank: recipe(
+ 'l',
+ ),
+ stick: recipe(
+ 'p',
+ 'p',
+ ),
+ pickaxe: recipe(
+ 'ppp',
+ ' s ',
+ ' s ',
+ ),
+ piston: recipe(
+ 'ppp',
+ 'cic',
+ 'cdc',
+ ),
+ chest: recipe(
+ 'ppp',
+ 'p p',
+ 'ppp',
+ ),
+ hopper: recipe(
+ 'i i',
+ 'iCi',
+ ' i ',
+ ),
+ rod: recipe(
+ ' s',
+ ' sS',
+ 's S',
+ ),
+ bow: recipe(
+ ' sS',
+ 's S',
+ ' sS',
+ ),
+};
+
+export const amounts = {
+ plank: 4,
+ stick: 4,
+}
+
+export const stackSizes = {
+ helmet: 1,
+ chestplate: 1,
+ leggings: 1,
+ boots: 1,
+ pickaxe: 1,
+ rod: 1,
+ bow: 1,
+}
diff --git a/2026/games_submissions/the0x539/index.html b/2026/games_submissions/the0x539/index.html
new file mode 100644
index 0000000..5a073ed
--- /dev/null
+++ b/2026/games_submissions/the0x539/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <script defer type="module" src="./main.js"></script>
+ <link rel="stylesheet" href="./screen.css" />
+ </head>
+ <body>
+ <img class="background" src="./ui.png" />
+
+ <label for="todo-list">To do:</label>
+ <ul id="todo-list"></ul>
+
+ <crafting-grid></crafting-grid>
+ <crafting-output></crafting-output>
+ <inventory-grid></inventory-grid>
+ <follow-cursor>
+ <item-tooltip></item-tooltip>
+ <grabbed-stack></grabbed-stack>
+ </follow-cursor>
+
+ <audio src="./sfx/small-ding.ogg" id="small-ding" preload="auto"></audio>
+ <audio src="./sfx/big-ding.ogg" id="big-ding" preload="auto"></audio>
+ <audio src="./sfx/oof.ogg" id="oof" preload="auto"></audio>
+ </body>
+</html>
diff --git a/2026/games_submissions/the0x539/interaction.js b/2026/games_submissions/the0x539/interaction.js
new file mode 100644
index 0000000..9c3cd37
--- /dev/null
+++ b/2026/games_submissions/the0x539/interaction.js
@@ -0,0 +1,581 @@
+import { stackSizes } from './data.js';
+import { getRecipeOutput } from './crafting.js';
+
+class CraftEvent extends Event {
+ item;
+ constructor(item) {
+ super('craft');
+ this.item = item;
+ }
+}
+
+const grabbedStack = document.querySelector('grabbed-stack');
+const craftingGrid = document.querySelector('crafting-grid');
+const inventoryGrid = document.querySelector('inventory-grid');
+const outputCell = document.querySelector('crafting-output');
+
+let state = 'idle';
+
+const splitTargets = new Set();
+
+export function resetInventory() {
+ for (const stack of document.querySelectorAll('item-stack')) {
+ stack.remove();
+ }
+ splitTargets.clear();
+ state = 'idle';
+}
+
+export function createItem(item, count) {
+ const elem = document.createElement('item-stack');
+ elem.setAttribute('data-item', item);
+ elem.setAttribute('data-count', count);
+ const countElem = document.createElement('data');
+ countElem.textContent = count.toString();
+ elem.appendChild(countElem);
+ return elem;
+}
+
+export function mouseDown(event) {
+ if (state !== 'idle') return;
+ if (event.button !== 0 && event.button !== 2) return;
+
+ const cell = event.currentTarget;
+
+ // Shift click: Transfer a full stack to the other place.
+ if (event.shiftKey) {
+ const sourceStack = cell.firstElementChild;
+ if (sourceStack) {
+ const targetGrid = cell.parentElement === inventoryGrid ? craftingGrid : inventoryGrid;
+ shiftClickTransfer(sourceStack, targetGrid);
+ }
+ state = 'shift-drag';
+ return;
+ }
+
+ const heldStack = grabbedStack.firstElementChild;
+
+ // Click with empty cursor: pick up items
+ if (!heldStack) {
+ const sourceStack = cell.firstElementChild;
+ if (!sourceStack) {
+ return;
+ }
+
+ if (event.button === 2) {
+ // Right click: take half the stack, rounded up
+ const sourceCount = getCount(sourceStack);
+ if (sourceCount === 1) {
+ grabbedStack.appendChild(sourceStack);
+ updateRecipeOutput();
+ return;
+ }
+
+ const takenCount = Math.ceil(sourceCount / 2);
+ const newCount = sourceCount - takenCount;
+ const item = sourceStack.getAttribute('data-item');
+ setCount(sourceStack, newCount);
+ grabbedStack.appendChild(createItem(item, takenCount));
+ } else {
+ // Left click: take the whole stack. Drag to pick up other stacks
+ grabbedStack.appendChild(sourceStack);
+ state = 'pickup-drag';
+ updateRecipeOutput();
+ }
+ } else {
+ // Click with items held: deposit items
+
+ if (event.button === 2) {
+ // Right click: deposit one item. Drag to deposit one item in each cell
+ state = 'split-one';
+ depositOne(cell);
+ } else {
+ // Left click: split stack evenly across all empty/compatible cells.
+ // If no compatible cells are dragged over, swap stack with the cell the mouse was released on.
+ // (In the common case, this is just what a "click" looks like on a cell with incompatible contents.)
+ heldStack.setAttribute('data-original-count', heldStack.getAttribute('data-count'));
+ state = 'split-evenly';
+ addSplitTarget(cell);
+ }
+ }
+}
+
+export function mouseEnter(event) {
+ const cell = event.currentTarget;
+
+ switch (state) {
+ case 'shift-drag': {
+ const sourceStack = cell.firstElementChild;
+ if (!sourceStack) break;
+ const targetGrid = cell.parentElement === inventoryGrid ? craftingGrid : inventoryGrid;
+ shiftClickTransfer(sourceStack, targetGrid);
+ updateRecipeOutput();
+ break;
+ }
+
+ case 'pickup-drag': {
+ const sourceStack = cell.firstElementChild;
+ if (!sourceStack) break;
+ const heldStack = grabbedStack.firstElementChild;
+ const item = sourceStack.getAttribute('data-item');
+ if (item !== heldStack.getAttribute('data-item')) return;
+
+ const stackSize = stackSizes[item] ?? 64;
+
+ const newCount = getCount(heldStack) + getCount(sourceStack);
+ if (newCount > stackSize) return;
+
+ setCount(heldStack, newCount);
+ sourceStack.remove();
+ updateRecipeOutput();
+ break;
+ }
+
+ case 'split-one':
+ depositOne(cell);
+ break;
+
+ case 'split-evenly':
+ addSplitTarget(cell);
+ break;
+
+ default:
+ break;
+ }
+}
+
+export function mouseUp(event) {
+ switch (state) {
+ case 'split-evenly':
+ case 'split-exhausted': {
+ if (event.button !== 0) return;
+
+ if (splitTargets.size <= 1) {
+ const targetCell = event.target.closest('inventory-cell');
+ if (targetCell) {
+ depositAll(targetCell);
+ }
+ } else {
+ commitSplit();
+ }
+ break;
+ }
+
+ case 'pickup-drag':
+ if (event.button !== 0) return;
+ state = 'idle';
+ break;
+
+ case 'split-one':
+ if (event.button !== 2) return;
+ state = 'idle';
+ break;
+
+ case 'shift-drag':
+ // this could be either button. whatever.
+ state = 'idle';
+ break;
+
+ default:
+ break;
+ }
+}
+
+export function craftClick(event) {
+ if (event.shiftKey) {
+ craftAll();
+ } else {
+ craftOne();
+ }
+}
+
+function craftOne() {
+ const previewStack = outputCell.firstElementChild;
+ if (!previewStack) return;
+
+ const item = previewStack.getAttribute('data-item');
+ const craftCount = getCount(previewStack);
+ const stackSize = stackSizes[item] ?? 64;
+
+ const heldStack = grabbedStack.firstElementChild;
+ if (heldStack !== null) {
+ if (heldStack.getAttribute('data-item') !== item) {
+ return;
+ }
+
+ const heldCount = getCount(heldStack);
+ if (heldCount + craftCount > stackSize) {
+ return;
+ }
+
+ setCount(heldStack, heldCount + craftCount);
+ } else {
+ grabbedStack.appendChild(createItem(item, craftCount));
+ }
+ consumeIngredients();
+ document.dispatchEvent(new CraftEvent(item));
+}
+
+function craftAll() {
+ const previewStack = outputCell.firstElementChild;
+ if (!previewStack) return;
+
+ const item = previewStack.getAttribute('data-item');
+ const craftCount = getCount(previewStack);
+ const stackSize = stackSizes[item] ?? 64;
+
+ let success = false;
+
+ do {
+ let unallocated = craftCount;
+
+ const nonEmptyTargets = [];
+ let emptyTarget = null;
+
+ for (const cell of inventoryGrid.children) {
+ const stack = cell.firstElementChild;
+ if (stack === null) {
+ // This slot is empty.
+ // All crafting outputs that don't fit into existing stacks
+ // will be deposited into the first empty slot.
+ emptyTarget ??= cell;
+ continue;
+ }
+
+ if (stack.getAttribute('data-item') !== item) {
+ // This slot already contains a different item,
+ // so we can't add crafting outputs to it.
+ continue;
+ }
+
+ const spareCapacity = stackSize - getCount(stack);
+ if (spareCapacity <= 0) {
+ // This slot contains the correct item, but is already full.
+ continue;
+ }
+
+ const amountToAdd = Math.min(unallocated, spareCapacity);
+ unallocated -= amountToAdd;
+ nonEmptyTargets.push([stack, amountToAdd]);
+
+ if (unallocated === 0) {
+ // All crafting outputs can fit into existing stacks of the item.
+ break;
+ }
+ }
+
+ if (unallocated > 0 && emptyTarget === null) {
+ // The inventory is full: no slots are empty and existing stacks don't have enough room.
+ // Abort this attempt and conclude the loop.
+ break;
+ }
+
+ // Add crafting output to existing stacks, to whatever extent possible.
+ for (const [stack, amountToAdd] of nonEmptyTargets) {
+ setCount(stack, getCount(stack) + amountToAdd);
+ }
+
+ // Put any remaining items into the first empty slot.
+ if (unallocated > 0) {
+ emptyTarget.appendChild(createItem(item, unallocated));
+ }
+
+ success = true;
+
+ // Consume one set of ingredients.
+ // Cease crafting if this causes the recipe output to change.
+ } while (!consumeIngredients());
+
+ if (success) {
+ document.dispatchEvent(new CraftEvent(item));
+ }
+}
+
+function consumeIngredients() {
+ let anyExhausted = false;
+
+ for (const cell of craftingGrid.children) {
+ const stack = cell.firstElementChild;
+ if (!stack) continue;
+
+ const count = getCount(stack);
+ if (count === 1) {
+ stack.remove();
+ anyExhausted = true;
+ } else {
+ setCount(stack, count - 1);
+ }
+ }
+
+ return anyExhausted && updateRecipeOutput();
+}
+
+// Returns whether the output actually changed
+function updateRecipeOutput() {
+ const outputStack = outputCell.firstElementChild;
+
+ const output = getRecipeOutput();
+ if (output === null) {
+ if (outputStack === null) {
+ return false;
+ } else {
+ outputStack.remove();
+ return true;
+ }
+ }
+
+ const [item, count] = output;
+ if (outputStack === null) {
+ outputCell.appendChild(createItem(item, count));
+ return true;
+ }
+
+ if (outputStack.getAttribute('data-item') === item && getCount(outputStack) === count) {
+ return false;
+ }
+
+ outputStack.setAttribute('data-item', item);
+ setCount(outputStack, count);
+ return true;
+}
+
+export function consumeClock() {
+ if (state === 'split-exhausted') {
+ commitSplit();
+ }
+
+ if (state === 'split-evenly' && splitTargets.size > 1) {
+ // oh dear. this is a complicated situation
+ // first, let's try to search for uninvolved clocks
+ const uninvolved = document.querySelector('inventory-cell > item-stack[data-item="clock"]:not([data-original-count])');
+ if (!!uninvolved) {
+ const count = getCount(uninvolved);
+ if (count === 1) {
+ uninvolved.remove();
+ } else {
+ setCount(uninvolved, count - 1);
+ }
+ // phew
+ return;
+ }
+
+ // okay so we will actually need to remove a clock from the items being split, ugh
+ // not a lot of code, but low confidence that it works properly
+ const heldStack = grabbedStack.firstElementChild;
+ const count = +heldStack.getAttribute('data-original-count');
+ heldStack.setAttribute('data-original-count', count - 1);
+ updateSplitPreview();
+ return;
+ }
+
+ const stack = document.querySelector('item-stack[data-item="clock"][data-count]');
+ const count = getCount(stack);
+ if (count === 1) {
+ if (stack.parentElement === grabbedStack) {
+ // conclude drag operations if they're done using a stack of clocks that gets deleted
+ state = 'idle';
+ }
+ stack.remove();
+ } else {
+ setCount(stack, count - 1);
+ }
+}
+
+function getCount(itemStack) {
+ return +itemStack.getAttribute('data-count');
+}
+
+function setCount(itemStack, value) {
+ itemStack.setAttribute('data-count', value);
+ itemStack.firstElementChild.textContent = value.toString();
+}
+
+function depositOne(targetCell) {
+ const heldStack = grabbedStack.firstElementChild;
+ const item = heldStack.getAttribute('data-item');
+ const heldCount = getCount(heldStack);
+
+ const stackSize = stackSizes[item] ?? 64;
+
+ const targetStack = targetCell.firstElementChild;
+ if (targetStack) {
+ if (targetStack.getAttribute('data-item') !== item) {
+ return;
+ }
+
+ const targetCount = getCount(targetStack);
+ if (targetCount >= stackSize) return;
+
+ setCount(targetStack, targetCount + 1);
+ } else {
+ targetCell.appendChild(createItem(item, 1));
+ }
+
+ if (heldCount === 1) {
+ heldStack.remove();
+ state = 'idle';
+ } else {
+ setCount(heldStack, heldCount - 1);
+ }
+
+ updateRecipeOutput();
+}
+
+function shiftClickTransfer(sourceStack, targetGrid) {
+ const item = sourceStack.getAttribute('data-item');
+ const stackSize = stackSizes[item] ?? 64;
+ let count = getCount(sourceStack);
+
+ // First, search for existing stacks to add to
+ for (const cell of targetGrid.children) {
+ const targetStack = cell.firstElementChild;
+ if (!targetStack) continue;
+ if (targetStack.getAttribute('data-item') !== item) continue;
+ const curCount = getCount(targetStack);
+ const available = stackSize - curCount;
+ if (available === 0) continue;
+
+ const transferSize = Math.min(count, available);
+ const newCount = curCount + transferSize;
+ setCount(targetStack, newCount);
+
+ count -= transferSize;
+ if (count === 0) {
+ sourceStack.remove();
+ updateRecipeOutput();
+ return;
+ } else {
+ setCount(sourceStack, count);
+ }
+ }
+
+ // Now, look for any empty slots to move whatever's left to
+ for (const cell of targetGrid.children) {
+ if (!cell.firstElementChild) {
+ cell.appendChild(sourceStack);
+ break;
+ }
+ }
+
+ updateRecipeOutput();
+}
+
+function addSplitTarget(targetCell) {
+ const heldStack = grabbedStack.firstElementChild;
+ const item = heldStack.getAttribute('data-item');
+
+ const targetStack = targetCell.firstElementChild;
+ if (targetStack) {
+ if (targetStack.getAttribute('data-item') !== item) {
+ return;
+ }
+ }
+
+ splitTargets.add(targetCell);
+
+ if (splitTargets.size > 1) {
+ updateSplitPreview();
+ }
+}
+
+function updateSplitPreview() {
+ const heldStack = grabbedStack.firstElementChild;
+ const item = heldStack.getAttribute('data-item');
+ const heldCount = +heldStack.getAttribute('data-original-count');
+
+ const stackSize = stackSizes[item] ?? 64;
+
+ const splitCount = Math.max(1, Math.floor(heldCount / splitTargets.size));
+ let remainder = heldCount;
+
+ for (const targetCell of splitTargets) {
+ let targetStack = targetCell.firstElementChild;
+ if (!targetStack) {
+ targetStack = createItem(item, 0);
+ targetCell.appendChild(targetStack);
+ }
+ if (!targetStack.hasAttribute('data-original-count')) {
+ targetStack.setAttribute('data-original-count', targetStack.getAttribute('data-count'));
+ }
+ const targetCount = +targetStack.getAttribute('data-original-count');
+ const available = stackSize - targetCount;
+ const transferSize = Math.min(splitCount, available);
+ setCount(targetStack, targetCount + transferSize);
+ remainder -= transferSize;
+ // TODO: give stacks yellow text if they're too full to accomodate their full share of the split
+
+ if (remainder === 0) break;
+ }
+
+ setCount(heldStack, remainder);
+
+ if (splitCount === 1 && remainder === 0) {
+ state = 'split-exhausted';
+ }
+}
+
+function commitSplit() {
+ const heldStack = grabbedStack.firstElementChild;
+ const heldCount = getCount(heldStack);
+
+ if (heldCount === 0) {
+ heldStack.remove();
+ } else {
+ heldStack.removeAttribute('data-original-count');
+ }
+
+ for (const target of splitTargets) {
+ target.firstElementChild?.removeAttribute('data-original-count');
+ }
+
+ splitTargets.clear();
+
+ state = 'idle';
+
+ updateRecipeOutput();
+}
+
+function depositAll(targetCell) {
+ const heldStack = grabbedStack.firstElementChild;
+ const item = heldStack.getAttribute('data-item');
+ const heldCount = getCount(heldStack);
+
+ const stackSize = stackSizes[item] ?? 64;
+
+ const targetStack = targetCell.firstElementChild;
+ if (!!targetStack && targetStack.getAttribute('data-item') !== item) {
+ // swap held and target stacks because they don't match
+ targetCell.appendChild(heldStack);
+ grabbedStack.appendChild(targetStack);
+ } else {
+ if (!targetStack) {
+ // deposit the full stack to the slot
+ targetCell.appendChild(heldStack);
+ } else {
+ // deposit as much as possible
+ const targetCount = getCount(targetStack);
+ const available = stackSize - targetCount;
+ const transferSize = Math.min(heldCount, available);
+ const newCount = targetCount + transferSize;
+ setCount(targetStack, newCount);
+
+ if (transferSize < heldCount) {
+ setCount(heldStack, heldCount - transferSize);
+ } else {
+ heldStack.remove();
+ }
+ }
+ }
+
+ heldStack.removeAttribute('data-original-count');
+
+ for (const target of splitTargets) {
+ target.firstElementChild?.removeAttribute('data-original-count');
+ }
+
+ splitTargets.clear();
+
+ state = 'idle';
+
+ updateRecipeOutput();
+}
diff --git a/2026/games_submissions/the0x539/items/boots.png b/2026/games_submissions/the0x539/items/boots.png
new file mode 100755
index 0000000..ca6c33b
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/boots.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/bow.png b/2026/games_submissions/the0x539/items/bow.png
new file mode 100755
index 0000000..6922e69
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/bow.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/chest.png b/2026/games_submissions/the0x539/items/chest.png
new file mode 100755
index 0000000..eb7f766
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/chest.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/chestplate.png b/2026/games_submissions/the0x539/items/chestplate.png
new file mode 100644
index 0000000..0cc9a63
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/chestplate.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/clock.gif b/2026/games_submissions/the0x539/items/clock.gif
new file mode 100755
index 0000000..d4a7698
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/clock.gif
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/cobble.png b/2026/games_submissions/the0x539/items/cobble.png
new file mode 100755
index 0000000..55dbc4f
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/cobble.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/dust.png b/2026/games_submissions/the0x539/items/dust.png
new file mode 100755
index 0000000..9d17c14
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/dust.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/helmet.png b/2026/games_submissions/the0x539/items/helmet.png
new file mode 100755
index 0000000..850dac6
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/helmet.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/hopper.png b/2026/games_submissions/the0x539/items/hopper.png
new file mode 100644
index 0000000..9655818
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/hopper.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/ingot.png b/2026/games_submissions/the0x539/items/ingot.png
new file mode 100755
index 0000000..17e8aa8
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/ingot.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/leggings.png b/2026/games_submissions/the0x539/items/leggings.png
new file mode 100755
index 0000000..a28a10f
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/leggings.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/log.png b/2026/games_submissions/the0x539/items/log.png
new file mode 100755
index 0000000..56d24f0
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/log.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/pickaxe.png b/2026/games_submissions/the0x539/items/pickaxe.png
new file mode 100755
index 0000000..9dea9e2
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/pickaxe.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/piston.png b/2026/games_submissions/the0x539/items/piston.png
new file mode 100755
index 0000000..f08618a
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/piston.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/plank.png b/2026/games_submissions/the0x539/items/plank.png
new file mode 100644
index 0000000..863206a
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/plank.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/rod.png b/2026/games_submissions/the0x539/items/rod.png
new file mode 100755
index 0000000..6610384
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/rod.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/stick.png b/2026/games_submissions/the0x539/items/stick.png
new file mode 100755
index 0000000..d32381d
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/stick.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/string.png b/2026/games_submissions/the0x539/items/string.png
new file mode 100755
index 0000000..ad78eb4
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/string.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/items/tooltip-backdrop.png b/2026/games_submissions/the0x539/items/tooltip-backdrop.png
new file mode 100755
index 0000000..05c02cd
--- /dev/null
+++ b/2026/games_submissions/the0x539/items/tooltip-backdrop.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/main.js b/2026/games_submissions/the0x539/main.js
new file mode 100644
index 0000000..b89465e
--- /dev/null
+++ b/2026/games_submissions/the0x539/main.js
@@ -0,0 +1,196 @@
+import { recipes, amounts } from './data.js';
+import {
+ createItem,
+ mouseDown,
+ mouseEnter,
+ mouseUp,
+ craftClick,
+ consumeClock,
+ resetInventory,
+} from './interaction.js';
+
+const followCursor = document.querySelector('follow-cursor');
+const tooltip = document.querySelector('item-tooltip');
+const craftingGrid = document.querySelector('crafting-grid');
+const inventoryGrid = document.querySelector('inventory-grid');
+const craftingOutput = document.querySelector('crafting-output');
+const todoList = document.getElementById('todo-list');
+
+const desiredCrafts = new Set();
+
+const playSound = {
+ playImpl(id, volume = 1.0, pitch = 1.0) {
+ const audio = document.getElementById(id);
+
+ audio.currentTime = 0;
+
+ const p = new Promise(resolve => {
+ const f = (event) => {
+ audio.removeEventListener('ended', f);
+ resolve(event);
+ };
+ audio.addEventListener('ended', f);
+ });
+
+ audio.volume = volume;
+ audio.playbackRate = pitch;
+ audio.preservesPitch = false;
+
+ audio.play();
+ return p;
+ },
+
+ smallDing() {
+ return this.playImpl('small-ding', 0.1, 0.55 + 0.7 * Math.random());
+ },
+
+ bigDing() {
+ return this.playImpl('big-ding', 0.75);
+ },
+
+ oof() {
+ return this.playImpl('oof', 0.75);
+ },
+};
+
+function onCraft(event) {
+ const item = event.item;
+ if (desiredCrafts.has(item)) {
+ desiredCrafts.delete(item);
+
+ document.querySelector(`#todo-list > li:has([data-item="${item}"])`)?.remove();
+
+ if (desiredCrafts.size > 0) {
+ playSound.smallDing();
+ } else {
+ endGame(true);
+ }
+ }
+}
+
+function handleMessage(msg) {
+ if (msg.op === 'start') {
+ startGame(msg.difficulty);
+ }
+}
+
+function giveItem(item, count) {
+ const emptySlots = document.querySelectorAll('inventory-grid > inventory-cell:empty');
+ const index = Math.floor(Math.random() * emptySlots.length);
+ emptySlots[index].appendChild(createItem(item, count));
+}
+
+let timerInterval = null;
+
+function timer() {
+ if (!document.querySelector('item-stack[data-item="clock"]')) {
+ // time's up!
+ endGame(false);
+ } else {
+ consumeClock();
+ }
+}
+
+function startGame(difficulty) {
+ document.body.classList.remove('failed');
+
+ resetInventory();
+
+ desiredCrafts.clear();
+ desiredCrafts.add('helmet');
+ desiredCrafts.add('chestplate');
+ desiredCrafts.add('leggings');
+ desiredCrafts.add('boots');
+
+ todoList.replaceChildren();
+ for (const item of desiredCrafts) {
+ const listItem = document.createElement('li');
+
+ const stack = document.createElement('item-stack');
+ stack.setAttribute('data-item', item);
+ listItem.append(stack);
+
+ todoList.append(listItem);
+ }
+
+ giveItem('log', 64);
+ giveItem('ingot', 64);
+ giveItem('clock', 12);
+ giveItem('dust', 16);
+ giveItem('cobble', 16);
+
+ document.addEventListener('mousemove', onMouseMove);
+ timerInterval = setInterval(timer, 1000);
+ window.parent.postMessage({ op: 'started', verb: 'craft!' });
+}
+
+async function endGame(win) {
+ clearInterval(timerInterval);
+ timerInterval = null;
+ if (win) {
+ await playSound.bigDing();
+ } else {
+ document.body.classList.add('failed');
+ await playSound.oof();
+ }
+ await new Promise(resolve => setTimeout(resolve, 500));
+ document.removeEventListener('mousemove', onMouseMove);
+ window.parent.postMessage({ op: 'done', win });
+}
+
+function createCell() {
+ const elem = document.createElement('inventory-cell');
+ elem.addEventListener('mousedown', mouseDown);
+ elem.addEventListener('mouseenter', mouseEnter);
+ return elem;
+}
+
+function onMouseMove(event) {
+ followCursor.setAttribute('style', `left: ${event.clientX}px; top: ${event.clientY}px`);
+
+ const hoverItem = document.querySelector('item-stack:hover');
+ if (hoverItem !== null) {
+ const name = hoverItem.getAttribute('data-item');
+ tooltip.textContent = name;
+ } else {
+ tooltip.textContent = '';
+ }
+}
+
+document.addEventListener('contextmenu', e => e.preventDefault());
+
+const allItems = new Set();
+for (const [output, shape] of Object.entries(recipes)) {
+ allItems.add(output);
+ for (const item of shape.flat()) {
+ if (item) {
+ allItems.add(item);
+ }
+ }
+}
+
+const stylesheet = document.styleSheets[0];
+
+for (const item of allItems) {
+ stylesheet.insertRule(`
+ item-stack[data-item="${item}"]::before {
+ background-image: url("./items/${item}.png");
+ }
+ `);
+}
+
+for (let i = 0; i < 3 * 3; i++) {
+ craftingGrid.appendChild(createCell());
+}
+
+for (let i = 0; i < 12 * 3; i++) {
+ inventoryGrid.appendChild(createCell());
+}
+
+document.addEventListener('mouseup', mouseUp);
+document.addEventListener('craft', onCraft);
+
+craftingOutput.addEventListener('click', craftClick);
+
+window.addEventListener('message', m => handleMessage(m.data));
+window.parent.postMessage({ op: 'ready' });
diff --git a/2026/games_submissions/the0x539/screen.css b/2026/games_submissions/the0x539/screen.css
new file mode 100644
index 0000000..d9a6f72
--- /dev/null
+++ b/2026/games_submissions/the0x539/screen.css
@@ -0,0 +1,168 @@
+@font-face {
+ font-family: "Minecraft";
+ src: url("./MinecraftStandard.otf") format("opentype");
+}
+
+* {
+ box-sizing: border-box;
+ user-select: none;
+ font-family: Minecraft;
+ font-smooth: never;
+ font-size: 6px;
+ text-rendering: geometricPrecision;
+ line-height: 1em;
+}
+
+body {
+ margin: 0;
+}
+
+crafting-grid,
+inventory-grid {
+ position: absolute;
+ display: grid;
+ align-items: center;
+ justify-items: center;
+}
+
+follow-cursor {
+ position: absolute;
+ pointer-events: none;
+}
+
+grabbed-stack {
+ display: block;
+ translate: -50% -50%;
+ z-index: 10;
+ pointer-events: none;
+}
+
+:root:has(grabbed-stack > item-stack) {
+ cursor: grabbing;
+}
+
+inventory-cell,
+crafting-output {
+ width: 16px;
+ height: 16px;
+
+ &:hover,
+ &:has(> item-stack[data-original-count]) {
+ background-color: rgba(255, 255, 255, 0.5);
+ }
+
+ &:has(> item-stack) {
+ cursor: grab;
+ }
+}
+
+crafting-grid {
+ left: 62px;
+ top: 22px;
+ grid-template: repeat(3, 18px) / repeat(3, 18px);
+}
+
+crafting-output {
+ position: absolute;
+ left: 157px;
+ top: 41px;
+}
+
+inventory-grid {
+ left: 12px;
+ top: 94px;
+ grid-template: repeat(3, 18px) / repeat(12, 18px);
+}
+
+item-stack {
+ display: block;
+ position: relative;
+
+ & > data {
+ position: absolute;
+ color: #fcfcfc;
+ text-shadow: #3e3e3e 1px 1px;
+ bottom: 0;
+ right: 0;
+ translate: 1px -1px;
+ }
+
+ &[data-count="0"] {
+ display: none;
+ }
+
+ &[data-count="1"] > data {
+ display: none;
+ }
+
+ &[data-item="clock"]::before {
+ background-image: url("./items/clock.gif");
+ }
+
+ &::before {
+ display: block;
+ width: 16px;
+ height: 16px;
+ content: "";
+ background-size: 16px 16px;
+ }
+}
+
+img {
+ pointer-events: none;
+}
+
+item-tooltip {
+ display: block;
+ color: white;
+ border-style: solid;
+ border-width: 4px;
+ border-image-source: url("./tooltip-backdrop.png");
+ border-image-slice: 4 fill;
+ text-shadow: gray 1px 1px;
+ height: 16px;
+ line-height: 1em;
+ color: #fcfcfc;
+ text-shadow: #3e3e3e 1px 1px;
+ translate: 8px -100%;
+
+ &:empty,
+ &:has(+ * > item-stack) {
+ display: none;
+ }
+}
+
+body.failed {
+ filter: brightness(0.8) sepia(75%) hue-rotate(-45deg);
+ cursor: not-allowed;
+ & > * {
+ pointer-events: none;
+ }
+}
+
+label[for="todo-list"] {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ color: #3f3f3f;
+
+ &:has(+ #todo-list:empty) {
+ display: none;
+ }
+}
+
+#todo-list {
+ position: absolute;
+ left: 12px;
+ top: 24px;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ width: 32px;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ & > li {
+ list-style: none;
+ }
+}
diff --git a/2026/games_submissions/the0x539/sfx/big-ding.ogg b/2026/games_submissions/the0x539/sfx/big-ding.ogg
new file mode 100755
index 0000000..a3f4c0b
--- /dev/null
+++ b/2026/games_submissions/the0x539/sfx/big-ding.ogg
Binary files differ
diff --git a/2026/games_submissions/the0x539/sfx/oof.ogg b/2026/games_submissions/the0x539/sfx/oof.ogg
new file mode 100755
index 0000000..10448dc
--- /dev/null
+++ b/2026/games_submissions/the0x539/sfx/oof.ogg
Binary files differ
diff --git a/2026/games_submissions/the0x539/sfx/small-ding.ogg b/2026/games_submissions/the0x539/sfx/small-ding.ogg
new file mode 100755
index 0000000..a38b2cc
--- /dev/null
+++ b/2026/games_submissions/the0x539/sfx/small-ding.ogg
Binary files differ
diff --git a/2026/games_submissions/the0x539/tooltip-backdrop.png b/2026/games_submissions/the0x539/tooltip-backdrop.png
new file mode 100755
index 0000000..893b468
--- /dev/null
+++ b/2026/games_submissions/the0x539/tooltip-backdrop.png
Binary files differ
diff --git a/2026/games_submissions/the0x539/ui.png b/2026/games_submissions/the0x539/ui.png
new file mode 100644
index 0000000..ab1873c
--- /dev/null
+++ b/2026/games_submissions/the0x539/ui.png
Binary files differ