diff --git a/AGENTS.md b/AGENTS.md index 117bc6e..d7ffa53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,7 @@ components/ lib/ engine/ # 문제 생성 알고리즘 엔진 parenthesis-sign.ts # 숫자 괄호 엔진 + parenthesis-latex.ts # 괄호 식 KaTeX 포맷 및 현재 항 강조 유틸 variable-parenthesis.ts # 미지수 괄호 엔진 types.ts # 숫자 괄호 타입 variable-types.ts # 미지수 괄호 타입 @@ -82,6 +83,7 @@ prisma/ - 모든 수학 수식은 KaTeX로 렌더링한다. 일반 텍스트나 Unicode 수식 문자를 직접 사용하지 않는다. - KaTeX 컴포넌트는 `components/math/` 하위에 위치한다. - 현재 수식 컴포넌트는 `katex.renderToString()`을 직접 사용한다. 별도 래퍼 라이브러리를 추가하기 전에 Next.js App Router 클라이언트 번들에서 런타임 문제가 없는지 확인한다. +- 괄호 식 수식 조립과 풀이 중 현재 항 강조는 `lib/engine/parenthesis-latex.ts`의 공통 유틸을 사용한다. 숫자/미지수 풀이 화면에 같은 포맷 로직을 중복 구현하지 않는다. ### 상태 관리 @@ -109,8 +111,8 @@ prisma/ - 모든 항을 하나의 공유 input으로 입력한다. - input 위 우상단에는 각 항의 진행 상태를 progress 스타일로 표시한다. - 진행 상태는 값 입력 전에는 괄호 안 원래 항 값을 표시하고, 값 입력 중이거나 입력 후에는 사용자가 입력한 값을 표시한다. -- 첫째 항에서 Enter를 누르면 같은 input이 둘째 항 입력으로 전환된다. -- 둘째 항에서 Enter를 누르면 제출 동작을 실행한다. 별도 제출 버튼이 없어도 form submit 흐름이 유지되어야 한다. +- Enter를 누르면 다음 항 입력으로 전환되고, 마지막 항에서 Enter를 누르면 제출 동작을 실행한다. 별도 제출 버튼이 없어도 form submit 흐름이 유지되어야 한다. +- 풀이 중인 항의 수, 곱해지는 수, 해당 괄호는 KaTeX 색상으로 강조하고 나머지 항은 강조하지 않는다. - 터치 기반 입력 환경에서는 input focus 시 커스텀 숫자 키패드를 표시한다. 키패드는 숫자, `+/-`, `.`, `⌫`, `↵` 버튼을 제공한다. - 마우스/트랙패드처럼 hover 가능한 정밀 포인터가 있는 환경은 물리 키보드 사용 환경으로 간주하고 커스텀 숫자 키패드를 숨긴다. 웹 표준 API로 물리 키보드 연결 여부 자체를 안정적으로 판별할 수 없으므로 해상도 기준으로 분기하지 않는다. (`lib/input-device.ts`의 `DESKTOP_INPUT_DEVICE_QUERY` 사용) - 입력 중에는 `-`, `1.` 같은 임시 값을 허용할 수 있지만 제출 시에는 정상적인 양의 실수 또는 음의 실수 표현만 통과시킨다. @@ -120,10 +122,11 @@ prisma/ #### 미지수 괄호 풀이 화면 (`/variable-parentheses/play`) - 답을 직접 입력하지 않는다. 각 항의 보기 버튼 중에서 선택한다. -- 첫째 항 선택 후 자동으로 둘째 항 보기로 focus가 이동한다. -- 둘째 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 제출되어 피드백 화면으로 전환된다. +- 항을 선택하면 자동으로 다음 항 보기로 focus가 이동한다. +- 마지막 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 제출되어 피드백 화면으로 전환된다. - 입력 항 진행 상태는 progress 스타일로 표시한다. 선택 전에는 괄호 안 원래 항 값을 표시하고, 선택 후에는 고른 보기 값을 표시한다. - 선택된 보기 버튼은 `aria-pressed`로 표시한다. +- 풀이 중인 항의 수, 곱해지는 수, 해당 괄호는 KaTeX 색상으로 강조하고 나머지 항은 강조하지 않는다. ### API / 백엔드 @@ -140,8 +143,11 @@ prisma/ - 문제 세션에서 중복을 방지할 때는 랜덤 id가 아니라 수학적 문제 조건을 나타내는 안정적인 key를 기준으로 한다. - 모든 엔진은 전체 가능한 시드 목록을 모듈 로드 시 한 번 생성(`createAllProblemSeeds`)하고, 세션 생성 시 셔플 후 슬라이스하는 방식으로 중복 없이 출제한다. - 숫자 괄호 엔진: `lib/engine/parenthesis-sign.ts` / 타입: `lib/engine/types.ts` + - 문제 구조는 `terms`, `operators`, `distributedTerms` 배열을 기준으로 한다. `a`, `b`, `distributedA`, `distributedB` 같은 2항 전용 필드를 추가하지 않는다. + - 채점 입력은 `{ terms: number[] }` 형태만 사용한다. - 미지수 괄호 엔진: `lib/engine/variable-parenthesis.ts` / 타입: `lib/engine/variable-types.ts` - - 괄호 안 두 항 중 정확히 하나가 `정수 × x` 항이다. + - 괄호 안 항 중 정확히 하나가 `정수 × x` 항이다. + - 문제 구조는 `terms`, `operators`, `correctTerms`, `choices` 배열을 기준으로 한다. `firstTerm`, `secondTerm`, `operator` 같은 2항 전용 필드를 추가하지 않는다. - 각 항의 보기는 정답 계수의 절대값 기준 음수/양수 두 개를 셔플해 제공한다. - 채점은 choice id 기반으로 수행한다. @@ -194,9 +200,10 @@ NEXTAUTH_SECRET=your-secret ``` - 시작 화면(`/`)은 학습 내용 선택 화면이다. `숫자 괄호 풀기`, `미지수 괄호 풀기`가 활성화되어 있고 분수/실수 괄호는 disabled 상태다. -- 준비 화면은 문제 수 `[5] [10]` 버튼 그룹과 괄호 종류 버튼 그룹을 제공한다. 기본값은 `10`이며, 현재는 `()`만 선택 가능하다. +- 준비 화면은 문제 수 `[5] [10]`, 괄호 종류, 괄호 안 최대 항 수 `[2] [3] [4]` 버튼 그룹을 제공한다. 기본 문제 수는 `10`, 기본 최대 항 수는 `2`이며, 괄호 종류는 현재 `()`만 선택 가능하다. - 준비 화면의 `연습 시작`은 Zustand 세션을 초기화하고 풀이 화면으로 이동한다. -- 풀이 화면은 URL의 `count` 쿼리와 Zustand의 선택 문제 수를 동기화한다. 허용 문제 수는 `5`, `10`이며 유효하지 않은 값은 `10`으로 처리한다. +- 풀이 화면은 URL의 `count`, `terms` 쿼리와 Zustand의 선택 문제 수·최대 항 수를 동기화한다. 허용 문제 수는 `5`, `10`이며 유효하지 않은 값은 `10`으로 처리한다. 허용 최대 항 수는 `2`, `3`, `4`이며 유효하지 않은 값은 `2`로 처리한다. +- 풀이 화면의 문제 수식은 현재 풀고 있는 항의 수, 곱해지는 수, 해당 괄호를 강조 색상으로 표시하고 나머지 항은 기본 색상으로 표시한다. - 정답·오답 피드백은 `계산식 = 내가 입력한(고른) 답` 형식으로 표시한다. 정답은 `○`, 오답은 붉은색 답과 `×`, 굵은 정답값을 함께 보여준다. - 최종 결과 화면은 지금까지 푼 문제를 `문제 = 내가 입력한(고른) 답` 형식으로 나열하고, 피드백 화면과 같은 정답·오답 색상 및 기호 규칙을 사용한다. diff --git a/README.md b/README.md index c09886b..38f32d4 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ - 시작 화면(`/`)에서 학습 내용을 선택합니다. `숫자 괄호 풀기`, `미지수 괄호 풀기`가 활성 상태이고 분수·실수 괄호는 준비 중으로 비활성화되어 있습니다. - 문제와 정답 수식은 KaTeX로 렌더링합니다. +- 풀이 중인 문제 수식은 현재 항, 곱해지는 수, 해당 괄호를 강조 색상으로 표시하고 나머지 항은 기본 색상으로 표시합니다. - 진행 단계는 현재 문제, 완료된 정답, 완료된 오답, 남은 문제를 구분해서 표시합니다. - 정답·오답 피드백은 `계산식 = 내가 입력한(고른) 답` 형식으로 표시합니다. - 정답은 정답 색상 박스와 `○` 표시, 오답은 오답 색상 박스, 붉은색 답, `×`, 굵은 정답값을 표시합니다. @@ -55,20 +56,20 @@ ### 숫자 괄호 풀기 (`/number-parentheses` → `/number-parentheses/play`) -- 준비 화면에서 문제 수 5 / 10을 선택하고 연습을 시작합니다. 기본값은 10. +- 준비 화면에서 문제 수 5 / 10과 괄호 안 최대 항 수 2 / 3 / 4를 선택하고 연습을 시작합니다. 기본값은 문제 수 10, 최대 항 수 2입니다. - 괄호 종류는 현재 `()`만 선택 가능하고 `{}`, `[]`는 비활성화되어 있습니다. -- 풀이 화면에서 두 항을 직접 숫자로 입력합니다. - - 모든 항은 하나의 input에서 순서대로 입력하며, 첫째 항 Enter → 둘째 항 전환, 둘째 항 Enter → 제출. +- 풀이 화면에서 각 항을 직접 숫자로 입력합니다. + - 모든 항은 하나의 input에서 순서대로 입력하며, Enter로 다음 항으로 이동하고 마지막 항에서 Enter를 누르면 제출합니다. - input 위 우상단의 progress 표시에서 각 항의 진행 상태를 보여줍니다. 입력 전에는 괄호 안 원래 항 값, 입력 중/입력 후에는 사용자가 입력한 값을 표시합니다. - 터치 환경에서는 커스텀 숫자 키패드(숫자, `+/-`, `.`, `⌫`, `↵`)가 표시되며, 정밀 포인터가 있는 환경(마우스/트랙패드)에서는 숨깁니다. ### 미지수 괄호 풀기 (`/variable-parentheses` → `/variable-parentheses/play`) - 준비 화면 구성은 숫자 괄호와 동일합니다. -- 풀이 화면에서 두 항을 보기 중에서 선택합니다. +- 풀이 화면에서 각 항을 보기 중에서 선택합니다. - 각 항의 보기는 정답 계수의 절대값을 기준으로 음수/양수 두 개를 제공합니다. - - 첫째 항 선택 후 자동으로 둘째 항 보기로 focus가 이동합니다. - - 둘째 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 피드백 화면으로 전환됩니다. + - 항 선택 후 자동으로 다음 항 보기로 focus가 이동합니다. + - 마지막 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 피드백 화면으로 전환됩니다. - 입력 항 progress 표시는 선택 전에는 괄호 안 원래 항 값, 선택 후에는 고른 보기 값을 표시합니다. --- @@ -141,7 +142,10 @@ components/ lib/ engine/ # 문제 생성 알고리즘 엔진 parenthesis-sign.ts # 숫자 괄호 엔진 + parenthesis-latex.ts # 괄호 식 KaTeX 포맷 및 현재 항 강조 유틸 variable-parenthesis.ts # 미지수 괄호 엔진 + types.ts # 숫자 괄호 타입 + variable-types.ts # 미지수 괄호 타입 input-device.ts # 입력 장치 감지 미디어 쿼리 prisma.ts # Prisma Client 싱글턴 redis.ts # Redis Client 싱글턴 diff --git a/app/(game)/number-parentheses/play/NumberPlayClient.tsx b/app/(game)/number-parentheses/play/NumberPlayClient.tsx index afb8106..f3b3822 100644 --- a/app/(game)/number-parentheses/play/NumberPlayClient.tsx +++ b/app/(game)/number-parentheses/play/NumberPlayClient.tsx @@ -17,34 +17,9 @@ import { DESKTOP_INPUT_DEVICE_QUERY } from "@/lib/input-device"; import KatexRenderer from "@/components/math/KatexRenderer"; import { useGameStore } from "@/store/gameStore"; import type { MaxTermCount, ProblemCount } from "@/store/gameStore"; -import type { ParenthesisSignProblem } from "@/lib/engine/types"; +import { formatParenthesisExpressionLatex } from "@/lib/engine/parenthesis-latex"; const REAL_NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/; -const ACTIVE_TERM_COLOR = "#f97316"; - -function highlightLatex(latex: string) { - return `\\textcolor{${ACTIVE_TERM_COLOR}}{${latex}}`; -} - -function formatActiveTermProblemLatex( - problem: ParenthesisSignProblem, - activeTermIndex: number, -) { - const termLatex = problem.terms - .map((term, index) => { - const displayTerm = - index === activeTermIndex ? highlightLatex(`${term}`) : `${term}`; - - if (index === 0) return displayTerm; - - return `${problem.operators[index - 1]} ${displayTerm}`; - }) - .join(" "); - - return `${highlightLatex(`${problem.multiplier}`)}${highlightLatex( - "(", - )}${termLatex}${highlightLatex(")")}`; -} function parseAnswer(value: string) { const trimmed = value.trim(); @@ -233,7 +208,13 @@ export default function NumberPlayClient({ const isLastProblem = session.currentIndex === session.problems.length - 1; const problemLatex = submitted ? currentProblem.latexBefore - : formatActiveTermProblemLatex(currentProblem, activeInput); + : formatParenthesisExpressionLatex({ + multiplier: currentProblem.multiplier, + terms: currentProblem.terms, + operators: currentProblem.operators, + formatTermLatex: (term) => `${term}`, + activeTermIndex: activeInput, + }); const pendingAnswerLabels = currentProblem.terms.map((term, index) => { if (index === 0) return String(term); diff --git a/app/(game)/variable-parentheses/play/VariablePlayClient.tsx b/app/(game)/variable-parentheses/play/VariablePlayClient.tsx index 401b8f3..0c39725 100644 --- a/app/(game)/variable-parentheses/play/VariablePlayClient.tsx +++ b/app/(game)/variable-parentheses/play/VariablePlayClient.tsx @@ -10,41 +10,13 @@ import VariableFeedbackPanel from "@/components/game/VariableFeedbackPanel"; import VariableScoreSummary from "@/components/game/VariableScoreSummary"; import KatexRenderer from "@/components/math/KatexRenderer"; import { formatVariableTermLatex } from "@/lib/engine/variable-parenthesis"; -import type { VariableParenthesisProblem } from "@/lib/engine/variable-types"; +import { formatParenthesisExpressionLatex } from "@/lib/engine/parenthesis-latex"; import { useVariableGameStore, type VariableMaxTermCount, type VariableProblemCount, } from "@/store/variableGameStore"; -const ACTIVE_TERM_COLOR = "#f97316"; - -function highlightLatex(latex: string) { - return `\\textcolor{${ACTIVE_TERM_COLOR}}{${latex}}`; -} - -function formatActiveTermProblemLatex( - problem: VariableParenthesisProblem, - activeTermIndex: number, -) { - const termLatex = problem.terms - .map((term, index) => { - const displayTerm = - index === activeTermIndex - ? highlightLatex(formatVariableTermLatex(term)) - : formatVariableTermLatex(term); - - if (index === 0) return displayTerm; - - return `${problem.operators[index - 1]} ${displayTerm}`; - }) - .join(" "); - - return `${highlightLatex(`${problem.multiplier}`)}${highlightLatex( - "(", - )}${termLatex}${highlightLatex(")")}`; -} - interface VariablePlayClientProps { problemCount: VariableProblemCount; maxTermCount: VariableMaxTermCount; @@ -187,7 +159,13 @@ export default function VariablePlayClient({ const isLastProblem = session.currentIndex === session.problems.length - 1; const problemLatex = submitted ? currentProblem.latexBefore - : formatActiveTermProblemLatex(currentProblem, activeTermIndex); + : formatParenthesisExpressionLatex({ + multiplier: currentProblem.multiplier, + terms: currentProblem.terms, + operators: currentProblem.operators, + formatTermLatex: formatVariableTermLatex, + activeTermIndex, + }); const pendingChoiceLabels = currentProblem.terms.map((term, index) => formatVariableTermLatex({ ...term, diff --git a/lib/engine/parenthesis-latex.ts b/lib/engine/parenthesis-latex.ts new file mode 100644 index 0000000..b24dc5d --- /dev/null +++ b/lib/engine/parenthesis-latex.ts @@ -0,0 +1,48 @@ +export const ACTIVE_DISTRIBUTION_COLOR = "#f97316"; + +function colorLatex(latex: string, color: string) { + return `\\textcolor{${color}}{${latex}}`; +} + +interface FormatParenthesisExpressionLatexParams { + multiplier: number; + terms: T[]; + operators: ("+" | "-")[]; + formatTermLatex: (term: T) => string; + activeTermIndex?: number; + activeColor?: string; +} + +export function formatParenthesisExpressionLatex({ + multiplier, + terms, + operators, + formatTermLatex, + activeTermIndex, + activeColor = ACTIVE_DISTRIBUTION_COLOR, +}: FormatParenthesisExpressionLatexParams) { + const hasActiveTerm = activeTermIndex !== undefined; + const maybeColor = (latex: string, active: boolean) => + hasActiveTerm && active ? colorLatex(latex, activeColor) : latex; + const termLatex = terms + .map((term, index) => { + const displayTerm = maybeColor( + formatTermLatex(term), + index === activeTermIndex, + ); + + if (index === 0) return displayTerm; + + return `${operators[index - 1]} ${displayTerm}`; + }) + .join(" "); + + if (hasActiveTerm) { + return `${colorLatex(`${multiplier}`, activeColor)}${colorLatex( + "(", + activeColor, + )}${termLatex}${colorLatex(")", activeColor)}`; + } + + return `${multiplier}\\left(${termLatex}\\right)`; +} diff --git a/lib/engine/parenthesis-sign.test.ts b/lib/engine/parenthesis-sign.test.ts index c80055f..2eaf08c 100644 --- a/lib/engine/parenthesis-sign.test.ts +++ b/lib/engine/parenthesis-sign.test.ts @@ -14,22 +14,26 @@ describe("parenthesis sign problem engine", () => { expect(Number.isInteger(problem.multiplier)).toBe(true); expect(problem.multiplier).toBeGreaterThanOrEqual(-9); expect(problem.multiplier).toBeLessThanOrEqual(-1); - expect(problem.a).toBeGreaterThanOrEqual(1); - expect(problem.a).toBeLessThanOrEqual(9); - expect(problem.b).toBeGreaterThanOrEqual(1); - expect(problem.b).toBeLessThanOrEqual(9); - expect(problem.a).not.toBe(problem.b); - expect(["+", "-"]).toContain(problem.operator); + expect(problem.terms).toHaveLength(2); + expect(problem.operators).toHaveLength(1); + expect(problem.terms.every((term) => term >= 1 && term <= 9)).toBe(true); + expect(new Set(problem.terms).size).toBe(problem.terms.length); + expect(["+", "-"]).toContain(problem.operators[0]); } }); it("distributes the multiplier correctly for both operators", () => { for (let index = 0; index < 100; index += 1) { const problem = generateProblem(); - const signedB = problem.operator === "-" ? -problem.b : problem.b; + const signedTerms = problem.terms.map((term, termIndex) => { + if (termIndex === 0) return term; - expect(problem.distributedA).toBe(problem.multiplier * problem.a); - expect(problem.distributedB).toBe(problem.multiplier * signedB); + return problem.operators[termIndex - 1] === "-" ? -term : term; + }); + + expect(problem.distributedTerms).toEqual( + signedTerms.map((term) => problem.multiplier * term), + ); } }); @@ -37,20 +41,15 @@ describe("parenthesis sign problem engine", () => { const problem = generateProblem(); const correctResult = gradeAnswer(problem, { - a: problem.distributedA, - b: problem.distributedB, + terms: problem.distributedTerms, }); expect(correctResult.correct).toBe(true); - expect(correctResult.userAnswer).toEqual([ - problem.distributedA, - problem.distributedB, - ]); + expect(correctResult.userAnswer).toEqual(problem.distributedTerms); expect( gradeAnswer(problem, { - a: problem.distributedB, - b: problem.distributedA, + terms: [...problem.distributedTerms].reverse(), }).correct, ).toBe(false); }); diff --git a/lib/engine/parenthesis-sign.ts b/lib/engine/parenthesis-sign.ts index 99666c3..cd8c0ba 100644 --- a/lib/engine/parenthesis-sign.ts +++ b/lib/engine/parenthesis-sign.ts @@ -3,6 +3,7 @@ import type { ParenthesisSignProblem, UserAnswer, } from "./types"; +import { formatParenthesisExpressionLatex } from "./parenthesis-latex"; const MIN_MULTIPLIER = -9; const MAX_MULTIPLIER = -1; @@ -33,19 +34,6 @@ function formatLatexTerms(terms: number[]) { .join(" "); } -function formatParenthesisTerms( - terms: number[], - operators: ("+" | "-")[], -) { - return terms - .map((term, index) => { - if (index === 0) return `${term}`; - - return `${operators[index - 1]} ${term}`; - }) - .join(" "); -} - function createProblem({ multiplier, terms, @@ -57,9 +45,6 @@ function createProblem({ return operators[index - 1] === "-" ? -term : term; }); const distributedTerms = signedTerms.map((term) => multiplier * term); - const [a, b] = terms; - const [operator] = operators; - const [distributedA, distributedB] = distributedTerms; const key = [ multiplier, ...terms.flatMap((term, index) => @@ -73,16 +58,13 @@ function createProblem({ multiplier, terms, operators, - a, - operator, - b, distributedTerms, - distributedA, - distributedB, - latexBefore: `${multiplier}\\left(${formatParenthesisTerms( + latexBefore: formatParenthesisExpressionLatex({ + multiplier, terms, operators, - )}\\right)`, + formatTermLatex: (term) => `${term}`, + }), latexAfter: formatLatexTerms(distributedTerms), }; } @@ -222,16 +204,11 @@ export function gradeAnswer( problem: ParenthesisSignProblem, answer: UserAnswer, ): GradeResult { - const userAnswer = - answer.terms ?? (answer.a !== undefined && answer.b !== undefined - ? [answer.a, answer.b] - : []); - return { correct: - userAnswer.length === problem.distributedTerms.length && - userAnswer.every((value, index) => value === problem.distributedTerms[index]), - userAnswer, + answer.terms.length === problem.distributedTerms.length && + answer.terms.every((value, index) => value === problem.distributedTerms[index]), + userAnswer: answer.terms, correctAnswer: problem.distributedTerms, }; } diff --git a/lib/engine/types.ts b/lib/engine/types.ts index f8bd24a..7a2751d 100644 --- a/lib/engine/types.ts +++ b/lib/engine/types.ts @@ -4,20 +4,13 @@ export interface ParenthesisSignProblem { multiplier: number; terms: number[]; operators: ("+" | "-")[]; - a: number; - operator: "+" | "-"; - b: number; distributedTerms: number[]; - distributedA: number; - distributedB: number; latexBefore: string; latexAfter: string; } export interface UserAnswer { - terms?: number[]; - a?: number; - b?: number; + terms: number[]; } export interface GradeResult { diff --git a/lib/engine/variable-parenthesis.test.ts b/lib/engine/variable-parenthesis.test.ts index d2bba27..f64f21f 100644 --- a/lib/engine/variable-parenthesis.test.ts +++ b/lib/engine/variable-parenthesis.test.ts @@ -25,27 +25,26 @@ function findCorrectChoiceIds(problem: VariableParenthesisProblem) { describe("variable parenthesis engine", () => { it("generates a problem with exactly one variable term inside parentheses", () => { const problem = generateVariableParenthesisProblem(); - const terms = [problem.firstTerm, problem.secondTerm]; - expect(terms.filter((term) => term.kind === "variable")).toHaveLength(1); - expect(terms.filter((term) => term.kind === "constant")).toHaveLength(1); + expect(problem.terms.filter((term) => term.kind === "variable")).toHaveLength(1); + expect(problem.terms.filter((term) => term.kind === "constant")).toHaveLength(1); expect(problem.multiplier).toBeLessThan(0); expect(Number.isInteger(problem.multiplier)).toBe(true); - expect(problem.firstTerm.coefficient).toBeGreaterThanOrEqual(1); - expect(problem.secondTerm.coefficient).toBeGreaterThanOrEqual(1); + expect( + problem.terms.every((term) => term.coefficient >= 1 && term.coefficient <= 9), + ).toBe(true); }); it("distributes signs correctly for addition and subtraction", () => { const problems = generateVariableParenthesisSession(50); for (const problem of problems) { - const secondSign = problem.operator === "-" ? -1 : 1; + expect(problem.correctTerms.map((term) => term.coefficient)).toEqual( + problem.terms.map((term, index) => { + const sign = index === 0 || problem.operators[index - 1] === "+" ? 1 : -1; - expect(problem.correctTerms[0].coefficient).toBe( - problem.multiplier * problem.firstTerm.coefficient, - ); - expect(problem.correctTerms[1].coefficient).toBe( - problem.multiplier * secondSign * problem.secondTerm.coefficient, + return problem.multiplier * sign * term.coefficient; + }), ); } }); diff --git a/lib/engine/variable-parenthesis.ts b/lib/engine/variable-parenthesis.ts index c053fe0..883c8f3 100644 --- a/lib/engine/variable-parenthesis.ts +++ b/lib/engine/variable-parenthesis.ts @@ -6,6 +6,7 @@ import type { VariableUserAnswer, VariableGradeResult, } from "./variable-types"; +import { formatParenthesisExpressionLatex } from "./parenthesis-latex"; const MIN_MULTIPLIER = -9; const MAX_MULTIPLIER = -1; @@ -95,10 +96,6 @@ export function formatVariableTermLatex( return `${term.coefficient}x`; } -function formatParenthesisTermLatex(term: VariableParenthesisTerm) { - return formatVariableTermLatex(term); -} - export function formatVariableExpressionLatex( terms: DistributedVariableTerm[], ) { @@ -164,8 +161,6 @@ function createChoices( function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem { const { multiplier, terms, operators } = seed; - const [firstTerm, secondTerm] = terms; - const [operator] = operators; const key = [ multiplier, ...terms.flatMap((term, index) => @@ -189,18 +184,14 @@ function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem { multiplier, terms, operators, - operator, - firstTerm, - secondTerm, correctTerms, choices, - latexBefore: `${multiplier}\\left(${terms - .map((term, index) => { - if (index === 0) return formatParenthesisTermLatex(term); - - return `${operators[index - 1]} ${formatParenthesisTermLatex(term)}`; - }) - .join(" ")}\\right)`, + latexBefore: formatParenthesisExpressionLatex({ + multiplier, + terms, + operators, + formatTermLatex: formatVariableTermLatex, + }), latexAfter: formatVariableExpressionLatex(correctTerms), }; } diff --git a/lib/engine/variable-types.ts b/lib/engine/variable-types.ts index 623209d..06889bf 100644 --- a/lib/engine/variable-types.ts +++ b/lib/engine/variable-types.ts @@ -26,9 +26,6 @@ export interface VariableParenthesisProblem { multiplier: number; terms: VariableParenthesisTerm[]; operators: ("+" | "-")[]; - operator: "+" | "-"; - firstTerm: VariableParenthesisTerm; - secondTerm: VariableParenthesisTerm; correctTerms: DistributedVariableTerm[]; choices: VariableTermChoice[][]; latexBefore: string;