fractions add/sub fix
This commit is contained in:
@@ -1,12 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import type { MathProblem, Difficulty } from "@/lib/problems/types";
|
import type { MathProblem, Difficulty } from "@/lib/problems/types";
|
||||||
import { checkFractionAnswer, checkDecimalAnswer, checkRatioAnswer, checkIntegerAnswer, type AnswerResult } from "@/lib/math/validation";
|
import {
|
||||||
|
checkFractionAnswer,
|
||||||
|
checkDecimalAnswer,
|
||||||
|
checkRatioAnswer,
|
||||||
|
checkIntegerAnswer,
|
||||||
|
parseStrictDecimal,
|
||||||
|
parseStrictSignedDecimal,
|
||||||
|
parseStrictSignedInt,
|
||||||
|
type AnswerResult,
|
||||||
|
} from "@/lib/math/validation";
|
||||||
import { FractionInput, DecimalInput, RatioInput } from "./fraction-input";
|
import { FractionInput, DecimalInput, RatioInput } from "./fraction-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { MathDisplay } from "@/components/math/math-display";
|
||||||
import katex from "katex";
|
import katex from "katex";
|
||||||
|
|
||||||
interface PracticeSectionProps {
|
interface PracticeSectionProps {
|
||||||
@@ -24,7 +34,18 @@ const activeDifficultyStyle: Record<UnitColor, string> = {
|
|||||||
"unit-4": "bg-unit-4 text-white",
|
"unit-4": "bg-unit-4 text-white",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RECENT_PROBLEM_LIMIT = 8;
|
||||||
|
const MAX_GENERATION_RETRIES = 12;
|
||||||
|
|
||||||
|
function getProblemKey(problem: MathProblem): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
prompt: problem.prompt,
|
||||||
|
answer: problem.answer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function PracticeSection({ title, generator, unitColor = "unit-1" }: PracticeSectionProps) {
|
export function PracticeSection({ title, generator, unitColor = "unit-1" }: PracticeSectionProps) {
|
||||||
|
const recentProblemKeysRef = useRef<string[] | null>(null);
|
||||||
const [difficulty, setDifficulty] = useState<Difficulty>(1);
|
const [difficulty, setDifficulty] = useState<Difficulty>(1);
|
||||||
const [problem, setProblem] = useState<MathProblem>(() => generator(1));
|
const [problem, setProblem] = useState<MathProblem>(() => generator(1));
|
||||||
const [userAnswer, setUserAnswer] = useState<Record<string, string>>({});
|
const [userAnswer, setUserAnswer] = useState<Record<string, string>>({});
|
||||||
@@ -33,15 +54,39 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
const [hintIndex, setHintIndex] = useState(0);
|
const [hintIndex, setHintIndex] = useState(0);
|
||||||
const [showSolution, setShowSolution] = useState(false);
|
const [showSolution, setShowSolution] = useState(false);
|
||||||
const [score, setScore] = useState({ correct: 0, total: 0 });
|
const [score, setScore] = useState({ correct: 0, total: 0 });
|
||||||
|
const isFullyCorrect = result?.correct === true && result.simplified === true;
|
||||||
|
|
||||||
|
const generateUniqueProblem = useCallback((diff: Difficulty): MathProblem => {
|
||||||
|
const recentProblemKeys = recentProblemKeysRef.current ?? [getProblemKey(problem)];
|
||||||
|
let nextProblem = generator(diff);
|
||||||
|
let nextKey = getProblemKey(nextProblem);
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
recentProblemKeys.includes(nextKey) &&
|
||||||
|
retries < MAX_GENERATION_RETRIES
|
||||||
|
) {
|
||||||
|
nextProblem = generator(diff);
|
||||||
|
nextKey = getProblemKey(nextProblem);
|
||||||
|
retries += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
recentProblemKeysRef.current = [
|
||||||
|
nextKey,
|
||||||
|
...recentProblemKeys.filter((key) => key !== nextKey),
|
||||||
|
].slice(0, RECENT_PROBLEM_LIMIT);
|
||||||
|
|
||||||
|
return nextProblem;
|
||||||
|
}, [generator, problem]);
|
||||||
|
|
||||||
const generateNew = useCallback((diff: Difficulty) => {
|
const generateNew = useCallback((diff: Difficulty) => {
|
||||||
setProblem(generator(diff));
|
setProblem(generateUniqueProblem(diff));
|
||||||
setUserAnswer({});
|
setUserAnswer({});
|
||||||
setResult(null);
|
setResult(null);
|
||||||
setShowHint(false);
|
setShowHint(false);
|
||||||
setHintIndex(0);
|
setHintIndex(0);
|
||||||
setShowSolution(false);
|
setShowSolution(false);
|
||||||
}, [generator]);
|
}, [generateUniqueProblem]);
|
||||||
|
|
||||||
function checkAnswer() {
|
function checkAnswer() {
|
||||||
const answer = problem.answer;
|
const answer = problem.answer;
|
||||||
@@ -49,9 +94,9 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
|
|
||||||
switch (answer.kind) {
|
switch (answer.kind) {
|
||||||
case "fraction": {
|
case "fraction": {
|
||||||
const num = parseInt(userAnswer.numerator || "0");
|
const num = parseStrictSignedInt(userAnswer.numerator || "");
|
||||||
const den = parseInt(userAnswer.denominator || "0");
|
const den = parseStrictSignedInt(userAnswer.denominator || "");
|
||||||
if (isNaN(num) || isNaN(den)) {
|
if (num === null || den === null) {
|
||||||
res = { correct: false, message: "Enter valid numbers" };
|
res = { correct: false, message: "Enter valid numbers" };
|
||||||
} else {
|
} else {
|
||||||
res = checkFractionAnswer(num, den, answer.numerator, answer.denominator);
|
res = checkFractionAnswer(num, den, answer.numerator, answer.denominator);
|
||||||
@@ -59,8 +104,8 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "decimal": {
|
case "decimal": {
|
||||||
const val = parseFloat(userAnswer.value || "");
|
const val = parseStrictSignedDecimal(userAnswer.value || "");
|
||||||
if (isNaN(val)) {
|
if (val === null) {
|
||||||
res = { correct: false, message: "Enter a valid number" };
|
res = { correct: false, message: "Enter a valid number" };
|
||||||
} else {
|
} else {
|
||||||
res = checkDecimalAnswer(val, answer.value);
|
res = checkDecimalAnswer(val, answer.value);
|
||||||
@@ -68,8 +113,8 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "integer": {
|
case "integer": {
|
||||||
const val = parseInt(userAnswer.value || "");
|
const val = parseStrictSignedInt(userAnswer.value || "");
|
||||||
if (isNaN(val)) {
|
if (val === null) {
|
||||||
res = { correct: false, message: "Enter a valid number" };
|
res = { correct: false, message: "Enter a valid number" };
|
||||||
} else {
|
} else {
|
||||||
res = checkIntegerAnswer(val, answer.value);
|
res = checkIntegerAnswer(val, answer.value);
|
||||||
@@ -86,9 +131,9 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "standardForm": {
|
case "standardForm": {
|
||||||
const coeff = parseFloat(userAnswer.coefficient || "");
|
const coeff = parseStrictDecimal(userAnswer.coefficient || "");
|
||||||
const exp = parseInt(userAnswer.exponent || "");
|
const exp = parseStrictSignedInt(userAnswer.exponent || "");
|
||||||
if (isNaN(coeff) || isNaN(exp)) {
|
if (coeff === null || exp === null) {
|
||||||
res = { correct: false, message: "Enter valid numbers" };
|
res = { correct: false, message: "Enter valid numbers" };
|
||||||
} else if (
|
} else if (
|
||||||
Math.abs(coeff - answer.coefficient) < 0.01 &&
|
Math.abs(coeff - answer.coefficient) < 0.01 &&
|
||||||
@@ -111,10 +156,13 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextHint() {
|
function nextHint() {
|
||||||
|
if (!showHint) {
|
||||||
|
setShowHint(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (hintIndex < problem.hints.length - 1) {
|
if (hintIndex < problem.hints.length - 1) {
|
||||||
setHintIndex((i) => i + 1);
|
setHintIndex((i) => i + 1);
|
||||||
}
|
}
|
||||||
setShowHint(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptHtml = katex.renderToString(problem.prompt, { throwOnError: false, displayMode: true });
|
const promptHtml = katex.renderToString(problem.prompt, { throwOnError: false, displayMode: true });
|
||||||
@@ -162,7 +210,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
denominator={userAnswer.denominator || ""}
|
denominator={userAnswer.denominator || ""}
|
||||||
onNumeratorChange={(v) => setUserAnswer((a) => ({ ...a, numerator: v }))}
|
onNumeratorChange={(v) => setUserAnswer((a) => ({ ...a, numerator: v }))}
|
||||||
onDenominatorChange={(v) => setUserAnswer((a) => ({ ...a, denominator: v }))}
|
onDenominatorChange={(v) => setUserAnswer((a) => ({ ...a, denominator: v }))}
|
||||||
disabled={result?.correct === true}
|
disabled={isFullyCorrect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -170,7 +218,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
<DecimalInput
|
<DecimalInput
|
||||||
value={userAnswer.value || ""}
|
value={userAnswer.value || ""}
|
||||||
onChange={(v) => setUserAnswer((a) => ({ ...a, value: v }))}
|
onChange={(v) => setUserAnswer((a) => ({ ...a, value: v }))}
|
||||||
disabled={result?.correct === true}
|
disabled={isFullyCorrect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -194,7 +242,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
ratio: current.join(":"),
|
ratio: current.join(":"),
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
disabled={result?.correct === true}
|
disabled={isFullyCorrect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -206,7 +254,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
value={userAnswer.coefficient || ""}
|
value={userAnswer.coefficient || ""}
|
||||||
onChange={(e) => setUserAnswer((a) => ({ ...a, coefficient: e.target.value }))}
|
onChange={(e) => setUserAnswer((a) => ({ ...a, coefficient: e.target.value }))}
|
||||||
disabled={result?.correct === true}
|
disabled={isFullyCorrect}
|
||||||
className="w-16 rounded-lg border border-border bg-surface px-2 py-2 text-center font-bold focus:border-unit-2 focus:outline-none"
|
className="w-16 rounded-lg border border-border bg-surface px-2 py-2 text-center font-bold focus:border-unit-2 focus:outline-none"
|
||||||
placeholder="?"
|
placeholder="?"
|
||||||
aria-label="Coefficient"
|
aria-label="Coefficient"
|
||||||
@@ -217,7 +265,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
value={userAnswer.exponent || ""}
|
value={userAnswer.exponent || ""}
|
||||||
onChange={(e) => setUserAnswer((a) => ({ ...a, exponent: e.target.value }))}
|
onChange={(e) => setUserAnswer((a) => ({ ...a, exponent: e.target.value }))}
|
||||||
disabled={result?.correct === true}
|
disabled={isFullyCorrect}
|
||||||
className="w-12 -translate-y-2 rounded-lg border border-border bg-surface px-2 py-1 text-center text-sm font-bold focus:border-unit-2 focus:outline-none"
|
className="w-12 -translate-y-2 rounded-lg border border-border bg-surface px-2 py-1 text-center text-sm font-bold focus:border-unit-2 focus:outline-none"
|
||||||
placeholder="?"
|
placeholder="?"
|
||||||
aria-label="Exponent"
|
aria-label="Exponent"
|
||||||
@@ -228,7 +276,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="mb-4 flex flex-wrap justify-center gap-3">
|
<div className="mb-4 flex flex-wrap justify-center gap-3">
|
||||||
{!result?.correct && (
|
{!isFullyCorrect && (
|
||||||
<>
|
<>
|
||||||
<Button variant={unitColor} size="sm" onClick={checkAnswer}>
|
<Button variant={unitColor} size="sm" onClick={checkAnswer}>
|
||||||
Check Answer
|
Check Answer
|
||||||
@@ -241,7 +289,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{result?.correct && (
|
{isFullyCorrect && (
|
||||||
<Button variant={unitColor} size="sm" onClick={() => generateNew(difficulty)}>
|
<Button variant={unitColor} size="sm" onClick={() => generateNew(difficulty)}>
|
||||||
Next Problem
|
Next Problem
|
||||||
</Button>
|
</Button>
|
||||||
@@ -297,7 +345,10 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
|||||||
{problem.steps.map((step, i) => (
|
{problem.steps.map((step, i) => (
|
||||||
<div key={i} className="flex gap-2 text-sm">
|
<div key={i} className="flex gap-2 text-sm">
|
||||||
<span className="shrink-0 font-bold text-muted">{i + 1}.</span>
|
<span className="shrink-0 font-bold text-muted">{i + 1}.</span>
|
||||||
<span>{step.explanation}</span>
|
<div className="min-w-0 flex-1 space-y-1">
|
||||||
|
<div>{step.explanation}</div>
|
||||||
|
{step.math && <MathDisplay math={step.math} className="text-lg" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ export function parseStrictInt(input: string): number | null {
|
|||||||
return parseInt(s, 10);
|
return parseInt(s, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse a strict signed integer string. Reject decimals, scientific notation, and trailing text. */
|
||||||
|
export function parseStrictSignedInt(input: string): number | null {
|
||||||
|
const s = input.trim();
|
||||||
|
if (!/^-?\d+$/.test(s)) return null;
|
||||||
|
return parseInt(s, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a strict signed decimal string. Reject scientific notation and trailing text. */
|
||||||
|
export function parseStrictSignedDecimal(input: string): number | null {
|
||||||
|
const s = input.trim();
|
||||||
|
if (!/^-?\d+(\.\d+)?$/.test(s)) return null;
|
||||||
|
return parseFloat(s);
|
||||||
|
}
|
||||||
|
|
||||||
export type AnswerResult =
|
export type AnswerResult =
|
||||||
| { correct: true; simplified: boolean }
|
| { correct: true; simplified: boolean }
|
||||||
| { correct: false; message: string };
|
| { correct: false; message: string };
|
||||||
@@ -37,6 +51,11 @@ export function checkFractionAnswer(
|
|||||||
return { correct: false, message: "That's not quite right. Try again!" };
|
return { correct: false, message: "That's not quite right. Try again!" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any non-zero denominator gives the same value for zero, so don't force 0/1 in the UI.
|
||||||
|
if (userNum === 0) {
|
||||||
|
return { correct: true, simplified: true };
|
||||||
|
}
|
||||||
|
|
||||||
const isSimplified = gcd(Math.abs(userNum), Math.abs(userDen)) === 1;
|
const isSimplified = gcd(Math.abs(userNum), Math.abs(userDen)) === 1;
|
||||||
|
|
||||||
if (requireSimplified && !isSimplified) {
|
if (requireSimplified && !isSimplified) {
|
||||||
|
|||||||
@@ -34,22 +34,51 @@ export function generateFractionAddSubtract(difficulty: Difficulty): MathProblem
|
|||||||
n2 = randomInt(1, d2 - 1);
|
n2 = randomInt(1, d2 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ansNum, ansDen] = operation === "add"
|
|
||||||
? frac.add(n1, d1, n2, d2)
|
|
||||||
: frac.subtract(n1, d1, n2, d2);
|
|
||||||
|
|
||||||
const prompt = `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}}`;
|
const prompt = `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}}`;
|
||||||
|
|
||||||
const commonDen = frac.lcm(d1, d2);
|
const commonDen = frac.lcm(d1, d2);
|
||||||
|
const convertedN1 = n1 * (commonDen / d1);
|
||||||
|
const convertedN2 = n2 * (commonDen / d2);
|
||||||
|
const rawNum = operation === "add"
|
||||||
|
? convertedN1 + convertedN2
|
||||||
|
: convertedN1 - convertedN2;
|
||||||
|
const [ansNum, ansDen] = frac.simplify(rawNum, commonDen);
|
||||||
|
|
||||||
const steps = d1 === d2
|
const steps = d1 === d2
|
||||||
? [
|
? [
|
||||||
{ explanation: "Denominators are the same, so just " + (operation === "add" ? "add" : "subtract") + " the numerators" },
|
{
|
||||||
{ explanation: `${n1} ${op} ${n2} = ${operation === "add" ? n1 + n2 : n1 - n2}`, math: `\\frac{${operation === "add" ? n1 + n2 : n1 - n2}}{${d1}}` },
|
explanation: `The denominators already match, so keep ${d1} as the denominator.`,
|
||||||
|
math: `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
explanation: `${operation === "add" ? "Add" : "Subtract"} the numerators and keep the denominator the same.`,
|
||||||
|
math: `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}} = \\frac{${rawNum}}{${d1}}`,
|
||||||
|
},
|
||||||
|
...(ansNum !== rawNum || ansDen !== d1
|
||||||
|
? [{
|
||||||
|
explanation: "Simplify the fraction.",
|
||||||
|
math: `\\frac{${rawNum}}{${d1}} = \\frac{${ansNum}}{${ansDen}}`,
|
||||||
|
}]
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ explanation: `Find the common denominator: LCM(${d1}, ${d2}) = ${commonDen}` },
|
{
|
||||||
{ explanation: `Convert: ${n1}×${commonDen / d1}/${d1}×${commonDen / d1} and ${n2}×${commonDen / d2}/${d2}×${commonDen / d2}` },
|
explanation: `Find the lowest common denominator: LCM(${d1}, ${d2}) = ${commonDen}.`,
|
||||||
{ explanation: `${op === "+" ? "Add" : "Subtract"} numerators`, math: `\\frac{${ansNum}}{${ansDen}}` },
|
math: `\\text{LCD} = ${commonDen}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
explanation: "Rewrite both fractions with that denominator.",
|
||||||
|
math: `\\frac{${n1}}{${d1}} = \\frac{${convertedN1}}{${commonDen}} \\quad \\frac{${n2}}{${d2}} = \\frac{${convertedN2}}{${commonDen}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
explanation: `${operation === "add" ? "Add" : "Subtract"} the numerators and keep the common denominator.`,
|
||||||
|
math: `\\frac{${convertedN1}}{${commonDen}} ${op} \\frac{${convertedN2}}{${commonDen}} = \\frac{${rawNum}}{${commonDen}}`,
|
||||||
|
},
|
||||||
|
...(ansNum !== rawNum || ansDen !== commonDen
|
||||||
|
? [{
|
||||||
|
explanation: "Simplify the fraction.",
|
||||||
|
math: `\\frac{${rawNum}}{${commonDen}} = \\frac{${ansNum}}{${ansDen}}`,
|
||||||
|
}]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -58,7 +87,7 @@ export function generateFractionAddSubtract(difficulty: Difficulty): MathProblem
|
|||||||
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
|
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
|
||||||
hints: [
|
hints: [
|
||||||
d1 === d2 ? "The denominators are already the same!" : "Find a common denominator first",
|
d1 === d2 ? "The denominators are already the same!" : "Find a common denominator first",
|
||||||
d1 !== d2 ? `Try using the Butterfly Method: cross multiply` : `Just ${operation} the numerators`,
|
d1 !== d2 ? "Rewrite both fractions using that denominator" : `Just ${operation} the numerators`,
|
||||||
],
|
],
|
||||||
steps,
|
steps,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function generateRatioWordProblem(difficulty: Difficulty): MathProblem {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: nextId(),
|
id: nextId(),
|
||||||
prompt: `\\text{${ctx.item.charAt(0).toUpperCase() + ctx.item.slice(1)} were shared between ${ctx.nameA} and ${ctx.nameB} in the ratio ${a}:${b}. If ${bigger} received ${extraAmount} more ${ctx.item}, find the total shared.}`,
|
prompt: `\\begin{array}{l}\\text{${ctx.item.charAt(0).toUpperCase() + ctx.item.slice(1)} were shared between ${ctx.nameA} and ${ctx.nameB}}\\\\\\text{in the ratio ${a}:${b}. If ${bigger} received ${extraAmount} more ${ctx.item}, find the total shared.}\\end{array}`,
|
||||||
answer: { kind: "integer", value: total },
|
answer: { kind: "integer", value: total },
|
||||||
hints: [
|
hints: [
|
||||||
`The difference in ratio parts is ${biggerRatio} - ${smallerRatio} = ${diff}`,
|
`The difference in ratio parts is ${biggerRatio} - ${smallerRatio} = ${diff}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user