Skill: factoring-gcf
Factoring with the GCF Worksheet
Find each GCF. When factoring, pull out the COMPLETE GCF — keep going until nothing inside shares a factor. For grouping problems, pair terms and factor out the common binomial. Check every factorization by multiplying back.
Each click of Generate builds a brand-new set of 20 problems (7 easy, 8 medium, 5 hard). Toggle the answer key any time; printing includes the key only when it is visible.
Factoring with the GCF Worksheet
Directions: Find each GCF. When factoring, pull out the COMPLETE GCF — keep going until nothing inside shares a factor. For grouping problems, pair terms and factor out the common binomial. Check every factorization by multiplying back.
Answer Key
For the teacher or for self-checking after you finish.
/* ============================================================================ 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 }; });
/*__WS_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-gcf (key gcf). Pure math in gen(), no DOM.
Key formats:
GCF_NUM|| GCF_WORD|| GCF_MONO|
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; } function randNZ(rand, lo, hi) { return FP.rpickNZ(rand, lo, hi); } /* true GCF monomial of an integer-coefficient poly */ function gcfOfPoly(p) { var g = 0, d = 99, i; for (i = 0; i < p.length; i++) { if (!FP.fis0(p[i])) { g = igcd(g, p[i].n); if (i < d) d = i; } } if (d === 99) d = 0; var arr = [], k; for (k = 0; k < d; k++) arr.push(fi(0)); arr.push(fi(g)); return FP.pnorm(arr); } function monoText(c, d) { if (d === 0) return String(c); if (d === 1) return c + 'x'; return c + 'x^' + d; } function randMonoText(rand, clo, chi, dhi) { return monoText(randNZ(rand, clo, chi), FP.rint(rand, 0, dhi)); } var T = []; T.push({ id: 'GCF_NUM', tag: 'GCF of two numbers', gen: function (rand) { var a = FP.rint(rand, 6, 40), b = FP.rint(rand, 6, 40); var ans = igcd(a, b); var key = 'GCF_NUM|' + a + '|' + b; return finish({ tag: 'GCF of two numbers', kind: 'num', promptHTML: 'Find the GCF of ' + a + ' and ' + b + '.', answerDisplayHTML: '' + ans + '', acceptText: String(ans), hints: [ 'List the divisors of each number.', 'Circle the divisors they share.', 'The greatest shared one is the GCF.' ], whySteps: [ 'Common divisors of ' + a + ' and ' + b + ' top out at ' + ans + '.', 'GCF(' + a + ', ' + b + ') = ' + ans + '.' ], check: function (r1) { return WC.checkNum(fi(ans), r1); }, key: key }); }});
T.push({ id: 'GCF_WORD', tag: 'GCF word problem', gen: function (rand) { var items = [ ['ribbons', 'in. long', 'equal-length pieces with nothing left over'], ['tiles', 'cm wide', 'identical square stacks with nothing left over'], ['stickers', 'in a pack', 'identical goody bags with nothing left over'] ]; var it = items[FP.rint(rand, 0, 2)]; var a = FP.rint(rand, 6, 30), b = FP.rint(rand, 6, 30); var ans = igcd(a, b); var key = 'GCF_WORD|' + a + '|' + b; return finish({ tag: 'GCF word problem', kind: 'num', promptHTML: 'Two ' + it[0] + ' measure ' + a + ' and ' + b + ' ' + it[1] + '. What is the longest length that divides both into ' + it[2] + '?', answerDisplayHTML: '' + ans + '', acceptText: String(ans), hints: [ '"Longest length that divides both" is the GCF in disguise.', 'Find the common divisors of ' + a + ' and ' + b + '.', 'Take the greatest one.' ], whySteps: [ 'The length must divide both ' + a + ' and ' + b + ' — that is the GCF.', 'GCF(' + a + ', ' + b + ') = ' + ans + '.' ], check: function (r1) { return WC.checkNum(fi(ans), r1); }, key: key }); }});
T.push({ id: 'GCF_MONO', tag: 'GCF of two monomials', gen: function (rand) { var m1 = randMonoText(rand, 2, 12, 4), m2 = randMonoText(rand, 2, 12, 4); var p1 = FP.parsePoly(m1), p2 = FP.parsePoly(m2); var g = igcd(p1[FP.pdeg(p1)].n, p2[FP.pdeg(p2)].n); var d = Math.min(FP.pdeg(p1), FP.pdeg(p2)); var ans = monoText(g, d); var key = 'GCF_MONO|' + m1 + '|' + m2; return finish({ tag: 'GCF of two monomials', kind: 'poly', promptHTML: 'Find the GCF of ' + m1 + ' and ' + m2 + '.', answerDisplayHTML: '' + ans + '', acceptText: ans, hints: [ 'Handle numbers and variables separately.', 'GCF of the coefficients: gcd(' + p1[0].n + ', ' + p2[0].n + ').', 'For x: the SMALLER exponent wins.' ], whySteps: [ 'gcd(' + p1[0].n + ', ' + p2[0].n + ') = ' + g + '; min exponent = ' + d + '.', 'GCF = ' + ans + '.' ], check: function (r1) { return PP.checkPolyAnswer(FP.parsePoly(ans), r1, false); }, key: key }); }});
T.push({ id: 'GCF_TERMS', tag: 'GCF of three monomials', gen: function (rand) { var ms = [randMonoText(rand, 2, 12, 4), randMonoText(rand, 2, 12, 4), randMonoText(rand, 2, 12, 4)]; var ps = ms.map(function (m) { return FP.parsePoly(m); }); function lead(p) { return p[FP.pdeg(p)].n; } var g = igcd(igcd(lead(ps[0]), lead(ps[1])), lead(ps[2])); var d = Math.min(FP.pdeg(ps[0]), FP.pdeg(ps[1]), FP.pdeg(ps[2])); var ans = monoText(g, d); var key = 'GCF_TERMS|' + ms.join('|'); return finish({ tag: 'GCF of three monomials', kind: 'poly', promptHTML: 'Find the GCF of ' + ms.join(', ') + '.', answerDisplayHTML: '' + ans + '', acceptText: ans, hints: [ 'The GCF must divide ALL THREE terms.', 'gcd of the three coefficients, then the smallest exponent.', 'Check: does your answer divide each term evenly?' ], whySteps: [ 'gcd of coefficients = ' + g + '; smallest exponent = ' + d + '.', 'GCF = ' + ans + '.' ], check: function (r1) { return PP.checkPolyAnswer(FP.parsePoly(ans), r1, false); }, key: key }); }});
T.push({ id: 'GCF_POLY', tag: 'Factor out the GCF', gen: function (rand) { var p, g, guard = 0; do { p = GU.randPoly(rand, FP.rint(rand, 1, 3), -9, 9); g = gcfOfPoly(p); guard++; } while (!((FP.pdeg(g) > 0 || g[0].n > 1) || guard >= 40)); var rest = (function () { var gd = FP.pdeg(g), gc = g[gd].n, rr = [], i; for (i = 0; i < p.length; i++) { if (i < gd) { if (!FP.fis0(p[i])) return null; } else { rr.push(frac2(p[i].n, gc)); } } return FP.pnorm(rr); })(); function frac2(n, d) { var gg = igcd(n, d); return { n: n / gg, d: d / gg }; } var key = 'GCF_POLY|' + pT(p); var prompt = 'Factor out the GCF: ' + pH(p) + ''; var acc, disp, chk; if (rest === null) { /* fallback: GCF is 1, nothing to factor */ acc = pT(p); disp = '' + pH(p) + ' (already fully factored)'; chk = function (r1) { return PP.checkPolyAnswer(p, r1, false); }; } else { var restP = rest; var gText = pT(g), restText = pT(restP); acc = gText + '(' + restText + ')'; disp = '' + pH(g) + '(' + pH(restP) + ')'; chk = (function (pp) { return function (r1) { return WC.checkFactored(pp, r1); }; })(p); } return finish({ tag: 'Factor out the GCF', kind: 'factored', promptHTML: prompt, answerDisplayHTML: disp, acceptText: acc, hints: [ 'Find the GCF of all the terms: gcd of coefficients, smallest x-exponent.', 'Divide every term by the GCF — what is left goes in parentheses.', 'Check: nothing inside the parentheses should share a factor.' ], whySteps: [ 'GCF of the terms = ' + pT(g) + '.', 'Dividing each term: ' + acc + '.' ], check: chk, key: key }); }});
T.push({ id: 'GCF_GROUP', tag: 'Factor by grouping', gen: function (rand) { var u, v, w, p, g, guard = 0; do { u = randNZ(rand, -5, 5); v = randNZ(rand, -5, 5); w = randNZ(rand, -5, 5); p = FP.pmul(GU.intPoly([v, u]), GU.intPoly([w, 0, 1])); g = gcfOfPoly(p); guard++; } while (!((!((FP.pdeg(g) !== 0) || (g[0].n !== 1))) || guard >= 40)); var f1 = GU.intPoly([v, u]), f2 = GU.intPoly([w, 0, 1]); var key = 'GCF_GROUP|' + u + '|' + v + '|' + w; return finish({ tag: 'Factor by grouping', kind: 'factored', promptHTML: 'Factor by grouping: ' + pH(p) + '', answerDisplayHTML: '(' + pH(f1) + ')(' + pH(f2) + ')', acceptText: '(' + pT(f1) + ')(' + pT(f2) + ')', hints: [ 'Pair the first two terms and the last two terms.', 'Factor the GCF out of each pair.', 'A common binomial should appear — factor it out.' ], whySteps: [ 'Pairs: x²(' + pT(f1) + ') + ' + w + '(' + pT(f1) + ').', 'Common binomial (' + pT(f1) + ') → (' + pT(f1) + ')(' + pT(f2) + ').' ], check: (function (pp) { return function (r1) { return WC.checkFactored(pp, r1); }; })(p), key: key }); }});
T.push({ id: 'GCF_CHECK', tag: 'Multiply back to check', gen: function (rand) { var g = FP.pnorm((function () { var d = FP.rint(rand, 1, 2), arr = [], k; for (k = 0; k < d; k++) arr.push(fi(0)); arr.push(fi(randNZ(rand, 2, 6))); return arr; })()); var h = GU.randPoly(rand, 1, -5, 5); var exp = FP.pmul(g, h); var key = 'GCF_CHECK|' + pT(g) + '(' + pT(h) + ')'; return finish({ tag: 'Multiply back to check', kind: 'poly', promptHTML: 'Multiply to check the factorization: ' + pH(g) + '(' + pH(h) + ')', answerDisplayHTML: '' + pH(exp) + '', acceptText: pT(exp), hints: [ 'Distribute ' + pT(g) + ' to every term inside.', 'Multiply coefficients; add exponents.', 'The result should match the original polynomial.' ], whySteps: [ 'Distributing ' + pT(g) + ' across ' + pT(h) + '.', 'Product: ' + pT(exp) + '.' ], check: function (r1) { return PP.checkPolyAnswer(exp, r1, false); }, key: key }); }});
T.push({ id: 'GCF_IDENT', tag: 'Fully factored?', gen: function (rand) { var kind = FP.rint(rand, 0, 1); var expr, ans; if (kind === 0) { var yesOpts = ['3x^2(2x+3)', '5x(x+4)', '2(x^2+1)', '7x^3(x+2)']; expr = yesOpts[FP.rint(rand, 0, 3)]; ans = 'yes'; } else { var opts = ['2x(3x+6)', '3x(2x^2+3x)', '4(x^2+2x)', '6x^2(2x+4)', 'x(3x+9)', '2x^2(4x+6)']; expr = opts[FP.rint(rand, 0, 5)]; ans = 'no'; } var key = 'GCF_IDENT|' + kind + '|' + expr; return finish({ tag: 'Fully factored?', kind: 'text', promptHTML: 'Is ' + expr.replace(/x\^(\d)/g, 'x$1') + ' fully factored? Answer yes or no.', answerDisplayHTML: '' + ans + '', acceptText: ans, hints: [ 'Look INSIDE the parentheses: do those terms share a factor?', 'Also check the outside factor against the inside.', 'Fully factored means no GCF remains anywhere.' ], whySteps: [ kind === 0 ? 'Inside: 2x + 3 shares nothing; outside 3x² shares nothing with it. Fully factored: yes.' : expr + ' still has a common factor inside — keep going. Answer: no.' ], check: function (r1) { return WC.checkVariants([ans], r1); }, key: key }); }});
return T; })();
/*__WS_TEMPLATES_END__*/
var WS_DIFF = {easy: ['GCF_NUM', 'GCF_WORD', 'GCF_MONO'], medium: ['GCF_TERMS', 'GCF_POLY', 'GCF_CHECK'], hard: ['GCF_GROUP', 'GCF_IDENT', 'GCF_POLY']};
/*__WORKSHEET_SRC__*/ /* ============================================================================ Worksheet core + glue (shared across team-E skills). buildSheet picks 20 problems (7 easy / 8 medium / 5 hard) from the practice templates with a seeded RNG; the glue renders the printable sheet. Expects globals: FiboPoly, PRACTICE_TEMPLATES. Double-ampersand-free. ============================================================================ */ /*__WORKSHEET_CORE__*/ var WorksheetCore = (function () { 'use strict'; function buildSheet(templates, diffMap, seed, FP) { var byId = {}, i; for (i = 0; i < templates.length; i++) byId[templates[i].id] = templates[i]; var rand = FP.mulberry32(seed >>> 0); var sections = [ { name: 'Easy — warm up', ids: diffMap.easy, count: 7 }, { name: 'Medium — core skills', ids: diffMap.medium, count: 8 }, { name: 'Hard — stretch', ids: diffMap.hard, count: 5 } ]; var used = {}, num = 0, out = []; sections.forEach(function (sec) { var items = [], guard = 0; while (items.length < sec.count) { guard++; if (guard > 400) break; var t = byId[sec.ids[Math.floor(rand() * sec.ids.length)]]; if (!t) break; var p; try { p = t.gen(rand); } catch (e) { continue; } if (used[p.key]) continue; used[p.key] = 1; num++; items.push({ num: num, tag: p.tag, kind: p.kind, promptHTML: p.promptHTML, answerHTML: p.answerDisplayHTML }); } out.push({ name: sec.name, items: items }); }); return { sections: out, seed: seed >>> 0 }; } return { buildSheet: buildSheet }; })(); if (typeof module === 'undefined') { } else { if (module.exports) module.exports = WorksheetCore; } /*__WORKSHEET_CORE_END__*/ /*__WORKSHEET_GLUE__*/ (function () { 'use strict'; function el(id) { return document.getElementById(id); } var seed = (Date.now() % 100000) + 1;
function esc(s) { return String(s); }
function render() { var sheet = WorksheetCore.buildSheet(PRACTICE_TEMPLATES, WS_DIFF, seed, FiboPoly); var probHTML = '', keyHTML = ''; sheet.sections.forEach(function (sec) { probHTML += '
'; keyHTML += '
'; }); probHTML += '
'; }); el('probWrap').innerHTML = probHTML; el('keyGrid').innerHTML = keyHTML; el('seedLine').textContent = 'Worksheet seed ' + sheet.seed + ' — generate again for a brand-new set.'; }
el('btnGen').addEventListener('click', function () { seed = Math.floor(Math.random() * 1000000000) + 1; var keyShown = !el('keySection').hasAttribute('hidden'); render(); if (keyShown) { el('keySection').removeAttribute('hidden'); el('btnKey').setAttribute('aria-pressed', 'true'); el('btnKey').textContent = 'Hide Answer Key'; } }); el('btnKey').addEventListener('click', function () { var ks = el('keySection'); var show = ks.hasAttribute('hidden'); if (show) { ks.removeAttribute('hidden'); } else { ks.setAttribute('hidden', ''); } el('btnKey').setAttribute('aria-pressed', show ? 'true' : 'false'); el('btnKey').textContent = show ? 'Hide Answer Key' : 'Show Answer Key'; }); el('btnPrint').addEventListener('click', function () { window.print(); }); render(); /*__WORKSHEET_GLUE_END__*/ })();
/*__WORKSHEET_SRC_END__*/