From 9781b56eb02c41637fcb23031fbfe050a44a2cd1 Mon Sep 17 00:00:00 2001 From: tatehito <48908346+Tatehito@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:53:18 +0900 Subject: [PATCH] feat: Add checkpoint problem (#30) --- .../[problemId]/CheckpointProblem.tsx | 99 +++++++++++++++++++ .../[problemId]/ExecutionResultProblem.tsx | 65 ++++++++++++ .../(withAuth)/problems/[problemId]/page.tsx | 65 ++++-------- src/app/lib/solveProblem.ts | 19 +++- .../organisms/SyntaxHighlighter.tsx | 20 ++-- src/components/organisms/TurtleGraphics.tsx | 51 +++++----- src/problems/problemData.ts | 10 +- src/types.ts | 2 + tests/solveProblem.test.ts | 30 ++++++ 9 files changed, 277 insertions(+), 84 deletions(-) create mode 100644 src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx create mode 100644 src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx diff --git a/src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx b/src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx new file mode 100644 index 00000000..5d953304 --- /dev/null +++ b/src/app/(withAuth)/problems/[problemId]/CheckpointProblem.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; +import { useEffect, useRef, useState } from 'react'; + +import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; +import { generateProgram } from '../../../../problems/problemData'; +import type { ProblemType } from '../../../../types'; +import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; + +interface CheckpointProblemProps { + problemId: string; + setStep: (step: ProblemType) => void; +} + +export const CheckpointProblem: React.FC = ({ problemId }) => { + const turtleGraphicsRef = useRef(null); + const [selectedLanguageId, setSelectedLanguageId] = useState(''); + + // TODO: チェックポイントを取得する処理が実装できたら置き換える + const getCheckPointLines = [2, 4]; + const [problemProgram, setProblemProgram] = useState(''); + const [beforeCheckPointLine, setBeforeCheckPointLine] = useState(1); + const [currentCheckPointLine, setCurrentCheckPointLine] = useState(getCheckPointLines[0]); + + useEffect(() => { + setSelectedLanguageId(getLanguageIdFromSessionStorage()); + }, []); + + useEffect(() => { + setProblemProgram(generateProgram(problemId, selectedLanguageId)); + }, [problemId, selectedLanguageId]); + + const handleClickResetButton = (): void => { + turtleGraphicsRef.current?.init(); + }; + + const handleClickAnswerButton = (): void => { + const isCorrect = turtleGraphicsRef.current?.isCorrect(); + + // TODO: 一旦アラートで表示 + if (isCorrect) { + alert('正解です'); + + if (currentCheckPointLine === getCheckPointLines.at(-1)) return; + + setBeforeCheckPointLine(currentCheckPointLine); + setCurrentCheckPointLine(getCheckPointLines[getCheckPointLines.indexOf(currentCheckPointLine) + 1]); + } else { + alert('不正解です'); + // setStep('step'); + } + }; + + return ( + + + 茶色にハイライトされている行における盤面を作成してください。 + 青色のハイライト時点の実行結果 + + + + 茶色のハイライト時点の実行結果 + + + + + + + + + + + + + + + + ); +}; diff --git a/src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx b/src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx new file mode 100644 index 00000000..d5634228 --- /dev/null +++ b/src/app/(withAuth)/problems/[problemId]/ExecutionResultProblem.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; +import { useEffect, useState, useRef } from 'react'; + +import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; +import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; +import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; +import { generateProgram } from '../../../../problems/problemData'; +import type { ProblemType } from '../../../../types'; +import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; + +interface ExecutionResultProblemProps { + problemId: string; + setStep: (step: ProblemType) => void; +} + +export const ExecutionResultProblem: React.FC = ({ problemId, setStep }) => { + const turtleGraphicsRef = useRef(null); + const [selectedLanguageId, setSelectedLanguageId] = useState(''); + + useEffect(() => { + setSelectedLanguageId(getLanguageIdFromSessionStorage()); + }, []); + + const problemProgram = generateProgram(problemId, selectedLanguageId); + + const handleClickResetButton = (): void => { + turtleGraphicsRef.current?.init(); + }; + + const handleClickAnswerButton = (): void => { + const isCorrect = turtleGraphicsRef.current?.isCorrect(); + + // TODO: 一旦アラートで表示 + if (isCorrect) { + alert('正解です'); + } else { + alert('不正解です'); + setStep('checkpoint'); + } + }; + + return ( + + + プログラムの実行後の結果を解答してください。 + + + + + + + {/* 画面に収まる高さに設定 */} + + + + + + + + + + ); +}; diff --git a/src/app/(withAuth)/problems/[problemId]/page.tsx b/src/app/(withAuth)/problems/[problemId]/page.tsx index a33c1a40..61e6ee5b 100644 --- a/src/app/(withAuth)/problems/[problemId]/page.tsx +++ b/src/app/(withAuth)/problems/[problemId]/page.tsx @@ -1,37 +1,26 @@ 'use client'; -import { Box, Button, Flex, HStack, Heading, VStack } from '@chakra-ui/react'; +import { Heading, VStack } from '@chakra-ui/react'; import type { NextPage } from 'next'; -import { useEffect, useState, useRef } from 'react'; +import { useState } from 'react'; -import { SyntaxHighlighter } from '../../../../components/organisms/SyntaxHighlighter'; -import type { TurtleGraphicsHandle } from '../../../../components/organisms/TurtleGraphics'; -import { TurtleGraphics } from '../../../../components/organisms/TurtleGraphics'; -import { programIdToName, generateProgram } from '../../../../problems/problemData'; -import { getLanguageIdFromSessionStorage } from '../../../lib/SessionStorage'; +import { programIdToName } from '../../../../problems/problemData'; +import type { ProblemType } from '../../../../types'; -const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) => { - const turtleGraphicsRef = useRef(null); - const [selectedLanguageId, setSelectedLanguageId] = useState(''); - - useEffect(() => { - setSelectedLanguageId(getLanguageIdFromSessionStorage()); - }, []); +import { CheckpointProblem } from './CheckpointProblem'; +import { ExecutionResultProblem } from './ExecutionResultProblem'; - const problemProgram = generateProgram(params.problemId, selectedLanguageId); - - const handleClickResetButton = (): void => { - turtleGraphicsRef.current?.reset(); - }; - - const handleClickAnswerButton = (): void => { - const isCorrect = turtleGraphicsRef.current?.isCorrect(); - - // TODO: 一旦アラートで表示 - if (isCorrect) { - alert('正解です'); - } else { - alert('不正解です'); +const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) => { + const [step, setStep] = useState('normal'); + + const ProblemComponent: React.FC = () => { + switch (step) { + case 'normal': { + return ; + } + case 'checkpoint': { + return ; + } } }; @@ -39,25 +28,7 @@ const ProblemPage: NextPage<{ params: { problemId: string } }> = ({ params }) =>
{programIdToName[params.problemId]} - - - プログラムの実行後の結果を解答してください。 - - - - - - - {/* 画面に収まる高さに設定 */} - - - - - - - - - +
); diff --git a/src/app/lib/solveProblem.ts b/src/app/lib/solveProblem.ts index 875cf46a..d0076a93 100644 --- a/src/app/lib/solveProblem.ts +++ b/src/app/lib/solveProblem.ts @@ -53,6 +53,14 @@ export function solveProblem(program: string): SolveProblemResult { const characters = executeEval(mergedCommand); + const board = new BoardClass(); + for (const history of histories) { + if (!history.characters) continue; + + for (const character of history.characters) { + board.updateGrid(character); + } + } for (const character of characters) { board.updateGrid(character); } @@ -72,14 +80,15 @@ export function solveProblem(program: string): SolveProblemResult { export function isAnswerCorrect( problemProgram: string, answerCharacters: CharacterClass[], - answerBoard: BoardClass + answerBoard: BoardClass, + step?: number ): boolean { - const answer = solveProblem(problemProgram); + const correctAnswer = solveProblem(problemProgram).histories?.at(step || -1); - if (!answer.characters || !answer.board) return false; + if (!correctAnswer || !correctAnswer.characters) return false; // 順番は関係なく、id以外のキャラクターの状態が一致しているかチェック - const isCorrectCharacters: boolean = answer.characters.every((correctCharacter) => { + const isCorrectCharacters: boolean = correctAnswer.characters.every((correctCharacter) => { const character = answerCharacters.find( (answerCharacter) => answerCharacter.name === correctCharacter.name && @@ -93,7 +102,7 @@ export function isAnswerCorrect( }); // すべてのセルの色が一致しているかチェック - const isCorrectBoard: boolean = answer.board.grid.every((rows, rowIndex) => + const isCorrectBoard: boolean = correctAnswer.board.grid.every((rows, rowIndex) => rows.every((column, columnIndex) => { const cell = answerBoard.grid[rowIndex][columnIndex]; return cell.color === column.color; diff --git a/src/components/organisms/SyntaxHighlighter.tsx b/src/components/organisms/SyntaxHighlighter.tsx index 1eba52e1..11b63e0a 100644 --- a/src/components/organisms/SyntaxHighlighter.tsx +++ b/src/components/organisms/SyntaxHighlighter.tsx @@ -4,12 +4,16 @@ import { Prism } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; interface SyntaxHighlighterProps { + beforeCheckPointLine?: number; code: string; programmingLanguageId: string; + currentCheckPointLine?: number; } export const SyntaxHighlighter: React.FC = ({ + beforeCheckPointLine: beforeCheckPointLine, code: code, + currentCheckPointLine: currentCheckPointLine, programmingLanguageId: programmingLanguageId, }) => { return ( @@ -25,15 +29,17 @@ export const SyntaxHighlighter: React.FC = ({ }} language={programmingLanguageId === 'c' ? 'cpp' : programmingLanguageId} lineNumberStyle={{ paddingRight: 0, marginRight: 16, minWidth: '1rem' }} - // lineProps={(lineNumber) => { - lineProps={() => { - const style = { + lineProps={(lineNumber) => { + const style: React.CSSProperties = { padding: 0, + backgroundColor: '', }; - // TODO: チェックポイント問題・ステップ問題のハイライト - // if (isStepPage && problem && lineNumber === problem.traceList[highlightedLineCount].row + 1) { - // style.backgroundColor = '#744210'; - // } + // チェックポイント問題・ステップ問題のハイライト + if (lineNumber === beforeCheckPointLine) { + style.backgroundColor = '#2E3D9F'; + } else if (lineNumber === currentCheckPointLine) { + style.backgroundColor = '#744210'; + } return { style }; }} showLineNumbers={true} diff --git a/src/components/organisms/TurtleGraphics.tsx b/src/components/organisms/TurtleGraphics.tsx index 0ac8f3c8..ecb979e7 100644 --- a/src/components/organisms/TurtleGraphics.tsx +++ b/src/components/organisms/TurtleGraphics.tsx @@ -2,11 +2,11 @@ import { Box, Grid, GridItem } from '@chakra-ui/react'; import Image from 'next/image'; -import React, { useState, forwardRef, useImperativeHandle, useMemo } from 'react'; +import React, { useState, forwardRef, useImperativeHandle, useEffect, useCallback } from 'react'; import { Board } from '../../app/lib/Board'; import { Character } from '../../app/lib/Character'; -import { isAnswerCorrect } from '../../app/lib/solveProblem'; +import { isAnswerCorrect, solveProblem } from '../../app/lib/solveProblem'; import type { CellColor, CharacterDirection, SelectedCell } from '../../types'; import { TurtleGraphicsController } from '../molecules/TurtleGraphicsController'; @@ -21,37 +21,45 @@ export const GRID_SIZE = 40; interface TurtleGraphicsProps { isEnableOperation?: boolean; problemProgram: string; + currentCheckPointLine?: number; + beforeCheckPointLine?: number; } export interface TurtleGraphicsHandle { - reset(): void; + init(): void; isCorrect(): boolean; } export const TurtleGraphics = forwardRef( - ({ isEnableOperation = false, problemProgram }, ref) => { + ({ beforeCheckPointLine = 0, currentCheckPointLine, isEnableOperation = false, problemProgram }, ref) => { const [board, setBoard] = useState(new Board()); + const [characters, setCharacters] = useState([]); const [selectedCharacter, setSelectedCharacter] = useState(); const [selectedCell, setSelectedCell] = useState(); const [dragging, setDragging] = useState(false); + const init = useCallback((): void => { + if (!problemProgram) return; + + const solveResult = solveProblem(problemProgram).histories?.at(beforeCheckPointLine); + const initBoard = solveResult?.board; + const initCharacters = solveResult?.characters; + + setBoard(initBoard || new Board()); + setCharacters(initCharacters || []); + setSelectedCharacter(undefined); + setSelectedCell(undefined); + }, [beforeCheckPointLine, problemProgram]); + useImperativeHandle(ref, () => ({ // 親コンポーネントから関数を呼び出せるようにする - reset, + init, isCorrect, })); - // // TODO: プログラムから盤面を生成する処理ができたら置き換える - const getInitialBoard = (): Board => { - return new Board(); - }; - - // TODO: プログラムから盤面を生成する処理ができたら置き換える - const getInitialCharacters = (): Character[] => { - return []; - }; - const getInitialCharactersResult = useMemo(() => getInitialCharacters(), []); - const [characters, setCharacters] = useState(getInitialCharactersResult); + useEffect(() => { + init(); + }, [beforeCheckPointLine, init, problemProgram]); const updateCharacter = (updater: (char: Character) => void): void => { if (!selectedCharacter) return; @@ -71,15 +79,8 @@ export const TurtleGraphics = forwardRef { - setBoard(getInitialBoard()); - setCharacters(getInitialCharacters()); - setSelectedCharacter(undefined); - setSelectedCell(undefined); - }; - const isCorrect = (): boolean => { - return isAnswerCorrect(problemProgram, characters, board); + return isAnswerCorrect(problemProgram, characters, board, currentCheckPointLine); }; const handleClickCharacter = (character: Character): void => { @@ -187,6 +188,8 @@ export const TurtleGraphics = forwardRef { + if (!isEnableOperation) return; + setSelectedCharacter(undefined); setSelectedCell({ x, y }); }; diff --git a/src/problems/problemData.ts b/src/problems/problemData.ts index f755cc16..4399b128 100644 --- a/src/problems/problemData.ts +++ b/src/problems/problemData.ts @@ -36,7 +36,15 @@ export const courseIdToProgramIdLists: Record = { export function generateProgram(programId: ProgramId, languageId: LanguageId): string { // TODO(exKAZUu): 問題IDに紐づくプログラム(テンプレート)を取得して、乱数を使って具体的なプログラムを生成する。 - return programIdToLanguageIdToProgram[programId][languageId]; + return ( + `const character1 = new Character(); +character1.moveForward(); +character1.moveForward(); +character1.moveForward(); +character1.moveForward(); +character1.moveForward(); +` || programIdToLanguageIdToProgram[programId][languageId] + ); } export function getExplanation(programId: ProgramId, languageId: LanguageId): string { diff --git a/src/types.ts b/src/types.ts index fed17fbd..20cdeedb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,3 +31,5 @@ export type SolveProblemResult = { }; export type CharacterDirection = 'up' | 'down' | 'left' | 'right'; + +export type ProblemType = 'normal' | 'checkpoint' | 'step'; diff --git a/tests/solveProblem.test.ts b/tests/solveProblem.test.ts index 9aff852d..d703ce95 100644 --- a/tests/solveProblem.test.ts +++ b/tests/solveProblem.test.ts @@ -118,9 +118,39 @@ test('Solve a problem (1character)', () => { [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], ]); + + expect(answer.histories?.at(0)?.characters?.length).toEqual(0); + + expect(answer.histories?.at(1)?.characters?.length).toEqual(1); + expect(answer.histories?.at(1)?.characters?.[0]?.x).toEqual(1); + expect(answer.histories?.at(1)?.characters?.[0]?.y).toEqual(1); + expect(answer.histories?.at(-1)?.characters?.length).toEqual(1); expect(answer.histories?.at(-1)?.characters?.[0]?.x).toEqual(2); expect(answer.histories?.at(-1)?.characters?.[0]?.y).toEqual(3); + + // prettier-ignore + expect(answer.histories?.at(0)?.board?.grid).toEqual([ + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined },], + ]); + // prettier-ignore + expect(answer.histories?.at(1)?.board?.grid).toEqual([ + [{ color: 'red' }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + [{ color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }, { color: undefined }], + ]); }); test('Solve a problem (multiple characters)', () => {