You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateAdditionProblem(level){constmaxNum=level*20+10;constnum1=getRandomInt(1,maxNum);constnum2=getRandomInt(1,maxNum);constcorrectAnswer=num1+num2;consttext=`What is ${num1} + ${num2}?`;constoptionsArray=generateDistractors(correctAnswer,[num1,num2],'+');constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/algebra.js
// js/generators/algebra.jsimport{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateAlgebraProblem(level){lettext='';letcorrectAnswer=0;letexpressionString='';// For substitution problemsletvars={};// Store variables used for distractors// *** NEW: Choose between Solving Equation and Substitution ***constproblemType=(level>=2&&Math.random()>0.5) ? 'substitution' : 'solve_equation';if(problemType==='solve_equation'){// Original logicconsta=(level>2) ? getRandomInt(2,4) : 1;constb=getRandomInt(1,10+level);constx=getRandomInt(2,8+level);// The answerconstc=a*x+b;correctAnswer=x;// Format the equation stringconstxTerm=a===1 ? 'x' : `${a}x`;text=`If ${xTerm} + ${b} = ${c}, what is the value of x?`;vars={a, b, c};// Store for distractors}else{// *** NEW: Substitution Problem ***constxValue=getRandomInt(2,10+level);vars.x=xValue;constexprType=getRandomInt(1,3);// Choose expression typeif(exprType===1){// Simple ax + bconsta=getRandomInt(2,5+level);constb=getRandomInt(1,15+level);correctAnswer=a*xValue+b;expressionString=`${a}x + ${b}`;vars.a=a;vars.b=b;}elseif(exprType===2){// Simple a(x - b)consta=getRandomInt(2,4+level);// Ensure x - b is not zero or too smallconstb=getRandomInt(1,xValue-1);correctAnswer=a*(xValue-b);expressionString=`${a}(x - ${b})`;vars.a=a;vars.b=b;}else{// Simple ax - bconsta=getRandomInt(2,5+level);// Ensure ax > bconstb=getRandomInt(1,a*xValue-1);correctAnswer=a*xValue-b;expressionString=`${a}x - ${b}`;vars.a=a;vars.b=b;}text=`If x = ${xValue}, what is the value of ${expressionString}?`;}// Generate distractors - might need specific logic based on problem typeconstoptionsArray=generateDistractors(correctAnswer,Object.values(vars),problemType);constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/conversion.js
import{getRandomInt,simplifyFraction,formatNumber}from'../helpers.js';import{findCorrectKey}from'./distractors.js';// Conversions between fractions, decimals, and percentagesexportfunctiongenerateConversionProblem(level){consttype=getRandomInt(1,3);lettext,correctAnswer;if(type===1){// Fraction to decimal (2 dp)constden=[2,4,5,8,10][getRandomInt(0,Math.min(4,level))];constnum=getRandomInt(1,den-1);constval=num/den;correctAnswer=formatNumber(val);text=`Express <sup>${num}</sup>⁄<sub>${den}</sub> as a decimal (to 2 decimal places).`;}elseif(type===2){// Decimal to fractionconstdenom=[2,4,5,8,10,100][getRandomInt(0,Math.min(5,level+1))];constnumerator=getRandomInt(1,denom-1);constdec=numerator/denom;constsimp=simplifyFraction(numerator,denom);correctAnswer=`${simp.num}/${simp.den}`;text=`Convert ${formatNumber(dec)} to a fraction in simplest form.`;}else{// Decimal to percentageconstperc=[10,20,25,50,75][getRandomInt(0,Math.min(4,level))];correctAnswer=`${perc}%`;text=`Express ${perc/100} as a percentage.`;}// Build simple options: correct plus near distractorsconstbaseVals=[correctAnswer];if(type===1||type===3){// decimal or percentage treat as numberconstval=parseFloat(correctAnswer);baseVals.push(val+0.1,val-0.1);}else{// fraction string may be manipulatedbaseVals.push('1/2','2/3','3/4');}constformatted=baseVals.map(formatNumber);constopts={};['A','B','C','D','E'].forEach((k,i)=>opts[k]=formatted[i%formatted.length]);constcorrect=findCorrectKey(opts,correctAnswer);return{ text,options: opts, correct, level };}
js/generators/data.js
import{getRandomInt}from'../helpers.js';import{findCorrectKey}from'./distractors.js';// Statistics: mean, median, mode, rangeexportfunctiongenerateDataProblem(level){// Generate datasetconstsize=getRandomInt(5,7);constdata=Array.from({length: size},()=>getRandomInt(1,level*10+5));data.sort((a,b)=>a-b);// Choose measureconsttypes=['mean','median','mode','range'];consttype=types[getRandomInt(0,types.length-1)];letcorrectAnswer;if(type==='mean'){constsum=data.reduce((a,b)=>a+b,0);correctAnswer=parseFloat((sum/size).toFixed(1));}elseif(type==='median'){correctAnswer=size%2===1 ? data[(size-1)/2] : (data[size/2-1]+data[size/2])/2;}elseif(type==='mode'){constfreq={};data.forEach(v=>freq[v]=(freq[v]||0)+1);constmaxFreq=Math.max(...Object.values(freq));constmodes=Object.keys(freq).filter(k=>freq[k]===maxFreq);correctAnswer=parseInt(modes[0]);}else{// rangecorrectAnswer=data[data.length-1]-data[0];}consttext=`Given the data set: ${data.join(', ')}, what is the ${type}?`;constoptsArr=[correctAnswer,correctAnswer+getRandomInt(1,3),correctAnswer-getRandomInt(1,3),data[0],data[data.length-1]];constformatted=optsArr.map(v=>v.toString());constoptions={};['A','B','C','D','E'].forEach((k,i)=>options[k]=formatted[i]);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}
js/generators/distractors.js
import{shuffleArray,getRandomInt,formatNumber,simplifyFraction}from'../helpers.js';exportfunctiongenerateDistractors(correctAnswer,operands=[],operation='',initialDistractors=[]){constformattedCorrect=formatNumber(correctAnswer);letdistractors=newSet(initialDistractors.map(formatNumber));distractors.add(formattedCorrect);// Common numeric variationsdistractors.add(formatNumber(correctAnswer+1));distractors.add(formatNumber(correctAnswer-1));if(correctAnswer>10){distractors.add(formatNumber(correctAnswer+10));distractors.add(formatNumber(correctAnswer-10));}if(operands.length===2){const[op1,op2]=operands;if(operation!=='+'&&formatNumber(op1+op2)!==formattedCorrect)distractors.add(formatNumber(op1+op2));if(operation!=='-'&&formatNumber(op1-op2)!==formattedCorrect)distractors.add(formatNumber(op1-op2));if(operation!=='*'&&formatNumber(op1*op2)!==formattedCorrect)distractors.add(formatNumber(op1*op2));if(operation!=='/'&&op2!==0&&Number.isInteger(op1/op2)&&formatNumber(op1/op2)!==formattedCorrect)distractors.add(formatNumber(op1/op2));distractors.add(formatNumber(op1));distractors.add(formatNumber(op2));}letoptions=Array.from(distractors).filter(d=>d!==undefined&&d!==null&&d!==''&&(formattedCorrect.startsWith('-') ? true : parseFloat(d.replace(/[^\d.-]/g,''))>=0));// Exclude correct for selectionoptions=options.filter(d=>d!==formattedCorrect);shuffleArray(options);letchosen=options.slice(0,4);chosen.push(formattedCorrect);// Fill if underfullletattempts=0;while(chosen.length<5&&attempts<20){letrnd=correctAnswer+getRandomInt(2,15)*(Math.random()>0.5 ? 1 : -1);letstr=formatNumber(rnd);if(!chosen.includes(str)&&(rnd>=0||formattedCorrect.startsWith('-')))chosen.push(str);attempts++;}chosen=[...newSet(chosen)];attempts=0;while(chosen.length<5&&attempts<10){letrnd=correctAnswer+getRandomInt(16,30)*(Math.random()>0.5 ? 1 : -1);letstr=formatNumber(rnd);if(!chosen.includes(str)&&(rnd>=0||formattedCorrect.startsWith('-')))chosen.push(str);attempts++;}if(!chosen.includes(formattedCorrect)){chosen.pop();chosen.push(formattedCorrect);}returnchosen.slice(0,5);}exportfunctiongenerateFractionDistractors(correctStr,level){letdistractors=newSet([correctStr]);try{const[num,den]=correctStr.split('/').map(Number);if(num>1)distractors.add(formatNumber(simplifyFraction(num-1,den)));elsedistractors.add(formatNumber(simplifyFraction(1,den+1)));distractors.add(formatNumber(simplifyFraction(num+1,den)));if(den>1)distractors.add(formatNumber(simplifyFraction(num,den-1)));elsedistractors.add(formatNumber(simplifyFraction(num+1,1)));distractors.add(formatNumber(simplifyFraction(num,den+1)));distractors.add(`${num*2}/${den*2}`);distractors.add(`${den}/${num}`);}catch(e){console.error("Error parsing fraction for distractors:",correctStr,e);}letoptions=Array.from(distractors).filter(d=>d!==correctStr&&d&&!d.includes('/0'));shuffleArray(options);letchosen=options.slice(0,4);chosen.push(correctStr);letattempts=0;while(chosen.length<5&&attempts<20){constrn=getRandomInt(1,10+level);constrd=getRandomInt(rn+1,15+level);constsf=simplifyFraction(rn,rd);constfrac=`${sf.num}/${sf.den}`;if(!chosen.includes(frac))chosen.push(frac);attempts++;}if(!chosen.includes(correctStr)){chosen.pop();chosen.push(correctStr);}returnchosen.slice(0,5);}exportfunctionformatOptions(arr,correctAnswer){constfa=formatNumber(correctAnswer);letformatted=arr.map(formatNumber);if(!formatted.includes(fa)){console.error("Correct answer missing before final formatting!",fa,formatted);if(formatted.length)formatted.pop();formatted.push(fa);}while(formatted.length<5){letrnd=correctAnswer+getRandomInt(5,25)*(Math.random()>0.5 ? 1 : -1);letstr=formatNumber(rnd);if(!formatted.includes(str))formatted.push(str);}formatted=[...newSet(formatted)];while(formatted.length<5){letrnd=correctAnswer+getRandomInt(26,50)*(Math.random()>0.5 ? 1 : -1);letstr=formatNumber(rnd);if(!formatted.includes(str))formatted.push(str);}if(!formatted.includes(fa))formatted[4]=fa;shuffleArray(formatted);constkeys=['A','B','C','D','E'];letopts={};for(leti=0;i<5;i++)opts[keys[i]]=formatted[i];returnopts;}exportfunctionfindCorrectKey(opts,correctAnswer){constfa=formatNumber(correctAnswer);for(constkinopts){if(opts[k]===fa)returnk;}console.error(`Correct answer key not found! Searched for "${fa}" in`,opts);for(constkinopts){if(String(opts[k])===String(correctAnswer))returnk;}return'A';}
js/generators/division.js
import{getRandomInt,formatNumber}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateDivisionProblem(level){constmaxDivisor=level+3;constdivisor=getRandomInt(2,maxDivisor);constquotient=getRandomInt(2,12+level);constdividend=divisor*quotient;constcorrectAnswer=quotient;consttext=`What is ${dividend} ÷ ${divisor}?`;letoptionsArray=generateDistractors(correctAnswer,[dividend,divisor],'/');if(!optionsArray.map(formatNumber).includes(formatNumber(dividend))&&optionsArray.length<5){optionsArray.push(dividend);}optionsArray=optionsArray.slice(0,5);constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
import{getRandomInt,simplifyFraction}from'../helpers.js';import{generateFractionDistractors,formatOptions,findCorrectKey}from'./distractors.js';// Operations on fractions: add, subtract, multiply, divideexportfunctiongenerateFractionOpsProblem(level){consttype=getRandomInt(1,4);// Base denominatorconstd1=getRandomInt(2,level+3);constn1=getRandomInt(1,d1-1);constd2=getRandomInt(2,level+3);constn2=getRandomInt(1,d2-1);lettext,correctNum,correctDen;if(type===1){// additiontext=`What is <sup>${n1}</sup>⁄<sub>${d1}</sub> + <sup>${n2}</sup>⁄<sub>${d2}</sub>?`;correctNum=n1*d2+n2*d1;correctDen=d1*d2;}elseif(type===2){// subtractiontext=`What is <sup>${n1}</sup>⁄<sub>${d1}</sub> - <sup>${n2}</sup>⁄<sub>${d2}</sub>?`;correctNum=n1*d2-n2*d1;correctDen=d1*d2;}elseif(type===3){// multiplicationtext=`What is <sup>${n1}</sup>⁄<sub>${d1}</sub> × <sup>${n2}</sup>⁄<sub>${d2}</sub>?`;correctNum=n1*n2;correctDen=d1*d2;}else{// divisiontext=`What is <sup>${n1}</sup>⁄<sub>${d1}</sub> ÷ <sup>${n2}</sup>⁄<sub>${d2}</sub>?`;correctNum=n1*d2;correctDen=d1*n2;}constsimp=simplifyFraction(correctNum,correctDen);constcorrectAnswer=`${simp.num}/${simp.den}`;constoptsArr=generateFractionDistractors(correctAnswer,level);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}
js/generators/geometry.js
import{getRandomInt}from'../helpers.js';import{findCorrectKey}from'./distractors.js';// Geometry: 2D shapes, angles, coordinatesexportfunctiongenerateGeometryProblem(level){consttype=getRandomInt(1,3);lettext,correctAnswer;letoptions;if(type===1){// Shape from number of sidesconstsides=getRandomInt(3,8);constnames={3:'triangle',4:'quadrilateral',5:'pentagon',6:'hexagon',7:'heptagon',8:'octagon'};correctAnswer=names[sides]||`${sides}-gon`;text=`What is the name of a ${sides}-sided polygon?`;constchoices=Object.values(names).slice(0,5);options={};['A','B','C','D'].forEach((k,i)=>options[k]=choices[i]);options['E']=correctAnswer;}elseif(type===2){// Interior angle of regular polygonconstn=getRandomInt(3,8);correctAnswer=((n-2)*180/n);text=`What is the interior angle (in degrees) of a regular ${n}-sided polygon?`;constoptsArr=[correctAnswer,correctAnswer+10,correctAnswer-10,180/n,360/n];constformatted=optsArr.map(v=>v.toString());options={};['A','B','C','D','E'].forEach((k,i)=>options[k]=formatted[i]);}else{// Coordinates quadrantconstx=getRandomInt(-10,10);consty=getRandomInt(-10,10);text=`Which quadrant is the point (${x}, ${y}) in?`;if(x>0&&y>0)correctAnswer='I';elseif(x<0&&y>0)correctAnswer='II';elseif(x<0&&y<0)correctAnswer='III';elseif(x>0&&y<0)correctAnswer='IV';elsecorrectAnswer='On axis';constopts={'A':'I','B':'II','C':'III','D':'IV','E':'On axis'};options=opts;}constoptionsFinal=typeofoptions!=='undefined' ? options : {};constcorrect=findCorrectKey(optionsFinal,correctAnswer);return{ text,options: optionsFinal, correct, level };}
// js/generators/measurement.jsimport{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateMeasurementProblem(level){constproblemType=Math.random();lettext='';letcorrectAnswer=0;letvalue=0;// *** UPDATED: Added 'g', 'kg', 'ml', 'l' ***constunits=['mm','cm','m','km','g','kg','ml','l'];constconversionFactors={'mm': {'cm': 10,'m': 1000},'cm': {'mm': 0.1,'m': 100,'km': 100000},'m': {'mm': 0.001,'cm': 0.01,'km': 1000},'km': {'m': 0.001,'cm': 0.00001},'g': {'kg': 1000},// NEW'kg': {'g': 0.001},// NEW'ml': {'l': 1000},// NEW'l': {'ml': 0.001}// NEW};if(problemType<0.5){// *** UPDATED: Unit Conversion (Length, Mass, Capacity) ***letfromUnit,toUnit;// Ensure compatible conversion existsdo{fromUnit=units[getRandomInt(0,units.length-1)];constpossibleConversions=Object.keys(conversionFactors[fromUnit]||{});if(possibleConversions.length>0){toUnit=possibleConversions[getRandomInt(0,possibleConversions.length-1)];}else{toUnit=null;// Force retry if no conversions defined for fromUnit}}while(!toUnit||!conversionFactors[fromUnit]?.[toUnit]);constfactor=conversionFactors[fromUnit][toUnit];// Adjust random value based on unit to keep numbers reasonableletmaxMultiplier=50+level*20;if(fromUnit==='mm'||fromUnit==='ml'||fromUnit==='g')maxMultiplier*=10;if(fromUnit==='km'||fromUnit==='kg'||fromUnit==='l')maxMultiplier=Math.max(10,Math.floor(maxMultiplier/100));value=getRandomInt(1,maxMultiplier);// Ensure nice numbers for division if neededif(factor>1){// If dividing for conversion (e.g., m to km, factor is 1000)value=Math.ceil(value/factor)*factor;// Make value divisible by factorif(value===0)value=factor;// Avoid value being 0}correctAnswer=parseFloat((value/factor).toFixed(4));// Use parseFloat to remove trailing zeros if appropriate// Remove unnecessary trailing zeros and potentially '.0' for displaycorrectAnswer=+correctAnswer;text=`Convert ${value}${fromUnit} to ${toUnit}`;}elseif(problemType<0.8){// Perimeter/Area of Rectangle (Original)constlength=getRandomInt(2,10+level*2);constwidth=getRandomInt(1,length);// Width <= Lengthif(Math.random()<0.5){// PerimetercorrectAnswer=2*(length+width);text=`What is the perimeter of a rectangle with length ${length}cm and width ${width}cm?`;}else{// AreacorrectAnswer=length*width;text=`What is the area of a rectangle with length ${length}cm and width ${width}cm?`;}}else{// Time Calculation (Original)conststartHour=getRandomInt(0,22);conststartMinute=getRandomInt(0,5)*10;// 0, 10, 20, 30, 40, 50constdurationHours=getRandomInt(0,3+level);constdurationMinutes=getRandomInt(1,5)*10;// 10, 20, 30, 40, 50conststartTime=newDate(2024,0,1,startHour,startMinute);// Use a fixed dateconstendTime=newDate(startTime.getTime()+(durationHours*60+durationMinutes)*60000);constformatTime=(date)=>`${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}`;correctAnswer=formatTime(endTime);text=`A programme starts at ${formatTime(startTime)} and lasts for ${durationHours} hours and ${durationMinutes} minutes. What time does it finish?`;}// Generate appropriate distractorsconstoptionsArray=generateDistractors(correctAnswer,[],'',typeofcorrectAnswer==='string' ? undefined : [value]);// Pass value for numeric typesconstformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/multiplication.js
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateMultiplicationProblem(level){constmaxFactor=level+4;constnum1=getRandomInt(2,maxFactor);constnum2=getRandomInt(2,maxFactor+level);constcorrectAnswer=num1*num2;consttext=`What is ${num1} × ${num2}?`;constoptionsArray=generateDistractors(correctAnswer,[num1,num2],'*');constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/multistep.js
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateMultiStepProblem(level){constpacks=getRandomInt(2,4+level);constperPack=getRandomInt(5,10+level);consttotalInit=packs*perPack;constused=getRandomInt(3,Math.max(4,Math.floor(totalInit*0.6)));if(totalInit<=used)returngenerateMultiStepProblem(level);constcorrectAnswer=totalInit-used;constcontexts=[{item:'stickers',container:'pack',p:'buys',s:'uses'},{item:'cakes',container:'box',p:'bakes',s:'eats'},{item:'pencils',container:'box',p:'gets',s:'gives away'}];constctx=contexts[getRandomInt(0,contexts.length-1)];consttext=`David ${ctx.p}${packs}${ctx.container}s of ${ctx.item}. Each ${ctx.container} has ${perPack}${ctx.item}. He then ${ctx.s}${used}${ctx.item}. How many ${ctx.item} does he have left?`;letopts=[totalInit,packs*(perPack-used),totalInit+used,used,perPack,packs];opts=[...newSet(opts.filter(o=>typeofo==='number'&&o>0&&o!==correctAnswer))];constoptionsArray=generateDistractors(correctAnswer,[],'',opts);constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/number.js
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';// Place Value: ask value of digit in a numberexportfunctiongeneratePlaceValueProblem(level){// Generate a number up to millions depending on levelconstmaxDigits=Math.min(6,level+1);letnum='';for(leti=0;i<maxDigits;i++)num+=getRandomInt(i===0?1:0,9);constdigits=num.split('');constidx=getRandomInt(0,digits.length-1);constdigit=digits[idx];constcorrectAnswer=parseInt(digit)*Math.pow(10,digits.length-1-idx);consttext=`In the number ${num}, what is the place value of the digit ${digit}?`;constoptsArr=generateDistractors(correctAnswer,[],'',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}// Rounding to nearest 10,100,1000exportfunctiongenerateRoundingProblem(level){constbase=Math.pow(10,getRandomInt(1,Math.min(3,level+1)));constnum=getRandomInt(base/2,base*10);constcorrectAnswer=Math.round(num/base)*base;consttext=`Round ${num} to the nearest ${base}.`;constoptsArr=generateDistractors(correctAnswer,[],'',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}// Negative numbers: compare or add/subtract negativesexportfunctiongenerateNegativeNumbersProblem(level){// Compare two negativesconsta=-getRandomInt(1,level*10);constb=-getRandomInt(1,level*10);consttext=`Which is greater: ${a} or ${b}?`;constcorrectAnswer=a>b ? a : b;constoptsArr=[a,b];constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}// Order of Operations: simple BODMASexportfunctiongenerateOrderOfOperationsProblem(level){consta=getRandomInt(1,10);constb=getRandomInt(1,10);constc=getRandomInt(1,10);constd=getRandomInt(1,10);constexpr=`${a} + ${b} × ${c} - ${d}`;constcorrectAnswer=a+b*c-d;consttext=`Calculate: ${expr}`;constoptsArr=generateDistractors(correctAnswer,[],'',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}// Multiply/Divide by 10,100,1000exportfunctiongenerateShiftProblem(level){constbase=[10,100,1000][getRandomInt(0,Math.min(2,level))];constnum=getRandomInt(1,level*50);constop=Math.random()>0.5 ? '×' : '÷';constcorrectAnswer=op==='×' ? num*base : num/base;consttext=`What is ${num}${op}${base}?`;constoptsArr=generateDistractors(correctAnswer,[],'',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}
js/generators/numberProperties.js
import{getRandomInt,gcd}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';// Number Properties: factors, multiples, prime, square, cube, HCF, LCMexportfunctiongenerateNumberPropertiesProblem(level){consttype=getRandomInt(1,5);lettext,correctAnswer;if(type===1){// Factors of a numberconstn=getRandomInt(10,level*10+10);constfactors=[];for(leti=1;i<=n;i++)if(n%i===0)factors.push(i);correctAnswer=factors.join(', ');text=`List all factors of ${n}.`;// For listing answers, put correct answer as single string, distractors placeholdersconstoptions={A: correctAnswer,B: ' ',C: ' ',D: ' ',E: ' '};return{ text, options,correct: 'A', level };}elseif(type===2){// Prime, square or cube classificationconstn=getRandomInt(2,level*10+20);constisPrime=[...Array(n).keys()].filter(i=>i>1&&n%i===0).length===1;constsq=Number.isInteger(Math.sqrt(n));constcb=Number.isInteger(Math.cbrt(n));letcategory=isPrime ? 'prime' : sq ? 'square' : cb ? 'cube' : 'none';correctAnswer=category;text=`Is ${n} a prime number, square number, cube number, or none?`;constchoices=['prime','square','cube','none'];constoptions={};['A','B','C','D'].forEach((k,i)=>options[k]=choices[i]);return{ text, options,correct: Object.keys(options).find(k=>options[k]===correctAnswer), level };}elseif(type===3){// HCF of two numbersconsta=getRandomInt(10,level*10+20);constb=getRandomInt(10,level*10+20);correctAnswer=gcd(a,b);text=`What is the highest common factor (HCF) of ${a} and ${b}?`;constoptsArr=generateDistractors(correctAnswer,[a,b],'hcf',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}elseif(type===4){// LCM of two numbersconsta=getRandomInt(2,level*5+5);constb=getRandomInt(2,level*5+5);consth=gcd(a,b);correctAnswer=(a*b)/h;text=`What is the lowest common multiple (LCM) of ${a} and ${b}?`;constoptsArr=generateDistractors(correctAnswer,[a,b],'lcm',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}else{// Multiples of a numberconstn=getRandomInt(2,level*5+5);correctAnswer=n*getRandomInt(2,5);text=`Give a multiple of ${n}.`;constoptsArr=generateDistractors(correctAnswer,[n],'multiple',[]);constoptions=formatOptions(optsArr,correctAnswer);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}}
js/generators/percentage.js
// js/generators/percentage.jsimport{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongeneratePercentageProblem(level){constpercentages=[10,20,25,50,75];// Keep simple percentages for nowif(level>2)percentages.push(5,15,30,40,60);// Add more at higher levelsconstpercent=percentages[getRandomInt(0,percentages.length-1)];lettext='';letcorrectAnswer=0;letoriginalAmount=0;// *** NEW: Add Increase/Decrease Types ***constproblemType=Math.random();if(problemType<0.5||level<2){// Type 1: Find percentage of amount (Original)// Generate amount divisible by necessary factors for the percentageletmultiplier=1;if(percent===10||percent===20||percent===30||percent===40||percent===60)multiplier=10;if(percent===25||percent===75)multiplier=4;if(percent===50)multiplier=2;if(percent===5||percent===15)multiplier=20;// Need divisibility by 20constbaseValue=getRandomInt(1,10+level*2);originalAmount=baseValue*multiplier;correctAnswer=(percent/100)*originalAmount;text=`What is ${percent}% of ${originalAmount}?`;}elseif(problemType<0.75){// Type 2: Percentage Increaseletmultiplier=1;// As above to ensure clean calculationsif(percent===10||percent===20||percent===30||percent===40||percent===60)multiplier=10;if(percent===25||percent===75)multiplier=4;if(percent===50)multiplier=2;if(percent===5||percent===15)multiplier=20;constbaseValue=getRandomInt(1,10+level*2);originalAmount=baseValue*multiplier;constincreaseAmount=(percent/100)*originalAmount;correctAnswer=originalAmount+increaseAmount;text=`Increase ${originalAmount} by ${percent}%`;}else{// Type 3: Percentage Decreaseletmultiplier=1;// As aboveif(percent===10||percent===20||percent===30||percent===40||percent===60)multiplier=10;if(percent===25||percent===75)multiplier=4;if(percent===50)multiplier=2;if(percent===5||percent===15)multiplier=20;constbaseValue=getRandomInt(1,10+level*2);originalAmount=baseValue*multiplier;constdecreaseAmount=(percent/100)*originalAmount;correctAnswer=originalAmount-decreaseAmount;text=`Decrease ${originalAmount} by ${percent}%`;}// Generate distractors, passing original amount might be helpfulconstoptionsArray=generateDistractors(correctAnswer,[originalAmount,percent],'%');constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/probability.js
import{getRandomInt}from'../helpers.js';import{findCorrectKey}from'./distractors.js';// Probability: basic probability questionsexportfunctiongenerateProbabilityProblem(level){// Simple marble bagconsttotal=getRandomInt(5,level*5+5);constfavorable=getRandomInt(1,total-1);consttext=`A bag contains ${total} marbles, of which ${favorable} are red. What is the probability of drawing a red marble?`;// Represent as fractionconstnum=favorable;constden=total;constcorrectAnswer=`${num}/${den}`;constoptsArr=[correctAnswer,`${favorable+1}/${den}`,`${num}/${den-1}`,`${Math.round((favorable/total)*100)}%`,`${Math.round((total-favorable)/total*10)}/10`];constoptions={};['A','B','C','D','E'].forEach((k,i)=>options[k]=optsArr[i]);constcorrect=findCorrectKey(options,correctAnswer);return{ text, options, correct, level };}
js/generators/ratio.js
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateRatioProblem(level){constr1=getRandomInt(1,3+level);constr2=getRandomInt(1,5+level);if(r1===r2)returngenerateRatioProblem(level);consttotalParts=r1+r2;constmul=getRandomInt(2,5+level);consttotal=totalParts*mul;constshare1=r1*mul;constshare2=r2*mul;constpart=Math.random()>0.5?1:2;constcorrectAnswer=part===1? share1 : share2;consttext=`Share £${total} in the ratio ${r1}:${r2}. What is the value of the ${part===1?'first':'second'} share?`;letopts=[part===1? share2 : share1,total,mul,total/r1,total/r2];opts=opts.map(o=>Number.isFinite(o)?(Number.isInteger(o)?o:parseFloat(o.toFixed(1))):undefined).filter(o=>o!==undefined);opts=[...newSet(opts)];constoptionsArray=generateDistractors(correctAnswer,[],'',opts);constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/sequence.js
import{getRandomInt}from'../helpers.js';exportfunctiongenerateSequenceProblem(level){letsequenceType;// Determine sequence type based on levelif(level<=1){sequenceType='simple_arithmetic_pos';}elseif(level===2){// Add chance for negative step arithmeticsequenceType=(Math.random()<0.6) ? 'simple_arithmetic_pos' : 'simple_arithmetic_neg';}elseif(level===3){// Add chance for geometric or varying differenceconstrand=Math.random();if(rand<0.4)sequenceType='simple_arithmetic_pos_neg';// Mix pos/neg stepselseif(rand<0.7)sequenceType='simple_geometric';elsesequenceType='varying_difference';}else{// Level 4+// More complex patterns, maybe ask for nth term or missing internal termconstrand=Math.random();if(rand<0.3)sequenceType='simple_arithmetic_pos_neg';elseif(rand<0.5)sequenceType='simple_geometric';elseif(rand<0.7)sequenceType='varying_difference';elsesequenceType='complex_pattern';// e.g., squares, alternating}letstart,step,ratio,terms,missingIndex,questionType,text,correctAnswer,potentialDistractors;constsequenceLength=6;// Generate 6 terms usually// --- Generate sequence based on type ---switch(sequenceType){case'simple_arithmetic_neg':
case'simple_arithmetic_pos':
case'simple_arithmetic_pos_neg':
// Similar logic to original, but allow negative steps for neg/pos_negstart=getRandomInt(1,30+level*5);if(sequenceType==='simple_arithmetic_neg'||(sequenceType==='simple_arithmetic_pos_neg'&&Math.random()<0.5)){step=getRandomInt(-(2+level),-2);// Negative step// Ensure start is high enough for a few termsif(start+step*(sequenceLength-1)<0){start=Math.abs(step*(sequenceLength-1))+getRandomInt(5,15);}}else{step=getRandomInt(2,5+level);// Positive step}terms=[start];for(leti=1;i<sequenceLength;i++)terms.push(terms[i-1]+step);break;case'simple_geometric':
// Simple integer ratios (x2, x3, /2, /3 maybe)ratio=(Math.random()<0.5) ? getRandomInt(2,3+Math.floor(level/2)) : 1/getRandomInt(2,3);start=(ratio>1) ? getRandomInt(1,5+level) : getRandomInt(10,30+level*5)*Math.abs(1/ratio);// Ensure start is divisible if ratio < 1// Ensure start is appropriately divisible for division sequencesif(ratio<1){letdenominator=1/ratio;// Ensure start is divisible enough timesletrequiredMultiple=Math.pow(denominator,sequenceLength-1);start=Math.ceil(getRandomInt(1,5)*requiredMultiple/denominator)*denominator;// Generate a suitable multipleif(start===0)start=requiredMultiple;// Handle edge case if random is 0}terms=[start];for(leti=1;i<sequenceLength;i++){letnextTerm=terms[i-1]*ratio;// Handle potential floating point issues slightly for simple divisionif(ratio<1&&nextTerm!==Math.floor(nextTerm)){// This shouldn't happen with the start calculation, but as a fallback:nextTerm=Math.round(nextTerm);// Or maybe stop sequence? For 11+ stick to integers mostly.}// Stop if terms get too large/small or non-integer for divisionif(Math.abs(nextTerm)>1000||Math.abs(nextTerm)<0.1&&nextTerm!==0)break;terms.push(nextTerm);}// Recalculate sequenceLength if it broke early// sequenceLength = terms.length; // Re-evaluate if this is needed or if we just fail generationif(terms.length<4)returngenerateSequenceProblem(level);// Regenerate if sequence is too shortbreak;case'varying_difference': {start=getRandomInt(1,20+level*3);letinitialStep=getRandomInt(1,3+level);letstepChange=(Math.random()<0.5) ? 1 : -1;// Difference increases or decreasesif(initialStep<=1&&stepChange===-1)stepChange=1;// Avoid step becoming too small too quicklyterms=[start];letcurrentStep=initialStep;for(leti=1;i<sequenceLength;i++){terms.push(terms[i-1]+currentStep);currentStep+=stepChange;}break;}case'complex_pattern': {// e.g., square numbers, alternating operationconstpatternType=Math.random();if(patternType<0.5){// Square numbers +/- constantconstopRand=Math.random();constconstOffset=getRandomInt(-3,3);terms=[];for(leti=1;i<=sequenceLength;i++){terms.push(i*i+constOffset);}}else{// Alternating operation +a, -bstart=getRandomInt(10,30+level*5);letstepA=getRandomInt(2,5+level);letstepB=getRandomInt(-(5+level),-2);terms=[start];for(leti=1;i<sequenceLength;i++){terms.push(terms[i-1]+(i%2===1 ? stepA : stepB));// Alternate +a, -b}}break;}default: // Fallback to simple arithmeticstart=getRandomInt(1,20);step=getRandomInt(2,5);terms=[start];for(leti=1;i<sequenceLength;i++)terms.push(terms[i-1]+step);break;// Should not happen}// --- Determine Question Type & Missing Index ---// Allow asking for internal missing term at higher levelsletminVisible=3;// Need at least 3 terms shown usuallyif(level>=3&&Math.random()<0.4){// 40% chance to ask for internal missing term level 3+questionType='missing_internal';// Ensure we don't pick index 0, 1, or 2 as missing (need terms before it)missingIndex=getRandomInt(minVisible,terms.length-2);// Don't pick the very last one either for this typecorrectAnswer=terms[missingIndex];// Show terms before and after missing one. e.g., [a, b, c, __, e, f]// Need to show terms up to missingIndex-1 and from missingIndex+1lettermsBefore=terms.slice(0,missingIndex).join(', ');// For 11+ simplicity, maybe just show the term after? Or just before?// Let's show terms before, placeholder, and term afterlettermAfter=terms[missingIndex+1];text=`Find the missing number: ${termsBefore}, __, ${termAfter}, ...`;}else{// Ask for the next term (most common)questionType='next_term';// Decide how many terms to show (3, 4 or 5)letshowCount=getRandomInt(minVisible,terms.length-1);missingIndex=showCount;// The index of the term *after* the last shown termcorrectAnswer=terms[missingIndex];text=`What is the next number in the sequence: ${terms.slice(0,missingIndex).join(', ')}, ...?`;}// Handle case where sequence generation was shorter than plannedif(missingIndex>=terms.length){// This might happen if geometric sequence terms grew too fast/small// Or maybe we didn't generate enough terms for the chosen question type// Simplest fix: regenerate the problemconsole.warn("Regenerating sequence due to length issue.");returngenerateSequenceProblem(level);}// --- Generate Distractors ---// This part needs careful thought based on sequence type and likely errorspotentialDistractors=newSet();// Use a Set to avoid duplicates easily// Generic distractorsif(missingIndex>0)potentialDistractors.add(terms[missingIndex-1]);// Previous termif(missingIndex<terms.length-1)potentialDistractors.add(terms[missingIndex+1]);// Next term (if asking for internal)potentialDistractors.add(correctAnswer+getRandomInt(1,3));// Close incorrectpotentialDistractors.add(correctAnswer-getRandomInt(1,3));// Close incorrect// Type-specific distractorsif(sequenceType.includes('arithmetic')){if(step){if(missingIndex>0)potentialDistractors.add(terms[missingIndex-1]+(step>0 ? step+1 : step-1));// Incorrect step calculationif(missingIndex>1)potentialDistractors.add(terms[missingIndex-1]+terms[missingIndex-2]);// Fibonacci mistake}}elseif(sequenceType.includes('geometric')){if(ratio&&missingIndex>0){potentialDistractors.add(terms[missingIndex-1]+ratio);// Add ratio instead of multiplypotentialDistractors.add(terms[missingIndex-1]*(ratio+(ratio>1 ? 1 : -0.5)));// Incorrect ratioif(ratio<1)potentialDistractors.add(terms[missingIndex-1]/(1/ratio+1));// Incorrect division}}elseif(sequenceType==='varying_difference'){// Continue pattern incorrectly, e.g. use last step againif(missingIndex>1){letlastStepUsed=terms[missingIndex-1]-terms[missingIndex-2];potentialDistractors.add(terms[missingIndex-1]+lastStepUsed);}}elseif(sequenceType==='complex_pattern'){// Add distractors based on misinterpreting the specific pattern (e.g., wrong alternating step)if(terms.length>2){potentialDistractors.add(terms[missingIndex-1]+(terms[missingIndex-1]-terms[missingIndex-2]));// Repeat last difference}}// Filter out the correct answer from distractors and ensure they are numberspotentialDistractors.delete(correctAnswer);letdistractors=Array.from(potentialDistractors).filter(d=>typeofd==='number'&&Number.isInteger(d));// Stick to integers for 11+ usually// --- Finalize Options (Needs external helper functions) ---// Assume generateDistractors intelligently selects from the pool and adds random ones if needed// Assume formatOptions creates { A: val1, B: val2, ... }// Assume findCorrectKey finds the letter for the correct answer// **Simulation of final steps:**letfinalOptionsRaw=[correctAnswer];// Shuffle distractors and pick 3-4 unique onesdistractors.sort(()=>Math.random()-0.5);for(leti=0;i<Math.min(distractors.length,4);i++){finalOptionsRaw.push(distractors[i]);}// Ensure we have 4 or 5 options total, add random if neededwhile(finalOptionsRaw.length<5){letrandomOpt=correctAnswer+getRandomInt(-10,10);if(randomOpt!==correctAnswer&&!finalOptionsRaw.includes(randomOpt)){finalOptionsRaw.push(randomOpt);}}// Shuffle the final raw options before assigning lettersfinalOptionsRaw.sort(()=>Math.random()-0.5);// --- !! Replace below with calls to your actual helper functions !! ---// Simulated formatted options (replace with call to formatOptions)constformattedOptions={};constkeys=['A','B','C','D','E'];letcorrectKey='';finalOptionsRaw.slice(0,5).forEach((opt,index)=>{letkey=keys[index];formattedOptions[key]=opt;if(opt===correctAnswer){correctKey=key;}});// If correctAnswer wasn't included somehow (error state), handle itif(!correctKey){console.error("Correct answer missing from final options!");// Simple fix: put correct answer as option A and reshuffle othersletotherOptions=finalOptionsRaw.filter(o=>o!==correctAnswer);otherOptions.sort(()=>Math.random()-0.5);formattedOptions['A']=correctAnswer;correctKey='A';keys.slice(1).forEach((key,index)=>{if(index<otherOptions.length){formattedOptions[key]=otherOptions[index];}else{// Need fallback if not enough unique options generatedformattedOptions[key]=correctAnswer+index+1;// Placeholder}});}// --- !! End of section to replace !! ---return{
text,options: formattedOptions,// Use the output from your formatOptions functioncorrect: correctKey,// Use the output from your findCorrectKey function
level,debug_sequence: terms,// Optional: For debugging purposesdebug_type: sequenceType// Optional: For debugging};}// --- Example Usage (Illustrative) ---/*// Assume generateDistractors, formatOptions, findCorrectKey exist elsewherefor (let lvl = 1; lvl <= 4; lvl++) { console.log(`\n--- Level ${lvl} Problem ---`); let problem = generateSequenceProblem(lvl); console.log(`Type: ${problem.debug_type}`); console.log(`Sequence: ${problem.debug_sequence.join(', ')}`); console.log(problem.text); console.log(problem.options); console.log(`Correct Answer Key: ${problem.correct} (Value: ${problem.options[problem.correct]})`);}*/
js/generators/simpleword.js
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateSimpleWordProblem(level){constopers=['+','-','*'];constoperation=opers[getRandomInt(0,Math.min(2,level))];constmaxNum=level*15+10;letnum1=getRandomInt(5,maxNum);letnum2=getRandomInt(2,operation==='*' ? (level+4) : num1-1);if(operation==='-'&&num1<num2)[num1,num2]=[num2,num1];if(operation==='-'&&num1===num2)num1++;constcontexts=[{item:'apples',p:'buys',s:'sells'},{item:'books',p:'reads',s:'returns'},{item:'sweets',p:'gets',s:'eats'},{item:'marbles',p:'finds',s:'loses'}];constctx=contexts[getRandomInt(0,contexts.length-1)];lettext='';letcorrectAnswer=0;switch(operation){case'+':
text=`Sarah has ${num1}${ctx.item}. She ${ctx.p}${num2} more. How many ${ctx.item} does she have now?`;correctAnswer=num1+num2;break;case'-':
text=`John starts with ${num1}${ctx.item}. He ${ctx.s}${num2}. How many are left?`;correctAnswer=num1-num2;break;case'*':
text=`There are ${num1} bags. Each bag contains ${num2}${ctx.item}. How many ${ctx.item} are there in total?`;correctAnswer=num1*num2;break;}constoptionsArray=generateDistractors(correctAnswer,[num1,num2],operation);constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/speed.js
// js/generators/speed.jsimport{getRandomInt}from'../helpers.js';// Assuming distractors.js handles basic numerical distractors if neededimport{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateSpeedProblem(level){lettext='';letcorrectAnswer=0;consttype=Math.random();// Choose between distance, time, or speed calculationif(type<0.4){// Calculate Distance (Original)constspeed=getRandomInt(10,level*10+40);// km/hconsttime=getRandomInt(2,level+4);// hourscorrectAnswer=speed*time;text=`How far do you travel going at ${speed} km/h for ${time} hours? (in km)`;}elseif(type<0.8){// Calculate Time (Original - slight adjustment for non-integer times)constspeed=getRandomInt(10,level*10+40);// km/h// Ensure time is reasonable, allow decimals for timeconsttimeHours=getRandomInt(1,level+3)+(Math.random()<0.5 ? 0.5 : 0);// e.g., 2 hrs, 2.5 hrs etc.constdistance=speed*timeHours;correctAnswer=timeHours;// Adjust text slightly if time is .5consttimeText=timeHours%1===0 ? `${timeHours} hours` : `${Math.floor(timeHours)} hours and 30 minutes`;text=`How long does it take to travel ${distance} km at ${speed} km/h?`;// Answer expected in hours e.g. 2.5}else{// *** NEW: Calculate Speed ***consttime=getRandomInt(2,level+4);// hours// Generate speed first to ensure distance/time gives a nice speed answerconstspeed=getRandomInt(10,level*10+40);// km/hconstdistance=speed*time;correctAnswer=speed;text=`What speed is needed to travel ${distance} km in ${time} hours? (in km/h)`;}// Generate distractors (using the generic numerical generator for simplicity here)// You might want more specific distractors based on common S=D/T errorsconstoptionsArray=generateDistractors(correctAnswer,[],'');// Provide empty array/op for generic numericconstformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}
js/generators/subtraction.js
import{getRandomInt}from'../helpers.js';import{generateDistractors,formatOptions,findCorrectKey}from'./distractors.js';exportfunctiongenerateSubtractionProblem(level){constmaxNum=level*20+10;letnum1=getRandomInt(5,maxNum);letnum2=getRandomInt(1,num1-1);if(Math.random()>0.7&&level>1)[num1,num2]=[num2,num1];constcorrectAnswer=num1-num2;consttext=`What is ${num1} - ${num2}?`;constoptionsArray=generateDistractors(correctAnswer,[num1,num2],'-');constformattedOptions=formatOptions(optionsArray,correctAnswer);constcorrect=findCorrectKey(formattedOptions,correctAnswer);return{ text,options: formattedOptions, correct, level };}