Circuit Orchard
by NovaCaptain32439 lines16.0 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#102421">
<title>Circuit Orchard</title>
<style>:root {
color-scheme: dark;
--back: #102421;
--edge: #28483b;
--accent: #b8fa82;
font-family: system-ui, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100dvh;
display: grid;
place-items: center;
padding: clamp(8px, 2vw, 20px);
background: radial-gradient(ellipse at 50% 15%, var(--edge), var(--back) 70%);
color: #f4f6fa;
}
.cabinet { width: min(100%, 1040px, calc((100dvh - 114px) * 1.7778)); }
.masthead, .console, .brand, .actions { display: flex; align-items: center; }
.masthead, .console { justify-content: space-between; gap: 12px; padding: 8px 2px; }
.brand, .actions { gap: 9px; }
.brand, .eyebrow, .details, .console {
font-family: ui-monospace, Consolas, monospace;
font-size: 12px;
}
.brand { font-weight: 800; letter-spacing: .08em; }
.mark { width: 10px; height: 10px; background: var(--accent); transform: rotate(45deg); }
.edition { color: #bbc4cf; font-size: 10px; white-space: nowrap; }
button {
color: inherit;
background: var(--back);
border: 1px solid #ffffff42;
border-radius: 6px;
cursor: pointer;
padding: 7px 10px;
font: inherit;
font-size: 12px;
white-space: nowrap;
}
button:focus-visible, canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:disabled { opacity: .45; cursor: default; }
.stage { position: relative; overflow: hidden; border: 1px solid #ffffff42; border-radius: 12px; background: var(--back); }
canvas { display: block; width: 100%; height: auto; touch-action: none; }
.overlay { position: absolute; inset: 0; display: grid; place-items: center; padding: 8px; background: #070b14a0; }
.overlay[hidden] { display: none; }
.card {
width: min(94%, 510px);
padding: clamp(10px, 3vw, 30px);
text-align: center;
background: color-mix(in srgb, var(--back), transparent 6%);
border: 1px solid var(--accent);
border-radius: 12px;
box-shadow: 0 12px 65px #0008;
}
.eyebrow { color: var(--accent); font-weight: 800; letter-spacing: .12em; text-transform: uppercase; margin: 0 0 7px; }
h1 { margin: 0; font-size: clamp(25px, 5.5vw, 54px); line-height: .98; letter-spacing: -.055em; }
.message { max-width: 37ch; margin: 12px auto; line-height: 1.45; font-size: clamp(12px, 2.2vw, 16px); color: #e0e4ed; }
.primary { background: var(--accent); color: #10141a; padding: 9px 22px; font-size: 15px; font-weight: 850; }
.primary:focus-visible { outline-color: white; }
.details { color: #ccd1d9; margin: 8px 0 0; }
.details:empty { display: none; }
.console { align-items: flex-start; color: #d4dae4; line-height: 1.45; }
.console p { margin: 0; }
output { color: var(--accent); text-align: right; min-width: 8ch; }
@media (max-width: 550px) {
.cabinet { width: 100%; }
.edition { display: none; }
.masthead, .console { gap: 6px; }
.message { margin: 6px auto; }
.card { padding: 9px; }
.eyebrow, .details, .console { font-size: 10px; }
.primary { padding: 5px 17px; }
}
</style>
</head>
<body>
<main class="cabinet">
<header class="masthead">
<div class="brand"><span class="mark"></span><span>D3MONGAMES</span><span class="edition">01 / PUZZLE</span></div>
<div class="actions">
<button id="sound" type="button" aria-pressed="false">Sound off</button>
<button id="pause" type="button" disabled>Pause</button>
</div>
</header>
<section class="stage" aria-label="Circuit Orchard game">
<canvas id="game" width="960" height="540" tabindex="0" aria-label="Circuit Orchard playfield"></canvas>
<div id="overlay" class="overlay">
<div class="card">
<p id="eyebrow" class="eyebrow">A quiet pipe puzzle</p>
<h1 id="title">Circuit Orchard</h1>
<p id="message" class="message">Turn the pipe tiles and bring water to six dry plots. Take all the time you need.</p>
<button id="primary" class="primary" type="button">Play</button>
<p id="details" class="details"></p>
</div>
</div>
</section>
<footer class="console">
<p id="help">Click or tap a tile to turn it. Keyboard ยท Arrows, then Space. R resets the plot.</p>
<output id="status" role="status"></output>
</footer>
<noscript>This game needs JavaScript to run.</noscript>
</main>
<script>"use strict";
const Arcade = (() => {
const canvas = document.querySelector("#game");
const ctx = canvas.getContext("2d");
const overlay = document.querySelector("#overlay");
const primary = document.querySelector("#primary");
const pause = document.querySelector("#pause");
const sound = document.querySelector("#sound");
const status = document.querySelector("#status");
const keys = new Set();
let audio, soundOn = false, active = false, screen = "menu";
let game, nextAction, lastTime = 0, announcement = "";
function setStatus(message) {
if (announcement !== message) {
status.value = message;
announcement = message;
}
}
function show(kind, eyebrow, title, message, button, action, detail = "") {
active = false;
screen = kind;
keys.clear();
overlay.hidden = false;
pause.disabled = kind !== "paused";
pause.textContent = kind === "paused" ? "Resume" : "Pause";
document.querySelector("#eyebrow").textContent = eyebrow;
document.querySelector("#title").textContent = title;
document.querySelector("#message").textContent = message;
document.querySelector("#details").textContent = detail;
primary.textContent = button;
nextAction = action;
primary.focus({ preventScroll: true });
}
function play() {
active = true;
screen = "playing";
overlay.hidden = true;
pause.disabled = false;
pause.textContent = "Pause";
canvas.focus({ preventScroll: true });
}
function togglePause() {
if (screen === "playing") {
game.release?.();
show("paused", "Intermission", "Paused", "Your run is ready when you are.", "Resume", play);
} else if (screen === "paused") {
play();
}
}
function tone(frequency, duration = .11, type = "sine") {
if (!soundOn || !audio || audio.state !== "running") return;
const oscillator = audio.createOscillator();
const gain = audio.createGain();
oscillator.type = type;
oscillator.frequency.value = frequency;
oscillator.connect(gain);
gain.connect(audio.destination);
gain.gain.setValueAtTime(.07, audio.currentTime);
gain.gain.exponentialRampToValueAtTime(.001, audio.currentTime + duration);
oscillator.start();
oscillator.stop(audio.currentTime + duration);
}
function best(key, score = 0) {
try {
const prior = Number(localStorage.getItem("d3mg." + key)) || 0;
if (score > prior) localStorage.setItem("d3mg." + key, String(score));
return Math.max(score, prior);
} catch {
return score;
}
}
function pointer(event) {
const box = canvas.getBoundingClientRect();
return {
x: (event.clientX - box.left) * canvas.width / box.width,
y: (event.clientY - box.top) * canvas.height / box.height
};
}
function start(controller) {
game = controller;
nextAction = () => { game.reset(); play(); };
primary.addEventListener("click", () => nextAction());
pause.addEventListener("click", togglePause);
sound.addEventListener("click", () => {
soundOn = !soundOn;
if (soundOn && window.AudioContext) {
audio ??= new AudioContext();
audio.resume().then(() => tone(480)).catch(() => {});
}
sound.textContent = soundOn ? "Sound on" : "Sound off";
sound.setAttribute("aria-pressed", String(soundOn));
});
document.addEventListener("keydown", event => {
const key = event.key.toLowerCase();
if (key === "escape" || key === "p") {
if (!event.repeat) togglePause();
return;
}
if (!active || (event.target instanceof HTMLButtonElement && key === " ")) return;
if ([" ", "arrowup", "arrowdown", "arrowleft", "arrowright"].includes(key)) event.preventDefault();
keys.add(key);
if (!event.repeat) game.key?.(key);
});
document.addEventListener("keyup", event => keys.delete(event.key.toLowerCase()));
canvas.addEventListener("pointerdown", event => {
if (!active) return;
canvas.setPointerCapture(event.pointerId);
game.pointer?.(pointer(event));
});
canvas.addEventListener("pointermove", event => {
if (active && canvas.hasPointerCapture(event.pointerId)) game.drag?.(pointer(event));
});
for (const type of ["pointerup", "pointercancel", "lostpointercapture"]) {
canvas.addEventListener(type, () => game.release?.());
}
window.addEventListener("blur", () => {
keys.clear();
game.release?.();
});
document.addEventListener("visibilitychange", () => {
if (document.hidden && active) togglePause();
});
function frame(time) {
const dt = Math.min(.04, (time - (lastTime || time)) / 1000);
lastTime = time;
if (active) game.update?.(dt);
game.draw(ctx, time / 1000);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
function text(context, value, x, y, color = "#f4f6fa", size = 18, align = "left") {
context.font = `700 ${size}px ui-monospace, Consolas, monospace`;
context.textAlign = align;
context.textBaseline = "middle";
context.fillStyle = color;
context.fillText(value, x, y);
}
function box(context, x, y, width, height, radius, color) {
context.beginPath();
context.roundRect(x, y, width, height, radius);
context.fillStyle = color;
context.fill();
}
return { start, show, play, setStatus, tone, best, text, box, keys };
})();
(() => {
const { start, show, play, setStatus, tone, best, text, box } = Arcade;
const levels = [
{ name: "First sprout", route: [10, 11, 12, 7, 8, 13, 14] },
{ name: "Crosswinds", route: [0, 1, 6, 11, 12, 17, 18, 23, 24] },
{ name: "Rain ladder", route: [20, 15, 16, 17, 12, 7, 8, 3, 4, 9, 14] },
{ name: "Root spiral", route: [0, 1, 2, 3, 4, 9, 14, 19, 24, 23, 22, 21, 16, 11, 12, 13] },
{ name: "Greenhouse bend", route: [5, 6, 1, 2, 3, 8, 13, 12, 17, 22, 23, 24, 19, 14] },
{ name: "Canopy maze", route: [0, 5, 10, 15, 20, 21, 22, 23, 24, 19, 14, 9, 4, 3, 2, 1, 6, 11, 12, 13, 18, 17, 16] }
];
const greens = { mint: "#b8fa82", muted: "#98ad9f", tile: "#1b322d", line: "#5f8b79" };
const steps = [[0, -1], [1, 0], [0, 1], [-1, 0]];
const originX = 348, originY = 57, tileSize = 81;
let level = 0, moves = 0, cursor = 0, cells = [], flow = new Set();
function neighbor(index, direction) {
const [dx, dy] = steps[direction];
const x = index % 5 + dx;
const y = Math.floor(index / 5) + dy;
return x >= 0 && x < 5 && y >= 0 && y < 5 ? y * 5 + x : -1;
}
function direction(from, to) {
return steps.findIndex((_, i) => neighbor(from, i) === to);
}
function trace() {
const { route } = levels[level];
const source = route[0];
const target = route.at(-1);
flow = new Set([source]);
const queue = [source];
while (queue.length > 0) {
const current = queue.shift();
for (const port of cells[current]) {
const next = neighbor(current, port);
if (next < 0 || flow.has(next) || !cells[next].includes((port + 2) % 4)) continue;
flow.add(next);
queue.push(next);
}
}
return flow.has(target);
}
function load(index) {
level = index;
moves = 0;
const { route } = levels[level];
cells = Array.from({ length: 25 }, (_, i) => {
const rotation = (i * 3 + level) % 4;
return [rotation, (rotation + (i % 3 === 0 ? 2 : 1)) % 4];
});
route.forEach((tile, i) => {
const ports = [];
if (i > 0) ports.push(direction(tile, route[i - 1]));
if (i + 1 < route.length) ports.push(direction(tile, route[i + 1]));
const twist = ports.length === 2 ? 1 + ((tile + i * 2 + level) % 3) : 0;
cells[tile] = ports.map(port => (port + twist) % 4);
});
cursor = route[1];
trace();
setStatus(`Plot ${level + 1}/${levels.length}`);
}
function rotate(tile) {
if (cells[tile].length === 1) return;
cells[tile] = cells[tile].map(port => (port + 1) % 4);
moves++;
tone(240 + (moves % 5) * 40, .045, "triangle");
if (!trace()) return;
const cleared = best("circuit-plots", level + 1);
tone(720, .3, "triangle");
const last = level === levels.length - 1;
setStatus(last ? "Orchard restored" : `Plot ${level + 1} restored`);
show(
"result", last ? "Six plots connected" : "Water is flowing",
last ? "Orchard restored" : levels[level].name,
`You watered the sapling in ${moves} turns.`,
last ? "Play again" : "Next plot",
() => { load(last ? 0 : level + 1); play(); },
`Best ยท ${cleared}/${levels.length} plots restored`
);
}
function key(value) {
const arrow = ["arrowup", "arrowright", "arrowdown", "arrowleft"].indexOf(value);
if (arrow >= 0) {
const next = neighbor(cursor, arrow);
if (next >= 0) cursor = next;
}
if (value === " " || value === "enter") rotate(cursor);
if (value === "r") load(level);
}
function pointer({ x, y }) {
const col = Math.floor((x - originX) / tileSize);
const row = Math.floor((y - originY) / tileSize);
if (col < 0 || col >= 5 || row < 0 || row >= 5) return;
cursor = row * 5 + col;
rotate(cursor);
}
function draw(ctx, time) {
ctx.fillStyle = "#102421";
ctx.fillRect(0, 0, 960, 540);
ctx.fillStyle = "#142d26";
for (let y = 24; y < 540; y += 22) {
for (let x = 21; x < 960; x += 22) ctx.fillRect(x, y, 2, 2);
}
box(ctx, 26, 59, 285, 405, 13, "#192f28");
text(ctx, "IRRIGATION / FIELD MAP", 49, 91, greens.mint, 16);
text(ctx, `PLOT ${String(level + 1).padStart(2, "0")} / 06`, 49, 135, greens.muted, 15);
text(ctx, levels[level].name, 49, 167, "#f1f9de", 24);
text(ctx, String(moves).padStart(2, "0"), 49, 236, greens.mint, 56);
text(ctx, "TILE TURNS", 49, 282, greens.muted, 15);
ctx.fillStyle = "#3a5d4d";
ctx.fillRect(49, 314, 239, 1);
text(ctx, "CONNECT SOURCE TO SPROUT", 49, 343, "#e0e9d9", 14);
text(ctx, "Light pipes carry water.", 49, 369, greens.muted, 14);
text(ctx, "R resets this plot.", 49, 423, greens.muted, 13);
text(ctx, "CANOPY NETWORK", 352, 29, greens.mint, 16);
const { route } = levels[level];
cells.forEach((ports, index) => {
const x = originX + index % 5 * tileSize;
const y = originY + Math.floor(index / 5) * tileSize;
const cx = x + tileSize / 2, cy = y + tileSize / 2;
box(ctx, x + 2, y + 2, tileSize - 4, tileSize - 4, 8, flow.has(index) ? "#274333" : greens.tile);
if (cursor === index) {
ctx.strokeStyle = "#f1f9de";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x + 3, y + 3, tileSize - 6, tileSize - 6, 8);
ctx.stroke();
}
ctx.lineCap = "round";
for (const port of ports) {
const [dx, dy] = steps[port];
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + dx * 34, cy + dy * 34);
ctx.lineWidth = 13;
ctx.strokeStyle = "#102421";
ctx.stroke();
ctx.lineWidth = 6;
ctx.strokeStyle = flow.has(index) ? greens.mint : greens.line;
ctx.stroke();
}
if (ports.length === 1) {
const source = index === route[0];
box(ctx, cx - 12, cy - 12, 24, 24, 6, source || flow.has(index) ? greens.mint : "#e5c17d");
text(ctx, source ? "S" : "G", cx, cy + 1, "#102421", 15, "center");
}
});
const sourcePulse = 7 + (Math.sin(time * 3) + 1) * 2;
ctx.beginPath();
ctx.arc(803, 98, sourcePulse, 0, Math.PI * 2);
ctx.fillStyle = greens.mint;
ctx.fill();
text(ctx, "S SOURCE", 822, 98, greens.muted, 14);
box(ctx, 794, 123, 18, 18, 4, "#e5c17d");
text(ctx, "G SPROUT", 822, 133, greens.muted, 14);
text(ctx, "01 / ORCHARD SYSTEMS", 29, 503, greens.muted, 13);
}
load(0);
start({ reset: () => load(0), draw, key, pointer });
})();
</script>
</body>
</html>
Game Source: Circuit Orchard
Creator: NovaCaptain32
Libraries: none
Complexity: complex (439 lines, 16.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: circuit-orchard-novacaptain32" to link back to the original. Then publish at arcadelab.ai/publish.