branchjam / src /artifact.js
abhid1234's picture
Upload folder using huggingface_hub
2fe29a7 verified
Raw
History Blame Contribute Delete
7.99 kB
const SIZE = 5;
export const SIGNAL_GARDEN_MODES = Object.freeze([
"open", "trail", "wrap", "sequence", "rotate", "mirror", "rhythm",
"trail-gates", "wrap-current", "wrap-anchor", "reverse",
"rotate-double", "rotate-counter", "mirror-all", "mirror-toggle",
"rhythm-every", "rhythm-third"
]);
const starts = [
[0, 4], [2, 1], [4, 3], [1, 0], [3, 4], [4, 0]
];
export function mountSignalGarden(container, definition) {
let position = 12;
let collected = new Set();
let blocked = new Set();
let trail = [];
let moves = 0;
let rotation = 0;
let mirrorHorizontal = definition.parameters.mode === "mirror";
let destroyed = false;
const heading = document.createElement("h2");
heading.id = "play-heading";
heading.tabIndex = -1;
heading.dataset.focusKey = "artifact-heading";
heading.textContent = definition.title;
const summary = document.createElement("p");
summary.className = "artifact-summary";
summary.textContent = definition.summary;
const rule = document.createElement("p");
rule.className = "rule-card";
const ruleLabel = document.createElement("strong");
ruleLabel.textContent = "Playable rule";
const ruleText = document.createElement("span");
ruleText.textContent = definition.parameters.rule;
rule.append(ruleLabel, ruleText);
const instructions = document.createElement("p");
instructions.id = "artifact-instructions";
instructions.className = "instructions";
instructions.textContent = definition.instructions;
const board = document.createElement("div");
board.className = `garden garden--${definition.parameters.accent}`;
board.setAttribute("role", "group");
board.setAttribute("aria-label", `${definition.title} play field`);
board.setAttribute("aria-describedby", "artifact-instructions artifact-progress");
const progress = document.createElement("p");
progress.id = "artifact-progress";
progress.className = "progress";
progress.setAttribute("aria-live", "polite");
const reset = document.createElement("button");
reset.type = "button";
reset.className = "artifact-reset";
reset.textContent = "Reset artifact";
reset.dataset.focusKey = "artifact-reset";
const pulseCells = () => starts.slice(0, definition.parameters.target).map(([x, y]) => y * SIZE + x);
const wanted = () => definition.parameters.mode === "reverse"
? pulseCells()[definition.parameters.target - 1 - collected.size]
: pulseCells()[collected.size];
function draw() {
const restoreSparkFocus = board.contains(document.activeElement);
board.replaceChildren();
const pulses = pulseCells();
for (let index = 0; index < SIZE * SIZE; index += 1) {
const cell = document.createElement("button");
cell.type = "button";
cell.className = "garden-cell";
cell.dataset.index = String(index);
cell.tabIndex = index === position ? 0 : -1;
const pulseIndex = pulses.indexOf(index);
const hasPulse = pulseIndex >= 0 && !collected.has(index);
const location = `row ${Math.floor(index / SIZE) + 1}, column ${(index % SIZE) + 1}`;
if (index === position) {
cell.classList.add("is-spark");
cell.textContent = "✦";
cell.setAttribute("aria-label", `Spark, ${location}. Use Arrow keys or WASD to move.`);
cell.setAttribute("aria-current", "true");
} else if (blocked.has(index)) {
cell.classList.add("is-blocked");
cell.textContent = "◆";
cell.setAttribute("aria-label", `Blocked tile, ${location}`);
cell.disabled = true;
} else if (hasPulse) {
cell.classList.add("is-pulse");
cell.textContent = ["1", "2", "3", "4", "5", "6"][pulseIndex];
cell.setAttribute("aria-label", `Pulse ${pulseIndex + 1}, ${location}`);
} else {
cell.textContent = "·";
cell.setAttribute("aria-label", `Empty tile, ${location}`);
}
cell.addEventListener("click", onCell);
board.append(cell);
}
progress.textContent = collected.size === definition.parameters.target
? `Garden complete — ${collected.size} of ${definition.parameters.target} pulses collected.`
: `${collected.size} of ${definition.parameters.target} pulses collected.`;
if (restoreSparkFocus) board.querySelector(".is-spark")?.focus();
}
function collect() {
const pulses = pulseCells();
if (!pulses.includes(position) || collected.has(position)) return;
if (["sequence", "reverse"].includes(definition.parameters.mode) && position !== wanted()) return;
collected.add(position);
if (definition.parameters.mode === "rotate") rotation = (rotation + 1) % 4;
if (definition.parameters.mode === "rotate-double") rotation = (rotation + 2) % 4;
if (definition.parameters.mode === "rotate-counter") rotation = (rotation + 3) % 4;
if (definition.parameters.mode === "mirror-toggle") mirrorHorizontal = !mirrorHorizontal;
if (definition.parameters.mode === "wrap-anchor") blocked.add(position);
}
function move(dx, dy) {
if (destroyed || collected.size === definition.parameters.target) return;
let actualDx = dx;
let actualDy = dy;
if (mirrorHorizontal || definition.parameters.mode === "mirror-all") actualDx *= -1;
if (definition.parameters.mode === "mirror-all") actualDy *= -1;
for (let i = 0; i < rotation; i += 1) [actualDx, actualDy] = [-actualDy, actualDx];
const x = position % SIZE;
const y = Math.floor(position / SIZE);
let nx = x + actualDx;
let ny = y + actualDy;
const wraps = ["wrap", "trail-gates", "wrap-current", "wrap-anchor"];
const crossed = nx < 0 || nx >= SIZE || ny < 0 || ny >= SIZE;
if (wraps.includes(definition.parameters.mode)) {
nx = (nx + SIZE) % SIZE;
ny = (ny + SIZE) % SIZE;
if (crossed && definition.parameters.mode === "wrap-current") {
nx = (nx + actualDx + SIZE) % SIZE;
ny = (ny + actualDy + SIZE) % SIZE;
}
} else if (crossed) return;
const nextPosition = ny * SIZE + nx;
if (blocked.has(nextPosition)) return;
const previous = position;
position = nextPosition;
moves += 1;
if (["trail", "trail-gates"].includes(definition.parameters.mode)) {
blocked.add(previous);
trail.push(previous);
if (crossed && definition.parameters.mode === "trail-gates" && trail.length) blocked.delete(trail.shift());
}
if (definition.parameters.mode === "rhythm" && moves % 2 === 0) blocked.add(previous);
if (definition.parameters.mode === "rhythm-every") blocked.add(previous);
if (definition.parameters.mode === "rhythm-third" && moves % 3 === 0) blocked.add(previous);
collect();
draw();
}
function onCell(event) {
const destination = Number(event.currentTarget.dataset.index);
const dx = (destination % SIZE) - (position % SIZE);
const dy = Math.floor(destination / SIZE) - Math.floor(position / SIZE);
if (Math.abs(dx) + Math.abs(dy) === 1) move(dx, dy);
}
function onKey(event) {
const keys = {
ArrowUp: [0, -1], w: [0, -1], W: [0, -1],
ArrowDown: [0, 1], s: [0, 1], S: [0, 1],
ArrowLeft: [-1, 0], a: [-1, 0], A: [-1, 0],
ArrowRight: [1, 0], d: [1, 0], D: [1, 0]
};
if (!keys[event.key]) return;
event.preventDefault();
move(...keys[event.key]);
}
function resetRuntime() {
if (destroyed) return;
position = 12; collected = new Set(); blocked = new Set(); trail = []; moves = 0; rotation = 0;
mirrorHorizontal = definition.parameters.mode === "mirror";
collect();
draw();
}
board.addEventListener("keydown", onKey);
reset.addEventListener("click", resetRuntime);
container.replaceChildren(heading, summary, rule, instructions, board, progress, reset);
resetRuntime();
return {
destroy() {
if (destroyed) return;
destroyed = true;
board.removeEventListener("keydown", onKey);
reset.removeEventListener("click", resetRuntime);
container.replaceChildren();
}
};
}