Snake
by sloprun seed
2026-08-14
0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
It is already playing when you arrive — a small solver drives the snake, and it only chases the apple when it can still trace a path back to its own tail, so it survives long enough to be worth watching. Press an arrow key, WASD, or tap the pad and the board is yours: every apple adds a cell and a little speed, dragging the board from under 7 cells a second up to 16, and apples pay out more the longer you get. Turns are buffered two deep, which is the whole difference between a game that feels crisp and one that feels like it is ignoring you — a fast up → right around a corner always lands. Try it first on solid edges, then switch to wrap around: same board, completely different game.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<link rel="icon" href="data:,">
<title>Snake — sloprun.dev</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end tokens ───────────────────────────────────────────────────────────── */
/* demo-local additions (scrims need alpha; tokens are opaque hex) */
:root { --scrim: rgba(246,246,243,.66); --shadow: 0 1px 2px rgba(26,29,33,.05); --lift: 0 8px 28px rgba(26,29,33,.14); }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --scrim: rgba(20,22,26,.66); --shadow: 0 1px 2px rgba(0,0,0,.35); --lift: 0 8px 28px rgba(0,0,0,.5); }
}
:root[data-theme="dark"] { --scrim: rgba(20,22,26,.66); --shadow: 0 1px 2px rgba(0,0,0,.35); --lift: 0 8px 28px rgba(0,0,0,.5); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100%; overflow-x: hidden; }
.wrap {
max-width: 1000px; margin: 0 auto;
padding: 22px 16px 26px;
display: flex; flex-direction: column; gap: 16px;
}
@media (max-width: 420px) { .wrap { padding: 16px 12px 20px; gap: 13px; } }
h1 { margin: 0 0 6px; font-size: clamp(21px, 3.6vw, 29px); letter-spacing: -.021em; line-height: 1.15; }
.lede { margin: 0; max-width: 66ch; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
.lede b { color: var(--ink); font-weight: 600; }
.lede kbd {
font-family: var(--font-mono); font-size: .84em; color: var(--accent);
background: var(--accent-soft); border-radius: 4px; padding: 1px 5px; border: 0;
}
.stage { display: grid; grid-template-columns: minmax(0,1fr) 268px; gap: 16px; align-items: start; }
.boardwrap { grid-column: 1; grid-row: 1; }
.panel { grid-column: 2; grid-row: 1 / span 2; }
.keyscard { grid-column: 1; grid-row: 2; }
@media (max-width: 880px) {
.stage { grid-template-columns: minmax(0,1fr); }
.boardwrap, .panel, .keyscard { grid-column: auto; grid-row: auto; }
.steercard { order: -1; } /* thumbs first: the pad sits right under the board */
}
.boardwrap { display: flex; flex-direction: column; align-items: center; gap: 9px; min-width: 0; }
.board {
position: relative; width: 100%; max-width: 620px; aspect-ratio: 1 / 1;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); overflow: hidden; box-shadow: var(--shadow);
}
canvas { display: block; width: 100%; height: 100%; touch-action: none; }
canvas:focus { outline: none; }
canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
.hint {
margin: 0; min-height: 15px; text-align: center; max-width: 100%;
font-family: var(--font-mono); font-size: 11px; letter-spacing: .01em;
color: var(--muted); transition: opacity .35s ease;
}
.hint.off { opacity: 0; }
@media (max-width: 420px) { .hint { font-size: 10px; line-height: 1.35; } }
.pops { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.pop {
position: absolute; transform: translate(-50%, -50%);
font-family: var(--font-mono); font-size: 13px; font-weight: 600;
color: var(--run); white-space: nowrap;
text-shadow: 0 0 3px var(--surface), 0 0 3px var(--surface);
}
@media (prefers-reduced-motion: no-preference) {
.pop { animation: rise .85s cubic-bezier(.2,.7,.3,1) forwards; }
}
@keyframes rise {
0% { opacity: 0; transform: translate(-50%, -30%) scale(.85); }
22% { opacity: 1; transform: translate(-50%, -60%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -170%) scale(1); }
}
.overlay {
position: absolute; inset: 0; display: grid; place-items: center;
background: var(--scrim); padding: 16px; text-align: center;
}
.overlay[hidden] { display: none; }
.overlay .box {
display: flex; flex-direction: column; align-items: center; gap: 10px;
max-width: 34ch; padding: 18px 20px 16px;
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); box-shadow: var(--lift);
}
@media (max-width: 420px) { .overlay .box { padding: 14px 14px 12px; gap: 8px; } }
.overlay h2 { margin: 0; font-size: clamp(19px, 4.4vw, 25px); letter-spacing: -.02em; line-height: 1.1; }
.overlay p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); }
.overlay .final {
font-family: var(--font-mono); font-size: 12.5px; color: var(--ink);
font-variant-numeric: tabular-nums; letter-spacing: .01em;
}
.overlay .keyhint { font-family: var(--font-mono); font-size: 11px; letter-spacing: .03em; }
.tag {
font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
border: 1px solid var(--line); border-radius: 999px; padding: 3px 9px; color: var(--muted); background: var(--bg);
}
.tag.ok { color: var(--run); background: var(--run-soft); border-color: transparent; }
.tag.bad { color: var(--danger); background: var(--surface); border-color: var(--danger); }
.panel { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 12px 13px; box-shadow: var(--shadow);
}
.card h2 {
margin: 0 0 10px; font-family: var(--font-mono); font-size: 10.5px;
letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 600;
}
.stats { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 11px 8px; }
.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stat .v {
font-family: var(--font-mono); font-size: 19px; line-height: 1.05;
font-variant-numeric: tabular-nums; letter-spacing: -.02em; white-space: nowrap;
}
.stat .v.sm { font-size: 15px; }
.stat .k {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted);
}
.stat .v.good { color: var(--run); }
.stat .v .u { font-size: .62em; color: var(--muted); letter-spacing: 0; }
.barrow { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-top: 12px; }
.barrow .lbl { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }
.barrow .num { font-family: var(--font-mono); font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.bar { height: 6px; border-radius: 3px; background: var(--line); margin-top: 7px; overflow: hidden; }
.bar > i { display: block; height: 100%; width: 0%; background: var(--accent); border-radius: 3px; transition: width .2s ease; }
.pill {
display: inline-flex; align-items: center; gap: 6px; margin-top: 12px;
font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;
padding: 3px 9px 3px 7px; border-radius: 999px;
border: 1px solid var(--line); color: var(--muted); background: var(--bg);
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex: none; }
.pill.ok { color: var(--run); background: var(--run-soft); border-color: transparent; }
.pill.ok .dot { background: var(--run); }
.pill.live { color: var(--accent); background: var(--accent-soft); border-color: transparent; }
.pill.live .dot { background: var(--accent); }
.pill.bad { color: var(--danger); border-color: var(--danger); background: var(--surface); }
.pill.bad .dot { background: var(--danger); }
button {
font: inherit; font-size: 13px; font-family: var(--font-sans);
color: var(--ink); background: var(--surface);
border: 1px solid var(--line); border-radius: 8px;
padding: 7px 11px; cursor: pointer; line-height: 1.2;
transition: border-color .12s ease, color .12s ease, background .12s ease;
-webkit-tap-highlight-color: transparent;
}
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:disabled { opacity: .42; cursor: not-allowed; }
button.primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
button.primary:hover:not(:disabled) { color: var(--surface); filter: brightness(1.07); }
button.big { font-size: 14px; padding: 9px 18px; }
.row { display: flex; flex-wrap: wrap; gap: 7px; }
.row > button { flex: 1 1 auto; }
.seg { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 7px; }
.seg.two { grid-template-columns: repeat(2, minmax(0,1fr)); }
.seg button { padding: 7px 4px; font-size: 12.5px; }
.seg button[aria-pressed="true"] {
background: var(--accent-soft); color: var(--accent); border-color: var(--accent); font-weight: 600;
}
@media (max-width: 880px) {
button { padding: 11px 12px; } /* roomier thumb targets */
.seg button { padding: 12px 4px; }
button.big { padding: 12px 20px; }
}
.sublbl {
display: block; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted); margin: 0 0 6px;
}
.sublbl.gap { margin-top: 12px; }
.cap { margin: 9px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
.cap b { color: var(--ink); font-weight: 600; }
.cap code { font-family: var(--font-mono); font-size: .92em; color: var(--ink); }
/* d-pad */
.dpad {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
width: 100%; max-width: 186px; margin: 0 auto;
}
@media (max-width: 880px) { .dpad { max-width: 234px; gap: 8px; } }
.dpad button {
aspect-ratio: 1 / 1; display: grid; place-items: center; padding: 0;
font-size: 16px; line-height: 1; color: var(--muted);
}
.dpad button:hover:not(:disabled) { color: var(--accent); }
.dpad button.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.dpad .gap { visibility: hidden; pointer-events: none; }
.dpad .mid { color: var(--ink); }
.dpad .mid.hot { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
/* the middle key is a transport control, not a direction — the ring keeps its
play triangle from reading as another ► steer arrow right next to it */
.ico {
display: block; width: 21px; height: 21px; position: relative;
border: 1.6px solid currentColor; border-radius: 50%;
}
.ico.pause::before, .ico.pause::after {
content: ""; position: absolute; top: 6px; width: 2.5px; height: 9px;
background: currentColor; border-radius: 1px;
}
.ico.pause::before { left: 6px; } .ico.pause::after { right: 6px; }
.ico.play::before {
content: ""; position: absolute; top: 50%; left: 57%; transform: translate(-50%, -50%);
border-left: 7px solid currentColor; border-top: 4.5px solid transparent; border-bottom: 4.5px solid transparent;
}
.keys { display: flex; flex-direction: column; gap: 6px; margin: 0; }
.keys.keyrow { flex-direction: row; flex-wrap: wrap; gap: 8px 20px; }
.keys div { display: flex; align-items: baseline; gap: 9px; font-size: 12.5px; color: var(--muted); }
.keys kbd {
font-family: var(--font-mono); font-size: 10.5px; color: var(--ink);
background: var(--bg); border: 1px solid var(--line); border-bottom-width: 2px;
border-radius: 5px; padding: 2px 6px; min-width: 5.4em; text-align: center; flex: none;
}
footer {
font-family: var(--font-mono); font-size: 11px; color: var(--muted);
letter-spacing: .05em; padding-top: 2px;
}
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Snake</h1>
<p class="lede">
One snake, one apple, and a board that speeds up as you grow. It is
<b>already playing itself</b> — press <kbd>←</kbd> <kbd>→</kbd>, <kbd>WASD</kbd>,
or tap the pad to take the board.
</p>
</header>
<main class="stage">
<div class="boardwrap">
<div class="board" id="board">
<canvas id="c" tabindex="0" aria-label="Snake board. Arrow keys or W A S D steer; space pauses.">
A canvas game: a snake chases an apple around a grid. Your browser does not support canvas.
</canvas>
<div class="pops" id="pops" aria-hidden="true"></div>
<div class="overlay" id="overlay" hidden>
<div class="box">
<span class="tag" id="ovTag">paused</span>
<h2 id="ovTitle">Paused</h2>
<p id="ovNote">Nothing is moving.</p>
<p class="final" id="ovFinal" hidden></p>
<button class="primary big" id="ovBtn" type="button">Resume</button>
<p class="keyhint" id="ovKey">space to resume</p>
</div>
</div>
</div>
<p class="hint" id="hint"><span id="hintText">watching the demo play · press an arrow to take over</span></p>
</div>
<div class="panel">
<section class="card" aria-label="Run readout">
<h2>Readout</h2>
<div class="stats">
<div class="stat"><span class="v" id="sScore">0</span><span class="k">score</span></div>
<div class="stat"><span class="v" id="sBest">—</span><span class="k">your best</span></div>
<div class="stat"><span class="v" id="sLen">3</span><span class="k">length</span></div>
<div class="stat"><span class="v" id="sApples">0</span><span class="k">apples</span></div>
<div class="stat"><span class="v sm" id="sSpeed">6.7<span class="u"> c/s</span></span><span class="k">speed</span></div>
<div class="stat"><span class="v sm" id="sNext">+10</span><span class="k">next apple</span></div>
</div>
<div class="barrow"><span class="lbl">pace</span><span class="num" id="sPace">calm</span></div>
<div class="bar"><i id="barFill"></i></div>
<span class="pill live" id="pill"><span class="dot"></span><span id="pillText">demo playing</span></span>
</section>
<section class="card steercard" aria-label="Steering pad">
<h2>Steer</h2>
<div class="dpad" id="dpad">
<button type="button" class="gap" tabindex="-1" aria-hidden="true"></button>
<button type="button" data-d="up" aria-label="Steer up">▲</button>
<button type="button" class="gap" tabindex="-1" aria-hidden="true"></button>
<button type="button" data-d="left" aria-label="Steer left">◄</button>
<button type="button" class="mid" id="midBtn" aria-label="Pause"><span class="ico pause" id="midIco"></span></button>
<button type="button" data-d="right" aria-label="Steer right">►</button>
<button type="button" class="gap" tabindex="-1" aria-hidden="true"></button>
<button type="button" data-d="down" aria-label="Steer down">▼</button>
<button type="button" class="gap" tabindex="-1" aria-hidden="true"></button>
</div>
<p class="cap">Turns are buffered two deep, so a fast <code>up → right</code> around a corner always lands. You can also swipe on the board.</p>
</section>
<section class="card" aria-label="Game controls">
<h2>Game</h2>
<div class="row">
<button type="button" class="primary" id="mainBtn">Take over</button>
<button type="button" id="watchBtn">Watch demo</button>
</div>
</section>
<section class="card" aria-label="Board rules">
<h2>Rules</h2>
<span class="sublbl" id="lblEdges">Edges</span>
<div class="seg two" id="edges" role="group" aria-labelledby="lblEdges">
<button type="button" data-e="0" aria-pressed="true">Solid</button>
<button type="button" data-e="1" aria-pressed="false">Wrap around</button>
</div>
<span class="sublbl gap" id="lblSize">Cells per side</span>
<div class="seg" id="sizes" role="group" aria-labelledby="lblSize">
<button type="button" data-n="15" aria-pressed="false">15</button>
<button type="button" data-n="21" aria-pressed="true">21</button>
<button type="button" data-n="27" aria-pressed="false">27</button>
</div>
<p class="cap" id="edgeCap">Hit a wall and the run ends. Same board, much less room to think. Changing either setting starts a fresh run.</p>
</section>
</div>
<section class="card keyscard" aria-label="Keys">
<h2>Keys</h2>
<div class="keys keyrow">
<div><kbd>← ↑ → ↓</kbd><span>steer</span></div>
<div><kbd>W A S D</kbd><span>steer</span></div>
<div><kbd>space</kbd><span>pause · resume · play again</span></div>
<div><kbd>R</kbd><span>new run</span></div>
</div>
</section>
</main>
<footer>demo · sloprun.dev</footer>
</div>
<div class="sr-only" role="status" aria-live="polite" id="live"></div>
<script>
(function () {
"use strict";
/* ── theme plumbing ─────────────────────────────────────────────────────── */
var root = document.documentElement;
var mqDark = window.matchMedia ? matchMedia("(prefers-color-scheme: dark)") : null;
var mqRM = window.matchMedia ? matchMedia("(prefers-reduced-motion: reduce)") : null;
var reduced = !!(mqRM && mqRM.matches);
window.addEventListener("message", function (e) {
var d = e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
root.setAttribute("data-theme", d.theme);
readTheme();
}
});
if (mqDark) {
if (mqDark.addEventListener) mqDark.addEventListener("change", readTheme);
else if (mqDark.addListener) mqDark.addListener(readTheme);
}
/* also follow data-theme when a host sets the attribute directly (no message) */
if (window.MutationObserver) {
new MutationObserver(function () { readTheme(); })
.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
}
if (mqRM) {
var onRM = function () { reduced = mqRM.matches; };
if (mqRM.addEventListener) mqRM.addEventListener("change", onRM);
else if (mqRM.addListener) mqRM.addListener(onRM);
}
/* ── colours read from the tokens ───────────────────────────────────────── */
var COL = {
bg: [246,246,243], surface: [255,255,255], ink: [26,29,33], muted: [90,96,104],
line: [227,226,220], accent: [192,86,0], run: [26,127,55], danger: [201,60,55]
};
var TAIL = [150,150,150];
function hexToRgb(h) {
h = String(h).trim();
if (h.charAt(0) === "#") h = h.slice(1);
if (h.length === 3) h = h.charAt(0)+h.charAt(0)+h.charAt(1)+h.charAt(1)+h.charAt(2)+h.charAt(2);
if (h.length !== 6) return null;
var n = parseInt(h, 16);
if (isNaN(n)) return null;
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function tok(name, fb) { return hexToRgb(getComputedStyle(root).getPropertyValue(name)) || fb; }
function rgb(c) { return "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")"; }
function rgba(c, a) { return "rgba(" + c[0] + "," + c[1] + "," + c[2] + "," + a + ")"; }
function mix(a, b, t) {
return [Math.round(a[0]+(b[0]-a[0])*t), Math.round(a[1]+(b[1]-a[1])*t), Math.round(a[2]+(b[2]-a[2])*t)];
}
function readTheme() {
COL.bg = tok("--bg", COL.bg);
COL.surface = tok("--surface", COL.surface);
COL.ink = tok("--ink", COL.ink);
COL.muted = tok("--muted", COL.muted);
COL.line = tok("--line", COL.line);
COL.accent = tok("--accent", COL.accent);
COL.run = tok("--run", COL.run);
COL.danger = tok("--danger", COL.danger);
TAIL = mix(COL.surface, COL.ink, 0.52);
}
/* ── dom ────────────────────────────────────────────────────────────────── */
var boardEl = document.getElementById("board");
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var popsEl = document.getElementById("pops");
var hintEl = document.getElementById("hint");
var hintTx = document.getElementById("hintText");
var overlay = document.getElementById("overlay");
var ovTag = document.getElementById("ovTag");
var ovTitle = document.getElementById("ovTitle");
var ovNote = document.getElementById("ovNote");
var ovFinal = document.getElementById("ovFinal");
var ovBtn = document.getElementById("ovBtn");
var ovKey = document.getElementById("ovKey");
var sScore = document.getElementById("sScore");
var sBest = document.getElementById("sBest");
var sLen = document.getElementById("sLen");
var sApples = document.getElementById("sApples");
var sSpeed = document.getElementById("sSpeed");
var sNext = document.getElementById("sNext");
var sPace = document.getElementById("sPace");
var barFill = document.getElementById("barFill");
var pill = document.getElementById("pill");
var pillTx = document.getElementById("pillText");
var dpad = document.getElementById("dpad");
var midBtn = document.getElementById("midBtn");
var midIco = document.getElementById("midIco");
var mainBtn = document.getElementById("mainBtn");
var watchBtn= document.getElementById("watchBtn");
var edgesEl = document.getElementById("edges");
var edgeCap = document.getElementById("edgeCap");
var sizesEl = document.getElementById("sizes");
var live = document.getElementById("live");
/* ── model ──────────────────────────────────────────────────────────────── */
var UP = {x:0,y:-1}, DOWN = {x:0,y:1}, LEFT = {x:-1,y:0}, RIGHT = {x:1,y:0};
var DIRLIST = [UP, RIGHT, DOWN, LEFT];
var DIRNAME = { up: UP, down: DOWN, left: LEFT, right: RIGHT };
var TICK_SLOW = 150, TICK_FAST = 62, RAMP = 3.4;
var N = 21; // cells per side
var wrap = false; // edges
var mode = "demo"; // "demo" | "you"
var state = "run"; // "run" | "paused" | "over" | "won"
var body = [], occ = null, food = {x:0,y:0}, dir = RIGHT, old = null;
var queue = []; // buffered turns, max 2
var score = 0, apples = 0, best = 0, bestAtStart = 0, newBest = false;
var elapsed = 0, crash = null, deadReason = "";
var rings = [], shake = 0, demoTimer = 0;
var blk = null, vis = null, dist = null, bq = null;
function allocScratch() {
var s = N * N;
blk = new Uint8Array(s); vis = new Uint8Array(s);
dist = new Int16Array(s); bq = new Int32Array(s);
occ = new Uint8Array(s);
}
function head() { return body[0]; }
function tickMs() {
return Math.max(TICK_FAST, TICK_SLOW - (body.length - 3) * RAMP);
}
function valueFor(len) { return 10 + 5 * Math.floor(Math.max(0, len - 3) / 5); }
function paceWord(ms) {
var f = (TICK_SLOW - ms) / (TICK_SLOW - TICK_FAST);
if (f < 0.18) return "calm";
if (f < 0.42) return "steady";
if (f < 0.66) return "brisk";
if (f < 0.88) return "quick";
return "flat out";
}
function reset(startDir) {
allocScratch();
dir = startDir || RIGHT;
var c = Math.floor(N / 2);
body = [];
for (var i = 0; i < 3; i++) {
var x = ((c - dir.x * i) % N + N) % N;
var y = ((c - dir.y * i) % N + N) % N;
body.push({ x: x, y: y });
occ[y * N + x] = 1;
}
old = null; queue = []; rings = []; crash = null; deadReason = "";
score = 0; apples = 0; elapsed = 0; shake = 0; newBest = false;
bestAtStart = best;
placeFood();
}
function placeFood() {
var free = [], h = head();
for (var y = 0; y < N; y++) for (var x = 0; x < N; x++) {
if (!occ[y * N + x]) free.push(y * N + x);
}
if (!free.length) { winRun(); return; }
var far = [];
for (var i = 0; i < free.length; i++) {
var fx = free[i] % N, fy = (free[i] - fx) / N;
if (Math.abs(fx - h.x) + Math.abs(fy - h.y) >= 3) far.push(free[i]);
}
var pool = far.length ? far : free;
var pick = pool[(Math.random() * pool.length) | 0];
food.x = pick % N; food.y = (pick - food.x) / N;
}
/* ── the demo driver: greedy toward the apple, but only when it can still
trace a path back to its own tail ─────────────────────────────────── */
function evalMove(d) {
var h = head(), nx = h.x + d.x, ny = h.y + d.y;
if (wrap) { nx = (nx + N) % N; ny = (ny + N) % N; }
else if (nx < 0 || ny < 0 || nx >= N || ny >= N) return null;
var idx = ny * N + nx, L = body.length;
var tail = body[L - 1], tIdx = tail.y * N + tail.x;
var eats = (nx === food.x && ny === food.y);
if (occ[idx] && !(!eats && idx === tIdx)) return null;
blk.set(occ);
if (!eats) blk[tIdx] = 0;
blk[idx] = 1;
var nt = eats ? tail : (L >= 2 ? body[L - 2] : { x: nx, y: ny });
var ntIdx = nt.y * N + nt.x;
blk[ntIdx] = 0;
vis.fill(0);
var qh = 0, qt = 0;
bq[qt++] = idx; vis[idx] = 1; dist[idx] = 0;
var space = 0, fIdx = food.y * N + food.x, fd = eats ? 0 : -1;
while (qh < qt) {
var cur = bq[qh++], cx = cur % N, cy = (cur - cx) / N, cd = dist[cur];
for (var k = 0; k < 4; k++) {
var dd = DIRLIST[k], ax = cx + dd.x, ay = cy + dd.y;
if (wrap) { ax = (ax + N) % N; ay = (ay + N) % N; }
else if (ax < 0 || ay < 0 || ax >= N || ay >= N) continue;
var ai = ay * N + ax;
if (vis[ai] || blk[ai]) continue;
vis[ai] = 1; dist[ai] = cd + 1; bq[qt++] = ai; space++;
if (ai === fIdx && fd < 0) fd = cd + 1;
}
}
return { d: d, safe: vis[ntIdx] === 1, space: space, dist: fd };
}
function demoDir() {
var opts = [], i;
for (i = 0; i < 4; i++) {
var d = DIRLIST[i];
if (d.x === -dir.x && d.y === -dir.y) continue;
var r = evalMove(d);
if (r) opts.push(r);
}
if (!opts.length) return dir;
var safe = [];
for (i = 0; i < opts.length; i++) if (opts[i].safe) safe.push(opts[i]);
var pool = safe.length ? safe : opts;
var chase = [];
for (i = 0; i < pool.length; i++) if (pool[i].dist >= 0) chase.push(pool[i]);
var use = chase.length ? chase : pool;
var bestO = null, bs = -Infinity;
for (i = 0; i < use.length; i++) {
var o = use[i];
var s = chase.length ? (-o.dist * 1000 + o.space) : (o.space * 10);
if (o.d === dir) s += 3;
if (s > bs) { bs = s; bestO = o; }
}
return bestO ? bestO.d : dir;
}
/* ── one step ───────────────────────────────────────────────────────────── */
function tick() {
if (mode === "demo") { dir = demoDir(); }
else if (queue.length) { dir = queue.shift(); }
old = [];
for (var i = 0; i < body.length; i++) old.push({ x: body[i].x, y: body[i].y });
var h = body[0], nx = h.x + dir.x, ny = h.y + dir.y;
if (wrap) { nx = (nx + N) % N; ny = (ny + N) % N; }
else if (nx < 0 || ny < 0 || nx >= N || ny >= N) { die(nx, ny, "wall"); return; }
var idx = ny * N + nx, L = body.length;
var tail = body[L - 1], tIdx = tail.y * N + tail.x;
var eats = (nx === food.x && ny === food.y);
if (occ[idx] && !(!eats && idx === tIdx)) { die(nx, ny, "self"); return; }
if (!eats) { body.pop(); occ[tIdx] = 0; }
body.unshift({ x: nx, y: ny }); occ[idx] = 1;
if (eats) eat(nx, ny);
sync();
}
/* one definition of "this run is your best", used by the overlay, the pill and
the readout so they can never disagree. `newBest` alone means "you passed a
score you had already set"; the first scoring run has nothing to pass. */
function bestRun() {
return newBest || (mode === "you" && score > 0 && bestAtStart === 0 && score >= best);
}
function eat(x, y) {
apples++;
var val = valueFor(body.length - 1);
score += val;
addPop(x, y, "+" + val);
if (!reduced) rings.push({ x: x, y: y, t: 0 });
if (mode === "you" && score > best) {
best = score;
if (bestAtStart > 0 && !newBest) { newBest = true; say("New best: " + score + "."); }
}
placeFood();
}
function die(x, y, reason) {
crash = { x: x, y: y };
deadReason = reason;
old = null;
if (!reduced) shake = 7;
if (mode === "you") {
state = "over";
say("Run over. Score " + score + ", length " + body.length + ".");
showOverlay();
} else {
state = "over";
demoTimer = 1500;
}
sync();
}
function winRun() {
old = null;
state = "won";
if (mode === "you") {
if (score > best) { best = score; if (bestAtStart > 0) newBest = true; }
showOverlay(); say("Board full. Perfect run.");
}
else demoTimer = 2200;
sync();
}
/* ── state moves ────────────────────────────────────────────────────────── */
function startDemo() {
mode = "demo"; state = "run";
reset(RIGHT);
hintTx.textContent = "watching the demo play · press an arrow to take over";
hintEl.classList.remove("off");
hideOverlay(); sync();
}
function takeOver(d) {
mode = "you"; state = "run";
reset(d || RIGHT);
hintTx.textContent = "your run · space pauses · R for a fresh one";
hintEl.classList.remove("off");
hideOverlay(); sync();
setTimeout(function () { if (mode === "you" && state === "run") hintEl.classList.add("off"); }, 3000);
}
function pause() {
if (mode !== "you" || state !== "run") return;
state = "paused"; showOverlay(); say("Paused."); sync();
}
function resume() {
if (state !== "paused") return;
state = "run"; hideOverlay(); say("Resumed."); sync();
}
function again() { takeOver(dir); }
function showOverlay() {
if (state === "paused") {
ovTag.className = "tag"; ovTag.textContent = "paused";
ovTitle.textContent = "Paused";
ovNote.textContent = "The board is holding still.";
ovFinal.hidden = false;
ovFinal.textContent = "score " + score + " · length " + body.length;
ovBtn.textContent = "Resume";
ovKey.textContent = "space to resume";
} else if (state === "won") {
ovTag.className = "tag ok"; ovTag.textContent = "board full";
ovTitle.textContent = "Perfect run";
ovNote.textContent = "Every cell is snake. There is nowhere left to put an apple.";
ovFinal.hidden = false;
ovFinal.textContent = "score " + score + " · length " + body.length + " · " + clock(elapsed);
ovBtn.textContent = "Play again";
ovKey.textContent = "space to play again";
} else {
var isBest = bestRun();
ovTag.className = isBest ? "tag ok" : "tag bad";
ovTag.textContent = isBest ? "new best" : (deadReason === "wall" ? "hit the wall" : "hit itself");
ovTitle.textContent = isBest ? "Best run yet"
: (score >= 100 ? "Good run" : (score === 0 ? "That was quick" : "Run over"));
ovNote.textContent = deadReason === "wall"
? "The head ran off the edge. Try the wrap-around edges for more room."
: "The head ran into the body. Longer snake, tighter corners.";
ovFinal.hidden = false;
ovFinal.textContent = "score " + score + " · length " + body.length +
" · " + apples + " apples · " + clock(elapsed);
ovBtn.textContent = "Play again";
ovKey.textContent = "space or R to play again";
}
overlay.hidden = false;
try { ovBtn.focus({ preventScroll: true }); } catch (e) { ovBtn.focus(); }
}
function hideOverlay() {
var inside = overlay.contains(document.activeElement);
overlay.hidden = true;
if (inside && canvas.focus) { try { canvas.focus({ preventScroll: true }); } catch (e) { canvas.focus(); } }
}
function clock(ms) {
var s = Math.floor(ms / 1000);
var m = Math.floor(s / 60);
var r = s % 60;
return m + ":" + (r < 10 ? "0" : "") + r;
}
function say(t) { live.textContent = t; }
/* ── readout ────────────────────────────────────────────────────────────── */
function sync() {
var ms = tickMs();
var ended = (state === "over" || state === "won");
var wonBest = ended && bestRun(); // celebrate only once the run is done
sScore.textContent = score;
sBest.textContent = best > 0 ? best : "—";
sBest.className = "v" + (best > 0 && (newBest || wonBest) ? " good" : "");
sLen.textContent = body.length;
sApples.textContent = apples;
sSpeed.innerHTML = (1000 / ms).toFixed(1) + '<span class="u"> c/s</span>';
sNext.textContent = "+" + valueFor(body.length);
sPace.textContent = paceWord(ms);
var f = (TICK_SLOW - ms) / (TICK_SLOW - TICK_FAST);
barFill.style.width = Math.round(Math.max(0, Math.min(1, f)) * 100) + "%";
if (mode === "demo") {
pill.className = "pill live";
pillTx.textContent = state === "run" ? "demo playing" : "demo run ended";
} else if (state === "run") {
pill.className = newBest ? "pill ok" : "pill";
pillTx.textContent = newBest ? "new best" : "your run";
} else if (state === "paused") {
pill.className = "pill"; pillTx.textContent = "paused";
} else if (state === "won") {
pill.className = "pill ok"; pillTx.textContent = "board full";
} else {
pill.className = wonBest ? "pill ok" : "pill bad";
pillTx.textContent = wonBest ? "new best" : "run over";
}
if (mode === "demo") {
mainBtn.textContent = "Take over";
midBtn.setAttribute("aria-label", "Take over");
midIco.className = "ico play";
midBtn.classList.add("hot");
} else if (state === "run") {
mainBtn.textContent = "Pause";
midBtn.setAttribute("aria-label", "Pause");
midIco.className = "ico pause";
midBtn.classList.remove("hot");
} else if (state === "paused") {
mainBtn.textContent = "Resume";
midBtn.setAttribute("aria-label", "Resume");
midIco.className = "ico play";
midBtn.classList.add("hot");
} else {
mainBtn.textContent = "Play again";
midBtn.setAttribute("aria-label", "Play again");
midIco.className = "ico play";
midBtn.classList.add("hot");
}
// never let focus fall on the floor when this button disables itself
if (mode === "demo" && document.activeElement === watchBtn) {
try { canvas.focus({ preventScroll: true }); } catch (e) { canvas.focus(); }
}
watchBtn.disabled = (mode === "demo");
var kids = dpad.children;
for (var i = 0; i < kids.length; i++) {
var k = kids[i], nm = k.getAttribute("data-d");
if (!nm) continue;
var d = DIRNAME[nm];
var live2 = (mode === "you" && state === "run" && d.x === dir.x && d.y === dir.y);
if (live2) k.classList.add("on"); else k.classList.remove("on");
}
}
/* ── score pops ─────────────────────────────────────────────────────────── */
function addPop(x, y, text) {
if (!canvas.width) return;
var el = document.createElement("span");
el.className = "pop";
el.textContent = text;
el.style.left = ((offX + (x + 0.5) * cell) / canvas.width * 100) + "%";
el.style.top = ((offY + (y + 0.5) * cell) / canvas.height * 100) + "%";
popsEl.appendChild(el);
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 900);
}
/* ── geometry ───────────────────────────────────────────────────────────── */
var DPR = 1, cell = 16, boardPx = 0, offX = 0, offY = 0;
function resize() {
var rect = boardEl.getBoundingClientRect();
var w = Math.max(120, Math.round(rect.width));
var h = Math.max(120, Math.round(rect.height));
DPR = Math.min(2.5, window.devicePixelRatio || 1);
var dw = Math.round(w * DPR), dh = Math.round(h * DPR);
if (canvas.width !== dw || canvas.height !== dh) { canvas.width = dw; canvas.height = dh; }
cell = Math.max(3, Math.floor(Math.min(dw, dh) / N));
boardPx = cell * N;
offX = Math.floor((dw - boardPx) / 2);
offY = Math.floor((dh - boardPx) / 2);
}
function cc(v) { return v * cell + cell / 2; }
function rr(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
/* ── draw ───────────────────────────────────────────────────────────────── */
var tt = 0, nowMs = 0;
function draw() {
var W = canvas.width, H = canvas.height;
if (!W || !H) return;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = rgb(COL.surface);
ctx.fillRect(0, 0, W, H);
var sx = 0, sy = 0;
if (shake > 0.05 && !reduced) {
sx = (Math.random() * 2 - 1) * shake * DPR;
sy = (Math.random() * 2 - 1) * shake * DPR;
}
ctx.setTransform(1, 0, 0, 1, offX + sx, offY + sy);
ctx.save();
ctx.beginPath(); ctx.rect(-offX - 4, -offY - 4, W + 8, H + 8); ctx.clip();
drawGrid();
drawFood();
drawRings();
drawSnake();
drawCrash();
ctx.restore();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
function drawGrid() {
var lw = Math.max(1, Math.round(DPR * 0.5));
ctx.fillStyle = rgb(COL.line);
for (var i = 1; i < N; i++) {
var p = Math.round(i * cell);
ctx.fillRect(p, 0, lw, boardPx);
ctx.fillRect(0, p, boardPx, lw);
}
if (wrap) {
ctx.save();
ctx.setLineDash([Math.max(3, cell * 0.36), Math.max(3, cell * 0.36)]);
ctx.lineWidth = Math.max(2, DPR * 1.6);
ctx.strokeStyle = rgba(COL.accent, 0.5);
var o = ctx.lineWidth / 2;
ctx.strokeRect(o, o, boardPx - ctx.lineWidth, boardPx - ctx.lineWidth);
ctx.restore();
}
}
function drawFood() {
if (state === "won") return;
var x = cc(food.x), y = cc(food.y);
var pulse = reduced ? 1 : (1 + 0.07 * Math.sin(nowMs / 340));
var r = cell * 0.30 * pulse;
ctx.fillStyle = rgba(COL.accent, 0.15);
ctx.beginPath(); ctx.arc(x, y, r * 1.85, 0, 6.2832); ctx.fill();
ctx.fillStyle = rgb(COL.accent);
ctx.beginPath(); ctx.arc(x, y, r, 0, 6.2832); ctx.fill();
ctx.fillStyle = rgba(COL.surface, 0.5);
ctx.beginPath(); ctx.arc(x - r * 0.32, y - r * 0.34, Math.max(1, r * 0.22), 0, 6.2832); ctx.fill();
}
function drawRings() {
for (var i = 0; i < rings.length; i++) {
var g = rings[i], k = g.t / 420;
if (k > 1) continue;
var x = cc(g.x), y = cc(g.y);
ctx.strokeStyle = rgba(COL.run, 0.5 * (1 - k));
ctx.lineWidth = Math.max(1.5, DPR * 1.6);
ctx.beginPath();
ctx.arc(x, y, cell * (0.34 + k * 0.95), 0, 6.2832);
ctx.stroke();
}
}
function drawSnake() {
var L = body.length, pts = [], i;
for (i = 0; i < L; i++) {
var c = body[i], p = (old && old[i]) ? old[i] : c;
var dx = c.x - p.x, dy = c.y - p.y, px, py;
if (Math.abs(dx) > 1 || Math.abs(dy) > 1) { px = c.x; py = c.y; }
else { px = p.x + dx * tt; py = p.y + dy * tt; }
pts.push([cc(px), cc(py)]);
}
var w = cell * 0.74;
var gap = cell * 1.6;
ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = w;
for (i = L - 1; i > 0; i--) {
var f = (L > 1) ? i / (L - 1) : 0;
ctx.strokeStyle = rgb(mix(COL.ink, TAIL, f));
var a = pts[i], b = pts[i - 1];
ctx.beginPath();
if (Math.abs(a[0] - b[0]) > gap || Math.abs(a[1] - b[1]) > gap) {
ctx.moveTo(a[0], a[1]); ctx.lineTo(a[0], a[1]);
} else {
ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]);
}
ctx.stroke();
}
// head
var hx = pts[0][0], hy = pts[0][1];
var hs = cell * 0.84, hr = hs * 0.34;
var dead = (state === "over");
ctx.fillStyle = dead ? rgb(COL.danger) : rgb(COL.ink);
rr(hx - hs / 2, hy - hs / 2, hs, hs, hr);
ctx.fill();
// eyes
var ex = dir.y !== 0 ? 1 : 0, ey = dir.x !== 0 ? 1 : 0;
var fwd = cell * 0.15, side = cell * 0.19, er = Math.max(1, cell * 0.085);
ctx.fillStyle = rgb(COL.surface);
for (var s = -1; s <= 1; s += 2) {
ctx.beginPath();
ctx.arc(hx + dir.x * fwd + ex * side * s, hy + dir.y * fwd + ey * side * s, er, 0, 6.2832);
ctx.fill();
}
}
function drawCrash() {
if (!crash) return;
var x = cc(crash.x), y = cc(crash.y), r = cell * 0.34;
ctx.strokeStyle = rgb(COL.danger);
ctx.lineWidth = Math.max(2, DPR * 1.8);
ctx.lineCap = "round";
ctx.beginPath(); ctx.arc(x, y, r, 0, 6.2832); ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - r * 0.5, y - r * 0.5); ctx.lineTo(x + r * 0.5, y + r * 0.5);
ctx.moveTo(x + r * 0.5, y - r * 0.5); ctx.lineTo(x - r * 0.5, y + r * 0.5);
ctx.stroke();
}
/* ── loop ───────────────────────────────────────────────────────────────── */
var acc = 0, last = 0;
function frame(now) {
requestAnimationFrame(frame);
nowMs = now;
var dt = last ? Math.min(120, now - last) : 16;
last = now;
for (var i = rings.length - 1; i >= 0; i--) {
rings[i].t += dt;
if (rings[i].t > 460) rings.splice(i, 1);
}
if (shake > 0) shake = Math.max(0, shake - dt * 0.03);
if (state === "run") {
elapsed += dt;
acc += dt;
var iv = tickMs();
var guard = 0;
while (acc >= iv && state === "run" && guard++ < 6) {
acc -= iv;
tick();
iv = tickMs();
}
tt = state === "run" ? Math.max(0, Math.min(1, acc / iv)) : 1;
} else if (state === "over" && mode === "demo") {
demoTimer -= dt;
if (demoTimer <= 0) startDemo();
} else if (state === "won" && mode === "demo") {
demoTimer -= dt;
if (demoTimer <= 0) startDemo();
}
draw();
}
/* ── input ──────────────────────────────────────────────────────────────── */
function pushDir(d) {
if (!d) return;
if (mode === "demo") { takeOver(d); return; }
if (state === "over" || state === "won") { takeOver(d); return; }
if (state === "paused") resume();
if (state !== "run") return;
var last2 = queue.length ? queue[queue.length - 1] : dir;
if (d.x === -last2.x && d.y === -last2.y) return;
if (d.x === last2.x && d.y === last2.y) return;
if (queue.length >= 2) return;
queue.push(d);
sync();
}
function mainAction() {
if (mode === "demo") takeOver(dir);
else if (state === "run") pause();
else if (state === "paused") resume();
else again();
}
var KEYS = {
ArrowUp: UP, ArrowDown: DOWN, ArrowLeft: LEFT, ArrowRight: RIGHT,
w: UP, a: LEFT, s: DOWN, d: RIGHT,
W: UP, A: LEFT, S: DOWN, D: RIGHT,
Up: UP, Down: DOWN, Left: LEFT, Right: RIGHT
};
function keyDir(k) {
return Object.prototype.hasOwnProperty.call(KEYS, k) ? KEYS[k] : null;
}
window.addEventListener("keydown", function (e) {
if (e.ctrlKey || e.metaKey || e.altKey) return;
var k = e.key;
var tag = e.target && e.target.tagName;
var onControl = (tag === "BUTTON" || tag === "INPUT" || tag === "A");
var kd = keyDir(k);
if (kd) {
e.preventDefault();
pushDir(kd);
return;
}
if (k === " " || k === "Spacebar" || k === "Enter") {
if (onControl) return; // let the focused control handle it
e.preventDefault();
mainAction();
return;
}
if (k === "p" || k === "P") { e.preventDefault(); if (state === "paused") resume(); else pause(); return; }
if (k === "r" || k === "R") { e.preventDefault(); takeOver(RIGHT); say("Fresh run."); return; }
if (k === "Escape") { pause(); }
});
dpad.addEventListener("pointerdown", function (e) {
var b = e.target.closest ? e.target.closest("button[data-d]") : null;
if (!b) return;
e.preventDefault();
pushDir(DIRNAME[b.getAttribute("data-d")]);
});
dpad.addEventListener("click", function (e) {
var b = e.target.closest ? e.target.closest("button[data-d]") : null;
if (!b) return;
pushDir(DIRNAME[b.getAttribute("data-d")]);
});
midBtn.addEventListener("click", mainAction);
mainBtn.addEventListener("click", mainAction);
watchBtn.addEventListener("click", function () { startDemo(); say("Demo is driving."); });
ovBtn.addEventListener("click", function () {
if (state === "paused") resume(); else again();
});
edgesEl.addEventListener("click", function (e) {
var b = e.target.closest ? e.target.closest("button[data-e]") : null;
if (!b) return;
wrap = b.getAttribute("data-e") === "1";
var kids = edgesEl.children;
for (var i = 0; i < kids.length; i++) {
kids[i].setAttribute("aria-pressed", (kids[i] === b) ? "true" : "false");
}
edgeCap.textContent = (wrap
? "Slide off one edge and reappear on the other. The dashed border shows the openings."
: "Hit a wall and the run ends. Same board, much less room to think.")
+ " Changing either setting starts a fresh run.";
restartSame();
say(wrap ? "Edges wrap around." : "Edges are solid.");
});
sizesEl.addEventListener("click", function (e) {
var b = e.target.closest ? e.target.closest("button[data-n]") : null;
if (!b) return;
N = parseInt(b.getAttribute("data-n"), 10) || 21;
var kids = sizesEl.children;
for (var i = 0; i < kids.length; i++) {
kids[i].setAttribute("aria-pressed", (kids[i] === b) ? "true" : "false");
}
resize();
restartSame();
say("Board is " + N + " by " + N + " cells.");
});
function restartSame() {
if (mode === "demo") startDemo();
else takeOver(RIGHT);
}
/* swipe on the board */
var sw = null;
canvas.addEventListener("pointerdown", function (e) {
sw = { x: e.clientX, y: e.clientY, used: false };
if (canvas.setPointerCapture && e.pointerId != null) {
try { canvas.setPointerCapture(e.pointerId); } catch (err) {}
}
});
canvas.addEventListener("pointermove", function (e) {
if (!sw || sw.used) return;
var dx = e.clientX - sw.x, dy = e.clientY - sw.y;
if (Math.abs(dx) < 22 && Math.abs(dy) < 22) return;
sw.used = true;
pushDir(Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? RIGHT : LEFT) : (dy > 0 ? DOWN : UP));
});
canvas.addEventListener("pointerup", function (e) {
if (sw && !sw.used && e.pointerType === "touch") mainAction();
sw = null;
});
canvas.addEventListener("pointercancel", function () { sw = null; });
document.addEventListener("visibilitychange", function () {
if (document.hidden) { acc = 0; pause(); }
});
if (window.ResizeObserver) {
new ResizeObserver(function () { resize(); }).observe(boardEl);
}
window.addEventListener("resize", resize);
window.addEventListener("orientationchange", function () { setTimeout(resize, 120); });
/* ── go ─────────────────────────────────────────────────────────────────── */
readTheme();
resize();
startDemo();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.