diff --git a/pom.xml b/pom.xml index 78ee59d..e327487 100644 --- a/pom.xml +++ b/pom.xml @@ -4,71 +4,71 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.javarush.khmelov - project-ledzeppelin + com.javarush.trukhanova + jungle-quest 1.0-SNAPSHOT - ProjectLedzeppelin war - UTF-8 21 21 + UTF-8 5.10.2 + 5.5.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.5 - - - - - - org.springframework.boot - spring-boot-dependencies - pom - import - 3.3.5 - - - - - - jakarta.servlet - jakarta.servlet-api - provided - jakarta.servlet.jsp.jstl jakarta.servlet.jsp.jstl-api + 3.0.0 + org.glassfish.web jakarta.servlet.jsp.jstl + 3.0.1 org.projectlombok lombok + 1.18.34 provided + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.17.0 + + + org.apache.logging.log4j + log4j-core + 2.24.1 + + org.junit.jupiter junit-jupiter-api + ${junit.version} test org.junit.jupiter junit-jupiter-engine + ${junit.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} test - + jungle-quest org.apache.maven.plugins @@ -78,6 +78,7 @@ org.apache.maven.plugins maven-compiler-plugin + 3.13.0 @@ -89,6 +90,5 @@ - \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/cmd/Command.java b/src/main/java/com/javarush/khmelov/cmd/Command.java deleted file mode 100644 index fd4035b..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/Command.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.javarush.khmelov.cmd; - -import jakarta.servlet.http.HttpServletRequest; - -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public interface Command { - - default String doGet(HttpServletRequest request) { - return getView(); - } - - default String doPost(HttpServletRequest request) { - return getView(); - } - - default String getView() { - String simpleName = this.getClass().getSimpleName(); - return convertCamelCaseToKebabStyle(simpleName); - } - - private static String convertCamelCaseToKebabStyle(String string) { - String snakeName = string.chars() - .mapToObj(s -> String.valueOf((char) s)) - .flatMap(s -> s.matches("[A-Z]") - ? Stream.of("-", s) - : Stream.of(s)) - .collect(Collectors.joining()) - .toLowerCase(); - return snakeName.startsWith("-") - ? snakeName.substring(1) - : snakeName; - } - - -} diff --git a/src/main/java/com/javarush/khmelov/cmd/EditUser.java b/src/main/java/com/javarush/khmelov/cmd/EditUser.java deleted file mode 100644 index ae191b4..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/EditUser.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.javarush.khmelov.cmd; - -import com.javarush.khmelov.entity.Role; -import com.javarush.khmelov.entity.User; -import com.javarush.khmelov.service.UserService; -import jakarta.servlet.http.HttpServletRequest; - -import java.util.Optional; - - -@SuppressWarnings("unused") -public class EditUser implements Command { - - private final UserService userService; - - public EditUser(UserService userService) { - this.userService = userService; - } - - - @Override - public String doGet(HttpServletRequest req) { - String stringId = req.getParameter("id"); - if (stringId != null) { - long id = Long.parseLong(stringId); - Optional optionalUser = userService.get(id); - if (optionalUser.isPresent()) { - User user = optionalUser.get(); - req.setAttribute("user", user); - } - } - return getView(); - } - - @Override - public String doPost(HttpServletRequest req) { - User user = User.builder() - .login(req.getParameter("login")) - .password(req.getParameter("password")) - .role(Role.valueOf(req.getParameter("role"))) - .build(); - if (req.getParameter("create") != null) { - userService.create(user); - } else if (req.getParameter("update") != null) { - user.setId(Long.parseLong(req.getParameter("id"))); - userService.update(user); - } - return getView() + "?id=" + user.getId(); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/cmd/ListUser.java b/src/main/java/com/javarush/khmelov/cmd/ListUser.java deleted file mode 100644 index 9257917..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/ListUser.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.cmd; - -import com.javarush.khmelov.entity.User; -import com.javarush.khmelov.service.UserService; -import jakarta.servlet.http.HttpServletRequest; - -import java.util.Collection; - -@SuppressWarnings("unused") -public class ListUser implements Command { - - private final UserService userService; - - public ListUser(UserService userService) { - this.userService = userService; - } - - @Override - public String doGet(HttpServletRequest request) { - Collection users = userService.getAll(); - request.setAttribute("users", users); - return getView(); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/javarush/khmelov/cmd/StartPage.java b/src/main/java/com/javarush/khmelov/cmd/StartPage.java deleted file mode 100644 index d268f93..0000000 --- a/src/main/java/com/javarush/khmelov/cmd/StartPage.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.javarush.khmelov.cmd; - -@SuppressWarnings("unused") -public class StartPage implements Command { - -} diff --git a/src/main/java/com/javarush/khmelov/config/Winter.java b/src/main/java/com/javarush/khmelov/config/Winter.java deleted file mode 100644 index 48bd8a7..0000000 --- a/src/main/java/com/javarush/khmelov/config/Winter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.javarush.khmelov.config; - -import lombok.SneakyThrows; - -import java.lang.reflect.Constructor; -import java.util.concurrent.ConcurrentHashMap; - -public class Winter { - - public static ConcurrentHashMap, Object> components = new ConcurrentHashMap<>(); - - - @SuppressWarnings("unchecked") - @SneakyThrows - public static T find(Class aClass) { - Object component = components.get(aClass); - if (component == null) { - Constructor constructor = aClass.getConstructors()[0]; - Class[] parameterTypes = constructor.getParameterTypes(); - Object[] parameters = new Object[parameterTypes.length]; - for (int i = 0; i < parameters.length; i++) { - parameters[i] = Winter.find(parameterTypes[i]); - } - Object newInstance = constructor.newInstance(parameters); - components.put(aClass, newInstance); - } - return (T) components.get(aClass); - } -} diff --git a/src/main/java/com/javarush/khmelov/controller/FrontController.java b/src/main/java/com/javarush/khmelov/controller/FrontController.java deleted file mode 100644 index 33242b2..0000000 --- a/src/main/java/com/javarush/khmelov/controller/FrontController.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.javarush.khmelov.controller; - -import com.javarush.khmelov.cmd.Command; -import com.javarush.khmelov.config.Winter; -import com.javarush.khmelov.entity.Role; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; - -@WebServlet({"", "/home", "/list-user", "/edit-user"}) -public class FrontController extends HttpServlet { - - private final HttpResolver httpResolver = Winter.find(HttpResolver.class); - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Command command = httpResolver.resolve(req); - String view = command.doGet(req); - String jsp = getJsp(view); - req.getRequestDispatcher(jsp).forward(req, resp); - } - - @Override - public void init(ServletConfig config) { - config.getServletContext().setAttribute("roles", Role.values()); - } - - private static String getJsp(String view) { - return "/WEB-INF/" + view + ".jsp"; - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Command command = httpResolver.resolve(req); - String redirect = command.doPost(req); - resp.sendRedirect(redirect); - } -} diff --git a/src/main/java/com/javarush/khmelov/controller/HttpResolver.java b/src/main/java/com/javarush/khmelov/controller/HttpResolver.java deleted file mode 100644 index 18bb761..0000000 --- a/src/main/java/com/javarush/khmelov/controller/HttpResolver.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.javarush.khmelov.controller; - -import com.javarush.khmelov.cmd.Command; -import com.javarush.khmelov.config.Winter; -import jakarta.servlet.http.HttpServletRequest; - -public class HttpResolver { - - public Command resolve(HttpServletRequest request) { - // /cmd-example - try { - String requestURI = request.getRequestURI(); - requestURI = requestURI.equals("/") ? "/start-page" : requestURI; - String kebabName = requestURI.split("[?#/]")[1]; - String simpleName = convertKebabStyleToCamelCase(kebabName); - String fullName = Command.class.getPackageName() + "." + simpleName; - Class aClass = Class.forName(fullName); - return (Command) Winter.find(aClass); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - private String convertKebabStyleToCamelCase(String input) { - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = true; - for (char c : input.toCharArray()) { - if (c == '-') { - capitalizeNext = true; - } else { - if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(Character.toLowerCase(c)); - } - } - } - return result.toString(); - } -} diff --git a/src/main/java/com/javarush/khmelov/entity/Role.java b/src/main/java/com/javarush/khmelov/entity/Role.java deleted file mode 100644 index 5ae365f..0000000 --- a/src/main/java/com/javarush/khmelov/entity/Role.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.javarush.khmelov.entity; - -public enum Role { - USER, ADMIN, GUEST -} diff --git a/src/main/java/com/javarush/khmelov/entity/User.java b/src/main/java/com/javarush/khmelov/entity/User.java deleted file mode 100644 index f7fa2d6..0000000 --- a/src/main/java/com/javarush/khmelov/entity/User.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javarush.khmelov.entity; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class User { - - private Long id; - - private String login; - - private String password; - - private Role role; - - public String getImage() { //TODO move to DTO - return "image-" + id; - } - -} diff --git a/src/main/java/com/javarush/khmelov/repository/Repository.java b/src/main/java/com/javarush/khmelov/repository/Repository.java deleted file mode 100644 index f1abdac..0000000 --- a/src/main/java/com/javarush/khmelov/repository/Repository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.javarush.khmelov.repository; - -import com.javarush.khmelov.entity.User; - -import java.util.Collection; -import java.util.Optional; - -public interface Repository { - - Collection getAll(); - - Optional get(long id); - - void create(T entity); - - void update(T entity); - - void delete(T entity); -} diff --git a/src/main/java/com/javarush/khmelov/repository/UserRepository.java b/src/main/java/com/javarush/khmelov/repository/UserRepository.java deleted file mode 100644 index 58b32ea..0000000 --- a/src/main/java/com/javarush/khmelov/repository/UserRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.javarush.khmelov.repository; - -import com.javarush.khmelov.entity.Role; -import com.javarush.khmelov.entity.User; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; - -public class UserRepository implements Repository { - - private final Map map = new HashMap<>(); - - public static final AtomicLong id = new AtomicLong(System.currentTimeMillis()); - - public UserRepository() { - map.put(1L, new User(1L, "Alisa", "qwerty", Role.USER)); - map.put(2L, new User(2L, "Bob", "", Role.GUEST)); - map.put(3L, new User(3L, "Carl", "admin", Role.ADMIN)); - map.put(4L, new User(4L, "Khmelov", "admin", Role.ADMIN)); - } - - @Override - public Collection getAll() { - return map.values(); - } - - @Override - public Optional get(long id) { - return Optional.ofNullable(map.get(id)); - } - - @Override - public void create(User entity) { - entity.setId(id.incrementAndGet()); - update(entity); - } - - @Override - public void update(User entity) { - map.put(entity.getId(), entity); - } - - @Override - public void delete(User entity) { - map.remove(entity.getId()); - } -} diff --git a/src/main/java/com/javarush/khmelov/service/UserService.java b/src/main/java/com/javarush/khmelov/service/UserService.java deleted file mode 100644 index b17527c..0000000 --- a/src/main/java/com/javarush/khmelov/service/UserService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.javarush.khmelov.service; - -import com.javarush.khmelov.entity.User; -import com.javarush.khmelov.repository.UserRepository; - -import java.util.Collection; -import java.util.Optional; - -public class UserService { - - private final UserRepository userRepository; - - public UserService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public void create(User user) { - userRepository.create(user); - } - - public void update(User user) { - userRepository.update(user); - } - - public void delete(User user) { - userRepository.delete(user); - } - - public Collection getAll() { - return userRepository.getAll(); - } - - public Optional get(long id) { - return userRepository.get(id); - } -} diff --git a/src/main/java/com/javarush/trukhanova/config/AppContextListener.java b/src/main/java/com/javarush/trukhanova/config/AppContextListener.java new file mode 100644 index 0000000..c6b34fb --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/config/AppContextListener.java @@ -0,0 +1,18 @@ +package com.javarush.trukhanova.config; + +import com.javarush.trukhanova.repository.QuestRepository; +import com.javarush.trukhanova.service.GameService; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.annotation.WebListener; + +@WebListener +public class AppContextListener implements ServletContextListener { + @Override + public void contextInitialized(ServletContextEvent sce) { + QuestRepository repository = new QuestRepository(); + GameService gameService = new GameService(repository); + + sce.getServletContext().setAttribute("gameService", gameService); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/controller/InitServlet.java b/src/main/java/com/javarush/trukhanova/controller/InitServlet.java new file mode 100644 index 0000000..f545ec7 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/controller/InitServlet.java @@ -0,0 +1,43 @@ +package com.javarush.trukhanova.controller; + +import com.javarush.trukhanova.entity.Player; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebServlet(name = "InitServlet", value = "/init") +public class InitServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException { + + String playerName = request.getParameter("playerName"); + String selectedAvatar = request.getParameter("avatar"); + + if (playerName == null || playerName.isBlank()) { + playerName = "Неизвестный герой"; + } + if (selectedAvatar == null || selectedAvatar.isEmpty()) { + selectedAvatar = "static/images/avatars/1.png"; + } + + HttpSession session = request.getSession(); + + Player player = (Player) session.getAttribute("player"); + + if (player == null) { + player = new Player(playerName, selectedAvatar); + session.setAttribute("player", player); + } else { + player.setName(playerName); + player.setAvatarPath(selectedAvatar); + } + + response.sendRedirect(request.getContextPath() + "/logic?id=1"); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/controller/LogicServlet.java b/src/main/java/com/javarush/trukhanova/controller/LogicServlet.java new file mode 100644 index 0000000..25e5b31 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/controller/LogicServlet.java @@ -0,0 +1,67 @@ +package com.javarush.trukhanova.controller; + +import com.javarush.trukhanova.entity.Player; +import com.javarush.trukhanova.entity.QuestStep; +import com.javarush.trukhanova.exception.StepNotFoundException; +import com.javarush.trukhanova.service.GameLogic; +import com.javarush.trukhanova.service.TimerManager; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.io.IOException; + +@WebServlet(name = "LogicServlet", value = "/logic") +public class LogicServlet extends HttpServlet { + private static final Logger logger = LogManager.getLogger(LogicServlet.class); + private static final int RESPONSE_TIME_LIMIT = 20; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + HttpSession session = request.getSession(); + String sessionId = session.getId(); + + TimerManager.getInstance().stopTimer(sessionId); + + GameLogic gameService = (GameLogic) getServletContext().getAttribute("gameService"); + if (gameService == null) { + response.sendRedirect("index.jsp"); + return; + } + + try { + String idParam = request.getParameter("id"); + int stepId = (idParam == null) ? 1 : Integer.parseInt(idParam); + + QuestStep currentStep = gameService.getNextStep(stepId); + request.setAttribute("step", currentStep); + + if (gameService.isGameOver(currentStep)) { + Player player = (Player) session.getAttribute("player"); + if (player != null) { + player.incrementGamesPlayed(); + request.setAttribute("player", player); + } + request.getRequestDispatcher("/WEB-INF/jsp/final.jsp").forward(request, response); + return; + } + + TimerManager.getInstance().startTimer(sessionId, RESPONSE_TIME_LIMIT, () -> { + logger.warn("Время вышло для сессии: {}", sessionId); + session.setAttribute("isTimeOut", true); + }); + + request.getRequestDispatcher("/WEB-INF/jsp/game.jsp").forward(request, response); + + } catch (StepNotFoundException e) { + response.sendRedirect("logic?id=1"); + } catch (Exception e) { + logger.error("Ошибка: ", e); + response.sendRedirect("index.jsp"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/controller/RestartServlet.java b/src/main/java/com/javarush/trukhanova/controller/RestartServlet.java new file mode 100644 index 0000000..f31a41c --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/controller/RestartServlet.java @@ -0,0 +1,19 @@ +package com.javarush.trukhanova.controller; + +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; + +@WebServlet(name = "RestartServlet", value = "/restart") +public class RestartServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + HttpSession session = request.getSession(); + session.removeAttribute("isTimeOut"); + response.sendRedirect(request.getContextPath() + "/logic?id=1"); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/controller/StartServlet.java b/src/main/java/com/javarush/trukhanova/controller/StartServlet.java new file mode 100644 index 0000000..16ee217 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/controller/StartServlet.java @@ -0,0 +1,31 @@ +package com.javarush.trukhanova.controller; + +import com.javarush.trukhanova.entity.Player; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; + +@WebServlet(name = "StartServlet", value = "/start") +public class StartServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException { + + String name = request.getParameter("playerName"); + String avatar = request.getParameter("avatar"); + + if (name == null || name.isBlank()) name = "Исследователь"; + if (avatar == null) avatar = "static/images/avatars/1.png"; + + Player player = new Player(name, avatar); + + HttpSession session = request.getSession(); + session.setAttribute("player", player); + + response.sendRedirect("logic?id=1"); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/entity/Answer.java b/src/main/java/com/javarush/trukhanova/entity/Answer.java new file mode 100644 index 0000000..964725f --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/entity/Answer.java @@ -0,0 +1,13 @@ +package com.javarush.trukhanova.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Answer { + private String text; + private int nextStepId; +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/entity/Player.java b/src/main/java/com/javarush/trukhanova/entity/Player.java new file mode 100644 index 0000000..d9f0cf0 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/entity/Player.java @@ -0,0 +1,24 @@ +package com.javarush.trukhanova.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Player { + private String name; + private int gamesPlayed; + private String avatarPath; + + public Player(String name, String avatarPath) { + this.name = name; + this.avatarPath = avatarPath; + this.gamesPlayed = 0; + } + + public void incrementGamesPlayed() { + this.gamesPlayed++; + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/entity/QuestStep.java b/src/main/java/com/javarush/trukhanova/entity/QuestStep.java new file mode 100644 index 0000000..b9ccca4 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/entity/QuestStep.java @@ -0,0 +1,17 @@ +package com.javarush.trukhanova.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QuestStep { + private int id; + private String title; + private String description; + private String imagePath; + private List answers; +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/exception/QuestException.java b/src/main/java/com/javarush/trukhanova/exception/QuestException.java new file mode 100644 index 0000000..5b58ca6 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/exception/QuestException.java @@ -0,0 +1,13 @@ +package com.javarush.trukhanova.exception; + + +public class QuestException extends RuntimeException { + + public QuestException(String message) { + super(message); + } + + public QuestException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/exception/RepositoryException.java b/src/main/java/com/javarush/trukhanova/exception/RepositoryException.java new file mode 100644 index 0000000..d65568e --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/exception/RepositoryException.java @@ -0,0 +1,7 @@ +package com.javarush.trukhanova.exception; + +public class RepositoryException extends QuestException { + public RepositoryException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/exception/StepNotFoundException.java b/src/main/java/com/javarush/trukhanova/exception/StepNotFoundException.java new file mode 100644 index 0000000..761041e --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/exception/StepNotFoundException.java @@ -0,0 +1,7 @@ +package com.javarush.trukhanova.exception; + +public class StepNotFoundException extends QuestException { + public StepNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/repository/QuestRepository.java b/src/main/java/com/javarush/trukhanova/repository/QuestRepository.java new file mode 100644 index 0000000..1d180fc --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/repository/QuestRepository.java @@ -0,0 +1,66 @@ +package com.javarush.trukhanova.repository; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import com.javarush.trukhanova.entity.QuestStep; +import com.javarush.trukhanova.exception.RepositoryException; +import com.javarush.trukhanova.exception.StepNotFoundException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class QuestRepository implements Repository { + private static final Logger logger = LogManager.getLogger(QuestRepository.class); + private final Map steps = new HashMap<>(); + + private final List dataFiles = List.of( + "prologue.yml", + "gameplay.yml", + "final.yml" + ); + + public QuestRepository() { + load(); + } + + @Override + public void load() { + logger.info("Начало загрузки конфигурационных файлов квеста..."); + YAMLMapper mapper = new YAMLMapper(); + + for (String fileName : dataFiles) { + try (InputStream is = getClass().getClassLoader().getResourceAsStream(fileName)) { + if (is == null) { + logger.fatal("Файл ресурсов не найден: {}", fileName); + throw new RepositoryException("Критическая ошибка: файл " + fileName + " не найден!", null); + } + + List loaded = mapper.readValue(is, new TypeReference<>() {}); + for (QuestStep step : loaded) { + steps.put(step.getId(), step); + } + + logger.info("Успешно загружено шагов: {} из файла: {}", loaded.size(), fileName); + + } catch (Exception e) { + logger.error("Ошибка парсинга файла {}: {}", fileName, e.getMessage()); + throw new RepositoryException("Ошибка при чтении или парсинге YAML файла: " + fileName, e); + } + } + logger.info("Загрузка всех данных завершена. Общее количество шагов в базе: {}", steps.size()); + } + + @Override + public QuestStep getById(int id) { + QuestStep step = steps.get(id); + if (step == null) { + logger.warn("Запрошен несуществующий шаг с ID: {}", id); + throw new StepNotFoundException("Попытка перехода на несуществующий ID: " + id); + } + return step; + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/repository/Repository.java b/src/main/java/com/javarush/trukhanova/repository/Repository.java new file mode 100644 index 0000000..433590a --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/repository/Repository.java @@ -0,0 +1,8 @@ +package com.javarush.trukhanova.repository; + +public interface Repository { + + T getById(int id); + + void load(); +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/service/GameLogic.java b/src/main/java/com/javarush/trukhanova/service/GameLogic.java new file mode 100644 index 0000000..20703de --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/service/GameLogic.java @@ -0,0 +1,9 @@ +package com.javarush.trukhanova.service; + +import com.javarush.trukhanova.entity.QuestStep; + + +public interface GameLogic { + QuestStep getNextStep(int id); + boolean isGameOver(QuestStep step); +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/service/GameService.java b/src/main/java/com/javarush/trukhanova/service/GameService.java new file mode 100644 index 0000000..f4969a1 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/service/GameService.java @@ -0,0 +1,31 @@ +package com.javarush.trukhanova.service; + +import com.javarush.trukhanova.entity.QuestStep; +import com.javarush.trukhanova.repository.Repository; + +public class GameService implements GameLogic { + + private final Repository repository; + + public GameService(Repository repository) { + this.repository = repository; + } + + @Override + public QuestStep getNextStep(int id) { + return repository.getById(id); + } + + @Override + public boolean isGameOver(QuestStep step) { + if (step == null || step.getTitle() == null) { + return step != null && (step.getAnswers() == null || step.getAnswers().isEmpty()); + } + + String title = step.getTitle().toLowerCase(); + return title.contains("победа") || + title.contains("поражение") || + step.getAnswers() == null || + step.getAnswers().isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/trukhanova/service/TimerManager.java b/src/main/java/com/javarush/trukhanova/service/TimerManager.java new file mode 100644 index 0000000..69a8e62 --- /dev/null +++ b/src/main/java/com/javarush/trukhanova/service/TimerManager.java @@ -0,0 +1,32 @@ +package com.javarush.trukhanova.service; + +import java.util.concurrent.*; +import java.util.Map; + +public class TimerManager { + private static TimerManager instance; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4); + private final Map> activeTimers = new ConcurrentHashMap<>(); + + private TimerManager() {} + + public static synchronized TimerManager getInstance() { + if (instance == null) { + instance = new TimerManager(); + } + return instance; + } + + public void startTimer(String sessionId, int seconds, Runnable onTimeout) { + stopTimer(sessionId); + ScheduledFuture future = scheduler.schedule(onTimeout, seconds, TimeUnit.SECONDS); + activeTimers.put(sessionId, future); + } + + public void stopTimer(String sessionId) { + ScheduledFuture future = activeTimers.remove(sessionId); + if (future != null && !future.isDone()) { + future.cancel(false); + } + } +} \ No newline at end of file diff --git a/src/main/resources/final.yml b/src/main/resources/final.yml new file mode 100644 index 0000000..33ab4a2 --- /dev/null +++ b/src/main/resources/final.yml @@ -0,0 +1,40 @@ +- id: 7 + title: "ПОБЕДА" + imagePath: "static/images/win.jpg" + description: "Живительная влага наполнила вас силой. Вы нашли выход и сокровища. Экспедиция вошла в историю как самая успешная!" + + +- id: 11 + title: "КОНЕЦ ПУТИ" + imagePath: "static/images/lost.jpg" + description: "Лес оказался коварен. Без амулета и проводников вы заблудились в гуще мангровых зарослей и стали добычей ягуара. Игра окончена." + + +- id: 12 + title: "ЛОВУШКА" + imagePath: "static/images/lost.jpg" + description: "Как только вы ударили по двери, сработал древний механизм. Потолок начал опускаться... Храм не терпит грубости. Игра окончена." + + +- id: 13 + title: "ЯД" + imagePath: "static/images/lost.jpg" + description: "В чаше оказался смертельный экстракт лиан. Вы засыпаете навсегда под золотыми сводами храма. Игра окончена." + + +- id: 14 + title: "ПЛЕН" + imagePath: "static/images/lost.jpg" + description: "Вас оставляют в племени в качестве вечного слуги. Вы больше никогда не увидите родной дом. Игра окончена." + +- id: 15 + title: "ПЛЕН" + imagePath: "static/images/lost.jpg" + description: "Бревно оказалось скользким, вы не удержались и упали в коварную реку. Игра окончена." + + + +- id: 99 + title: "ВРЕМЯ ВЫШЛО" + imagePath: "static/images/lost.jpg" + description: "Джунгли не прощают медлительности. Пока вы раздумывали, хищник подобрался слишком близко. Игра окончена." diff --git a/src/main/resources/gameplay.yml b/src/main/resources/gameplay.yml new file mode 100644 index 0000000..ec70cfa --- /dev/null +++ b/src/main/resources/gameplay.yml @@ -0,0 +1,59 @@ +- id: 4 + title: "Золотой Амулет" + imagePath: "static/images/amulet.jpg" + description: "Вы нашли древний амулет в форме солнца! Он удивительно тяжелый и теплый на ощупь. С этим артефактом вы чувствуете себя увереннее." + answers: + - text: "Спрятать амулет и идти к горам" + nextStepId: 8 + - text: "Бросить его (плохое предчувствие)" + nextStepId: 10 + +- id: 8 + title: "Племя Ягуара" + imagePath: "static/images/village.jpg" + description: "Вас окружают воины в масках. Вождь указывает на ваше снаряжение. Кажется, он что-то ищет." + answers: + - text: "Показать амулет солнца" + nextStepId: 9 + - text: "Попробовать договориться жестами" + nextStepId: 14 + +- id: 10 + title: "Тернистый путь" + imagePath: "static/images/village.jpg" + description: "Вы выходите к деревне племени Ягуара. У вас нет священного амулета, и воины смотрят на вас как на обычного нарушителя границ." + answers: + - text: "Попытаться пройти мимо" + nextStepId: 14 + - text: "Убежать обратно в лес" + nextStepId: 11 + +- id: 9 + title: "Загадка Духов" + imagePath: "static/images/riddle.jpg" + description: "Вождь кивает, увидев амулет, но требует испытания ума: 'Что всегда бежит, но не имеет ног?'" + answers: + - text: "Ответить: Время" + nextStepId: 14 + - text: "Ответить: Река" + nextStepId: 5 + - text: "Ответить: Ветер" + nextStepId: 14 +- id: 5 + title: "Храм Солнца" + imagePath: "static/images/temple.jpg" + description: "Вы достигли цели! Перед вами огромная пирамида. На массивных дверях — углубление точно под ваш амулет." + answers: + - text: "Вставить амулет в замок" + nextStepId: 6 + - text: "Попробовать выбить дверь" + nextStepId: 12 +- id: 6 + title: "Тронный Зал" + imagePath: "static/images/throne.jpg" + description: "Залы полны золота, но путь к выходу преграждает призрачный страж. Он предлагает испить из одной из двух чаш." + answers: + - text: "Выпить из золотого кубка" + nextStepId: 7 + - text: "Выпить из чаши из кости" + nextStepId: 13 \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..35a6a3c --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/prologue.yml b/src/main/resources/prologue.yml new file mode 100644 index 0000000..d81b7e4 --- /dev/null +++ b/src/main/resources/prologue.yml @@ -0,0 +1,28 @@ +- id: 1 + title: "Крушение" + imagePath: "static/images/start.jpg" + description: "Ваш самолет разбился посреди непроходимых джунглей. Связи нет, припасов мало. Перед вами стена зелени. С чего начнете?" + answers: + - text: "Прорубаться мачете вглубь леса" + nextStepId: 2 + - text: "Искать обходной путь вдоль реки" + nextStepId: 3 + +- id: 2 + title: "Древний Тотем" + imagePath: "static/images/totem.png" + description: "В густых зарослях вы натыкаетесь на каменный идол. Его глаза инкрустированы странными камнями, а у подножия лежит металлический предмет." + answers: + - text: "Осмотреть тайник под идолом" + nextStepId: 4 + - text: "Не трогать реликвии и идти дальше" + nextStepId: 10 +- id: 3 + title: "Коварная Река" + imagePath: "static/images/river.jpg" + description: "Берег реки топкий, а в воде видны спины кайманов. Единственный путь — перейти на тот берег по поваленному дереву." + answers: + - text: "Рискнуть и перебежать" + nextStepId: 15 + - text: "Вернуться в джунгли" + nextStepId: 2 \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/edit-user.jsp b/src/main/webapp/WEB-INF/edit-user.jsp deleted file mode 100644 index f274104..0000000 --- a/src/main/webapp/WEB-INF/edit-user.jsp +++ /dev/null @@ -1,73 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@include file="head.jsp" %> - -
-
-
- - - Edit user: - - -
- -
- - min 3 symbols -
-
- - -
- -
- - min 8 symb -
-
- - - -
- -
- -
-
- - -
- -
- - - - - - - -
-
- -
-
-
- - diff --git a/src/main/webapp/WEB-INF/head.jsp b/src/main/webapp/WEB-INF/head.jsp deleted file mode 100644 index 2f7b9f2..0000000 --- a/src/main/webapp/WEB-INF/head.jsp +++ /dev/null @@ -1,11 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - Title - - - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/final.jsp b/src/main/webapp/WEB-INF/jsp/final.jsp new file mode 100644 index 0000000..8a65fb0 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/final.jsp @@ -0,0 +1,97 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%--@elvariable id="step" type="com.javarush.trukhanova.entity.QuestStep"--%> +<%--@elvariable id="player" type="com.javarush.trukhanova.entity.Player"--%> + + + Финал — ${step.title} + + + +
+ +

${step.title}

+ + +

${sessionScope.player.name}

+

Завершено экспедиций: ${sessionScope.player.gamesPlayed}

+ +
${step.description}
+ + +
+ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/game.jsp b/src/main/webapp/WEB-INF/jsp/game.jsp new file mode 100644 index 0000000..95b019a --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/game.jsp @@ -0,0 +1,195 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%--@elvariable id="step" type="com.javarush.trukhanova.entity.QuestStep"--%> + + + ${step.title} + + + +
+ +
+
Осталось времени20 сек.
+
+
+
+ +
+
+ Игрок: ${sessionScope.player.name} | Экспедиций: ${sessionScope.player.gamesPlayed} +
+ Avatar +
+ +

${step.title}

+ +
+ +
+
+ +
${step.description}
+
+ +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/list-user.jsp b/src/main/webapp/WEB-INF/list-user.jsp deleted file mode 100644 index dd52c55..0000000 --- a/src/main/webapp/WEB-INF/list-user.jsp +++ /dev/null @@ -1,9 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@include file="head.jsp"%> - - - ${user.login} - - - - diff --git a/src/main/webapp/WEB-INF/start-page.jsp b/src/main/webapp/WEB-INF/start-page.jsp deleted file mode 100644 index 0531c1c..0000000 --- a/src/main/webapp/WEB-INF/start-page.jsp +++ /dev/null @@ -1,8 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> -<%@include file="head.jsp"%> - -

<%= "Hello World!" %> -

-
-List Users - diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 0bf2fcb..5cb4fc3 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -3,4 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> + + index.jsp + \ No newline at end of file diff --git a/src/main/webapp/images/cat.png b/src/main/webapp/images/cat.png deleted file mode 100644 index 41771a1..0000000 Binary files a/src/main/webapp/images/cat.png and /dev/null differ diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp new file mode 100644 index 0000000..7d01d1c --- /dev/null +++ b/src/main/webapp/index.jsp @@ -0,0 +1,122 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="jakarta.tags.core" %> + + + Экспедиция в джунгли + + + +
+

Начало экспедиции

+
+ + + + Выберите облик героя +
+ + + +
+ +
+
+ + \ No newline at end of file diff --git a/src/main/webapp/static/images/amulet.jpg b/src/main/webapp/static/images/amulet.jpg new file mode 100644 index 0000000..0c8f8d5 Binary files /dev/null and b/src/main/webapp/static/images/amulet.jpg differ diff --git a/src/main/webapp/static/images/avatars/1.png b/src/main/webapp/static/images/avatars/1.png new file mode 100644 index 0000000..e9728f7 Binary files /dev/null and b/src/main/webapp/static/images/avatars/1.png differ diff --git a/src/main/webapp/static/images/avatars/2.png b/src/main/webapp/static/images/avatars/2.png new file mode 100644 index 0000000..1c7e584 Binary files /dev/null and b/src/main/webapp/static/images/avatars/2.png differ diff --git a/src/main/webapp/static/images/avatars/3.png b/src/main/webapp/static/images/avatars/3.png new file mode 100644 index 0000000..7e4019c Binary files /dev/null and b/src/main/webapp/static/images/avatars/3.png differ diff --git a/src/main/webapp/static/images/avatars/4.png b/src/main/webapp/static/images/avatars/4.png new file mode 100644 index 0000000..8395f72 Binary files /dev/null and b/src/main/webapp/static/images/avatars/4.png differ diff --git a/src/main/webapp/static/images/avatars/5.png b/src/main/webapp/static/images/avatars/5.png new file mode 100644 index 0000000..2eab000 Binary files /dev/null and b/src/main/webapp/static/images/avatars/5.png differ diff --git a/src/main/webapp/static/images/avatars/6.png b/src/main/webapp/static/images/avatars/6.png new file mode 100644 index 0000000..1887e71 Binary files /dev/null and b/src/main/webapp/static/images/avatars/6.png differ diff --git a/src/main/webapp/static/images/avatars/7.png b/src/main/webapp/static/images/avatars/7.png new file mode 100644 index 0000000..1e39a23 Binary files /dev/null and b/src/main/webapp/static/images/avatars/7.png differ diff --git a/src/main/webapp/static/images/avatars/8.png b/src/main/webapp/static/images/avatars/8.png new file mode 100644 index 0000000..f3fbfc8 Binary files /dev/null and b/src/main/webapp/static/images/avatars/8.png differ diff --git a/src/main/webapp/static/images/avatars/9.png b/src/main/webapp/static/images/avatars/9.png new file mode 100644 index 0000000..45214d8 Binary files /dev/null and b/src/main/webapp/static/images/avatars/9.png differ diff --git a/src/main/webapp/static/images/lost.jpg b/src/main/webapp/static/images/lost.jpg new file mode 100644 index 0000000..432e1c0 Binary files /dev/null and b/src/main/webapp/static/images/lost.jpg differ diff --git a/src/main/webapp/static/images/riddle.jpg b/src/main/webapp/static/images/riddle.jpg new file mode 100644 index 0000000..66d5f29 Binary files /dev/null and b/src/main/webapp/static/images/riddle.jpg differ diff --git a/src/main/webapp/static/images/river.jpg b/src/main/webapp/static/images/river.jpg new file mode 100644 index 0000000..6bfb546 Binary files /dev/null and b/src/main/webapp/static/images/river.jpg differ diff --git a/src/main/webapp/static/images/start.jpg b/src/main/webapp/static/images/start.jpg new file mode 100644 index 0000000..243ff10 Binary files /dev/null and b/src/main/webapp/static/images/start.jpg differ diff --git a/src/main/webapp/static/images/temple.jpg b/src/main/webapp/static/images/temple.jpg new file mode 100644 index 0000000..62a09fb Binary files /dev/null and b/src/main/webapp/static/images/temple.jpg differ diff --git a/src/main/webapp/static/images/throne.jpg b/src/main/webapp/static/images/throne.jpg new file mode 100644 index 0000000..45c25cc Binary files /dev/null and b/src/main/webapp/static/images/throne.jpg differ diff --git a/src/main/webapp/static/images/totem.png b/src/main/webapp/static/images/totem.png new file mode 100644 index 0000000..6a40d32 Binary files /dev/null and b/src/main/webapp/static/images/totem.png differ diff --git a/src/main/webapp/static/images/village.jpg b/src/main/webapp/static/images/village.jpg new file mode 100644 index 0000000..9e5d80a Binary files /dev/null and b/src/main/webapp/static/images/village.jpg differ diff --git a/src/main/webapp/static/images/win.jpg b/src/main/webapp/static/images/win.jpg new file mode 100644 index 0000000..af7550e Binary files /dev/null and b/src/main/webapp/static/images/win.jpg differ diff --git a/src/test/java/com/javarush/trukhanova/entity/PlayerTest.java b/src/test/java/com/javarush/trukhanova/entity/PlayerTest.java new file mode 100644 index 0000000..1487056 --- /dev/null +++ b/src/test/java/com/javarush/trukhanova/entity/PlayerTest.java @@ -0,0 +1,23 @@ +package com.javarush.trukhanova.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class PlayerTest { + + @Test + @DisplayName("Проверка создания игрока и инкремента счетчика") + void shouldCreatePlayerAndIncrementGames() { + Player player = new Player("Герой", "static/images/avatars/1.png"); + + assertAll("Инициализация игрока", + () -> assertEquals("Герой", player.getName()), + () -> assertEquals("static/images/avatars/1.png", player.getAvatarPath()), + () -> assertEquals(0, player.getGamesPlayed(), "На старте должно быть 0 игр") + ); + + player.incrementGamesPlayed(); + assertEquals(1, player.getGamesPlayed(), "Счетчик должен стать равным 1"); + } +} \ No newline at end of file diff --git a/src/test/java/com/javarush/trukhanova/service/GameServiceTest.java b/src/test/java/com/javarush/trukhanova/service/GameServiceTest.java new file mode 100644 index 0000000..8957661 --- /dev/null +++ b/src/test/java/com/javarush/trukhanova/service/GameServiceTest.java @@ -0,0 +1,83 @@ +package com.javarush.trukhanova.service; + +import com.javarush.trukhanova.entity.QuestStep; +import com.javarush.trukhanova.entity.Answer; +import com.javarush.trukhanova.exception.StepNotFoundException; +import com.javarush.trukhanova.repository.QuestRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GameServiceTest { + + private GameService gameService; + + @Mock + private QuestRepository repository; + + @BeforeEach + void setUp() { + gameService = new GameService(repository); + } + + @Test + @DisplayName("Должен возвращать корректный шаг из репозитория") + void shouldReturnCorrectStepFromRepository() { + QuestStep mockStep = new QuestStep(1, "Начало", "Описаниe", "img.png", Collections.emptyList()); + when(repository.getById(1)).thenReturn(mockStep); + + QuestStep result = gameService.getNextStep(1); + + assertNotNull(result); + assertEquals("Начало", result.getTitle()); + verify(repository).getById(1); + } + + @Test + @DisplayName("Должен выбрасывать исключение при ошибке репозитория") + void shouldThrowExceptionWhenRepositoryFails() { + when(repository.getById(anyInt())).thenThrow(new StepNotFoundException("Шаг не найден")); + + assertThrows(StepNotFoundException.class, () -> gameService.getNextStep(999)); + } + + @Test + @DisplayName("Должен подтверждать окончание игры (нет ответов)") + void shouldIdentifyGameOver() { + QuestStep finalStep = new QuestStep(); + finalStep.setTitle("Финал"); + finalStep.setAnswers(Collections.emptyList()); + + assertTrue(gameService.isGameOver(finalStep)); + } + + @Test + @DisplayName("Должен понимать, что игра продолжается (есть ответы)") + void shouldIdentifyThatGameContinues() { + QuestStep step = new QuestStep(); + step.setTitle("Обычный шаг"); + step.setAnswers(List.of(new Answer("Вперед", 2))); + + assertFalse(gameService.isGameOver(step)); + } + + @Test + @DisplayName("Безопасная проверка на null в списке ответов") + void shouldHandleNullAnswersList() { + QuestStep step = new QuestStep(); + step.setTitle("Технический шаг"); + step.setAnswers(null); + + assertTrue(gameService.isGameOver(step), "Если списка ответов нет (null), игра должна считаться оконченной"); + } +} \ No newline at end of file