diff --git a/frontend/src/board/Board.tsx b/frontend/src/board/Board.tsx index d6010b95c..d3865baf6 100644 --- a/frontend/src/board/Board.tsx +++ b/frontend/src/board/Board.tsx @@ -176,7 +176,6 @@ interface BoardProps { const promotionPieces = ['q', 'n', 'r', 'b']; const Board: React.FC = ({ config, onInitialize, onMove }) => { - // const chess = useState(new Chess())[0]; const { chess } = useChess(); const [board, setBoard] = useState(null); const boardRef = useRef(null); diff --git a/frontend/src/board/pgn/PgnBoard.tsx b/frontend/src/board/pgn/PgnBoard.tsx index 75a1eed40..dc9225be1 100644 --- a/frontend/src/board/pgn/PgnBoard.tsx +++ b/frontend/src/board/pgn/PgnBoard.tsx @@ -46,6 +46,7 @@ export interface PgnBoardApi { interface PgnBoardProps { underboardTabs: UnderboardTab[]; + initialUnderboardTab?: string; pgn?: string; fen?: string; showPlayerHeaders?: boolean; @@ -57,6 +58,7 @@ const PgnBoard = forwardRef( ( { underboardTabs, + initialUnderboardTab, pgn, fen, showPlayerHeaders = true, @@ -132,7 +134,6 @@ const PgnBoard = forwardRef( () => { return { getPgn() { - console.log('History: ', chess.history()); return chess.renderPgn() || ''; }, }; @@ -160,6 +161,7 @@ const PgnBoard = forwardRef( = ({ underboardTabs, + initialUnderboardTab, showPlayerHeaders, pgn, fen, @@ -92,6 +94,7 @@ const ResizableContainer: React.FC = ({ diff --git a/frontend/src/board/pgn/boardTools/underboard/Underboard.tsx b/frontend/src/board/pgn/boardTools/underboard/Underboard.tsx index bac486c99..0096ef3d9 100644 --- a/frontend/src/board/pgn/boardTools/underboard/Underboard.tsx +++ b/frontend/src/board/pgn/boardTools/underboard/Underboard.tsx @@ -98,23 +98,26 @@ export interface UnderboardApi { interface UnderboardProps { tabs: UnderboardTab[]; + initialTab?: string; resizeData: ResizableData; onResize: (width: number, height: number) => void; } const Underboard = forwardRef( - ({ tabs, resizeData, onResize }, ref) => { + ({ tabs, initialTab, resizeData, onResize }, ref) => { const { chess } = useChess(); const { game, isOwner } = useGame(); const [focusEditor, setFocusEditor] = useState(false); const [focusCommenter, setFocusCommenter] = useState(false); const [underboard, setUnderboard] = useState( - isOwner - ? DefaultUnderboardTab.Editor - : Boolean(game) - ? DefaultUnderboardTab.Comments - : DefaultUnderboardTab.Explorer, + initialTab + ? initialTab + : isOwner + ? DefaultUnderboardTab.Editor + : Boolean(game) + ? DefaultUnderboardTab.Comments + : DefaultUnderboardTab.Explorer, ); const light = useLightMode(); @@ -151,6 +154,10 @@ const Underboard = forwardRef( return null; } + const customTab = tabs.find( + (t) => typeof t !== 'string' && t.name === underboard, + ) as CustomUnderboardTab; + return ( ( setFocusEditor={setFocusCommenter} /> )} + + {customTab && customTab.element} diff --git a/frontend/src/board/pgn/pgnText/MoveButton.tsx b/frontend/src/board/pgn/pgnText/MoveButton.tsx index 88eb26347..77f0cd822 100644 --- a/frontend/src/board/pgn/pgnText/MoveButton.tsx +++ b/frontend/src/board/pgn/pgnText/MoveButton.tsx @@ -17,7 +17,7 @@ import { } from '@mui/material'; import React, { forwardRef, useEffect, useRef, useState } from 'react'; -import { Cancel, CheckCircle, RemoveCircle } from '@mui/icons-material'; +import { RemoveCircle } from '@mui/icons-material'; import { useLocalStorage } from 'usehooks-ts'; import { useGame } from '../../../games/view/GamePage'; import { getMoveDescription } from '../../../tactics/tactics'; @@ -139,42 +139,41 @@ const Button = forwardRef((props, ref) => { )} - {(score !== undefined || found) && ( + {score > 0 && ( - {score ? ( - + - - {found ? '+' : '-'} - {score} - - - ) : found ? ( - - ) : ( - - )} + {found ? '+' : '-'} + {score} + + )} diff --git a/frontend/src/tactics/TacticsExamPage.tsx b/frontend/src/tactics/TacticsExamPage.tsx index ebf225f50..085df878f 100644 --- a/frontend/src/tactics/TacticsExamPage.tsx +++ b/frontend/src/tactics/TacticsExamPage.tsx @@ -1,82 +1,181 @@ -import { Button, Container, Stack } from '@mui/material'; -import React, { useRef, useState } from 'react'; -import { CountdownCircleTimer } from 'react-countdown-circle-timer'; +import { Quiz } from '@mui/icons-material'; +import { + Button, + Container, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { useCountdown } from 'react-countdown-circle-timer'; import { BoardApi, Chess } from '../board/Board'; -import { DefaultUnderboardTab } from '../board/pgn/boardTools/underboard/Underboard'; -import PgnBoard, { PgnBoardApi } from '../board/pgn/PgnBoard'; -import { addExtraVariation, getSolutionScore, scoreVariation } from './tactics'; - -const startingPositionFen = 'r1r3nk/p1q4p/4pppP/2B5/1pQ1P3/8/PPPR1PP1/2KR4 w q - 0 1'; - -const testPgn = `[Event "Tactics Test #1: From scratch #22"] -[Site "https://lichess.org/study/IIXmZsLb/wAhrGVAi"] -[Result "*"] -[Variant "Standard"] -[ECO "?"] -[Opening "?"] -[Annotator "https://lichess.org/@/jessekraai"] -[FEN "r1r3nk/p1q4p/4pppP/2B5/1pQ1P3/8/PPPR1PP1/2KR4 w q - 0 1"] -[SetUp "1"] -[UTCDate "2024.04.01"] -[UTCTime "15:08:39"] - -1. Bf8! Qxc4 2. Bg7# { [1] } * - - -`; - -const minuteSeconds = 60; -const hourSeconds = 3600; - -const getTimeSeconds = (time: number) => - `0${(time % hourSeconds) % minuteSeconds | 0}`.slice(-2); -const getTimeMinutes = (time: number) => ((time % hourSeconds) / minuteSeconds) | 0; +import { + DefaultUnderboardTab, + UnderboardTab, +} from '../board/pgn/boardTools/underboard/Underboard'; +import PgnBoard, { + BlockBoardKeyboardShortcuts, + PgnBoardApi, +} from '../board/pgn/PgnBoard'; +import { + addExtraVariation, + firstTest, + getSolutionScore, + scoreVariation, +} from './tactics'; +import TacticsExamPgnSelector from './TacticsExamPgnSelector'; + +export interface Scores { + total: { + user: number; + solution: number; + }; + problems: { + user: number; + solution: number; + }[]; +} const TacticsExamPage = () => { const pgnApi = useRef(null); - const [completedPgn, setCompletedPgn] = useState(''); - - const onFinish = () => { - console.log('Current PGN: ', pgnApi.current?.getPgn()); - setCompletedPgn(pgnApi.current?.getPgn() || ''); + const [selectedProblem, setSelectedProblem] = useState(0); + const answerPgns = useRef(firstTest.map(() => '')); + const [isTimeOver, setIsTimeOver] = useState(false); + const [isComplete, setIsComplete] = useState(false); + + const onCountdownComplete = useCallback(() => { + setIsTimeOver(true); + }, [setIsTimeOver]); + + const countdown = useCountdown({ + isPlaying: true, + size: 80, + strokeWidth: 6, + duration: 3600, + colors: ['#66bb6a', '#29b6f6', '#ce93d8', '#ffa726', '#f44336'], + colorsTime: [3600, 2700, 1800, 900, 0], + trailColor: 'rgba(0,0,0,0)', + onComplete: onCountdownComplete, + }); + + const scores: Scores | undefined = useMemo(() => { + if (!isComplete) { + return undefined; + } + + const scores: Scores = { + total: { user: 0, solution: 0 }, + problems: [], + }; + + for (let i = 0; i < firstTest.length; i++) { + const solutionChess = new Chess({ pgn: firstTest[i].solution }); + const userChess = new Chess({ pgn: answerPgns.current[i] }); + const solutionScore = getSolutionScore(solutionChess.history()); + const userScore = scoreVariation(solutionChess.history(), null, userChess); + + scores.total.solution += solutionScore; + scores.total.user += userScore; + scores.problems.push({ + user: userScore, + solution: solutionScore, + }); + } + + return scores; + }, [isComplete, answerPgns]); + + const onChangeProblem = (index: number) => { + if (!isComplete) { + answerPgns.current[selectedProblem] = pgnApi.current?.getPgn() || ''; + } + setSelectedProblem(index); }; - if (completedPgn) { - return ; + if (isComplete) { + return ( + + , + element: ( + + ), + }, + ]} + initialUnderboardTab='testInfo' + /> + + ); } + const onComplete = () => { + answerPgns.current[selectedProblem] = pgnApi.current?.getPgn() || ''; + setIsComplete(true); + setSelectedProblem(0); + }; + return ( - - - - {({ remainingTime, color }) => ( - -
- {getTimeMinutes(remainingTime)}: - {getTimeSeconds(remainingTime)} -
-
- )} -
- - -
+ , + element: ( + + ), + }, + DefaultUnderboardTab.Editor, + ]} + initialUnderboardTab='testInfo' /> + + + Test Complete + + + Your time has run out, and the test is over. Let's see how you + did! + + + + + + ); }; @@ -86,32 +185,28 @@ export default TacticsExamPage; interface CompletedTacticsTestProps { userPgn: string; solutionPgn: string; + underboardTabs: UnderboardTab[]; + initialUnderboardTab?: string; orientation?: 'white' | 'black'; } export const CompletedTacticsTest: React.FC = ({ userPgn, solutionPgn, + underboardTabs, + initialUnderboardTab, orientation, }) => { - const onInitialize = (_board: BoardApi, chess: Chess) => { - console.log('User PGN: ', userPgn); - console.log('Solution PGN: ', solutionPgn); - console.log('Solution history: ', chess.history()); - - const totalScore = getSolutionScore(chess.history()); - console.log('Total Score: ', totalScore); - - const answerChess = new Chess({ pgn: userPgn }); - answerChess.seek(null); - - console.log('Scoring answer'); - const answerScore = scoreVariation(chess.history(), null, answerChess); - console.log('Answer Score: ', answerScore); - console.log('Final Solution History: ', chess.history()); - - addExtraVariation(answerChess.history(), null, chess); - }; + const onInitialize = useCallback( + (_board: BoardApi, chess: Chess) => { + getSolutionScore(chess.history()); + const answerChess = new Chess({ pgn: userPgn }); + answerChess.seek(null); + scoreVariation(chess.history(), null, answerChess); + addExtraVariation(answerChess.history(), null, chess); + }, + [userPgn], + ); return ( = ({ pgn={solutionPgn} showPlayerHeaders={false} startOrientation={orientation} - underboardTabs={[]} + underboardTabs={underboardTabs} + initialUnderboardTab={initialUnderboardTab} /> ); }; diff --git a/frontend/src/tactics/TacticsExamPgnSelector.tsx b/frontend/src/tactics/TacticsExamPgnSelector.tsx index 3d03389f9..29458a058 100644 --- a/frontend/src/tactics/TacticsExamPgnSelector.tsx +++ b/frontend/src/tactics/TacticsExamPgnSelector.tsx @@ -1,40 +1,205 @@ -import { Button, CardContent, Stack } from '@mui/material'; -import { CountdownCircleTimer } from 'react-countdown-circle-timer'; +import { + Button, + CardContent, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + List, + ListItem, + ListItemButton, + ListItemIcon, + Stack, + Typography, +} from '@mui/material'; +import { useState } from 'react'; +import { ColorFormat } from 'react-countdown-circle-timer'; +import { BlockBoardKeyboardShortcuts } from '../board/pgn/PgnBoard'; +import { Scores } from './TacticsExamPage'; -const minuteSeconds = 60; -const hourSeconds = 3600; +interface TacticsExamPgnSelectorProps { + count: number; + selected: number; + onSelect: (v: number) => void; + countdown?: CountdownProps; + onComplete?: () => void; + scores?: Scores; +} -const getTimeSeconds = (time: number) => - `0${(time % hourSeconds) % minuteSeconds | 0}`.slice(-2); -const getTimeMinutes = (time: number) => ((time % hourSeconds) / minuteSeconds) | 0; +const TacticsExamPgnSelector: React.FC = ({ + count, + selected, + onSelect, + countdown, + onComplete, + scores, +}) => { + const [isFinishEarly, setIsFinishEarly] = useState(false); -const TacticsExamPgnSelector = () => { return ( - - - {({ remainingTime, color }) => ( - -
- {getTimeMinutes(remainingTime)}: - {getTimeSeconds(remainingTime)} -
-
- )} -
- - + + {countdown ? ( + <> + + + + ) : ( + + Test Complete + + Total Score: {scores?.total.user} / {scores?.total.solution} + + + )} + + + {Array.from(Array(count)).map((_, i) => ( + + onSelect(i)} + > + + + + {i + 1} + + + + + Problem {i + 1} + + {scores && ( + + {scores.problems[i].user} /{' '} + {scores.problems[i].solution} + + )} + + + + ))} + + + setIsFinishEarly(false)} + classes={{ + container: BlockBoardKeyboardShortcuts, + }} + fullWidth + > + Finish Early? + + + Are you sure you want to finish the test early? You will not be + able to change your answers if you continue. + + + + + + +
); }; export default TacticsExamPgnSelector; + +const minuteSeconds = 60; +const hourSeconds = 3600; + +const getTimeSeconds = (time: number) => + `0${(time % hourSeconds) % minuteSeconds | 0}`.slice(-2); +const getTimeMinutes = (time: number) => ((time % hourSeconds) / minuteSeconds) | 0; + +interface CountdownProps { + elapsedTime: number; + path: string; + pathLength: number; + remainingTime: number; + rotation: 'clockwise' | 'counterclockwise'; + size: number; + stroke: ColorFormat; + strokeDashoffset: number; + strokeWidth: number; +} + +const CountdownTimer = (props: CountdownProps) => { + const { + path, + pathLength, + stroke, + strokeDashoffset, + remainingTime, + size, + strokeWidth, + } = props; + return ( +
+ + + + +
+ +
+ {getTimeMinutes(remainingTime)}:{getTimeSeconds(remainingTime)} +
+
+
+
+ ); +}; diff --git a/frontend/src/tactics/TacticsInstructionsPage.tsx b/frontend/src/tactics/TacticsInstructionsPage.tsx index bb54318c9..502752a9b 100644 --- a/frontend/src/tactics/TacticsInstructionsPage.tsx +++ b/frontend/src/tactics/TacticsInstructionsPage.tsx @@ -1,7 +1,6 @@ import { Button, Container, Stack, Typography } from '@mui/material'; import { useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { DefaultUnderboardTab } from '../board/pgn/boardTools/underboard/Underboard'; import PgnBoard, { PgnBoardApi } from '../board/pgn/PgnBoard'; import { sampleProblem } from './tactics'; import { CompletedTacticsTest } from './TacticsExamPage'; @@ -97,6 +96,7 @@ const TacticsInstructionsPage = () => { userPgn={completedPgn} solutionPgn={sampleProblem.solution} orientation={sampleProblem.orientation} + underboardTabs={[]} /> ) : ( { fen={sampleProblem.fen} showPlayerHeaders={false} startOrientation={sampleProblem.orientation} - underboardTabs={[DefaultUnderboardTab.Editor]} + underboardTabs={[]} /> )} diff --git a/frontend/src/tactics/tactics.ts b/frontend/src/tactics/tactics.ts index 8537ffabd..ec96fe997 100644 --- a/frontend/src/tactics/tactics.ts +++ b/frontend/src/tactics/tactics.ts @@ -31,7 +31,7 @@ export const firstTest: TacticsProblem[] = [ fen: 'r4rk1/pp3pp1/4pq1B/2N4R/6Q1/P7/1P3PPP/n5K1 w q - 0 1', solution: `[FEN "r4rk1/pp3pp1/4pq1B/2N4R/6Q1/P7/1P3PPP/n5K1 w q - 0 1"] [SetUp "1"] - + 1. Bxg7! { [1] } 1... Qg6 (1... Qxg7 2. Rg5 Qxg5 3. Qxg5+ Kh7 4. Nd7) 2. Bf6! { [1] The key move to see. Black is getting mated } *`, }, {