From 01c718354d4f33a99a7b58888aae266c0f6f318d Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:16:59 +0200 Subject: [PATCH 01/19] fix: dependecy warnings --- webapp/src/pages/Game.jsx | 97 ++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index d5f875b2..7606a204 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; @@ -10,8 +10,7 @@ import MenuButton from '../components/MenuButton'; import { HttpStatusCode } from "axios"; export default function Game() { - const navigate = useNavigate(); - + const navigate = useRef(useNavigate()).current; const [loading, setLoading] = useState(true); const [gameId, setGameId] = useState(null); const [question, setQuestion] = useState(null); @@ -38,7 +37,7 @@ export default function Game() { return Math.min(Math.max(percentage, 0), 100); }; - const assignQuestion = async (gameId) => { + const assignQuestion = useCallback(async (gameId) => { try { const result = await getCurrentQuestion(gameId); if (result.status === HttpStatusCode.Ok) { @@ -52,17 +51,17 @@ export default function Game() { console.error("Error fetching question:", error); navigate("/dashboard"); } - } + }, [setQuestion, setNextDisabled, setTimeElapsed, navigate]) const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; setSelectedOption(selectedOptionIndex); - await setAnswer(answer); + setAnswer(answer); const anyOptionSelected = selectedOptionIndex !== null; setNextDisabled(!anyOptionSelected); }; - const startNewRound = async (gameId) => { + const startNewRound = useCallback(async (gameId) => { try{ const result = await startRound(gameId); setTimeStartRound(new Date(result.data.round_start_time).getTime()); @@ -73,51 +72,18 @@ export default function Game() { } catch(error){ console.log(error) - if(error.status === 409){ + if(error.response.status === 409){ if(roundNumber >= 9){ navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); } else { await assignQuestion(gameId) } } - } + }, [setTimeStartRound, setRoundDuration, setRoundNumber, + assignQuestion, setLoading, navigate, correctAnswers, roundNumber]) - } - - /* - Initialize game when loading the page - */ - const initializeGame = async () => { - try { - const newGameResponse = await newGame(); - if (newGameResponse) { - setGameId(newGameResponse.id); - setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); - setRoundDuration(newGameResponse.round_duration) - setMaxRoundNumber(newGameResponse.rounds); - try{ - const result = await getCurrentQuestion(newGameResponse.id); - if (result.status === HttpStatusCode.Ok) { - setQuestion(result.data); - setNextDisabled(false); - setLoading(false); - } - }catch(error){ - startNewRound(newGameResponse.id); - } - - - } else { - navigate("/dashboard"); - } - } catch (error) { - console.error("Error initializing game:", error); - navigate("/dashboard"); - } - }; - - const nextRound = async () => { + const nextRound = useCallback(async () => { if (roundNumber + 1 > maxRoundNumber) { navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); } else { @@ -125,7 +91,7 @@ export default function Game() { setNextDisabled(true); await startNewRound(gameId); } - } + }, [navigate, setAnswer, setNextDisabled, startNewRound, correctAnswers, gameId, maxRoundNumber, roundNumber]); const nextButtonClick = async () => { try { @@ -142,16 +108,42 @@ export default function Game() { } catch (error) { if(error.response.status === 400){ setTimeout(nextButtonClick, 2000) - }else{ - console.log('xd'+error.response.status) } } - }; + } + useEffect(() => { - // Empty dependency array [] ensures this effect runs only once after initial render + const initializeGame = async () => { + try { + const newGameResponse = await newGame(); + if (newGameResponse) { + setGameId(newGameResponse.id); + setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); + setRoundDuration(newGameResponse.round_duration) + setMaxRoundNumber(newGameResponse.rounds); + try{ + const result = await getCurrentQuestion(newGameResponse.id); + if (result.status === HttpStatusCode.Ok) { + setQuestion(result.data); + setNextDisabled(false); + setLoading(false); + } + }catch(error){ + startNewRound(newGameResponse.id); + } + + + } else { + navigate("/dashboard"); + } + } catch (error) { + console.error("Error initializing game:", error); + navigate("/dashboard"); + } + }; initializeGame(); - // eslint-disable-next-line - }, []); + }, [setGameId, setTimeStartRound, setRoundDuration, setMaxRoundNumber, + setQuestion, setNextDisabled, setLoading, startNewRound, navigate]); useEffect(() => { let timeout; if (showConfetti) @@ -170,8 +162,7 @@ export default function Game() { }, 1000); } return () => clearTimeout(timeout); - // eslint-disable-next-line - }, [timeElapsed, timeStartRound, roundDuration]); + }, [timeElapsed, timeStartRound, roundDuration, nextRound]); return ( From 618736ffa0e31c93ac6d4c731d7fb588903263e1 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:18:02 +0200 Subject: [PATCH 02/19] feat: time left inside timer --- webapp/src/pages/Game.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 7606a204..51020af8 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; -import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; +import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress, CircularProgressLabel } from "@chakra-ui/react"; import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; @@ -174,7 +174,9 @@ export default function Game() { {`Correct answers: ${correctAnswers}`} - + + {roundDuration - timeElapsed} + {loading ? ( From af332ce4d0168d23bc01708e2d393f65e4c0ccd5 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:18:32 +0200 Subject: [PATCH 03/19] feat: backwards-going timer --- webapp/src/pages/Game.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 51020af8..2de04385 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -33,7 +33,7 @@ export default function Game() { }; const calculateProgress = () => { - const percentage = (timeElapsed / roundDuration) * 100; + const percentage = (roundDuration - timeElapsed / roundDuration) * 100; return Math.min(Math.max(percentage, 0), 100); }; From ef0e8b1b394d92107c15b1388d126df029e24efd Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:20:59 +0200 Subject: [PATCH 04/19] feat: disabled next button when no answer is selected --- webapp/src/pages/Game.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 2de04385..a8a37df8 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -42,7 +42,6 @@ export default function Game() { const result = await getCurrentQuestion(gameId); if (result.status === HttpStatusCode.Ok) { setQuestion(result.data); - setNextDisabled(false); setTimeElapsed(0); } else { navigate("/dashboard"); @@ -103,7 +102,7 @@ export default function Game() { } setNextDisabled(true); setSelectedOption(null); - await nextRound() + await nextRound(); } catch (error) { if(error.response.status === 400){ From 860493aa7e43fdfc6d29b2d2e8687eed0bb7368f Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:26:54 +0200 Subject: [PATCH 05/19] feat: add total amount of rounds --- webapp/public/locales/en/translation.json | 5 +++-- webapp/public/locales/es/translation.json | 5 +++-- webapp/src/pages/Game.jsx | 7 +++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index b61195dc..5ea6b68a 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -74,8 +74,9 @@ } }, "game": { - "round": "Round ", - "answer": "Answer" + "round": "Round {{currentRound, number}} of {{roundAmount, number}}", + "answer": "Answer", + "correct_counter": "Correct answers: {{correctCount, number}}" }, "about": { "title": "About", diff --git a/webapp/public/locales/es/translation.json b/webapp/public/locales/es/translation.json index a67bc786..96e3078d 100644 --- a/webapp/public/locales/es/translation.json +++ b/webapp/public/locales/es/translation.json @@ -73,8 +73,9 @@ } }, "game": { - "round": "Ronda ", - "answer": "Responder" + "round": "Ronda {{currentRound, number}} de {{roundAmount, number}}", + "answer": "Responder", + "correct_counter": "Respuestas correctas: {{correctCount, number}}" }, "about": { "title": "Sobre nosotros", diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index a8a37df8..ab034ea9 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -50,7 +50,7 @@ export default function Game() { console.error("Error fetching question:", error); navigate("/dashboard"); } - }, [setQuestion, setNextDisabled, setTimeElapsed, navigate]) + }, [setQuestion, setTimeElapsed, navigate]) const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; @@ -169,9 +169,8 @@ export default function Game() { setIsMenuOpen(true)} /> setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> - {t("game.round") + `${roundNumber}`} - - {`Correct answers: ${correctAnswers}`} + {t("game.round", {currentRound: roundNumber, roundAmount: maxRoundNumber})} + {t("game.correct_counter", {correctCounter: correctAnswers})} {roundDuration - timeElapsed} From 522163df88bbd5a38c2574ef7c3b4764ab4f7253 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:31:06 +0200 Subject: [PATCH 06/19] fix: correct answers not showing correctly --- webapp/src/pages/Game.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index ab034ea9..ea96e015 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -90,7 +90,8 @@ export default function Game() { setNextDisabled(true); await startNewRound(gameId); } - }, [navigate, setAnswer, setNextDisabled, startNewRound, correctAnswers, gameId, maxRoundNumber, roundNumber]); + }, [navigate, setAnswer, setNextDisabled, startNewRound, correctAnswers, + gameId, maxRoundNumber, roundNumber]); const nextButtonClick = async () => { try { @@ -170,7 +171,7 @@ export default function Game() { setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> {t("game.round", {currentRound: roundNumber, roundAmount: maxRoundNumber})} - {t("game.correct_counter", {correctCounter: correctAnswers})} + {t("game.correct_counter", {correctCount: correctAnswers})} {roundDuration - timeElapsed} From 113c975658d7f773f64c951cd5008b70947c9349 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 22:42:30 +0200 Subject: [PATCH 07/19] feat: use workaround to stop useEffect to create a new game every time a new question is answered --- webapp/src/pages/Game.jsx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index ea96e015..2f48c388 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -47,8 +47,12 @@ export default function Game() { navigate("/dashboard"); } } catch (error) { - console.error("Error fetching question:", error); - navigate("/dashboard"); + if (error.response.status === HttpStatusCode.Conflict) { + throw error; + } else { + console.error("Error fetching question:", error); + navigate("/dashboard"); + } } }, [setQuestion, setTimeElapsed, navigate]) @@ -114,6 +118,9 @@ export default function Game() { useEffect(() => { const initializeGame = async () => { + if (gameId) { + return; + } try { const newGameResponse = await newGame(); if (newGameResponse) { @@ -122,17 +129,11 @@ export default function Game() { setRoundDuration(newGameResponse.round_duration) setMaxRoundNumber(newGameResponse.rounds); try{ - const result = await getCurrentQuestion(newGameResponse.id); - if (result.status === HttpStatusCode.Ok) { - setQuestion(result.data); - setNextDisabled(false); - setLoading(false); - } + await assignQuestion(newGameResponse.id); + setLoading(false); }catch(error){ startNewRound(newGameResponse.id); } - - } else { navigate("/dashboard"); } @@ -142,8 +143,8 @@ export default function Game() { } }; initializeGame(); - }, [setGameId, setTimeStartRound, setRoundDuration, setMaxRoundNumber, - setQuestion, setNextDisabled, setLoading, startNewRound, navigate]); + }, [setGameId, gameId, setTimeStartRound, setRoundDuration, setMaxRoundNumber, + setQuestion, setLoading, startNewRound, navigate, assignQuestion]); useEffect(() => { let timeout; if (showConfetti) From a34c373073f8a9e023a73c1e0e15c65d766da66b Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 13 Apr 2024 23:00:39 +0200 Subject: [PATCH 08/19] fix: backwards timer --- webapp/src/pages/Game.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 2f48c388..ded7575c 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -33,7 +33,7 @@ export default function Game() { }; const calculateProgress = () => { - const percentage = (roundDuration - timeElapsed / roundDuration) * 100; + const percentage = ((roundDuration - timeElapsed) / roundDuration) * 100; return Math.min(Math.max(percentage, 0), 100); }; From 07134e8b9c11ee14668cecadd958733947fdf045 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 14 Apr 2024 10:59:30 +0200 Subject: [PATCH 09/19] chore: change new game endpoint --- .../quizapi/commons/utils/GameModeUtils.java | 114 +-- .../main/java/lab/en2b/quizapi/game/Game.java | 338 +++---- .../lab/en2b/quizapi/game/GameController.java | 240 ++--- .../java/lab/en2b/quizapi/game/GameMode.java | 24 +- .../lab/en2b/quizapi/game/GameService.java | 364 ++++---- .../en2b/quizapi/game/dtos/CustomGameDto.java | 66 +- .../en2b/quizapi/game/dtos/GameModeDto.java | 48 +- .../quizapi/game/dtos/GameResponseDto.java | 100 +- .../game/mappers/GameResponseDtoMapper.java | 60 +- .../question/QuestionRepository.java | 26 +- .../questions/question/QuestionService.java | 184 ++-- .../en2b/quizapi/game/GameControllerTest.java | 446 ++++----- .../en2b/quizapi/game/GameServiceTest.java | 872 +++++++++--------- .../questions/QuestionServiceTest.java | 322 +++---- webapp/src/components/game/Game.js | 2 +- 15 files changed, 1603 insertions(+), 1603 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java index 4e6c2886..195fff14 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java @@ -1,57 +1,57 @@ -package lab.en2b.quizapi.commons.utils; - -import lab.en2b.quizapi.game.Game; -import lab.en2b.quizapi.game.GameMode; -import lab.en2b.quizapi.questions.question.QuestionCategory; - -import java.util.List; - -import static lab.en2b.quizapi.game.GameMode.KIWI_QUEST; - -public class GameModeUtils { - public static List getQuestionCategoriesForGamemode(GameMode gamemode, List questionCategoriesForCustom){ - if(gamemode == null){ - gamemode = KIWI_QUEST; - } - return switch (gamemode) { - case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); - case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); - case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); - case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); - case ANCIENT_ODYSSEY -> List.of(QuestionCategory.MUSIC,QuestionCategory.ART); - case RANDOM -> List.of(QuestionCategory.values()); - case CUSTOM -> questionCategoriesForCustom; - }; - } - public static void setGamemodeParams(Game game){ - switch(game.getGamemode()){ - case KIWI_QUEST: - game.setRounds(9L); - game.setRoundDuration(30); - break; - case FOOTBALL_SHOWDOWN: - game.setRounds(9L); - game.setRoundDuration(30); - break; - case GEO_GENIUS: - game.setRounds(9L); - game.setRoundDuration(30); - break; - case VIDEOGAME_ADVENTURE: - game.setRounds(9L); - game.setRoundDuration(30); - break; - case ANCIENT_ODYSSEY: - game.setRounds(9L); - game.setRoundDuration(30); - break; - case RANDOM: - game.setRounds(9L); - game.setRoundDuration(30); - break; - default: - game.setRounds(9L); - game.setRoundDuration(30); - } - } -} +package lab.en2b.quizapi.commons.utils; + +import lab.en2b.quizapi.game.Game; +import lab.en2b.quizapi.game.GameMode; +import lab.en2b.quizapi.questions.question.QuestionCategory; + +import java.util.List; + +import static lab.en2b.quizapi.game.GameMode.KIWI_QUEST; + +public class GameModeUtils { + public static List getQuestionCategoriesForGamemode(GameMode gamemode, List questionCategoriesForCustom){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } + return switch (gamemode) { + case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); + case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); + case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); + case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); + case ANCIENT_ODYSSEY -> List.of(QuestionCategory.MUSIC,QuestionCategory.ART); + case RANDOM -> List.of(QuestionCategory.values()); + case CUSTOM -> questionCategoriesForCustom; + }; + } + public static void setGamemodeParams(Game game){ + switch(game.getGamemode()){ + case KIWI_QUEST: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case FOOTBALL_SHOWDOWN: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case GEO_GENIUS: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case VIDEOGAME_ADVENTURE: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case ANCIENT_ODYSSEY: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case RANDOM: + game.setRounds(9L); + game.setRoundDuration(30); + break; + default: + game.setRounds(9L); + game.setRoundDuration(30); + } + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index 19097219..a12cb59d 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -1,169 +1,169 @@ -package lab.en2b.quizapi.game; - -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lab.en2b.quizapi.commons.user.User; -import lab.en2b.quizapi.commons.utils.GameModeUtils; -import lab.en2b.quizapi.game.dtos.CustomGameDto; -import lab.en2b.quizapi.questions.answer.Answer; -import lab.en2b.quizapi.questions.question.Question; -import lab.en2b.quizapi.questions.question.QuestionCategory; -import lombok.*; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static lab.en2b.quizapi.game.GameMode.*; - -@Entity -@Table(name = "games") -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@Builder -public class Game { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Setter(AccessLevel.NONE) - private Long id; - - private Long rounds = 9L; - private Long actualRound = 0L; - - private Long correctlyAnsweredQuestions = 0L; - private String language; - private Long roundStartTime = 0L; - @NonNull - private Integer roundDuration; - private boolean currentQuestionAnswered; - @Enumerated(EnumType.STRING) - private GameMode gamemode; - @ManyToOne - @NotNull - @JoinColumn(name = "user_id") - private User user; - - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name="games_questions", - joinColumns= - @JoinColumn(name="game_id", referencedColumnName="id"), - inverseJoinColumns= - @JoinColumn(name="question_id", referencedColumnName="id") - ) - - @OrderColumn - private List questions; - private boolean isGameOver; - @Enumerated(EnumType.STRING) - private List questionCategoriesForCustom; - - public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ - this.user = user; - this.questions = new ArrayList<>(); - this.actualRound = 0L; - setLanguage(lang); - if(gamemode == CUSTOM) - setCustomGameMode(gameDto); - else - setGameMode(gamemode); - } - - public void newRound(Question question){ - if(getActualRound() != 0){ - if (isGameOver()) - throw new IllegalStateException("You can't start a round for a finished game!"); - if(!currentRoundIsOver()) - throw new IllegalStateException("You can't start a new round when the current round is not over yet!"); - } - - setCurrentQuestionAnswered(false); - getQuestions().add(question); - increaseRound(); - setRoundStartTime(Instant.now().toEpochMilli()); - } - - private void increaseRound(){ - setActualRound(getActualRound() + 1); - } - - public boolean isGameOver(){ - return isGameOver && getActualRound() >= getRounds(); - } - - - public Question getCurrentQuestion() { - if(getRoundStartTime() == null){ - throw new IllegalStateException("The round is not active!"); - } - if(currentRoundIsOver()) - throw new IllegalStateException("The current round is over!"); - if(isGameOver()) - throw new IllegalStateException("The game is over!"); - return getQuestions().get(getQuestions().size()-1); - } - - private boolean currentRoundIsOver(){ - return currentQuestionAnswered || roundTimeHasExpired(); - } - - private boolean roundTimeHasExpired(){ - return getRoundStartTime()!= null && Instant.now().isAfter(Instant.ofEpochMilli(getRoundStartTime()).plusSeconds(getRoundDuration())); - } - - public boolean answerQuestion(Long answerId){ - if(currentRoundIsOver()) - throw new IllegalStateException("You can't answer a question when the current round is over!"); - if (isGameOver()) - throw new IllegalStateException("You can't answer a question when the game is over!"); - Question q = getCurrentQuestion(); - if (q.getAnswers().stream().map(Answer::getId).noneMatch(i -> i.equals(answerId))) - throw new IllegalArgumentException("The answer you provided is not one of the options"); - if(q.isCorrectAnswer(answerId)){ - setCorrectlyAnsweredQuestions(getCorrectlyAnsweredQuestions() + 1); - } - setCurrentQuestionAnswered(true); - return q.isCorrectAnswer(answerId); - } - public void setLanguage(String language){ - if(language == null){ - language = "en"; - } - if(!isLanguageSupported(language)) - throw new IllegalArgumentException("The language you provided is not supported"); - this.language = language; - } - public void setCustomGameMode(CustomGameDto gameDto){ - setRounds(gameDto.getRounds()); - setRoundDuration(gameDto.getRoundDuration()); - this.gamemode = CUSTOM; - setQuestionCategoriesForCustom(gameDto.getCategories()); - } - public void setGameMode(GameMode gamemode){ - if(gamemode == null){ - gamemode = KIWI_QUEST; - } - this.gamemode = gamemode; - GameModeUtils.setGamemodeParams(this); - } - - public void setQuestionCategoriesForCustom(List questionCategoriesForCustom) { - if(gamemode != CUSTOM) - throw new IllegalStateException("You can't set custom categories for a non-custom gamemode!"); - if(questionCategoriesForCustom == null || questionCategoriesForCustom.isEmpty()) - throw new IllegalArgumentException("You can't set an empty list of categories for a custom gamemode!"); - this.questionCategoriesForCustom = questionCategoriesForCustom; - } - - public List getQuestionCategoriesForGamemode(){ - return GameModeUtils.getQuestionCategoriesForGamemode(gamemode,questionCategoriesForCustom); - } - private boolean isLanguageSupported(String language) { - return language.equals("en") || language.equals("es"); - } - - public boolean shouldBeGameOver() { - return getActualRound() >= getRounds() && !isGameOver && currentRoundIsOver(); - } -} +package lab.en2b.quizapi.game; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.utils.GameModeUtils; +import lab.en2b.quizapi.game.dtos.CustomGameDto; +import lab.en2b.quizapi.questions.answer.Answer; +import lab.en2b.quizapi.questions.question.Question; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lombok.*; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static lab.en2b.quizapi.game.GameMode.*; + +@Entity +@Table(name = "games") +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class Game { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Setter(AccessLevel.NONE) + private Long id; + + private Long rounds = 9L; + private Long actualRound = 0L; + + private Long correctlyAnsweredQuestions = 0L; + private String language; + private Long roundStartTime = 0L; + @NonNull + private Integer roundDuration; + private boolean currentQuestionAnswered; + @Enumerated(EnumType.STRING) + private GameMode gamemode; + @ManyToOne + @NotNull + @JoinColumn(name = "user_id") + private User user; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name="games_questions", + joinColumns= + @JoinColumn(name="game_id", referencedColumnName="id"), + inverseJoinColumns= + @JoinColumn(name="question_id", referencedColumnName="id") + ) + + @OrderColumn + private List questions; + private boolean isGameOver; + @Enumerated(EnumType.STRING) + private List questionCategoriesForCustom; + + public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ + this.user = user; + this.questions = new ArrayList<>(); + this.actualRound = 0L; + setLanguage(lang); + if(gamemode == CUSTOM) + setCustomGameMode(gameDto); + else + setGameMode(gamemode); + } + + public void newRound(Question question){ + if(getActualRound() != 0){ + if (isGameOver()) + throw new IllegalStateException("You can't start a round for a finished game!"); + if(!currentRoundIsOver()) + throw new IllegalStateException("You can't start a new round when the current round is not over yet!"); + } + + setCurrentQuestionAnswered(false); + getQuestions().add(question); + increaseRound(); + setRoundStartTime(Instant.now().toEpochMilli()); + } + + private void increaseRound(){ + setActualRound(getActualRound() + 1); + } + + public boolean isGameOver(){ + return isGameOver && getActualRound() >= getRounds(); + } + + + public Question getCurrentQuestion() { + if(getRoundStartTime() == null){ + throw new IllegalStateException("The round is not active!"); + } + if(currentRoundIsOver()) + throw new IllegalStateException("The current round is over!"); + if(isGameOver()) + throw new IllegalStateException("The game is over!"); + return getQuestions().get(getQuestions().size()-1); + } + + private boolean currentRoundIsOver(){ + return currentQuestionAnswered || roundTimeHasExpired(); + } + + private boolean roundTimeHasExpired(){ + return getRoundStartTime()!= null && Instant.now().isAfter(Instant.ofEpochMilli(getRoundStartTime()).plusSeconds(getRoundDuration())); + } + + public boolean answerQuestion(Long answerId){ + if(currentRoundIsOver()) + throw new IllegalStateException("You can't answer a question when the current round is over!"); + if (isGameOver()) + throw new IllegalStateException("You can't answer a question when the game is over!"); + Question q = getCurrentQuestion(); + if (q.getAnswers().stream().map(Answer::getId).noneMatch(i -> i.equals(answerId))) + throw new IllegalArgumentException("The answer you provided is not one of the options"); + if(q.isCorrectAnswer(answerId)){ + setCorrectlyAnsweredQuestions(getCorrectlyAnsweredQuestions() + 1); + } + setCurrentQuestionAnswered(true); + return q.isCorrectAnswer(answerId); + } + public void setLanguage(String language){ + if(language == null){ + language = "en"; + } + if(!isLanguageSupported(language)) + throw new IllegalArgumentException("The language you provided is not supported"); + this.language = language; + } + public void setCustomGameMode(CustomGameDto gameDto){ + setRounds(gameDto.getRounds()); + setRoundDuration(gameDto.getRoundDuration()); + this.gamemode = CUSTOM; + setQuestionCategoriesForCustom(gameDto.getCategories()); + } + public void setGameMode(GameMode gamemode){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } + this.gamemode = gamemode; + GameModeUtils.setGamemodeParams(this); + } + + public void setQuestionCategoriesForCustom(List questionCategoriesForCustom) { + if(gamemode != CUSTOM) + throw new IllegalStateException("You can't set custom categories for a non-custom gamemode!"); + if(questionCategoriesForCustom == null || questionCategoriesForCustom.isEmpty()) + throw new IllegalArgumentException("You can't set an empty list of categories for a custom gamemode!"); + this.questionCategoriesForCustom = questionCategoriesForCustom; + } + + public List getQuestionCategoriesForGamemode(){ + return GameModeUtils.getQuestionCategoriesForGamemode(gamemode,questionCategoriesForCustom); + } + private boolean isLanguageSupported(String language) { + return language.equals("en") || language.equals("es"); + } + + public boolean shouldBeGameOver() { + return getActualRound() >= getRounds() && !isGameOver && currentRoundIsOver(); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 08af1201..1e3f6062 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -1,120 +1,120 @@ -package lab.en2b.quizapi.game; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.validation.Valid; -import lab.en2b.quizapi.game.dtos.*; -import lab.en2b.quizapi.questions.question.QuestionCategory; -import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/games") -@RequiredArgsConstructor -public class GameController { - private final GameService gameService; - - @Operation(summary = "Starts new game", description = "Requests the API to create a new game for a given authentication (a player)") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "400", description = "Given when: \n * language provided is not valid \n * gamemode provided is not valid \n * body is not provided with custom game", content = @io.swagger.v3.oas.annotations.media.Content), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameters({ - @Parameter(name = "lang", description = "The language of the game", example = "en"), - @Parameter(name = "gamemode", description = "The gamemode of the game", example = "KIWI_QUEST") - }) - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") - @PostMapping("/play") - public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) @Valid CustomGameDto customGameDto, Authentication authentication){ - if(gamemode == GameMode.CUSTOM && customGameDto == null) - throw new IllegalArgumentException("Custom game mode requires a body"); - return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); - } - - @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameter(name = "id", description = "The id of the game to start the round for", example = "1") - @PostMapping("/{id}/startRound") - public ResponseEntity startRound(@PathVariable Long id, Authentication authentication){ - return ResponseEntity.ok(gameService.startRound(id, authentication)); - } - - @Operation(summary = "Gets the current question", description = "Gets the question and its possible answers from the API for a given authentication (a player)") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameter(name = "id", description = "The id of the game to get the current question for", example = "1") - @GetMapping("/{id}/question") - public ResponseEntity getCurrentQuestion(@PathVariable Long id, Authentication authentication){ - return ResponseEntity.ok(gameService.getCurrentQuestion(id, authentication)); - } - - @Operation(summary = "Answers the question", description = "Answers the question for the current game") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "400", description = "Not a valid answer", content = @io.swagger.v3.oas.annotations.media.Content), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameter(name = "id", description = "The id of the game to answer the question for", example = "1") - @PostMapping("/{id}/answer") - public ResponseEntity answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){ - return ResponseEntity.ok(gameService.answerQuestion(id, dto, authentication)); - } - - @Operation(summary = "Changing languages", description = "Changes the language of the game for a given authentication (a player) and a language supported. Changes may are applied on the next round.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "400", description = "Not a valid language to change to", content = @io.swagger.v3.oas.annotations.media.Content), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameter(name = "id", description = "The id of the game to change the language for", example = "1") - @PutMapping("/{id}/language") - public ResponseEntity changeLanguage(@PathVariable Long id, @RequestParam String language, Authentication authentication){ - return ResponseEntity.ok(gameService.changeLanguage(id, language, authentication)); - } - - @Operation(summary = "Get the summary of a game") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), - }) - @Parameter(name = "id", description = "The id of the game to get the summary for", example = "1") - @GetMapping("/{id}/details") - public ResponseEntity getGameDetails(@PathVariable Long id, Authentication authentication){ - return ResponseEntity.ok(gameService.getGameDetails(id, authentication)); - } - - @Operation(summary = "Get the list of gamemodes a game can have") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) - }) - @GetMapping("/gamemodes") - public ResponseEntity> getQuestionGameModes(){ - return ResponseEntity.ok(gameService.getQuestionGameModes()); - } - - @Operation(summary = "Get the list of categories a game can have") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) - }) - @GetMapping("/question-categories") - public ResponseEntity> getQuestionCategories(){ - return ResponseEntity.ok(gameService.getQuestionCategories()); - } - -} +package lab.en2b.quizapi.game; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lab.en2b.quizapi.game.dtos.*; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/games") +@RequiredArgsConstructor +public class GameController { + private final GameService gameService; + + @Operation(summary = "Starts new game", description = "Requests the API to create a new game for a given authentication (a player)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Given when: \n * language provided is not valid \n * gamemode provided is not valid \n * body is not provided with custom game", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameters({ + @Parameter(name = "lang", description = "The language of the game", example = "en"), + @Parameter(name = "gamemode", description = "The gamemode of the game", example = "KIWI_QUEST") + }) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") + @PostMapping("/play") + public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) @Valid CustomGameDto customGameDto, Authentication authentication){ + if(gamemode == GameMode.CUSTOM && customGameDto == null) + throw new IllegalArgumentException("Custom game mode requires a body"); + return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); + } + + @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameter(name = "id", description = "The id of the game to start the round for", example = "1") + @PostMapping("/{id}/startRound") + public ResponseEntity startRound(@PathVariable Long id, Authentication authentication){ + return ResponseEntity.ok(gameService.startRound(id, authentication)); + } + + @Operation(summary = "Gets the current question", description = "Gets the question and its possible answers from the API for a given authentication (a player)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameter(name = "id", description = "The id of the game to get the current question for", example = "1") + @GetMapping("/{id}/question") + public ResponseEntity getCurrentQuestion(@PathVariable Long id, Authentication authentication){ + return ResponseEntity.ok(gameService.getCurrentQuestion(id, authentication)); + } + + @Operation(summary = "Answers the question", description = "Answers the question for the current game") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Not a valid answer", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameter(name = "id", description = "The id of the game to answer the question for", example = "1") + @PostMapping("/{id}/answer") + public ResponseEntity answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){ + return ResponseEntity.ok(gameService.answerQuestion(id, dto, authentication)); + } + + @Operation(summary = "Changing languages", description = "Changes the language of the game for a given authentication (a player) and a language supported. Changes may are applied on the next round.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Not a valid language to change to", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameter(name = "id", description = "The id of the game to change the language for", example = "1") + @PutMapping("/{id}/language") + public ResponseEntity changeLanguage(@PathVariable Long id, @RequestParam String language, Authentication authentication){ + return ResponseEntity.ok(gameService.changeLanguage(id, language, authentication)); + } + + @Operation(summary = "Get the summary of a game") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), + }) + @Parameter(name = "id", description = "The id of the game to get the summary for", example = "1") + @GetMapping("/{id}/details") + public ResponseEntity getGameDetails(@PathVariable Long id, Authentication authentication){ + return ResponseEntity.ok(gameService.getGameDetails(id, authentication)); + } + + @Operation(summary = "Get the list of gamemodes a game can have") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) + }) + @GetMapping("/gamemodes") + public ResponseEntity> getQuestionGameModes(){ + return ResponseEntity.ok(gameService.getQuestionGameModes()); + } + + @Operation(summary = "Get the list of categories a game can have") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) + }) + @GetMapping("/question-categories") + public ResponseEntity> getQuestionCategories(){ + return ResponseEntity.ok(gameService.getQuestionCategories()); + } + +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameMode.java b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java index 60e6bf7f..d76cddb0 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameMode.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java @@ -1,12 +1,12 @@ -package lab.en2b.quizapi.game; - -public enum GameMode { - KIWI_QUEST, - FOOTBALL_SHOWDOWN, - GEO_GENIUS, - VIDEOGAME_ADVENTURE, - ANCIENT_ODYSSEY, - RANDOM, - CUSTOM -} - +package lab.en2b.quizapi.game; + +public enum GameMode { + KIWI_QUEST, + FOOTBALL_SHOWDOWN, + GEO_GENIUS, + VIDEOGAME_ADVENTURE, + ANCIENT_ODYSSEY, + RANDOM, + CUSTOM +} + diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 9b084ef0..f259813a 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -1,182 +1,182 @@ -package lab.en2b.quizapi.game; - -import lab.en2b.quizapi.commons.user.UserService; -import lab.en2b.quizapi.game.dtos.*; -import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; -import lab.en2b.quizapi.questions.question.QuestionCategory; -import lab.en2b.quizapi.questions.question.QuestionService; -import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; -import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; -import lab.en2b.quizapi.statistics.Statistics; -import lab.en2b.quizapi.statistics.StatisticsRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class GameService { - private final GameRepository gameRepository; - private final GameResponseDtoMapper gameResponseDtoMapper; - private final UserService userService; - private final QuestionService questionService; - private final QuestionResponseDtoMapper questionResponseDtoMapper; - private final StatisticsRepository statisticsRepository; - - /** - * Creates a new game for the user - * @param lang the language of the game, default is ENGLISH - * @param gamemode the gamemode of the game, default is KIWI_QUEST - * @param newGameDto the custom game dto, only required if the gamemode is CUSTOM - * @param authentication the authentication of the user - * @return the newly created game - */ - @Transactional - public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto newGameDto, Authentication authentication) { - // Check if there is an active game for the user - Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); - if (game.isPresent() && !wasGameMeantToBeOver(game.get())){ - // If there is an active game and it should not be over, return it - return gameResponseDtoMapper.apply(game.get()); - } - return gameResponseDtoMapper.apply(gameRepository.save( - new Game(userService.getUserByAuthentication(authentication),gamemode,lang,newGameDto) - )); - } - - /** - * Starts a new round for the game - * @param id the id of the game to start the round for - * @param authentication the authentication of the user - * @return the game with the new round started - */ - @Transactional - public GameResponseDto startRound(Long id, Authentication authentication) { - // Get the game by id and user - Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - // Check if the game should be over - wasGameMeantToBeOver(game); - // Start a new round - game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getQuestionCategoriesForGamemode())); - - return gameResponseDtoMapper.apply(gameRepository.save(game)); - } - - /** - * Gets the current question for the game - * @param id the id of the game to get the question for - * @param authentication the authentication of the user - * @return the current question - */ - public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentication){ - Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - return questionResponseDtoMapper.apply(game.getCurrentQuestion()); - } - - /** - * Answers the current question for the game - * @param id the id of the game to answer the question for - * @param dto the answer dto - * @param authentication the authentication of the user - * @return the response of the answer - */ - @Transactional - public AnswerGameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ - Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - // Check if the game should be over - wasGameMeantToBeOver(game); - // Answer the question - boolean wasCorrect = game.answerQuestion(dto.getAnswerId()); - // Check if the game is over after the answer - wasGameMeantToBeOver(game); - - return new AnswerGameResponseDto(wasCorrect); - } - - /** - * Saves the statistics of the game - * @param game the game to save the statistics for - */ - private void saveStatistics(Game game){ - Statistics statistics; - if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ - // If there are statistics for the user, update them - statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); - statistics.updateStatistics(game.getCorrectlyAnsweredQuestions(), - game.getQuestions().size()-game.getCorrectlyAnsweredQuestions(), - game.getRounds()); - } else { - // If there are no statistics for the user, create new ones - statistics = Statistics.builder() - .user(game.getUser()) - .correct(game.getCorrectlyAnsweredQuestions()) - .wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()) - .total(game.getRounds()) - .build(); - } - statisticsRepository.save(statistics); - } - - /** - * Changes the language of the game. The game language will only change after the next round. - * @param id the id of the game to change the language for - * @param language the language to change to - * @param authentication the authentication of the user - * @return the game with the new language - */ - public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) { - Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - if(game.isGameOver()){ - throw new IllegalStateException("Cannot change language after the game is over!"); - } - game.setLanguage(language); - return gameResponseDtoMapper.apply(gameRepository.save(game)); - } - - /** - * Gets the game details - * @param id the id of the game to get the details for - * @param authentication the authentication of the user - * @return the game details - */ - public GameResponseDto getGameDetails(Long id, Authentication authentication) { - Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - wasGameMeantToBeOver(game); - return gameResponseDtoMapper.apply(game); - } - - public List getQuestionCategories() { - return Arrays.asList(QuestionCategory.values()); - } - - private boolean wasGameMeantToBeOver(Game game) { - if (game.shouldBeGameOver()){ - game.setGameOver(true); - gameRepository.save(game); - saveStatistics(game); - return true; - } - return false; - } - - /** - * Gets the list of gamemodes a game can have - * @return the list of gamemodes - */ - public List getQuestionGameModes() { - return List.of( - new GameModeDto("Kiwi Quest","Our curated selection of the most exquisite questions. Enjoy with a glass of wine",GameMode.KIWI_QUEST,"FaKiwiBird"), - new GameModeDto("Football Showdown","Like sports? Like balls? This gamemode is for you!",GameMode.FOOTBALL_SHOWDOWN,"IoIosFootball"), - new GameModeDto("Geo Genius","Do you know the capital of Mongolia? I don't, so if you do this game is for you!",GameMode.GEO_GENIUS,"FaGlobeAmericas"), - new GameModeDto("Videogame Adventure","It's dangerous to go alone, guess this!",GameMode.VIDEOGAME_ADVENTURE,"IoLogoGameControllerB"), - new GameModeDto("Ancient Odyssey","Antiques are pricey for a reason!",GameMode.ANCIENT_ODYSSEY,"FaPalette"), - new GameModeDto("Random","Try a bit of everything!",GameMode.RANDOM,"FaRandom"), - new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") - ); - } -} +package lab.en2b.quizapi.game; + +import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.dtos.*; +import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lab.en2b.quizapi.questions.question.QuestionService; +import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; +import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import lab.en2b.quizapi.statistics.Statistics; +import lab.en2b.quizapi.statistics.StatisticsRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class GameService { + private final GameRepository gameRepository; + private final GameResponseDtoMapper gameResponseDtoMapper; + private final UserService userService; + private final QuestionService questionService; + private final QuestionResponseDtoMapper questionResponseDtoMapper; + private final StatisticsRepository statisticsRepository; + + /** + * Creates a new game for the user + * @param lang the language of the game, default is ENGLISH + * @param gamemode the gamemode of the game, default is KIWI_QUEST + * @param newGameDto the custom game dto, only required if the gamemode is CUSTOM + * @param authentication the authentication of the user + * @return the newly created game + */ + @Transactional + public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto newGameDto, Authentication authentication) { + // Check if there is an active game for the user + Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); + if (game.isPresent() && !wasGameMeantToBeOver(game.get())){ + // If there is an active game and it should not be over, return it + return gameResponseDtoMapper.apply(game.get()); + } + return gameResponseDtoMapper.apply(gameRepository.save( + new Game(userService.getUserByAuthentication(authentication),gamemode,lang,newGameDto) + )); + } + + /** + * Starts a new round for the game + * @param id the id of the game to start the round for + * @param authentication the authentication of the user + * @return the game with the new round started + */ + @Transactional + public GameResponseDto startRound(Long id, Authentication authentication) { + // Get the game by id and user + Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + // Check if the game should be over + wasGameMeantToBeOver(game); + // Start a new round + game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getQuestionCategoriesForGamemode())); + + return gameResponseDtoMapper.apply(gameRepository.save(game)); + } + + /** + * Gets the current question for the game + * @param id the id of the game to get the question for + * @param authentication the authentication of the user + * @return the current question + */ + public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentication){ + Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + return questionResponseDtoMapper.apply(game.getCurrentQuestion()); + } + + /** + * Answers the current question for the game + * @param id the id of the game to answer the question for + * @param dto the answer dto + * @param authentication the authentication of the user + * @return the response of the answer + */ + @Transactional + public AnswerGameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ + Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + // Check if the game should be over + wasGameMeantToBeOver(game); + // Answer the question + boolean wasCorrect = game.answerQuestion(dto.getAnswerId()); + // Check if the game is over after the answer + wasGameMeantToBeOver(game); + + return new AnswerGameResponseDto(wasCorrect); + } + + /** + * Saves the statistics of the game + * @param game the game to save the statistics for + */ + private void saveStatistics(Game game){ + Statistics statistics; + if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ + // If there are statistics for the user, update them + statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); + statistics.updateStatistics(game.getCorrectlyAnsweredQuestions(), + game.getQuestions().size()-game.getCorrectlyAnsweredQuestions(), + game.getRounds()); + } else { + // If there are no statistics for the user, create new ones + statistics = Statistics.builder() + .user(game.getUser()) + .correct(game.getCorrectlyAnsweredQuestions()) + .wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()) + .total(game.getRounds()) + .build(); + } + statisticsRepository.save(statistics); + } + + /** + * Changes the language of the game. The game language will only change after the next round. + * @param id the id of the game to change the language for + * @param language the language to change to + * @param authentication the authentication of the user + * @return the game with the new language + */ + public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) { + Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + if(game.isGameOver()){ + throw new IllegalStateException("Cannot change language after the game is over!"); + } + game.setLanguage(language); + return gameResponseDtoMapper.apply(gameRepository.save(game)); + } + + /** + * Gets the game details + * @param id the id of the game to get the details for + * @param authentication the authentication of the user + * @return the game details + */ + public GameResponseDto getGameDetails(Long id, Authentication authentication) { + Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + wasGameMeantToBeOver(game); + return gameResponseDtoMapper.apply(game); + } + + public List getQuestionCategories() { + return Arrays.asList(QuestionCategory.values()); + } + + private boolean wasGameMeantToBeOver(Game game) { + if (game.shouldBeGameOver()){ + game.setGameOver(true); + gameRepository.save(game); + saveStatistics(game); + return true; + } + return false; + } + + /** + * Gets the list of gamemodes a game can have + * @return the list of gamemodes + */ + public List getQuestionGameModes() { + return List.of( + new GameModeDto("Kiwi Quest","Our curated selection of the most exquisite questions. Enjoy with a glass of wine",GameMode.KIWI_QUEST,"FaKiwiBird"), + new GameModeDto("Football Showdown","Like sports? Like balls? This gamemode is for you!",GameMode.FOOTBALL_SHOWDOWN,"IoIosFootball"), + new GameModeDto("Geo Genius","Do you know the capital of Mongolia? I don't, so if you do this game is for you!",GameMode.GEO_GENIUS,"FaGlobeAmericas"), + new GameModeDto("Videogame Adventure","It's dangerous to go alone, guess this!",GameMode.VIDEOGAME_ADVENTURE,"IoLogoGameControllerB"), + new GameModeDto("Ancient Odyssey","Antiques are pricey for a reason!",GameMode.ANCIENT_ODYSSEY,"FaPalette"), + new GameModeDto("Random","Try a bit of everything!",GameMode.RANDOM,"FaRandom"), + new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") + ); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java index 4dd9fa22..638f17ac 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java @@ -1,33 +1,33 @@ -package lab.en2b.quizapi.game.dtos; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import lab.en2b.quizapi.questions.question.QuestionCategory; -import lombok.*; - -import java.util.List; - -@Getter -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Setter -public class CustomGameDto { - @Positive - @NotNull - @NonNull - @Schema(description = "Number of rounds for the custom game",example = "9") - private Long rounds; - @Positive - @NotNull - @NonNull - @JsonProperty("round_duration") - @Schema(description = "Duration of the round in seconds",example = "30") - private Integer roundDuration; - @NotNull - @NonNull - @Schema(description = "Categories selected for questions",example = "[\"HISTORY\",\"SCIENCE\"]") - private List categories; -} +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lombok.*; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class CustomGameDto { + @Positive + @NotNull + @NonNull + @Schema(description = "Number of rounds for the custom game",example = "9") + private Long rounds; + @Positive + @NotNull + @NonNull + @JsonProperty("round_duration") + @Schema(description = "Duration of the round in seconds",example = "30") + private Integer roundDuration; + @NotNull + @NonNull + @Schema(description = "Categories selected for questions",example = "[\"HISTORY\",\"SCIENCE\"]") + private List categories; +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java index 82550eba..b6e75cbc 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java @@ -1,24 +1,24 @@ -package lab.en2b.quizapi.game.dtos; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import lab.en2b.quizapi.game.GameMode; -import lombok.*; - -@Getter -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Setter -public class GameModeDto { - @Schema(description = "Beautified name of the game mode",example = "Quiwi Quest") - private String name; - @Schema(description = "Description of the game mode",example = "Test description of the game mode") - private String description; - @JsonProperty("internal_representation") - @Schema(description = "Internal code used for describing the game mode",example = "KIWI_QUEST") - private GameMode internalRepresentation; - @JsonProperty("icon_name") - @Schema(description = "Code for the icon used in the frontend of the application",example = "FaKiwiBird") - private String iconName; -} +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lab.en2b.quizapi.game.GameMode; +import lombok.*; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class GameModeDto { + @Schema(description = "Beautified name of the game mode",example = "Quiwi Quest") + private String name; + @Schema(description = "Description of the game mode",example = "Test description of the game mode") + private String description; + @JsonProperty("internal_representation") + @Schema(description = "Internal code used for describing the game mode",example = "KIWI_QUEST") + private GameMode internalRepresentation; + @JsonProperty("icon_name") + @Schema(description = "Code for the icon used in the frontend of the application",example = "FaKiwiBird") + private String iconName; +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index 57a63abc..2316cd23 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -1,50 +1,50 @@ -package lab.en2b.quizapi.game.dtos; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import lab.en2b.quizapi.commons.user.UserResponseDto; -import lab.en2b.quizapi.game.GameMode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; -import java.time.OffsetDateTime; - -@AllArgsConstructor -@Data -@Builder -@EqualsAndHashCode -public class GameResponseDto { - @Schema(description = "Identification for a game", example = "23483743") - private Long id; - - @Schema(description = "User for the game", example = "{\"id\":1,\"username\":\"Hordi Jurtado\",\"email\":\"chipiChipi@chapaChapa.es \"}") - private UserResponseDto user; - - @Schema(description = "Total rounds for the game", example = "9") - private Long rounds; - - @Schema(description = "Actual round for the game", example = "3") - @JsonProperty("actual_round") - private Long actualRound; - - @Schema(description = "Number of correct answered questions", example = "2") - @JsonProperty("correctly_answered_questions") - private Long correctlyAnsweredQuestions; - - @Schema(description = "Moment when the timer has started", example = "LocalDateTime.now()") - @JsonProperty("round_start_time") - private String roundStartTime; - - @Schema(description = "Number of seconds for the player to answer the question", example = "20") - @JsonProperty("round_duration") - private Integer roundDuration; - - @Schema(description = "Whether the game has finished or not", example = "true") - private boolean isGameOver; - - @Schema(description = "Game mode for the game", example = "KIWI_QUEST") - private GameMode gamemode; -} +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.game.GameMode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +@AllArgsConstructor +@Data +@Builder +@EqualsAndHashCode +public class GameResponseDto { + @Schema(description = "Identification for a game", example = "23483743") + private Long id; + + @Schema(description = "User for the game", example = "{\"id\":1,\"username\":\"Hordi Jurtado\",\"email\":\"chipiChipi@chapaChapa.es \"}") + private UserResponseDto user; + + @Schema(description = "Total rounds for the game", example = "9") + private Long rounds; + + @Schema(description = "Actual round for the game", example = "3") + @JsonProperty("actual_round") + private Long actualRound; + + @Schema(description = "Number of correct answered questions", example = "2") + @JsonProperty("correctly_answered_questions") + private Long correctlyAnsweredQuestions; + + @Schema(description = "Moment when the timer has started", example = "LocalDateTime.now()") + @JsonProperty("round_start_time") + private String roundStartTime; + + @Schema(description = "Number of seconds for the player to answer the question", example = "20") + @JsonProperty("round_duration") + private Integer roundDuration; + + @Schema(description = "Whether the game has finished or not", example = "true") + private boolean isGameOver; + + @Schema(description = "Game mode for the game", example = "KIWI_QUEST") + private GameMode gamemode; +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java index 3fbc0b0f..58c49036 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java @@ -1,30 +1,30 @@ -package lab.en2b.quizapi.game.mappers; - -import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; -import lab.en2b.quizapi.game.Game; -import lab.en2b.quizapi.game.dtos.GameResponseDto; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.function.Function; - -@Service -@RequiredArgsConstructor -public class GameResponseDtoMapper implements Function{ - private final UserResponseDtoMapper userResponseDtoMapper; - @Override - public GameResponseDto apply(Game game) { - return GameResponseDto.builder() - .id(game.getId()) - .user(userResponseDtoMapper.apply(game.getUser())) - .rounds(game.getRounds()) - .correctlyAnsweredQuestions(game.getCorrectlyAnsweredQuestions()) - .actualRound(game.getActualRound()) - .roundDuration(game.getRoundDuration()) - .roundStartTime(game.getRoundStartTime() != null? Instant.ofEpochMilli(game.getRoundStartTime()).toString(): null) - .gamemode(game.getGamemode()) - .isGameOver(game.isGameOver()) - .build(); - } -} +package lab.en2b.quizapi.game.mappers; + +import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; +import lab.en2b.quizapi.game.Game; +import lab.en2b.quizapi.game.dtos.GameResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.function.Function; + +@Service +@RequiredArgsConstructor +public class GameResponseDtoMapper implements Function{ + private final UserResponseDtoMapper userResponseDtoMapper; + @Override + public GameResponseDto apply(Game game) { + return GameResponseDto.builder() + .id(game.getId()) + .user(userResponseDtoMapper.apply(game.getUser())) + .rounds(game.getRounds()) + .correctlyAnsweredQuestions(game.getCorrectlyAnsweredQuestions()) + .actualRound(game.getActualRound()) + .roundDuration(game.getRoundDuration()) + .roundStartTime(game.getRoundStartTime() != null? Instant.ofEpochMilli(game.getRoundStartTime()).toString(): null) + .gamemode(game.getGamemode()) + .isGameOver(game.isGameOver()) + .build(); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java index fd8e2eca..98688045 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java @@ -1,13 +1,13 @@ -package lab.en2b.quizapi.questions.question; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import java.util.List; - -public interface QuestionRepository extends JpaRepository { - @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 " + - "AND q.question_category IN ?2 " + - " ORDER BY RANDOM() LIMIT 1 ", nativeQuery = true) - Question findRandomQuestion(String lang, List questionCategories); -} +package lab.en2b.quizapi.questions.question; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface QuestionRepository extends JpaRepository { + @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 " + + "AND q.question_category IN ?2 " + + " ORDER BY RANDOM() LIMIT 1 ", nativeQuery = true) + Question findRandomQuestion(String lang, List questionCategories); +} diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 47a8e549..8d72f0fe 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -1,92 +1,92 @@ -package lab.en2b.quizapi.questions.question; - -import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; -import lab.en2b.quizapi.questions.answer.Answer; -import lab.en2b.quizapi.questions.answer.AnswerRepository; -import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; -import lab.en2b.quizapi.questions.question.dtos.AnswerCheckResponseDto; -import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; -import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class QuestionService { - - private final AnswerRepository answerRepository; - private final QuestionRepository questionRepository; - private final QuestionResponseDtoMapper questionResponseDtoMapper; - - /** - * Answer a question - * @param id The id of the question - * @param answerDto The answer dto - * @return The response dto - */ - public AnswerCheckResponseDto answerQuestion(Long id, AnswerDto answerDto) { - Question question = questionRepository.findById(id).orElseThrow(); - if(question.getCorrectAnswer().getId().equals(answerDto.getAnswerId())){ - return new AnswerCheckResponseDto(true); - } - else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto.getAnswerId()))){ - throw new IllegalArgumentException("The answer you provided is not one of the options"); - } - else { - return new AnswerCheckResponseDto(false); - } - } - - public QuestionResponseDto getRandomQuestion(String lang) { - return questionResponseDtoMapper.apply(findRandomQuestion(lang, List.of(QuestionCategory.values()))); - } - - /** - * Find a random question for the specified language - * @param language The language to find the question for - * @return The random question - */ - - public Question findRandomQuestion(String language, List questionCategoriesForCustom) { - if (language==null || language.isBlank()) { - language = "en"; - } - Question q = questionRepository.findRandomQuestion(language,questionCategoriesForCustom.stream().map(Enum::toString).toList()); - if(q==null) { - throw new InternalApiErrorException("No questions found for the specified language!"); - } - loadAnswers(q); - return q; - } - - public QuestionResponseDto getQuestionById(Long id) { - return questionResponseDtoMapper.apply(questionRepository.findById(id).orElseThrow()); - } - - - /** - * Load the answers for a question (The distractors and the correct one) - * @param question The question to load the answers for - */ - //TODO: CHAPUZAS, FIXEAR ESTO - private void loadAnswers(Question question) { - // Create the new answers list with the distractors - if(question.getAnswers().size() > 1) { - return; - } - List answers = new ArrayList<>(QuestionHelper.getDistractors(answerRepository, question)); - // Add the correct - answers.add(question.getCorrectAnswer()); - - // Shuffle the answers - Collections.shuffle(answers); - - question.setAnswers(answers); - questionRepository.save(question); - } - -} +package lab.en2b.quizapi.questions.question; + +import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; +import lab.en2b.quizapi.questions.answer.Answer; +import lab.en2b.quizapi.questions.answer.AnswerRepository; +import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; +import lab.en2b.quizapi.questions.question.dtos.AnswerCheckResponseDto; +import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; +import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class QuestionService { + + private final AnswerRepository answerRepository; + private final QuestionRepository questionRepository; + private final QuestionResponseDtoMapper questionResponseDtoMapper; + + /** + * Answer a question + * @param id The id of the question + * @param answerDto The answer dto + * @return The response dto + */ + public AnswerCheckResponseDto answerQuestion(Long id, AnswerDto answerDto) { + Question question = questionRepository.findById(id).orElseThrow(); + if(question.getCorrectAnswer().getId().equals(answerDto.getAnswerId())){ + return new AnswerCheckResponseDto(true); + } + else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto.getAnswerId()))){ + throw new IllegalArgumentException("The answer you provided is not one of the options"); + } + else { + return new AnswerCheckResponseDto(false); + } + } + + public QuestionResponseDto getRandomQuestion(String lang) { + return questionResponseDtoMapper.apply(findRandomQuestion(lang, List.of(QuestionCategory.values()))); + } + + /** + * Find a random question for the specified language + * @param language The language to find the question for + * @return The random question + */ + + public Question findRandomQuestion(String language, List questionCategoriesForCustom) { + if (language==null || language.isBlank()) { + language = "en"; + } + Question q = questionRepository.findRandomQuestion(language,questionCategoriesForCustom.stream().map(Enum::toString).toList()); + if(q==null) { + throw new InternalApiErrorException("No questions found for the specified language!"); + } + loadAnswers(q); + return q; + } + + public QuestionResponseDto getQuestionById(Long id) { + return questionResponseDtoMapper.apply(questionRepository.findById(id).orElseThrow()); + } + + + /** + * Load the answers for a question (The distractors and the correct one) + * @param question The question to load the answers for + */ + //TODO: CHAPUZAS, FIXEAR ESTO + private void loadAnswers(Question question) { + // Create the new answers list with the distractors + if(question.getAnswers().size() > 1) { + return; + } + List answers = new ArrayList<>(QuestionHelper.getDistractors(answerRepository, question)); + // Add the correct + answers.add(question.getCorrectAnswer()); + + // Shuffle the answers + Collections.shuffle(answers); + + question.setAnswers(answers); + questionRepository.save(question); + } + +} diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index d570e709..91462004 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -1,223 +1,223 @@ -package lab.en2b.quizapi.game; - -import lab.en2b.quizapi.auth.config.SecurityConfig; -import lab.en2b.quizapi.auth.jwt.JwtUtils; -import lab.en2b.quizapi.commons.user.UserService; -import lab.en2b.quizapi.game.dtos.CustomGameDto; -import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.test.web.servlet.MockMvc; - -import static lab.en2b.quizapi.commons.utils.TestUtils.asJsonString; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(GameController.class) -@AutoConfigureMockMvc -@Import(SecurityConfig.class) -public class GameControllerTest { - - @Autowired - MockMvc mockMvc; - - @MockBean - JwtUtils jwtUtils; - - @MockBean - UserService userService; - - @MockBean - GameService gameService; - - @Test - void newQuestionShouldReturn403() throws Exception{ - mockMvc.perform(post("/games/play") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void newGameShouldReturn200() throws Exception{ - mockMvc.perform(post("/games/play") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - @Test - void newGameCustomNoBodyShouldReturn400() throws Exception{ - mockMvc.perform(post("/games/play?gamemode=CUSTOM") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isBadRequest()); - } - @Test - void newGameInvalidBodyForCustomShouldReturn400() throws Exception{ - mockMvc.perform(post("/games/play?gamemode=CUSTOM") - .with(user("test").roles("user")) - .content(asJsonString(new CustomGameDto())) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isBadRequest()); - } - @Test - void newGameInvalidGameModeShouldReturn400() throws Exception{ - mockMvc.perform(post("/games/play?gamemode=patata") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isBadRequest()); - } - - @Test - void startRoundShouldReturn403() throws Exception{ - mockMvc.perform(post("/games/1/startRound") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void startRoundShouldReturn200() throws Exception{ - mockMvc.perform(post("/games/1/startRound") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void getCurrentQuestionShouldReturn403() throws Exception{ - mockMvc.perform(get("/games/1/question") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void getCurrentQuestionShouldReturn200() throws Exception{ - mockMvc.perform(get("/games/1/question") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void answerQuestionShouldReturn403() throws Exception{ - mockMvc.perform(post("/games/1/answer") - .contentType("application/json") - .content(asJsonString(new GameAnswerDto(1L))) - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void answerQuestionShouldReturn200() throws Exception{ - mockMvc.perform(post("/games/1/answer") - .content(asJsonString(new GameAnswerDto(1L))) - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void answerQuestionShouldReturn400() throws Exception{ - mockMvc.perform(post("/games/1/answer") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isBadRequest()); - } - - @Test - void changeLanguageShouldReturn403() throws Exception{ - mockMvc.perform(put("/games/1/language?language=en") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void changeLanguageShouldReturn200() throws Exception{ - mockMvc.perform(put("/games/1/language?language=en") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void changeLanguageShouldReturn400() throws Exception{ - mockMvc.perform(put("/games/1/language") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isBadRequest()); - } - - @Test - void getGameDetailsShouldReturn403() throws Exception{ - mockMvc.perform(get("/games/1/details") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void getGameDetailsShouldReturn200() throws Exception{ - mockMvc.perform(get("/games/1/details") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void getQuestionCategoriesShouldReturn200() throws Exception{ - mockMvc.perform(get("/games/question-categories") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void getQuestionCategoriesShouldReturn403() throws Exception{ - mockMvc.perform(get("/games/question-categories") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - void getGameModeshouldReturn200() throws Exception{ - mockMvc.perform(get("/games/gamemodes") - .with(user("test").roles("user")) - .contentType("application/json") - .with(csrf())) - .andExpect(status().isOk()); - } - - @Test - void getGameModesShouldReturn403() throws Exception{ - mockMvc.perform(get("/games/gamemodes") - .contentType("application/json") - .with(csrf())) - .andExpect(status().isForbidden()); - } - - -} +package lab.en2b.quizapi.game; + +import lab.en2b.quizapi.auth.config.SecurityConfig; +import lab.en2b.quizapi.auth.jwt.JwtUtils; +import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.dtos.CustomGameDto; +import lab.en2b.quizapi.game.dtos.GameAnswerDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +import static lab.en2b.quizapi.commons.utils.TestUtils.asJsonString; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(GameController.class) +@AutoConfigureMockMvc +@Import(SecurityConfig.class) +public class GameControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + JwtUtils jwtUtils; + + @MockBean + UserService userService; + + @MockBean + GameService gameService; + + @Test + void newQuestionShouldReturn403() throws Exception{ + mockMvc.perform(post("/games/play") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void newGameShouldReturn200() throws Exception{ + mockMvc.perform(post("/games/play") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + @Test + void newGameCustomNoBodyShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=CUSTOM") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + @Test + void newGameInvalidBodyForCustomShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=CUSTOM") + .with(user("test").roles("user")) + .content(asJsonString(new CustomGameDto())) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + @Test + void newGameInvalidGameModeShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=patata") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + + @Test + void startRoundShouldReturn403() throws Exception{ + mockMvc.perform(post("/games/1/startRound") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void startRoundShouldReturn200() throws Exception{ + mockMvc.perform(post("/games/1/startRound") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getCurrentQuestionShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/1/question") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void getCurrentQuestionShouldReturn200() throws Exception{ + mockMvc.perform(get("/games/1/question") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void answerQuestionShouldReturn403() throws Exception{ + mockMvc.perform(post("/games/1/answer") + .contentType("application/json") + .content(asJsonString(new GameAnswerDto(1L))) + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void answerQuestionShouldReturn200() throws Exception{ + mockMvc.perform(post("/games/1/answer") + .content(asJsonString(new GameAnswerDto(1L))) + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void answerQuestionShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/1/answer") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + + @Test + void changeLanguageShouldReturn403() throws Exception{ + mockMvc.perform(put("/games/1/language?language=en") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void changeLanguageShouldReturn200() throws Exception{ + mockMvc.perform(put("/games/1/language?language=en") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void changeLanguageShouldReturn400() throws Exception{ + mockMvc.perform(put("/games/1/language") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + + @Test + void getGameDetailsShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/1/details") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void getGameDetailsShouldReturn200() throws Exception{ + mockMvc.perform(get("/games/1/details") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getQuestionCategoriesShouldReturn200() throws Exception{ + mockMvc.perform(get("/games/question-categories") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getQuestionCategoriesShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/question-categories") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void getGameModeshouldReturn200() throws Exception{ + mockMvc.perform(get("/games/gamemodes") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getGameModesShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/gamemodes") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + +} diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 4c8f0cb3..5ad610d7 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -1,436 +1,436 @@ -package lab.en2b.quizapi.game; - -import ch.qos.logback.core.util.TimeUtil; -import lab.en2b.quizapi.commons.user.User; -import lab.en2b.quizapi.commons.user.UserResponseDto; -import lab.en2b.quizapi.commons.user.UserService; -import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; -import lab.en2b.quizapi.game.dtos.CustomGameDto; -import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.game.dtos.GameResponseDto; -import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; -import lab.en2b.quizapi.questions.answer.Answer; -import lab.en2b.quizapi.questions.answer.AnswerCategory; -import lab.en2b.quizapi.questions.answer.mappers.AnswerResponseDtoMapper; -import lab.en2b.quizapi.questions.question.*; -import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; -import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; -import lab.en2b.quizapi.statistics.Statistics; -import lab.en2b.quizapi.statistics.StatisticsRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.core.Authentication; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith({MockitoExtension.class, SpringExtension.class}) -public class GameServiceTest { - - @InjectMocks - private GameService gameService; - - @Mock - private UserService userService; - - @Mock - private GameRepository gameRepository; - - @Mock - private StatisticsRepository statisticsRepository; - - @Mock - private QuestionService questionService; - - private User defaultUser; - private Question defaultQuestion; - private QuestionResponseDto defaultQuestionResponseDto; - private GameResponseDto defaultGameResponseDto; - - private UserResponseDto defaultUserResponseDto; - - private QuestionResponseDtoMapper questionResponseDtoMapper; - - @Mock - private Authentication authentication; - - private Game defaultGame; - - private Answer defaultCorrectAnswer; - - @BeforeEach - void setUp() { - this.questionResponseDtoMapper = new QuestionResponseDtoMapper(); - this.gameService = new GameService(gameRepository,new GameResponseDtoMapper(new UserResponseDtoMapper()), userService, questionService, questionResponseDtoMapper, statisticsRepository); - this.defaultUser = User.builder() - .id(1L) - .email("test@email.com") - .username("test") - .role("user") - .password("password") - .refreshToken("token") - .refreshExpiration(Instant.ofEpochSecond(TimeUtil.computeStartOfNextSecond(System.currentTimeMillis()+ 1000))) - .build(); - - this.defaultQuestion = Question.builder() - .id(1L) - .content("What is the capital of France?") - .answers(new ArrayList<>()) - .questionCategory(QuestionCategory.GEOGRAPHY) - .type(QuestionType.TEXT) - .build(); - - defaultCorrectAnswer = Answer.builder() - .id(1L) - .text("Paris") - .category(AnswerCategory.CAPITAL_CITY) - .questions(List.of(defaultQuestion)) - .questionsWithThisAnswer(List.of(defaultQuestion)) - .language("en") - .build(); - - Answer defaultIncorrectAnswer = Answer.builder() - .id(2L) - .text("Tokio") - .category(AnswerCategory.CAPITAL_CITY) - .questions(List.of(defaultQuestion)) - .questionsWithThisAnswer(List.of(defaultQuestion)) - .build(); - - defaultQuestion.setCorrectAnswer(defaultCorrectAnswer); - defaultQuestion.getAnswers().add(defaultCorrectAnswer); - defaultQuestion.getAnswers().add(defaultIncorrectAnswer); - - this.defaultUserResponseDto = UserResponseDto.builder() - .id(1L) - .email("test@email.com") - .username("test") - .build(); - - this.defaultQuestionResponseDto = QuestionResponseDto.builder() - .id(1L) - .content("What is the capital of France?") - .answers(new ArrayList<>()) - .language("en") - .questionCategory(QuestionCategory.GEOGRAPHY) - .answerCategory(AnswerCategory.CAPITAL_CITY) - .type(QuestionType.TEXT) - .build(); - defaultQuestionResponseDto.getAnswers().add(new AnswerResponseDtoMapper().apply(defaultCorrectAnswer)); - defaultQuestionResponseDto.getAnswers().add(new AnswerResponseDtoMapper().apply(defaultIncorrectAnswer)); - LocalDateTime now = LocalDateTime.now(); - this.defaultGameResponseDto = GameResponseDto.builder() - .user(defaultUserResponseDto) - .rounds(9L) - .correctlyAnsweredQuestions(0L) - .roundStartTime(Instant.ofEpochSecond(0L).toString()) - .actualRound(0L) - .roundDuration(30) - .gamemode(GameMode.KIWI_QUEST) - .build(); - this.defaultGame = Game.builder() - .id(1L) - .user(defaultUser) - .questions(new ArrayList<>()) - .rounds(9L) - .actualRound(0L) - .roundStartTime(0L) - .gamemode(GameMode.KIWI_QUEST) - .correctlyAnsweredQuestions(0L) - .language("en") - .roundDuration(30) - .build(); - } - - // NEW GAME TESTS - @Test - public void newGame(){ - Authentication authentication = mock(Authentication.class); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); - assertEquals(defaultGameResponseDto, gameDto); - } - - @Test - public void newGameActiveGame(){ - Authentication authentication = mock(Authentication.class); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); - GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); - defaultGameResponseDto.setId(1L); - assertEquals(defaultGameResponseDto, gameDto); - } - - @Test - public void newGameWasMeantToBeOver(){ - Authentication authentication = mock(Authentication.class); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - defaultGame.setActualRound(10L); - gameService.newGame(null,null,null,authentication); - verify(statisticsRepository, times(1)).save(any()); - } - - @Test - public void newGameWasMeantToBeOverExistingLeaderboard(){ - Authentication authentication = mock(Authentication.class); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); - when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(Statistics.builder().user(new User()) - .correct(0L) - .wrong(0L) - .total(0L) - .build())); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - defaultGame.setActualRound(10L); - gameService.newGame(null,null,null,authentication); - verify(statisticsRepository, times(1)).save(any()); - } - - @Test - public void newGameCustomGame(){ - Authentication authentication = mock(Authentication.class); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - GameResponseDto gameDto = gameService.newGame("es",GameMode.CUSTOM, - CustomGameDto.builder() - .roundDuration(30) - .categories(List.of(QuestionCategory.GEOGRAPHY)) - .rounds(10L) - .build() - ,authentication); - defaultGameResponseDto.setGamemode(GameMode.CUSTOM); - defaultGameResponseDto.setRounds(10L); - defaultGameResponseDto.setRoundDuration(30); - - assertEquals(defaultGameResponseDto, gameDto); - } - - // START ROUND TESTS - @Test - public void startRound(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - GameResponseDto gameDto = gameService.startRound(1L, authentication); - GameResponseDto result = defaultGameResponseDto; - result.setActualRound(1L); - result.setId(1L); - result.setRoundStartTime(Instant.ofEpochMilli(defaultGame.getRoundStartTime()).toString()); - assertEquals(result, gameDto); - } - - @Test - public void startRoundGameOver(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - defaultGame.setActualRound(10L); - assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); - } - - @Test - public void startRoundWhenRoundNotFinished(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.startRound(1L,authentication); - assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); - } - - @Test - public void getCurrentQuestion() { - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.startRound(1L,authentication); - QuestionResponseDto questionDto = gameService.getCurrentQuestion(1L,authentication); - assertEquals(defaultQuestionResponseDto, questionDto); - } - - @Test - public void getCurrentQuestionRoundNotStarted() { - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); - } - - @Test - public void getCurrentQuestionRoundFinished() { - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.startRound(1L,authentication); - defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); - assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); - } - - @Test - public void getCurrentQuestionGameFinished() { - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.startRound(1L,authentication); - defaultGame.setGameOver(true); - defaultGame.setActualRound(10L); - assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); - } - - @Test - public void answerQuestionCorrectly(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); - gameService.getGameDetails(1L, authentication); - assertEquals(defaultGame.getCorrectlyAnsweredQuestions(), 1); - assertTrue(defaultGame.isCurrentQuestionAnswered()); - } - - @Test - public void answerQuestionIncorrectly(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - gameService.answerQuestion(1L, new GameAnswerDto(2L), authentication); - gameService.getGameDetails(1L, authentication); - assertEquals(defaultGame.getCorrectlyAnsweredQuestions(), 0); - assertTrue(defaultGame.isCurrentQuestionAnswered()); - } - - @Test - public void answerQuestionWhenGameHasFinished(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - defaultGame.setGameOver(true); - defaultGame.setActualRound(30L); - assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); - } - - @Test - public void answerQuestionLastRound(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.newGame(null,null,null,authentication); - defaultGame.setActualRound(8L); - gameService.startRound(1L, authentication); - gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); - verify(statisticsRepository, times(1)).save(any()); - } - - @Test - public void answerQuestionWhenRoundHasFinished(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); - assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); - } - - @Test - public void answerQuestionInvalidId(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - assertThrows(IllegalArgumentException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(3L), authentication)); - } - - @Test - public void changeLanguage(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - gameService.changeLanguage(1L, "es", authentication); - gameService.getGameDetails(1L, authentication); - assertEquals("es",defaultGame.getLanguage()); - } - - @Test - public void changeLanguageGameOver(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - defaultGame.setGameOver(true); - defaultGame.setActualRound(10L); - assertThrows(IllegalStateException.class,() -> gameService.changeLanguage(1L, "es", authentication)); - - } - - @Test - public void changeLanguageInvalidLanguage(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(null,null,null,authentication); - assertThrows(IllegalArgumentException.class, () -> gameService.changeLanguage(1L, "patata", authentication)); - } - - @Test - public void getGameDetails(){ - when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - - GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - gameService.getGameDetails(1L, authentication); - - assertEquals(defaultGameResponseDto, gameDto); - } - - @Test - public void getGameDetailsInvalidId(){ - when(gameRepository.findByIdForUser(1L, 1L)).thenReturn(Optional.of(defaultGame)); - when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - gameService.newGame(null,null,null,authentication); - gameService.startRound(1L, authentication); - assertThrows(NoSuchElementException.class, () -> gameService.getGameDetails(2L, authentication)); - } - - @Test - public void testGetQuestionCategories(){ - assertEquals(Arrays.asList(QuestionCategory.values()), gameService.getQuestionCategories()); - } - -} +package lab.en2b.quizapi.game; + +import ch.qos.logback.core.util.TimeUtil; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; +import lab.en2b.quizapi.game.dtos.CustomGameDto; +import lab.en2b.quizapi.game.dtos.GameAnswerDto; +import lab.en2b.quizapi.game.dtos.GameResponseDto; +import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; +import lab.en2b.quizapi.questions.answer.Answer; +import lab.en2b.quizapi.questions.answer.AnswerCategory; +import lab.en2b.quizapi.questions.answer.mappers.AnswerResponseDtoMapper; +import lab.en2b.quizapi.questions.question.*; +import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; +import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import lab.en2b.quizapi.statistics.Statistics; +import lab.en2b.quizapi.statistics.StatisticsRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith({MockitoExtension.class, SpringExtension.class}) +public class GameServiceTest { + + @InjectMocks + private GameService gameService; + + @Mock + private UserService userService; + + @Mock + private GameRepository gameRepository; + + @Mock + private StatisticsRepository statisticsRepository; + + @Mock + private QuestionService questionService; + + private User defaultUser; + private Question defaultQuestion; + private QuestionResponseDto defaultQuestionResponseDto; + private GameResponseDto defaultGameResponseDto; + + private UserResponseDto defaultUserResponseDto; + + private QuestionResponseDtoMapper questionResponseDtoMapper; + + @Mock + private Authentication authentication; + + private Game defaultGame; + + private Answer defaultCorrectAnswer; + + @BeforeEach + void setUp() { + this.questionResponseDtoMapper = new QuestionResponseDtoMapper(); + this.gameService = new GameService(gameRepository,new GameResponseDtoMapper(new UserResponseDtoMapper()), userService, questionService, questionResponseDtoMapper, statisticsRepository); + this.defaultUser = User.builder() + .id(1L) + .email("test@email.com") + .username("test") + .role("user") + .password("password") + .refreshToken("token") + .refreshExpiration(Instant.ofEpochSecond(TimeUtil.computeStartOfNextSecond(System.currentTimeMillis()+ 1000))) + .build(); + + this.defaultQuestion = Question.builder() + .id(1L) + .content("What is the capital of France?") + .answers(new ArrayList<>()) + .questionCategory(QuestionCategory.GEOGRAPHY) + .type(QuestionType.TEXT) + .build(); + + defaultCorrectAnswer = Answer.builder() + .id(1L) + .text("Paris") + .category(AnswerCategory.CAPITAL_CITY) + .questions(List.of(defaultQuestion)) + .questionsWithThisAnswer(List.of(defaultQuestion)) + .language("en") + .build(); + + Answer defaultIncorrectAnswer = Answer.builder() + .id(2L) + .text("Tokio") + .category(AnswerCategory.CAPITAL_CITY) + .questions(List.of(defaultQuestion)) + .questionsWithThisAnswer(List.of(defaultQuestion)) + .build(); + + defaultQuestion.setCorrectAnswer(defaultCorrectAnswer); + defaultQuestion.getAnswers().add(defaultCorrectAnswer); + defaultQuestion.getAnswers().add(defaultIncorrectAnswer); + + this.defaultUserResponseDto = UserResponseDto.builder() + .id(1L) + .email("test@email.com") + .username("test") + .build(); + + this.defaultQuestionResponseDto = QuestionResponseDto.builder() + .id(1L) + .content("What is the capital of France?") + .answers(new ArrayList<>()) + .language("en") + .questionCategory(QuestionCategory.GEOGRAPHY) + .answerCategory(AnswerCategory.CAPITAL_CITY) + .type(QuestionType.TEXT) + .build(); + defaultQuestionResponseDto.getAnswers().add(new AnswerResponseDtoMapper().apply(defaultCorrectAnswer)); + defaultQuestionResponseDto.getAnswers().add(new AnswerResponseDtoMapper().apply(defaultIncorrectAnswer)); + LocalDateTime now = LocalDateTime.now(); + this.defaultGameResponseDto = GameResponseDto.builder() + .user(defaultUserResponseDto) + .rounds(9L) + .correctlyAnsweredQuestions(0L) + .roundStartTime(Instant.ofEpochSecond(0L).toString()) + .actualRound(0L) + .roundDuration(30) + .gamemode(GameMode.KIWI_QUEST) + .build(); + this.defaultGame = Game.builder() + .id(1L) + .user(defaultUser) + .questions(new ArrayList<>()) + .rounds(9L) + .actualRound(0L) + .roundStartTime(0L) + .gamemode(GameMode.KIWI_QUEST) + .correctlyAnsweredQuestions(0L) + .language("en") + .roundDuration(30) + .build(); + } + + // NEW GAME TESTS + @Test + public void newGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + assertEquals(defaultGameResponseDto, gameDto); + } + + @Test + public void newGameActiveGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + defaultGameResponseDto.setId(1L); + assertEquals(defaultGameResponseDto, gameDto); + } + + @Test + public void newGameWasMeantToBeOver(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + defaultGame.setActualRound(10L); + gameService.newGame(null,null,null,authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void newGameWasMeantToBeOverExistingLeaderboard(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(Statistics.builder().user(new User()) + .correct(0L) + .wrong(0L) + .total(0L) + .build())); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + defaultGame.setActualRound(10L); + gameService.newGame(null,null,null,authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void newGameCustomGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + GameResponseDto gameDto = gameService.newGame("es",GameMode.CUSTOM, + CustomGameDto.builder() + .roundDuration(30) + .categories(List.of(QuestionCategory.GEOGRAPHY)) + .rounds(10L) + .build() + ,authentication); + defaultGameResponseDto.setGamemode(GameMode.CUSTOM); + defaultGameResponseDto.setRounds(10L); + defaultGameResponseDto.setRoundDuration(30); + + assertEquals(defaultGameResponseDto, gameDto); + } + + // START ROUND TESTS + @Test + public void startRound(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + GameResponseDto gameDto = gameService.startRound(1L, authentication); + GameResponseDto result = defaultGameResponseDto; + result.setActualRound(1L); + result.setId(1L); + result.setRoundStartTime(Instant.ofEpochMilli(defaultGame.getRoundStartTime()).toString()); + assertEquals(result, gameDto); + } + + @Test + public void startRoundGameOver(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + defaultGame.setActualRound(10L); + assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); + } + + @Test + public void startRoundWhenRoundNotFinished(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + gameService.startRound(1L,authentication); + assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); + } + + @Test + public void getCurrentQuestion() { + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + gameService.startRound(1L,authentication); + QuestionResponseDto questionDto = gameService.getCurrentQuestion(1L,authentication); + assertEquals(defaultQuestionResponseDto, questionDto); + } + + @Test + public void getCurrentQuestionRoundNotStarted() { + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); + } + + @Test + public void getCurrentQuestionRoundFinished() { + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + gameService.startRound(1L,authentication); + defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); + assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); + } + + @Test + public void getCurrentQuestionGameFinished() { + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.startRound(1L,authentication); + defaultGame.setGameOver(true); + defaultGame.setActualRound(10L); + assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); + } + + @Test + public void answerQuestionCorrectly(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); + gameService.getGameDetails(1L, authentication); + assertEquals(defaultGame.getCorrectlyAnsweredQuestions(), 1); + assertTrue(defaultGame.isCurrentQuestionAnswered()); + } + + @Test + public void answerQuestionIncorrectly(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + gameService.answerQuestion(1L, new GameAnswerDto(2L), authentication); + gameService.getGameDetails(1L, authentication); + assertEquals(defaultGame.getCorrectlyAnsweredQuestions(), 0); + assertTrue(defaultGame.isCurrentQuestionAnswered()); + } + + @Test + public void answerQuestionWhenGameHasFinished(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + defaultGame.setGameOver(true); + defaultGame.setActualRound(30L); + assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); + } + + @Test + public void answerQuestionLastRound(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + defaultGame.setActualRound(8L); + gameService.startRound(1L, authentication); + gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void answerQuestionWhenRoundHasFinished(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); + assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); + } + + @Test + public void answerQuestionInvalidId(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + assertThrows(IllegalArgumentException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(3L), authentication)); + } + + @Test + public void changeLanguage(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + gameService.changeLanguage(1L, "es", authentication); + gameService.getGameDetails(1L, authentication); + assertEquals("es",defaultGame.getLanguage()); + } + + @Test + public void changeLanguageGameOver(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + defaultGame.setGameOver(true); + defaultGame.setActualRound(10L); + assertThrows(IllegalStateException.class,() -> gameService.changeLanguage(1L, "es", authentication)); + + } + + @Test + public void changeLanguageInvalidLanguage(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + gameService.newGame(null,null,null,authentication); + assertThrows(IllegalArgumentException.class, () -> gameService.changeLanguage(1L, "patata", authentication)); + } + + @Test + public void getGameDetails(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + gameService.getGameDetails(1L, authentication); + + assertEquals(defaultGameResponseDto, gameDto); + } + + @Test + public void getGameDetailsInvalidId(){ + when(gameRepository.findByIdForUser(1L, 1L)).thenReturn(Optional.of(defaultGame)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + gameService.newGame(null,null,null,authentication); + gameService.startRound(1L, authentication); + assertThrows(NoSuchElementException.class, () -> gameService.getGameDetails(2L, authentication)); + } + + @Test + public void testGetQuestionCategories(){ + assertEquals(Arrays.asList(QuestionCategory.values()), gameService.getQuestionCategories()); + } + +} diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java index a46ed90d..1b378aee 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -1,161 +1,161 @@ -package lab.en2b.quizapi.questions; - -import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; -import lab.en2b.quizapi.questions.answer.Answer; -import lab.en2b.quizapi.questions.answer.AnswerCategory; -import lab.en2b.quizapi.questions.answer.AnswerRepository; -import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; -import lab.en2b.quizapi.questions.answer.dtos.AnswerResponseDto; -import lab.en2b.quizapi.questions.question.*; -import lab.en2b.quizapi.questions.question.dtos.AnswerCheckResponseDto; -import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; -import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith({MockitoExtension.class, SpringExtension.class}) -public class QuestionServiceTest { - @InjectMocks - QuestionService questionService; - - @Mock - QuestionRepository questionRepository; - @Mock - AnswerRepository answerRepository; - - Question defaultQuestion; - QuestionResponseDto defaultResponseDto; - - Answer defaultCorrectAnswer; - Answer defaultIncorrectAnswer; - - @BeforeEach - void setUp() { - this.questionService = new QuestionService(answerRepository, questionRepository, new QuestionResponseDtoMapper()); - - - defaultQuestion = Question.builder() - .id(1L) - .answers(new ArrayList<>()) - .questionCategory(QuestionCategory.GEOGRAPHY) - .type(QuestionType.TEXT) - .build(); - defaultCorrectAnswer = Answer.builder() - .id(1L) - .text("Paris") - .category(AnswerCategory.CAPITAL_CITY) - .questions(List.of(defaultQuestion)) - .questionsWithThisAnswer(List.of(defaultQuestion)) - .build(); - - defaultIncorrectAnswer = Answer.builder() - .id(2L) - .text("Tokio") - .category(AnswerCategory.CAPITAL_CITY) - .questions(List.of(defaultQuestion)) - .questionsWithThisAnswer(List.of(defaultQuestion)) - .build(); - - defaultQuestion.setCorrectAnswer(defaultCorrectAnswer); - defaultQuestion.getAnswers().add(defaultCorrectAnswer); - defaultQuestion.getAnswers().add(defaultIncorrectAnswer); - - List answersDto = new ArrayList<>(); - answersDto.add(AnswerResponseDto.builder() - .id(1L) - .category(AnswerCategory.CAPITAL_CITY) - .text("Paris") - .build()); - answersDto.add(AnswerResponseDto.builder() - .id(2L) - .category(AnswerCategory.CAPITAL_CITY) - .text("Tokio") - .build()); - defaultResponseDto = QuestionResponseDto.builder() - .id(1L) - .content("What is the capital of France?") - .answers(answersDto) - .language("en") - .questionCategory(QuestionCategory.GEOGRAPHY) - .answerCategory(AnswerCategory.CAPITAL_CITY) - .type(QuestionType.TEXT) - .build(); - } - - @Test - void testGetRandomQuestion() { - when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - QuestionResponseDto response = questionService.getRandomQuestion(""); - - assertEquals(response.getId(), defaultResponseDto.getId()); - } - @Test - void testGetRandomQuestionAnswersNotYetLoaded() { - when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); - defaultQuestion.setAnswers(List.of()); - QuestionResponseDto response = questionService.getRandomQuestion(""); - - assertEquals(response.getId(), defaultResponseDto.getId()); - } - @Test - void testGetRandomQuestionNoQuestionsFound() { - assertThrows(InternalApiErrorException.class,() -> questionService.getRandomQuestion("")); - } - - @Test - void testGetQuestionById(){ - when(questionRepository.findById(any())).thenReturn(Optional.of(defaultQuestion)); - QuestionResponseDto response = questionService.getQuestionById(1L); - - assertEquals(response.getId(), defaultResponseDto.getId()); - } - - @Test - void testGetQuestionByIdNotFound(){ - when(questionRepository.findById(any())).thenReturn(Optional.empty()); - assertThrows(NoSuchElementException.class,() -> questionService.getQuestionById(1L)); - } - - @Test - void testAnswerQuestionCorrectAnswer(){ - when(questionRepository.findById(1L)).thenReturn(Optional.of(defaultQuestion)); - AnswerCheckResponseDto response = questionService.answerQuestion(1L, AnswerDto.builder().answerId(1L).build()); - assertEquals(response, new AnswerCheckResponseDto(true)); - } - - @Test - void testAnswerQuestionIncorrectAnswer(){ - when(questionRepository.findById(1L)).thenReturn(Optional.of(defaultQuestion)); - AnswerCheckResponseDto response = questionService.answerQuestion(1L, AnswerDto.builder().answerId(2L).build()); - assertEquals(response, new AnswerCheckResponseDto(false)); - } - - @Test - void testAnswerQuestionNotFound(){ - when(questionRepository.findById(3L)).thenReturn(Optional.empty()); - assertThrows(NoSuchElementException.class,() -> questionService.answerQuestion(3L, AnswerDto.builder().answerId(1L).build())); - } - - @Test - void testAnswerQuestionInvalidAnswer(){ - when(questionRepository.findById(1L)).thenReturn(Optional.of(defaultQuestion)); - assertThrows(IllegalArgumentException.class,() -> questionService.answerQuestion(1L, AnswerDto.builder().answerId(3L).build())); - } - - -} +package lab.en2b.quizapi.questions; + +import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; +import lab.en2b.quizapi.questions.answer.Answer; +import lab.en2b.quizapi.questions.answer.AnswerCategory; +import lab.en2b.quizapi.questions.answer.AnswerRepository; +import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; +import lab.en2b.quizapi.questions.answer.dtos.AnswerResponseDto; +import lab.en2b.quizapi.questions.question.*; +import lab.en2b.quizapi.questions.question.dtos.AnswerCheckResponseDto; +import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; +import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class, SpringExtension.class}) +public class QuestionServiceTest { + @InjectMocks + QuestionService questionService; + + @Mock + QuestionRepository questionRepository; + @Mock + AnswerRepository answerRepository; + + Question defaultQuestion; + QuestionResponseDto defaultResponseDto; + + Answer defaultCorrectAnswer; + Answer defaultIncorrectAnswer; + + @BeforeEach + void setUp() { + this.questionService = new QuestionService(answerRepository, questionRepository, new QuestionResponseDtoMapper()); + + + defaultQuestion = Question.builder() + .id(1L) + .answers(new ArrayList<>()) + .questionCategory(QuestionCategory.GEOGRAPHY) + .type(QuestionType.TEXT) + .build(); + defaultCorrectAnswer = Answer.builder() + .id(1L) + .text("Paris") + .category(AnswerCategory.CAPITAL_CITY) + .questions(List.of(defaultQuestion)) + .questionsWithThisAnswer(List.of(defaultQuestion)) + .build(); + + defaultIncorrectAnswer = Answer.builder() + .id(2L) + .text("Tokio") + .category(AnswerCategory.CAPITAL_CITY) + .questions(List.of(defaultQuestion)) + .questionsWithThisAnswer(List.of(defaultQuestion)) + .build(); + + defaultQuestion.setCorrectAnswer(defaultCorrectAnswer); + defaultQuestion.getAnswers().add(defaultCorrectAnswer); + defaultQuestion.getAnswers().add(defaultIncorrectAnswer); + + List answersDto = new ArrayList<>(); + answersDto.add(AnswerResponseDto.builder() + .id(1L) + .category(AnswerCategory.CAPITAL_CITY) + .text("Paris") + .build()); + answersDto.add(AnswerResponseDto.builder() + .id(2L) + .category(AnswerCategory.CAPITAL_CITY) + .text("Tokio") + .build()); + defaultResponseDto = QuestionResponseDto.builder() + .id(1L) + .content("What is the capital of France?") + .answers(answersDto) + .language("en") + .questionCategory(QuestionCategory.GEOGRAPHY) + .answerCategory(AnswerCategory.CAPITAL_CITY) + .type(QuestionType.TEXT) + .build(); + } + + @Test + void testGetRandomQuestion() { + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + QuestionResponseDto response = questionService.getRandomQuestion(""); + + assertEquals(response.getId(), defaultResponseDto.getId()); + } + @Test + void testGetRandomQuestionAnswersNotYetLoaded() { + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + defaultQuestion.setAnswers(List.of()); + QuestionResponseDto response = questionService.getRandomQuestion(""); + + assertEquals(response.getId(), defaultResponseDto.getId()); + } + @Test + void testGetRandomQuestionNoQuestionsFound() { + assertThrows(InternalApiErrorException.class,() -> questionService.getRandomQuestion("")); + } + + @Test + void testGetQuestionById(){ + when(questionRepository.findById(any())).thenReturn(Optional.of(defaultQuestion)); + QuestionResponseDto response = questionService.getQuestionById(1L); + + assertEquals(response.getId(), defaultResponseDto.getId()); + } + + @Test + void testGetQuestionByIdNotFound(){ + when(questionRepository.findById(any())).thenReturn(Optional.empty()); + assertThrows(NoSuchElementException.class,() -> questionService.getQuestionById(1L)); + } + + @Test + void testAnswerQuestionCorrectAnswer(){ + when(questionRepository.findById(1L)).thenReturn(Optional.of(defaultQuestion)); + AnswerCheckResponseDto response = questionService.answerQuestion(1L, AnswerDto.builder().answerId(1L).build()); + assertEquals(response, new AnswerCheckResponseDto(true)); + } + + @Test + void testAnswerQuestionIncorrectAnswer(){ + when(questionRepository.findById(1L)).thenReturn(Optional.of(defaultQuestion)); + AnswerCheckResponseDto response = questionService.answerQuestion(1L, AnswerDto.builder().answerId(2L).build()); + assertEquals(response, new AnswerCheckResponseDto(false)); + } + + @Test + void testAnswerQuestionNotFound(){ + when(questionRepository.findById(3L)).thenReturn(Optional.empty()); + assertThrows(NoSuchElementException.class,() -> questionService.answerQuestion(3L, AnswerDto.builder().answerId(1L).build())); + } + + @Test + void testAnswerQuestionInvalidAnswer(){ + when(questionRepository.findById(1L)).thenReturn(Optional.of(defaultQuestion)); + assertThrows(IllegalArgumentException.class,() -> questionService.answerQuestion(1L, AnswerDto.builder().answerId(3L).build())); + } + + +} diff --git a/webapp/src/components/game/Game.js b/webapp/src/components/game/Game.js index ffe3ac04..880e74cd 100644 --- a/webapp/src/components/game/Game.js +++ b/webapp/src/components/game/Game.js @@ -5,7 +5,7 @@ const authManager = new AuthManager(); export async function newGame() { try { - let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/new"); + let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/play"); if (HttpStatusCode.Ok === requestAnswer.status) { return requestAnswer.data; } From 4a45c33aed0003941214612091bc9ebd283b80b5 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 14 Apr 2024 14:23:32 +0200 Subject: [PATCH 10/19] chore: remove non-used try-catch blocks. Exceptions now propagate --- webapp/src/components/game/Game.js | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/webapp/src/components/game/Game.js b/webapp/src/components/game/Game.js index 880e74cd..751fc863 100644 --- a/webapp/src/components/game/Game.js +++ b/webapp/src/components/game/Game.js @@ -1,17 +1,9 @@ -import {HttpStatusCode} from "axios"; import AuthManager from "components/auth/AuthManager"; const authManager = new AuthManager(); export async function newGame() { - try { - let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/play"); - if (HttpStatusCode.Ok === requestAnswer.status) { - return requestAnswer.data; - } - } catch { - - } + await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/play"); } export async function startRound(gameId) { @@ -23,14 +15,7 @@ export async function getCurrentQuestion(gameId) { } export async function changeLanguage(gameId, language) { - try { - let requestAnswer = await authManager.getAxiosInstance().put(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/language?language=" + language); - if (HttpStatusCode.Ok === requestAnswer.status) { - return requestAnswer.data; - } - } catch { - - } + await authManager.getAxiosInstance().put(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/language?language=" + language); } export async function answerQuestion(gameId, aId) { @@ -38,13 +23,6 @@ export async function answerQuestion(gameId, aId) { } export async function getGameDetails(gameId) { - try { - let requestAnswer = await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/details"); - if (HttpStatusCode.Ok === requestAnswer.status) { - return requestAnswer.data; - } - } catch { - - } + return await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/details"); } From 13596f3e44c07d0c33b201d0b849f99b920c3e0b Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 17 Apr 2024 23:24:10 +0200 Subject: [PATCH 11/19] feat: add image support to the game UI --- webapp/src/components/game/Game.js | 4 ++++ webapp/src/pages/Game.jsx | 16 +++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/game/Game.js b/webapp/src/components/game/Game.js index 751fc863..70aa8799 100644 --- a/webapp/src/components/game/Game.js +++ b/webapp/src/components/game/Game.js @@ -6,6 +6,10 @@ export async function newGame() { await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/play"); } +export async function getCurrentGame() { + return await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/play"); +} + export async function startRound(gameId) { return await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/startRound"); } diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index ded7575c..b8a97c25 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,10 +1,10 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; -import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress, CircularProgressLabel } from "@chakra-ui/react"; +import { Grid, Flex, Heading, Button, Box, Text, Image, Spinner, CircularProgress, CircularProgressLabel } from "@chakra-ui/react"; import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import Confetti from "react-confetti"; -import { newGame, startRound, getCurrentQuestion, answerQuestion } from '../components/game/Game'; +import { startRound, getCurrentQuestion, answerQuestion, getCurrentGame } from '../components/game/Game'; import LateralMenu from '../components/LateralMenu'; import MenuButton from '../components/MenuButton'; import { HttpStatusCode } from "axios"; @@ -24,6 +24,7 @@ export default function Game() { const [timeStartRound, setTimeStartRound] = useState(-1); const [roundDuration, setRoundDuration] = useState(0); const [maxRoundNumber, setMaxRoundNumber] = useState(9); + const [hasImage, setHasImage] = useState(false); const { t, i18n } = useTranslation(); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -43,6 +44,9 @@ export default function Game() { if (result.status === HttpStatusCode.Ok) { setQuestion(result.data); setTimeElapsed(0); + if (result.data.image) { + setHasImage(true); + } } else { navigate("/dashboard"); } @@ -91,6 +95,7 @@ export default function Game() { navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); } else { setAnswer({}); + setHasImage(false); setNextDisabled(true); await startNewRound(gameId); } @@ -122,7 +127,7 @@ export default function Game() { return; } try { - const newGameResponse = await newGame(); + const newGameResponse = await getCurrentGame(); if (newGameResponse) { setGameId(newGameResponse.id); setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); @@ -188,10 +193,11 @@ export default function Game() { size='xl' /> ) : ( - question && ( <> {question.content} - + { hasImage && + {t("game.image")} + } {question.answers.map((answer, index) => ( - ))} - - - - - - - {showConfetti && ( - - )} - - ) - )} + ))} + + + + + + + {showConfetti && ( + + )} + + } ); From 9e3bed2206c00f1898fc7d43ea6068a9790f0019 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Thu, 18 Apr 2024 22:02:17 +0200 Subject: [PATCH 15/19] feat: improved responsiveness of the game --- webapp/src/pages/Game.jsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 90a1fc95..2a1deea0 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; -import { Grid, Flex, Heading, Button, Box, Text, Image, Spinner, CircularProgress, CircularProgressLabel } from "@chakra-ui/react"; +import { Flex, Heading, Button, Box, Text, Image, Spinner, CircularProgress, CircularProgressLabel } from "@chakra-ui/react"; import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; @@ -182,7 +182,12 @@ export default function Game() { {roundDuration - timeElapsed} - + { + (!loading && hasImage) && + {t("game.image")} + + } {loading ? ( ) : <> - {question.content} - { hasImage && - {t("game.image")} - - } - + {question.content} + {question.answers.map((answer, index) => ( ))} - + - From 32ec5d92602ac35a81926e8d89efa8068f8685bb Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Thu, 18 Apr 2024 22:22:11 +0200 Subject: [PATCH 16/19] feat: even better responsiveness --- webapp/src/pages/Game.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 2a1deea0..213d7bf6 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -172,7 +172,8 @@ export default function Game() { return ( -
+
setIsMenuOpen(true)} /> setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> @@ -183,12 +184,12 @@ export default function Game() { {roundDuration - timeElapsed} { - (!loading && hasImage) && + (!loading && hasImage) && {t("game.image")} } - + {loading ? ( answerButtonClick(index, answer)} + fontSize={["0.85em", "1em"]} display={"flex"} style={{ backgroundColor: selectedOption === index ? "green" : "white", color: selectedOption === index ? "white" : "green" }} > - {answer.text} + {answer.text} ))} From 7d036f7da812b6c62f80092195019f3d42291bfc Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Thu, 18 Apr 2024 22:30:52 +0200 Subject: [PATCH 17/19] chore: reformatted some code --- webapp/src/pages/Game.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 213d7bf6..61bbbd17 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -123,9 +123,6 @@ export default function Game() { useEffect(() => { const initializeGame = async () => { - if (gameId) { - return; - } try { const newGameResponse = (await getCurrentGame()).data; if (newGameResponse) { @@ -147,7 +144,9 @@ export default function Game() { navigate("/dashboard"); } }; - initializeGame(); + if (!gameId) { + initializeGame(); + } }, [setGameId, gameId, setTimeStartRound, setRoundDuration, setMaxRoundNumber, setQuestion, setLoading, startNewRound, navigate, assignQuestion]); useEffect(() => { From aac562064400288c7d95973590669b798aa7b967 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Thu, 18 Apr 2024 22:33:23 +0200 Subject: [PATCH 18/19] chore: due to the design of the backend, add petition to end the game --- webapp/src/pages/Game.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 61bbbd17..f437db8f 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -4,7 +4,7 @@ import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import Confetti from "react-confetti"; -import { startRound, getCurrentQuestion, answerQuestion, getCurrentGame } from '../components/game/Game'; +import { startRound, getCurrentQuestion, answerQuestion, getCurrentGame, getGameDetails } from '../components/game/Game'; import LateralMenu from '../components/menu/LateralMenu'; import MenuButton from '../components/menu/MenuButton'; import { HttpStatusCode } from "axios"; @@ -92,6 +92,7 @@ export default function Game() { const nextRound = useCallback(async () => { if (roundNumber + 1 > maxRoundNumber) { + await getGameDetails(gameId); navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); } else { setAnswer({}); From 7eb7b2924abbf2f311f5b68160e4df467d0b6c37 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Fri, 19 Apr 2024 12:48:39 +0200 Subject: [PATCH 19/19] fix: minor issues due to the merge --- .../components/dashboard/CustomGameMenu.jsx | 24 ++++++++++++------- webapp/src/pages/Game.jsx | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/dashboard/CustomGameMenu.jsx b/webapp/src/components/dashboard/CustomGameMenu.jsx index d522097e..603566d9 100644 --- a/webapp/src/components/dashboard/CustomGameMenu.jsx +++ b/webapp/src/components/dashboard/CustomGameMenu.jsx @@ -22,13 +22,15 @@ const CustomGameMenu = ({ isOpen, onClose }) => { async function fetchCategories() { try { let lang = i18n.language; - if (lang.includes("en")) + if (lang.includes("en")) { lang = "en"; - else if (lang.includes("es")) + } else if (lang.includes("es")) { lang = "es" - else + } else { lang = "en"; - const categoriesData = await gameCategories(lang); + } + + const categoriesData = (await gameCategories(lang)).data; const formattedCategories = categoriesData.map(category => category.name); setCategories(formattedCategories); } catch (error) { @@ -49,17 +51,20 @@ const CustomGameMenu = ({ isOpen, onClose }) => { const initializeCustomGameMode = async () => { try { let lang = i18n.language; - if (lang.includes("en")) + if (lang.includes("en")) { lang = "en"; - else if (lang.includes("es")) + } else if (lang.includes("es")) { lang = "es" - else + } else { lang = "en"; + } const gamemode = 'CUSTOM'; let uppercaseCategories = selectedCategories.map(category => category.toUpperCase()); - if (uppercaseCategories.length === 0) + if (uppercaseCategories.length === 0) { uppercaseCategories = ["GEOGRAPHY", "SPORTS", "MUSIC", "ART", "VIDEOGAMES"]; + } + const customGameDto = { rounds: rounds, categories: uppercaseCategories, @@ -67,8 +72,9 @@ const CustomGameMenu = ({ isOpen, onClose }) => { } const newGameResponse = await newGame(lang, gamemode, customGameDto); - if (newGameResponse) + if (newGameResponse) { navigate("/dashboard/game"); + } } catch (error) { console.error("Error initializing game:", error); } diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index f437db8f..2e968ca6 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -108,7 +108,7 @@ export default function Game() { const result = await answerQuestion(gameId, answer.id); let isCorrect = result.data.was_correct; if (isCorrect) { - setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); + setCorrectAnswers(correctAnswers + 1); setShowConfetti(true); } setNextDisabled(true);