返回资讯中心
外部精选
软件工程
#开发者实时流

Show HN: Earithmetic – multiply in your head when digits are notes or rhythms

beat.segs.forEach((sg, si) => sg.slots.forEach((sl, ci) => {

Hacker News 最新quietcircuit2026/10/28 · 2 天前21 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

; return; } const notes = []; beat.segs.forEach((sg, si) => sg.slots.forEach((sl, ci) => { const x = X(sl); if (sl.note) { notes.push({ x, l: lev(sl.dur), si, ci }); g += ``; g += ``; if (sl.acc) g += ``; } else { g += ``; } })); const sx = n => n.x + 2.2; const bar = (a, b, L) => ``; if (notes.length === 1 && notes[0].l >= 1) { const n = notes[0]; for (let k = 0; k ; } else if (notes.length >= 2 && notes.every(n => n.l >= 1)) { g += bar(sx(notes[0]), sx(notes[notes.length - 1]), 1); const maxL = Math.max(...notes.map(n => n.l)), last = notes[notes.length - 1]; for (let L = 2; L { let run = []; const flush = () => { if (run.length >= 2) g += bar(sx(run[0]), sx(run[run.length - 1]), L); else if (run.length === 1) { const a = sx(run[0]); g += run[0] === last ? bar(a - 3.5, a, L) : bar(a, a + 3.5, L); } run = []; }; sg.slots.forEach((sl, ci) => { const n = notes.find(q => q.si === si && q.ci === ci); if (n && n.l >= L) run.push(n); else flush(); }); flush(); }); } } const nums = []; beat.segs.forEach((sg, si) => { if (pow2(sg.k)) return; const a = X(sg.slots[0]), b = X(sg.slots[sg.k - 1]); nums.push([(a + b) / 2 + 2.2, sg.k]); }); if (!nums.length && !pow2(beat.segs.length) && notes.length) nums.push([(sx(notes[0]) + sx(notes[notes.length - 1])) / 2, beat.segs.length]); nums.forEach(([x, n]) => { g += ${n}; }); }); return ${g}; }

