Spaces:
Running
Running
File size: 10,579 Bytes
41a71f1 7154ad5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | // 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;
}
|