dewhop / core.js
CyberMax-tools's picture
Deploy dewhop
346fc43 verified
Raw History Blame Contribute Delete
4.4 kB
// 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;
}