// Dewhop core: deterministic simulation (no DOM). A dewdrop sits on a leaf; the next leaf above sways from side to // side on its vine. One tap makes the drop hop straight up; it lands if the swaying leaf is under it when the hop // ends (dead centre = a perfect). Miss once and the drop falls: the run is over. Wait too long and the sun dries the // drop off its leaf. Leaves get narrower and faster; later ones gust (speed up and slow down) or drift. // Same leaves for everyone each UTC day (seed = date). Fixed 60 Hz ticks. (c) 2026 CyberMax. All rights reserved. export const HZ = 60; export const HOP = 16; // ticks a hop takes (the leaf keeps swaying meanwhile: lead your tap) export const DRY = 5 * HZ; // ticks a drop can wait on one leaf before the sun dries it export const SETTLE = 14; // ticks after a landing before the next tap counts (the landing splash) export const X0 = 0.5; // the drop's column (fraction of the width); every leaf sways across it export function rng(seed) { let h = 0x811c9dc5; for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 0x01000193); } let a = h >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** Leaf i of a seed: half-width w (fraction of the width), sway amplitude a, angular speed s (rad/s), phase ph, * gust g (the sway speeds up and slows down), side (the vine hangs from the left or the right), hue shift. */ export function leaf(seed, i) { const r = rng(`dewhop:${seed}:${i}`); return { w: Math.max(0.055, 0.135 - i * 0.0042 - r() * 0.015), a: Math.min(0.36, 0.2 + i * 0.008 + r() * 0.1), s: Math.min(3.0, 1.35 + i * 0.06 + r() * 0.35), ph: r() * Math.PI * 2, g: i < 5 ? 0 : Math.min(0.55, 0.15 + r() * 0.4), side: r() < 0.5 ? -1 : 1, tint: Math.floor(r() * 40) - 20, }; } /** The leaf centre (fraction of the width) after t ticks on it. Never stands still, always crosses the drop's column. */ export function leafX(l, t) { const x = t / HZ; const ang = l.ph + l.s * (x + l.g * Math.sin(x * 2.3) / 2.3); return X0 + l.a * Math.sin(ang); } /** Judge a landing: 'perfect' (middle third of the leaf), 'good' (on the leaf) or 'miss'. */ export function judge(l, t) { const d = Math.abs(leafX(l, t) - X0); return d <= l.w / 3 ? 'perfect' : d <= l.w ? 'good' : 'miss'; } /** A run: step(tap) once per tick; returns 'perfect' | 'good' | 'miss' | 'dry' | ''. */ export function makeSim(seed) { return { seed, i: 0, t: 0, wait: 0, hop: 0, cur: leaf(seed, 0), marks: [], score: 0, perfect: 0, streak: 0, best: 0, dead: false, last: '', dried: false, step(tap) { if (this.dead) return ''; this.t++; if (this.wait > 0) { this.wait--; return ''; } if (this.hop > 0) { if (--this.hop > 0) return ''; const res = judge(this.cur, this.t); this.marks.push(res); this.last = res; if (res === 'miss') { this.dead = true; return res; } this.score++; if (res === 'perfect') { this.perfect++; this.streak++; this.best = Math.max(this.best, this.streak); } else this.streak = 0; this.i++; this.cur = leaf(this.seed, this.i); this.t = 0; this.wait = SETTLE; return res; } if (tap) { this.hop = HOP; return ''; } if (this.t > DRY) { this.marks.push('miss'); this.last = 'dry'; this.dried = true; this.dead = true; return 'dry'; } return ''; }, }; } /** Emoji rows for the share text: 🟩 perfect, 🟨 good, 🟥 the miss that ended the run (rows of 10). */ export function grid(marks) { const e = marks.map((m) => (m === 'perfect' ? '🟩' : m === 'good' ? '🟨' : '🟥')); const rows = []; for (let i = 0; i < e.length; i += 10) rows.push(e.slice(i, i + 10).join('')); return rows.join('\n'); } /** Ticks from now (on leaf l at tick t) until a tap would land dead centre: the bot and the fairness tests use it. */ export function bestTap(l, t, horizon = 4 * HZ) { let best = null; for (let k = 0; k < horizon; k++) { const d = Math.abs(leafX(l, t + k + 1 + HOP) - X0); // the tap tick, then HOP ticks of hop if (best === null || d < best.d - 1e-9) best = { k, d }; if (d < l.w / 6) return { k, d }; } return best; }