diff --git a/frontend/src/api/Game.ts b/frontend/src/api/Game.ts index d7fbadb8..96aa5ea4 100644 --- a/frontend/src/api/Game.ts +++ b/frontend/src/api/Game.ts @@ -8,10 +8,8 @@ import { Color } from './Color'; export type Player = { user: User, - code: string, - language: string, submissions: Submission[], - solved: boolean, + solved: boolean[], color: Color, }; @@ -41,12 +39,14 @@ export type RunSolutionParams = { initiator: User, input: string, code: string, + problemIndex: number, language: string, }; export type SubmitSolutionParams = { initiator: User, code: string, + problemIndex: number, language: string, }; @@ -68,6 +68,7 @@ export enum SubmissionType { export type Submission = { code: string, + problemIndex: number, language: string, results: SubmissionResult[], numCorrect: number, diff --git a/frontend/src/api/Room.ts b/frontend/src/api/Room.ts index 2c7c6633..065f2801 100644 --- a/frontend/src/api/Room.ts +++ b/frontend/src/api/Room.ts @@ -34,6 +34,7 @@ export type UpdateSettingsParams = { duration?: number, problems?: ProblemIdParam[], size?: number, + numProblems?: number, }; export type ChangeHostParams = { diff --git a/frontend/src/components/card/LeaderboardCard.tsx b/frontend/src/components/card/LeaderboardCard.tsx index da0ed314..bbd93666 100644 --- a/frontend/src/components/card/LeaderboardCard.tsx +++ b/frontend/src/components/card/LeaderboardCard.tsx @@ -4,7 +4,7 @@ import { Player } from '../../api/Game'; import { LowMarginText, SmallText } from '../core/Text'; import PlayerIcon from './PlayerIcon'; import { Color } from '../../api/Color'; -import { useBestSubmission } from '../../util/Hook'; +import { useGetScore, useGetSubmissionTime } from '../../util/Hook'; type ContentStyleType = { isCurrentPlayer: boolean, @@ -67,37 +67,39 @@ type LeaderboardCardProps = { isCurrentPlayer: boolean, place: number, color: Color, + numProblems: number, }; function LeaderboardCard(props: LeaderboardCardProps) { const { - place, player, isCurrentPlayer, color, + place, player, isCurrentPlayer, color, numProblems, } = props; const [showHover, setShowHover] = useState(false); - const bestSubmission = useBestSubmission(player); + const score = useGetScore(player); + const time = useGetSubmissionTime(player); const getScoreDisplay = () => { - if (!bestSubmission) { - return '0'; + if (!score) { + return 0; } - return `${bestSubmission.numCorrect}/${bestSubmission.numTestCases}`; + return score; }; const getScorePercentage = () => { - if (!bestSubmission) { + if (!score) { return ''; } - return ` ${Math.round((bestSubmission.numCorrect / bestSubmission.numTestCases) * 100)}%`; + return ` ${Math.round((score / numProblems) * 100)}%`; }; const getSubmissionTime = () => { - if (!bestSubmission) { + if (!time) { return 'Never'; } const currentTime = new Date().getTime(); - const diffMilliseconds = currentTime - new Date(bestSubmission.startTime).getTime(); + const diffMilliseconds = currentTime - new Date(time).getTime(); const diffMinutes = Math.floor(diffMilliseconds / (60 * 1000)); return `${diffMinutes}m ago`; }; @@ -113,7 +115,7 @@ function LeaderboardCard(props: LeaderboardCardProps) { nickname={player.user.nickname} active={Boolean(player.user.sessionId)} /> - {`${place}.${getScorePercentage()}`} + !element).length === 0}>{`${place}.${getScorePercentage()}`} {showHover ? ( diff --git a/frontend/src/components/game/Console.tsx b/frontend/src/components/game/Console.tsx index 5358460a..5195658d 100644 --- a/frontend/src/components/game/Console.tsx +++ b/frontend/src/components/game/Console.tsx @@ -130,7 +130,7 @@ function Console(props: ConsoleProps) { // Default values setTitle('Console'); setSubtitle(''); - setInput(testCases[0].input); + setInput(testCases ? testCases[0].input : ''); setOutput(''); setConsoleOutput(''); } diff --git a/frontend/src/components/game/Editor.tsx b/frontend/src/components/game/Editor.tsx index 2be616f2..9409d625 100644 --- a/frontend/src/components/game/Editor.tsx +++ b/frontend/src/components/game/Editor.tsx @@ -5,12 +5,21 @@ import styled, { ThemeContext } from 'styled-components'; import Language, { fromString, languageToEditorLanguage } from '../../api/Language'; import { DefaultCodeType } from '../../api/Problem'; +/** + * onLanguageChange - a callback, called when the currentLanguage changes + * onCodeChange - a callback, called when the code changes + * getCurrentLanguage - a function passed in, which can be called to get the current language + * defaultCodeMap - a map of all the default code for each problem and language + * currentProblem - the problem which is current being worked on + * liveCode - a string used for spectator view + */ type EditorProps = { onLanguageChange: ((language: Language) => void) | null, onCodeChange: ((code: string) => void) | null, - codeMap: DefaultCodeType | null, - defaultLanguage: Language, + getCurrentLanguage: (() => Language) | null, + defaultCodeMap: DefaultCodeType[] | null, defaultCode: string | null, + currentProblem: number, liveCode: string | null, }; @@ -89,17 +98,19 @@ const monacoEditorOptions: EditorConstructionOptions = { // This function refreshes the width of Monaco editor upon change in container size function ResizableMonacoEditor(props: EditorProps) { const { - onLanguageChange, onCodeChange, codeMap, defaultLanguage, - defaultCode, liveCode, + onLanguageChange, onCodeChange, getCurrentLanguage, + defaultCodeMap, defaultCode, currentProblem, liveCode, } = props; const theme = useContext(ThemeContext); - const [currentLanguage, setCurrentLanguage] = useState(defaultLanguage); const [codeEditor, setCodeEditor] = useState(null); + const [codeMap, setCodeMap] = useState(defaultCodeMap); + const [previousProblem, setPreviousProblem] = useState(0); + const [currentLanguage, setCurrentLanguage] = useState(Language.Java); useEffect(() => { - setCurrentLanguage(defaultLanguage); - }, [defaultLanguage]); + setCodeMap(defaultCodeMap); + }, [defaultCodeMap]); const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => { setCodeEditor(editor); @@ -126,22 +137,45 @@ function ResizableMonacoEditor(props: EditorProps) { const handleLanguageChange = (language: Language) => { // Save the code for this language if (codeMap != null && codeEditor != null) { - codeMap[currentLanguage] = codeEditor.getValue(); - codeEditor.setValue(codeMap[language]); + const codeMapTemp = codeMap; + codeMapTemp[currentProblem][currentLanguage] = codeEditor.getValue(); + setCodeMap(codeMapTemp); + codeEditor.setValue(codeMap[currentProblem][language]); } - // Change the language and initial code for the editor - setCurrentLanguage(language); + // Call onLanguageChange if (onLanguageChange) { onLanguageChange(language); } }; + // This hook will be called if the user switches the problem he or she is working on useEffect(() => { if (codeMap != null && codeEditor != null) { - codeEditor.setValue(codeMap[currentLanguage]); + if (codeMap[currentProblem] != null) { + const codeMapTemp = codeMap; + + // If the value of the editor is not "Loading...", save it in the codeMap + if (codeEditor.getValue() !== 'Loading...') { + codeMapTemp[previousProblem][currentLanguage] = codeEditor.getValue(); + } + + setCodeMap(codeMapTemp); + setPreviousProblem(currentProblem); + + // If getCurrentLanguage is defined, set currentLanguage to the returned value, + // which is the language that was last used for the new problem, + // and set the CodeEditor to be the code for the problem and language + if (getCurrentLanguage) { + setCurrentLanguage(getCurrentLanguage()); + codeEditor.setValue(codeMap[currentProblem][getCurrentLanguage()]); + } else { + codeEditor.setValue(codeMap[currentProblem][currentLanguage]); + } + } } - }, [currentLanguage, codeMap, codeEditor, setCodeEditor]); + }, [currentLanguage, codeMap, codeEditor, setCodeEditor, getCurrentLanguage, + currentProblem, previousProblem]); return ( diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index 46251a72..3a93948b 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -33,14 +33,16 @@ import { Player, } from '../../api/Game'; import LeaderboardCard from '../card/LeaderboardCard'; -import { getDifficultyDisplayButton, InheritedTextButton } from '../core/Button'; +import { getDifficultyDisplayButton, InheritedTextButton, SmallButton } from '../core/Button'; import { SpectatorBackIcon } from '../core/Icon'; import Language from '../../api/Language'; import { CopyIndicator, BottomCopyIndicatorContainer, InlineCopyIcon } from '../special/CopyIndicator'; -import { useAppSelector, useBestSubmission } from '../../util/Hook'; +import { useAppSelector, useBestSubmission, useGetSubmission } from '../../util/Hook'; import { routes, send, subscribe } from '../../api/Socket'; import { User } from '../../api/User'; -import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; +import { + getScore, getSubmissionCount, getSubmissionTime, getSubmission, +} from '../../util/Utility'; const StyledMarkdownEditor = styled(MarkdownEditor)` margin-top: 15px; @@ -135,6 +137,7 @@ type StateRefType = { currentUser: User | null, currentCode: string, currentLanguage: string, + currentProblemIndex: number, } /** @@ -158,13 +161,17 @@ function PlayerGameView(props: PlayerGameViewProps) { const { currentUser, game } = useAppSelector((state) => state); const [copiedEmail, setCopiedEmail] = useState(false); - const [submission, setSubmission] = useState(null); + const [submissions, setSubmissions] = useState([]); + + const [problems, setProblems] = useState([]); + const [languageList, setLanguageList] = useState([Language.Java]); + const [codeList, setCodeList] = useState(['']); + const [currentProblemIndex, setCurrentProblemIndex] = useState(0); + let currentSubmission = useGetSubmission(currentProblemIndex, submissions); const [loading, setLoading] = useState(false); const [error, setError] = useState(gameError); - const [currentLanguage, setCurrentLanguage] = useState(Language.Java); - const [currentCode, setCurrentCode] = useState(''); const [defaultCodeList, setDefaultCodeList] = useState([]); // Variable to hold whether the user is subscribed to their own player socket. @@ -174,6 +181,8 @@ function PlayerGameView(props: PlayerGameViewProps) { const [spectatedPlayer, setSpectatedPlayer] = useState(null); const bestSubmission = useBestSubmission(spectatedPlayer); + useEffect(() => setProblems(game?.problems || []), [game]); + /** * Display beforeUnload message to inform the user that they may lose * their code / data if they leave the page. @@ -182,17 +191,41 @@ function PlayerGameView(props: PlayerGameViewProps) { */ useBeforeunload(() => 'Leaving this page may cause you to lose your current code and data.'); + const getCurrentLanguage = useCallback(() => languageList[currentProblemIndex], + [languageList, currentProblemIndex]); + + const setOneCurrentLanguage = (newLanguage: Language) => { + setLanguageList(languageList.map((current, index) => { + if (index === currentProblemIndex) { + return newLanguage; + } + return current; + })); + }; + + const setOneCurrentCode = (newCode: string) => { + setCodeList(codeList.map((current, index) => { + if (index === currentProblemIndex) { + return newCode; + } + return current; + })); + }; + // References necessary for the spectator subscription callback. const stateRef = useRef(); stateRef.current = { game, currentUser, - currentCode, - currentLanguage, + currentCode: codeList[currentProblemIndex], + currentLanguage: languageList[currentProblemIndex], + currentProblemIndex, }; const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[], - code: string, language: Language) => { + playerSubmissions: Submission[]) => { + setSubmissions(playerSubmissions); + const promises: Promise[] = []; problemsParam.forEach((problem) => { if (problem && problem.problemId) { @@ -202,31 +235,46 @@ function PlayerGameView(props: PlayerGameViewProps) { // Get the result of promises and set the default code list. Promise.all(promises).then((result) => { - const codeMap = result[0]; - - // If previous code and language specified, save those as defaults - if (code) { - codeMap[language] = code; + const newCodeList = []; + const newLanguageList = []; + const codeMap = result; + + // Save the default code in these temporary lists + for (let i = 0; i < result.length; i += 1) { + newCodeList.push(result[i][Language.Java]); + newLanguageList.push(Language.Java); } - // Set this user's current code and language - setCurrentCode(codeMap[language]); - setCurrentLanguage(language); + // If previous code and language specified, override the defaults + for (let i = 0; i < result.length; i += 1) { + const temp = getSubmission(i, playerSubmissions); - setDefaultCodeList(result); + if (temp) { + newCodeList[i] = temp.code; + codeMap[i][temp.language as Language] = temp.code; + newLanguageList[i] = temp.language as Language; + setCurrentProblemIndex(i); + } + } + + // Set this user's current code + setCodeList(newCodeList); + setLanguageList(newLanguageList); + setDefaultCodeList(codeMap); }).catch((err) => { setError(err.message); }); - }, [setDefaultCodeList, setCurrentCode, setCurrentLanguage]); + }, [setDefaultCodeList, setCodeList, setLanguageList]); const sendViewUpdate = useCallback((gameParam: Game | null | undefined, currentUserParam: User | null | undefined, currentCodeParam: string | undefined, - currentLanguageParam: string | undefined) => { + currentLanguageParam: string | undefined, + currentIndexParam: number | undefined) => { if (gameParam && currentUserParam) { const spectatorViewBody: string = JSON.stringify({ user: currentUserParam, - problem: gameParam.problems[0], + problem: gameParam.problems[currentIndexParam || 0], // must satisfy problems.length > 0 code: currentCodeParam, language: currentLanguageParam, }); @@ -240,8 +288,9 @@ function PlayerGameView(props: PlayerGameViewProps) { // Send updates via socket to any spectators. useEffect(() => { - sendViewUpdate(game, currentUser, currentCode, currentLanguage); - }, [game, currentUser, currentCode, currentLanguage, sendViewUpdate]); + sendViewUpdate(game, currentUser, codeList[currentProblemIndex], + languageList[currentProblemIndex], currentProblemIndex); + }, [game, currentUser, codeList, languageList, currentProblemIndex, sendViewUpdate]); // Re-subscribe in order to get the correct subscription callback. const subscribePlayer = useCallback((roomIdParam: string, userIdParam: string) => { @@ -249,7 +298,8 @@ function PlayerGameView(props: PlayerGameViewProps) { const subscribePlayerCallback = (result: Message) => { if (JSON.parse(result.body).newSpectator) { sendViewUpdate(stateRef.current?.game, stateRef.current?.currentUser, - stateRef.current?.currentCode, stateRef.current?.currentLanguage); + stateRef.current?.currentCode, stateRef.current?.currentLanguage, + stateRef.current?.currentProblemIndex); } }; @@ -293,15 +343,15 @@ function PlayerGameView(props: PlayerGameViewProps) { // If this user refreshed and has already submitted code, load and save their latest code game.players.forEach((player) => { - if (player.user.userId === currentUser?.userId && player.code) { - setDefaultCodeFromProblems(game.problems, player.code, player.language as Language); + if (player.user.userId === currentUser?.userId && player.submissions) { + setDefaultCodeFromProblems(game.problems, player.submissions); matchFound = true; } }); // If no previous code, proceed as normal with the default Java language if (!matchFound) { - setDefaultCodeFromProblems(game.problems, '', Language.Java); + setDefaultCodeFromProblems(game.problems, []); } } } @@ -321,8 +371,9 @@ function PlayerGameView(props: PlayerGameViewProps) { const request = { initiator: currentUser!, input, - code: currentCode, - language: currentLanguage, + code: codeList[currentProblemIndex], + language: languageList[currentProblemIndex], + problemIndex: currentProblemIndex, }; runSolution(game!.room.roomId, request) @@ -332,7 +383,7 @@ function PlayerGameView(props: PlayerGameViewProps) { // Set the 'test' submission type to correctly display result. // eslint-disable-next-line no-param-reassign res.submissionType = SubmissionType.Test; - setSubmission(res); + currentSubmission = res; }) .catch((err) => { setLoading(false); @@ -346,8 +397,9 @@ function PlayerGameView(props: PlayerGameViewProps) { setError(''); const request = { initiator: currentUser!, - code: currentCode, - language: currentLanguage, + code: codeList[currentProblemIndex], + language: languageList[currentProblemIndex], + problemIndex: currentProblemIndex, }; submitSolution(game!.room.roomId, request) @@ -357,7 +409,7 @@ function PlayerGameView(props: PlayerGameViewProps) { // Set the 'submit' submission type to correctly display result. // eslint-disable-next-line no-param-reassign res.submissionType = SubmissionType.Submit; - setSubmission(res); + setSubmissions(submissions.concat([res])); }) .catch((err) => { setLoading(false); @@ -365,14 +417,29 @@ function PlayerGameView(props: PlayerGameViewProps) { }); }; + const nextProblem = () => { + setCurrentProblemIndex((currentProblemIndex + 1) % problems?.length); + }; + + const previousProblem = () => { + let temp = currentProblemIndex - 1; + + if (temp < 0) { + temp += problems?.length; + } + + setCurrentProblemIndex(temp); + }; + const displayPlayerLeaderboard = useCallback(() => game?.players.map((player, index) => ( - )), [game, currentUser]); + )), [game, currentUser, problems.length]); return ( <> @@ -433,12 +500,13 @@ function PlayerGameView(props: PlayerGameViewProps) { {/* Problem title/description panel */} - {!spectateGame ? game?.problems[0]?.name : spectateGame?.problem.name} + {!spectateGame + ? game?.problems[currentProblemIndex]?.name + : spectateGame?.problem.name} - {/* TODO: I don't know whether we have to verify that the problem exists. */} { !spectateGame ? ( - getDifficultyDisplayButton(game?.problems[0].difficulty!) + getDifficultyDisplayButton(game?.problems[currentProblemIndex].difficulty!) ) : ( getDifficultyDisplayButton(spectateGame?.problem.difficulty!) ) @@ -449,7 +517,9 @@ function PlayerGameView(props: PlayerGameViewProps) { ) : ( spectateGame?.problem.description )} - value={spectateGame ? spectateGame?.problem.description : undefined} + value={spectateGame + ? spectateGame?.problem.description + : problems[currentProblemIndex]?.description} onChange={() => ''} readOnly /> @@ -478,18 +548,19 @@ function PlayerGameView(props: PlayerGameViewProps) { > @@ -500,8 +571,9 @@ function PlayerGameView(props: PlayerGameViewProps) { spectateGame?.language as Language || Language.Java} + defaultCodeMap={null} + currentProblem={currentProblemIndex} defaultCode={spectateGame?.code} liveCode={spectateGame?.code} /> @@ -510,6 +582,10 @@ function PlayerGameView(props: PlayerGameViewProps) { } + + Previous + Next + setCopiedEmail(false)}> Email copied!  ✕ diff --git a/frontend/src/components/game/SpectatorGameView.tsx b/frontend/src/components/game/SpectatorGameView.tsx index 1b65c695..7e3f8f7c 100644 --- a/frontend/src/components/game/SpectatorGameView.tsx +++ b/frontend/src/components/game/SpectatorGameView.tsx @@ -83,6 +83,7 @@ function SpectatorGameView() { subscribePlayer(game.room.roomId, game.players[index].user.userId!); } }} + numProblems={game?.problems.length || 1} /> {error ? : null} {loading ? : null} diff --git a/frontend/src/components/results/PlayerResultsItem.tsx b/frontend/src/components/results/PlayerResultsItem.tsx index e5aeab21..462696f7 100644 --- a/frontend/src/components/results/PlayerResultsItem.tsx +++ b/frontend/src/components/results/PlayerResultsItem.tsx @@ -1,12 +1,11 @@ import React from 'react'; import styled from 'styled-components'; +import { Player, Submission } from '../../api/Game'; import { LowMarginText, Text } from '../core/Text'; import { Color } from '../../api/Color'; -import { Player } from '../../api/Game'; -import { useBestSubmission } from '../../util/Hook'; +import { useBestSubmission, useGetScore, useGetSubmissionTime } from '../../util/Hook'; import Language, { displayNameFromLanguage } from '../../api/Language'; import { TextButton } from '../core/Button'; -import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; const Content = styled.tr` border-radius: 5px; @@ -83,23 +82,38 @@ type PlayerResultsCardProps = { isCurrentPlayer: boolean, gameStartTime: string, color: Color, + numProblems: number, onViewCode: (() => void) | null, onSpectateLive: (() => void) | null, }; function PlayerResultsItem(props: PlayerResultsCardProps) { const { - player, place, isCurrentPlayer, color, gameStartTime, onViewCode, - onSpectateLive, + player, place, isCurrentPlayer, color, onViewCode, onSpectateLive, } = props; - const bestSubmission = useBestSubmission(player); + const score = useGetScore(player); + const time = useGetSubmissionTime(player); + const bestSubmission : Submission | null = useBestSubmission(player); + + const getSubmissionCount = () => player.submissions.length || '0'; const getDisplayNickname = () => { const { nickname } = player.user; return `${nickname} ${isCurrentPlayer ? '(you)' : ''}`; }; + const getSubmissionTime = () => { + if (!time) { + return 'Never'; + } + + const currentTime = new Date().getTime(); + const diffMilliseconds = currentTime - new Date(time).getTime(); + const diffMinutes = Math.floor(diffMilliseconds / (60 * 1000)); + return `${diffMinutes}m ago`; + }; + const getSubmissionLanguage = () => { if (!bestSubmission) { return 'N/A'; @@ -129,13 +143,13 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { - {getScore(bestSubmission)} + {score} - {getSubmissionTime(bestSubmission, gameStartTime)} + {getSubmissionTime()} - {getSubmissionCount(player)} + {getSubmissionCount()} {!onSpectateLive ? ( {getSubmissionLanguage()} diff --git a/frontend/src/components/results/Podium.tsx b/frontend/src/components/results/Podium.tsx index e11a55b6..3aab8cdc 100644 --- a/frontend/src/components/results/Podium.tsx +++ b/frontend/src/components/results/Podium.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { Player } from '../../api/Game'; import { Text, MediumText } from '../core/Text'; import Language, { displayNameFromLanguage } from '../../api/Language'; -import { useBestSubmission } from '../../util/Hook'; +import { useBestSubmission, useGetScore, useGetSubmissionTime } from '../../util/Hook'; type PodiumProps = { place: number, @@ -12,6 +12,7 @@ type PodiumProps = { inviteContent: React.ReactNode, loading: boolean, isCurrentPlayer: boolean, + numProblems: number, }; type MedalProps = { @@ -85,10 +86,12 @@ const Medal = styled.div` function Podium(props: PodiumProps) { const { - place, player, gameStartTime, loading, inviteContent, isCurrentPlayer, + place, player, gameStartTime, loading, inviteContent, isCurrentPlayer, numProblems, } = props; + const score = useGetScore(player); const bestSubmission = useBestSubmission(player); + const time = useGetSubmissionTime(player); const getMedalColor = () => { switch (place) { @@ -120,7 +123,7 @@ function Podium(props: PodiumProps) { return inviteContent; } - if (!bestSubmission) { + if (!score) { return ( Scored @@ -130,7 +133,7 @@ function Podium(props: PodiumProps) { ); } - const percent = Math.round((bestSubmission.numCorrect / bestSubmission.numTestCases) * 100); + const percent = Math.round((score / numProblems) * 100); return ( Scored @@ -140,13 +143,13 @@ function Podium(props: PodiumProps) { }; const getTimeText = () => { - if (!bestSubmission) { + if (!time) { return ; } // Calculate time from start of game till best submission const startTime = new Date(gameStartTime).getTime(); - const diffMilliseconds = new Date(bestSubmission.startTime).getTime() - startTime; + const diffMilliseconds = new Date(time).getTime() - startTime; const diffMinutes = Math.floor(diffMilliseconds / (60 * 1000)); return ( diff --git a/frontend/src/components/results/PreviewCodeContent.tsx b/frontend/src/components/results/PreviewCodeContent.tsx index 4cbbb94d..fb614873 100644 --- a/frontend/src/components/results/PreviewCodeContent.tsx +++ b/frontend/src/components/results/PreviewCodeContent.tsx @@ -45,8 +45,9 @@ function PreviewCodeContent(props: PreviewCodeContentProps) { bestSubmission?.language as Language || Language.Java} + defaultCodeMap={null} + currentProblem={0} defaultCode={bestSubmission?.code || 'Uh oh! An error occurred fetching this player\'s code'} liveCode={null} /> diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index 1421982c..71413150 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -37,13 +37,14 @@ type ResultsTableProps = { players: Player[], currentUser: User | null, gameStartTime: string, + numProblems: number, viewPlayerCode: ((index: number) => void) | null, spectatePlayer: ((index: number) => void) | null, }; function ResultsTable(props: ResultsTableProps) { const { - players, currentUser, gameStartTime, viewPlayerCode, spectatePlayer, + players, currentUser, gameStartTime, numProblems, viewPlayerCode, spectatePlayer, } = props; return ( @@ -64,6 +65,7 @@ function ResultsTable(props: ResultsTableProps) { isCurrentPlayer={currentUser?.userId === player.user.userId} gameStartTime={gameStartTime} color={player.color} + numProblems={numProblems} onViewCode={viewPlayerCode ? (() => viewPlayerCode(index)) : null} onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} /> diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index 3dc5c3fa..919eb0e7 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -30,6 +30,68 @@ export const useBestSubmission = (player?: Player | null) => { return bestSubmission; }; +export const useGetScore = (player?: Player) => { + const counted = new Set(); + const [score, setScore] = useState(0); + + useEffect(() => { + if (player) { + for (let i = 0; i < player.submissions.length; i += 1) { + if (player.submissions[i].numCorrect === player.submissions[i].numTestCases + && !counted.has(player.submissions[i].problemIndex)) { + counted.add(player.submissions[i].problemIndex); + } + } + + setScore(counted.size); + } + }, [player, setScore, counted]); + + if (player == null || player.submissions.length === 0) { + return null; + } + return score; +}; + +export const useGetSubmissionTime = (player?: Player) => { + const counted = new Set(); + const [time, setTime] = useState(); + + useEffect(() => { + if (player) { + for (let i = 0; i < player.submissions.length; i += 1) { + if (player.submissions[i].numCorrect === player.submissions[i].numTestCases + && !counted.has(player.submissions[i].problemIndex)) { + counted.add(player.submissions[i].problemIndex); + setTime(player.submissions[i].startTime); + } + } + } + }, [player, counted]); + + if (!time && player && player.submissions.length > 0) { + setTime(player.submissions[player.submissions.length - 1].startTime); + } + + return time; +}; + +// Returns the most recent submission made for problem of index curr. +export const useGetSubmission = (curr: number, playerSubmissions: Submission[]) => { + const [submission, setSubmission] = useState(null); + + useEffect(() => { + for (let i = playerSubmissions.length - 1; i >= 0; i -= 1) { + if (playerSubmissions[i].problemIndex === curr) { + setSubmission(playerSubmissions[i]); + i = -1; + } + } + }, [curr, playerSubmissions]); + + return submission; +}; + export const useProblemEditable = (user: FirebaseUserType | null, problem: Problem | null) => { const [editable, setEditable] = useState(false); diff --git a/frontend/src/util/Utility.ts b/frontend/src/util/Utility.ts index b87c0245..96b1277e 100644 --- a/frontend/src/util/Utility.ts +++ b/frontend/src/util/Utility.ts @@ -143,6 +143,14 @@ export const getSubmissionTime = (bestSubmission: Submission | null, export const getSubmissionCount = (player: Player | null) => player?.submissions.length || '0'; +export const getSubmission = (curr: number, playerSubmissions: Submission[]) => { + for (let i = playerSubmissions.length - 1; i >= 0; i -= 1) { + if (playerSubmissions[i].problemIndex === curr) { + return playerSubmissions[i]; + } + } +}; + /** * De-duplicate a list of SelectableProblems. * Solution at: https://stackoverflow.com/a/1584377/7517518. diff --git a/frontend/src/views/Game.tsx b/frontend/src/views/Game.tsx index ca080c21..39b371c6 100644 --- a/frontend/src/views/Game.tsx +++ b/frontend/src/views/Game.tsx @@ -17,9 +17,7 @@ import { Difficulty } from '../api/Difficulty'; import { Game, manuallyEndGame } from '../api/Game'; import GameTimerContainer from '../components/game/GameTimerContainer'; import { GameTimer } from '../api/GameTimer'; -import { - TextButton, DangerButton, -} from '../components/core/Button'; +import { TextButton, DangerButton } from '../components/core/Button'; import { connect, routes, subscribe, } from '../api/Socket'; @@ -53,6 +51,7 @@ function GamePage() { const [host, setHost] = useState(null); const [spectators, setSpectators] = useState([]); const [gameTimer, setGameTimer] = useState(null); + const [timeUp, setTimeUp] = useState(false); const [allSolved, setAllSolved] = useState(false); const [gameEnded, setGameEnded] = useState(false); diff --git a/frontend/src/views/Lobby.tsx b/frontend/src/views/Lobby.tsx index b7b10709..16e20fb0 100644 --- a/frontend/src/views/Lobby.tsx +++ b/frontend/src/views/Lobby.tsx @@ -166,6 +166,7 @@ function LobbyPage() { const [duration, setDuration] = useState(15); const [selectedProblems, setSelectedProblems] = useState([]); const [size, setSize] = useState(10); + const [numProblems, setNumProblems] = useState(1); const [hoverVisible, setHoverVisible] = useState(false); const [showProblemSelector, setShowProblemSelector] = useState(true); @@ -218,6 +219,7 @@ function LobbyPage() { setDuration(newRoom.duration / 60); setSelectedProblems(newRoom.problems); setSize(newRoom.size); + setNumProblems(newRoom.numProblems); // Set the room and current user. dispatch(setRoom(newRoom)); @@ -343,7 +345,8 @@ function LobbyPage() { }); // eslint-disable-next-line no-alert - if (!allSpectators || window.confirm('Everybody in this room is a spectator, which means this is going to be a pretty boring game... are you sure you want to start the game?')) { + if (!allSpectators || window.confirm('Everybody in this room is a spectator, which means this is ' + + 'going to be a pretty boring game... are you sure you want to start the game?')) { startGame(currentRoomId, request) .then(() => { setLoading(true); @@ -496,6 +499,25 @@ function LobbyPage() { }); }; + const updateNumProblems = () => { + setError(''); + setLoading(true); + const prevNumProblems = numProblems; + const settings = { + initiator: currentUser!, + numProblems, + }; + + updateRoomSettings(currentRoomId, settings) + .catch((err) => { + setError(err.message); + // Set numProblems back to original if REST call failed + setNumProblems(prevNumProblems); + }).finally(() => { + setLoading(false); + }); + }; + /** * Display the passed-in list of users on the UI, either as * active or inactive. @@ -826,6 +848,28 @@ function LobbyPage() { /> + Number of Problems + + {`${numProblems} problem${numProblems === 1 ? '' : 's'}`} + + + + + { + const newNumProblems = Number(e.target.value); + if (newNumProblems >= 1 && newNumProblems <= 10) { + setNumProblems(newNumProblems); + } + }} + onMouseUp={updateNumProblems} + /> + + diff --git a/frontend/src/views/Results.tsx b/frontend/src/views/Results.tsx index fbe4fb40..5024fd4f 100644 --- a/frontend/src/views/Results.tsx +++ b/frontend/src/views/Results.tsx @@ -295,6 +295,7 @@ function GameResultsPage() { inviteContent={inviteContent()} loading={loading} isCurrentPlayer={players[1]?.user.userId === currentUser?.userId} + numProblems={game?.problems.length || 1} /> @@ -353,6 +356,7 @@ function GameResultsPage() { players={players} currentUser={currentUser} gameStartTime={startTime} + numProblems={game?.problems.length || 1} viewPlayerCode={(index: number) => setCodeModal(index)} spectatePlayer={null} /> diff --git a/src/main/java/com/codejoust/main/dto/game/GameMapper.java b/src/main/java/com/codejoust/main/dto/game/GameMapper.java index b3724405..4925a471 100644 --- a/src/main/java/com/codejoust/main/dto/game/GameMapper.java +++ b/src/main/java/com/codejoust/main/dto/game/GameMapper.java @@ -3,10 +3,13 @@ import org.modelmapper.ModelMapper; import org.modelmapper.convention.MatchingStrategies; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import com.codejoust.main.dto.problem.ProblemDto; import com.codejoust.main.dto.problem.ProblemMapper; @@ -71,6 +74,7 @@ public static Game fromRoom(Room room) { if (!user.getSpectator()) { Player player = PlayerMapper.playerFromUser(user); player.setColor(colorList.get(index)); + player.setSolved(new boolean[room.getNumProblems()]); players.put(user.getUserId(), player); index = (index + 1) % colorList.size(); } @@ -102,29 +106,52 @@ public static void sortLeaderboard(List players) { return -1; } - SubmissionDto bestSub1 = submissions1.get(0); - SubmissionDto bestSub2 = submissions2.get(0); + int score1 = getScore(submissions1); + int score2 = getScore(submissions2); - // Get the best solution by each player (highest score, then earliest submission) - for (SubmissionDto sub : submissions1) { - if (sub.getNumCorrect() > bestSub1.getNumCorrect()) { - bestSub1 = sub; - } - } + // If both have the same numCorrect, whoever submits earlier is first + if (score1 == score2) { + Instant time1 = getTime(submissions1); + Instant time2 = getTime(submissions2); - for (SubmissionDto sub : submissions2) { - if (sub.getNumCorrect() > bestSub2.getNumCorrect()) { - bestSub2 = sub; + // If neither has submitted correctly, oh well (if one is null, the other must be as well) + if (time1 == null || time2 == null) { + return 0; } - } - // If both have the same numCorrect, whoever submits earlier is first - if (bestSub1.getNumCorrect().equals(bestSub2.getNumCorrect())) { - return bestSub1.getStartTime().compareTo(bestSub2.getStartTime()); + return time1.compareTo(time2); } // Whoever has higher numCorrect is first - return bestSub2.getNumCorrect() - bestSub1.getNumCorrect(); + return score2 - score1; }); } + + // Get total number of problems solved + private static int getScore(List submissions) { + Set set = new HashSet<>(); + for (SubmissionDto submission : submissions) { + if (submission.getNumCorrect().equals(submission.getNumTestCases())) { + set.add(submission.getProblemIndex()); + } + } + + return set.size(); + } + + // Get time of latest correct solution, or null if none exists + private static Instant getTime(List submissions) { + Set set = new HashSet<>(); + Instant instant = null; + + for (SubmissionDto submission : submissions) { + if (submission.getNumCorrect().equals(submission.getNumTestCases()) + && !set.contains(submission.getProblemIndex())) { + set.add(submission.getProblemIndex()); + instant = submission.getStartTime(); + } + } + + return instant; + } } diff --git a/src/main/java/com/codejoust/main/dto/game/PlayerDto.java b/src/main/java/com/codejoust/main/dto/game/PlayerDto.java index 95478ad8..9107f091 100644 --- a/src/main/java/com/codejoust/main/dto/game/PlayerDto.java +++ b/src/main/java/com/codejoust/main/dto/game/PlayerDto.java @@ -13,12 +13,13 @@ @Getter @Setter -@EqualsAndHashCode +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class PlayerDto { + @EqualsAndHashCode.Include private UserDto user; private String code; private CodeLanguage language; private List submissions = new ArrayList<>(); - private Boolean solved; + private boolean[] solved; private Color color; } diff --git a/src/main/java/com/codejoust/main/dto/game/SubmissionDto.java b/src/main/java/com/codejoust/main/dto/game/SubmissionDto.java index 5478668b..3fe813bd 100644 --- a/src/main/java/com/codejoust/main/dto/game/SubmissionDto.java +++ b/src/main/java/com/codejoust/main/dto/game/SubmissionDto.java @@ -19,6 +19,7 @@ public class SubmissionDto { private CodeLanguage language; private String code; + private int problemIndex; private List results; private Integer numCorrect; private Integer numTestCases; diff --git a/src/main/java/com/codejoust/main/dto/game/SubmissionRequest.java b/src/main/java/com/codejoust/main/dto/game/SubmissionRequest.java index ff7277fb..463f583f 100644 --- a/src/main/java/com/codejoust/main/dto/game/SubmissionRequest.java +++ b/src/main/java/com/codejoust/main/dto/game/SubmissionRequest.java @@ -13,4 +13,5 @@ public class SubmissionRequest { private String code; private String input; private UserDto initiator; + private int problemIndex = 0; } diff --git a/src/main/java/com/codejoust/main/game_object/Player.java b/src/main/java/com/codejoust/main/game_object/Player.java index c5db6761..bb9b9f18 100644 --- a/src/main/java/com/codejoust/main/game_object/Player.java +++ b/src/main/java/com/codejoust/main/game_object/Player.java @@ -28,9 +28,8 @@ public class Player { * Solved variable if the user has successfully solved the problem, * or is still competing. */ - private Boolean solved = false; + private boolean[] solved; // Color associated with this player, generated on backend in game start. private Color color; - } diff --git a/src/main/java/com/codejoust/main/game_object/Submission.java b/src/main/java/com/codejoust/main/game_object/Submission.java index cf28a3f6..fc7f4d8a 100644 --- a/src/main/java/com/codejoust/main/game_object/Submission.java +++ b/src/main/java/com/codejoust/main/game_object/Submission.java @@ -13,6 +13,7 @@ public class Submission { private PlayerCode playerCode; + private int problemIndex; private List results; diff --git a/src/main/java/com/codejoust/main/service/GameManagementService.java b/src/main/java/com/codejoust/main/service/GameManagementService.java index 96a9bca0..510e1f07 100644 --- a/src/main/java/com/codejoust/main/service/GameManagementService.java +++ b/src/main/java/com/codejoust/main/service/GameManagementService.java @@ -186,6 +186,10 @@ public SubmissionDto runCode(String roomId, SubmissionRequest request) { throw new ApiException(GameError.EMPTY_FIELD); } + if (request.getProblemIndex() >= game.getProblems().size() || request.getProblemIndex() < 0) { + throw new ApiException(GameError.BAD_SETTING); + } + String initiatorUserId = request.getInitiator().getUserId(); if (!game.getPlayers().containsKey(initiatorUserId)) { throw new ApiException(GameError.INVALID_PERMISSIONS); @@ -202,6 +206,10 @@ public SubmissionDto submitSolution(String roomId, SubmissionRequest request) { throw new ApiException(GameError.EMPTY_FIELD); } + if (request.getProblemIndex() >= game.getProblems().size() || request.getProblemIndex() < 0) { + throw new ApiException(GameError.BAD_SETTING); + } + String initiatorUserId = request.getInitiator().getUserId(); if (!game.getPlayers().containsKey(initiatorUserId)) { throw new ApiException(GameError.INVALID_PERMISSIONS); diff --git a/src/main/java/com/codejoust/main/service/SubmitService.java b/src/main/java/com/codejoust/main/service/SubmitService.java index e8f7f2ad..f3e113fa 100644 --- a/src/main/java/com/codejoust/main/service/SubmitService.java +++ b/src/main/java/com/codejoust/main/service/SubmitService.java @@ -92,21 +92,18 @@ private Submission getDummySubmission(TesterRequest request) { // Test the submission and send a socket update. public SubmissionDto runCode(Game game, SubmissionRequest request) { String userId = request.getInitiator().getUserId(); - Player player = game.getPlayers().get(userId); PlayerCode playerCode = new PlayerCode(); playerCode.setCode(request.getCode()); playerCode.setLanguage(request.getLanguage()); - player.setPlayerCode(playerCode); - // Make a call to the tester service TesterRequest testerRequest = new TesterRequest(); testerRequest.setCode(request.getCode()); testerRequest.setLanguage(request.getLanguage()); // Set the problem with the single provided test case. - ProblemDto problemDto = getStrippedProblemDto(game.getProblems().get(0)); + ProblemDto problemDto = getStrippedProblemDto(game.getProblems().get(request.getProblemIndex())); /** * Provide a temporary output to circumvent output parsing error. @@ -126,6 +123,7 @@ public SubmissionDto runCode(Game game, SubmissionRequest request) { // Return submission, and no further records necessary for running code. Submission submission = getSubmission(testerRequest); + submission.setProblemIndex(request.getProblemIndex()); return GameMapper.submissionToDto(submission); } @@ -138,30 +136,31 @@ public SubmissionDto submitSolution(Game game, SubmissionRequest request) { playerCode.setCode(request.getCode()); playerCode.setLanguage(request.getLanguage()); - player.setPlayerCode(playerCode); - // Make a call to the tester service TesterRequest testerRequest = new TesterRequest(); testerRequest.setCode(request.getCode()); testerRequest.setLanguage(request.getLanguage()); // Invariant: Games have at least one problem (else it will fail to create) - ProblemDto problemDto = getStrippedProblemDto(game.getProblems().get(0)); + ProblemDto problemDto = getStrippedProblemDto(game.getProblems().get(request.getProblemIndex())); testerRequest.setProblem(problemDto); Submission submission = getSubmission(testerRequest); + submission.setProblemIndex(request.getProblemIndex()); player.getSubmissions().add(submission); if (submission.getNumCorrect().equals(submission.getNumTestCases())) { - player.setSolved(true); + player.getSolved()[request.getProblemIndex()] = true; } // Variable to indicate whether all players have solved the problem. boolean allSolved = true; for (Player p : game.getPlayers().values()) { - if (p.getSolved() == null || !p.getSolved()) { - allSolved = false; - break; + for (Boolean b : p.getSolved()) { + if (b == null || !b) { + allSolved = false; + break; + } } } diff --git a/src/test/java/com/codejoust/main/mapper/GameMapperTests.java b/src/test/java/com/codejoust/main/mapper/GameMapperTests.java index efdd3a77..6081578e 100644 --- a/src/test/java/com/codejoust/main/mapper/GameMapperTests.java +++ b/src/test/java/com/codejoust/main/mapper/GameMapperTests.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -34,9 +35,10 @@ public class GameMapperTests { private static final int TEST_CASES = 10; // Helper method to add a dummy submission to a PlayerDto object - private void addSubmissionHelper(PlayerDto playerDto, int numCorrect) { + private void addSubmissionHelper(PlayerDto playerDto, int numCorrect, int problemIndex) { SubmissionDto submissionDto = new SubmissionDto(); submissionDto.setNumCorrect(numCorrect); + submissionDto.setProblemIndex(problemIndex); submissionDto.setStartTime(Instant.now()); playerDto.getSubmissions().add(submissionDto); @@ -104,7 +106,7 @@ public void toDto() { submission.setNumCorrect(TEST_CASES); Player player = game.getPlayers().get(TestFields.USER_ID); - player.setSolved(true); + player.setSolved(new boolean[]{true}); player.setPlayerCode(playerCode); player.getSubmissions().add(submission); @@ -119,9 +121,7 @@ public void toDto() { PlayerDto playerDto = gameDto.getPlayers().get(0); assertEquals(UserMapper.toDto(user), playerDto.getUser()); - assertEquals(player.getSolved(), playerDto.getSolved()); - assertEquals(playerCode.getCode(), playerDto.getCode()); - assertEquals(playerCode.getLanguage(), playerDto.getLanguage()); + assertArrayEquals(player.getSolved(), playerDto.getSolved()); assertEquals(1, playerDto.getSubmissions().size()); assertEquals(player.getColor(), playerDto.getColor()); @@ -159,20 +159,23 @@ public void sortLeaderboardSuccess() { // Note: order of addSubmissionHelper matters (time of submission) PlayerDto player1 = new PlayerDto(); - addSubmissionHelper(player1, 0); + addSubmissionHelper(player1, TEST_CASES, 0); + addSubmissionHelper(player1, TEST_CASES, 0); + addSubmissionHelper(player1, TEST_CASES, 0); PlayerDto player2 = new PlayerDto(); - addSubmissionHelper(player2, 0); - addSubmissionHelper(player2, 3); + addSubmissionHelper(player2, TEST_CASES, 0); + addSubmissionHelper(player2, TEST_CASES, 1); PlayerDto player3 = new PlayerDto(); - addSubmissionHelper(player3, 3); - - addSubmissionHelper(player2, 3); + addSubmissionHelper(player3, 0, 0); PlayerDto player4 = new PlayerDto(); - addSubmissionHelper(player4, 5); - addSubmissionHelper(player4, 1); + addSubmissionHelper(player4, TEST_CASES, 0); + addSubmissionHelper(player4, TEST_CASES, 1); + + // Player 2 submits wrong afterwards, but it doesn't count against his time + addSubmissionHelper(player2, 0, 1); PlayerDto player5 = new PlayerDto(); @@ -182,13 +185,13 @@ public void sortLeaderboardSuccess() { players.add(player4); players.add(player5); - // Player order should be: [4, 2, 3, 1, 5] + // Player order should be: [2, 4, 1, 3, 5] GameMapper.sortLeaderboard(players); - assertEquals(player4, players.get(0)); - assertEquals(player2, players.get(1)); - assertEquals(player3, players.get(2)); - assertEquals(player1, players.get(3)); + assertEquals(player2, players.get(0)); + assertEquals(player4, players.get(1)); + assertEquals(player1, players.get(2)); + assertEquals(player3, players.get(3)); assertEquals(player5, players.get(4)); } @@ -196,8 +199,10 @@ public void sortLeaderboardSuccess() { public void toDtoSortsLeaderboard() { Submission sub1 = new Submission(); sub1.setNumCorrect(0); + sub1.setNumTestCases(TEST_CASES); Submission sub2 = new Submission(); - sub2.setNumCorrect(1); + sub2.setNumCorrect(TEST_CASES); + sub2.setNumTestCases(TEST_CASES); Player player1 = new Player(); player1.getSubmissions().add(sub1); @@ -213,7 +218,7 @@ public void toDtoSortsLeaderboard() { List players = gameDto.getPlayers(); assertEquals(2, players.size()); - assertEquals(1, players.get(0).getSubmissions().get(0).getNumCorrect()); + assertEquals(TEST_CASES, players.get(0).getSubmissions().get(0).getNumCorrect()); assertEquals(0, players.get(1).getSubmissions().get(0).getNumCorrect()); } } diff --git a/src/test/java/com/codejoust/main/mapper/PlayerMapperTests.java b/src/test/java/com/codejoust/main/mapper/PlayerMapperTests.java index 62f5161a..2c6e3063 100644 --- a/src/test/java/com/codejoust/main/mapper/PlayerMapperTests.java +++ b/src/test/java/com/codejoust/main/mapper/PlayerMapperTests.java @@ -5,7 +5,6 @@ import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import com.codejoust.main.dto.game.PlayerMapper; @@ -25,7 +24,6 @@ public void playerFromUser() { assertEquals(user, player.getUser()); assertNull(player.getPlayerCode()); - assertFalse(player.getSolved()); assertEquals(0, player.getSubmissions().size()); } } diff --git a/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java b/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java index fcd8f08c..6c6e5705 100644 --- a/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java +++ b/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java @@ -87,7 +87,7 @@ private void addSubmissionHelper(Player player, int numCorrect) { player.getSubmissions().add(submission); if (numCorrect == TestFields.NUM_PROBLEMS) { - player.setSolved(true); + player.setSolved(new boolean[]{true}); } } diff --git a/src/test/java/com/codejoust/main/service/RoomServiceTests.java b/src/test/java/com/codejoust/main/service/RoomServiceTests.java index 87f1e779..a88c390e 100644 --- a/src/test/java/com/codejoust/main/service/RoomServiceTests.java +++ b/src/test/java/com/codejoust/main/service/RoomServiceTests.java @@ -277,6 +277,31 @@ public void manyUsersJoiningAnInfinitelySizedRoomSuccess() { assertEquals(102, room.getUsers().size()); } + @Test + public void setInvalidNumProblemsFailure() { + /** + * Verify update settings request fails when numProblems is + * set to outside of the allowable range + */ + User host = new User(); + host.setNickname(TestFields.NICKNAME); + + Room room = new Room(); + room.setRoomId(TestFields.ROOM_ID); + room.setHost(host); + room.addUser(host); + + UpdateSettingsRequest request = new UpdateSettingsRequest(); + request.setInitiator(UserMapper.toDto(host)); + request.setNumProblems(15); + + // Mock repository to return room when called + Mockito.doReturn(room).when(repository).findRoomByRoomId(eq(TestFields.ROOM_ID)); + ApiException exception = assertThrows(ApiException.class, () -> roomService.updateRoomSettings(TestFields.ROOM_ID, request)); + + verify(repository).findRoomByRoomId(TestFields.ROOM_ID); + assertEquals(ProblemError.INVALID_NUMBER_REQUEST, exception.getError()); + } @Test public void getRoomSuccess() {