Skill: factoring-trinomials-a1
Trinomial Sprint
60 seconds, 3 lives. Find the two numbers and factor at speed.
How to play
You get 60 seconds and 3 lives. Find number pairs, factor trinomials, name sign patterns, and solve by zero product. Type factored forms like (x+3)(x+4), pairs like 3, 4, roots like 1, 3.
- 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 the correct answer with a short explanation.
- 3Questions ramp up: the first 6 are easy, the next 8 are medium, then it is all hard. The game ends at 0 seconds or 0 lives.
/* ============================================================================ FiboPoly — polynomial / rational-expression math core for FiboTutors skill ecosystems. Pure JavaScript, no DOM access. Free of the double-ampersand operator by construction (nested ifs / De Morgan). Exposed as FiboPoly (browser) or module.exports (node). ============================================================================ */ (function (root, factory) { 'use strict'; var api = factory(); if (typeof module === 'undefined') { root.FiboPoly = api; } else if (module.exports) { module.exports = api; } else { root.FiboPoly = api; } })(typeof self !== 'undefined' ? self : this, function () { 'use strict';
/* ================= integers ================= */ function igcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { var t = a % b; a = b; b = t; } return a || 1; }
/* ================= fractions {n,d}, d > 0, reduced ================= */ function fred(n, d) { if (d === 0) throw new Error('fred: zero denominator'); if (d < 0) { n = -n; d = -d; } if (n === 0) return { n: 0, d: 1 }; var g = igcd(n, d); return { n: n / g, d: d / g }; } function fi(k) { return { n: k, d: 1 }; } function fadd(a, b) { return fred(a.n * b.d + b.n * a.d, a.d * b.d); } function fsub(a, b) { return fred(a.n * b.d - b.n * a.d, a.d * b.d); } function fmul(a, b) { return fred(a.n * b.n, a.d * b.d); } function fdiv(a, b) { if (b.n === 0) throw new Error('fdiv: division by zero'); return fred(a.n * b.d, a.d * b.n); } function fneg(a) { return { n: -a.n, d: a.d }; } function feq(a, b) { return !(a.n !== b.n || a.d !== b.d); } function fis0(a) { return a.n === 0; } function ffmt(f) { return f.d === 1 ? String(f.n) : f.n + '/' + f.d; } /* parse "5", "-3", "3/4", "0.75" (also unicode minus) -> fraction or null */ function fparse(str) { var s = String(str).replace(/−/g, '-').replace(/\s+/g, ''); if (!s) return null; var m = /^([+-]?\d+)\/([+-]?\d+)$/.exec(s); if (m) { var d = parseInt(m[2], 10); if (d === 0) return null; return fred(parseInt(m[1], 10), d); } m = /^([+-]?(?:\d+\.?\d*|\.\d+))$/.exec(s); if (m) { var body = m[1].replace(/^[+-]/, ''); var dot = body.indexOf('.'); var places = dot < 0 ? 0 : body.length - dot - 1; if (places > 9) places = 9; var k = Math.pow(10, places); var num = Math.round(parseFloat(m[1]) * k); if (!isFinite(num)) return null; return fred(num, k); } return null; }
/* ================= polys: arrays of fractions, index = degree ================= */ function pnorm(p) { var q = p.slice(); while (q.length > 1) { if (!fis0(q[q.length - 1])) break; q.pop(); } return q; } function pzero() { return [{ n: 0, d: 1 }]; } function pconst(k) { return pnorm([fi(k)]); } function pconstF(f) { return pnorm([{ n: f.n, d: f.d }]); } function pvar() { return [{ n: 0, d: 1 }, { n: 1, d: 1 }]; } function pdeg(p) { return pnorm(p).length - 1; } function pis0(p) { var q = pnorm(p); if (q.length !== 1) return false; return fis0(q[0]); } function padd(a, b) { var n = Math.max(a.length, b.length), r = [], i; for (i = 0; i < n; i++) { var x = i < a.length ? a[i] : fi(0); var y = i < b.length ? b[i] : fi(0); r.push(fadd(x, y)); } return pnorm(r); } function psub(a, b) { var n = Math.max(a.length, b.length), r = [], i; for (i = 0; i < n; i++) { var x = i < a.length ? a[i] : fi(0); var y = i < b.length ? b[i] : fi(0); r.push(fsub(x, y)); } return pnorm(r); } function pmul(a, b) { var r = [], i, j; for (i = 0; i < a.length + b.length - 1; i++) r.push(fi(0)); for (i = 0; i < a.length; i++) { for (j = 0; j < b.length; j++) { r[i + j] = fadd(r[i + j], fmul(a[i], b[j])); } } return pnorm(r); } function pmulC(a, k) { return pnorm(a.map(function (c) { return fmul(c, k); })); } function ppow(p, e) { var r = pconst(1); for (var i = 0; i < e; i++) r = pmul(r, p); return r; } function peq(a, b) { a = pnorm(a); b = pnorm(b); if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) if (!feq(a[i], b[i])) return false; return true; } function peval(p, x) { var acc = fi(0); for (var i = p.length - 1; i >= 0; i--) acc = fadd(fmul(acc, x), p[i]); return acc; } /* synthetic division by (x - r), r a fraction. returns {q, rem} */ function pdivLin(p, r) { var a = pnorm(p).slice(), n = a.length - 1, i; if (n < 1) return { q: pzero(), rem: a[0] || fi(0) }; var q = new Array(n), carry = fi(0); for (i = n; i >= 1; i--) { var c = fadd(a[i], carry); q[i - 1] = c; carry = fmul(c, r); } return { q: pnorm(q), rem: fadd(a[0], carry) }; } /* scale to integer coefficients: returns {p:[ints], k} where p = k * orig */ function pscaleInt(p) { p = pnorm(p); var L = 1, i; for (i = 0; i < p.length; i++) { var d = p[i].d; L = (L / igcd(L, d)) * d; } var ints = p.map(function (c) { return (c.n * L) / c.d; }); return { p: ints, k: L }; } /* gcd of every integer coefficient of num and den (after scaling). Uses a zero-safe gcd so leading zeros do not corrupt the accumulation. */ function pgcdAll(numP, denP) { function g0(x, y) { x = Math.abs(x); y = Math.abs(y); while (y) { var t = x % y; x = y; y = t; } return x; } var a = pscaleInt(numP).p, b = pscaleInt(denP).p, g = 0, i; for (i = 0; i < a.length; i++) g = g0(g, a[i]); for (i = 0; i < b.length; i++) g = g0(g, b[i]); return g === 0 ? 1 : g; } /* ================= parsing ================= */ function tokenize(s) { var toks = [], i = 0, n = s.length, c, j; while (i < n) { c = s[i]; if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; } if (c === '+' || c === '-' || c === '*' || c === '^' || c === '(' || c === ')') { toks.push({ t: c }); i++; continue; } if (c === 'x' || c === 'X') { toks.push({ t: 'x' }); i++; continue; } var isDig = (c >= '0'); if (isDig) isDig = (c <= '9'); if (isDig || c === '.') { j = i; while (j < n) { var cj = s[j]; var jd = (cj >= '0'); if (jd) jd = (cj <= '9'); if (jd || cj === '.') { j++; continue; } break; } toks.push({ t: 'num', v: s.slice(i, j) }); i = j; continue; } return null; } return toks; } function decFrac(v) { var m = /^(\d*)\.?(\d*)$/.exec(v); if (!m) return null; var ip = m[1] || '0', fp = m[2] || ''; if (ip === '0') { if (fp === '') return { n: 0, d: 1 }; } if (!/^\d+$/.test(ip)) return null; if (fp !== '') { if (!/^\d+$/.test(fp)) return null; } var places = fp.length; var k = Math.pow(10, Math.min(places, 9)); var num; if (places > 9) { num = Math.round(parseFloat(v) * k); } else { num = parseInt(ip, 10) * k + (fp === '' ? 0 : parseInt(fp, 10)); } return fred(num, k); } /* parsePoly(str) -> Poly or null. Supports + - * ^ ( ) x, decimals, implicit multiplication (2x, 2(x+1), (x+1)(x+2), x(x+3)), x^2, unicode. */ function parsePoly(str) { var s = String(str).replace(/−/g, '-').replace(/²/g, '^2').replace(/³/g, '^3').replace(/×/g, '*'); var toks = tokenize(s); if (!toks || toks.length === 0) return null; var t2 = [], i; for (i = 0; i < toks.length; i++) { t2.push(toks[i]); if (i + 1 < toks.length) { var a = toks[i].t, b = toks[i + 1].t; var left = (a === 'num' || a === 'x' || a === ')'); var right = (b === 'num' || b === 'x' || b === '('); if (left) { if (right) { if (a !== 'num' || b !== 'num') t2.push({ t: '*' }); } } } } var pos = 0; function peek() { return pos < t2.length ? t2[pos] : null; } function parseIntExp() { var k = peek(); if (!k || k.t !== 'num') return null; if (!/^\d+$/.test(k.v)) return null; pos++; return parseInt(k.v, 10); } function parseExpr() { var p = parseTerm(); if (!p) return null; for (;;) { var k = peek(); if (!k) break; if (k.t === '+') { pos++; var q = parseTerm(); if (!q) return null; p = padd(p, q); } else if (k.t === '-') { pos++; var q2 = parseTerm(); if (!q2) return null; p = psub(p, q2); } else break; } return p; } function parseTerm() { var p = parseFactor(); if (!p) return null; for (;;) { var k = peek(); if (!k || k.t !== '*') break; pos++; var q = parseFactor(); if (!q) return null; p = pmul(p, q); } return p; } function parseFactor() { var neg = false, k = peek(); if (k) { if (k.t === '+' || k.t === '-') { neg = (k.t === '-'); pos++; } } k = peek(); if (!k) return null; var p = null; if (k.t === 'num') { pos++; var f = decFrac(k.v); if (!f) return null; p = [f]; var kk = peek(); if (kk) { if (kk.t === '^') { pos++; var e = parseIntExp(); if (e === null || e > 9) return null; p = ppow(p, e); } } } else if (k.t === 'x') { pos++; p = pvar(); var kk2 = peek(); if (kk2) { if (kk2.t === '^') { pos++; var e2 = parseIntExp(); if (e2 === null || e2 > 9) return null; p = ppow(pvar(), e2); } } } else if (k.t === '(') { pos++; p = parseExpr(); if (!p) return null; k = peek(); if (!k || k.t !== ')') return null; pos++; var kk3 = peek(); if (kk3) { if (kk3.t === '^') { pos++; var e3 = parseIntExp(); if (e3 === null || e3 > 4) return null; p = ppow(p, e3); } } } else { return null; } if (neg) p = pmulC(p, fi(-1)); return p; } var res = parseExpr(); if (!res || pos !== t2.length) return null; return pnorm(res); } /* parseRational(str) -> {num, den} or null. Splits on one top-level '/'. */ function parseRational(str) { var s = String(str).replace(/−/g, '-').replace(/²/g, '^2').replace(/³/g, '^3').replace(/×/g, '*'); var depth = 0, idx = -1, i; for (i = 0; i < s.length; i++) { var c = s[i]; if (c === '(') depth++; else if (c === ')') { depth--; if (depth < 0) return null; } else if (c === '/') { if (depth === 0) { if (idx >= 0) return null; idx = i; } } } if (depth !== 0) return null; var num, den; if (idx < 0) { num = parsePoly(s); den = pconst(1); } else { num = parsePoly(s.slice(0, idx)); den = parsePoly(s.slice(idx + 1)); } if (!num || !den) return null; if (pis0(den)) return null; return { num: num, den: den }; } /* parse a list of excluded values: "0, -1", "x≠0,-1", "x != 0" -> [frac] or null */ function parseDomainList(str) { var s = String(str).replace(/−/g, '-').replace(/≠/g, '').replace(/!/g, '') .replace(/=/g, '').replace(/x/gi, '').replace(/\s+/g, ''); if (!s) return null; var parts = s.split(/[,;]+/), out = [], i; for (i = 0; i < parts.length; i++) { if (parts[i] === '') continue; var f = fparse(parts[i]); if (!f) return null; out.push(f); } if (out.length === 0) return null; return out; } function domainSetEq(a, b) { if (a.length !== b.length) return false; var used = [], i, j; for (i = 0; i < a.length; i++) used.push(false); for (i = 0; i < a.length; i++) { var found = false; for (j = 0; j < b.length; j++) { if (!used[j]) { if (feq(a[i], b[j])) { used[j] = true; found = true; break; } } } if (!found) return false; } return true; } /* ================= formatting ================= */ function coefMagText(c) { var a = Math.abs(c.n); return c.d === 1 ? String(a) : a + '/' + c.d; } /* canonical plain text: "4x^2-5x-1" */ function pfmtText(p) { p = pnorm(p); if (pis0(p)) return '0'; var s = '', d; for (d = p.length - 1; d >= 0; d--) { var c = p[d]; if (fis0(c)) continue; var neg = c.n < 0, mag; var one = !(Math.abs(c.n) !== 1 || c.d !== 1); if (d === 0) mag = coefMagText(c); else if (d === 1) mag = one ? 'x' : coefMagText(c) + 'x'; else mag = one ? 'x^' + d : coefMagText(c) + 'x^' + d; if (s === '') s = (neg ? '-' : '') + mag; else s += (neg ? '-' : '+') + mag; } return s; } /* pretty HTML with exponents and proper minus signs */ function pfmtHTML(p) { p = pnorm(p); if (pis0(p)) return '0'; var s = '', d; for (d = p.length - 1; d >= 0; d--) { var c = p[d]; if (fis0(c)) continue; var neg = c.n < 0, mag; var one = !(Math.abs(c.n) !== 1 || c.d !== 1); if (d === 0) mag = coefMagText(c); else if (d === 1) mag = one ? 'x' : coefMagText(c) + 'x'; else mag = one ? 'x' + d + '' : coefMagText(c) + 'x' + d + ''; if (s === '') s = (neg ? '−' : '') + mag; else s += (neg ? ' − ' : ' + ') + mag; } return s; } /* factored-form HTML from factorQuad-style linear factors */ function linFactorHTML(d, e) { /* (dx + e) with integers d, e */ var s = '('; if (d === 1) s += 'x'; else if (d === -1) s += '−x'; else s += String(d).replace('-', '−') + 'x'; if (e > 0) s += ' + ' + e; else if (e < 0) s += ' − ' + Math.abs(e); return s + ')'; } function escHTML(s) { return String(s).replace(/\x26/g, '&').replace(//g, '>'); }
/* ================= factoring ax^2 + bx + c over integers ================= */ function divisors(n) { n = Math.abs(n); var d = [], i; for (i = 1; i * i <= n; i++) { if (n % i === 0) { d.push(i); if (i * i !== n) d.push(n / i); } } return d.sort(function (x, y) { return x - y; }); } /* factorQuad(a,b,c): integers. Returns a result object (see below). */ function factorQuad(a, b, c) { if (a === 0) return { ok: false, error: 'not quadratic (a = 0)' }; var neg = false; if (a < 0) { neg = true; a = -a; b = -b; c = -c; } var g = igcd(igcd(a, b), c); var A = a / g, B = b / g, C = c / g; var factors = []; var res = { ok: true, neg: neg, gcf: g, A: A, B: B, C: C, cZero: false, factors: factors }; if (C === 0) { res.cZero = true; if (B === 0) { if (A !== 1) factors.push(pconst(A)); factors.push(pvar()); factors.push(pvar()); res.bZero = true; } else { var g2 = igcd(A, B); var f1 = A / g2, g1 = B / g2; if (g2 > 1) factors.push(pconst(g2)); factors.push(pvar()); factors.push(pnorm([fi(g1), fi(f1)])); res.lin = { f: f1, g: g1 }; } } else { var da = divisors(A), dc = divisors(C), sol = null, i, j, k; var done = false; for (i = 0; i < da.length; i++) { if (done) break; var d = da[i], f = A / d; for (j = 0; j < dc.length; j++) { if (done) break; var e0 = dc[j], g0 = C / e0; var pairs = [[e0, g0], [-e0, -g0]]; for (k = 0; k < pairs.length; k++) { var e = pairs[k][0], gg = pairs[k][1]; if (d * gg + e * f === B) { sol = { d: d, e: e, f: f, g: gg }; done = true; break; } } } } if (!sol) { res.ok = false; res.error = 'does not factor over the integers'; return res; } res.sol = sol; res.hunt = { p: sol.d * sol.g, q: sol.e * sol.f }; factors.push(pnorm([fi(sol.e), fi(sol.d)])); factors.push(pnorm([fi(sol.g), fi(sol.f)])); } if (g > 1) factors.unshift(pconst(g)); if (neg) factors.unshift(pconst(-1)); return res; } /* verify a factorQuad result by expansion; origA,origB,origC are the ORIGINAL coeffs */ function factorQuadVerify(r, oa, ob, oc) { if (!r.ok) return false; var acc = pconst(1), i; for (i = 0; i < r.factors.length; i++) acc = pmul(acc, r.factors[i]); return peq(acc, pnorm([fi(oc), fi(ob), fi(oa)])); } /* ================= rational expressions ================= */ /* integer/rational roots of a poly (deg <= 2); {ok, roots:[frac]} or {ok:false, approx:[...]} */ /* exact rational roots of an integer-coefficient quadratic; {ok, roots:[frac]} or {ok:false, approx:[numbers]} */ function intRootsDeg2(c) { var roots = [], i; var r = factorQuad(c[2], c[1], c[0]); if (!r.ok) { var D = c[1] * c[1] - 4 * c[2] * c[0], approx = []; if (D >= 0) { var sq = Math.sqrt(D); approx.push((-c[1] + sq) / (2 * c[2])); approx.push((-c[1] - sq) / (2 * c[2])); } return { ok: false, approx: approx }; } for (i = 0; i < r.factors.length; i++) { var f = pnorm(r.factors[i]); if (f.length === 2) { if (!fis0(f[1])) roots.push(fdiv(fneg(f[0]), f[1])); } } return { ok: true, roots: roots }; } /* candidate rational roots p/q for an integer-coefficient array c (const term nonzero) */ function ratRootCandidates(c) { var lead = Math.abs(c[c.length - 1]), con = Math.abs(c[0]); var ps = divisors(con), qs = divisors(lead), out = [], seen = {}, i, j; for (i = 0; i < ps.length; i++) { for (j = 0; j < qs.length; j++) { var k1 = ps[i] + '/' + qs[j]; if (!seen[k1]) { seen[k1] = 1; out.push(fred(ps[i], qs[j])); } var k2 = (-ps[i]) + '/' + qs[j]; if (!seen[k2]) { seen[k2] = 1; out.push(fred(-ps[i], qs[j])); } } } return out; } function intRootsOfPoly(p) { var s = pscaleInt(p), c = s.p, roots = [], i; var deg = c.length - 1; if (deg <= 0) return { ok: true, roots: [] }; if (deg === 1) { if (c[1] === 0) return { ok: false }; return { ok: true, roots: [fred(-c[0], c[1])] }; } if (deg === 2) return intRootsDeg2(c); /* deg >= 3: peel off rational roots one by one, finish with the quadratic case */ var cc = c.map(fi), guard = 0; for (;;) { guard++; if (guard > 64) break; var dd = cc.length - 1; if (dd < 2) break; while (!(!(cc.length > 1) || !fis0(cc[0]))) { roots.push(fi(0)); cc = cc.slice(1); } dd = cc.length - 1; if (dd < 2) break; if (fis0(cc[dd])) break; var ci = pscaleInt(cc).p, found = null, cands = ratRootCandidates(ci), k; for (k = 0; k < cands.length; k++) { if (fis0(peval(cc, cands[k]))) { found = cands[k]; break; } } if (found === null) break; var dv = pdivLin(cc, found); if (!fis0(dv.rem)) break; roots.push(found); cc = dv.q; } var rest = cc.length - 1; if (rest === 2) { var q2 = intRootsDeg2(pscaleInt(cc).p); if (!q2.ok) return { ok: false, approx: q2.approx }; return { ok: true, roots: roots.concat(q2.roots) }; } if (rest === 1) { if (!fis0(cc[1])) roots.push(fdiv(fneg(cc[0]), cc[1])); return { ok: true, roots: roots }; } if (rest <= 0) return { ok: true, roots: roots }; return { ok: false }; } /* rationalSimplify(numP, denP) -> simplified form + domain. Domain roots are exact fractions when the denominator factors over rationals. */ function rationalSimplify(numP, denP) { if (pis0(denP)) return { ok: false, error: 'The denominator cannot be zero.' }; var dn = intRootsOfPoly(denP); if (!dn.ok) { return { ok: false, error: 'The denominator does not factor over the rationals.', approx: dn.approx || [] }; } var domain = dn.roots.slice(); var nq = pnorm(numP), dq = pnorm(denP), canceled = [], i; var seen = {}; for (i = 0; i < domain.length; i++) { var r = domain[i]; var key = r.n + '/' + r.d; if (seen[key]) continue; seen[key] = 1; for (;;) { var dvn = pdivLin(nq, r); if (!fis0(dvn.rem)) break; var dvd = pdivLin(dq, r); if (!fis0(dvd.rem)) break; nq = dvn.q; dq = dvd.q; canceled.push(r); } } var G = pgcdAll(nq, dq); if (G > 1) { nq = pnorm(nq.map(function (cc) { return fdiv(cc, fi(G)); })); dq = pnorm(dq.map(function (cc) { return fdiv(cc, fi(G)); })); } var lc = dq[dq.length - 1]; if (lc.n < 0) { nq = pmulC(nq, fi(-1)); dq = pmulC(dq, fi(-1)); } return { ok: true, num: nq, den: dq, canceled: canceled, domain: domain, constGcd: G, simplest: !(canceled.length !== 0 || G > 1) }; } /* (n1/d1) op (n2/d2); op in 'add','sub','mul','div'. Domain from ORIGINAL dens. */ function rationalOp(n1, d1, op, n2, d2) { var r1 = intRootsOfPoly(d1), r2 = intRootsOfPoly(d2); if (!r1.ok || !r2.ok) return { ok: false, error: 'A denominator does not factor over the rationals.' }; var domain = r1.roots.concat(r2.roots); var num, den; if (op === 'mul') { num = pmul(n1, n2); den = pmul(d1, d2); } else if (op === 'div') { if (pis0(n2)) return { ok: false, error: 'Cannot divide by zero.' }; var rn2 = intRootsOfPoly(n2); if (!rn2.ok) return { ok: false, error: 'The divisor does not factor over the rationals.' }; /* dividing by n2/d2 also excludes the zeros of n2 */ domain = domain.concat(rn2.roots); num = pmul(n1, d2); den = pmul(d1, n2); } else if (op === 'add') { num = padd(pmul(n1, d2), pmul(n2, d1)); den = pmul(d1, d2); } else if (op === 'sub') { num = psub(pmul(n1, d2), pmul(n2, d1)); den = pmul(d1, d2); } else return { ok: false, error: 'Unknown operation.' }; var s = rationalSimplify(num, den); if (!s.ok) return s; s.domain = domain; return s; } /* pretty domain text: "x ≠ −3" / "x ≠ 0, −1" */ function fmtDomain(domain) { if (!domain.length) return 'no excluded values'; var parts = domain.map(function (f) { return ffmt(f).replace('-', '−'); }); return 'x ≠ ' + parts.join(', '); } /* simplified rational as HTML: stacked fraction when needed */ function rationalHTML(num, den) { var denOne = (den.length === 1); if (denOne) denOne = feq(den[0], fi(1)); if (denOne) return pfmtHTML(num); return '' + pfmtHTML(num) + '' + pfmtHTML(den) + ''; } function rationalText(num, den) { var denOne = (den.length === 1); if (denOne) denOne = feq(den[0], fi(1)); if (denOne) return pfmtText(num); return '(' + pfmtText(num) + ')/(' + pfmtText(den) + ')'; }
/* ================= seeded RNG for generators ================= */ function mulberry32(seed) { var a = (seed >>> 0) || 1; 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 rint(rand, lo, hi) { return lo + Math.floor(rand() * (hi - lo + 1)); } function rpick(rand, arr) { return arr[Math.floor(rand() * arr.length)]; } function rpickNZ(rand, lo, hi) { var v = 0, guard = 0; while (v === 0) { if (guard >= 50) break; v = rint(rand, lo, hi); guard++; } return v === 0 ? 1 : v; } function rshuffle(rand, arr) { var a = arr.slice(), i, j, t; for (i = a.length - 1; i > 0; i--) { j = Math.floor(rand() * (i + 1)); t = a[i]; a[i] = a[j]; a[j] = t; } return a; }
return { igcd: igcd, fred: fred, fi: fi, fadd: fadd, fsub: fsub, fmul: fmul, fdiv: fdiv, fneg: fneg, feq: feq, fis0: fis0, ffmt: ffmt, fparse: fparse, pnorm: pnorm, pzero: pzero, pconst: pconst, pconstF: pconstF, pvar: pvar, pdeg: pdeg, pis0: pis0, padd: padd, psub: psub, pmul: pmul, pmulC: pmulC, ppow: ppow, peq: peq, peval: peval, pdivLin: pdivLin, pscaleInt: pscaleInt, pgcdAll: pgcdAll, parsePoly: parsePoly, parseRational: parseRational, parseDomainList: parseDomainList, domainSetEq: domainSetEq, pfmtText: pfmtText, pfmtHTML: pfmtHTML, linFactorHTML: linFactorHTML, escHTML: escHTML, divisors: divisors, factorQuad: factorQuad, factorQuadVerify: factorQuadVerify, intRootsOfPoly: intRootsOfPoly, rationalSimplify: rationalSimplify, rationalOp: rationalOp, fmtDomain: fmtDomain, rationalHTML: rationalHTML, rationalText: rationalText, mulberry32: mulberry32, rint: rint, rpick: rpick, rpickNZ: rpickNZ, rshuffle: rshuffle }; });
/*__GAME_TEMPLATES__*/ /* ============================================================================ GenUtil — small random-generation helpers for practice/worksheet/game. Expects global FiboPoly. double-ampersand-free by construction. ============================================================================ */ var GenUtil = (function () { 'use strict'; var FP = FiboPoly; function ri(rand, lo, hi) { return FP.rint(rand, lo, hi); } function rsign(rand) { return rand() < 0.5 ? -1 : 1; } function intPoly(coeffsAsc) { return FP.pnorm(coeffsAsc.map(function (k) { return FP.fi(k); })); } /* random integer-coefficient poly of exact degree deg, coeffs in [lo,hi] */ function randPoly(rand, deg, lo, hi) { var c = [], d; for (d = 0; d < deg; d++) c.push(ri(rand, lo, hi)); c.push(FP.rpickNZ(rand, lo, hi)); return intPoly(c); } /* factor pairs of n (n != 0): [[d, n/d], ...] over positive divisors */ function factorPairs(n) { var out = [], ds = FP.divisors(n), i; for (i = 0; i < ds.length; i++) out.push([ds[i], n / ds[i]]); return out; } /* random nonzero monomial k*x^d, k in [lo,hi], d in 1..2 */ function randMono(rand, lo, hi) { var k = FP.rpickNZ(rand, lo, hi), d = ri(rand, 1, 2), c = [], i; for (i = 0; i < d; i++) c.push(FP.fi(0)); c.push(FP.fi(k)); return FP.pnorm(c); } /* random binomial a*x + b; monicX -> a = +/-1 */ function randBinom(rand, lo, hi, monicX) { var a = monicX ? rsign(rand) : FP.rpickNZ(rand, lo, hi); var b = FP.rpickNZ(rand, lo, hi); return FP.pnorm([FP.fi(b), FP.fi(a)]); } return { ri: ri, rsign: rsign, intPoly: intPoly, randPoly: randPoly, factorPairs: factorPairs, randMono: randMono, randBinom: randBinom }; })(); if (typeof module === 'undefined') { } else { if (module.exports) module.exports = GenUtil; }
/* ============================================================================ Practice engine — shared across polynomial-ecosystem practice pages. Pure JavaScript, no DOM. Expects global FiboPoly (inlined earlier). double-ampersand-free by construction. ============================================================================ */ var PolyPractice = (function () { 'use strict'; var FP = FiboPoly;
/* ---------------- answer checkers ---------------- */ function normStr(s) { return String(s).toLowerCase().replace(/−/g, '-').replace(/\s+/g, ''); }
function checkPolyAnswer(expected, raw, requireParens) { var s = String(raw === undefined || raw === null ? '' : raw); if (requireParens) { if (s.indexOf('(') < 0) { return { status: 'invalid', msg: 'Write your answer in factored form with parentheses, like (x+2)(x+3).' }; } } var p = FP.parsePoly(s); if (!p) { return { status: 'invalid', msg: 'Could not read that as a polynomial. Use x for the variable and ^ for powers, like 4x^2-5x-1.' }; } if (FP.peq(p, expected)) return { status: 'ok' }; return { status: 'wrong' }; } function checkTextAnswer(expected, raw) { var a = normStr(raw); if (!a) return { status: 'invalid', msg: 'Type an answer first.' }; if (a === normStr(expected)) return { status: 'ok' }; return { status: 'wrong' }; } /* expected: simplified num/den polys + domain fraction array */ function checkRationalAnswer(expNum, expDen, expDomain, rawExpr, rawDom) { var r = FP.parseRational(String(rawExpr === undefined || rawExpr === null ? '' : rawExpr)); if (!r) { return { status: 'invalid', msg: 'Could not read the expression. Write it like x-3, (x+3)/2, or 3x/(x-1).' }; } var s = FP.rationalSimplify(r.num, r.den); if (!s.ok) return { status: 'invalid', msg: s.error }; if (!s.simplest) { return { status: 'wrong', msg: 'not-simplest' }; } var dom = FP.parseDomainList(String(rawDom === undefined || rawDom === null ? '' : rawDom)); if (!dom) { return { status: 'invalid', msg: 'List the excluded values separated by commas, like 0, -1. (They come from the ORIGINAL denominator.)' }; } var exprOk = FP.peq(s.num, expNum); if (exprOk) exprOk = FP.peq(s.den, expDen); var domOk = FP.domainSetEq(dom, expDomain); if (exprOk) { if (domOk) return { status: 'ok' }; } if (!exprOk) { if (!domOk) return { status: 'wrong', msg: 'both' }; return { status: 'wrong', msg: 'expr' }; } return { status: 'wrong', msg: 'domain' }; } function wrongFeedback(msg) { if (msg === 'not-simplest') { return 'That is equivalent, but it is not fully simplified — factor the top and bottom, cancel the common factor, and try again.'; } if (msg === 'expr') { return 'Your excluded values look right, but the simplified expression is not quite right yet.'; } if (msg === 'domain') { return 'The simplified expression is right, but the excluded values are not — they come from the ORIGINAL denominator, before anything cancels.'; } return 'Not quite — check both the simplified form and the excluded values.'; }
/* ---------------- problem finishing ---------------- */ function finishProblem(o) { /* o: {tag, kind, promptHTML, answerDisplayHTML, acceptText, hints[3], whySteps[], check} */ var why = '
- ' +
o.whySteps.map(function (s) { return '
- ' + s + '
'; }).join('') + '
' + '
' + '
'; return { tag: o.tag, kind: o.kind, promptHTML: o.promptHTML, hints: o.hints, whyHTML: why, answerDisplayHTML: o.answerDisplayHTML, acceptText: o.acceptText, check: o.check, key: o.key }; }
/* ---------------- session: scoring, streaks, no-repeat ---------------- */
function PracticeSession(seed, templates) {
this._rand = FP.mulberry32(seed >>> 0);
this._templates = templates;
this.usedKeys = {};
this.score = 0;
this.streak = 0;
this.problemNum = 0;
this.current = null;
this.wrongCount = 0;
this.revealed = false;
this._done = false;
}
PracticeSession.prototype._draw = function () {
var guard, t, pr;
for (guard = 0; guard < 150; guard++) {
t = this._templates[Math.floor(this._rand() * this._templates.length)];
pr = t.gen(this._rand);
if (this.usedKeys[pr.key]) continue;
this.usedKeys[pr.key] = 1;
pr.templateId = t.id;
pr.tag = t.tag;
return pr;
}
this.usedKeys = {};
t = this._templates[0];
pr = t.gen(this._rand);
pr.templateId = t.id;
pr.tag = t.tag;
return pr;
};
PracticeSession.prototype.next = function () {
this.current = this._draw();
this.wrongCount = 0;
this.revealed = false;
this._done = false;
this.problemNum++;
return this.current;
};
PracticeSession.prototype.submit = function (raw1, raw2) {
var pr = this.current;
if (!pr) return { status: 'none' };
if (this._done) return { status: 'already' };
var chk = pr.check(raw1, raw2);
if (chk.status === 'invalid') return { status: 'invalid', msg: chk.msg };
if (chk.status === 'ok') {
this._done = true;
var firstTry = (this.wrongCount === 0);
if (firstTry) firstTry = !this.revealed;
if (!this.revealed) {
this.streak++;
if (firstTry) this.score++;
}
return { status: 'correct', firstTry: firstTry, score: this.score, streak: this.streak, answerHTML: pr.answerDisplayHTML };
}
this.wrongCount++;
this.streak = 0;
if (this.wrongCount === 1) return { status: 'hint', hintHTML: pr.hints[0], streak: 0 };
if (this.wrongCount === 2) return { status: 'hint', hintHTML: pr.hints[1], streak: 0 };
this.revealed = true;
var fb = chk.msg ? wrongFeedback(chk.msg) : null;
return { status: 'why', whyHTML: pr.whyHTML, streak: 0, feedback: fb };
};
PracticeSession.prototype.reveal = function () {
if (!this.current) return null;
this.revealed = true;
return this.current.whyHTML;
};
return {
PracticeSession: PracticeSession,
finishProblem: finishProblem,
checkPolyAnswer: checkPolyAnswer,
checkTextAnswer: checkTextAnswer,
checkRationalAnswer: checkRationalAnswer,
wrongFeedback: wrongFeedback,
normStr: normStr
};
})();
if (typeof module === 'undefined') { }
else { if (module.exports) module.exports = PolyPractice; }
/* ============================================================================
W2Check — extra answer checkers for wave-2 practice/game templates.
Pure JavaScript, no DOM. Expects globals FiboPoly (FP) and Alg2.
Double-ampersand-free by construction.
============================================================================ */
var W2Check = (function () {
'use strict';
var FP = (typeof FiboPoly !== 'undefined') ? FiboPoly : null;
function bad(msg){ return { status: 'invalid', msg: msg }; }
/* numeric answer: expected fraction {n,d}; accepts int, a/b, decimal, mixed */
function checkNum(exp, raw){
var s = String(raw === undefined || raw === null ? '' : raw);
var f = FP.fparse(s);
if (!f) return bad('Could not read that as a number. Try an integer, a fraction like 3/2, or a decimal.');
if (FP.feq(f, exp)) return { status: 'ok' };
return { status: 'wrong' };
}
/* parse a comma/space separated list of numbers -> [frac] or null */
function parseNumList(raw){
var s = String(raw === undefined || raw === null ? '' : raw).replace(/−/g, '-');
var parts = s.split(/[,;]+/);
var out = [], i;
for (i = 0; i < parts.length; i++){
var t = parts[i].trim();
if (!t) continue;
/* allow "x = 2" or "x=2" style */
var m = /^[a-zA-Z]\s*=\s*(.+)$/.exec(t);
if (m) t = m[1].trim();
var f = FP.fparse(t);
if (!f) return null;
out.push(f);
}
return out.length ? out : null;
}
/* root set: expected array of fractions; order-insensitive, duplicates matter */
function checkRoots(expRoots, raw){
var got = parseNumList(raw);
if (!got) return bad('List the solutions separated by commas, like 2, -3.');
if (got.length !== expRoots.length) return { status: 'wrong' };
var used = [], i, j;
for (i = 0; i < expRoots.length; i++) used.push(false);
for (i = 0; i < got.length; i++){
var hit = false;
for (j = 0; j < expRoots.length; j++){
if (used[j]) continue;
if (FP.feq(got[i], expRoots[j])){ used[j] = true; hit = true; break; }
}
if (!hit) return { status: 'wrong' };
}
return { status: 'ok' };
}
/* ordered pair: expected [f1, f2]; accepts "(a, b)", "a, b", "a,b" */
function checkPair(exp1, exp2, raw){
var s = String(raw === undefined || raw === null ? '' : raw).replace(/−/g, '-').trim();
s = s.replace(/^\(/, '').replace(/\)$/, '');
var got = parseNumList(s);
if (!got) return bad('Type the ordered pair like (3, 2).');
if (got.length !== 2) return { status: 'wrong' };
if (FP.feq(got[0], exp1)) { if (FP.feq(got[1], exp2)) return { status: 'ok' }; }
return { status: 'wrong' };
}
/* factored form: expected polynomial; input must contain parens and expand to it */
function checkFactored(expPoly, raw){
var s = String(raw === undefined || raw === null ? '' : raw);
if (s.indexOf('(') < 0) return bad('Write the factored form with parentheses, like (x+2)(x-3).');
var p = FP.parsePoly(s);
if (!p) return bad('Could not read that as a polynomial. Use x and ^, like (x+2)(x-3).');
if (FP.peq(p, expPoly)) return { status: 'ok' };
return { status: 'wrong' };
}
/* "prime" / "no solution" style text answers with synonyms */
function checkWord(synonyms, raw){
var a = String(raw === undefined || raw === null ? '' : raw).toLowerCase().replace(/[\s._-]+/g, '');
if (!a) return bad('Type an answer first.');
var i;
for (i = 0; i < synonyms.length; i++){
if (a === synonyms[i].toLowerCase().replace(/[\s._-]+/g, '')) return { status: 'ok' };
}
return { status: 'wrong' };
}
/* multiple choice: expected letter */
function checkChoice(letter, raw){
var a = String(raw === undefined || raw === null ? '' : raw).toLowerCase().replace(/[\s.)(\-]+/g, '');
if (!a) return bad('Choose A, B, C, or D.');
if (a === letter.toLowerCase()) return { status: 'ok' };
if (a === 'a' || a === 'b' || a === 'c' || a === 'd') return { status: 'wrong' };
return bad('Choose A, B, C, or D.');
}
/* text with accepted variants */
function checkVariants(variants, raw){
var a = String(raw === undefined || raw === null ? '' : raw).toLowerCase().replace(/\s+/g, '');
if (!a) return bad('Type an answer first.');
var i;
for (i = 0; i < variants.length; i++){
if (a === String(variants[i]).toLowerCase().replace(/\s+/g, '')) return { status: 'ok' };
}
return { status: 'wrong' };
}
return {
checkNum: checkNum, parseNumList: parseNumList, checkRoots: checkRoots,
checkPair: checkPair, checkFactored: checkFactored, checkWord: checkWord,
checkChoice: checkChoice, checkVariants: checkVariants
};
})();
if (typeof module === 'undefined') { }
else { if (module.exports) module.exports = W2Check; }
/* Practice templates: factoring-trinomials-a1 (key trinom1). Pure math in gen(), no DOM.
Key formats:
T1_TWO|
var T = [];
T.push({ id: 'T1_TWO', tag: 'Find the two numbers', gen: function (rand) {
var m = randNZ(rand, -9, 9), n = randNZ(rand, -9, 9);
if (m === n) n = n > 0 ? n - 1 : n + 1;
if (n === 0) n = 1;
var p = m * n, s = m + n;
var key = 'T1_TWO|' + m + '|' + n;
return finish({
tag: 'Find the two numbers', kind: 'roots',
promptHTML: 'Find two numbers with product ' + p + ' and sum ' + s + '. Type them as m, n (either order).',
answerDisplayHTML: '' + m + ' and ' + n + '',
acceptText: m + ', ' + n,
hints: [
'List factor pairs of ' + p + ' (with signs!).',
'Which pair adds to ' + s + '?',
'Remember: c ' + (p > 0 ? '> 0' : '< 0') + ' means the numbers have ' + (p > 0 ? 'the same sign' : 'opposite signs') + '.'
],
whySteps: [
'Factor pairs of ' + p + ': test each sum.',
m + ' x ' + n + ' = ' + p + ' and ' + m + ' + ' + n + ' = ' + s + '.'
],
check: function (r1) { return WC.checkRoots([fi(m), fi(n)], r1); },
key: key
});
}});
T.push({ id: 'T1_FAC', tag: 'Factor x^2+bx+c', gen: function (rand) { var m = randNZ(rand, -9, 9), n = randNZ(rand, -9, 9); if (m === n) n = n > 0 ? n - 1 : n + 1; if (n === 0) n = 1; var b = m + n, c = m * n; var p = GU.intPoly([c, b, 1]); var key = 'T1_FAC|' + b + '|' + c; return finish({ tag: 'Factor x^2+bx+c', kind: 'factored', promptHTML: 'Factor: ' + pH(p) + '', answerDisplayHTML: '(x + ' + m + ')(x + ' + n + ')', acceptText: '(x+' + m + ')(x+' + n + ')', hints: [ 'Find two numbers: product ' + c + ', sum ' + b + '.', 'Sign check: c ' + (c > 0 ? '> 0' : '< 0') + ' → ' + signName(b, c) + '.', 'Write (x + m)(x + n) with your numbers.' ], whySteps: [ m + ' x ' + n + ' = ' + c + '; ' + m + ' + ' + n + ' = ' + b + '.', 'So ' + pT(p) + ' = (x+' + m + ')(x+' + n + ').' ], check: (function (pp) { return function (r1) { return WC.checkFactored(pp, r1); }; })(p), key: key }); }}); T.push({ id: 'T1_SIGN', tag: 'Name the sign pattern', gen: function (rand) { var m = randNZ(rand, -9, 9), n = randNZ(rand, -9, 9); if (m === n) n = n > 0 ? n - 1 : n + 1; if (n === 0) n = 1; var b = m + n, c = m * n; var ans = signName(b, c); var key = 'T1_SIGN|' + b + '|' + c; return finish({ tag: 'Name the sign pattern', kind: 'text', promptHTML: 'For x² + (' + b + ')x + (' + c + '), the two numbers in the factors are: both positive, both negative, or opposite signs?', answerDisplayHTML: '' + ans + '', acceptText: ans, hints: [ 'Look at c first: positive or negative?', 'c > 0 → same sign; c < 0 → opposite signs.', 'If same sign, b decides: b > 0 both positive, b < 0 both negative.' ], whySteps: [ 'c = ' + c + ' ' + (c > 0 ? '> 0 → same sign' : '< 0 → opposite signs') + '.', c > 0 ? 'b = ' + b + ' ' + (b > 0 ? '> 0' : '< 0') + ' → ' + ans + '.' : 'Answer: ' + ans + '.' ], check: function (r1) { return WC.checkVariants([ans], r1); }, key: key }); }}); T.push({ id: 'T1_GCF1', tag: 'GCF first, then factor', gen: function (rand) { var g = FP.rint(rand, 2, 5); var m = randNZ(rand, -8, 8), n = randNZ(rand, -8, 8); if (m === n) n = n > 0 ? n - 1 : n + 1; if (n === 0) n = 1; var p = FP.pmul(GU.intPoly([g]), GU.intPoly([m * n, m + n, 1])); var key = 'T1_GCF1|' + g + '|' + m + '|' + n; return finish({ tag: 'GCF first, then factor', kind: 'factored', promptHTML: 'Factor completely: ' + pH(p) + '', answerDisplayHTML: '' + g + '(x + ' + m + ')(x + ' + n + ')', acceptText: g + '(x+' + m + ')(x+' + n + ')', hints: [ 'The two-numbers method needs a = 1 — is there a GCF to pull first?', 'Pull out ' + g + ', then factor what remains.', 'Do not leave the GCF behind: it is part of the answer.' ], whySteps: [ 'GCF = ' + g + ': ' + pT(p) + ' = ' + g + '(x² + ' + (m + n) + 'x + ' + (m * n) + ').', 'Two numbers ' + m + ', ' + n + ' → ' + g + '(x+' + m + ')(x+' + n + ').' ], check: (function (pp) { return function (r1) { return WC.checkFactored(pp, r1); }; })(p), key: key }); }});
T.push({ id: 'T1_MISS', tag: 'Find the missing constant', gen: function (rand) { var m = randNZ(rand, -9, 9), n = randNZ(rand, -9, 9); if (n === 0) n = 2; var b = m + n, c = m * n; var key = 'T1_MISS|' + m + '|' + n; return finish({ tag: 'Find the missing constant', kind: 'num', promptHTML: 'If (x + ' + m + ')(x + n) = x² + ' + b + 'x + ' + c + ', what is n?', answerDisplayHTML: '' + n + '', acceptText: String(n), hints: [ 'Expand (x + ' + m + ')(x + n): the constant is ' + m + '·n.', 'So ' + m + '·n = ' + c + '.', 'Divide to find n.' ], whySteps: [ 'Constant term: ' + m + ' x n = ' + c + '.', 'n = ' + c + ' / ' + m + ' = ' + n + '.' ], check: function (r1) { return WC.checkNum(fi(n), r1); }, key: key }); }});
T.push({ id: 'T1_PRIME', tag: 'Prime or factorable?', gen: function (rand) { var prime = rand() < 0.5; var b, c; if (prime) { var opts = [[3, 5], [2, 7], [4, 11], [-3, 5], [1, 6], [-2, 3]]; var o = opts[FP.rint(rand, 0, 5)]; b = o[0]; c = o[1]; } else { var m = randNZ(rand, -8, 8), n = randNZ(rand, -8, 8); if (n === 0) n = 1; b = m + n; c = m * n; } var p = GU.intPoly([c, b, 1]); var key = 'T1_PRIME|' + b + '|' + c; var ans = prime ? 'prime' : 'factorable'; return finish({ tag: 'Prime or factorable?', kind: 'text', promptHTML: 'Is ' + pH(p) + ' prime or factorable over the integers?', answerDisplayHTML: '' + ans + '', acceptText: ans, hints: [ 'Hunt for two integers: product ' + c + ', sum ' + b + '.', 'List every factor pair of ' + c + ' (mind the signs).', 'No pair works → prime. One works → factorable.' ], whySteps: [ prime ? 'No integer pair multiplies to ' + c + ' and adds to ' + b + ' → prime.' : 'A pair works → factorable.' ], check: function (r1) { return WC.checkVariants([ans, prime ? 'not factorable' : 'factors'], r1); }, key: key }); }});
T.push({ id: 'T1_ZERO', tag: 'Solve by zero product', gen: function (rand) {
var m = randNZ(rand, -9, 9), n = randNZ(rand, -9, 9);
if (m === n) n = n > 0 ? n - 1 : n + 1;
if (n === 0) n = 1;
if (m === 0) m = -1;
var p = GU.intPoly([m * n, m + n, 1]);
var key = 'T1_ZERO|' + m + '|' + n;
return finish({
tag: 'Solve by zero product', kind: 'roots',
promptHTML: 'Solve: ' + pH(p) + ' = 0 — type the roots as r1, r2.',
answerDisplayHTML: '' + (-m) + ' and ' + (-n) + '',
acceptText: (-m) + ', ' + (-n),
hints: [
'Factor first: product ' + (m * n) + ', sum ' + (m + n) + '.',
'(x + ' + m + ')(x + ' + n + ') = 0.',
'Zero product: x + ' + m + ' = 0 or x + ' + n + ' = 0.'
],
whySteps: [
pT(p) + ' = (x+' + m + ')(x+' + n + ').',
'x = ' + (-m) + ' or x = ' + (-n) + '.'
],
check: function (r1) { return WC.checkRoots([fi(-m), fi(-n)], r1); },
key: key
});
}});
T.push({ id: 'T1_EXPAND', tag: 'Expand to check', gen: function (rand) { var m = randNZ(rand, -9, 9), n = randNZ(rand, -9, 9); if (n === 0) n = 1; var exp = GU.intPoly([m * n, m + n, 1]); var key = 'T1_EXPAND|' + m + '|' + n; return finish({ tag: 'Expand to check', kind: 'poly', promptHTML: 'Expand: (x + ' + m + ')(x + ' + n + ')', answerDisplayHTML: '' + pH(exp) + '', acceptText: pT(exp), hints: [ 'FOIL: First, Outer, Inner, Last.', 'The middle terms combine: ' + m + 'x + ' + n + 'x.', 'The constant is ' + m + ' x ' + n + '.' ], whySteps: [ 'FOIL: x² + ' + n + 'x + ' + m + 'x + ' + (m * n) + '.', 'Combined: ' + pT(exp) + '.' ], check: function (r1) { return PP.checkPolyAnswer(exp, r1, false); }, key: key }); }});
return T; })();
/*__GAME_TEMPLATES_END__*/
var GAME_DIFF = {easy: ['T1_TWO', 'T1_EXPAND', 'T1_FAC'], medium: ['T1_SIGN', 'T1_MISS', 'T1_GCF1'], hard: ['T1_PRIME', 'T1_ZERO', 'T1_FAC']}; var GAME_INPUT_HINT = 'Type the pair, the factorization, or the roots.'; var GAME_PLACEHOLDER_1 = '(x+3)(x+4)';
/*__GAME_SRC__*/ /* ============================================================================ Game core + glue (shared across team-E skills): 60-second sprint. 60 seconds, 3 lives, +10 per correct, streak bonus +2 per level beyond 3. Difficulty ramps: questions 1-6 easy, 7-14 medium, 15+ hard. Expects globals: FiboPoly, PRACTICE_TEMPLATES. Double-ampersand-free. ============================================================================ */ /*__GAME_CORE__*/ var SprintGame = (function () { 'use strict'; function Game(templates, diffMap) { var byId = {}, i; for (i = 0; i < templates.length; i++) byId[templates[i].id] = templates[i]; this._byId = byId; this._diffMap = diffMap; this._used = {}; this.timeLeft = 60; this.lives = 3; this.score = 0; this.streak = 0; this.bestStreak = 0; this.answered = 0; this.correctCount = 0; this.current = null; this.over = false; this.overReason = ''; } Game.prototype.tierFor = function (n) { if (n < 6) return 'easy'; if (n < 14) return 'medium'; return 'hard'; }; Game.prototype.next = function (rand, FP) { if (this.over) return null; var tier = this.tierFor(this.answered); var ids = this._diffMap[tier], guard = 0, p = null; while (guard < 120) { guard++; var t = this._byId[ids[Math.floor(rand() * ids.length)]]; if (!t) break; var cand; try { cand = t.gen(rand); } catch (e) { continue; } if (this._used[cand.key]) continue; this._used[cand.key] = 1; p = cand; break; } if (!p) { var t0 = this._byId[ids[0]]; p = t0.gen(rand); } p.tier = tier; this.current = p; return p; }; Game.prototype.submit = function (raw1, raw2) { if (this.over || !this.current) return null; var p = this.current; var r = p.kind === 'rational' ? p.check(raw1, raw2) : p.check(raw1); this.answered++; var correct = !(!r || !(r.status === 'ok')); var points = 0; if (correct) { this.correctCount++; this.streak++; if (this.streak > this.bestStreak) this.bestStreak = this.streak; points = 10; if (this.streak >= 4) points += 2 * (this.streak - 3); this.score += points; } else { this.streak = 0; this.lives--; if (this.lives <= 0) { this.over = true; this.overReason = 'out of lives'; } } return { correct: correct, points: points, score: this.score, lives: this.lives, streak: this.streak, over: this.over, overReason: this.overReason, status: r ? r.status : 'wrong', msg: !(!r || !r.msg) ? r.msg : '', answerHTML: p.answerDisplayHTML }; }; Game.prototype.tick = function () { if (this.over) return; this.timeLeft--; if (this.timeLeft <= 0) { this.timeLeft = 0; this.over = true; this.overReason = 'time up'; } }; Game.prototype.accuracy = function () { if (this.answered === 0) return 0; return Math.round(100 * this.correctCount / this.answered); }; return { Game: Game }; })(); if (typeof module === 'undefined') { } else { if (module.exports) module.exports = SprintGame; } /*__GAME_CORE_END__*/ /*__GAME_GLUE__*/ (function () { 'use strict'; function el(id) { return document.getElementById(id); } var game = null, rand = null, timerId = null; function heartsHTML(lives) { var h = ''; for (var i = 0; i < 3; i++) { h += ''; } return h; }
function show(id) { ['startScreen', 'playScreen', 'overScreen'].forEach(function (s) { el(s).classList.toggle('show', s === id); }); }
function updateHUD() { el('hudScore').textContent = game.score; el('hudStreak').textContent = game.streak; el('hudTime').textContent = game.timeLeft + 's'; el('hudLives').innerHTML = heartsHTML(game.lives); el('timerBox').classList.toggle('timer-low', game.timeLeft <= 10); } function deal() { var p = game.next(rand, FiboPoly); if (!p) { endGame(); return; } el('qType').textContent = p.tag + ' · ' + p.tier; el('qPrompt').innerHTML = p.promptHTML; el('qSub').textContent = p.kind === 'rational' ? 'Type the simplified expression, then the excluded values.' : GAME_INPUT_HINT; var two = p.kind === 'rational'; el('gAnswer2Wrap').style.display = two ? '' : 'none'; el('gAnswer').value = ''; el('gAnswer2').value = ''; el('gAnswer').placeholder = GAME_PLACEHOLDER_1; el('gAnswer2').placeholder = 'x≠…'; el('gAnswer').disabled = false; el('gAnswer2').disabled = false; el('feedback').className = 'feedback'; el('feedback').innerHTML = ''; el('gAnswer').focus(); } function flash(result) { var fb = el('feedback'); if (result.correct) { var bonus = result.points > 10 ? ' (includes +' + (result.points - 10) + ' streak bonus)' : ''; fb.innerHTML = '
'; fb.className = 'feedback show correct'; } else { var why = result.status === 'invalid' ? 'I could not read that. ' + (result.msg || '') : 'The answer was ' + result.answerHTML + '.'; fb.innerHTML = '
'; fb.className = 'feedback show wrong'; } }
function submitAnswer() { if (!game || game.over) return; var r = game.submit(el('gAnswer').value, el('gAnswer2').value); if (!r) return; el('gAnswer').disabled = true; el('gAnswer2').disabled = true; updateHUD(); if (r.over) { flash(r); setTimeout(endGame, 1400); return; } flash(r); setTimeout(deal, r.correct ? 700 : 2200); }
function tick() { game.tick(); updateHUD(); if (game.over) { endGame(); } }
function endGame() { clearInterval(timerId); timerId = null; game.over = true; el('overTitle').textContent = game.overReason === 'out of lives' ? 'Out of lives!' : 'Time’s up!'; el('statScore').textContent = game.score; el('statCorrect').textContent = game.correctCount + '/' + game.answered; el('statStreak').textContent = game.bestStreak; el('statAcc').textContent = game.accuracy() + '%'; var msg = el('overMsg'); if (game.score >= 200) msg.textContent = 'Outstanding — sprint champion material.'; else if (game.score >= 120) msg.textContent = 'Great run! A little more streak power and you will fly.'; else if (game.score >= 60) msg.textContent = 'Solid start. Review the lesson, then sprint again.'; else msg.textContent = 'Every sprint makes you faster. Try the practice page, then come back.'; show('overScreen'); }
function startGame() { game = new SprintGame.Game(PRACTICE_TEMPLATES, GAME_DIFF); rand = Math.random; updateHUD(); show('playScreen'); deal(); timerId = setInterval(tick, 1000); }
el('startBtn').addEventListener('click', startGame); el('againBtn').addEventListener('click', startGame); el('submitBtn').addEventListener('click', submitAnswer); el('gAnswer').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submitAnswer(); } }); el('gAnswer2').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submitAnswer(); } }); el('hudLives').innerHTML = heartsHTML(3); show('startScreen'); /*__GAME_GLUE_END__*/ })();
/*__GAME_SRC_END__*/