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..9214b87 --- /dev/null +++ b/pom.xml @@ -0,0 +1,157 @@ + + 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 + 2.20.0 + 2.10.1 + + + + + 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.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 + 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 + + + + com.google.code.gson + gson + ${gson.version} + + + + + + + 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..c59ef11 --- /dev/null +++ b/src/main/java/controller/QuestServlet.java @@ -0,0 +1,174 @@ +package controller; + +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; +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; + 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{ + // Проверяем, ввел ли игрок имя + if (playerName == null || playerName.trim().isEmpty()) { + logger.warn("No player name in session, redirecting to welcome"); + response.sendRedirect(request.getContextPath() + "/welcome"); + return; + } + + // Обработка сброса игры + String action = request.getParameter("action"); + if ("restart".equals(action)) { + logger.info("Player '{}' restarted the game", playerName); + session.removeAttribute("playerState"); + session.removeAttribute("errorMessage"); + } + + // Получаем или создаем состояние игрока + PlayerState playerState = (PlayerState) session.getAttribute("playerState"); + 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"); + } + + // Передаем данные в 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"); + 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 { + // Проверяем, есть ли имя игрока + 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; + } + + 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 = "Для этого действия нужен предмет из инвентаря!"; + + if ("dark_tunnel".equals(stepId) && choice.contains("лампой")) { + errorMessage = "Нужна лампа, чтобы идти вперёд в темноте!"; + } else if ("hall_escape".equals(stepId) && choice.contains("ключом")) { + errorMessage = "Нужен ключ, чтобы открыть эту дверь!"; + } + + 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 new file mode 100644 index 0000000..590bc8c --- /dev/null +++ b/src/main/java/controller/WelcomeServlet.java @@ -0,0 +1,98 @@ +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; +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 { + 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"); + 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()); + 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); + } + } +} \ 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/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 new file mode 100644 index 0000000..7aa39a2 --- /dev/null +++ b/src/main/java/service/QuestService.java @@ -0,0 +1,110 @@ +package service; + +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; + private static final Logger logger = LogManager.getLogger(QuestService.class); + + public QuestService() { + QuestConfigLoader configLoader = new QuestConfigLoader(); + this.steps = configLoader.getSteps(); + logger.info("QuestService initialized with {} steps", steps.size()); + } + + public QuestStep getStep(String 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) { + 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) { + logger.error("Current step not found: {}", currentStepId); + return null; + } + + if (!currentStep.getOptions().containsKey(choice)) { + 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"; + } + } + + // Применяем эффекты выбора (добавляем предметы в инвентарь) + applyEffects(playerState, currentStep, choice); + + // Устанавливаем следующий шаг + playerState.setCurrentStepId(nextStepId); + QuestStep nextStep = getStep(nextStepId); + + if (nextStep == null) { + 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; + + String item = step.getEffects().get(choice); + if (item == null) return; + + if (!playerState.hasItem(item)) { + playerState.addToInventory(item); + logger.info("Player acquired item: {}", 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())) { + logger.debug("Requirement not met: need {} for step", req.getKey()); + return false; + } + } + return true; + } +} \ 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..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 new file mode 100644 index 0000000..dd3b3fe --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,49 @@ + + + + QuestEscape + + + welcome + + + + + EncodingFilter + controller.EncodingFilter + + + + EncodingFilter + /* + + + + + 404 + /error.jsp + + + + 500 + /error.jsp + + + + 403 + /error.jsp + + + + 30 + + true + + + + \ No newline at end of file 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 new file mode 100644 index 0000000..5b6f9e8 --- /dev/null +++ b/src/main/webapp/quest.jsp @@ -0,0 +1,506 @@ +<%@ 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