diff --git a/src/app/(withAuth)/admin/problems/[problemId]/page.tsx b/src/app/(withAuth)/admin/problems/[problemId]/page.tsx index de23afcd..ef1c9361 100644 --- a/src/app/(withAuth)/admin/problems/[problemId]/page.tsx +++ b/src/app/(withAuth)/admin/problems/[problemId]/page.tsx @@ -80,7 +80,6 @@ async function fetchUserProblemInfo(problemId: ProblemId): Promise ({ diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardEditor.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardEditor.tsx index 1c320314..60cfeda7 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardEditor.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardEditor.tsx @@ -1,3 +1,5 @@ +'use client'; + import { zenkakuAlphanumericalsToHankaku } from '@willbooster/shared-lib'; import fastDeepEqual from 'fast-deep-equal'; import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardViewer.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardViewer.tsx index e27191ea..04c71d5c 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardViewer.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/BoardViewer.tsx @@ -1,3 +1,5 @@ +'use client'; + import type { BoxProps } from '@chakra-ui/react'; import { keyframes } from '@emotion/react'; import React, { useMemo } from 'react'; diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/ProblmBody.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/ProblmBody.tsx index acaf36e3..02e01714 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/ProblmBody.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/ProblmBody.tsx @@ -1,6 +1,8 @@ +'use client'; + import type { ProblemSession } from '@prisma/client'; import { useRouter } from 'next/navigation'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { MAX_CHALLENGE_COUNT } from '../../../../../../../../constants'; import { backendTrpcReact } from '../../../../../../../../infrastructures/trpcBackend/client'; @@ -47,7 +49,6 @@ export const ProblemBody: React.FC = (props) => { : // ProblemSession作成後の問題の改変に対応するため。 Math.min(props.problemSession.traceItemIndex, props.problem.traceItems.length - 1); const previousTraceItemIndex = problemType === 'executionResult' ? 0 : currentTraceItemIndex - 1; - const [viewingTraceItemIndex, setViewingTraceItemIndex] = useState(previousTraceItemIndex); const currentVariables = problemType === 'executionResult' ? props.problem.finalVars : props.problem.traceItems[currentTraceItemIndex].vars; const initialVariables = useMemo( @@ -62,6 +63,11 @@ export const ProblemBody: React.FC = (props) => { [problemType, props.problem.traceItems, previousTraceItemIndex, currentTraceItemIndex, currentVariables] ); + const [viewingTraceItemIndex, setViewingTraceItemIndex] = useState(previousTraceItemIndex); + useEffect(() => { + setViewingTraceItemIndex(previousTraceItemIndex); + }, [previousTraceItemIndex]); + const { isOpen: isAlertOpen, onClose: onAlertClose, onOpen: onAlertOpen } = useDisclosure(); const cancelRef = useRef(null); @@ -147,7 +153,6 @@ export const ProblemBody: React.FC = (props) => { } else { await props.updateProblemSession('step', currentTraceItemIndex + 1); openAlertDialog('正解', '正解です。次のステップに進みます。'); - setViewingTraceItemIndex(currentTraceItemIndex); } } break; @@ -208,6 +213,13 @@ export const ProblemBody: React.FC = (props) => { + props.problem.callerIdToLineIndex.get(id) + ) + } code={props.problem.displayProgram} currentFocusLine={ problemType === 'executionResult' @@ -294,17 +306,36 @@ function getInitialVariables( currentVariables: TraceItemVariable ): Record { let adjustedPreviousTraceItemIndex = previousTraceItemIndex; + if (problemType === 'step') { while ( adjustedPreviousTraceItemIndex > 0 && traceItems[currentTraceItemIndex].depth !== traceItems[adjustedPreviousTraceItemIndex].depth ) { + if (traceItems[currentTraceItemIndex].depth > traceItems[adjustedPreviousTraceItemIndex].depth) { + return getEmptyVariables(currentVariables); + } adjustedPreviousTraceItemIndex--; } + + if ( + traceItems[currentTraceItemIndex].callStack.at(-1) !== traceItems[adjustedPreviousTraceItemIndex].callStack.at(-1) + ) { + return getEmptyVariables(currentVariables); + } } + return Object.fromEntries( Object.entries(currentVariables) .filter(([_, value]) => typeof value === 'number' || typeof value === 'string') .map(([key]) => [key, traceItems[adjustedPreviousTraceItemIndex].vars[key]?.toString() ?? '']) ); } + +function getEmptyVariables(currentVariables: TraceItemVariable): Record { + return Object.fromEntries( + Object.entries(currentVariables) + .filter(([_, value]) => typeof value === 'number' || typeof value === 'string') + .map(([key]) => [key, '']) + ); +} diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/SyntaxHighlighter.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/SyntaxHighlighter.tsx index 564e25f3..9185e04b 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/SyntaxHighlighter.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/SyntaxHighlighter.tsx @@ -6,13 +6,15 @@ import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { Box } from '../../../../../../../../infrastructures/useClient/chakra'; type SyntaxHighlighterProps = BoxProps & { - previousFocusLine?: number; code: string; - programmingLanguageId: string; + callerLines?: (number | undefined)[]; currentFocusLine?: number; + previousFocusLine?: number; + programmingLanguageId: string; }; export const SyntaxHighlighter: React.FC = ({ + callerLines, code, currentFocusLine, previousFocusLine, @@ -33,6 +35,10 @@ export const SyntaxHighlighter: React.FC = ({ lineNumberStyle={{ minWidth: '1.5rem', marginRight: '2rem', paddingRight: 0 }} lineProps={(lineNumber) => { const style: React.CSSProperties = { padding: '0 1rem', backgroundColor: '', border: '' }; + // Show dotted border for callerLines + if (callerLines?.includes(lineNumber)) { + style.border = '2px dotted #f56565'; /* red.500 */ + } // previousFocusLine と currentFocusLine が等しくなるケースがある。 if (lineNumber === previousFocusLine) { style.backgroundColor = '#feebc8' /* orange.100 */; diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/TraceViewer.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/TraceViewer.tsx index 96ebedd4..170b7321 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/TraceViewer.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/TraceViewer.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Box, Button, Card, Heading, HStack, VStack } from '@chakra-ui/react'; import React from 'react'; diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Variables.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Variables.tsx index fc179bff..8b2a4c0d 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Variables.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Variables.tsx @@ -36,7 +36,7 @@ export const Variables: React.FC = ({ traceItemVars }) => { {variable.key} - {variable.value} + {Array.isArray(variable.value) ? variable.value.join(', ') : variable.value} ))} diff --git a/src/problems/instantiateProblem.ts b/src/problems/instantiateProblem.ts index 06397623..7ca51e3b 100644 --- a/src/problems/instantiateProblem.ts +++ b/src/problems/instantiateProblem.ts @@ -25,6 +25,11 @@ export type InstantiatedProblem = { */ sidToLineIndex: Map; + /** + * The mapping from caller ID to line index. + */ + callerIdToLineIndex: Map; + /** * The variables of the final state */ diff --git a/src/problems/problemData.ts b/src/problems/problemData.ts index 92763de7..fea94c37 100644 --- a/src/problems/problemData.ts +++ b/src/problems/problemData.ts @@ -2050,9 +2050,9 @@ function threeStepsForward(t) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid: 1 - 二歩前に進める(t); + 二歩前に進める(t); // caller t.右を向く(); // sid: 2 - 三歩前に進める(t); + 三歩前に進める(t); // caller } static void 二歩前に進める(Turtle t) { t.前に進む(); // sid: 3 @@ -2088,8 +2088,8 @@ function turnAround(t) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid: - 二歩前に進める(t); - 後ろを向く(t); + 二歩前に進める(t); // caller + 後ろを向く(t); // caller t.前に進む(); // sid } static void 二歩前に進める(Turtle t) { @@ -2121,9 +2121,9 @@ function forwardGivenSteps(t, n) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid - N歩前に進める(t, <3-4>); + N歩前に進める(t, <3-4>); // caller t.右を向く(); // sid - N歩前に進める(t, 2); + N歩前に進める(t, 2); // caller } static void N歩前に進める(Turtle t, int n) { @@ -2155,17 +2155,17 @@ function forwardFourSteps(t) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid - 二歩前に進める(t); + 二歩前に進める(t); // caller t.右を向く(); // sid - 四歩前に進める(t); + 四歩前に進める(t); // caller } static void 二歩前に進める(Turtle t) { t.前に進む(); // sid t.前に進む(); // sid } static void 四歩前に進める(Turtle t) { - 二歩前に進める(t); - 二歩前に進める(t); + 二歩前に進める(t); // caller + 二歩前に進める(t); // caller } } `.trim(), @@ -2175,6 +2175,7 @@ public class Main { const t = new Turtle(); call(drawSquare, 't')(t); t.backward(); +t.backward(); call(drawSquare, 't')(t); function drawSquare(t) { @@ -2189,9 +2190,10 @@ function drawSquare(t) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid - 正方形を描く(t); + 正方形を描く(t); // caller + t.後に戻る(); // sid t.後に戻る(); // sid - 正方形を描く(t); + 正方形を描く(t); // caller } static void 正方形を描く(Turtle t) { for (int i = 0; i < 3; i++) { // sid @@ -2223,8 +2225,8 @@ function double(a) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid - int x = 二倍する(<2-3>); // sid - N歩前に進める(t, x); + int x = 二倍する(<2-3>); // sid // caller + N歩前に進める(t, x); // caller } static void N歩前に進める(Turtle t, int n) { for (int i = 0; i < n; i++) { // sid @@ -2261,11 +2263,11 @@ function double(a) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid - int x = 二倍する(2); // sid - N歩前に進める(t, x); + int x = 二倍する(2); // sid // caller + N歩前に進める(t, x); // caller t.右を向く(); // sid - x = 二倍する(x - 1); // sid - N歩前に進める(t, x); + x = 二倍する(x - 1); // sid // caller + N歩前に進める(t, x); // caller } static void N歩前に進める(Turtle t, int n) { for (int i = 0; i < n; i++) { // sid @@ -2299,8 +2301,8 @@ function add(a, b) { public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid - N歩前に進める(t, 加算する(1, 1)); - N歩前に進める(t, 加算する(1, 2)); + N歩前に進める(t, 加算する(1, 1)); // caller + N歩前に進める(t, 加算する(1, 2)); // caller } static void N歩前に進める(Turtle t, int n) { for (int i = 0; i < n; i++) { // sid @@ -2317,10 +2319,15 @@ public class Main { instrumented: ` const t = new Turtle(); for (s.set('i', 0); s.get('i') < <3-4>; s.set('i', s.get('i') + 1)) { - if (call(isEven, 'a')(s.get('i'))) + if (call(isEven, 'a')(s.get('i'))) { t.turnRight(); - else call(forwardTwoSteps, 't')(t); + } + else { + call(forwardTwoSteps, 't')(t); + t.turnRight(); + t.turnRight(); + } } delete s.vars['i']; @@ -2330,7 +2337,7 @@ function forwardTwoSteps(t) { } function isEven(a) { - return a % 2 == 0; + return a % 2 === 0; } `.trim(), @@ -2339,10 +2346,14 @@ public class Main { public static void main(String[] args) { Turtle t = new Turtle(); // sid for (int i = 0; i < <3-4>; i++) { // sid - if (偶数か(i)) + if (偶数か(i)) { // caller t.右を向く(); // sid - else - 二歩前に進める(t); + 二歩前に進める(t); // caller + } else { + 二歩前に進める(t); // caller + t.右を向く(); // sid + t.右を向く(); // sid + } } } static void 二歩前に進める(Turtle t) { @@ -2350,7 +2361,7 @@ public class Main { t.前に進む(); // sid } static boolean 偶数か(int a) { - return a % 2 == 0; // sid + return a % 2 == 0; } } `.trim(), @@ -2385,10 +2396,10 @@ public class Main { Turtle t = new Turtle(); // sid for (int i = 0; i < 3; i++) { // sid for (int j = 0; j < 3; j++) { // sid - if (等しいか(i, j)) + if (等しいか(i, j)) // caller t.右を向く(); // sid else - 二歩前に進める(t); + 二歩前に進める(t); // caller } t.左を向く(); // sid } @@ -2427,7 +2438,7 @@ public class Main { Turtle t = new Turtle(); // sid int[] arr = { 2, <1-2>, <1-2> }; // sid for (int i = 0; i < arr.length; i++) { // sid - N歩前に進める(t, arr[i]); + N歩前に進める(t, arr[i]); // caller t.右を向く(); // sid } } @@ -2462,7 +2473,7 @@ public class Main { Turtle t = new Turtle(); // sid int[] arr = { <4-5>, <3-4>, <3-4> }; // sid for (int i = 0; i < arr.length; i++) { // sid - N歩前に進める(t, arr[i]); + N歩前に進める(t, arr[i]); // caller t.右を向く(); // sid } } @@ -2567,6 +2578,7 @@ for (const cmd of [0, 1, 0, 2, 0, 3, 0]) { s.set('steps', s.get('steps') + 1); break; } } +delete s.vars['cmd']; `.trim(), java: ` public class Main { @@ -2709,7 +2721,8 @@ const t = new Turtle(); s.set('cmds', ['ri', 'aa', 'fo']); for (const cmd of ['ri', 'aa', 'fo']) { s.set('cmd', cmd); - call(parse, 't', 'cmd')(t, s.get('cmd')); + t.forward(); + call(parse, 't', 'c')(t, s.get('cmd')); } delete s.vars['cmd']; @@ -2724,7 +2737,8 @@ public class Main { Turtle t = new Turtle(); // sid String[] cmds = { "ri", "aa", "fo" }; // sid for (String cmd : cmds) { // sid - parse(t, cmd); + t.前に進む(); // sid + parse(t, cmd); // caller } } static void parse(Turtle t, String c) { @@ -2743,14 +2757,15 @@ s.set('cmds', ['ri', 'add', 'fo', 'add', 'le', 'fo', 'fo']); s.set('x', 0); for (const cmd of ['ri', 'add', 'fo', 'add', 'le', 'fo', 'fo']) { s.set('cmd', cmd); - call(parse, 't', 'cmd', 'x')(t, s.get('cmd'), s.get('x')); + call(parse, 't', 'c', 'x')(t, s.get('cmd'), s.get('x')); if (cmd === 'add') { s.set('x', s.get('x') + 1); } } +delete s.vars['cmd']; function parse(t, c, x) { - if (c === 'fo') call(forwardGivenSteps, 't', 'x')(t, x); + if (c === 'fo') call(forwardGivenSteps, 't', 'n')(t, x); else if (c === 'ri') t.turnRight(); else if (c === 'le') t.turnLeft(); } @@ -2769,7 +2784,7 @@ public class Main { String[] cmds = { "ri", "add", "fo", "add", "le", "fo", "fo" }; // sid int x = 0; // sid for (String cmd : cmds) { // sid - parse(t, cmd, x); + parse(t, cmd, x); // caller if (cmd.equals("add")) { x++; // sid } @@ -2777,7 +2792,7 @@ public class Main { } static void parse(Turtle t, String c, int x) { if (c.equals("fo")) - forwardGivenSteps(t, x); + forwardGivenSteps(t, x); // caller else if (c.equals("ri")) t.turnRight(); // sid else if (c.equals("le")) diff --git a/src/problems/traceProgram.ts b/src/problems/traceProgram.ts index a9015fd2..784699ac 100644 --- a/src/problems/traceProgram.ts +++ b/src/problems/traceProgram.ts @@ -190,9 +190,11 @@ function call(cid, f, ...argNames) { throw new Error(\`Expected \${argNames.length} arguments, got \${argValues.length}.\`); } try { + callStack.push(cid); s.enterNewScope(argNames.map((n, i) => [n, argValues[i]]).filter(([n, v]) => !(v instanceof Turtle))); return f(...argValues); } finally { + callStack.pop(); s.leaveScope(); } }; @@ -231,5 +233,12 @@ ${modifiedCode.trim()} refinedLines.push(refinedLine); } - return { languageId, displayProgram: refinedLines.join('\n'), traceItems: trace, sidToLineIndex, finalVars }; + return { + languageId, + displayProgram: refinedLines.join('\n'), + traceItems: trace, + sidToLineIndex, + callerIdToLineIndex, + finalVars, + }; }