/* ===== ui-game.js — pantalla de juego (#scr-game) en 4 niveles ===== / / Estado global del juego */ const G = { disc: 'mul', rk: 'mul', // clave de rating de la sesión ('mul' notas · 'mul_r' ritmo) sm: 'notes', // modo de sonido de la sesión mode: 'adapt', // 'adapt' | 'fixed' level: 0, // índice de nivel del problema actual fixedLevel: 0, prob: null, // objeto P buf: [], // cifras ingresadas; buf[0] = unidades (se escribe de derecha a izquierda) phase: 'idle', // 'answer' | 'feedback' | 'idle' t0: 0, // performance.now() en que arranca el cronómetro last: null, // último resultado session: null, // { n, ok, start, r0, res: [] } tick: null, advT: null };

const KEYLAYOUT = [0, 1, 2, 3, 4, 'back', 5, 6, 7, 8, 9, 'enter'];

/* ---------- NIVEL 1: fila de referencia (depende del modo, se redibuja al empezar) ---------- */ function renderRefKeys() { let h; if (isRhy()) { h = ♩${S.bpmRit}; for (let d = 0; d ${d}${rhythmSVG(rhythmOf(d))}; } else { h = ${noteShort(S.refMidi)}ref; for (let d = 0; d ${d}${noteShort(S.digitMidi[d])}; } $('#lv1').innerHTML = h; } function bindRefKeys() { $('#lv1').addEventListener('pointerdown', e => { const b = e.target.closest('[data-ref]'); if (!b) return; e.preventDefault(); const r = b.dataset.ref; if (r === 'A') playRefNow(); else playDigitNow(+r); flash(b); }); }

function buildKeys() { const el = $('#keys'); el.innerHTML = KEYLAYOUT.map(k => typeof k === 'number' ? ${k} : ${k === 'back' ? '⌫' : '⏎'} ).join(''); el.addEventListener('pointerdown', e => { const b = e.target.closest('[data-k]'); if (!b) return; e.preventDefault(); flash(b, 120); const k = b.dataset.k; press(/^\d$/.test(k) ? +k : k); }); }

function bindGame() { $('#g-exit').addEventListener('click', exitGame); $('#g-play').addEventListener('click', () => { playProblem(); }); $('#g-pass').addEventListener('click', () => { if (G.phase === 'answer') finish(false, true); }); // ▶ de abajo, sobre la tecla ⌫ $('#k-play').addEventListener('pointerdown', e => { e.preventDefault(); flash(e.currentTarget, 120); playProblem(); });

// Nivel 3: tocar casilleros con ♪ (o números en la corrección) para escuchar su cifra $('#lv3').addEventListener('pointerdown', e => { const c = e.target.closest('[data-d]'); if (!c) return; e.preventDefault(); playDigitNow(+c.dataset.d); flash(c, 140); });

// Nivel 4: pad de la cuenta → suena la cifra e ilumina su gemelo en el Nivel 2 $('#pad').addEventListener('pointerdown', e => { const b = e.target.closest('[data-d]'); if (!b) return; e.preventDefault(); playDigitNow(+b.dataset.d); flash(b, 140); hl(document.getElementById(vc-${b.dataset.r}-${b.dataset.i})); });

// Botones de la línea de feedback $('#fb').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; if (b.dataset.act === 'next') advance(); if (b.dataset.act === 'ans') playAnswer(); });

// Teclado físico document.addEventListener('keydown', e => { if (!$('#scr-game').classList.contains('on') || $('#ovl').classList.contains('on')) return; if (/^\d$/.test(e.key)) { press(+e.key); e.preventDefault(); } else if (e.key === 'Backspace') { press('back'); e.preventDefault(); } else if (e.key === 'Enter') { press('enter'); e.preventDefault(); } else if (e.key === ' ') { playProblem(); e.preventDefault(); } else if (e.key === 'Escape') exitGame(); });

window.addEventListener('resize', () => { if (G.prob && $('#scr-game').classList.contains('on')) renderGame(); }); }

/* ---------- flujo ---------- */ function startGame(mode, fixedLevel) { audioInit(); G.mode = mode; G.fixedLevel = fixedLevel || 0; G.sm = S.sound; G.rk = ratingKey(G.disc); G.session = { n: 0, ok: 0, start: Date.now(), r0: RATINGS[G.rk] || 0, res: [] }; $('#ovl').classList.remove('on'); renderRefKeys(); show('game'); nextProblem(); }

function nextProblem() { clearTimeout(G.advT); stopSeq(); const len = +S.sessionLen; if (len && G.session.n >= len) { endSession(); return; } const levels = DISCIPLINES[G.disc].levels; G.level = G.mode === 'adapt' ? levelOf(G.disc) : G.fixedLevel; G.prob = levels[G.level].g(); G.buf = []; G.phase = 'answer'; G.last = null; renderGame(); let delay = 0; if (S.autoPlay) delay = playProblem(); G.t0 = performance.now() + delay; startTick(); }

function press(k) { if (G.phase === 'feedback') { if (k === 'enter') advance(); return; } if (G.phase !== 'answer') return; const p = G.prob; if (typeof k === 'number') { if (G.buf.length >= p.maxLen) return; G.buf.push(k); if (S.keySound) playDigitNow(k); renderReg(); if (S.autoJudge && G.buf.length === p.maxLen) finish(isCorrect()); } else if (k === 'back') { G.buf.pop(); renderReg(); } else if (k === 'enter') { if (!G.buf.length) return; finish(isCorrect()); } }

function isCorrect() { if (!G.buf.length) return false; const s = G.buf.slice().reverse().join(''); return BigInt(s) === G.prob.ans; }

function finish(ok, passed) { if (G.phase !== 'answer') return; stopTick(); const secs = Math.max(0, (performance.now() - G.t0) / 1000); G.phase = 'feedback'; const levels = DISCIPLINES[G.disc].levels; const res = { ok: !!ok && !passed, passed: !!passed, secs }; if (G.mode === 'adapt') { RATINGS[G.rk] = nextRating(RATINGS[G.rk] || 0, res, levels); saveRatings(); } const p = G.prob; HIST.push({ ts: Date.now(), d: G.disc, sm: G.sm === 'rhythm' ? 'r' : 'n', m: G.mode, l: G.level, q: p.view.top + '×' + p.view.bot, a: p.ansStr, u: G.buf.slice().reverse().join(''), ok: res.ok, p: res.passed, s: Math.round(secs * 10) / 10, r: RATINGS[G.rk] || 0 }); saveHist(); G.session.n++; if (res.ok) G.session.ok++; G.session.res.push(res); G.last = res; if (S.fbSound) sfx(res.ok); renderGame(); if (res.ok) G.advT = setTimeout(advance, +S.advanceMs); else if (!S.wrongWait) G.advT = setTimeout(advance, +S.advanceMs * 2.5); }

function advance() { if (G.phase !== 'feedback') return; clearTimeout(G.advT); nextProblem(); }

function exitGame() { clearTimeout(G.advT); stopTick(); stopSeq(); G.phase = 'idle'; $('#ovl').classList.remove('on'); show('home'); }

function endSession() { stopTick(); stopSeq(); G.phase = 'idle'; const s = G.session, levels = DISCIPLINES[G.disc].levels; const okT = s.res.filter(r => r.ok).map(r => r.secs); const r1 = RATINGS[G.rk] || 0; const lv = r => levels[Math.min(Math.floor(r), levels.length - 1)].n; store.set(G.sm === 'rhythm' ? 'lastSession_r' : 'lastSession', { ts: Date.now(), n: s.n, ok: s.ok, med: median(okT), r0: s.r0, r1, mode: G.mode }); $('#ovl-body').innerHTML = ` ${t('end.title')}

${s.ok}/${s.n}${t('end.correct')}

${fmtS(median(okT))}${t('end.median')}

${G.mode === 'adapt' ? lv(r1) : levels[G.fixedLevel].n}${t('end.level')}

${G.mode === 'adapt' ? `${t('end.rating', { a: s.r0.toFixed(2), b: r1.toFixed(2), la: lv(s.r0), lb: lv(r1) })}

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →