Notation Sprint
How fast can you hop? Beat the 60-second clock, guard your 3 lives, and build a streak.
How to play
You get 60 seconds and 3 lives. Each question asks you to convert
scientific notation, count hops, multiply powers of 10, or compare magnitudes. Type your answer like
3.2×10^4 or 0.0025 — then press Enter or Submit.
- 1A correct answer scores +10 points. Streaks of 4 or more earn a bonus of +2 per streak level beyond 3 — keep the streak alive.
- 2A wrong answer costs 1 life, resets your streak, and shows a short explanation of why the correct answer is right.
- 3The game ends when the timer hits 0 or you lose all 3 lives. Questions start easy (converting big numbers) and ramp up to multiplying and word problems.
Best streak 0
6√2 or 6sqrt2), integers (7), and fractions like (5√2)/2.Sprint complete
function mulberry32(a){ return function(){ a |= 0; a = (a + 0x6D2B79F5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function ri(rng, lo, hi){ return lo + Math.floor(rng() * (hi - lo + 1)); } function pick(rng, arr){ return arr[Math.floor(rng() * arr.length)]; } function shuffleArr(rng, a){ for (var i = a.length - 1; i > 0; i--){ var j = Math.floor(rng() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } function gcd(a, b){ a = Math.abs(a); b = Math.abs(b); while (b !== 0){ var t = a % b; a = b; b = t; } return a === 0 ? 1 : a; } function red(n, d){ if (d === 0) throw new Error('red: zero denominator'); if (d < 0){ n = -n; d = -d; } if (n === 0) return { n: 0, d: 1 }; var g = gcd(n, d); return { n: n / g, d: d / g }; } function escHTML(s){ return String(s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function fmtInt(n){ return (n < 0 ? '\u2212' : '') + Math.abs(n); } function powHTML(base, e){ return '' + escHTML(base) + '' + e + ''; }
var RADSUPMAP = {'\u2070':'^0','\u00B9':'^1','\u00B2':'^2','\u00B3':'^3', '\u2074':'^4','\u2075':'^5','\u2076':'^6','\u2077':'^7','\u2078':'^8', '\u2079':'^9','\u207B':'^-'}; function radCaretize(s){ var out = ''; for (var i = 0; i < s.length; i++){ var c = s[i]; out += (RADSUPMAP[c] !== undefined ? RADSUPMAP[c] : c); } return out; } function radNormInput(raw){ return radCaretize(String(raw).replace(/\u2212/g, '-').replace(/\s+/g, '').toLowerCase()); } /* ================= PURE GAME LOGIC (tested in node) ================= */ function normNumStr(s){ s = String(s).replace(/,/g, '').replace(/\s+/g, ''); var m = /^(\d*)(?:\.(\d*))?$/.exec(s); if (!m || !(m[1] !== '' || m[2])) return null; var ip = m[1].replace(/^0+/, ''), fp = (m[2] || '').replace(/0+$/, ''); if (ip === '') ip = '0'; return fp ? ip + '.' + fp : ip; } function stdToSci(s){ s = String(s).replace(/,/g, '').replace(/\s+/g, ''); var m = /^(\d*)(?:\.(\d*))?$/.exec(s); if (!m || !(m[1] !== '' || m[2])) return null; var ip = m[1].replace(/^0+/, ''), fp = m[2] || ''; if (ip !== ''){ var rest = (ip.slice(1) + fp).replace(/0+$/, ''); return { c:ip[0] + (rest ? '.' + rest : ''), e:ip.length - 1 }; } var k = 0; while (!(k >= fp.length || fp[k] !== '0')) k++; if (k === fp.length) return null; var rest2 = fp.slice(k + 1).replace(/0+$/, ''); return { c:fp[k] + (rest2 ? '.' + rest2 : ''), e:-(k + 1) }; } function sciToStd(c, e){ var parts = String(c).split('.'); var ipi = parts[0].replace(/^0+/, '') || '0'; var fp = parts[1] || ''; var digits = ipi + fp; var newPos = ipi.length + e; var out; if (newPos <= 0) out = '0.' + '0'.repeat(-newPos) + digits; else if (newPos >= digits.length) out = digits + '0'.repeat(newPos - digits.length); else out = digits.slice(0, newPos) + '.' + digits.slice(newPos); if (out.indexOf('.') !== -1) out = out.replace(/0+$/, '').replace(/\.$/, ''); out = out.replace(/^0+(?=\d)/, ''); if (out === '' || out[0] === '.') out = '0' + out; return out; } function mulDecStr(a, b){ var pa = (String(a).split('.')[1] || '').length; var pb = (String(b).split('.')[1] || '').length; var ia = parseInt(String(a).replace('.', ''), 10); var ib = parseInt(String(b).replace('.', ''), 10); var prod = ia * ib, places = pa + pb; var s = String(prod); while (s.length <= places) s = '0' + s; var res = places ? s.slice(0, s.length - places) + '.' + s.slice(s.length - places) : s; if (res.indexOf('.') !== -1) res = res.replace(/0+$/, '').replace(/\.$/, ''); return res.replace(/^0+(?=\d)/, '') || '0'; } function sciNormalize(c, e){ var parts = String(c).split('.'); var ipi = parts[0].replace(/^0+/, '') || '0'; var fp = (parts[1] || '').replace(/0+$/, ''); if (ipi.length > 1){ var shift = ipi.length - 1; var nc = ipi[0] + '.' + (ipi.slice(1) + fp).replace(/0+$/, ''); nc = nc.replace(/\.$/, ''); return { c:nc, e:e + shift }; } return { c:fp ? ipi + '.' + fp : ipi, e:e }; } function sciMultiply(c1, e1, c2, e2){ return sciNormalize(mulDecStr(c1, c2), e1 + e2); } function sciHTML(c, e){ return '' + c + ' × 10' + e + ''; } function fmtInt(s){ var parts = String(s).split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); return parts.join('.'); } function normCoeff(s){ var m = /^(\d+)(?:\.(\d+))?$/.exec(String(s).replace(/\s+/g, '')); if (!m) return null; var ip = m[1].replace(/^0+/, ''), fp = (m[2] || '').replace(/0+$/, ''); if (ip === '' || ip.length !== 1) return null; return fp ? ip + '.' + fp : ip; } function parseSciInput(raw){ var s = String(raw).replace(/−/g, '-').replace(/\s+/g, '').toLowerCase(); if (!s) return null; s = s.replace(/×/g, 'x').replace(/\*/g, 'x'); var m; if ((m = /^(\d*\.?\d+)x10\^(-?\d+)$/.exec(s))){ var c = normCoeff(m[1]); if (c === null) return null; return { t:'sci', c:c, e:parseInt(m[2], 10) }; } if ((m = /^(\d*\.?\d+)e(-?\d+)$/.exec(s))){ var c2 = normCoeff(m[1]); if (c2 === null) return null; return { t:'sci', c:c2, e:parseInt(m[2], 10) }; } if ((m = /^(\d*\.?\d+)$/.exec(s))){ var v = normNumStr(m[1]); if (v === null) return null; return { t:'std', v:v }; } return null; } function sciAnswersEqual(exp, got){ if (!got) return false; if (exp.t === 'sci'){ if (got.t !== 'sci') return false; return !(exp.c !== got.c || exp.e !== got.e); } if (exp.t === 'std'){ if (got.t !== 'std') return false; return exp.v === got.v; } return false; } function sciAnsText(a){ if (a.t === 'sci') return a.c + '×' + '10^' + a.e; if (a.t === 'std') return a.v; return '?'; } function checkSciAnswer(expected, raw){ return sciAnswersEqual(expected, parseSciInput(raw)); } function dec1(rng){ var d = ri(rng, 11, 99); if (d % 10 === 0) d += 1; return (d / 10).toFixed(1); }
/* ---------------- question generators ---------------- */ function gQ(tier, rng){ var q; function R(lo, hi){ return lo + Math.floor(rng() * (hi - lo + 1)); } if (tier === 0){ var kind = R(0, 3); if (kind === 0){ var base = R(11, 99), zeros = R(1, 4); var n = String(base) + '0'.repeat(zeros), s = stdToSci(n); q = { prompt:'Write ' + fmtInt(n) + ' in scientific notation.', answer:{ t:'sci', c:s.c, e:s.e }, tag:'convert', explain:'Hop left ' + s.e + ' places: ' + fmtInt(n) + ' → ' + s.c + '×10^' + s.e + '.', key:'g|s2s|' + n }; } else if (kind === 1){ var d = R(2, 9), z2 = R(1, 3); var n2 = String(d) + '0'.repeat(z2), s2 = stdToSci(n2); q = { prompt:'Write ' + fmtInt(n2) + ' in scientific notation.', answer:{ t:'sci', c:s2.c, e:s2.e }, tag:'convert', explain:fmtInt(n2) + ' = ' + d + ' × 10^' + s2.e + ' = ' + s2.c + '×10^' + s2.e + '.', key:'g|s2sint|' + n2 }; } else if (kind === 2){ var c3 = dec1(rng), e3 = R(2, 5), v3 = sciToStd(c3, e3); q = { prompt:'Write ' + sciHTML(c3, e3) + ' in standard form.', answer:{ t:'std', v:v3 }, tag:'standard-form', explain:'Hop right ' + e3 + ' places: ' + sciHTML(c3, e3) + ' → ' + fmtInt(v3) + '.', key:'g|sci2std|' + c3 + '|' + e3 }; } else { var c4 = dec1(rng); q = { prompt:'Write ' + sciHTML(c4, 0) + ' in standard form.', answer:{ t:'std', v:c4 }, tag:'zero-exponent', explain:'10^0 = 1, so the number is just ' + c4 + '.', key:'g|sci2std0|' + c4 }; } } else if (tier === 1){ var kind1 = R(0, 3); if (kind1 === 0){ var dig = R(11, 99), z3 = R(2, 5); var s0 = '0.' + '0'.repeat(z3) + dig, s5 = stdToSci(s0); q = { prompt:'Write ' + s0 + ' in scientific notation.', answer:{ t:'sci', c:s5.c, e:s5.e }, tag:'convert-small', explain:'Hop right ' + (-s5.e) + ' places: ' + s0 + ' → ' + s5.c + '×10^' + s5.e + '.', key:'g|s2ssm|' + s0 }; } else if (kind1 === 1){ var c6 = dec1(rng), e6 = -R(2, 4), v6 = sciToStd(c6, e6); q = { prompt:'Write ' + sciHTML(c6, e6) + ' in standard form.', answer:{ t:'std', v:v6 }, tag:'standard-form-small', explain:'Hop left ' + (-e6) + ' places past the front: ' + sciHTML(c6, e6) + ' → ' + v6 + '.', key:'g|sci2stdn|' + c6 + '|' + e6 }; } else if (kind1 === 2){ var e1a = R(2, 5), e2a = e1a + R(1, 3); var c1a = dec1(rng), c2a = dec1(rng); q = { prompt:'Which is larger: ' + sciHTML(c1a, e1a) + ' or ' + sciHTML(c2a, e2a) + '? Type the larger one.', answer:{ t:'sci', c:c2a, e:e2a }, tag:'compare', explain:'Compare exponents first: ' + e2a + ' > ' + e1a + ', so ' + c2a + '×10^' + e2a + ' is larger.', key:'g|cmp|' + c1a + '|' + e1a + '|' + c2a + '|' + e2a }; } else { var c7 = dec1(rng), e7 = (rng() < 0.5 ? -1 : 1) * R(2, 6); q = { prompt:'How many places does the decimal move writing ' + sciHTML(c7, e7) + ' in standard form?', answer:{ t:'std', v:String(Math.abs(e7)) }, tag:'hops', explain:'The exponent is the hop count: |' + e7 + '| = ' + Math.abs(e7) + '.', key:'g|hops|' + c7 + '|' + e7 }; } } else { var kind2 = R(0, 3); if (kind2 === 0){ var mc1 = R(2, 9), me1 = R(1, 3), mc2 = R(2, 9), me2 = R(1, 3); var mm = sciMultiply(String(mc1), me1, String(mc2), me2); q = { prompt:'Simplify (' + sciHTML(String(mc1), me1) + ')(' + sciHTML(String(mc2), me2) + ').', answer:{ t:'sci', c:mm.c, e:mm.e }, tag:'multiply', explain:(mc1 * mc2) + '×10^' + (me1 + me2) + ' → ' + mm.c + '×10^' + mm.e + '.', key:'g|mult|' + mc1 + '|' + me1 + '|' + mc2 + '|' + me2 }; } else if (kind2 === 1){ var rc1 = R(4, 9), re1 = R(1, 3), rc2 = R(4, 9), re2 = R(1, 3); var rm = sciMultiply(String(rc1), re1, String(rc2), re2); q = { prompt:'Simplify (' + sciHTML(String(rc1), re1) + ')(' + sciHTML(String(rc2), re2) + ').', answer:{ t:'sci', c:rm.c, e:rm.e }, tag:'multiply-renorm', explain:(rc1 * rc2) + '×10^' + (re1 + re2) + ' is not proper → hop once → ' + rm.c + '×10^' + rm.e + '.', key:'g|multr|' + rc1 + '|' + re1 + '|' + rc2 + '|' + re2 }; } else if (kind2 === 2){ var wc = dec1(rng); q = { prompt:'Write ' + wc + ' in scientific notation.', answer:{ t:'sci', c:wc, e:0 }, tag:'convert', explain:wc + ' is already proper: ' + wc + '×10^0.', key:'g|s2sdec|' + wc }; } else { var bank = ['9460000000000', '150000000', '0.0000007', '0.0000000001']; var wstd = bank[Math.floor(rng() * bank.length)]; var ws = stdToSci(wstd); q = { prompt:'Write ' + fmtInt(wstd) + ' in scientific notation.', answer:{ t:'sci', c:ws.c, e:ws.e }, tag:'word', explain:'Count the hops: ' + fmtInt(wstd) + ' → ' + ws.c + '×10^' + ws.e + '.', key:'g|word|' + wstd }; } } return q; } function buildDeck(rng){ var used = {}, deck = [], i, t; var tiers = []; for (i = 0; i < 8; i++) tiers.push(0); for (i = 0; i < 8; i++) tiers.push(1); for (i = 0; i < 8; i++) tiers.push(2); for (i = 0; i < tiers.length; i++){ t = tiers[i]; var guard = 0, q; do { q = gQ(t, rng); guard++; } while (!(!used[q.key] || guard >= 60)); used[q.key] = 1; q.tier = t; deck.push(q); } function shufRange(a, b){ for (var i2 = b - 1; i2 > a; i2--){ var j2 = a + Math.floor(rng() * (i2 - a + 1)); var tmp = deck[i2]; deck[i2] = deck[j2]; deck[j2] = tmp; } } shufRange(0, 8); shufRange(8, 16); shufRange(16, 24); return deck; } function scoreForAnswer(correct, streakAfter){ if (!correct) return 0; return 10 + (streakAfter >= 4 ? (streakAfter - 3) * 2 : 0); } var TIER_NAMES = ['CONVERT', 'FLIP & COMPARE', 'POWER PLAY']; var PRACTICE_TIPS = { 'convert':'Converting — count the hops; big numbers hop left (positive exponent).', 'standard-form':'Standard form — positive exponent: hop right and grow.', 'zero-exponent':'Zero exponent — 10^0 = 1, so nothing moves.', 'convert-small':'Small decimals — hop right; the exponent is negative.', 'standard-form-small':'Small results — negative exponent gives leading zeros.', 'compare':'Comparing — the bigger exponent wins, almost always.', 'hops':'Hop count — the exponent IS the number of hops; the sign is just direction.', 'multiply':'Multiplying — multiply coefficients, add exponents.', 'multiply-renorm':'Renormalizing — if the coefficient hits 10, hop once more.', 'word':'Word problems — strip the story, count the hops.' }; /* ================= END PURE GAME LOGIC ================= */
/* ---------------- UI ---------------- */
var G = null;
function el(id){ return document.getElementById(id); }
function heartSVG(lost){
var fill = lost ? '#E2E8F0' : '#EF4444';
return '';
}
function renderHearts(){
var h = '';
for (var i = 0; i < 3; i++) h += heartSVG(i >= G.lives);
el('livesVal').innerHTML = h;
el('livesVal').setAttribute('aria-label', G.lives + ' lives remaining');
}
function updateHUD(){
el('timerVal').textContent = Math.ceil(G.timeLeft);
el('scoreVal').textContent = G.score;
el('streakVal').textContent = G.streak;
el('bestStreakHud').textContent = 'Best streak ' + G.best;
var box = el('timerBox');
if (G.timeLeft <= 10){ box.classList.add('danger'); } else { box.classList.remove('danger'); }
}
function showScreen(id){
var screens = ['startScreen', 'gameScreen', 'overScreen'];
for (var i = 0; i < screens.length; i++){
el(screens[i]).classList.toggle('show', screens[i] === id);
}
}
function setFeedback(html, cls){
var f = el('feedback');
f.innerHTML = html;
f.className = 'feedback show ' + (cls || '');
}
function nextQuestion(){
if (G.over) return;
if (G.qi >= G.deck.length){ endGame('deck'); return; }
G.q = G.deck[G.qi];
G.qi++;
G.locked = false;
el('qType').textContent = TIER_NAMES[G.q.tier];
el('qCount').textContent = 'Question ' + G.qi + ' of ' + G.deck.length;
el('qPrompt').innerHTML = G.q.prompt;
el('answerInput').value = '';
setFeedback('', '');
el('feedback').className = 'feedback';
updateHUD();
renderHearts();
el('answerInput').focus();
}
function submitAnswer(){
if (!G || G.over || G.locked) return;
G.locked = true;
var q = G.q;
var raw = el('answerInput').value;
var parsed = parseSciInput(raw);
var ok = checkSciAnswer(q.answer, raw);
G.answered++;
if (ok){
G.streak++;
G.best = Math.max(G.best, G.streak);
var pts = scoreForAnswer(true, G.streak);
G.score += pts;
G.correct++;
setFeedback('Correct. +' + pts + ' points. ' + escHTML(q.explain), 'good');
} else {
G.streak = 0;
G.lives--;
G.slow.push(q.tag);
var why = parsed ? 'Not ' + escHTML(raw) + '.' : 'That was not a readable answer.';
setFeedback('' + why + ' ' + escHTML(q.explain) + ' (Correct: ' + escHTML(sciAnsText(q.answer)) + ')', 'bad');
if (G.lives <= 0){
updateHUD(); renderHearts();
setTimeout(function(){ endGame('lives'); }, 1400);
return;
}
}
updateHUD(); renderHearts();
setTimeout(function(){ if (!G.over) nextQuestion(); }, 1500);
}
function practiceItems(){
var counts = {}, i;
for (i = 0; i < G.slow.length; i++){ counts[G.slow[i]] = (counts[G.slow[i]] || 0) + 1; }
var tags = Object.keys(counts).sort(function(a, b){ return counts[b] - counts[a]; });
var items = [];
for (i = 0; !(i >= tags.length || items.length >= 3); i++){
if (PRACTICE_TIPS[tags[i]]) items.push(PRACTICE_TIPS[tags[i]]);
}
while (items.length < 3) items.push('Speed check: name the rule out loud before you type — multiply/add, divide/subtract, power/multiply.');
return items.slice(0, 3);
}
function endGame(reason){
if (!G || G.over) return;
G.over = true;
if (G.clock) clearInterval(G.clock);
var acc = G.answered ? Math.round(G.correct / G.answered * 100) : 0;
el('overTitle').textContent = reason === 'lives' ? 'Out of lives' : 'Sprint complete';
el('overStats').innerHTML =
'
' + '
' + '
' + '
'; var tips = practiceItems(); el('overPractice').innerHTML = '3 things to practice (your trickiest):
- ' +
tips.map(function(x){ return '
- ' + x + '
'; }).join('') + '
'; showScreen('overScreen'); } function startGame(){ var rng = mulberry32((Date.now() % 2147483647) || 7); G = { deck:buildDeck(rng), qi:0, q:null, score:0, streak:0, best:0, correct:0, answered:0, lives:3, timeLeft:60, over:false, locked:false, slow:[] }; showScreen('gameScreen'); if (G.clock) clearInterval(G.clock); G.clock = setInterval(function(){ if (!G || G.over) return; G.timeLeft -= 0.25; var fill = el('timeFill'); if (fill) fill.style.width = Math.max(0, G.timeLeft / 60 * 100) + '%'; if (G.timeLeft <= 0){ G.timeLeft = 0; updateHUD(); endGame('time'); return; } updateHUD(); }, 250); updateHUD(); nextQuestion(); } function initGame(){ el('startBtn').addEventListener('click', startGame); el('againBtn').addEventListener('click', startGame); el('answerForm').addEventListener('submit', function(e){ e.preventDefault(); submitAnswer(); }); } if (!(typeof document === 'undefined' || !document.addEventListener)){ document.addEventListener('DOMContentLoaded', initGame); }