Skill: polynomial-operations
Polynomial Sprint
How fast can you operate on polynomials? 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 add, subtract, or multiply polynomials — including special products. Type your answer as a polynomial, like 4x^2-5x-1.
- 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; } return { ri: ri, rsign: rsign, intPoly: intPoly, randPoly: randPoly, factorPairs: factorPairs }; })(); 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; }
/* ============================================================================
Practice templates: polynomial-operations.
Defines PRACTICE_TEMPLATES. Expects globals: FiboPoly, PolyPractice, GenUtil.
double-ampersand-free by construction.
============================================================================ */
var PRACTICE_TEMPLATES = (function () {
'use strict';
var FP = FiboPoly, PP = PolyPractice, GU = GenUtil;
var finish = PP.finishProblem;
function pH(p) { return FP.pfmtHTML(p); }
function pT(p) { return FP.pfmtText(p); }
function I(k) { return FP.fi(k); }
/* per-degree pair line for add/subtract steps: "(a) + (b) = s" for the x^d column */
function degPairHTML(a, b, d) {
var unit = d === 0 ? '' : (d === 1 ? 'x' : 'x' + d + '');
function t(c) {
if (FP.fis0(c)) return '0' + unit;
var neg = c.n < 0;
var isOne = !(Math.abs(c.n) !== 1 || c.d !== 1);
var mag = FP.ffmt({ n: Math.abs(c.n), d: c.d });
if (isOne) { if (d > 0) mag = ''; }
return (neg ? '−' : '') + mag + unit;
}
return '(' + t(a) + ' + ' + t(b) + ') = ' + t(FP.fadd(a, b)) + '';
}
function addSubSteps(p1, p2eff, p2label) {
var steps = [], d;
var n = Math.max(p1.length, p2eff.length);
steps.push('Write the sum: (' + pH(p1) + ') + (' + p2label + ').');
steps.push('Group like terms — same variable, same exponent — and add each column:');
var cols = [];
for (d = n - 1; d >= 0; d--) {
var a = d < p1.length ? p1[d] : I(0);
var b = d < p2eff.length ? p2eff[d] : I(0);
if (FP.fis0(a)) { if (FP.fis0(b)) continue; }
cols.push(degPairHTML(a, b, d));
}
steps.push(cols.join('
'));
return steps;
}
/* distribution steps for multiplication */
function distSteps(p1, p2) {
var lines = [], d1, d2;
for (d1 = 0; d1 < p1.length; d1++) {
if (FP.fis0(p1[d1])) continue;
for (d2 = 0; d2 < p2.length; d2++) {
if (FP.fis0(p2[d2])) continue;
var t1 = termHTML(p1[d1], d1), t2 = termHTML(p2[d2], d2);
var prod = FP.fmul(p1[d1], p2[d2]);
lines.push('(' + t1 + ')·(' + t2 + ') = ' + termHTML(prod, d1 + d2));
}
}
return lines;
}
function termHTML(c, d) {
if (FP.fis0(c)) return '0';
var neg = c.n < 0;
var one = !(Math.abs(c.n) !== 1 || c.d !== 1);
var mag = FP.ffmt({ n: Math.abs(c.n), d: c.d });
if (one) { if (d > 0) mag = ''; }
var unit = d === 0 ? '' : (d === 1 ? 'x' : 'x' + d + '');
return (neg ? '−' : '') + mag + unit;
}
var T = [];
/* ---------- T1: add ---------- */
T.push({ id: 'P_ADD', tag: 'Add polynomials', gen: function (rand) {
var p1 = GU.randPoly(rand, FP.rpick(rand, [1, 2]), -6, 6);
var p2 = GU.randPoly(rand, FP.rpick(rand, [1, 2]), -6, 6);
var ans = FP.padd(p1, p2);
var key = 'add|' + pT(p1) + '|' + pT(p2);
var prompt = 'Add: (' + pH(p1) + ') + (' + pH(p2) + ')';
var steps = addSubSteps(p1, p2, pH(p2));
var hints = [
'Hint 1: Only identical twins combine: x2 terms with x2 terms, x terms with x terms, constants with constants.',
'Hint 2: Line up the columns:
' + [Math.max(p1.length, p2.length) - 1, Math.max(p1.length, p2.length) - 2, 0].map(function (d) {
if (d < 0) return '';
var a = d < p1.length ? p1[d] : I(0), b = d < p2.length ? p2[d] : I(0);
if (FP.fis0(a)) { if (FP.fis0(b)) return ''; }
return degPairHTML(a, b, d);
}).join('
'),
'Hint 3: Add each column to get ' + pH(ans) + '.'
];
return finish({
tag: 'Add polynomials', kind: 'poly', promptHTML: prompt,
answerDisplayHTML: '' + pH(ans) + '',
acceptText: pT(ans), hints: hints, whySteps: steps,
check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); },
key: key
});
} });
/* ---------- T2: subtract ---------- */ T.push({ id: 'P_SUB', tag: 'Subtract polynomials', gen: function (rand) { var p1 = GU.randPoly(rand, FP.rpick(rand, [1, 2]), -6, 6); var p2 = GU.randPoly(rand, FP.rpick(rand, [1, 2]), -6, 6); var negp2 = FP.pmulC(p2, I(-1)); var ans = FP.padd(p1, negp2); var key = 'sub|' + pT(p1) + '|' + pT(p2); var prompt = 'Subtract: (' + pH(p1) + ') − (' + pH(p2) + ')'; var steps = ['Distribute the minus to every term of the second polynomial: −(' + pH(p2) + ') = ' + pH(negp2) + '.'] .concat(addSubSteps(p1, negp2, pH(negp2)).slice(1)); var hints = [ 'Hint 1: The minus in front of the parentheses hits every term inside — not just the first one.', 'Hint 2: Rewrite as addition first: (' + pH(p1) + ') + (' + pH(negp2) + ').', 'Hint 3: Now combine like terms to get ' + pH(ans) + '.' ]; return finish({ tag: 'Subtract polynomials', kind: 'poly', promptHTML: prompt, answerDisplayHTML: '' + pH(ans) + '', acceptText: pT(ans), hints: hints, whySteps: steps, check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); }, key: key }); } });
/* ---------- T3: monomial x polynomial ---------- */
T.push({ id: 'P_MONO', tag: 'Monomial × polynomial', gen: function (rand) {
var a = FP.rpickNZ(rand, -5, 5);
var p2 = GU.randPoly(rand, FP.rpick(rand, [1, 2]), -6, 6);
var p1 = FP.pnorm([I(0), I(a)]);
var ans = FP.pmul(p1, p2);
var key = 'mono|' + a + '|' + pT(p2);
var prompt = 'Multiply: ' + termHTML(I(a), 1) + '(' + pH(p2) + ')';
var steps = ['Distribute ' + termHTML(I(a), 1) + ' to every term inside:'].concat(
distSteps(p1, p2)).concat(['Add the pieces: ' + pH(ans) + '.']);
var hints = [
'Hint 1: Multiply the coefficients, and add the exponents: x·x2 = x3.',
'Hint 2: ' + termHTML(I(a), 1) + ' times each term gives:
' + distSteps(p1, p2).join('
'),
'Hint 3: The product is ' + pH(ans) + '.'
];
return finish({
tag: 'Monomial × polynomial', kind: 'poly', promptHTML: prompt,
answerDisplayHTML: '' + pH(ans) + '',
acceptText: pT(ans), hints: hints, whySteps: steps,
check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); },
key: key
});
} });
/* ---------- T4: (x+a)(x+b) ---------- */
T.push({ id: 'P_BINOM', tag: 'Binomial × binomial', gen: function (rand) {
var a = FP.rpickNZ(rand, -9, 9), b = FP.rpickNZ(rand, -9, 9);
var p1 = FP.pnorm([I(a), I(1)]), p2 = FP.pnorm([I(b), I(1)]);
var ans = FP.pmul(p1, p2);
var key = 'binom|' + a + '|' + b;
var sa = a < 0 ? '− ' + Math.abs(a) : '+ ' + a;
var sb = b < 0 ? '− ' + Math.abs(b) : '+ ' + b;
var prompt = 'Multiply: (x ' + sa + ')(x ' + sb + ')';
var ab = a * b, sm = a + b;
var mid = sm === 0 ? '' : (sm < 0 ? ' − ' + Math.abs(sm) + 'x' : ' + ' + sm + 'x');
var steps = [
'FOIL — First, Outer, Inner, Last:',
'First: x·x = x2
Outer: x·(' + sb.trim() + ') = ' + termHTML(I(b), 1) +
'
Inner: (' + sa.trim() + ')·x = ' + termHTML(I(a), 1) +
'
Last: (' + sa.trim() + ')·(' + sb.trim() + ') = ' + (ab < 0 ? '−' : '') + Math.abs(ab),
'Combine the middle terms: ' + termHTML(I(b), 1) + ' + ' + termHTML(I(a), 1) + ' = ' +
(sm === 0 ? '0' : termHTML(I(sm), 1)) + '.',
'Result: ' + pH(ans) + '.'
];
var hints = [
'Hint 1: FOIL: multiply First, Outer, Inner, Last — four products.',
'Hint 2: x2 ' + mid + ' ' + (ab < 0 ? '− ' + Math.abs(ab) : '+ ' + ab) + ' — now combine the x terms.',
'Hint 3: The product is ' + pH(ans) + '.'
];
return finish({
tag: 'Binomial × binomial', kind: 'poly', promptHTML: prompt,
answerDisplayHTML: '' + pH(ans) + '',
acceptText: pT(ans), hints: hints, whySteps: steps,
check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); },
key: key
});
} });
/* ---------- T5: (ax+b)(cx+d) ---------- */ T.push({ id: 'P_BINOM2', tag: 'Binomial × binomial', gen: function (rand) { var a = FP.rpickNZ(rand, -3, 3), c = FP.rpickNZ(rand, -3, 3); var b = FP.rpickNZ(rand, -6, 6), d = FP.rpickNZ(rand, -6, 6); var p1 = FP.pnorm([I(b), I(a)]), p2 = FP.pnorm([I(d), I(c)]); var ans = FP.pmul(p1, p2); var key = 'binom2|' + a + ',' + b + '|' + c + ',' + d; var prompt = 'Multiply: (' + termHTML(I(a), 1) + (b < 0 ? ' − ' + Math.abs(b) : ' + ' + b) + ')(' + termHTML(I(c), 1) + (d < 0 ? ' − ' + Math.abs(d) : ' + ' + d) + ')'; var mid = a * d + b * c; var steps = ['Multiply every term of the first binomial by every term of the second:']. concat(distSteps(p1, p2)).concat([ 'Combine the x terms: ' + termHTML(I(a * d), 1) + ' + ' + termHTML(I(b * c), 1) + ' = ' + termHTML(I(mid), 1) + '.', 'Result: ' + pH(ans) + '.']); var hints = [ 'Hint 1: Four products: (' + termHTML(I(a), 1) + ')(' + termHTML(I(c), 1) + '), (' + termHTML(I(a), 1) + ')(' + d + '), (' + b + ')(' + termHTML(I(c), 1) + '), (' + b + ')(' + d + ').', 'Hint 2: That gives ' + pH(FP.pnorm([I(b * d), I(mid), I(a * c)])) + ' before combining — now add the x terms.', 'Hint 3: The product is ' + pH(ans) + '.' ]; return finish({ tag: 'Binomial × binomial', kind: 'poly', promptHTML: prompt, answerDisplayHTML: '' + pH(ans) + '', acceptText: pT(ans), hints: hints, whySteps: steps, check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); }, key: key }); } });
/* ---------- T6: (ax+b)^2 ---------- */
T.push({ id: 'P_SQUARE', tag: 'Special product', gen: function (rand) {
var a = FP.rpickNZ(rand, -4, 4), b = FP.rpickNZ(rand, -6, 6);
var p1 = FP.pnorm([I(b), I(a)]);
var ans = FP.pmul(p1, p1);
var key = 'sq|' + a + '|' + b;
var sb = b < 0 ? '− ' + Math.abs(b) : '+ ' + b;
var prompt = 'Multiply: (' + termHTML(I(a), 1) + ' ' + sb + ')2';
var a2 = a * a, tb = 2 * a * b, b2 = b * b;
var steps = [
'Use (u + v)2 = u2 + 2uv + v2 with u = ' + termHTML(I(a), 1) + ', v = ' + (b < 0 ? '−' + Math.abs(b) : String(b)) + ':',
'u2 = (' + termHTML(I(a), 1) + ')2 = ' + termHTML(I(a2), 2) +
'
2uv = 2(' + termHTML(I(a), 1) + ')(' + (b < 0 ? '−' + Math.abs(b) : String(b)) + ') = ' + termHTML(I(tb), 1) +
'
v2 = (' + (b < 0 ? '−' + Math.abs(b) : String(b)) + ')2 = ' + Math.abs(b2),
'Add them: ' + pH(ans) + '.',
'Warning: (' + termHTML(I(a), 1) + ' ' + sb + ')2 is not ' + termHTML(I(a2), 2) + ' + ' + Math.abs(b2) + ' — the middle term 2uv is not optional.'
];
var hints = [
'Hint 1: (u + v)2 = u2 + 2uv + v2 — three terms, not two.',
'Hint 2: u2 = ' + termHTML(I(a2), 2) + ', v2 = ' + Math.abs(b2) + ', and 2uv = ' + termHTML(I(tb), 1) + '.',
'Hint 3: The product is ' + pH(ans) + '.'
];
return finish({
tag: 'Special product', kind: 'poly', promptHTML: prompt,
answerDisplayHTML: '' + pH(ans) + '',
acceptText: pT(ans), hints: hints, whySteps: steps,
check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); },
key: key
});
} });
/* ---------- T7: (ax+b)(ax-b) ---------- */ T.push({ id: 'P_DIFFSQ', tag: 'Special product', gen: function (rand) { var a = FP.rpickNZ(rand, -4, 4), b = FP.rpickNZ(rand, 1, 7); var p1 = FP.pnorm([I(b), I(a)]), p2 = FP.pnorm([I(-b), I(a)]); var ans = FP.pmul(p1, p2); var key = 'dsq|' + a + '|' + b; var prompt = 'Multiply: (' + termHTML(I(a), 1) + ' + ' + b + ')(' + termHTML(I(a), 1) + ' − ' + b + ')'; var a2 = a * a, b2 = b * b; var steps = [ 'This is (u + v)(u − v) = u2 − v2 with u = ' + termHTML(I(a), 1) + ', v = ' + b + ':', 'u2 = ' + termHTML(I(a2), 2) + ', v2 = ' + b2 + '. The middle terms cancel: +' + termHTML(I(a * b), 1) + ' − ' + termHTML(I(a * b), 1) + ' = 0.', 'Result: ' + pH(ans) + '.' ]; var hints = [ 'Hint 1: (u + v)(u − v) = u2 − v2 — the middle terms always cancel.', 'Hint 2: (' + termHTML(I(a), 1) + ')2 − (' + b + ')2 = ' + termHTML(I(a2), 2) + ' − ' + b2 + '.', 'Hint 3: The product is ' + pH(ans) + '.' ]; return finish({ tag: 'Special product', kind: 'poly', promptHTML: prompt, answerDisplayHTML: '' + pH(ans) + '', acceptText: pT(ans), hints: hints, whySteps: steps, check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); }, key: key }); } });
/* ---------- T8: combine like terms ---------- */
T.push({ id: 'P_COMBINE', tag: 'Combine like terms', gen: function (rand) {
var ans = GU.randPoly(rand, 2, -7, 7);
/* split each coefficient into 1-2 pieces to make an unsimplified display */
var pieces = [], d;
for (d = 0; d < ans.length; d++) {
var c = ans[d].n;
if (c === 0) continue;
if (Math.abs(c) >= 2) {
var cut = FP.rpickNZ(rand, -Math.abs(c) + 1, Math.abs(c) - 1);
if (cut === c) cut = c - 1;
pieces.push([cut, d]);
pieces.push([c - cut, d]);
} else {
pieces.push([c, d]);
}
}
pieces = FP.rshuffle(rand, pieces);
var shown = pieces.map(function (pc) { return termHTML(I(pc[0]), pc[1]); }).join(' ');
var shownSigned = '';
pieces.forEach(function (pc, i) {
var t = termHTML(I(Math.abs(pc[0])), pc[1]);
if (i === 0) shownSigned = (pc[0] < 0 ? '−' : '') + t;
else shownSigned += (pc[0] < 0 ? ' − ' : ' + ') + t;
});
var key = 'combine|' + pT(ans) + '|' + pieces.length;
var prompt = 'Simplify by combining like terms: ' + shownSigned + '';
var steps = [
'Circle the like terms: x2 terms together, x terms together, constants together.',
'Add each family: ' + [2, 1, 0].map(function (d) {
var fam = pieces.filter(function (pc) { return pc[1] === d; });
if (!fam.length) return '';
var unit = d === 0 ? '' : (d === 1 ? 'x' : 'x' + d + '');
return '(' + fam.map(function (pc) { return pc[0]; }).join(' + ') + ')' + unit + ' = ' + termHTML(ans[d], d);
}).filter(function (x) { return x; }).join('
'),
'Simplified: ' + pH(ans) + '.'
];
var hints = [
'Hint 1: Only identical twins combine — 3x2 and −x2 merge, but 2x cannot join them.',
'Hint 2: Group first: x2 terms: ' + pieces.filter(function (pc) { return pc[1] === 2; }).map(function (pc) { return pc[0]; }).join(', ') + '.',
'Hint 3: The simplified form is ' + pH(ans) + '.'
];
return finish({
tag: 'Combine like terms', kind: 'poly', promptHTML: prompt,
answerDisplayHTML: '' + pH(ans) + '',
acceptText: pT(ans), hints: hints, whySteps: steps,
check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); },
key: key
});
} });
/* ---------- T9: area word problem ---------- */ T.push({ id: 'P_AREA', tag: 'Word problem', gen: function (rand) { var a = FP.rpickNZ(rand, 2, 8), b = FP.rpickNZ(rand, 2, 8); var p1 = FP.pnorm([I(a), I(1)]), p2 = FP.pnorm([I(-b), I(1)]); var ans = FP.pmul(p1, p2); var key = 'area|' + a + '|' + b; var prompt = 'A rectangle has length (x + ' + a + ') and width ' + '(x − ' + b + '). What is its area as a polynomial?'; var steps = [ 'Area = length × width = (x + ' + a + ')(x − ' + b + ').', 'FOIL: x2 − ' + b + 'x + ' + a + 'x − ' + (a * b) + '.', 'Combine the x terms: ' + pH(ans) + '.' ]; var hints = [ 'Hint 1: Area = length × width. Write the product first.', 'Hint 2: (x + ' + a + ')(x − ' + b + ') — FOIL it out.', 'Hint 3: The area is ' + pH(ans) + '.' ]; return finish({ tag: 'Word problem', kind: 'poly', promptHTML: prompt, answerDisplayHTML: '' + pH(ans) + '', acceptText: pT(ans), hints: hints, whySteps: steps, check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); }, key: key }); } });
/* ---------- T10: monomial x trinomial with negatives ---------- */
T.push({ id: 'P_MONO2', tag: 'Monomial × polynomial', gen: function (rand) {
var a = FP.rpickNZ(rand, -4, 4);
var b = FP.rpickNZ(rand, -6, 6), c = FP.rpickNZ(rand, -6, 6);
var p2 = FP.pnorm([I(c), I(0), I(b)]);
var p1 = FP.pnorm([I(0), I(a)]);
var ans = FP.pmul(p1, p2);
var key = 'mono2|' + a + '|' + b + '|' + c;
var prompt = 'Multiply: ' + termHTML(I(a), 1) + '(' + pH(p2) + ')';
var steps = ['Distribute:'].concat(distSteps(p1, p2)).concat(
['Add the pieces: ' + pH(ans) + '.']);
var hints = [
'Hint 1: Watch the signs: a negative times a negative is a positive.',
'Hint 2:
' + distSteps(p1, p2).join('
'),
'Hint 3: The product is ' + pH(ans) + '.'
];
return finish({
tag: 'Monomial × polynomial', kind: 'poly', promptHTML: prompt,
answerDisplayHTML: '' + pH(ans) + '',
acceptText: pT(ans), hints: hints, whySteps: steps,
check: function (r1) { return PP.checkPolyAnswer(ans, r1, false); },
key: key
});
} });
return T; })(); if (typeof module === 'undefined') { } else { if (module.exports) module.exports = PRACTICE_TEMPLATES; }
/*__GAME_TEMPLATES_END__*/
var GAME_DIFF = {easy: ['P_ADD', 'P_COMBINE', 'P_MONO'], medium: ['P_SUB', 'P_BINOM', 'P_BINOM2', 'P_MONO2'], hard: ['P_SQUARE', 'P_DIFFSQ', 'P_AREA']}; var GAME_INPUT_HINT = 'Type the polynomial, like 4x^2-5x-1.'; var GAME_PLACEHOLDER_1 = '4x^2-5x-1';
/*__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__*/