From 0d8f03c8e2f18cc88f85f387429128b0e1507562 Mon Sep 17 00:00:00 2001 From: KirillB Date: Fri, 9 Jan 2026 18:32:07 +0300 Subject: [PATCH 1/2] Add Java servlets project --- .gitignore | 39 ++ pom.xml | 126 ++++ src/main/java/controller/EncodingFilter.java | 26 + src/main/java/controller/QuestServlet.java | 129 ++++ src/main/java/controller/WelcomeServlet.java | 64 ++ src/main/java/model/PlayerState.java | 25 + src/main/java/model/QuestStep.java | 58 ++ src/main/java/service/QuestService.java | 287 +++++++++ src/main/webapp/WEB-INF/web.xml | 33 + src/main/webapp/quest.jsp | 488 ++++++++++++++ src/main/webapp/welcome.jsp | 596 ++++++++++++++++++ .../java/controller/QuestServletTest.java | 177 ++++++ .../java/controller/WelcomeServletTest.java | 109 ++++ src/test/java/model/PlayerStateTest.java | 108 ++++ src/test/java/model/QuestStepTest.java | 140 ++++ src/test/java/service/QuestServiceTest.java | 270 ++++++++ 16 files changed, 2675 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/controller/EncodingFilter.java create mode 100644 src/main/java/controller/QuestServlet.java create mode 100644 src/main/java/controller/WelcomeServlet.java create mode 100644 src/main/java/model/PlayerState.java create mode 100644 src/main/java/model/QuestStep.java create mode 100644 src/main/java/service/QuestService.java create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/quest.jsp create mode 100644 src/main/webapp/welcome.jsp create mode 100644 src/test/java/controller/QuestServletTest.java create mode 100644 src/test/java/controller/WelcomeServletTest.java create mode 100644 src/test/java/model/PlayerStateTest.java create mode 100644 src/test/java/model/QuestStepTest.java create mode 100644 src/test/java/service/QuestServiceTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4ffc6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +.idea/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bf30f27 --- /dev/null +++ b/pom.xml @@ -0,0 +1,126 @@ + + 4.0.0 + + com.javarush + sandbox-servlets + war + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 4.0.1 + 2.3.3 + 1.2 + 5.9.2 + 4.11.0 + + + + + javax.servlet + javax.servlet-api + ${servlet.api.version} + provided + + + + javax.servlet.jsp + javax.servlet.jsp-api + ${jsp.api.version} + provided + + + + javax.servlet + jstl + ${jstl.version} + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + + org.mockito + mockito-core + ${mockito.version} + test + + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + false + + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + 8080 + /sandbox-servlets + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + + **/*Test.java + + + + + + \ No newline at end of file diff --git a/src/main/java/controller/EncodingFilter.java b/src/main/java/controller/EncodingFilter.java new file mode 100644 index 0000000..cb380b0 --- /dev/null +++ b/src/main/java/controller/EncodingFilter.java @@ -0,0 +1,26 @@ +package controller; + +import javax.servlet.*; +import java.io.IOException; + +public class EncodingFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // Устанавливаем кодировку для всех запросов + request.setCharacterEncoding("UTF-8"); + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/html;charset=UTF-8"); + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/src/main/java/controller/QuestServlet.java b/src/main/java/controller/QuestServlet.java new file mode 100644 index 0000000..f675e1b --- /dev/null +++ b/src/main/java/controller/QuestServlet.java @@ -0,0 +1,129 @@ +package controller; + +import model.QuestStep; +import model.PlayerState; +import service.QuestService; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +@WebServlet("/quest") +public class QuestServlet extends HttpServlet { + private QuestService questService; + + @Override + public void init() throws ServletException { + super.init(); + questService = new QuestService(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(true); + + // Проверяем, ввел ли игрок имя + String playerName = (String) session.getAttribute("playerName"); + if (playerName == null || playerName.trim().isEmpty()) { + response.sendRedirect(request.getContextPath() + "/welcome"); + return; + } + + // Обработка сброса игры + String action = request.getParameter("action"); + if ("restart".equals(action)) { + session.removeAttribute("playerState"); + session.removeAttribute("errorMessage"); + } + + // Получаем или создаем состояние игрока + PlayerState playerState = (PlayerState) session.getAttribute("playerState"); + if (playerState == null) { + playerState = new PlayerState(); + session.setAttribute("playerState", playerState); + } + + // Получаем текущий шаг + String currentStepId = playerState.getCurrentStepId(); + QuestStep step = questService.getStep(currentStepId); + + // Если шаг не найден, начинаем сначала + if (step == null) { + playerState.setCurrentStepId("start"); + step = questService.getStep("start"); + } + + // Передаем данные в JSP + request.setAttribute("step", step); + request.setAttribute("playerState", playerState); + request.setAttribute("playerName", playerName); + + // Передаем сообщение об ошибке, если есть + String errorMessage = (String) session.getAttribute("errorMessage"); + if (errorMessage != null) { + request.setAttribute("errorMessage", errorMessage); + session.removeAttribute("errorMessage"); + } + + request.getRequestDispatcher("/quest.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(); + + // Проверяем, есть ли имя игрока + String playerName = (String) session.getAttribute("playerName"); + if (playerName == null || playerName.trim().isEmpty()) { + response.sendRedirect(request.getContextPath() + "/welcome"); + return; + } + + PlayerState playerState = (PlayerState) session.getAttribute("playerState"); + if (playerState == null) { + response.sendRedirect(request.getContextPath() + "/quest"); + return; + } + + String choice = request.getParameter("choice"); + session.removeAttribute("errorMessage"); + + if (choice != null && !choice.trim().isEmpty()) { + String previousStepId = playerState.getCurrentStepId(); + QuestStep nextStep = questService.processChoice(playerState, choice); + + if (nextStep != null) { + // Проверяем, не остались ли мы на том же шаге (зацикливание) + if (nextStep.getId().equals(previousStepId)) { + setErrorMessage(session, previousStepId, choice); + } + } else { + session.setAttribute("errorMessage", "Не удалось обработать ваш выбор. Попробуйте еще раз."); + } + } else { + session.setAttribute("errorMessage", "Вы не сделали выбор!"); + } + + // Сохраняем обновленное состояние + session.setAttribute("playerState", playerState); + response.sendRedirect(request.getContextPath() + "/quest"); + } + + private void setErrorMessage(HttpSession session, String stepId, String choice) { + String errorMessage = "Для этого действия нужен предмет из инвентаря!"; + + if ("dark_tunnel".equals(stepId) && choice.contains("лампой")) { + errorMessage = "Нужна лампа, чтобы идти вперёд в темноте!"; + } else if ("hall_escape".equals(stepId) && choice.contains("ключом")) { + errorMessage = "Нужен ключ, чтобы открыть эту дверь!"; + } + + session.setAttribute("errorMessage", errorMessage); + } +} \ No newline at end of file diff --git a/src/main/java/controller/WelcomeServlet.java b/src/main/java/controller/WelcomeServlet.java new file mode 100644 index 0000000..7433ff3 --- /dev/null +++ b/src/main/java/controller/WelcomeServlet.java @@ -0,0 +1,64 @@ +package controller; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +@WebServlet("/welcome") +public class WelcomeServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(); + + // Проверяем, пришел ли запрос на смену игрока + String action = request.getParameter("action"); + if ("changePlayer".equals(action)) { + // Очищаем данные текущего игрока + session.removeAttribute("playerName"); + session.removeAttribute("playerState"); + session.removeAttribute("errorMessage"); + System.out.println("DEBUG: Player data cleared for change player"); + } + + // Проверяем, есть ли уже имя в сессии + String playerName = (String) session.getAttribute("playerName"); + + if (playerName != null && !playerName.trim().isEmpty()) { + // Если имя уже есть, перенаправляем в квест + response.sendRedirect(request.getContextPath() + "/quest"); + return; + } + + // Иначе показываем страницу приветствия + request.getRequestDispatcher("/welcome.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(); + String playerName = request.getParameter("playerName"); + + if (playerName != null && !playerName.trim().isEmpty()) { + // Сохраняем имя в сессии + session.setAttribute("playerName", playerName.trim()); + System.out.println("DEBUG: Player name set: " + playerName); + + // Сбрасываем игру для нового игрока + session.removeAttribute("playerState"); + session.removeAttribute("errorMessage"); + + // Перенаправляем в квест + response.sendRedirect(request.getContextPath() + "/quest"); + } else { + // Если имя не введено, возвращаем с ошибкой + request.setAttribute("error", "Пожалуйста, введите ваше имя!"); + request.getRequestDispatcher("/welcome.jsp").forward(request, response); + } + } +} \ No newline at end of file diff --git a/src/main/java/model/PlayerState.java b/src/main/java/model/PlayerState.java new file mode 100644 index 0000000..06ce22a --- /dev/null +++ b/src/main/java/model/PlayerState.java @@ -0,0 +1,25 @@ +package model; + +import java.util.HashSet; +import java.util.Set; + +public class PlayerState { + private String currentStepId; + private Set inventory = new HashSet<>(); + private boolean gameOver = false; + + public PlayerState() { + this.currentStepId = "start"; + } + + public String getCurrentStepId() { return currentStepId; } + public void setCurrentStepId(String currentStepId) { this.currentStepId = currentStepId; } + + public Set getInventory() { return new HashSet<>(inventory); } + public void addToInventory(String item) { inventory.add(item); } + public boolean hasItem(String item) { return inventory.contains(item); } + public void removeFromInventory(String item) { inventory.remove(item); } + + public boolean isGameOver() { return gameOver; } + public void setGameOver(boolean gameOver) { this.gameOver = gameOver; } +} \ No newline at end of file diff --git a/src/main/java/model/QuestStep.java b/src/main/java/model/QuestStep.java new file mode 100644 index 0000000..c3fd326 --- /dev/null +++ b/src/main/java/model/QuestStep.java @@ -0,0 +1,58 @@ +package model; + +import java.util.Map; +import java.util.Objects; + +public class QuestStep { + private String id; + private String text; + private Map options; // вариант -> nextStepId + private Map requirements; // требования для доступности шага + private Map effects; // эффекты при выборе варианта + private boolean isEnding = false; + + public QuestStep() { + } + + public QuestStep(String id, String text, Map options, + Map requirements, Map effects, boolean isEnding) { + this.id = id; + this.text = text; + this.options = options; + this.requirements = requirements; + this.effects = effects; + this.isEnding = isEnding; + } + + // Геттеры и сеттеры + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public Map getOptions() { return options; } + public void setOptions(Map options) { this.options = options; } + + public Map getRequirements() { return requirements; } + public void setRequirements(Map requirements) { this.requirements = requirements; } + + public Map getEffects() { return effects; } + public void setEffects(Map effects) { this.effects = effects; } + + public boolean isEnding() { return isEnding; } + public void setEnding(boolean ending) { isEnding = ending; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QuestStep questStep = (QuestStep) o; + return Objects.equals(id, questStep.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} \ No newline at end of file diff --git a/src/main/java/service/QuestService.java b/src/main/java/service/QuestService.java new file mode 100644 index 0000000..6f77ce2 --- /dev/null +++ b/src/main/java/service/QuestService.java @@ -0,0 +1,287 @@ +package service; + +import model.QuestStep; +import model.PlayerState; +import java.util.*; + +public class QuestService { + private final Map steps = new HashMap<>(); + + public QuestService() { + initSteps(); + } + + private void initSteps() { + // 1. Стартовая комната + steps.put("start", new QuestStep("start", + "Вы просыпаетесь в затемнённой комнате. Голова гудит, воспоминания смутны. " + + "Вы лежите на холодном каменном полу. Единственный источник света — тусклый луч из-под двери прямо перед вами. " + + "Слева, в стене, вы замечаете небольшое зарешеченное окно на уровне пола.", + Map.of( + "Подойти и попробовать открыть дверь", "door_initial", + "Исследовать окно в стене", "window_initial", + "Осмотреться по сторонам в самой комнате", "inspect_room" + ), null, null, false)); + + // 2. Дверь (первый подход) + steps.put("door_initial", new QuestStep("door_initial", + "Дверь заперта, но неплотно притворена. В щель виден коридор, освещённый горящими факелами. " + + "На полу перед дверью лежит смятый клочок пергамента. На нём всего два слова: **\"Не кричи\"**. " + + "Вдалеке слышны неясные шаги.", + Map.of( + "Тихо протиснуться в коридор и пойти НАЛЕВО", "hall_guarded", + "Тихо протиснуться в коридор и пойти НАПРАВО", "storage_room", + "Кричать о помощи", "bad_end_noise" + ), null, null, false)); + + // 3. Окно (первый подход) + steps.put("window_initial", new QuestStep("window_initial", + "Подползая к окну, вы видите, что решетка шатается. За окном — сырая узкая каменная шахта, уходящая вверх. " + + "Рядом с окном, в пыли, вы нащупываете небольшой **ржавый ключ**.", + Map.of( + "Попробовать оторвать решётку и лезть в шахту", "bad_end_shaft", + "Вернуться к двери (подобрать ключ)", "door_unlocked" + ), null, Map.of("Вернуться к двери (подобрать ключ)", "key"), false)); + + // 4. Дверь открыта (есть ключ) + steps.put("door_unlocked", new QuestStep("door_unlocked", + "Ключ подходит! Дверь тихо открывается. Вы в коридоре с факелами. " + + "Налево слышны шаги, направо — тишина.", + Map.of( + "Пойти НАЛЕВО (слышны шаги)", "hall_guarded", + "Пойти НАПРАВО (тишина)", "storage_room" + ), null, null, false)); + + // 5. Кладовая + steps.put("storage_room", new QuestStep("storage_room", + "Вы крадётесь по тёмному коридору. Он приводит к небольшой кладовой. " + + "На столе лежит связка старых ключей и горит **масляная лампа**. " + + "На стене нацарапано: \"Он боится света\".", + Map.of( + "Взять лампу и вернуться к развилке", "corridor_with_lamp", + "Взять лампу и пойти дальше", "dark_tunnel", + "Проигнорировать лампу и пойти дальше", "dark_tunnel_no_lamp" + ), null, Map.of( + "Взять лампу и вернуться к развилке", "lamp", + "Взять лампу и пойти дальше", "lamp" + ), false)); + + // 6. Осмотр комнаты + steps.put("inspect_room", new QuestStep("inspect_room", + "Внимательно осмотрев комнату, вы находите под соломой старую **карту подземелья**. " + + "Теперь вы знаете планировку этого уровня.", + Map.of( + "Идти к двери", "door_initial", + "Осмотреть окно внимательнее", "window_with_knowledge" + ), null, Map.of("Осмотреть окно внимательнее", "knowledge"), false)); + + // 7. Окно со знанием карты + steps.put("window_with_knowledge", new QuestStep("window_with_knowledge", + "Благодаря карте вы понимаете, что шахта — это ловушка, ведущая в тупик. " + + "Но вы всё равно можете взять **ржавый ключ**, лежащий у окна.", + Map.of("Вернуться к двери (я знаю о ловушке)", "door_unlocked_smart"), + null, Map.of("Вернуться к двери (я знаю о ловушке)", "key"), false)); + + // 8. Дверь открыта (умный путь) + steps.put("door_unlocked_smart", new QuestStep("door_unlocked_smart", + "С картой в голове вы выходите в коридор. Вы знаете, что налево — страж, " + + "а направо — кладовая с полезными предметами.", + Map.of( + "Пойти НАЛЕВО (я знаю о страже)", "hall_guarded_smart", + "Пойти НАПРАВО (я знаю о кладовой)", "storage_room" + ), null, null, false)); + + // 9. Зал со стражем (обычный) + steps.put("hall_guarded", new QuestStep("hall_guarded", + "Вы выходите в просторный зал. Посреди него на троне сидит спящий страж в ржавых доспехах. " + + "За его спиной виден выход — большая дубовая дверь. " + + "Страж крепко сжимает в руке сияющий **голубой кристалл**.", + Map.of( + "Попробовать вытащить кристалл из руки стража", "bad_end_crystal", + "Обойти стража и попробовать открыть дверь", "final_door_no_key", + "Вернуться назад", "door_unlocked" + ), null, null, false)); + + // 10. Зал со стражем (умный) + steps.put("hall_guarded_smart", new QuestStep("hall_guarded_smart", + "Благодаря карте вы знаете о страже. На карте рядом с ним написано: \"Боится света\".", + Map.of( + "Использовать знание о страже", "hall_guarded_prepared", + "Попробовать вытащить кристалл", "bad_end_crystal" + ), null, null, false)); + + // 11. Подготовка в зале + steps.put("hall_guarded_prepared", new QuestStep("hall_guarded_prepared", + "У вас есть лампа, и вы знаете, что страж боится света.", + Map.of( + "Осветить лицо стража лампой", "hall_escape", + "Вернуться за лампой", "storage_room" + ), Map.of("lamp", true), null, false)); + + // 12. Побег из зала + steps.put("hall_escape", new QuestStep("hall_escape", + "Вы ослепляете стража лампой и подбегаете к двери.", + Map.of( + "Открыть дверь ключом", "good_end_freedom", + "Попытаться выбить дверь", "bad_end_caught" + ), Map.of("key", true), null, false)); + + // 13. Финальная дверь без ключа + steps.put("final_door_no_key", new QuestStep("final_door_no_key", + "Дверь заперта. Без ключа её не открыть.", + Map.of( + "Попытаться выбить дверь", "bad_end_caught", + "Вернуться назад", "hall_guarded" + ), null, null, false)); + + // 14. Коридор с лампой + steps.put("corridor_with_lamp", new QuestStep("corridor_with_lamp", + "С лампой в руках вы возвращаетесь на развилку.", + Map.of( + "Пойти налево к стражу", "hall_guarded", + "Осветить стены лампой", "hidden_path" + ), null, null, false)); + + // 15. Скрытый путь + steps.put("hidden_path", new QuestStep("hidden_path", + "Свет лампы выхватывает из темноты скрытый проход в стене.", + Map.of("Пройти по скрытому ходу", "good_end_secret"), + null, null, false)); + + // 16. Темный туннель (с проверкой лампы) + steps.put("dark_tunnel", new QuestStep("dark_tunnel", + "За дверью — абсолютно тёмный сырой туннель. Без света здесь опасно.", + Map.of( + "Идти вперёд с лампой", "good_end_tunnel", + "Идти на ощупь", "bad_end_dark" + ), Map.of("lamp", true), null, false)); + + // 17. Темный туннель без лампы + steps.put("dark_tunnel_no_lamp", new QuestStep("dark_tunnel_no_lamp", + "Без лампы в темноте не видно даже собственной руки.\n" + + "Это кажется крайне неразумным решением.", + Map.of( + "Вернуться в кладовую", "storage_room", + "Все равно идти вперёд", "bad_end_dark" + ), null, null, false)); + + // КОНЦОВКИ + steps.put("bad_end_noise", new QuestStep("bad_end_noise", + "Из темноты появляется страж. Удар по голове — и вы теряете сознание.\n\n" + + "**Конец. Ваша любовь к шуму погубила вас.**", + new HashMap<>(), null, null, true)); + + steps.put("bad_end_shaft", new QuestStep("bad_end_shaft", + "Вы карабкаетесь по шахте, но камень под ногой обваливается...\n\n" + + "**Конец. Ваше восхождение было недолгим.**", + new HashMap<>(), null, null, true)); + + steps.put("bad_end_crystal", new QuestStep("bad_end_crystal", + "Кристалл выскальзывает из руки стража. Он просыпается, и его глаза вспыхивают красным светом!\n\n" + + "**Конец. Вы нашли источник силы, но не смогли им воспользоваться.**", + new HashMap<>(), null, null, true)); + + steps.put("bad_end_dark", new QuestStep("bad_end_dark", + "Вы шагаете в темноту и проваливаетесь в яму.\n\n" + + "**Конец. Свет был бы полезен.**", + new HashMap<>(), null, null, true)); + + steps.put("bad_end_caught", new QuestStep("bad_end_caught", + "Шум будит стража. Тяжёлая длань хватает вас...\n\n" + + "**Конец. Выход был так близок.**", + new HashMap<>(), null, null, true)); + + steps.put("good_end_freedom", new QuestStep("good_end_freedom", + "Ключ поворачивается! Дверь открывается, впуская свежий ночной воздух.\n\n" + + "**Поздравляем! Вы сбежали благодаря находчивости и подобранным предметам.**\n\n" + + "*Путь: Ключ + Лампа + Знание*", + new HashMap<>(), null, null, true)); + + steps.put("good_end_tunnel", new QuestStep("good_end_tunnel", + "Лампа освещает путь. Туннель выводит вас к лестнице, ведущей на поверхность!\n\n" + + "**Поздравляем! Ваша осторожность (и лампа) спасли вас.**\n\n" + + "*Путь: Лампа*", + new HashMap<>(), null, null, true)); + + steps.put("good_end_secret", new QuestStep("good_end_secret", + "Свет лампы показывает скрытую дверь. Вы выходите в старую часовню!\n\n" + + "**Поздравляем! Вы нашли тайный путь к спасению.**\n\n" + + "*Путь: Лампа + Внимательность*", + new HashMap<>(), null, null, true)); + } + + public QuestStep getStep(String stepId) { + return steps.get(stepId); + } + + public QuestStep processChoice(PlayerState playerState, String choice) { + if (playerState == null || choice == null) { + System.out.println("ERROR: processChoice - playerState or choice is null"); + return null; + } + + String currentStepId = playerState.getCurrentStepId(); + QuestStep currentStep = getStep(currentStepId); + + if (currentStep == null) { + System.out.println("ERROR: Current step not found: " + currentStepId); + return null; + } + + if (!currentStep.getOptions().containsKey(choice)) { + System.out.println("WARNING: Choice '" + choice + "' not found in step: " + currentStepId); + return null; + } + + String nextStepId = currentStep.getOptions().get(choice); + + // Специальные проверки для шагов, требующих предметы + // Эти проверки должны выполняться в первую очередь + if ("dark_tunnel".equals(currentStepId) && "Идти вперёд с лампой".equals(choice)) { + if (!playerState.hasItem("lamp")) { + nextStepId = "dark_tunnel_no_lamp"; + } + } + + if ("hall_escape".equals(currentStepId) && "Открыть дверь ключом".equals(choice)) { + if (!playerState.hasItem("key")) { + nextStepId = "final_door_no_key"; + } + } + + // Применяем эффекты выбора (добавляем предметы в инвентарь) + applyEffects(playerState, currentStep, choice); + + // Устанавливаем следующий шаг + playerState.setCurrentStepId(nextStepId); + QuestStep nextStep = getStep(nextStepId); + + if (nextStep == null) { + System.out.println("ERROR: Next step not found: " + nextStepId); + return null; + } + + return nextStep; + } + private void applyEffects(PlayerState playerState, QuestStep step, String choice) { + if (step.getEffects() == null) return; + + String item = step.getEffects().get(choice); + if (item == null) return; + + if (!playerState.hasItem(item)) { + playerState.addToInventory(item); + } + } + + public boolean checkRequirements(PlayerState playerState, QuestStep step) { + if (step.getRequirements() == null) return true; + + for (Map.Entry req : step.getRequirements().entrySet()) { + if (req.getValue() && !playerState.hasItem(req.getKey())) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f94fff3 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + QuestEscape + + + welcome + + + + + EncodingFilter + controller.EncodingFilter + + + + EncodingFilter + /* + + + + 30 + + true + + + + \ No newline at end of file diff --git a/src/main/webapp/quest.jsp b/src/main/webapp/quest.jsp new file mode 100644 index 0000000..e2cc4a3 --- /dev/null +++ b/src/main/webapp/quest.jsp @@ -0,0 +1,488 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + + Побег из Подземелья + + + + + +
+ +
+

Побег из Подземелья

+

Выбери свою судьбу, путник...

+
+ + + +
+ ${errorMessage} +
+
+ + +
+
+ Локация: + + + Старт + + + ✓ Победа + ✗ Поражение + + + ${step.id} + + +
+
+ Игрок: ${playerName} +
+
+ + +
${step.text}
+ + + +
+

+ + + Победа! + + + Конец пути + + +

+

${step.text}

+ + + Новая игра + +
+
+ + + +
+
+ +
+ + +
+
+ + +

+ Нет доступных вариантов +

+
+
+ + + + +
+
+ + + +
+

Инвентарь

+
+ + + + +
+ + + Ржавый ключ + + + Масляная лампа + + + Знание карты + + + ${item} + + +
+
+
+
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/src/main/webapp/welcome.jsp b/src/main/webapp/welcome.jsp new file mode 100644 index 0000000..05c0770 --- /dev/null +++ b/src/main/webapp/welcome.jsp @@ -0,0 +1,596 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + + Побег из Подземелья - Начало + + + + + +
+ +
+

Побег из Подземелья

+

Добро пожаловать, отважный путник!

+
+ + +
+

Предыстория

+
+

Год 1347 от Рождения Второго Солнца. Вы — наёмник, чья удача, казалось, закончилась вместе с последним + золотым в кошельке. Отчаявшись, вы приняли заказ от таинственного незнакомца в капюшоне — + найти древний артефакт, спрятанный в заброшенных катакомбах под замком Лорда Мрака.

+ +

Ночью, под покровом тумана, вы проникли в подземелье. Но удача снова отвернулась от вас — + ловушка сработала, и вы потеряли сознание. Очнулись уже в тёмной камере, без оружия и снаряжения, + с одной лишь целью — выбраться живым.

+ +

Теперь ваша судьба зависит только от ваших решений. Каждый выбор может привести к свободе... + или стать последним в вашей жизни.

+ +
+
+ + +
+

Назови себя, путник

+ +
+
+ +
+ + + + + +
+ ${error} +
+
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/src/test/java/controller/QuestServletTest.java b/src/test/java/controller/QuestServletTest.java new file mode 100644 index 0000000..41ae090 --- /dev/null +++ b/src/test/java/controller/QuestServletTest.java @@ -0,0 +1,177 @@ +package controller; + +import model.PlayerState; +import model.QuestStep; +import service.QuestService; +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 javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class QuestServletTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private HttpSession session; + + @Mock + private RequestDispatcher requestDispatcher; + + @Mock + private QuestService questService; + + @InjectMocks + private QuestServlet questServlet; + + private PlayerState playerState; + private QuestStep questStep; + + @BeforeEach + void setUp() { + questServlet = new QuestServlet(); + + playerState = new PlayerState(); + questStep = new QuestStep(); + questStep.setId("start"); + questStep.setText("Test text"); + questStep.setOptions(new HashMap<>()); + questStep.setEnding(false); + + // Инициализация сервлета + try { + questServlet.init(); + } catch (ServletException e) { + fail("Servlet initialization failed: " + e.getMessage()); + } + } + + @Test + void testDoGetWithoutPlayerName() throws ServletException, IOException { + when(request.getSession(true)).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn(null); + when(request.getContextPath()).thenReturn("/context"); + + questServlet.doGet(request, response); + + verify(response).sendRedirect("/context/welcome"); + } + + @Test + void testDoGetWithPlayerName() throws ServletException, IOException { + when(request.getSession(true)).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn("TestPlayer"); + when(session.getAttribute("playerState")).thenReturn(playerState); + when(session.getAttribute("errorMessage")).thenReturn(null); + when(request.getRequestDispatcher("/quest.jsp")).thenReturn(requestDispatcher); + + // Mock QuestService внутри сервлета + QuestService mockService = mock(QuestService.class); + when(mockService.getStep("start")).thenReturn(questStep); + + // Используем reflection для установки mock service + try { + var field = QuestServlet.class.getDeclaredField("questService"); + field.setAccessible(true); + field.set(questServlet, mockService); + } catch (Exception e) { + fail("Failed to set questService field: " + e.getMessage()); + } + + questServlet.doGet(request, response); + + verify(request).setAttribute("step", questStep); + verify(request).setAttribute("playerState", playerState); + verify(request).setAttribute("playerName", "TestPlayer"); + verify(requestDispatcher).forward(request, response); + } + + @Test + void testDoGetWithRestartAction() throws ServletException, IOException { + when(request.getSession(true)).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn("TestPlayer"); + when(request.getParameter("action")).thenReturn("restart"); + when(request.getRequestDispatcher("/quest.jsp")).thenReturn(requestDispatcher); + + questServlet.doGet(request, response); + + verify(session).removeAttribute("playerState"); + verify(session).removeAttribute("errorMessage"); + } + + @Test + void testDoPostWithoutPlayerName() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn(null); + when(request.getContextPath()).thenReturn("/context"); + + questServlet.doPost(request, response); + + verify(response).sendRedirect("/context/welcome"); + } + + @Test + void testDoPostWithoutPlayerState() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn("TestPlayer"); + when(session.getAttribute("playerState")).thenReturn(null); + when(request.getContextPath()).thenReturn("/context"); + + questServlet.doPost(request, response); + + verify(response).sendRedirect("/context/quest"); + } + + @Test + void testDoPostWithEmptyChoice() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn("TestPlayer"); + when(session.getAttribute("playerState")).thenReturn(playerState); + when(request.getParameter("choice")).thenReturn(""); + when(request.getContextPath()).thenReturn("/context"); + + questServlet.doPost(request, response); + + verify(session).setAttribute("errorMessage", "Вы не сделали выбор!"); + verify(response).sendRedirect("/context/quest"); + } + + @Test + void testSetErrorMessageDarkTunnel() { + QuestServlet servlet = new QuestServlet(); + + // Используем reflection для тестирования приватного метода + try { + var method = QuestServlet.class.getDeclaredMethod("setErrorMessage", + HttpSession.class, String.class, String.class); + method.setAccessible(true); + + HttpSession mockSession = mock(HttpSession.class); + + method.invoke(servlet, mockSession, "dark_tunnel", "Идти вперёд с лампой"); + verify(mockSession).setAttribute("errorMessage", "Нужна лампа, чтобы идти вперёд в темноте!"); + + } catch (Exception e) { + fail("Failed to test setErrorMessage: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/controller/WelcomeServletTest.java b/src/test/java/controller/WelcomeServletTest.java new file mode 100644 index 0000000..1983f6b --- /dev/null +++ b/src/test/java/controller/WelcomeServletTest.java @@ -0,0 +1,109 @@ +package controller; + +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 javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class WelcomeServletTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private HttpSession session; + + @Mock + private RequestDispatcher requestDispatcher; + + @InjectMocks + private WelcomeServlet welcomeServlet; + + @Test + void testDoGetWithExistingPlayer() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn("ExistingPlayer"); + when(request.getContextPath()).thenReturn("/context"); + + welcomeServlet.doGet(request, response); + + verify(response).sendRedirect("/context/quest"); + } + + @Test + void testDoGetWithoutPlayer() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(session.getAttribute("playerName")).thenReturn(null); + when(request.getRequestDispatcher("/welcome.jsp")).thenReturn(requestDispatcher); + + welcomeServlet.doGet(request, response); + + verify(requestDispatcher).forward(request, response); + } + + @Test + void testDoPostWithValidName() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(request.getParameter("playerName")).thenReturn("NewPlayer"); + when(request.getContextPath()).thenReturn("/context"); + + welcomeServlet.doPost(request, response); + + verify(session).setAttribute("playerName", "NewPlayer"); + verify(session).removeAttribute("playerState"); + verify(session).removeAttribute("errorMessage"); + verify(response).sendRedirect("/context/quest"); + } + + @Test + void testDoPostWithEmptyName() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(request.getParameter("playerName")).thenReturn(""); + when(request.getRequestDispatcher("/welcome.jsp")).thenReturn(requestDispatcher); + + welcomeServlet.doPost(request, response); + + verify(request).setAttribute("error", "Пожалуйста, введите ваше имя!"); + verify(requestDispatcher).forward(request, response); + } + + @Test + void testDoPostWithNullName() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(request.getParameter("playerName")).thenReturn(null); + when(request.getRequestDispatcher("/welcome.jsp")).thenReturn(requestDispatcher); + + welcomeServlet.doPost(request, response); + + verify(request).setAttribute("error", "Пожалуйста, введите ваше имя!"); + verify(requestDispatcher).forward(request, response); + } + + @Test + void testDoGetWithChangePlayerAction() throws ServletException, IOException { + when(request.getSession()).thenReturn(session); + when(request.getParameter("action")).thenReturn("changePlayer"); + when(request.getRequestDispatcher("/welcome.jsp")).thenReturn(requestDispatcher); + + welcomeServlet.doGet(request, response); + + verify(session).removeAttribute("playerName"); + verify(session).removeAttribute("playerState"); + verify(session).removeAttribute("errorMessage"); + verify(requestDispatcher).forward(request, response); + } +} \ No newline at end of file diff --git a/src/test/java/model/PlayerStateTest.java b/src/test/java/model/PlayerStateTest.java new file mode 100644 index 0000000..7804945 --- /dev/null +++ b/src/test/java/model/PlayerStateTest.java @@ -0,0 +1,108 @@ +package model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class PlayerStateTest { + private PlayerState playerState; + + @BeforeEach + void setUp() { + playerState = new PlayerState(); + } + + @Test + void testDefaultConstructor() { + assertEquals("start", playerState.getCurrentStepId()); + assertNotNull(playerState.getInventory()); + assertTrue(playerState.getInventory().isEmpty()); + assertFalse(playerState.isGameOver()); + } + + @Test + void testSetAndGetCurrentStepId() { + playerState.setCurrentStepId("test_step"); + assertEquals("test_step", playerState.getCurrentStepId()); + } + + @Test + void testAddToInventory() { + playerState.addToInventory("key"); + assertTrue(playerState.hasItem("key")); + assertEquals(1, playerState.getInventory().size()); + } + + @Test + void testAddDuplicateItem() { + playerState.addToInventory("key"); + playerState.addToInventory("key"); + assertTrue(playerState.hasItem("key")); + assertEquals(1, playerState.getInventory().size()); + } + + @Test + void testHasItem() { + assertFalse(playerState.hasItem("key")); + playerState.addToInventory("key"); + assertTrue(playerState.hasItem("key")); + assertFalse(playerState.hasItem("nonexistent")); + } + + @Test + void testRemoveFromInventory() { + playerState.addToInventory("key"); + playerState.addToInventory("lamp"); + + playerState.removeFromInventory("key"); + + assertFalse(playerState.hasItem("key")); + assertTrue(playerState.hasItem("lamp")); + assertEquals(1, playerState.getInventory().size()); + } + + @Test + void testRemoveNonexistentItem() { + playerState.removeFromInventory("nonexistent"); + assertEquals(0, playerState.getInventory().size()); + } + + @Test + void testSetAndIsGameOver() { + playerState.setGameOver(true); + assertTrue(playerState.isGameOver()); + + playerState.setGameOver(false); + assertFalse(playerState.isGameOver()); + } + + @Test + void testGetInventoryReturnsCopy() { + playerState.addToInventory("key"); + Set inventory = playerState.getInventory(); + + // Modify the returned set + inventory.add("lamp"); + + // Original should not be affected + assertFalse(playerState.hasItem("lamp")); + assertEquals(1, playerState.getInventory().size()); + } + + @Test + void testMultipleItems() { + String[] items = {"key", "lamp", "map", "potion"}; + + for (String item : items) { + playerState.addToInventory(item); + } + + assertEquals(items.length, playerState.getInventory().size()); + for (String item : items) { + assertTrue(playerState.hasItem(item)); + } + } +} \ No newline at end of file diff --git a/src/test/java/model/QuestStepTest.java b/src/test/java/model/QuestStepTest.java new file mode 100644 index 0000000..0933d88 --- /dev/null +++ b/src/test/java/model/QuestStepTest.java @@ -0,0 +1,140 @@ +package model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class QuestStepTest { + private QuestStep questStep; + private Map options; + private Map requirements; + private Map effects; + + @BeforeEach + void setUp() { + options = new HashMap<>(); + options.put("Choice 1", "step1"); + options.put("Choice 2", "step2"); + + requirements = new HashMap<>(); + requirements.put("key", true); + requirements.put("lamp", false); + + effects = new HashMap<>(); + effects.put("Choice 1", "key"); + effects.put("Choice 2", "lamp"); + + questStep = new QuestStep("test_id", "Test text", options, requirements, effects, false); + } + + @Test + void testDefaultConstructor() { + QuestStep defaultStep = new QuestStep(); + assertNull(defaultStep.getId()); + assertNull(defaultStep.getText()); + assertNull(defaultStep.getOptions()); + assertNull(defaultStep.getRequirements()); + assertNull(defaultStep.getEffects()); + assertFalse(defaultStep.isEnding()); + } + + @Test + void testParameterizedConstructor() { + assertEquals("test_id", questStep.getId()); + assertEquals("Test text", questStep.getText()); + assertEquals(options, questStep.getOptions()); + assertEquals(requirements, questStep.getRequirements()); + assertEquals(effects, questStep.getEffects()); + assertFalse(questStep.isEnding()); + } + + @Test + void testSetAndGetId() { + questStep.setId("new_id"); + assertEquals("new_id", questStep.getId()); + } + + @Test + void testSetAndGetText() { + questStep.setText("New text"); + assertEquals("New text", questStep.getText()); + } + + @Test + void testSetAndGetOptions() { + Map newOptions = new HashMap<>(); + newOptions.put("New Choice", "new_step"); + + questStep.setOptions(newOptions); + assertEquals(newOptions, questStep.getOptions()); + assertEquals(1, questStep.getOptions().size()); + } + + @Test + void testSetAndGetRequirements() { + Map newRequirements = new HashMap<>(); + newRequirements.put("sword", true); + + questStep.setRequirements(newRequirements); + assertEquals(newRequirements, questStep.getRequirements()); + } + + @Test + void testSetAndGetEffects() { + Map newEffects = new HashMap<>(); + newEffects.put("Choice", "sword"); + + questStep.setEffects(newEffects); + assertEquals(newEffects, questStep.getEffects()); + } + + @Test + void testSetAndIsEnding() { + questStep.setEnding(true); + assertTrue(questStep.isEnding()); + + questStep.setEnding(false); + assertFalse(questStep.isEnding()); + } + + @Test + void testEquals() { + QuestStep sameStep = new QuestStep("test_id", "Different text", null, null, null, true); + QuestStep differentStep = new QuestStep("different_id", "Test text", options, requirements, effects, false); + + assertEquals(questStep, sameStep); + assertNotEquals(questStep, differentStep); + assertNotEquals(questStep, null); + assertNotEquals(questStep, "string"); + } + + @Test + void testHashCode() { + QuestStep sameStep = new QuestStep("test_id", "Different", null, null, null, true); + + assertEquals(questStep.hashCode(), sameStep.hashCode()); + } + + @Test + void testToStringNotOverridden() { + // Проверяем, что toString() не переопределен (возвращает стандартное представление) + String toString = questStep.toString(); + assertTrue(toString.contains("QuestStep") || toString.contains("@")); + } + + @Test + void testEmptyMaps() { + QuestStep emptyStep = new QuestStep("empty", "Text", + new HashMap<>(), new HashMap<>(), new HashMap<>(), false); + + assertNotNull(emptyStep.getOptions()); + assertNotNull(emptyStep.getRequirements()); + assertNotNull(emptyStep.getEffects()); + assertTrue(emptyStep.getOptions().isEmpty()); + assertTrue(emptyStep.getRequirements().isEmpty()); + assertTrue(emptyStep.getEffects().isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/service/QuestServiceTest.java b/src/test/java/service/QuestServiceTest.java new file mode 100644 index 0000000..be009e9 --- /dev/null +++ b/src/test/java/service/QuestServiceTest.java @@ -0,0 +1,270 @@ +package service; + +import model.QuestStep; +import model.PlayerState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class QuestServiceTest { + private QuestService questService; + private PlayerState playerState; + + @BeforeEach + void setUp() { + questService = new QuestService(); + playerState = new PlayerState(); + } + + @Test + void testConstructorInitializesSteps() { + // Проверяем, что шаги инициализируются + assertNotNull(questService.getStep("start")); + assertNotNull(questService.getStep("door_initial")); + assertNotNull(questService.getStep("storage_room")); + assertNotNull(questService.getStep("hall_guarded")); + } + + @Test + void testGetStep() { + // Получение существующего шага + QuestStep startStep = questService.getStep("start"); + assertNotNull(startStep); + assertEquals("start", startStep.getId()); + assertTrue(startStep.getText().contains("просыпаетесь")); + + // Получение несуществующего шага + assertNull(questService.getStep("nonexistent_step")); + } + + @Test + void testGetStepForAllIds() { + // Проверяем все основные шаги + String[] stepIds = { + "start", "door_initial", "window_initial", "door_unlocked", + "storage_room", "inspect_room", "window_with_knowledge", + "door_unlocked_smart", "hall_guarded", "hall_guarded_smart", + "hall_guarded_prepared", "hall_escape", "final_door_no_key", + "corridor_with_lamp", "hidden_path", "dark_tunnel", + "dark_tunnel_no_lamp" + }; + + for (String stepId : stepIds) { + QuestStep step = questService.getStep(stepId); + assertNotNull(step, "Step " + stepId + " should not be null"); + assertEquals(stepId, step.getId()); + } + } + + @Test + void testGetStepForEndings() { + // Проверяем концовки + String[] goodEndings = {"good_end_freedom", "good_end_tunnel", "good_end_secret"}; + String[] badEndings = {"bad_end_noise", "bad_end_shaft", "bad_end_crystal", + "bad_end_dark", "bad_end_caught"}; + + for (String ending : goodEndings) { + QuestStep step = questService.getStep(ending); + assertNotNull(step); + assertTrue(step.isEnding()); + assertTrue(step.getText().contains("Поздравляем")); + } + + for (String ending : badEndings) { + QuestStep step = questService.getStep(ending); + assertNotNull(step); + assertTrue(step.isEnding()); + assertTrue(step.getText().contains("Конец")); + } + } + + @Test + void testProcessChoiceWithNullParameters() { + // Null playerState + assertNull(questService.processChoice(null, "choice")); + + // Null choice + assertNull(questService.processChoice(playerState, null)); + + // Both null + assertNull(questService.processChoice(null, null)); + } + + @Test + void testProcessChoiceWithInvalidStep() { + playerState.setCurrentStepId("nonexistent_step"); + assertNull(questService.processChoice(playerState, "choice")); + } + + @Test + void testProcessChoiceWithInvalidChoice() { + playerState.setCurrentStepId("start"); + assertNull(questService.processChoice(playerState, "invalid_choice")); + } + + @Test + void testProcessChoiceValidTransition() { + playerState.setCurrentStepId("start"); + QuestStep nextStep = questService.processChoice(playerState, "Подойти и попробовать открыть дверь"); + + assertNotNull(nextStep); + assertEquals("door_initial", nextStep.getId()); + assertEquals("door_initial", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceAppliesEffects() { + playerState.setCurrentStepId("window_initial"); + QuestStep nextStep = questService.processChoice(playerState, "Вернуться к двери (подобрать ключ)"); + + assertNotNull(nextStep); + assertEquals("door_unlocked", nextStep.getId()); + assertTrue(playerState.hasItem("key")); + assertEquals("door_unlocked", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceDarkTunnelWithoutLamp() { + playerState.setCurrentStepId("dark_tunnel"); + // У игрока нет лампы + QuestStep nextStep = questService.processChoice(playerState, "Идти вперёд с лампой"); + + // Должен перенаправить на dark_tunnel_no_lamp + assertEquals("dark_tunnel_no_lamp", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceDarkTunnelWithLamp() { + playerState.setCurrentStepId("dark_tunnel"); + playerState.addToInventory("lamp"); + QuestStep nextStep = questService.processChoice(playerState, "Идти вперёд с лампой"); + + // Должен перейти к good_end_tunnel + assertEquals("good_end_tunnel", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceHallEscapeWithoutKey() { + playerState.setCurrentStepId("hall_escape"); + // У игрока нет ключа + QuestStep nextStep = questService.processChoice(playerState, "Открыть дверь ключом"); + + // Должен перенаправить на final_door_no_key + assertEquals("final_door_no_key", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceHallEscapeWithKey() { + playerState.setCurrentStepId("hall_escape"); + playerState.addToInventory("key"); + QuestStep nextStep = questService.processChoice(playerState, "Открыть дверь ключом"); + + // Должен перейти к good_end_freedom + assertEquals("good_end_freedom", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceWithRequirementsNotMet() { + playerState.setCurrentStepId("hall_guarded_prepared"); + // У игрока нет лампы + QuestStep nextStep = questService.processChoice(playerState, "Осветить лицо стража лампой"); + + // Должен вернуть текущий шаг (остаться на месте) + assertNotNull(nextStep); + assertEquals("hall_guarded_prepared", nextStep.getId()); + assertEquals("hall_guarded_prepared", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceWithRequirementsMet() { + playerState.setCurrentStepId("hall_guarded_prepared"); + playerState.addToInventory("lamp"); + QuestStep nextStep = questService.processChoice(playerState, "Осветить лицо стража лампой"); + + // Должен перейти к hall_escape + assertEquals("hall_escape", playerState.getCurrentStepId()); + } + + @Test + void testProcessChoiceDuplicateItemNotAdded() { + playerState.setCurrentStepId("storage_room"); + playerState.addToInventory("lamp"); // Уже есть лампа + + QuestStep nextStep = questService.processChoice(playerState, "Взять лампу и вернуться к развилке"); + + assertNotNull(nextStep); + assertEquals("corridor_with_lamp", playerState.getCurrentStepId()); + // В инвентаре все еще должна быть только одна лампа + assertTrue(playerState.hasItem("lamp")); + } + + @Test + void testAllStartOptionsLeadSomewhere() { + playerState.setCurrentStepId("start"); + + Map startOptions = questService.getStep("start").getOptions(); + + for (String choice : startOptions.keySet()) { + PlayerState tempState = new PlayerState(); + tempState.setCurrentStepId("start"); + + QuestStep result = questService.processChoice(tempState, choice); + assertNotNull(result, "Choice '" + choice + "' should lead to a valid step"); + assertNotNull(result.getId()); + } + } + + @Test + void testEndingStepsHaveNoOptions() { + String[] endings = { + "good_end_freedom", "good_end_tunnel", "good_end_secret", + "bad_end_noise", "bad_end_shaft", "bad_end_crystal", + "bad_end_dark", "bad_end_caught" + }; + + for (String ending : endings) { + QuestStep step = questService.getStep(ending); + assertNotNull(step); + assertTrue(step.isEnding()); + assertTrue(step.getOptions().isEmpty()); + } + } + + @Test + void testWindowInitialEffects() { + playerState.setCurrentStepId("window_initial"); + QuestStep nextStep = questService.processChoice(playerState, "Вернуться к двери (подобрать ключ)"); + + assertNotNull(nextStep); + assertEquals("door_unlocked", nextStep.getId()); + assertTrue(playerState.hasItem("key")); + } + + @Test + void testStorageRoomEffects() { + playerState.setCurrentStepId("storage_room"); + + // Проверяем оба варианта, которые дают лампу + String[] lampChoices = {"Взять лампу и вернуться к развилке", "Взять лампу и пойти дальше"}; + + for (String choice : lampChoices) { + PlayerState tempState = new PlayerState(); + tempState.setCurrentStepId("storage_room"); + + questService.processChoice(tempState, choice); + assertTrue(tempState.hasItem("lamp"), "Choice '" + choice + "' should give lamp"); + } + } + + @Test + void testInspectRoomEffects() { + playerState.setCurrentStepId("inspect_room"); + QuestStep nextStep = questService.processChoice(playerState, "Осмотреть окно внимательнее"); + + assertNotNull(nextStep); + assertEquals("window_with_knowledge", nextStep.getId()); + assertTrue(playerState.hasItem("knowledge")); + } +} \ No newline at end of file From ccabed8e2b050f95ff20ebd25d6c4fc593ed4453 Mon Sep 17 00:00:00 2001 From: KirillB Date: Sun, 25 Jan 2026 20:24:16 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20QuestService=20(=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=20=D0=B2=20QuestC?= =?UTF-8?q?onfigLoader,=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=20=D0=BA=D0=B2=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=20quest-config).=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D0=BA=D0=BD=D0=BE?= =?UTF-8?q?=D0=BF=D0=BA=D0=B0=D0=BC=20(=D0=9D=D0=BE=D0=B2=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0,=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C),=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D0=B9=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D1=81=D0=BC=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D0=B5=D0=BB=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D1=83=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B8=D1=80=D0=B5=D0=BA=D1=82=D0=B0=D1=85=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=BD=D0=BE=20=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=B5=D0=BD=D0=B4=D0=B0=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 33 ++- src/main/java/controller/QuestServlet.java | 53 +++- src/main/java/controller/WelcomeServlet.java | 40 ++- src/main/java/service/QuestConfigLoader.java | 60 +++++ src/main/java/service/QuestService.java | 237 +++--------------- src/main/resources/log4j2.xml | 36 +++ src/main/resources/quest-config.json | 247 +++++++++++++++++++ src/main/webapp/WEB-INF/web.xml | 16 ++ src/main/webapp/error.jsp | 110 +++++++++ src/main/webapp/quest.jsp | 22 +- 10 files changed, 637 insertions(+), 217 deletions(-) create mode 100644 src/main/java/service/QuestConfigLoader.java create mode 100644 src/main/resources/log4j2.xml create mode 100644 src/main/resources/quest-config.json create mode 100644 src/main/webapp/error.jsp diff --git a/pom.xml b/pom.xml index bf30f27..9214b87 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,8 @@ 1.2 5.9.2 4.11.0 + 2.20.0 + 2.10.1 @@ -41,6 +43,29 @@ ${jstl.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-web + ${log4j.version} + runtime + + org.junit.jupiter @@ -63,7 +88,7 @@ test - + org.mockito mockito-core @@ -77,6 +102,12 @@ ${mockito.version} test + + + com.google.code.gson + gson + ${gson.version} + diff --git a/src/main/java/controller/QuestServlet.java b/src/main/java/controller/QuestServlet.java index f675e1b..c59ef11 100644 --- a/src/main/java/controller/QuestServlet.java +++ b/src/main/java/controller/QuestServlet.java @@ -3,7 +3,8 @@ import model.QuestStep; import model.PlayerState; import service.QuestService; - +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; @@ -15,21 +16,27 @@ @WebServlet("/quest") public class QuestServlet extends HttpServlet { private QuestService questService; + private static final Logger logger = LogManager.getLogger(QuestServlet.class); @Override public void init() throws ServletException { super.init(); questService = new QuestService(); + logger.info("QuestServlet initialized"); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(true); + String sessionId = session.getId(); + String playerName = (String) session.getAttribute("playerName"); + logger.info("GET request - Session: {}, Player: {}", sessionId, playerName); + try{ // Проверяем, ввел ли игрок имя - String playerName = (String) session.getAttribute("playerName"); if (playerName == null || playerName.trim().isEmpty()) { + logger.warn("No player name in session, redirecting to welcome"); response.sendRedirect(request.getContextPath() + "/welcome"); return; } @@ -37,6 +44,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) // Обработка сброса игры String action = request.getParameter("action"); if ("restart".equals(action)) { + logger.info("Player '{}' restarted the game", playerName); session.removeAttribute("playerState"); session.removeAttribute("errorMessage"); } @@ -46,14 +54,19 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) if (playerState == null) { playerState = new PlayerState(); session.setAttribute("playerState", playerState); + logger.info("New game started for player '{}'", playerName); } // Получаем текущий шаг String currentStepId = playerState.getCurrentStepId(); + logger.debug("Player '{}' at step: {}", playerName, currentStepId); + QuestStep step = questService.getStep(currentStepId); // Если шаг не найден, начинаем сначала if (step == null) { + logger.warn("Step '{}' not found for player '{}', resetting to start", + currentStepId, playerName); playerState.setCurrentStepId("start"); step = questService.getStep("start"); } @@ -68,25 +81,38 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) if (errorMessage != null) { request.setAttribute("errorMessage", errorMessage); session.removeAttribute("errorMessage"); + logger.debug("Showing error message to player '{}': {}", playerName, errorMessage); } request.getRequestDispatcher("/quest.jsp").forward(request, response); + logger.debug("Forwarded to quest.jsp for player '{}'", playerName); + + } catch (Exception e) { + logger.error("Error in QuestServlet.doGet", e); + request.setAttribute("errorMessage", "Ошибка при загрузке игры: " + e.getMessage()); + request.getRequestDispatcher("/error.jsp").forward(request, response); + } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); + String sessionId = session.getId(); + String playerName = (String) session.getAttribute("playerName"); + logger.info("POST request - Session: {}, Player: {}", sessionId, playerName); + try { // Проверяем, есть ли имя игрока - String playerName = (String) session.getAttribute("playerName"); if (playerName == null || playerName.trim().isEmpty()) { + logger.warn("No player name in POST request, redirecting to welcome"); response.sendRedirect(request.getContextPath() + "/welcome"); return; } PlayerState playerState = (PlayerState) session.getAttribute("playerState"); if (playerState == null) { + logger.warn("No player state for player '{}', redirecting to quest", playerName); response.sendRedirect(request.getContextPath() + "/quest"); return; } @@ -94,27 +120,45 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) String choice = request.getParameter("choice"); session.removeAttribute("errorMessage"); + logger.debug("Player '{}' choice: {}", playerName, choice); + if (choice != null && !choice.trim().isEmpty()) { String previousStepId = playerState.getCurrentStepId(); + logger.info("Player '{}' making choice at step '{}': '{}'", + playerName, previousStepId, choice); + QuestStep nextStep = questService.processChoice(playerState, choice); if (nextStep != null) { // Проверяем, не остались ли мы на том же шаге (зацикливание) if (nextStep.getId().equals(previousStepId)) { setErrorMessage(session, previousStepId, choice); + logger.warn("Player '{}' stuck at step '{}' after choice '{}'", + playerName, previousStepId, choice); + } else { + logger.info("Player '{}' moved from '{}' to '{}'", + playerName, previousStepId, nextStep.getId()); } } else { session.setAttribute("errorMessage", "Не удалось обработать ваш выбор. Попробуйте еще раз."); + logger.error("Failed to process choice for player '{}': {}", playerName, choice); } } else { session.setAttribute("errorMessage", "Вы не сделали выбор!"); + logger.warn("Player '{}' submitted empty choice", playerName); } // Сохраняем обновленное состояние session.setAttribute("playerState", playerState); response.sendRedirect(request.getContextPath() + "/quest"); - } + logger.debug("Redirected player '{}' to quest page", playerName); + } catch (Exception e) { + logger.error("Error in QuestServlet.doPost", e); + session.setAttribute("errorMessage", "Ошибка при обработке выбора: " + e.getMessage()); + response.sendRedirect(request.getContextPath() + "/quest"); + } + } private void setErrorMessage(HttpSession session, String stepId, String choice) { String errorMessage = "Для этого действия нужен предмет из инвентаря!"; @@ -125,5 +169,6 @@ private void setErrorMessage(HttpSession session, String stepId, String choice) } session.setAttribute("errorMessage", errorMessage); + logger.debug("Set error message: {}", errorMessage); } } \ No newline at end of file diff --git a/src/main/java/controller/WelcomeServlet.java b/src/main/java/controller/WelcomeServlet.java index 7433ff3..590bc8c 100644 --- a/src/main/java/controller/WelcomeServlet.java +++ b/src/main/java/controller/WelcomeServlet.java @@ -1,5 +1,7 @@ package controller; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; @@ -10,54 +12,86 @@ @WebServlet("/welcome") public class WelcomeServlet extends HttpServlet { + private static final Logger logger = LogManager.getLogger(WelcomeServlet.class); + + @Override + public void init() throws ServletException { + super.init(); + logger.info("WelcomeServlet initialized"); + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); - + String sessionId = session.getId(); + logger.info("GET request to welcome page - Session: {}", sessionId); + try { // Проверяем, пришел ли запрос на смену игрока String action = request.getParameter("action"); if ("changePlayer".equals(action)) { + String oldPlayer = (String) session.getAttribute("playerName"); // Очищаем данные текущего игрока session.removeAttribute("playerName"); session.removeAttribute("playerState"); session.removeAttribute("errorMessage"); - System.out.println("DEBUG: Player data cleared for change player"); + logger.info("Player change requested. Old player: {}, Session: {}", + oldPlayer != null ? oldPlayer : "unknown", sessionId); } // Проверяем, есть ли уже имя в сессии String playerName = (String) session.getAttribute("playerName"); if (playerName != null && !playerName.trim().isEmpty()) { + logger.info("Player '{}' already registered, redirecting to quest", playerName); // Если имя уже есть, перенаправляем в квест response.sendRedirect(request.getContextPath() + "/quest"); return; } + logger.debug("Showing welcome page for session: {}", sessionId); // Иначе показываем страницу приветствия request.getRequestDispatcher("/welcome.jsp").forward(request, response); + } catch (Exception e) { + logger.error("Error in WelcomeServlet.doGet", e); + request.setAttribute("errorMessage", "Ошибка при загрузке страницы: " + e.getMessage()); + request.getRequestDispatcher("/error.jsp").forward(request, response); + } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); + try { + String sessionId = session.getId(); String playerName = request.getParameter("playerName"); + logger.info("POST request to welcome page - Session: {}, Player name submitted: {}", + sessionId, playerName); + if (playerName != null && !playerName.trim().isEmpty()) { // Сохраняем имя в сессии session.setAttribute("playerName", playerName.trim()); - System.out.println("DEBUG: Player name set: " + playerName); + logger.info("New player registered: '{}', Session: {}", + playerName.trim(), sessionId); // Сбрасываем игру для нового игрока session.removeAttribute("playerState"); session.removeAttribute("errorMessage"); // Перенаправляем в квест + logger.debug("Redirecting player '{}' to quest", playerName.trim()); response.sendRedirect(request.getContextPath() + "/quest"); } else { // Если имя не введено, возвращаем с ошибкой request.setAttribute("error", "Пожалуйста, введите ваше имя!"); + logger.warn("Empty player name submitted for session: {}", sessionId); + request.getRequestDispatcher("/welcome.jsp").forward(request, response); + } + } catch (Exception e) { + logger.error("Error in WelcomeServlet.doPost", e); + request.setAttribute("error", "Ошибка при регистрации: " + e.getMessage()); request.getRequestDispatcher("/welcome.jsp").forward(request, response); } } diff --git a/src/main/java/service/QuestConfigLoader.java b/src/main/java/service/QuestConfigLoader.java new file mode 100644 index 0000000..1aab8e8 --- /dev/null +++ b/src/main/java/service/QuestConfigLoader.java @@ -0,0 +1,60 @@ +package service; + +import model.QuestStep; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.Map; + +public class QuestConfigLoader { + private final Map steps; + + public QuestConfigLoader() { + try { + InputStream inputStream = getClass().getClassLoader() + .getResourceAsStream("quest-config.json"); + + if (inputStream == null) { + throw new RuntimeException("Конфигурационный файл не найден"); + } + + Gson gson = new Gson(); + Type type = new TypeToken>>(){}.getType(); + Map> config = gson.fromJson( + new InputStreamReader(inputStream, "UTF-8"), type); + + steps = convertConfigToSteps(config); + } catch (Exception e) { + throw new RuntimeException("Ошибка загрузки конфигурации", e); + } + } + + private Map convertConfigToSteps( + Map> config) { + Map stepsData = config.get("steps"); + Map steps = new java.util.HashMap<>(); + + for (Map.Entry entry : stepsData.entrySet()) { + String stepId = entry.getKey(); + Map stepData = (Map) entry.getValue(); + + String description = (String) stepData.get("description"); + Map options = (Map) stepData.get("options"); + Map requirements = (Map) stepData.get("requirements"); + Map effects = (Map) stepData.get("effects"); + Boolean isEnd = (Boolean) stepData.get("isEnd"); + + steps.put(stepId, new QuestStep( + stepId, description, options, requirements, effects, isEnd + )); + } + + return steps; + } + + public Map getSteps() { + return steps; + } +} \ No newline at end of file diff --git a/src/main/java/service/QuestService.java b/src/main/java/service/QuestService.java index 6f77ce2..7aa39a2 100644 --- a/src/main/java/service/QuestService.java +++ b/src/main/java/service/QuestService.java @@ -2,249 +2,63 @@ import model.QuestStep; import model.PlayerState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.*; public class QuestService { - private final Map steps = new HashMap<>(); + private final Map steps; + private static final Logger logger = LogManager.getLogger(QuestService.class); public QuestService() { - initSteps(); - } - - private void initSteps() { - // 1. Стартовая комната - steps.put("start", new QuestStep("start", - "Вы просыпаетесь в затемнённой комнате. Голова гудит, воспоминания смутны. " + - "Вы лежите на холодном каменном полу. Единственный источник света — тусклый луч из-под двери прямо перед вами. " + - "Слева, в стене, вы замечаете небольшое зарешеченное окно на уровне пола.", - Map.of( - "Подойти и попробовать открыть дверь", "door_initial", - "Исследовать окно в стене", "window_initial", - "Осмотреться по сторонам в самой комнате", "inspect_room" - ), null, null, false)); - - // 2. Дверь (первый подход) - steps.put("door_initial", new QuestStep("door_initial", - "Дверь заперта, но неплотно притворена. В щель виден коридор, освещённый горящими факелами. " + - "На полу перед дверью лежит смятый клочок пергамента. На нём всего два слова: **\"Не кричи\"**. " + - "Вдалеке слышны неясные шаги.", - Map.of( - "Тихо протиснуться в коридор и пойти НАЛЕВО", "hall_guarded", - "Тихо протиснуться в коридор и пойти НАПРАВО", "storage_room", - "Кричать о помощи", "bad_end_noise" - ), null, null, false)); - - // 3. Окно (первый подход) - steps.put("window_initial", new QuestStep("window_initial", - "Подползая к окну, вы видите, что решетка шатается. За окном — сырая узкая каменная шахта, уходящая вверх. " + - "Рядом с окном, в пыли, вы нащупываете небольшой **ржавый ключ**.", - Map.of( - "Попробовать оторвать решётку и лезть в шахту", "bad_end_shaft", - "Вернуться к двери (подобрать ключ)", "door_unlocked" - ), null, Map.of("Вернуться к двери (подобрать ключ)", "key"), false)); - - // 4. Дверь открыта (есть ключ) - steps.put("door_unlocked", new QuestStep("door_unlocked", - "Ключ подходит! Дверь тихо открывается. Вы в коридоре с факелами. " + - "Налево слышны шаги, направо — тишина.", - Map.of( - "Пойти НАЛЕВО (слышны шаги)", "hall_guarded", - "Пойти НАПРАВО (тишина)", "storage_room" - ), null, null, false)); - - // 5. Кладовая - steps.put("storage_room", new QuestStep("storage_room", - "Вы крадётесь по тёмному коридору. Он приводит к небольшой кладовой. " + - "На столе лежит связка старых ключей и горит **масляная лампа**. " + - "На стене нацарапано: \"Он боится света\".", - Map.of( - "Взять лампу и вернуться к развилке", "corridor_with_lamp", - "Взять лампу и пойти дальше", "dark_tunnel", - "Проигнорировать лампу и пойти дальше", "dark_tunnel_no_lamp" - ), null, Map.of( - "Взять лампу и вернуться к развилке", "lamp", - "Взять лампу и пойти дальше", "lamp" - ), false)); - - // 6. Осмотр комнаты - steps.put("inspect_room", new QuestStep("inspect_room", - "Внимательно осмотрев комнату, вы находите под соломой старую **карту подземелья**. " + - "Теперь вы знаете планировку этого уровня.", - Map.of( - "Идти к двери", "door_initial", - "Осмотреть окно внимательнее", "window_with_knowledge" - ), null, Map.of("Осмотреть окно внимательнее", "knowledge"), false)); - - // 7. Окно со знанием карты - steps.put("window_with_knowledge", new QuestStep("window_with_knowledge", - "Благодаря карте вы понимаете, что шахта — это ловушка, ведущая в тупик. " + - "Но вы всё равно можете взять **ржавый ключ**, лежащий у окна.", - Map.of("Вернуться к двери (я знаю о ловушке)", "door_unlocked_smart"), - null, Map.of("Вернуться к двери (я знаю о ловушке)", "key"), false)); - - // 8. Дверь открыта (умный путь) - steps.put("door_unlocked_smart", new QuestStep("door_unlocked_smart", - "С картой в голове вы выходите в коридор. Вы знаете, что налево — страж, " + - "а направо — кладовая с полезными предметами.", - Map.of( - "Пойти НАЛЕВО (я знаю о страже)", "hall_guarded_smart", - "Пойти НАПРАВО (я знаю о кладовой)", "storage_room" - ), null, null, false)); - - // 9. Зал со стражем (обычный) - steps.put("hall_guarded", new QuestStep("hall_guarded", - "Вы выходите в просторный зал. Посреди него на троне сидит спящий страж в ржавых доспехах. " + - "За его спиной виден выход — большая дубовая дверь. " + - "Страж крепко сжимает в руке сияющий **голубой кристалл**.", - Map.of( - "Попробовать вытащить кристалл из руки стража", "bad_end_crystal", - "Обойти стража и попробовать открыть дверь", "final_door_no_key", - "Вернуться назад", "door_unlocked" - ), null, null, false)); - - // 10. Зал со стражем (умный) - steps.put("hall_guarded_smart", new QuestStep("hall_guarded_smart", - "Благодаря карте вы знаете о страже. На карте рядом с ним написано: \"Боится света\".", - Map.of( - "Использовать знание о страже", "hall_guarded_prepared", - "Попробовать вытащить кристалл", "bad_end_crystal" - ), null, null, false)); - - // 11. Подготовка в зале - steps.put("hall_guarded_prepared", new QuestStep("hall_guarded_prepared", - "У вас есть лампа, и вы знаете, что страж боится света.", - Map.of( - "Осветить лицо стража лампой", "hall_escape", - "Вернуться за лампой", "storage_room" - ), Map.of("lamp", true), null, false)); - - // 12. Побег из зала - steps.put("hall_escape", new QuestStep("hall_escape", - "Вы ослепляете стража лампой и подбегаете к двери.", - Map.of( - "Открыть дверь ключом", "good_end_freedom", - "Попытаться выбить дверь", "bad_end_caught" - ), Map.of("key", true), null, false)); - - // 13. Финальная дверь без ключа - steps.put("final_door_no_key", new QuestStep("final_door_no_key", - "Дверь заперта. Без ключа её не открыть.", - Map.of( - "Попытаться выбить дверь", "bad_end_caught", - "Вернуться назад", "hall_guarded" - ), null, null, false)); - - // 14. Коридор с лампой - steps.put("corridor_with_lamp", new QuestStep("corridor_with_lamp", - "С лампой в руках вы возвращаетесь на развилку.", - Map.of( - "Пойти налево к стражу", "hall_guarded", - "Осветить стены лампой", "hidden_path" - ), null, null, false)); - - // 15. Скрытый путь - steps.put("hidden_path", new QuestStep("hidden_path", - "Свет лампы выхватывает из темноты скрытый проход в стене.", - Map.of("Пройти по скрытому ходу", "good_end_secret"), - null, null, false)); - - // 16. Темный туннель (с проверкой лампы) - steps.put("dark_tunnel", new QuestStep("dark_tunnel", - "За дверью — абсолютно тёмный сырой туннель. Без света здесь опасно.", - Map.of( - "Идти вперёд с лампой", "good_end_tunnel", - "Идти на ощупь", "bad_end_dark" - ), Map.of("lamp", true), null, false)); - - // 17. Темный туннель без лампы - steps.put("dark_tunnel_no_lamp", new QuestStep("dark_tunnel_no_lamp", - "Без лампы в темноте не видно даже собственной руки.\n" + - "Это кажется крайне неразумным решением.", - Map.of( - "Вернуться в кладовую", "storage_room", - "Все равно идти вперёд", "bad_end_dark" - ), null, null, false)); - - // КОНЦОВКИ - steps.put("bad_end_noise", new QuestStep("bad_end_noise", - "Из темноты появляется страж. Удар по голове — и вы теряете сознание.\n\n" + - "**Конец. Ваша любовь к шуму погубила вас.**", - new HashMap<>(), null, null, true)); - - steps.put("bad_end_shaft", new QuestStep("bad_end_shaft", - "Вы карабкаетесь по шахте, но камень под ногой обваливается...\n\n" + - "**Конец. Ваше восхождение было недолгим.**", - new HashMap<>(), null, null, true)); - - steps.put("bad_end_crystal", new QuestStep("bad_end_crystal", - "Кристалл выскальзывает из руки стража. Он просыпается, и его глаза вспыхивают красным светом!\n\n" + - "**Конец. Вы нашли источник силы, но не смогли им воспользоваться.**", - new HashMap<>(), null, null, true)); - - steps.put("bad_end_dark", new QuestStep("bad_end_dark", - "Вы шагаете в темноту и проваливаетесь в яму.\n\n" + - "**Конец. Свет был бы полезен.**", - new HashMap<>(), null, null, true)); - - steps.put("bad_end_caught", new QuestStep("bad_end_caught", - "Шум будит стража. Тяжёлая длань хватает вас...\n\n" + - "**Конец. Выход был так близок.**", - new HashMap<>(), null, null, true)); - - steps.put("good_end_freedom", new QuestStep("good_end_freedom", - "Ключ поворачивается! Дверь открывается, впуская свежий ночной воздух.\n\n" + - "**Поздравляем! Вы сбежали благодаря находчивости и подобранным предметам.**\n\n" + - "*Путь: Ключ + Лампа + Знание*", - new HashMap<>(), null, null, true)); - - steps.put("good_end_tunnel", new QuestStep("good_end_tunnel", - "Лампа освещает путь. Туннель выводит вас к лестнице, ведущей на поверхность!\n\n" + - "**Поздравляем! Ваша осторожность (и лампа) спасли вас.**\n\n" + - "*Путь: Лампа*", - new HashMap<>(), null, null, true)); - - steps.put("good_end_secret", new QuestStep("good_end_secret", - "Свет лампы показывает скрытую дверь. Вы выходите в старую часовню!\n\n" + - "**Поздравляем! Вы нашли тайный путь к спасению.**\n\n" + - "*Путь: Лампа + Внимательность*", - new HashMap<>(), null, null, true)); + QuestConfigLoader configLoader = new QuestConfigLoader(); + this.steps = configLoader.getSteps(); + logger.info("QuestService initialized with {} steps", steps.size()); } public QuestStep getStep(String stepId) { - return steps.get(stepId); + QuestStep step = steps.get(stepId); + if (step == null) { + logger.warn("Step not found: {}", stepId); + } + return step; } public QuestStep processChoice(PlayerState playerState, String choice) { if (playerState == null || choice == null) { - System.out.println("ERROR: processChoice - playerState or choice is null"); + logger.error("processChoice - playerState or choice is null"); return null; } String currentStepId = playerState.getCurrentStepId(); + logger.info("Processing choice: player at step '{}' chose '{}'", + currentStepId, choice); + QuestStep currentStep = getStep(currentStepId); if (currentStep == null) { - System.out.println("ERROR: Current step not found: " + currentStepId); + logger.error("Current step not found: {}", currentStepId); return null; } if (!currentStep.getOptions().containsKey(choice)) { - System.out.println("WARNING: Choice '" + choice + "' not found in step: " + currentStepId); + logger.warn("Choice '{}' not found in step: {}", choice, currentStepId); return null; } String nextStepId = currentStep.getOptions().get(choice); // Специальные проверки для шагов, требующих предметы - // Эти проверки должны выполняться в первую очередь if ("dark_tunnel".equals(currentStepId) && "Идти вперёд с лампой".equals(choice)) { if (!playerState.hasItem("lamp")) { + logger.warn("Player tried to go through dark tunnel without lamp"); nextStepId = "dark_tunnel_no_lamp"; } } if ("hall_escape".equals(currentStepId) && "Открыть дверь ключом".equals(choice)) { if (!playerState.hasItem("key")) { + logger.warn("Player tried to open door without key"); nextStepId = "final_door_no_key"; } } @@ -257,12 +71,19 @@ public QuestStep processChoice(PlayerState playerState, String choice) { QuestStep nextStep = getStep(nextStepId); if (nextStep == null) { - System.out.println("ERROR: Next step not found: " + nextStepId); + logger.error("Next step not found: {}", nextStepId); return null; } + logger.info("Player moved from '{}' to '{}'", currentStepId, nextStepId); + + if (nextStep.isEnding()) { + logger.info("Player reached ending: {}", nextStepId); + } + return nextStep; } + private void applyEffects(PlayerState playerState, QuestStep step, String choice) { if (step.getEffects() == null) return; @@ -271,6 +92,7 @@ private void applyEffects(PlayerState playerState, QuestStep step, String choice if (!playerState.hasItem(item)) { playerState.addToInventory(item); + logger.info("Player acquired item: {}", item); } } @@ -279,6 +101,7 @@ public boolean checkRequirements(PlayerState playerState, QuestStep step) { for (Map.Entry req : step.getRequirements().entrySet()) { if (req.getValue() && !playerState.hasItem(req.getKey())) { + logger.debug("Requirement not met: need {} for step", req.getKey()); return false; } } diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..8b70f56 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/quest-config.json b/src/main/resources/quest-config.json new file mode 100644 index 0000000..1ed6eb9 --- /dev/null +++ b/src/main/resources/quest-config.json @@ -0,0 +1,247 @@ +{ + "steps": { + "start": { + "description": "Вы просыпаетесь в затемнённой комнате. Голова гудит, воспоминания смутны. Вы лежите на холодном каменном полу. Единственный источник света — тусклый луч из-под двери прямо перед вами. Слева, в стене, вы замечаете небольшое зарешеченное окно на уровне пола.", + "options": { + "Подойти и попробовать открыть дверь": "door_initial", + "Исследовать окно в стене": "window_initial", + "Осмотреться по сторонам в самой комнате": "inspect_room" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "door_initial": { + "description": "Дверь заперта, но неплотно притворена. В щель виден коридор, освещённый горящими факелами. На полу перед дверью лежит смятый клочок пергамента. На нём всего два слова: **\"Не кричи\"**. Вдалеке слышны неясные шаги.", + "options": { + "Тихо протиснуться в коридор и пойти НАЛЕВО": "hall_guarded", + "Тихо протиснуться в коридор и пойти НАПРАВО": "storage_room", + "Кричать о помощи": "bad_end_noise" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "window_initial": { + "description": "Подползая к окну, вы видите, что решетка шатается. За окном — сырая узкая каменная шахта, уходящая вверх. Рядом с окном, в пыли, вы нащупываете небольшой **ржавый ключ**.", + "options": { + "Попробовать оторвать решётку и лезть в шахту": "bad_end_shaft", + "Вернуться к двери (подобрать ключ)": "door_unlocked" + }, + "requirements": null, + "effects": { + "Вернуться к двери (подобрать ключ)": "key" + }, + "isEnd": false + }, + "door_unlocked": { + "description": "Ключ подходит! Дверь тихо открывается. Вы в коридоре с факелами. Налево слышны шаги, направо — тишина.", + "options": { + "Пойти НАЛЕВО (слышны шаги)": "hall_guarded", + "Пойти НАПРАВО (тишина)": "storage_room" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "storage_room": { + "description": "Вы крадётесь по тёмному коридору. Он приводит к небольшой кладовой. На столе лежит связка старых ключей и горит **масляная лампа**. На стене нацарапано: \"Он боится света\".", + "options": { + "Взять лампу и вернуться к развилке": "corridor_with_lamp", + "Взять лампу и пойти дальше": "dark_tunnel", + "Проигнорировать лампу и пойти дальше": "dark_tunnel_no_lamp" + }, + "requirements": null, + "effects": { + "Взять лампу и вернуться к развилке": "lamp", + "Взять лампу и пойти дальше": "lamp" + }, + "isEnd": false + }, + "inspect_room": { + "description": "Внимательно осмотрев комнату, вы находите под соломой старую **карту подземелья**. Теперь вы знаете планировку этого уровня.", + "options": { + "Идти к двери": "door_initial", + "Осмотреть окно внимательнее": "window_with_knowledge" + }, + "requirements": null, + "effects": { + "Осмотреть окно внимательнее": "knowledge" + }, + "isEnd": false + }, + "window_with_knowledge": { + "description": "Благодаря карте вы понимаете, что шахта — это ловушка, ведущая в тупик. Но вы всё равно можете взять **ржавый ключ**, лежащий у окна.", + "options": { + "Вернуться к двери (я знаю о ловушке)": "door_unlocked_smart" + }, + "requirements": null, + "effects": { + "Вернуться к двери (я знаю о ловушке)": "key" + }, + "isEnd": false + }, + "door_unlocked_smart": { + "description": "С картой в голове вы выходите в коридор. Вы знаете, что налево — страж, а направо — кладовая с полезными предметами.", + "options": { + "Пойти НАЛЕВО (я знаю о страже)": "hall_guarded_smart", + "Пойти НАПРАВО (я знаю о кладовой)": "storage_room" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "hall_guarded": { + "description": "Вы выходите в просторный зал. Посреди него на троне сидит спящий страж в ржавых доспехах. За его спиной виден выход — большая дубовая дверь. Страж крепко сжимает в руке сияющий **голубой кристалл**.", + "options": { + "Попробовать вытащить кристалл из руки стража": "bad_end_crystal", + "Обойти стража и попробовать открыть дверь": "final_door_no_key", + "Вернуться назад": "door_unlocked" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "hall_guarded_smart": { + "description": "Благодаря карте вы знаете о страже. На карте рядом с ним написано: \"Боится света\".", + "options": { + "Использовать знание о страже": "hall_guarded_prepared", + "Попробовать вытащить кристалл": "bad_end_crystal" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "hall_guarded_prepared": { + "description": "У вас есть лампа, и вы знаете, что страж боится света.", + "options": { + "Осветить лицо стража лампой": "hall_escape", + "Вернуться за лампой": "storage_room" + }, + "requirements": { + "lamp": true + }, + "effects": null, + "isEnd": false + }, + "hall_escape": { + "description": "Вы ослепляете стража лампой и подбегаете к двери.", + "options": { + "Открыть дверь ключом": "good_end_freedom", + "Попытаться выбить дверь": "bad_end_caught" + }, + "requirements": { + "key": true + }, + "effects": null, + "isEnd": false + }, + "final_door_no_key": { + "description": "Дверь заперта. Без ключа её не открыть.", + "options": { + "Попытаться выбить дверь": "bad_end_caught", + "Вернуться назад": "hall_guarded" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "corridor_with_lamp": { + "description": "С лампой в руках вы возвращаетесь на развилку.", + "options": { + "Пойти налево к стражу": "hall_guarded", + "Осветить стены лампой": "hidden_path" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "hidden_path": { + "description": "Свет лампы выхватывает из темноты скрытый проход в стене.", + "options": { + "Пройти по скрытому ходу": "good_end_secret" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "dark_tunnel": { + "description": "За дверью — абсолютно тёмный сырой туннель. Без света здесь опасно.", + "options": { + "Идти вперёд с лампой": "good_end_tunnel", + "Идти на ощупь": "bad_end_dark" + }, + "requirements": { + "lamp": true + }, + "effects": null, + "isEnd": false + }, + "dark_tunnel_no_lamp": { + "description": "Без лампы в темноте не видно даже собственной руки.\nЭто кажется крайне неразумным решением.", + "options": { + "Вернуться в кладовую": "storage_room", + "Все равно идти вперёд": "bad_end_dark" + }, + "requirements": null, + "effects": null, + "isEnd": false + }, + "bad_end_noise": { + "description": "Из темноты появляется страж. Удар по голове — и вы теряете сознание.\n\n**Конец. Ваша любовь к шуму погубила вас.**", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "bad_end_shaft": { + "description": "Вы карабкаетесь по шахте, но камень под ногой обваливается...\n\n**Конец. Ваше восхождение было недолгим.**", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "bad_end_crystal": { + "description": "Кристалл выскальзывает из руки стража. Он просыпается, и его глаза вспыхивают красным светом!\n\n**Конец. Вы нашли источник силы, но не смогли им воспользоваться.**", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "bad_end_dark": { + "description": "Вы шагаете в темноту и проваливаетесь в яму.\n\n**Конец. Свет был бы полезен.**", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "bad_end_caught": { + "description": "Шум будит стража. Тяжёлая длань хватает вас...\n\n**Конец. Выход был так близок.**", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "good_end_freedom": { + "description": "Ключ поворачивается! Дверь открывается, впуская свежий ночной воздух.\n\n**Поздравляем! Вы сбежали благодаря находчивости и подобранным предметам.**\n\n*Путь: Ключ + Лампа + Знание*", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "good_end_tunnel": { + "description": "Лампа освещает путь. Туннель выводит вас к лестнице, ведущей на поверхность!\n\n**Поздравляем! Ваша осторожность (и лампа) спасли вас.**\n\n*Путь: Лампа*", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + }, + "good_end_secret": { + "description": "Свет лампы показывает скрытую дверь. Вы выходите в старую часовню!\n\n**Поздравляем! Вы нашли тайный путь к спасению.**\n\n*Путь: Лампа + Внимательность*", + "options": {}, + "requirements": null, + "effects": null, + "isEnd": true + } + } +} \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index f94fff3..dd3b3fe 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -23,6 +23,22 @@ /* + + + 404 + /error.jsp + + + + 500 + /error.jsp + + + + 403 + /error.jsp + + 30 diff --git a/src/main/webapp/error.jsp b/src/main/webapp/error.jsp new file mode 100644 index 0000000..4a0c873 --- /dev/null +++ b/src/main/webapp/error.jsp @@ -0,0 +1,110 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + + Ошибка + + + +
+

+ + + Страница не найдена + + + Ошибка сервера + + + Произошла ошибка + + +

+ +
+ + + ${errorMessage} + + + Запрашиваемая страница не существует или была перемещена. + + + На сервере произошла внутренняя ошибка. Пожалуйста, попробуйте позже. + + + Произошла непредвиденная ошибка. + + +
+ +
+ + + + В игру + +
+ + +
+ Код ошибки: ${pageContext.errorData.statusCode} +
+
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/quest.jsp b/src/main/webapp/quest.jsp index e2cc4a3..5b6f9e8 100644 --- a/src/main/webapp/quest.jsp +++ b/src/main/webapp/quest.jsp @@ -416,10 +416,10 @@