From 8288f3a1358b28caed0aa76a21ec2f2654aa2aec Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Wed, 25 Sep 2024 18:15:32 +0900 Subject: [PATCH] refactor: remove checkpoints (#142) --- .../migration.sql | 16 ++ prisma/schema.prisma | 4 +- .../[courseId]/lectures/[lectureId]/page.tsx | 13 +- .../lectures/[lectureId]/pageOnClient.tsx | 43 ++-- .../problems/[problemId]/BoardEditor.tsx | 24 +-- .../problems/[problemId]/BoardViewer.tsx | 4 +- .../problems/[problemId]/Problems.tsx | 190 +++++++----------- .../[problemId]/SyntaxHighlighter.tsx | 14 +- .../problems/[problemId]/Variables.tsx | 4 +- .../[lectureId]/problems/[problemId]/page.tsx | 15 +- .../problems/[problemId]/pageOnClient.tsx | 155 ++++++-------- .../(withAuth)/courses/[courseId]/page.tsx | 1 - src/app/{lib => utils}/random.ts | 0 src/app/{lib => utils}/sessionStorage.ts | 0 src/constants.ts | 15 +- .../trpcBackend/routers/index.ts | 17 +- src/problems/generateProblem.ts | 7 +- src/problems/problemData.ts | 108 +++++----- src/problems/traceProgram.ts | 15 +- src/types.ts | 2 +- src/utils/constants.ts | 3 - src/utils/sessionOnServer.ts | 3 +- tests/unit/traceCode.test.ts | 17 +- 23 files changed, 287 insertions(+), 383 deletions(-) create mode 100644 prisma/migrations/20240925045846_rename_to_problem_submission/migration.sql rename src/app/{lib => utils}/random.ts (100%) rename src/app/{lib => utils}/sessionStorage.ts (100%) delete mode 100644 src/utils/constants.ts diff --git a/prisma/migrations/20240925045846_rename_to_problem_submission/migration.sql b/prisma/migrations/20240925045846_rename_to_problem_submission/migration.sql new file mode 100644 index 00000000..d2e24fd1 --- /dev/null +++ b/prisma/migrations/20240925045846_rename_to_problem_submission/migration.sql @@ -0,0 +1,16 @@ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "ProblemSessionAnswer"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "ProblemSubmission" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "sessionId" INTEGER NOT NULL, + "problemType" TEXT NOT NULL, + "traceItemIndex" INTEGER NOT NULL, + "elapsedMilliseconds" INTEGER NOT NULL, + "isCorrect" BOOLEAN NOT NULL, + CONSTRAINT "ProblemSubmission_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "ProblemSession" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d93116c0..82615d2d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,12 +41,12 @@ model ProblemSession { elapsedMilliseconds Int @default(0) completedAt DateTime? - answers ProblemSessionAnswer[] + submissions ProblemSubmission[] @@index([userId, courseId, lectureId, problemId, completedAt]) } -model ProblemSessionAnswer { +model ProblemSubmission { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/page.tsx index e63cd08e..ea7f2995 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/page.tsx @@ -26,21 +26,12 @@ const LecturePage: NextPage = async (props) => { problemId: true, completedAt: true, elapsedMilliseconds: true, - answers: { select: { elapsedMilliseconds: true, isCorrect: true } }, + submissions: { select: { elapsedMilliseconds: true, isCorrect: true } }, }, where: { userId: session.superTokensUserId, courseId: props.params.courseId, lectureId: props.params.lectureId }, }); - return ( - s.completedAt).map((s) => s.problemId)) - } - currentUserProblemSessions={currentUserProblemSessions} - lectureIndex={lectureIndex} - params={props.params} - /> - ); + return ; }; export default LecturePage; diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/pageOnClient.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/pageOnClient.tsx index 74678d19..2be53132 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/pageOnClient.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/pageOnClient.tsx @@ -1,8 +1,8 @@ 'use client'; -import type { ProblemSession, ProblemSessionAnswer } from '@prisma/client'; +import type { ProblemSession, ProblemSubmission } from '@prisma/client'; import NextLink from 'next/link'; -import React from 'react'; +import React, { useMemo } from 'react'; import { MdCheckCircle, MdCheckCircleOutline, MdOutlineVerified, MdVerified } from 'react-icons/md'; import { @@ -35,18 +35,19 @@ import { type Props = { params: { courseId: CourseId; lectureId: string }; lectureIndex: number; - currentUserCompletedProblemIdSet: ReadonlySet; - currentUserProblemSessions: (Pick & { - answers: Pick[]; + problemSessions: (Pick & { + submissions: Pick[]; })[]; }; export const Lecture: React.FC = (props) => { - const lectureProblemIds = courseIdToLectureIndexToProblemIds[props.params.courseId][props.lectureIndex]; + const completedProblemIdSet = useMemo( + () => new Set(props.problemSessions.filter((s) => s.completedAt).map((s) => s.problemId)), + [props.problemSessions] + ); - const completedProblemCount = lectureProblemIds.filter((problemId) => - props.currentUserCompletedProblemIdSet.has(problemId) - ).length; + const lectureProblemIds = courseIdToLectureIndexToProblemIds[props.params.courseId][props.lectureIndex]; + const completedProblemCount = lectureProblemIds.filter((problemId) => completedProblemIdSet.has(problemId)).length; const isLessonCompleted = completedProblemCount >= lectureProblemIds.length; return ( @@ -91,23 +92,21 @@ export const Lecture: React.FC = (props) => { - 初回不正解 + 初回の不正解 - 初回所要時間 + 初回の完了日時 {lectureProblemIds.map((problemId) => { - const firstSession = props.currentUserProblemSessions.find((s) => s.problemId === problemId); - - const suspendedSession = props.currentUserProblemSessions.find( + const firstSession = props.problemSessions.find((s) => s.problemId === problemId); + const suspendedSession = props.problemSessions.find( (s) => s.problemId === problemId && !s.completedAt ); - - const isProblemCompleted = props.currentUserCompletedProblemIdSet.has(problemId); + const isProblemCompleted = completedProblemIdSet.has(problemId); return ( @@ -132,21 +131,13 @@ export const Lecture: React.FC = (props) => { - {firstSession?.answers.filter((a) => !a.isCorrect).length ?? 0} + {firstSession?.submissions.filter((a) => !a.isCorrect).length ?? 0} - {/* TODO: なぜ`firstSession?.elapsedMilliseconds`をそのまま表示しない実装になっているのか確認する。 */} - {typeof firstSession?.elapsedMilliseconds === 'number' - ? Math.floor( - firstSession.answers.reduce((sum, answer) => sum + answer.elapsedMilliseconds, 0) / 1000 - ) - : 0} - - 秒 - + {firstSession?.completedAt?.toLocaleString() ?? '未完了'} ); 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 b54db119..00ee30ff 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 @@ -34,32 +34,32 @@ interface TurtleGraphicsProps { isEditable?: boolean; problem: Problem; currentTraceItem?: TraceItem; - beforeTraceItem?: TraceItem; + previousTraceItem?: TraceItem; } export interface TurtleGraphicsHandle { initialize(): void; - isPassed(): boolean; + isCorrect(): boolean; } export const BoardEditor = forwardRef( - ({ beforeTraceItem, currentTraceItem, isEditable = false, problem }, ref) => { + ({ currentTraceItem, isEditable = false, previousTraceItem, problem }, ref) => { const [board, setBoard] = useState([]); const [characters, setCharacters] = useState<(CharacterTrace & { key: string })[]>([]); const [selectedCharacter, setSelectedCharacter] = useState(); const [selectedCell, setSelectedCell] = useState(); const initialize = useCallback((): void => { - console.log('initialize:', problem, beforeTraceItem); - if (!problem || !beforeTraceItem) return; + console.log('initialize:', problem, previousTraceItem); + if (!problem || !previousTraceItem) return; - const initBoard = beforeTraceItem.board + const initBoard = previousTraceItem.board .trim() .split('\n') .filter((line) => line.trim() !== '') .map((line) => [...line.trim()]); - const variables = beforeTraceItem.vars; + const variables = previousTraceItem.vars; const initCharacters = []; const initOtherVars = []; for (const key in variables) { @@ -75,17 +75,17 @@ export const BoardEditor = forwardRef setCharacters(initCharacters || []); setSelectedCharacter(undefined); setSelectedCell(undefined); - }, [beforeTraceItem, problem]); + }, [previousTraceItem, problem]); useImperativeHandle(ref, () => ({ // 親コンポーネントから関数を呼び出せるようにする initialize, - isPassed, + isCorrect, })); useEffect(() => { initialize(); - }, [beforeTraceItem, initialize, problem]); + }, [previousTraceItem, initialize, problem]); const updateCharacters = (character: CharacterTrace & { key: string }): void => { setSelectedCharacter(character); @@ -107,7 +107,7 @@ export const BoardEditor = forwardRef }); }; - const isPassed = (): boolean => { + const isCorrect = (): boolean => { if (!currentTraceItem) return false; const variables = currentTraceItem.vars; @@ -336,7 +336,7 @@ export const BoardEditor = forwardRef )} {!selectedCharacter && selectedPosition && ( )} 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 a5f85cd0..492a1167 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 @@ -11,7 +11,7 @@ import { TURTLE_GRAPHICS_BOARD_ROWS as ROWS, } from '../../../../../../../../constants'; import { Box, Grid, GridItem, Img, keyframes } from '../../../../../../../../infrastructures/useClient/chakra'; -import type { CharacterTrace, TraceItemVar } from '../../../../../../../../problems/traceProgram'; +import type { CharacterTrace, TraceItemVariable } from '../../../../../../../../problems/traceProgram'; import { charToColor } from '../../../../../../../../problems/traceProgram'; const CHAR_TO_BG_COLOR = { @@ -52,7 +52,7 @@ const focusRingKeyframes = keyframes({ type Props = BoxProps & { board: string | undefined; - vars: TraceItemVar | undefined; + vars: TraceItemVariable | undefined; focusedCell?: { x: number; y: number }; enableTransitions?: boolean; onCellClick?: (x: number, y: number) => void; diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Problems.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Problems.tsx index 24a300e4..2789625d 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Problems.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/Problems.tsx @@ -34,7 +34,7 @@ import { Variables } from './Variables'; interface ExecutionResultProblemProps { problem: Problem; - createAnswerLog: (isPassed: boolean) => void; + createSubmission: (isCorrect: boolean) => void; handleComplete: () => void; setCurrentTraceItemIndex: (line: number) => void; setProblemType: (step: ProblemType) => void; @@ -45,35 +45,20 @@ export const ExecutionResultProblem: React.FC = (pr return ( ); }; -interface CheckpointProblemProps { - setProblemType: (step: ProblemType) => void; - problem: Problem; - beforeTraceItemIndex: number; - createAnswerLog: (isPassed: boolean) => void; - currentTraceItemIndex: number; - setBeforeTraceItemIndex: (line: number) => void; - setCurrentTraceItemIndex: (line: number) => void; -} - -export const CheckpointProblem: React.FC = (props) => { - console.log('CheckpointProblem'); - return ; -}; - interface StepProblemProps { - beforeTraceItemIndex: number; - createAnswerLog: (isPassed: boolean) => void; + previousTraceItemIndex: number; + createSubmission: (isCorrect: boolean) => void; currentTraceItemIndex: number; problem: Problem; handleComplete: () => void; - setBeforeTraceItemIndex: (line: number) => void; + setPreviousTraceItemIndex: (line: number) => void; setCurrentTraceItemIndex: (line: number) => void; } @@ -84,23 +69,23 @@ export const StepProblem: React.FC = (props) => { interface ProblemProps { problem: Problem; - createAnswerLog: (isPassed: boolean) => void; + createSubmission: (isCorrect: boolean) => void; setCurrentTraceItemIndex: (line: number) => void; setProblemType?: (step: ProblemType) => void; handleComplete?: () => void; - beforeTraceItemIndex: number; + previousTraceItemIndex: number; currentTraceItemIndex: number; - setBeforeTraceItemIndex?: (line: number) => void; + setPreviousTraceItemIndex?: (line: number) => void; } -const ProblemComponent: React.FC = ({ - beforeTraceItemIndex, - createAnswerLog, +const ProblemComponent: React.FC = ({ + createSubmission, currentTraceItemIndex, handleComplete, + previousTraceItemIndex, problem, - setBeforeTraceItemIndex, setCurrentTraceItemIndex, + setPreviousTraceItemIndex, setProblemType, type, }) => { @@ -125,80 +110,38 @@ const ProblemComponent: React.FC => { - const isPassed = turtleGraphicsRef.current?.isPassed() || false; - createAnswerLog(isPassed); + const isCorrect = turtleGraphicsRef.current?.isCorrect() || false; + createSubmission(isCorrect); switch (type) { case 'executionResult': { - if (isPassed) { - handleComplete?.(); - openAlertDialog('正解', '正解です。この問題は終了です'); - break; - } - - // チェックポイント機能は理解しにくいので、一時的に無効化する。(ステップ実行機能だけを使う。) - openAlertDialog('不正解', '不正解です。ステップごとに回答してください', () => { - setCurrentTraceItemIndex(1); - setProblemType?.('step'); - }); - // if (problem.checkpointSids.length > 0) { - // openAlertDialog('不正解', '不正解です。チェックポイントごとに回答してください', () => { - // const nextCheckpointTraceItemIndex = problem.traceItems.findIndex( - // (traceItem) => traceItem.sid === problem.checkpointSids[0] - // ); - // setCurrentTraceItemIndex(nextCheckpointTraceItemIndex); - // setProblemType?.('checkpoint'); - // }); - // } else { - // openAlertDialog('不正解', '不正解です。ステップごとに回答してください', () => { - // setCurrentTraceItemIndex(1); - // setProblemType?.('step'); - // }); - // } - break; - } - case 'checkpoint': { - if (isPassed) { - setBeforeTraceItemIndex?.(currentTraceItemIndex); - if (problem.traceItems[currentTraceItemIndex].sid === problem.checkpointSids.at(-1)) { - openAlertDialog('正解', '正解です。このチェックポイントから1行ずつ回答してください', () => { - setCurrentTraceItemIndex(currentTraceItemIndex + 1); - setProblemType?.('step'); - }); - } else { - openAlertDialog('正解', '正解です。次のチェックポイントに進みます', () => { - const nextCheckpointIndex = - problem.checkpointSids.indexOf(problem.traceItems[currentTraceItemIndex].sid) + 1; - const nextCheckpointTraceItemIndex = problem.traceItems.findIndex( - (traceItem) => traceItem.sid === problem.checkpointSids[nextCheckpointIndex] - ); - setCurrentTraceItemIndex(nextCheckpointTraceItemIndex); - }); - } + if (!isCorrect) { + openAlertDialog('不正解', '不正解です。ステップごとに回答してください', () => { + setCurrentTraceItemIndex(1); + setProblemType?.('step'); + }); break; } - openAlertDialog('不正解', '不正解です。最後に正解したチェックポイントから1行ずつ回答してください', () => { - setCurrentTraceItemIndex(beforeTraceItemIndex + 1); - setProblemType?.('step'); - }); + handleComplete?.(); + openAlertDialog('正解', '正解です。この問題は終了です'); break; } case 'step': { - if (isPassed) { - if (currentTraceItemIndex === problem.traceItems.length - 1) { - handleComplete?.(); - openAlertDialog('正解', '正解です。この問題は終了です'); - } else { - openAlertDialog('正解', '正解です。次の行に進みます', () => { - setBeforeTraceItemIndex?.(currentTraceItemIndex); - setCurrentTraceItemIndex(currentTraceItemIndex + 1); - }); - } + if (!isCorrect) { + openAlertDialog('不正解', '不正解です。もう一度回答してください'); break; } - openAlertDialog('不正解', '不正解です。もう一度回答してください'); + if (currentTraceItemIndex === problem.traceItems.length - 1) { + handleComplete?.(); + openAlertDialog('正解', '正解です。この問題は終了です'); + } else { + openAlertDialog('正解', '正解です。次の行に進みます', () => { + setPreviousTraceItemIndex?.(currentTraceItemIndex); + setCurrentTraceItemIndex(currentTraceItemIndex + 1); + }); + } break; } } @@ -237,7 +180,7 @@ const ProblemComponent: React.FC {problem.sidToLineIndex.get(problem.traceItems[currentTraceItemIndex].sid)}行目 - を{type === 'checkpoint' ? '初めて' : ''}実行した後 + を実行した後 )} @@ -249,67 +192,70 @@ const ProblemComponent: React.FC - {type !== 'executionResult' && problem.sidToLineIndex.get(problem.traceItems[beforeTraceItemIndex]?.sid) && ( - - - - - {problem.sidToLineIndex.get(problem.traceItems[beforeTraceItemIndex]?.sid)}行目 - - を実行した後の盤面 - - - 実行前ではなく実行後! - - - - + {type !== 'executionResult' && + problem.sidToLineIndex.get(problem.traceItems[previousTraceItemIndex]?.sid) && ( + + + + 参考: + + {problem.sidToLineIndex.get(problem.traceItems[previousTraceItemIndex]?.sid)}行目 + + を実行した後( + + {problem.sidToLineIndex.get(problem.traceItems[currentTraceItemIndex]?.sid)}行目 + + を実行する前)の盤面 + + + + - - - )} + + + )} 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 562e06e9..eaef5646 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 @@ -8,16 +8,16 @@ import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { Box } from '../../../../../../../../infrastructures/useClient/chakra'; type SyntaxHighlighterProps = BoxProps & { - beforeCheckpointLine?: number; + previousFocusLine?: number; code: string; programmingLanguageId: string; - currentCheckpointLine?: number; + currentFocusLine?: number; }; export const SyntaxHighlighter: React.FC = ({ - beforeCheckpointLine, code, - currentCheckpointLine, + currentFocusLine, + previousFocusLine, programmingLanguageId, ...boxProps }) => { @@ -35,11 +35,11 @@ export const SyntaxHighlighter: React.FC = ({ lineNumberStyle={{ minWidth: '1.5rem', marginRight: '2rem', paddingRight: 0 }} lineProps={(lineNumber) => { const style: React.CSSProperties = { padding: '0 1rem', backgroundColor: '' }; - // チェックポイント問題・ステップ問題のハイライト - if (lineNumber === beforeCheckpointLine) { + // ステップ問題のハイライト + if (lineNumber === previousFocusLine) { style.backgroundColor = '#feebc8' /* orange.100 */; } - if (lineNumber === currentCheckpointLine) { + if (lineNumber === currentFocusLine) { style.backgroundColor = '#fed7d7' /* red.100 */; } return { style }; 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 5ee0b4ae..9785ecb4 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 @@ -12,10 +12,10 @@ import { Thead, Tr, } from '../../../../../../../../infrastructures/useClient/chakra'; -import { charToColor, type TraceItemVar } from '../../../../../../../../problems/traceProgram'; +import { charToColor, type TraceItemVariable } from '../../../../../../../../problems/traceProgram'; interface VariablesProps { - traceItemVars?: TraceItemVar; + traceItemVars?: TraceItemVariable; } export const dirCharToJapanese = { diff --git a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/page.tsx index e20aa919..9879e132 100644 --- a/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/lectures/[lectureId]/problems/[problemId]/page.tsx @@ -1,6 +1,7 @@ import type { NextPage } from 'next'; import { DEFAULT_LANGUAGE_ID } from '../../../../../../../../constants'; +import { logger } from '../../../../../../../../infrastructures/pino'; import { prisma } from '../../../../../../../../infrastructures/prisma'; import { generateProblem } from '../../../../../../../../problems/generateProblem'; import type { CourseId, ProblemId } from '../../../../../../../../problems/problemData'; @@ -15,7 +16,7 @@ type Props = { const ProblemPage: NextPage = async (props) => { const session = await getNonNullableSessionOnServer(); - let problemSession = await prisma.problemSession.findFirst({ + let incompleteProblemSession = await prisma.problemSession.findFirst({ where: { userId: session.superTokensUserId, courseId: props.params.courseId, @@ -25,9 +26,8 @@ const ProblemPage: NextPage = async (props) => { completedAt: null, }, }); - - if (!problemSession) { - problemSession = await prisma.problemSession.create({ + if (!incompleteProblemSession) { + incompleteProblemSession = await prisma.problemSession.create({ data: { userId: session.superTokensUserId, courseId: props.params.courseId, @@ -40,17 +40,18 @@ const ProblemPage: NextPage = async (props) => { }, }); } + logger.debug('incompleteProblemSession: %o', incompleteProblemSession); const problem = generateProblem( - problemSession.problemId as ProblemId, + incompleteProblemSession.problemId as ProblemId, DEFAULT_LANGUAGE_ID, - problemSession.problemVariablesSeed + incompleteProblemSession.problemVariablesSeed ); if (!problem) throw new Error('Failed to generate problem.'); return ( = (props) => { - const [suspendedSession, setSuspendedSession] = useState(props.initialProblemSession); - const [problemType, setProblemType] = useState( - props.initialProblemSession.currentProblemType as ProblemType - ); const lectureIndex = courseIdToLectureIds[props.params.courseId].indexOf(props.params.lectureId); - const [currentTraceItemIndex, setCurrentTraceItemIndex] = useState(0); - const [previousTraceItemIndex, setPreviousTraceItemIndex] = useState(0); - const [lastTimeSpent, setLastTimeSpent] = useState(0); - - const idleTimer = useIdleTimer({ timeout: 10_000, throttle: 500 }); + const [problemType, setProblemType] = useState(props.initialProblemSession.currentProblemType as ProblemType); + const [currentTraceItemIndex, setCurrentTraceItemIndex] = useState(props.initialProblemSession.currentTraceItemIndex); + const [previousTraceItemIndex, setPreviousTraceItemIndex] = useState( + props.initialProblemSession.previousTraceItemIndex + ); const updateProblemSessionMutation = backendTrpcReact.updateProblemSession.useMutation(); - const createProblemSessionAnswerMutation = backendTrpcReact.createProblemSessionAnswer.useMutation(); - - useEffect(() => { - const interval = window.setInterval(async () => { - if (suspendedSession && !idleTimer.isIdle()) { - await updateProblemSessionMutation.mutateAsync({ - id: suspendedSession.id, - elapsedMilliseconds: lastTimeSpent + idleTimer.getActiveTime(), - }); - } - }, INTERVAL_MS_OF_IDLE_TIMER); - - return () => { - window.clearInterval(interval); - }; - }, [suspendedSession, lastTimeSpent, updateProblemSessionMutation, idleTimer]); - - useEffect(() => { - // 中断中のセッションを再開する - if (!suspendedSession) return; - - setProblemType(suspendedSession.currentProblemType as ProblemType); - setPreviousTraceItemIndex(suspendedSession.previousTraceItemIndex); - setCurrentTraceItemIndex(suspendedSession.currentTraceItemIndex); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!props.userId || !props.params.courseId || !props.params.problemId || !suspendedSession) return; - - (async () => { - const updated = await updateProblemSessionMutation.mutateAsync({ - id: suspendedSession.id, - currentProblemType: problemType, - currentTraceItemIndex: problemType === 'executionResult' ? 0 : currentTraceItemIndex, - previousTraceItemIndex: problemType === 'executionResult' ? 0 : previousTraceItemIndex, - elapsedMilliseconds: suspendedSession.elapsedMilliseconds, + const createProblemSubmissionMutation = backendTrpcReact.createProblemSubmission.useMutation(); + + const lastActionTimeRef = useRef(Date.now()); + useIdleTimer({ + async onAction() { + await updateProblemSessionMutation.mutateAsync({ + id: props.initialProblemSession.id, + incrementalElapsedMilliseconds: getIncrementalElapsedMilliseconds(lastActionTimeRef), }); - if (updated) { - setSuspendedSession(updated); - setLastTimeSpent(updated.elapsedMilliseconds); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentTraceItemIndex, problemType]); + }, + // Events within the throttle period are ignored. + throttle: MIN_INTERVAL_MS_OF_ACTIVE_EVENTS, + }); - const handleSolveProblem = async (): Promise => { - console.log('handleSolveProblem:', props.userId, suspendedSession); - if (!props.userId || !suspendedSession) return; + useEffect(() => { + // ステップ実行中に以下の式は成り立たない。不整合を起こさないように、念の為チェックする。 + if (currentTraceItemIndex === previousTraceItemIndex) return; - await updateProblemSessionMutation.mutateAsync({ - id: suspendedSession.id, + void updateProblemSessionMutation.mutateAsync({ + id: props.initialProblemSession.id, + incrementalElapsedMilliseconds: getIncrementalElapsedMilliseconds(lastActionTimeRef), currentProblemType: problemType, - currentTraceItemIndex: problemType === 'executionResult' ? 0 : currentTraceItemIndex, - previousTraceItemIndex: problemType === 'executionResult' ? 0 : previousTraceItemIndex, - elapsedMilliseconds: suspendedSession.elapsedMilliseconds, - completedAt: new Date(), + currentTraceItemIndex, + previousTraceItemIndex, }); - }; - - const createAnswerLog = async (isCorrect: boolean): Promise => { - if (!props.userId || !suspendedSession) return; - - const activeTime = idleTimer.getActiveTime(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentTraceItemIndex]); - await createProblemSessionAnswerMutation.mutateAsync({ - sessionId: suspendedSession.id, + const createSubmission = async (isCorrect: boolean): Promise => { + const { elapsedMilliseconds } = await updateProblemSessionMutation.mutateAsync({ + id: props.initialProblemSession.id, + incrementalElapsedMilliseconds: getIncrementalElapsedMilliseconds(lastActionTimeRef), + }); + await createProblemSubmissionMutation.mutateAsync({ + sessionId: props.initialProblemSession.id, problemType, traceItemIndex: currentTraceItemIndex, - elapsedMilliseconds: activeTime, + elapsedMilliseconds, isCorrect, }); + }; - const updated = await updateProblemSessionMutation.mutateAsync({ - id: suspendedSession.id, - elapsedMilliseconds: lastTimeSpent + activeTime, + const handleSolveProblem = async (): Promise => { + await updateProblemSessionMutation.mutateAsync({ + id: props.initialProblemSession.id, + incrementalElapsedMilliseconds: getIncrementalElapsedMilliseconds(lastActionTimeRef), + completedAt: new Date(), }); - - setLastTimeSpent(updated.elapsedMilliseconds); - idleTimer.reset(); }; return ( @@ -140,33 +107,33 @@ export const ProblemPageOnClient: React.FC = (props) => { {problemType === 'executionResult' ? ( - ) : problemType === 'checkpoint' ? ( - ) : ( )} ); }; + +function getIncrementalElapsedMilliseconds(lastActionTimeRef: React.MutableRefObject): number { + const nowTime = Date.now(); + const incrementalElapsedMilliseconds = Math.min( + nowTime - lastActionTimeRef.current, + MAX_ACTIVE_DURATION_MS_AFTER_LAST_EVENT + ); + lastActionTimeRef.current = nowTime; + return incrementalElapsedMilliseconds; +} diff --git a/src/app/(withAuth)/courses/[courseId]/page.tsx b/src/app/(withAuth)/courses/[courseId]/page.tsx index eda7f7a5..157f148a 100644 --- a/src/app/(withAuth)/courses/[courseId]/page.tsx +++ b/src/app/(withAuth)/courses/[courseId]/page.tsx @@ -22,7 +22,6 @@ const CoursePage: NextPage = async (props) => { select: { problemId: true, completedAt: true }, where: { userId: session.superTokensUserId, courseId: props.params.courseId }, }); - console.trace('currentUserProblemSessions:', currentUserProblemSessions); return ( diff --git a/src/app/lib/random.ts b/src/app/utils/random.ts similarity index 100% rename from src/app/lib/random.ts rename to src/app/utils/random.ts diff --git a/src/app/lib/sessionStorage.ts b/src/app/utils/sessionStorage.ts similarity index 100% rename from src/app/lib/sessionStorage.ts rename to src/app/utils/sessionStorage.ts diff --git a/src/constants.ts b/src/constants.ts index 22453055..d6b8eeb7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,6 @@ export const APP_AUTHOR = 'Kazunori Sakamoto and contributors'; export const APP_DESCRIPTION = 'An educational web app for training trace skills.'; export const APP_NAME = 'トレース道場'; -export const INTERVAL_MS_OF_IDLE_TIMER = 1000 * 60; export const TURTLE_GRAPHICS_BOARD_COLUMNS = 7; export const TURTLE_GRAPHICS_BOARD_ROWS = 7; @@ -12,3 +11,17 @@ export const TURTLE_GRAPHICS_EMPTY_COLOR = '.'; export const TURTLE_GRAPHICS_DEFAULT_COLOR = '#'; export const DEFAULT_LANGUAGE_ID = 'java'; + +export const SUPERTOKENS_ACCESS_TOKEN_COOKIE_NAME = 'sAccessToken'; +export const SUPERTOKENS_REFRESH_TOKEN_COOKIE_NAME = 'sRefreshToken'; +export const SERVICE_ADMIN_ROLE = 'admin'; + +/** + * 最後のイベントから今回のイベント発生時の期間において、アクティブと解釈される最大時間。 + */ +export const MAX_ACTIVE_DURATION_MS_AFTER_LAST_EVENT = 1000 * 60 * 5; + +/** + * アクティブなイベントを検出するための最小間隔。 + */ +export const MIN_INTERVAL_MS_OF_ACTIVE_EVENTS = 1000 * 60; diff --git a/src/infrastructures/trpcBackend/routers/index.ts b/src/infrastructures/trpcBackend/routers/index.ts index 76ead387..6f95547e 100644 --- a/src/infrastructures/trpcBackend/routers/index.ts +++ b/src/infrastructures/trpcBackend/routers/index.ts @@ -20,18 +20,25 @@ export const backendRouter = router({ currentProblemType: z.string().min(1).optional(), currentTraceItemIndex: z.number().int().nonnegative().optional(), previousTraceItemIndex: z.number().int().nonnegative().optional(), - elapsedMilliseconds: z.number().nonnegative().optional(), + incrementalElapsedMilliseconds: z.number().nonnegative().optional(), completedAt: z.date().optional(), }) ) - .mutation(async ({ input: { id, ...data } }) => { - const problemSession = await prisma.problemSession.update({ where: { id }, data }); + .mutation(async ({ input: { id, incrementalElapsedMilliseconds, ...data } }) => { + const problemSession = await prisma.problemSession.update({ + where: { id }, + data: { + elapsedMilliseconds: { increment: incrementalElapsedMilliseconds }, + ...data, + }, + }); + // 開発環境ではページが更新されないので注意すること。 revalidatePath('/courses/[courseId]/lectures/[lectureId]', 'page'); console.log(`revalidatePath('/courses/[courseId]/lectures/[lectureId]', 'page');`); return problemSession; }), - createProblemSessionAnswer: procedure + createProblemSubmission: procedure .use(authorize) .input( z.object({ @@ -47,7 +54,7 @@ export const backendRouter = router({ if (!session) throw new TRPCError({ code: 'NOT_FOUND' }); if (session.userId !== ctx.session.superTokensUserId) throw new TRPCError({ code: 'UNAUTHORIZED' }); - await prisma.problemSessionAnswer.create({ data: input }); + await prisma.problemSubmission.create({ data: input }); }), }); diff --git a/src/problems/generateProblem.ts b/src/problems/generateProblem.ts index 33d1ec2e..a7575a25 100644 --- a/src/problems/generateProblem.ts +++ b/src/problems/generateProblem.ts @@ -1,4 +1,4 @@ -import { Random } from '../app/lib/random'; +import { Random } from '../app/utils/random'; import type { LanguageId, ProblemId } from './problemData'; import { problemIdToLanguageIdToProgram } from './problemData'; @@ -24,11 +24,6 @@ export type Problem = { * The mapping from statement ID to line index. */ sidToLineIndex: Map; - - /** - * The statement IDs of the checkpoints. - */ - checkpointSids: number[]; }; const randomNumberRegex = /<(\d+)-(\d+)>/g; diff --git a/src/problems/problemData.ts b/src/problems/problemData.ts index 7e0a541e..b8c77c38 100644 --- a/src/problems/problemData.ts +++ b/src/problems/problemData.ts @@ -128,7 +128,7 @@ export const problemIdToLanguageIdToProgram: Record, <1-4>)); s.get('亀').forward(); s.get('亀').turnRight(); -s.get('亀').forward(); // CP +s.get('亀').forward(); s.get('亀').turnRight(); s.get('亀').forward(); `.trim(), @@ -242,7 +242,7 @@ public class Main { variable: { instrumented: ` s.set('x', <1-5>); -s.set('亀', new Character(s.get('x'), <1-5>)); // CP +s.set('亀', new Character(s.get('x'), <1-5>)); s.get('亀').forward(); `.trim(), java: ` @@ -260,7 +260,7 @@ public class Main { s.set('x', <1-4>); s.set('x', s.get('x') + 1); s.set('y', s.get('x') + 1); -s.set('亀', new Character(s.get('x'), s.get('y'))); // CP +s.set('亀', new Character(s.get('x'), s.get('y'))); s.get('亀').forward(); `.trim(), java: ` @@ -281,7 +281,7 @@ s.set('x', <1-5>); s.set('x', s.get('x') - 1); s.set('y', s.get('x') * 2); s.set('y', Math.floor(s.get('y') / 3)); -s.set('亀', new Character(s.get('x'), s.get('y'))); // CP +s.set('亀', new Character(s.get('x'), s.get('y'))); s.get('亀').forward(); `.trim(), java: ` @@ -302,7 +302,7 @@ public class Main { s.set('亀', new Character()); s.set('i', 0); while (s.get('i') < <3-5>) { - s.get('亀').forward(); // CP + s.get('亀').forward(); s.set('i', s.get('i') + 1); } `.trim(), @@ -325,7 +325,7 @@ s.set('亀', new Character()); s.set('i', 0); while (s.get('i') < <2-3>) { s.set('i', s.get('i') + 1); - s.get('亀').forward(); // CP + s.get('亀').forward(); s.get('亀').turnRight(); } `.trim(), @@ -347,7 +347,7 @@ public class Main { instrumented: ` s.set('亀', new Character()); for (s.set('i', 0); s.get('i') < <3-5>; s.set('i', s.get('i') + 1)) { - s.get('亀').forward(); // CP + s.get('亀').forward(); } `.trim(), java: ` @@ -366,7 +366,7 @@ public class Main { s.set('亀', new Character()); s.set('i', 0); for (s.set('i', s.get('i')) ; s.get('i') < <2-3>; s.set('i', s.get('i'))) { - s.get('亀').forward(); // CP + s.get('亀').forward(); s.get('亀').turnRight(); s.set('i', s.get('i') + 1); } @@ -392,7 +392,7 @@ for (s.set('i', 2); s.get('i') <= <4-5>; s.set('i', s.get('i') + 1)) { s.set('x', s.get('x') + s.get('i')); } s.set('x', Math.floor(s.get('x') / 3)); -s.set('亀', new Character(s.get('x'), 0)); // CP +s.set('亀', new Character(s.get('x'), 0)); s.get('亀').forward(); `.trim(), java: ` @@ -414,7 +414,7 @@ public class Main { s.set('t', new Character()); for (s.set('i', 0); s.get('i') < <2-3>; s.set('i', s.get('i') + 1)) { for (s.set('j', 0); s.get('j') < <2-3>; s.set('j', s.get('j') + 1)) { - s.get('t').forward(); // CP + s.get('t').forward(); } s.get('t').turnRight(); } @@ -438,7 +438,7 @@ public class Main { s.set('t', new Character()); for (s.set('i', <3-4>); s.get('i') > 0; s.set('i', s.get('i') - 1)) { for (s.set('j', 0); s.get('j') < s.get('i'); s.set('j', s.get('j') + 1)) { - s.get('t').forward(); // CP + s.get('t').forward(); } s.get('t').turnRight(); } @@ -463,7 +463,7 @@ s.set('t', new Character()); for (s.set('i', 0); s.get('i') < <7-9>; s.set('i', s.get('i') + 1)) { s.get('t').forward(); if (s.get('i') % 3 === 2) { - s.get('t').turnRight(); // CP + s.get('t').turnRight(); } } `.trim(), @@ -487,9 +487,9 @@ s.set('t', new Character()); for (s.set('i', 0); s.get('i') < 4; s.set('i', s.get('i') + 1)) { s.get('t').forward(); if (s.get('i') % 2 === 0) { - s.get('t').turnRight(); // CP + s.get('t').turnRight(); } else { - s.get('t').turnLeft(); // CP + s.get('t').turnLeft(); } } `.trim(), @@ -516,9 +516,9 @@ for (s.set('i', 0); s.get('i') < <4-6>; s.set('i', s.get('i') + 1)) { if (s.get('i') < <2-3>) { s.get('t').forward(); } else if (s.get('i') === <2-3>) { - s.get('t').turnLeft(); // CP + s.get('t').turnLeft(); } else { - s.get('t').backward(); // CP + s.get('t').backward(); } } `.trim(), @@ -542,11 +542,11 @@ for (s.set('i', 0); s.get('i') < <5-7>; s.set('i', s.get('i') + 1)) { if (s.get('i') % 4 === 0) { s.get('t').forward(); } else if (s.get('i') % 4 === 1) { - s.get('t').turnRight(); // CP + s.get('t').turnRight(); } else if (s.get('i') % 4 === 2) { - s.get('t').forward(); // CP + s.get('t').forward(); } else { - s.get('t').turnLeft(); // CP + s.get('t').turnLeft(); } } `.trim(), @@ -572,9 +572,9 @@ for (s.set('i', 0); s.get('i') < <5-7>; s.set('i', s.get('i') + 1)) { case 0: case 1: s.get('t').forward(); break; case 2: - s.get('t').turnLeft(); break; // CP + s.get('t').turnLeft(); break; default: - s.get('t').backward(); break; // CP + s.get('t').backward(); break; } } `.trim(), @@ -602,11 +602,11 @@ s.set('t', new Character()); for (s.set('i', 0); s.get('i') < <5-7>; s.set('i', s.get('i') + 1)) { switch (s.get('i') % 4) { case 1: - s.get('t').turnRight(); break; // CP + s.get('t').turnRight(); break; case 3: - s.get('t').turnLeft(); break; // CP + s.get('t').turnLeft(); break; default: - s.get('t').forward(); break; // CP + s.get('t').forward(); break; } } `.trim(), @@ -633,7 +633,7 @@ public class Main { s.set('t', new Character()); while (true) { if (!s.get('t').canMoveForward()) break; - s.get('t').forward(); // CP + s.get('t').forward(); } `.trim(), java: ` @@ -654,11 +654,11 @@ s.set('t', new Character(<3-4>,<3-4>)); while (true) { if (!s.get('t').canMoveForward()) break; s.get('t').forward(); - s.get('t').turnRight(); // CP + s.get('t').turnRight(); if (!s.get('t').canMoveForward()) break; s.get('t').forward(); - s.get('t').turnLeft(); // CP + s.get('t').turnLeft(); } `.trim(), java: ` @@ -686,7 +686,7 @@ for (s.set('i', 0); s.get('i') < 4; s.set('i', s.get('i') + 1)) { s.get('t').forward(); if (!s.get('t').canMoveForward()) break; } - s.get('t').turnRight(); // CP + s.get('t').turnRight(); } `.trim(), java: ` @@ -711,7 +711,7 @@ s.set('t', new Character()); if (s.get('i') == 0) { continue; } - s.get('t').forward(); // CP + s.get('t').forward(); } `.trim(), java: ` @@ -733,7 +733,7 @@ public class Main { s.set('t', new Character()); for (s.set('i', 0); s.get('i') < <5-7>; s.set('i', s.get('i') + 1)) { if (s.get('i') % <2-3> == 1) { - s.get('t').turnRight(); // CP + s.get('t').turnRight(); continue; } s.get('t').forward(); @@ -760,9 +760,9 @@ s.set('t', new Character()); for (s.set('i', 0); s.get('i') < 2; s.set('i', s.get('i') + 1)) { for (s.set('j', s.get('i') * 4); s.get('j') < 8; s.set('j', s.get('j') + 1)) { if (s.get('j') % 4 == 1) { - s.get('t').turnRight(); continue; // CP + s.get('t').turnRight(); continue; } else if (s.get('j') % 4 == 3) { - s.get('t').turnLeft(); continue; // CP + s.get('t').turnLeft(); continue; } s.get('t').forward(); } @@ -792,7 +792,7 @@ public class Main { instrumented: ` s.set('t', new Character()); forwardTwoSteps(s.get('t')); -s.get('t').turnRight(); // CP +s.get('t').turnRight(); threeStepsForward(s.get('t')); function forwardTwoSteps(t) { @@ -840,7 +840,7 @@ public class Main { instrumented: ` s.set('t', new Character()); forwardGivenSteps(s.get('t'), <3-4>); -s.get('t').turnRight(); // CP +s.get('t').turnRight(); forwardGivenSteps(s.get('t'), 2); function forwardGivenSteps(t, n) { @@ -875,7 +875,7 @@ public class Main { instrumented: ` s.set('t', new Character()); forwardTwoSteps(s.get('t')); -s.get('t').turnRight(); // CP +s.get('t').turnRight(); forwardFourSteps(s.get('t')); function forwardTwoSteps(t) { @@ -920,7 +920,7 @@ public class Main { return1: { instrumented: ` s.set('t', new Character()); -s.set('x', double(<2-3>)); // CP +s.set('x', double(<2-3>)); forwardGivenSteps(s.get('t'), s.get('x')); function forwardGivenSteps(t, n) { @@ -1011,7 +1011,7 @@ s.set('t', new Character()); for (s.set('i', 0); s.get('i') < 3; s.set('i', s.get('i') + 1)) { for (s.set('j', 0); s.get('j') < 3; s.set('j', s.get('j') + 1)) { if (isEqual(s.get('i'), s.get('j'))) - s.get('t').turnRight(); // CP + s.get('t').turnRight(); else forwardTwoSteps(s.get('t')); } @@ -1064,10 +1064,10 @@ public class Main { array1: { instrumented: ` s.set('t', new Character()); -s.set('arr', [2, <1-2>, <1-2>]); // CP +s.set('arr', [2, <1-2>, <1-2>]); for (s.set('i', 0); s.get('i') < s.get('arr').length; s.set('i', s.get('i') + 1)) { forwardGivenSteps(s.get('t'), s.get('arr')[s.get('i')]); - s.get('t').turnRight(); // CP + s.get('t').turnRight(); } function forwardGivenSteps(t, n) { @@ -1106,7 +1106,7 @@ s.set('arr', [0, 1, 0, 2, 0]); for (s.set('i', 0); s.get('i') < s.get('arr').length; s.set('i', s.get('i') + 1)) { switch (s.get('arr')[s.get('i')]) { case 0: - s.get('t').forward(); break; // CP + s.get('t').forward(); break; case 1: s.get('t').turnRight(); break; case 2: @@ -1141,7 +1141,7 @@ for (const cmd of [0, 1, 0, 2, 0]) { s.set('cmd', cmd); switch (s.get('cmd')) { case 0: - s.get('t').forward(); break; // CP + s.get('t').forward(); break; case 1: s.get('t').turnRight(); break; case 2: @@ -1175,7 +1175,7 @@ s.set('s', 'frflf'); for (s.set('i', 0); s.get('i') < s.get('s').length; s.set('i', s.get('i') + 1)) { switch (s.get('s').charAt(s.get('i'))) { case 'f': - s.get('t').forward(); break; // CP + s.get('t').forward(); break; case 'r': s.get('t').turnRight(); break; case 'l': @@ -1210,7 +1210,7 @@ for (const ch of 'frflf') { s.set('ch', ch); switch (s.get('ch')) { case 'f': - s.get('t').forward(); break; // CP + s.get('t').forward(); break; case 'r': s.get('t').turnRight(); break; case 'l': @@ -1240,7 +1240,7 @@ public class Main { string3: { instrumented: ` s.set('t', new Character()); -s.set('cmds', ['ri', 'aa', 'fo']); // CP +s.set('cmds', ['ri', 'aa', 'fo']); for (const cmd of ['ri', 'aa', 'fo']) { s.set('cmd', cmd); parse(s.get('t'), s.get('cmd')); @@ -1278,7 +1278,7 @@ public class Main { instrumented: ` s.set('c', new Character()); s.get('c').forward(); -s.get('c').forward(); // CP +s.get('c').forward(); s.get('c').forward(); `.trim(), // sidがただの連番である場合、番号を省略できる。 @@ -1300,7 +1300,7 @@ public class Main { s.set('c', new Character()); for (s.set('i', 0); s.get('i') < 2; s.set('i', s.get('i') + 1)) { s.get('c').forward(); - s.get('c').forward(); // CP + s.get('c').forward(); s.get('c').turnRight(); } `.trim(), @@ -1397,11 +1397,11 @@ public class Main { test5: { instrumented: ` s.set('c', new Character()); -s.get('c').forward(); // CP +s.get('c').forward(); s.get('c').forward(); s.get('c').turnRight(); -s.get('c').forward(); // CP -s.get('c').forward(); // CP character at end: OK +s.get('c').forward(); +s.get('c').forward(); s.get('c').forward(); s.get('c').forward(); `.trim(), diff --git a/src/problems/traceProgram.ts b/src/problems/traceProgram.ts index ef3062fc..fe0fa25a 100644 --- a/src/problems/traceProgram.ts +++ b/src/problems/traceProgram.ts @@ -20,14 +20,14 @@ export interface CharacterTrace { export interface TraceItem { sid: number; - vars: TraceItemVar; + vars: TraceItemVariable; board: string; /** Pythonなどの拡張for文しかない言語において、削除すべき更新式か否か。 */ last?: boolean; } // できる限り、可能性のある型を具体的に列挙していきたい。 -export type TraceItemVar = Record; +export type TraceItemVariable = Record; export const charToColor = { '#': 'black', @@ -51,8 +51,6 @@ export function traceProgram(instrumented: string, rawDisplayProgram: string, la throw new Error('Instrumented program MUST NOT contain variable declarations.'); } - const checkpointSids: number[] = []; - let statementId = 1; const modifiedCodeLines = []; for (const line of instrumented.split('\n')) { @@ -66,11 +64,7 @@ export function traceProgram(instrumented: string, rawDisplayProgram: string, la const delimiter = args === '' ? '' : ', '; return `.${methodName}(${args}${delimiter}${statementId})${tail}`; } - ) - .replace(/\/\/\s*CP.*/, () => { - checkpointSids.push(statementId); - return ''; - }); + ); if (replaced) statementId++; modifiedCodeLines.push(newLine); } @@ -170,7 +164,6 @@ s = new Scope(); ${modifiedCode.trim()} trace; `; - console.log(executableCode); // TODO: remove this later let trace = eval(executableCode) as TraceItem[]; if ((languageId as string) === 'python') { @@ -194,5 +187,5 @@ trace; refinedLines.push(refinedLine); } - return { languageId, displayProgram: refinedLines.join('\n'), traceItems: trace, sidToLineIndex, checkpointSids }; + return { languageId, displayProgram: refinedLines.join('\n'), traceItems: trace, sidToLineIndex }; } diff --git a/src/types.ts b/src/types.ts index f10515bb..0c911806 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,4 +14,4 @@ export type SelectedCell = { y: number; }; -export type ProblemType = 'executionResult' | 'checkpoint' | 'step'; +export type ProblemType = 'executionResult' | 'step'; diff --git a/src/utils/constants.ts b/src/utils/constants.ts deleted file mode 100644 index f8a07eba..00000000 --- a/src/utils/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const SUPERTOKENS_ACCESS_TOKEN_COOKIE_NAME = 'sAccessToken'; -export const SUPERTOKENS_REFRESH_TOKEN_COOKIE_NAME = 'sRefreshToken'; -export const SERVICE_ADMIN_ROLE = 'admin'; diff --git a/src/utils/sessionOnServer.ts b/src/utils/sessionOnServer.ts index 8e4f3337..8eea0b66 100644 --- a/src/utils/sessionOnServer.ts +++ b/src/utils/sessionOnServer.ts @@ -1,6 +1,7 @@ import { cookies } from 'next/headers.js'; -import { SUPERTOKENS_ACCESS_TOKEN_COOKIE_NAME } from './constants'; +import { SUPERTOKENS_ACCESS_TOKEN_COOKIE_NAME } from '../constants'; + import type { SessionOnNode } from './sessionOnNode'; import { getSessionOnNode } from './sessionOnNode'; diff --git a/tests/unit/traceCode.test.ts b/tests/unit/traceCode.test.ts index a30b39db..caaee3ea 100644 --- a/tests/unit/traceCode.test.ts +++ b/tests/unit/traceCode.test.ts @@ -39,7 +39,6 @@ public class Main { 3: 7, 4: 8, }, - expectedCheckpointSids: [3], expectedTrace: [ { sid: 0, vars: {}, board: defaultBoard }, { @@ -100,7 +99,6 @@ public class Main { 4: 8, 5: 9, }, - expectedCheckpointSids: [4], expectedTrace: [ { sid: 0, vars: {}, board: defaultBoard }, { @@ -223,7 +221,6 @@ public class Main { 4: 10, 5: 14, }, - expectedCheckpointSids: [], expectedTrace: [ { sid: 0, vars: {}, board: defaultBoard }, { sid: 1, vars: { a: 1 }, board: defaultBoard }, @@ -269,7 +266,6 @@ public class Main { 11: 14, 12: 15, }, - expectedCheckpointSids: [], expectedTrace: [ { sid: 0, vars: {}, board: defaultBoard }, { @@ -455,7 +451,6 @@ public class Straight { 7: 9, 8: 10, }, - expectedCheckpointSids: [2, 5, 6], expectedTrace: [ { sid: 0, vars: {}, board: defaultBoard }, { @@ -539,23 +534,15 @@ public class Straight { }, ] as const)( 'Trace a program', - ({ - expectedCheckpointSids, - expectedDisplayProgram, - expectedSidToLineIndex, - expectedTrace, - languageId, - problemId, - }) => { + ({ expectedDisplayProgram, expectedSidToLineIndex, expectedTrace, languageId, problemId }) => { const problem = generateProblem(problemId, languageId, ''); if (!problem) throw new Error('Failed to generate problem.'); - const { checkpointSids, displayProgram, sidToLineIndex, traceItems } = problem; + const { displayProgram, sidToLineIndex, traceItems } = problem; expect(displayProgram).toEqual(expectedDisplayProgram); expect(sidToLineIndex).toEqual( new Map(Object.entries(expectedSidToLineIndex).map(([sid, lineIndex]) => [Number(sid), lineIndex])) ); - expect(checkpointSids).toEqual(expectedCheckpointSids); expect(stringifyObjects(traceItems)).toEqual(stringifyObjects(expectedTrace)); } );