Spaces:
Running
Running
Download core.js from CyberMax-tools/brimbloom: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/spaces/CyberMax-tools/brimbloom/resolve/main/core.js
- Command line
-
hf download hf://spaces/CyberMax-tools/brimbloom/core.js
-
curl -L -o core.js https://huggingface.co/spaces/CyberMax-tools/brimbloom/resolve/main/core.js
10.6 kB
| // Brimbloom core: deterministic simulation (no DOM). A flower blooms outward; one tap stops it. Stop inside the ring | |
| // (the brim) to clear the bloom; dead centre of the ring is a perfect. Miss once and the run ends. Blooms get faster, | |
| // rings thinner, and later blooms pulse or their ring breathes. Same blooms for everyone each UTC day (seed = date). | |
| // Fixed 60 Hz ticks. (c) 2026 CyberMax. | |
| export const HZ = 60; | |
| export const GAP = 24; // ticks between blooms (the result flash) | |
| export const LIMIT = 1.06; // a bloom that grows past the frame is a miss | |
| 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; | |
| }; | |
| } | |
| /** Bloom i of a seed: ring centre c and width w (radius 1 = the frame), growth speed s (radius/second), | |
| * pulse amplitude p (the bloom surges and eases), breathe b (the ring itself drifts in and out), petals n. */ | |
| export function bloom(seed, i) { | |
| const r = rng(`brimbloom:${seed}:${i}`); | |
| const w = Math.max(0.075, 0.17 - i * 0.0065 - r() * 0.02); | |
| return { | |
| c: 0.46 + r() * 0.4, w, | |
| s: Math.min(0.95, 0.42 + i * 0.04 + r() * 0.1), | |
| p: i < 4 ? 0 : Math.min(0.5, 0.15 + r() * 0.35), | |
| b: i < 7 || r() < 0.4 ? 0 : 0.02 + r() * 0.025, | |
| n: 5 + Math.floor(r() * 4), hue: Math.floor(r() * 360), | |
| }; | |
| } | |
| /** Radius after t ticks. With a pulse the bloom surges and eases but never shrinks (p <= 0.5 keeps it opening). */ | |
| export function radius(b, t) { | |
| const x = t / HZ; | |
| return b.s * (x + b.p * Math.sin(x * 4.4) / 4.4); | |
| } | |
| /** The ring centre after t ticks (it breathes on later blooms). */ | |
| export const ringC = (b, t) => b.c + (b.b ? b.b * Math.sin(t / HZ * 2.2) : 0); | |
| /** Judge a stop at radius rr against the ring: 'perfect' (middle half of the band), 'good', or 'miss'. */ | |
| export function judge(b, rr, t) { | |
| const d = Math.abs(rr - ringC(b, t)); | |
| return d <= b.w * 0.25 ? 'perfect' : d <= b.w / 2 ? 'good' : 'miss'; | |
| } | |
| /** A run: step(tap) once per tick; returns 'perfect' | 'good' | 'miss' | ''. */ | |
| export function makeSim(seed) { | |
| const s = { | |
| seed, i: 0, t: 0, wait: 0, cur: bloom(seed, 0), r: 0, marks: [], score: 0, perfect: 0, dead: false, last: '', | |
| step(tap) { | |
| if (this.dead) return ''; | |
| if (this.wait > 0) { if (--this.wait === 0) { this.i++; this.cur = bloom(this.seed, this.i); this.t = 0; this.r = 0; } return ''; } | |
| this.t++; | |
| const nr = radius(this.cur, this.t); | |
| this.r = nr > this.r ? nr : this.r; | |
| let res = ''; | |
| if (tap) res = judge(this.cur, this.r, this.t); | |
| else if (this.r > LIMIT) res = 'miss'; | |
| if (!res) return ''; | |
| this.marks.push(res); this.last = res; | |
| if (res === 'miss') { this.dead = true; return res; } | |
| this.score++; if (res === 'perfect') this.perfect++; | |
| this.wait = GAP; | |
| return res; | |
| }, | |
| }; | |
| return s; | |
| } | |
| /** Emoji row 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'); | |
| } | |
| // ------------------------------------------------------------------ 30 levels (own seed space: the daily is untouched) | |
| // Each level is a finite goal: bloom `goal` flowers before you run out of `lives` (a miss costs a life and the next | |
| // flower comes). Speeds s and widths w are [min, max] ranges; pulse = surge amplitude range, breathe = ring drift, | |
| // two = chance a flower has two brims (stop it twice), fade = the ring fades as the bloom nears it, decoy = a false | |
| // ring in a warning colour. Bosses (every 10th) give one life. (c) 2026 CyberMax. | |
| export const HOLD = 14; // ticks a two-brim flower rests after its first stop | |
| const L = (name, goal, s, w, o = {}) => ({ name, goal, s, w, lives: o.lives ?? 3, ...o }); | |
| export const LEVELS = [ | |
| L('First bud', 5, [0.36, 0.4], [0.2, 0.22]), | |
| L('Petal pond', 6, [0.4, 0.46], [0.18, 0.2]), | |
| L('Quickening', 7, [0.45, 0.52], [0.16, 0.18]), | |
| L('Narrow brim', 8, [0.48, 0.56], [0.14, 0.16]), | |
| L('Surge', 8, [0.46, 0.52], [0.16, 0.18], { pulse: [0.2, 0.35], twist: 'Surging blooms rush, then ease' }), | |
| L('Rush hour', 8, [0.5, 0.58], [0.15, 0.17], { pulse: [0.25, 0.4] }), | |
| L('Tide pool', 9, [0.54, 0.62], [0.14, 0.16], { pulse: [0.25, 0.45] }), | |
| L('Quick petals', 10, [0.58, 0.66], [0.13, 0.15], { pulse: [0.3, 0.5], lives: 2 }), | |
| L('Thin water', 10, [0.6, 0.68], [0.12, 0.14], { pulse: [0.3, 0.5], lives: 2 }), | |
| L('Garden gauntlet', 12, [0.6, 0.72], [0.12, 0.14], { pulse: [0.2, 0.5], lives: 1, boss: true }), | |
| L('Breathing ring', 8, [0.5, 0.56], [0.15, 0.17], { breathe: [0.02, 0.03], twist: 'Breathing rings drift in and out' }), | |
| L('Low tide', 9, [0.54, 0.6], [0.14, 0.16], { breathe: [0.02, 0.035] }), | |
| L('Swell', 9, [0.54, 0.62], [0.14, 0.16], { breathe: [0.02, 0.035], pulse: [0.2, 0.35] }), | |
| L('Ripples', 10, [0.58, 0.66], [0.13, 0.15], { breathe: [0.025, 0.04], pulse: [0.2, 0.4], lives: 2 }), | |
| L('Moon pond', 10, [0.62, 0.7], [0.12, 0.14], { breathe: [0.025, 0.04], pulse: [0.25, 0.45], lives: 2 }), | |
| L('Two brims', 6, [0.46, 0.52], [0.16, 0.18], { two: 1, twist: 'Two brims: stop each flower twice' }), | |
| L('Double take', 7, [0.5, 0.56], [0.15, 0.17], { two: 0.7 }), | |
| L('Second wind', 8, [0.52, 0.6], [0.14, 0.16], { two: 0.6, pulse: [0.2, 0.35] }), | |
| L('Echo bloom', 8, [0.56, 0.62], [0.13, 0.15], { two: 0.6, breathe: [0.02, 0.03], lives: 2 }), | |
| L('Twin gauntlet', 10, [0.58, 0.66], [0.12, 0.14], { two: 0.6, pulse: [0.2, 0.4], breathe: [0.02, 0.03], lives: 1, boss: true }), | |
| L('Fading brim', 8, [0.5, 0.56], [0.15, 0.17], { fade: true, twist: 'Fading rings vanish as the bloom nears' }), | |
| L('Mist', 9, [0.54, 0.6], [0.14, 0.16], { fade: true, pulse: [0.2, 0.35] }), | |
| L('Ghost pond', 9, [0.56, 0.62], [0.14, 0.16], { fade: true, breathe: [0.02, 0.03] }), | |
| L('Fog bank', 10, [0.58, 0.66], [0.13, 0.15], { fade: true, two: 0.4, lives: 2 }), | |
| L('Night bloom', 10, [0.6, 0.68], [0.12, 0.14], { fade: true, pulse: [0.25, 0.45], lives: 2 }), | |
| L('Thorn ring', 8, [0.52, 0.58], [0.14, 0.16], { decoy: true, twist: 'Thorn rings: only the bright ring counts' }), | |
| L('Bramble', 9, [0.56, 0.62], [0.13, 0.15], { decoy: true, pulse: [0.2, 0.4] }), | |
| L('Briar patch', 10, [0.58, 0.66], [0.13, 0.15], { decoy: true, breathe: [0.02, 0.03], two: 0.3, lives: 2 }), | |
| L('Thornfall', 10, [0.62, 0.7], [0.12, 0.14], { decoy: true, fade: true, pulse: [0.2, 0.4], lives: 2 }), | |
| L('Full bloom', 14, [0.6, 0.72], [0.11, 0.13], { pulse: [0.2, 0.45], breathe: [0.02, 0.03], two: 0.4, fade: true, decoy: true, lives: 1, boss: true }), | |
| ]; | |
| const pick = (r, [a, b]) => a + r() * (b - a); | |
| /** Flower i of level num (1-based): same shape as bloom() plus rings (one or two brims), fade and decoy. */ | |
| export function levelBloom(num, i) { | |
| const D = LEVELS[num - 1], r = rng(`brimbloom:lv:${num}:${i}`); | |
| const easeIn = Math.min(1, i / Math.max(1, D.goal - 1)); // speed climbs across the level | |
| const s = D.s[0] + (D.s[1] - D.s[0]) * (0.6 * easeIn + 0.4 * r()); | |
| const w = pick(r, D.w); | |
| const p = D.pulse && i > 0 ? pick(r, D.pulse) : 0; | |
| const b = D.breathe && i > 0 ? pick(r, D.breathe) : 0; | |
| const two = D.two && r() < D.two && (D.two >= 1 || i > 0); | |
| let rings; | |
| if (two) { | |
| const c1 = 0.34 + r() * 0.1; | |
| rings = [{ c: c1, w }, { c: Math.min(0.88, c1 + 0.34 + r() * 0.08), w }]; | |
| } else rings = [{ c: 0.5 + r() * 0.36, w }]; | |
| const out = { c: rings[0].c, w, s, p, b, n: 5 + Math.floor(r() * 4), hue: Math.floor(r() * 360), rings, k: 0, fade: !!D.fade }; | |
| if (D.decoy) { | |
| // a thorn ring well clear of every real brim (inside the first brim when there is room, else outside) | |
| const c0 = rings[0].c, last = rings[rings.length - 1].c; | |
| const dc = c0 - 0.28 >= 0.2 ? c0 - 0.24 - r() * 0.04 : last + 0.24; | |
| if (dc <= 0.98) out.decoy = { c: dc, w }; | |
| } | |
| return out; | |
| } | |
| /** Stops a level asks for (two-brim flowers count twice). */ | |
| export function levelStops(num) { | |
| let n = 0; | |
| for (let i = 0; i < LEVELS[num - 1].goal; i++) n += levelBloom(num, i).rings.length; | |
| return n; | |
| } | |
| /** Level score: 2 per perfect stop, 1 per good stop. Star thresholds [1 clear, 2, 3] in score points. */ | |
| export function levelThresholds(num) { | |
| const max = 2 * levelStops(num); | |
| return [1, Math.ceil(max * 0.7), Math.ceil(max * 0.86)]; | |
| } | |
| /** Stars for a finished level: 0 if failed, else 1-3 by score. */ | |
| export function levelStars(num, score, won = true) { | |
| if (!won) return 0; | |
| const t = levelThresholds(num); | |
| return score >= t[2] ? 3 : score >= t[1] ? 2 : 1; | |
| } | |
| /** A level run: same step(tap) contract as makeSim; ends with dead=true and won=true/false. */ | |
| export function makeLevelSim(num) { | |
| const D = LEVELS[num - 1]; | |
| const s = { | |
| level: num, goal: D.goal, lives: D.lives, i: 0, t: 0, wait: 0, hold: 0, cur: levelBloom(num, 0), r: 0, marks: [], | |
| score: 0, points: 0, perfect: 0, cleared: 0, dead: false, won: false, last: '', missed: 0, | |
| busy() { return this.wait > 0 || this.hold > 0; }, | |
| step(tap) { | |
| if (this.dead) return ''; | |
| if (this.wait > 0) { if (--this.wait === 0) { this.i++; this.cur = levelBloom(num, this.i); this.t = 0; this.r = 0; } return ''; } | |
| if (this.hold > 0) { this.hold--; return ''; } | |
| this.t++; | |
| const nr = radius(this.cur, this.t); | |
| this.r = nr > this.r ? nr : this.r; | |
| let res = ''; | |
| if (tap) res = judge(this.cur, this.r, this.t); | |
| else if (this.r > LIMIT || this.r > this.cur.c + this.cur.w / 2 + 0.12 && this.cur.k < this.cur.rings.length - 1) res = 'miss'; | |
| if (!res) return ''; | |
| this.marks.push(res); this.last = res; | |
| if (res === 'miss') { | |
| this.missed++; this.lives--; | |
| if (this.lives <= 0) { this.dead = true; return res; } | |
| this.wait = GAP + 6; | |
| return res; | |
| } | |
| this.score++; this.points += res === 'perfect' ? 2 : 1; if (res === 'perfect') this.perfect++; | |
| const b = this.cur; | |
| if (b.k < b.rings.length - 1) { b.k++; b.c = b.rings[b.k].c; b.w = b.rings[b.k].w; this.hold = HOLD; return res; } | |
| this.cleared++; | |
| if (this.cleared >= this.goal) { this.dead = true; this.won = true; return res; } | |
| this.wait = GAP; | |
| return res; | |
| }, | |
| }; | |
| return s; | |
| } | |