Ratio Sprint
How fast can you work with trig ratios? 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 is a trig ratio problem:
find sin, cos, or tan from two sides, solve a side from a special angle (30°, 45°, 60°),
or find an angle with inverse trig.
Type a fraction like 5/13, a radical like 4sqrt3, or a decimal like 9.40.
For angles, type the degrees (the ° sign is optional).
- 1A correct answer scores +10 points. Streaks of 4 or more earn a bonus of +2 per streak level beyond 3.
- 2A wrong answer costs 1 life, resets your streak, and shows a short explanation.
- 3The game ends when the timer hits 0 or you lose all 3 lives. Questions ramp from simple ratios to special angles, inverse trig, and word problems.
Best streak 0
5/13) or decimals. Radical answers accept sqrt48, 4sqrt3, or the decimal. Angles: nearest hundredth of a degree.'use strict'; /* === PURE FUNCTIONS (extracted by tests/test_game.js; keep DOM-free) === */ function trMulberry(seed){ var a = seed >>> 0; 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 trRandInt(rng, lo, hi){ return lo + Math.floor(rng() * (hi - lo + 1)); } function trPick(rng, arr){ return arr[Math.floor(rng() * arr.length)]; } function trDifficulty(qIndex){ if (qIndex < 5) return 0; if (qIndex < 15) return 1; return 2; } function trD2r(d){ return d * Math.PI / 180; } function trR2(v){ return Math.round(v * 100) / 100; } /* numeric value of a sqrt token like 'sqrt48', '\u221A48', '4sqrt3' | null */ function trSqrtVal(s){ var m = s.match(/^(\d*\.?\d*)\s*(?:sqrt|\u221A)\s*(\d+(?:\.\d+)?)$/); if (!m) return null; var coeff = m[1] === '' ? 1 : parseFloat(m[1]); var n = parseFloat(m[2]); if (!(coeff > 0) || !(n > 0)) return null; return coeff * Math.sqrt(n); } /* parse answer -> number | null. Fractions, radicals, decimals, degrees. */ function trParseGame(raw){ if (raw == null) return null; var s = String(raw).trim().replace(/,/g, '').toLowerCase(); if (s === '') return null; s = s.replace(/\u00B0$/, '').trim(); var v = trSqrtVal(s); if (v !== null) return v; var m = s.match(/^(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/); if (m){ var den = parseFloat(m[2]); if (!(den > 0)) return null; return parseFloat(m[1]) / den; } if (/^\d*\.?\d+$/.test(s)){ v = parseFloat(s); if (!isFinite(v)) return null; return v; } return null; } /* correct: {v:number, deg:bool}. Angles get +-0.051, others +-0.011. */ function trGameAnswerOK(correct, parsed){ if (parsed === null) return false; var tol = correct.deg ? 0.051 : 0.011; return Math.abs(parsed - correct.v) <= tol; } /* scoring: +10 base, streak>=4 earns +2 per level beyond 3 */ function trPoints(streakAfter){ var pts = 10; if (streakAfter >= 4) pts += 2 * (streakAfter - 3); return pts; } var TR_TRIPLES = [[3,4,5],[5,12,13],[8,15,17],[7,24,25],[6,8,10],[9,12,15]]; function trBuildQuestion(rng, level){ var q; if (level === 0){ var t = trPick(rng, TR_TRIPLES); var which = trPick(rng, ['sin', 'cos', 'tan']); var num, den, label; if (which === 'sin'){ num = t[0]; den = t[2]; label = 'the side opposite \u03B8 is ' + t[0] + ' and the hypotenuse is ' + t[2]; } else if (which === 'cos'){ num = t[1]; den = t[2]; label = 'the side adjacent to \u03B8 is ' + t[1] + ' and the hypotenuse is ' + t[2]; } else { num = t[0]; den = t[1]; label = 'the side opposite \u03B8 is ' + t[0] + ' and the side adjacent to \u03B8 is ' + t[1]; } q = { type: which === 'sin' ? 'Sine' : which === 'cos' ? 'Cosine' : 'Tangent', prompt: 'In a right triangle, ' + label + '. Find ' + which + ' \u03B8 (fraction or decimal).', correct: { v: num / den, deg: false }, correctText: num + '/' + den, why: which + ' \u03B8 = ' + num + '/' + den + ' \u2248 ' + trR2(num / den) + '.', key: which + '|' + t[0] + '|' + t[1] + '|' + t[2] }; } else if (level === 1){ var t1 = trPick(rng, ['ratio', 'side30', 'side45', 'side60', 'ladder']); if (t1 === 'ratio'){ var tt = trPick(rng, TR_TRIPLES); var w = trPick(rng, ['sin', 'cos', 'tan']); var n2, d2; if (w === 'sin'){ n2 = tt[0]; d2 = tt[2]; } else if (w === 'cos'){ n2 = tt[1]; d2 = tt[2]; } else { n2 = tt[0]; d2 = tt[1]; } q = { type: 'Ratio', prompt: 'Find ' + w + ' \u03B8 for a ' + tt[0] + '-' + tt[1] + '-' + tt[2] + ' triangle (\u03B8 opposite the ' + tt[0] + ' side).', correct: { v: n2 / d2, deg: false }, correctText: n2 + '/' + d2, why: w + ' \u03B8 = ' + n2 + '/' + d2 + ' \u2248 ' + trR2(n2 / d2) + '.', key: 'r|' + w + '|' + tt[0] + '|' + tt[1] + '|' + tt[2] }; } else if (t1 === 'side30'){ var adj = trPick(rng, [12, 9, 15, 18, 21]); var v30 = adj * Math.tan(trD2r(30)); q = { type: 'Special angle', prompt: 'tan 30\u00B0 = x/' + adj + '. Find x (nearest hundredth).', correct: { v: trR2(v30), deg: false }, correctText: String(trR2(v30)), why: 'x = ' + adj + ' \u00D7 tan 30\u00B0 \u2248 ' + trR2(v30) + '.', key: 's30|' + adj }; } else if (t1 === 'side45'){ var hyp = trPick(rng, [10, 8, 12, 14, 16]); var v45 = hyp * Math.sin(trD2r(45)); q = { type: 'Special angle', prompt: 'sin 45\u00B0 = x/' + hyp + '. Find x (nearest hundredth).', correct: { v: trR2(v45), deg: false }, correctText: String(trR2(v45)), why: 'x = ' + hyp + ' \u00D7 sin 45\u00B0 \u2248 ' + trR2(v45) + '.', key: 's45|' + hyp }; } else if (t1 === 'side60'){ var a60 = trRandInt(rng, 4, 9); q = { type: 'Special angle', prompt: 'cos 60\u00B0 = ' + a60 + '/h. Find the hypotenuse h.', correct: { v: a60 * 2, deg: false }, correctText: String(a60 * 2), why: 'cos 60\u00B0 = 1/2, so h = ' + (a60 * 2) + '.', key: 's60|' + a60 }; } else { var L = trRandInt(rng, 10, 16), a = trPick(rng, [60, 65, 70]); var h = trR2(L * Math.sin(trD2r(a))); q = { type: 'Word problem', prompt: 'A ' + L + '-ft ladder makes a ' + a + '\u00B0 angle with the ground. How high does it reach (ft, nearest hundredth)?', correct: { v: h, deg: false }, correctText: String(h), why: 'h = ' + L + ' \u00D7 sin ' + a + '\u00B0 \u2248 ' + h + ' ft.', key: 'lad|' + L + '|' + a }; } } else { var t2 = trPick(rng, ['angle', 'shadow', 'side30x']); if (t2 === 'angle'){ var ta = trPick(rng, [[8,17],[5,13],[7,25],[9,41],[12,13],[20,29],[15,17],[7,24]]); var dg = trR2(Math.acos(ta[0] / ta[1]) * 180 / Math.PI); q = { type: 'Find the angle', prompt: 'cos \u03B8 = ' + ta[0] + '/' + ta[1] + '. Find \u03B8 in degrees (nearest hundredth).', correct: { v: dg, deg: true }, correctText: String(dg), why: '\u03B8 = cos\u207B\u00B9(' + ta[0] + '/' + ta[1] + ') \u2248 ' + dg + '\u00B0.', key: 'ang|' + ta[0] + '|' + ta[1] }; } else if (t2 === 'shadow'){ var b = trRandInt(rng, 35, 65), sa = trPick(rng, [25, 30, 35, 40, 45]); var sh = trR2(b / Math.tan(trD2r(sa))); q = { type: 'Word problem', prompt: 'A ' + b + '-ft building casts a shadow when the sun\u2019s angle of elevation is ' + sa + '\u00B0. Shadow length (ft, nearest hundredth)?', correct: { v: sh, deg: false }, correctText: String(sh), why: 'shadow = ' + b + ' \u00F7 tan ' + sa + '\u00B0 \u2248 ' + sh + ' ft.', key: 'sh|' + b + '|' + sa }; } else { var adj2 = trPick(rng, [12, 9, 15, 18, 21, 24]); var v30b = trR2(adj2 * Math.tan(trD2r(30))); q = { type: 'Special angle', prompt: 'tan 30\u00B0 = x/' + adj2 + '. Find x (nearest hundredth).', correct: { v: v30b, deg: false }, correctText: String(v30b), why: 'x = ' + adj2 + ' \u00D7 tan 30\u00B0 \u2248 ' + v30b + '.', key: 's30b|' + adj2 }; } } return q; } function trGenQuestion(rng, asked, qIndex){ var level = trDifficulty(qIndex); var q = null, guard = 0; do { q = trBuildQuestion(rng, level); guard++; } while (!( !asked.has(q.key) || !(guard < 60) )); asked.add(q.key); return q; } /* === END PURE FUNCTIONS === */ /* ---------- DOM wiring ---------- */ (function(){ 'use strict'; var startScreen = document.getElementById('startScreen'); var gameScreen = document.getElementById('gameScreen'); var overScreen = document.getElementById('overScreen'); var timerVal = document.getElementById('timerVal'); var timerBox = document.getElementById('timerBox'); var scoreVal = document.getElementById('scoreVal'); var streakVal = document.getElementById('streakVal'); var livesVal = document.getElementById('livesVal'); var qCount = document.getElementById('qCount'); var timeFill = document.getElementById('timeFill'); var bestStreakHud = document.getElementById('bestStreakHud'); var qType = document.getElementById('qType'); var qPrompt = document.getElementById('qPrompt'); var answerForm = document.getElementById('answerForm'); var answerInput = document.getElementById('answerInput'); var feedback = document.getElementById('feedback'); var HEART_SVG = '';
var G = null; function newGame(){ G = { rng: trMulberry((Date.now() % 2000000000) + 13), asked: new Set(), qIndex: 0, score: 0, streak: 0, best: 0, lives: 3, answered: 0, correctCount: 0, timeLeft: 60, timerId: null, current: null, over: false }; } function renderHearts(){ var html = ''; for (var i = 0; i < 3; i++){ html += '' + HEART_SVG.replace('class="heart"', 'class="heart' + (i < G.lives ? '' : ' lost') + '"') + ''; } livesVal.innerHTML = html; livesVal.setAttribute('aria-label', G.lives + ' lives remaining'); } function renderHud(){ timerVal.textContent = G.timeLeft; scoreVal.textContent = G.score; streakVal.textContent = G.streak; bestStreakHud.textContent = 'Best streak ' + G.best; qCount.textContent = 'Question ' + (G.qIndex + 1); timeFill.style.width = (100 * (60 - G.timeLeft) / 60) + '%'; timerBox.classList.toggle('timer-low', G.timeLeft <= 10); renderHearts(); } function nextQuestion(){ G.current = trGenQuestion(G.rng, G.asked, G.qIndex); qType.textContent = G.current.type; qPrompt.textContent = G.current.prompt; answerInput.value = ''; answerInput.disabled = false; feedback.className = 'feedback'; feedback.innerHTML = ''; renderHud(); answerInput.focus(); } function showScreen(which){ startScreen.classList.toggle('show', which === 'start'); gameScreen.classList.toggle('show', which === 'game'); overScreen.classList.toggle('show', which === 'over'); } function endGame(reason){ if (G.over) return; G.over = true; clearInterval(G.timerId); document.getElementById('overTitle').textContent = reason === 'lives' ? 'Out of lives!' : 'Time\u2019s up!'; document.getElementById('overReason').textContent = reason === 'lives' ? 'You lost all 3 lives. Trig ratios reward careful labeling — review SOHCAHTOA and try again.' : 'You survived the full 60 seconds. Nice work.'; document.getElementById('finalScore').textContent = G.score; document.getElementById('finalBest').textContent = G.best; document.getElementById('finalAnswered').textContent = G.answered; document.getElementById('finalAcc').textContent = G.answered === 0 ? '0%' : Math.round(100 * G.correctCount / G.answered) + '%'; showScreen('over'); } function startGame(){ newGame(); showScreen('game'); nextQuestion(); G.timerId = setInterval(function(){ G.timeLeft--; if (G.timeLeft <= 0){ renderHud(); endGame('time'); return; } renderHud(); }, 1000); } answerForm.addEventListener('submit', function(e){ e.preventDefault(); if (!G || G.over) return; var parsed = trParseGame(answerInput.value); G.answered++; if (trGameAnswerOK(G.current.correct, parsed)){ G.correctCount++; G.streak++; if (G.streak > G.best) G.best = G.streak; var pts = trPoints(G.streak); G.score += pts; feedback.className = 'feedback show correct'; feedback.innerHTML = '
' + '
'; G.qIndex++; answerInput.disabled = true; setTimeout(function(){ if (!G.over) nextQuestion(); }, 900); } else { G.lives--; G.streak = 0; if (parsed === null){ feedback.className = 'feedback show wrong'; feedback.innerHTML = '
' + '
'; G.answered--; G.lives++; return; } feedback.className = 'feedback show wrong'; feedback.innerHTML = '
' + '
'; renderHud(); if (G.lives <= 0){ endGame('lives'); return; } G.qIndex++; answerInput.disabled = true; setTimeout(function(){ if (!G.over) nextQuestion(); }, 1600); } renderHud(); }); document.getElementById('startBtn').addEventListener('click', startGame); document.getElementById('againBtn').addEventListener('click', startGame); })();