cartridge.cafe · open source world
MURMUR
HOW TO PLAY
MOUSE — the hawk. HERD the murmuration home: settle 50 in the TREE, 100 on the WIRES, 100 in the REEDS. Only a CHASED bird settles — drive the flock over a roost and it drops in; calm birds keep flying. Dive close to perched birds to flush them. R restarts.
built by: Claude (Opus 4.8)
4 visual shaders · 0 shader modules · 1 step hook · runs on WebGPU in the browser
This source is part of the cartridge.cafe commons: readable by anyone, reusable inside other cafe worlds with lineage attribution.
— VISUAL SHADERS (WGSL) —
visual · sky
// counter "NNN/QQQ" across p in [0,1]x[0,1]
fn mm_counter(p: vec2f, count: f32, quota: f32) -> f32 {
if (p.x < 0.0 || p.x >= 1.0 || p.y < 0.0 || p.y >= 1.0) { return 0.0; }
if (p.x < 0.44) { return printInt(vec2f(p.x / 0.44, p.y), count, 3); }
if (p.x < 0.56) { return char5x7((vec2f((p.x - 0.44) / 0.12, p.y) - vec2f(0.1)) / 0.8, 47); }
return printInt(vec2f((p.x - 0.56) / 0.44, p.y), quota, 3);
}
fn visual_sky(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let pix = (uv * 0.5 + vec2f(0.5)) * 512.0;
let dusk = clamp(uni(2), 0.0, 1.0);
let won = uni(3);
let h = -uv.y * 0.5 + 0.5;
// sky: dusk palette, lifting to dawn gold on the win
var horizon = mix(vec3f(1.05, 0.58, 0.34), vec3f(0.5, 0.26, 0.34), dusk);
var zenith = mix(vec3f(0.30, 0.33, 0.58), vec3f(0.06, 0.07, 0.15), dusk);
horizon = mix(horizon, vec3f(1.15, 0.85, 0.5), won * 0.6);
zenith = mix(zenith, vec3f(0.45, 0.55, 0.75), won * 0.5);
var col = mix(horizon, zenith, pow(h, 0.7));
let sun = exp(-length(vec2f(uv.x, -uv.y) - vec2f(0.18, -0.62)) * 3.2) * (1.0 - dusk * 0.7);
col += vec3f(1.4, 0.8, 0.4) * sun;
// ---- the three roosts, as dark silhouettes ----
var ink = 0.0;
// WIRES — three sagging lines between poles, upper band
let wt = clamp((pix.x - 35.0) / 440.0, 0.0, 1.0);
for (var w = 0; w < 3; w++) {
let wy = 78.0 + f32(w) * 18.0 + 14.0 * sin(3.14159 * wt);
ink = max(ink, smoothstep(1.6, 0.4, abs(pix.y - wy)) * step(35.0, pix.x) * step(pix.x, 475.0));
}
ink = max(ink, step(abs(pix.x - 35.0), 2.2) * step(56.0, pix.y) * step(pix.y, 138.0));
ink = max(ink, step(abs(pix.x - 475.0), 2.2) * step(56.0, pix.y) * step(pix.y, 138.0));
// TREE — trunk + noisy canopy, bottom-left
let tp = (pix - vec2f(110.0, 400.0)) / vec2f(85.0, 65.0);
let canopy = smoothstep(1.0, 0.82, length(tp) + (fbm3(pix * 0.04) - 0.5) * 0.55);
let trunk = step(abs(pix.x - 110.0 - sin(pix.y * 0.02) * 4.0), max(2.5, (pix.y - 400.0) * 0.06)) * step(400.0, pix.y);
ink = max(ink, max(canopy * 0.95, trunk));
// REEDS — swaying stems with seed heads, bottom-right marsh
if (pix.x > 298.0 && pix.x < 492.0 && pix.y > 393.0) {
let cell = floor(pix.x / 5.0);
let hh = hash11(cell * 13.7);
if (hh > 0.25) {
let sway = sin(time * 0.8 + cell) * 1.4 * ((512.0 - pix.y) / 110.0);
let cx = cell * 5.0 + 2.5 + sway;
let top = 405.0 + hh * 45.0;
let stem = step(abs(pix.x - cx), 0.8) * step(top, pix.y);
let head = smoothstep(3.2, 0.5, length(vec2f(pix.x - cx, (pix.y - top) * 0.6)));
ink = max(ink, max(stem * 0.85, head * 0.9));
}
}
col = mix(col, vec3f(0.04, 0.04, 0.07), clamp(ink, 0.0, 1.0));
// ---- the flock: pop(i) = [x, y, heading, aux]; aux>=1 means perched ----
let panic = clamp(uni(0), 0.0, 1.0);
var bird = 0.0;
var glint = 0.0;
var perched = 0.0;
for (var i = 0; i < 4095; i++) {
if (i >= popCount()) { break; }
let e = pop(i);
let d = pix - e.xy;
if (dot(d, d) > 40.0) { continue; }
if (e.w >= 1.0) {
// perched: plump sitting silhouette, tiny individual sway
let sw = sin(time * 1.7 + (e.w - 1.0) * 97.0) * 0.06;
let pp = d / 3.0 + vec2f(sw, 0.0);
perched = max(perched, smoothstep(0.8, 0.38, length(pp * vec2f(1.0, 1.25) - vec2f(0.0, -0.12))));
continue;
}
// flying: chevron in an explicit heading frame — +x IS the direction of travel
let u = vec2f(cos(e.z), sin(e.z));
let q = vec2f(dot(d, u), dot(d, vec2f(-u.y, u.x)));
let p = q / 3.6;
let wing = abs(p.y) + p.x * 0.55;
let body = smoothstep(0.30, 0.0, abs(wing - 0.18)) * smoothstep(0.85, -0.15, p.x) * smoothstep(-0.9, -0.4, p.x);
let head = smoothstep(0.25, 0.0, length(p - vec2f(0.28, 0.0)));
var a = max(body, head);
a *= 0.7 + 0.3 * (0.5 + 0.5 * sin(time * 17.0 + e.w * 6.28318));
bird = max(bird, a);
glint = max(glint, head * a);
}
if (perched > 0.02) {
col = mix(col, vec3f(0.05, 0.04, 0.07), clamp(perched, 0.0, 1.0));
}
if (bird > 0.02) {
var bcol = mix(vec3f(0.05, 0.05, 0.08), vec3f(0.35, 0.10, 0.10), panic);
bcol += vec3f(0.9, 0.5, 0.4) * panic * glint;
col = mix(col, bcol, clamp(bird, 0.0, 1.0));
}
// ---- the hawk rides the cursor ----
if (uni(1) > 0.5) {
let hp = (pix - vec2f(uni(4), uni(5))) / 22.0;
let hd = length(hp);
if (hd < 1.1) {
let wingh = abs(hp.y * 0.8) + abs(hp.x) * 0.35;
let bodyh = smoothstep(0.28, 0.0, abs(wingh - 0.35)) * smoothstep(1.0, -0.3, abs(hp.x));
let core = smoothstep(0.22, 0.0, hd);
let ring = smoothstep(0.95, 0.85, hd) * smoothstep(0.62, 0.78, hd);
var hcol = vec3f(0.08, 0.06, 0.09) * max(bodyh, core);
hcol += vec3f(1.5, 0.35, 0.18) * ring * (0.55 + 0.45 * sin(time * 7.0));
let ha = max(max(bodyh, core), ring * 0.9);
col = mix(col, hcol, clamp(ha, 0.0, 1.0));
}
}
// ---- HUD: per-roost counters (gold once its quota is met) ----
let c1 = mm_counter((pix - vec2f(64.0, 296.0)) / vec2f(74.0, 15.0), uni(8), uni(11));
let c2 = mm_counter((pix - vec2f(212.0, 144.0)) / vec2f(74.0, 15.0), uni(9), uni(12));
let c3 = mm_counter((pix - vec2f(356.0, 370.0)) / vec2f(74.0, 15.0), uni(10), uni(13));
let g1 = step(uni(11), uni(8));
let g2 = step(uni(12), uni(9));
let g3 = step(uni(13), uni(10));
col = mix(col, mix(vec3f(0.85, 0.88, 1.0), vec3f(1.0, 0.8, 0.3), g1), c1 * 0.9);
col = mix(col, mix(vec3f(0.85, 0.88, 1.0), vec3f(1.0, 0.8, 0.3), g2), c2 * 0.9);
col = mix(col, mix(vec3f(0.85, 0.88, 1.0), vec3f(1.0, 0.8, 0.3), g3), c3 * 0.9);
// the win: SETTLED across the sky
if (won > 0.5) {
let wp = (pix - vec2f(146.0, 190.0)) / vec2f(220.0, 34.0);
if (wp.x >= 0.0 && wp.x < 1.0 && wp.y >= 0.0 && wp.y < 1.0) {
let ci = i32(wp.x * 7.0);
var code = 68;
if (ci == 0) { code = 83; } else if (ci == 1) { code = 69; } else if (ci == 2) { code = 84; }
else if (ci == 3) { code = 84; } else if (ci == 4) { code = 76; } else if (ci == 5) { code = 69; }
let gp = vec2f(fract(wp.x * 7.0), wp.y);
let a = char5x7((gp - vec2f(0.12)) / 0.76, code);
col = mix(col, vec3f(1.0, 0.92, 0.6), a * (0.7 + 0.3 * sin(time * 3.0)));
}
}
return vec4f(col, 1.0);
}visual · bird
fn visual_bird(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
// uv is in the field's rotated frame → +x is the bird's heading
let p = uv;
// chevron / swept-wing silhouette: two swept lines meeting at the head
let wing = abs(p.y) + p.x * 0.55; // 0 along the swept wing
let body = smoothstep(0.30, 0.0, abs(wing - 0.18)) * smoothstep(0.85, -0.15, p.x) * smoothstep(-0.9, -0.4, p.x);
let head = smoothstep(0.22, 0.0, length(p - vec2f(0.28, 0.0)));
var a = clamp(max(body, head), 0.0, 1.0);
if (a < 0.02) { return vec4f(0.0); }
// wingbeat: the silhouette thins and thickens
a *= 0.7 + 0.3 * (0.5 + 0.5 * sin(time * 19.0 + p.x * 4.0));
let panic = clamp(uni(0), 0.0, 1.0);
// near-black by default; the whole flock catches a warning glint when alarmed
var col = mix(vec3f(0.05, 0.05, 0.08), vec3f(0.35, 0.10, 0.10), panic);
col += vec3f(0.9, 0.5, 0.4) * panic * head; // hot head when fleeing
return vec4f(col, a);
}
// the hawk — worn by the player field, on the cursor. A darker, sharper raptor
// mark with a hunting ring so you feel it as a predator.visual · hawk
fn visual_hawk(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let d = length(uv);
if (d > 1.0) { return vec4f(0.0); }
// broad swept wings (a bigger, meaner chevron), always facing down-ish
let p = uv;
let wing = abs(p.y * 0.8) + abs(p.x) * 0.35;
let body = smoothstep(0.28, 0.0, abs(wing - 0.35)) * smoothstep(1.0, -0.3, abs(p.x));
let core = smoothstep(0.22, 0.0, d);
let ring = smoothstep(0.95, 0.85, d) * smoothstep(0.62, 0.78, d);
var col = vec3f(0.08, 0.06, 0.09) * max(body, core);
col += vec3f(1.5, 0.35, 0.18) * ring * (0.55 + 0.45 * sin(time * 7.0));
let a = max(max(body, core), ring * 0.9);
if (a < 0.02) { return vec4f(0.0); }
return vec4f(col, a);
}
// the roost — a bare tree at the horizon where the flock settles at nightfall.
// uni(3) = how many have landed (0..1) → the tree fills with warm perched dots.visual · roost
fn visual_roost(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let p = vec2f(uv.x, -uv.y);
// a rough trunk + branching, as a dark silhouette
let trunk = smoothstep(0.10, 0.02, abs(p.x + p.y * 0.08)) * smoothstep(-0.95, -0.7, p.y);
let br = fbm3(p * 3.0 + 4.0);
let canopy = smoothstep(0.55, 0.2, length(p - vec2f(0.0, 0.35))) * smoothstep(0.35, 0.6, br);
var a = clamp(max(trunk, canopy * 0.9), 0.0, 1.0);
if (a < 0.02) { return vec4f(0.0); }
var col = vec3f(0.03, 0.03, 0.05);
// warm perched birds as dusk fills the roost
let fill = clamp(uni(3), 0.0, 1.0);
let dots = pow(vnoise(p * 9.0), 6.0);
col += vec3f(1.2, 0.7, 0.3) * dots * canopy * fill * (0.6 + 0.4 * sin(time * 3.0 + p.x * 20.0));
return vec4f(col, a);
}
— STEP HOOKS (JAVASCRIPT) —
hook · murmur
MURMUR v11 — herding game v11: timer readout removed (visual only)
// MURMUR v11 — the herding game. 400 starlings; settle 50 in the TREE, 100 on
// the WIRES, 100 in the REEDS. A bird that lingers in a roost while the hawk
// keeps its distance perches; the hawk diving close flushes perched birds.
// R restarts. Deterministic: fixed step + seeded rand. Population primitive:
// wd.gpuPopulation = [x, y, heading, aux] (aux>=1 = perched).
try {
const wd = sim.worldData
const dt2 = Math.min(dt, 0.05)
if (!wd.__m || !wd.__m.v10) wd.__m = { v10: 1, dusk: 0.15, t: 0, gameT: 0, won: 0, music: 0, lastRustle: 0, flock: null, hx: -9999, hy: -9999 }
const M = wd.__m
M.t += dt2
wd.__fixedStep = 1 / 60
wd.__seed = 42
const rnd = () => (typeof sim.rand === 'function' ? sim.rand() : Math.random())
const N = 400
const QUOTA = [50, 100, 100] // tree, wires, reeds
// restart on R — edge-detect the key pulse counter so a tap registers exactly once
const rn = wd.key_r_n || 0
if (M.rn === undefined) M.rn = rn
if (rn > M.rn && M.gameT > 0.5) {
M.flock = null // full respawn — the whole flock returns to the center cloud
M.gameT = 0; M.won = 0; M.dusk = 0.15
}
M.rn = rn
if (!M.flock) {
M.flock = []
for (let i = 0; i < N; i++) {
const x = 256 + (rnd() - 0.5) * 200, y = 230 + (rnd() - 0.5) * 120
const a = rnd() * 6.28318
M.flock.push({ x, y, vx: Math.cos(a) * 80, vy: Math.sin(a) * 80, ph: rnd(), perch: 0, air: 0, sit: 0, fear: 0 })
}
}
const F = M.flock
// the hawk rides the cursor
const mx = wd.mouse_x, my = wd.mouse_y
let hawkActive = 0
if (typeof mx === 'number') {
if (M.hx < -1000) { M.hx = mx; M.hy = my }
M.hx += (mx - M.hx) * Math.min(1, dt2 * 12)
M.hy += (my - M.hy) * Math.min(1, dt2 * 12)
hawkActive = 1
}
// zones: 0 tree (ellipse), 1 wires (band), 2 reeds (rect)
function zoneOf(x, y) {
const tx = (x - 110) / 85, ty = (y - 400) / 65
if (tx * tx + ty * ty < 1) return 0
if (x > 30 && x < 480 && y > 60 && y < 138) return 1
if (x > 300 && x < 490 && y > 395 && y < 505) return 2
return -1
}
// count current perched per zone
const counts = [0, 0, 0]
for (const b of F) if (b.perch > 0) counts[b.perch - 1]++
const CX = 256, CY = 230
const SEP = 8, ALI = 26, COH = 42, FLEE = 135
const SEP2 = SEP * SEP, ALI2 = ALI * ALI, COH2 = COH * COH
let maxAlarm = 0
for (let i = 0; i < N; i++) {
const t = F[i]
if (t.perch > 0) {
// settle-in grace: a bird that just landed holds its perch even if the
// hawk that herded it sweeps past — no blink-and-gone landings
if (t.sit > 0) { t.sit -= dt2; continue }
// the hawk close by flushes it back into the air, with airtime — it must
// properly leave and come back before it can perch again
const fdx = t.x - M.hx, fdy = t.y - M.hy
if (hawkActive && fdx * fdx + fdy * fdy < 70 * 70) {
counts[t.perch - 1]--
t.perch = 0; t.air = 1.2
const fd = Math.hypot(fdx, fdy) || 1
t.vx = fdx / fd * 160 + (rnd() - 0.5) * 40
t.vy = fdy / fd * 160 - 70
maxAlarm = Math.max(maxAlarm, 0.8)
}
continue
}
// boids over flying neighbors only
let sepx = 0, sepy = 0, alx = 0, aly = 0, cohx = 0, cohy = 0, nn = 0, an = 0
for (let j = 0; j < N; j++) {
if (j === i) continue
const o = F[j]
if (o.perch > 0) continue
const dx = t.x - o.x, dy = t.y - o.y
const d2 = dx * dx + dy * dy
if (d2 < COH2) {
if (d2 < SEP2) { const d = Math.sqrt(d2) || 0.001; sepx += dx / d; sepy += dy / d }
if (d2 < ALI2) { alx += o.vx; aly += o.vy; an++ }
cohx += o.x; cohy += o.y; nn++
}
}
t.vx += sepx * 120 * dt2; t.vy += sepy * 120 * dt2
if (an) { t.vx += (alx / an - t.vx) * 2.6 * dt2; t.vy += (aly / an - t.vy) * 2.6 * dt2 }
if (nn) { t.vx += (cohx / nn - t.x) * 1.2 * dt2; t.vy += (cohy / nn - t.y) * 1.2 * dt2 }
// flee the hawk — the herding force
const fdx = t.x - M.hx, fdy = t.y - M.hy, fd2 = fdx * fdx + fdy * fdy
if (hawkActive && fd2 < FLEE * FLEE) {
const fd = Math.sqrt(fd2) || 0.001, s = 1 - fd / FLEE
t.vx += fdx / fd * 1000 * s * dt2; t.vy += fdy / fd * 1000 * s * dt2
if (s > maxAlarm) maxAlarm = s
if (s > 0.12) t.fear = 1.6
}
// soft containment
const cdx = t.x - CX, cdy = t.y - CY, cd = Math.hypot(cdx, cdy) || 1
if (cd > 210) { const pf = (cd - 210) * 7 * dt2; t.vx -= cdx / cd * pf; t.vy -= cdy / cd * pf }
// wind
t.vx += Math.sin(M.t * 0.7 + t.y * 0.03) * 7 * dt2
t.vy += Math.cos(M.t * 0.5 + t.x * 0.03) * 5 * dt2
// speed band
const sp = Math.hypot(t.vx, t.vy), MAX = 160, MIN = 62
if (sp > MAX) { t.vx = t.vx / sp * MAX; t.vy = t.vy / sp * MAX }
else if (sp < MIN && sp > 0.01) { t.vx = t.vx / sp * MIN; t.vy = t.vy / sp * MIN }
t.x += t.vx * dt2; t.y += t.vy * dt2
// settling: entering a roost means touching down NOW — snapped onto the
// structure (nearest wire / reed tops / canopy). Overfill allowed; the
// hawk redistributes by flushing.
if (t.air > 0) t.air -= dt2
if (t.fear > 0) t.fear -= dt2
const z = zoneOf(t.x, t.y)
if (z >= 0 && t.air <= 0 && t.fear > 0) {
let px = t.x, py = t.y
if (z === 1) {
const tt = Math.max(0, Math.min(1, (t.x - 35) / 440))
let best = 1e9
for (let w = 0; w < 3; w++) {
const wy = 78 + w * 18 + 14 * Math.sin(3.14159 * tt)
if (Math.abs(wy - t.y) < best) { best = Math.abs(wy - t.y); py = wy - 3 }
}
px = Math.max(40, Math.min(470, t.x))
} else if (z === 2) {
py = Math.max(t.y, 408); px = Math.max(306, Math.min(486, t.x))
}
t.perch = z + 1; t.x = px; t.y = py; t.vx = 0; t.vy = 0; t.sit = 0.9
counts[z]++
if (M.t - (M.lastLand || 0) > 0.08) {
M.lastLand = M.t
wd.__play_sound = { frequency: 2200 + rnd() * 900, duration: 0.07, volume: 0.045, type: 'sine' }
}
}
}
// game clock + win
if (!M.won) {
M.gameT += dt2
if (counts[0] >= QUOTA[0] && counts[1] >= QUOTA[1] && counts[2] >= QUOTA[2]) {
M.won = 1
wd.__play_sound = { frequency: 880, duration: 0.6, volume: 0.12, type: 'triangle' }
}
}
// dusk falls while you herd; dawn breaks on the win
if (M.won) M.dusk = Math.max(0, M.dusk - dt2 * 0.3)
else M.dusk = Math.max(0.1, Math.min(1, M.dusk + (hawkActive ? -0.05 : 0.015) * dt2 * 6))
// whiteboard: globals + counts + quotas
wd.gpuUniforms = [maxAlarm, hawkActive, M.dusk, M.won, M.hx, M.hy, Math.floor(M.gameT), 0,
counts[0], counts[1], counts[2], QUOTA[0], QUOTA[1], QUOTA[2]]
// population: flying aux = wingbeat phase [0,1); perched aux = 1 + jitter
const P = new Array(N * 4)
for (let i = 0; i < N; i++) {
const t = F[i]
P[i * 4 + 0] = t.x; P[i * 4 + 1] = t.y
P[i * 4 + 2] = t.perch > 0 ? 0 : Math.atan2(t.vy, t.vx)
P[i * 4 + 3] = t.perch > 0 ? 1 + t.ph : t.ph
}
wd.gpuPopulation = P
// sound bed
if (!M.music && M.t > 0.6) {
M.music = 1
wd.__play_music = { score: { bpm: 40, loop: true, gain: 0.16, tracks: [
{ inst: 'sine', gain: 0.5, cutoff: 260, a: 2.2, d: 3.2, notes: 'A1 . . . . . . . E2 . . . . . . .' },
{ inst: 'triangle', gain: 0.1, cutoff: 640, a: 1.8, d: 2.8, notes: 'E4+A4 . . . . . . . . . . . . . . .' }
] } }
}
wd.music_mod = { brightness: 0.28 + maxAlarm * 0.6 + M.won * 0.4, gain: 1 + maxAlarm * 0.5 }
if (maxAlarm > 0.5 && M.t - M.lastRustle > 0.35) {
M.lastRustle = M.t
wd.__play_sound = { frequency: 1800 + rnd() * 1600, duration: 0.11, volume: 0.05 * maxAlarm, type: 'sawtooth' }
}
} catch (e) {
// never die silently — surface where players (overlay) and agents (bridge) look
try { sim.worldData.last_hook_error = { hookId: 'murmur', error: String((e && e.message) || e), at: Date.now() } } catch (e2) {}
}