diff --git a/pom.xml b/pom.xml index cda3199..78ee59d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,9 +5,9 @@ 4.0.0 com.javarush.khmelov - project-pantera + project-ledzeppelin 1.0-SNAPSHOT - ProjectPantera + ProjectLedzeppelin war @@ -36,10 +36,6 @@ - - org.projectlombok - lombok - jakarta.servlet jakarta.servlet-api @@ -54,6 +50,11 @@ jakarta.servlet.jsp.jstl + + org.projectlombok + lombok + provided + org.junit.jupiter junit-jupiter-api @@ -64,10 +65,16 @@ junit-jupiter-engine test + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + org.apache.maven.plugins maven-compiler-plugin @@ -81,11 +88,7 @@ - - org.apache.maven.plugins - maven-war-plugin - 3.4.0 - + \ No newline at end of file diff --git a/src/main/java/com/javarush/martynov/GameState.java b/src/main/java/com/javarush/martynov/GameState.java new file mode 100644 index 0000000..2c26fdc --- /dev/null +++ b/src/main/java/com/javarush/martynov/GameState.java @@ -0,0 +1,77 @@ +package com.javarush.martynov; + +public class GameState { + private String playerName; + private int step = 0; + private int gamesPlayed = 0; + private String lastMessage; + private int wins; + private int losses; + + public int getWins() { + return wins; + } + + public void setWins(int wins) { + this.wins = wins; + } + + public int getLosses() { + return losses; + } + + public void setLosses(int losses) { + this.losses = losses; + } + + public GameState() { + } + + private String deathReason; + + public String getDeathReason() { + return deathReason; + } + + public void setDeathReason(String deathReason) { + this.deathReason = deathReason; + } + + public void resetGame() { + this.step = 0; + this.deathReason = ""; + } + + + public String getPlayerName() { + return playerName; + } + + public void setPlayerName(String playerName) { + this.playerName = playerName; + } + + public int getStep() { + return step; + } + + public void setStep(int step) { + this.step = step; + } + + public int getGamesPlayed() { + return gamesPlayed; + } + + public void incrementGames() { + this.gamesPlayed++; + } + + public String getLastMessage() { + return lastMessage; + } + + public void setLastMessage(String lastMessage) { + this.lastMessage = lastMessage; + } +} diff --git a/src/main/java/com/javarush/martynov/QuestServlet.java b/src/main/java/com/javarush/martynov/QuestServlet.java new file mode 100644 index 0000000..99a8117 --- /dev/null +++ b/src/main/java/com/javarush/martynov/QuestServlet.java @@ -0,0 +1,152 @@ +package com.javarush.martynov; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@WebServlet(name = "QuestServlet", value = "/quest") +public class QuestServlet extends HttpServlet { + private static final Logger logger = LoggerFactory.getLogger(QuestServlet.class); + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + HttpSession session = req.getSession(); + GameState state = (GameState) session.getAttribute("state"); + String sessionId = session.getId(); + + if (state == null) { + state = new GameState(); + session.setAttribute("state", state); + logger.info("New game state created for session: {}", sessionId); + } + + String action = req.getParameter("action"); + logger.debug("Received action: {} for session: {}", action, sessionId); + + if ("start".equals(action)) { + String name = req.getParameter("name"); + state.setPlayerName(name); + state.setStep(1); + logger.info("Player '{}' has started the journey (Session: {})", name, sessionId); + + } else if ("answer".equals(action)) { + String choice = req.getParameter("choice"); + logger.debug("Player '{}' made a choice: {} on step {}", state.getPlayerName(), choice, state.getStep()); + processGameStep(state, choice); + + } else if ("restart".equals(action)) { + logger.info("Player '{}' requested a restart. Games played before restart: {}", state.getPlayerName(), state.getGamesPlayed()); + state.resetGame(); + state.incrementGames(); + } + + req.getRequestDispatcher("/quest.jsp").forward(req, resp); + } + + private void updateGlobalStatistics(String name, boolean isWin) { + if (name == null || name.isEmpty()) { + logger.warn("Attempted to update statistics for a null or empty player name."); + return; + } + + ServletContext context = getServletContext(); + Map statsMap = (Map) context.getAttribute("globalStats"); + + if (statsMap == null) { + statsMap = new ConcurrentHashMap<>(); + context.setAttribute("globalStats", statsMap); + logger.debug("Global statistics map initialized in ServletContext."); + } + + UserStats userStats = statsMap.computeIfAbsent(name, k -> { + logger.debug("Creating new UserStats entry for player: {}", name); + return new UserStats(); + }); + + if (isWin) { + userStats.addWin(); + logger.info("Global stats updated: Player '{}' WON.", name); + } else { + userStats.addLoss(); + logger.info("Global stats updated: Player '{}' LOST.", name); + } + } + + private void processGameStep(GameState state, String choice) { + int currentStep = state.getStep(); + String name = state.getPlayerName(); + + switch (currentStep) { + case 1: + if ("trust".equals(choice)) { + state.setStep(2); + } else { + state.setDeathReason("Вы вышли на открытое пространство и стали легкой добычей для стаи зомби."); + state.setStep(-1); + logger.warn("Player '{}' died at Step 1. Choice: {}", name, choice); + updateGlobalStatistics(name, false); + } + break; + + case 2: + if ("truth".equals(choice)) { + state.setStep(3); + } else { + state.setDeathReason("Вступать в рукопашную с зомби было плохой идеей. Вас повалили числом."); + state.setStep(-1); + logger.warn("Player '{}' died at Step 2. Choice: {}", name, choice); + updateGlobalStatistics(name, false); + } + break; + + case 3: + if ("truth".equals(choice)) { + state.setStep(4); + } else { + state.setDeathReason("В сумерках вы не заметили ловушку в заброшенном здании и погибли."); + state.setStep(-1); + logger.warn("Player '{}' died at Step 3. Choice: {}", name, choice); + updateGlobalStatistics(name, false); + } + break; + + case 4: + if ("truth".equals(choice)) { + state.setStep(5); + } else { + state.setDeathReason("Охранник почувствовал ложь в вашем голосе. Он решил не рисковать и выстрелил."); + state.setStep(-1); + logger.warn("Player '{}' died at Step 4. Choice: {}", name, choice); + updateGlobalStatistics(name, false); + } + break; + + case 5: + if ("scan".equals(choice)) { + state.setStep(6); + logger.info("SUCCESS! Player '{}' reached the bunker.", name); + updateGlobalStatistics(name, true); + } else { + state.setDeathReason("Ваша попытка прорваться силой закончилась быстро. Охранники на вышках не промахиваются."); + state.setStep(-1); + logger.warn("Player '{}' died at Step 5 (The Scanner). Choice: {}", name, choice); + updateGlobalStatistics(name, false); + } + break; + + default: + logger.error("Unexpected game step {} encountered for player '{}'", currentStep, name); + break; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/martynov/UserStats.java b/src/main/java/com/javarush/martynov/UserStats.java new file mode 100644 index 0000000..ba3bd32 --- /dev/null +++ b/src/main/java/com/javarush/martynov/UserStats.java @@ -0,0 +1,14 @@ +package com.javarush.martynov; + +public class UserStats { + + private int wins = 0; + private int losses = 0; + + public void addWin() { wins++; } + public void addLoss() { losses++; } + + public int getWins() { return wins; } + public int getLosses() { return losses; } + public int getTotal() { return wins + losses; } +} diff --git a/src/main/java/com/javarush/martynov/resources/META-INF/beans.xml b/src/main/java/com/javarush/martynov/resources/META-INF/beans.xml new file mode 100644 index 0000000..96b4c76 --- /dev/null +++ b/src/main/java/com/javarush/martynov/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/main/java/com/javarush/martynov/resources/logback.xml b/src/main/java/com/javarush/martynov/resources/logback.xml new file mode 100644 index 0000000..763483e --- /dev/null +++ b/src/main/java/com/javarush/martynov/resources/logback.xml @@ -0,0 +1,20 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + logs/zombie-quest.log + true + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/src/main/java/test/java/QuestServletTest.java b/src/main/java/test/java/QuestServletTest.java new file mode 100644 index 0000000..99ec672 --- /dev/null +++ b/src/main/java/test/java/QuestServletTest.java @@ -0,0 +1,76 @@ +package test.java; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import martynov.GameState; +import martynov.QuestServlet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) +public class QuestServletTest { + + @InjectMocks + private QuestServlet servlet; + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private HttpSession session; + @Mock + private RequestDispatcher dispatcher; + @Mock + private ServletContext servletContext; + + private GameState state; + + @BeforeEach + void setUp() { + state = new GameState(); + when(request.getSession()).thenReturn(session); + when(session.getAttribute("state")).thenReturn(state); + when(request.getRequestDispatcher("/quest.jsp")).thenReturn(dispatcher); + } + + @Test + void testProcessAnswerCorrect() throws ServletException, IOException { + state.setStep(1); + state.setPlayerName("Stalker"); + + when(request.getParameter("action")).thenReturn("answer"); + when(request.getParameter("choice")).thenReturn("trust"); + + servlet.doGet(request, response); + + assertEquals(2, state.getStep(), "После выбора 'trust' на 1 шаге, текущий шаг должен быть 2"); + + verify(dispatcher, times(1)).forward(request, response); + } + + @Test + void testProcessAnswerWrong() throws ServletException, IOException { + state.setStep(1); + + when(request.getParameter("action")).thenReturn("answer"); + when(request.getParameter("choice")).thenReturn("run"); + + servlet.doGet(request, response); + + assertEquals(-1, state.getStep(), "После неправильного выбора шаг должен стать -1"); + } +} diff --git a/src/main/webapp/quest.jsp b/src/main/webapp/quest.jsp new file mode 100644 index 0000000..dd1bfdf --- /dev/null +++ b/src/main/webapp/quest.jsp @@ -0,0 +1,259 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + Zombie Apocalypse: 2028 + + + + + +
+ + <%-- Шаг 0: Ввод имени --%> + +

*** ЗОМБИ АПОКАЛИПСИС ***

+

В 2028 году после очередной пандемии люди начали превращаться в зомби, ты один из немногих кому повезло. + Единственный шанс на спасение, это найти укрытие в старом заброшенном бункере, но до него стоит еще + дойти сквозь серые безлюдные улицы населенные зомби...

+

Введи свое имя выживший, что бы начать путь к бункеру! :

+
+ + + +
+
+ + <%-- Шаг 1: Движение по городу --%> + +

Привет выживший, ${state.playerName}!

+

"Ты видишь открытое пространство в городе, как будешь двигаться?"

+ "Пойду вдоль домов, буду действовать скрытно" + "Пойду прямо по открытому пространству, ведь ни кого + нет в округе" +
+ <%-- Шаг 2: Нападение зомби --%> + +

"Ты остановился в одном из домов для небольшой передышки и на тебя бежит зомби"

+ "Убежать и спрятаться" + "Вступить в драку" +
+ + <%-- Шаг 3: Отдых --%> + +

"Тебе удалось спастись от зомби, но пока ты прятался стало уже темно"

+ "Найти место для ночевки" + "Двигаться в сумерках. Ведь так будет безопаснее" +
+ + +

"Ты проснулся и продолжил движение в бункер. Через некоторое время ты дошел до него. Охранник на воротах + спрашивает: *Стой! Ты не заражен? Я вижу на тебе следы крови.*"

+ "Я поранился пока шел сюда спасаясь от зомби, но я полностью чист" + "Ты что кровь ни когда не видел? тут пораниться дело плевое" +
+ + <%-- Шаг 4: Разговор с охранником --%> + +

"Ты дошел до входа в бункер. Охранник целится в тебя из винтовки: + *Стой! Ты не заражен? Я вижу на тебе следы крови.*"

+ "Я поранился, пока шел сюда, но я полностью чист" + "Ты что, кровь никогда не видел? Тут пораниться — дело плевое" +
+ + <%-- Шаг 5: Проверка на человечность (Финал перед входом) --%> + +

"Охранник опускает ствол, но не убирает палец со спускового крючка. + *Ладно... Но правила одни для всех. Брось рюкзак и пройди через сканер. + Если он запищит и на тебе буду следы укусов — я стреляю без предупреждения.*"

+ "Согласиться на проверку" + "Послать охранника" +
+ + <%-- Финал: Победа (Шаг 6) --%> + +

ПОБЕДА!

+

Сканер горит зеленым. Ты с облегчением выдыхаешь... Тяжелая стальная дверь со скрипом закрывается за твоей спиной. + Снаружи слышны вопли тех, кто остался в темноте, но здесь есть еда, вода и кров. Ты спасен. + Вы в безопасности, ${state.playerName}.

+

Всего игр пройдено: ${state.gamesPlayed + 1}

+ Начать новую историю +
+ + <%-- Финал: Смерть --%> + +

ВЫ ПОГИБЛИ

+

${state.deathReason}

+

Всего попыток: ${state.gamesPlayed + 1}

+ Попробовать еще раз +
+
+ <%-- Секция Глобальной Статистики --%> +
+ +
+

Архив Выживших

+ + + + + + + + + + + + <%-- Проходим циклом по нашей карте из ServletContext --%> + + + + + + + + + + <%-- Если статистика еще пуста --%> + + + + + + +
ИмяПобедыСмертиВсего
+ ${entry.key} + + ${entry.value.wins} + + ${entry.value.losses} + + ${entry.value.total} +
+ Данные о выживших отсутствуют... пока что. +
+
+ +
+ + \ No newline at end of file