Dzisiaj napiszemy grę w kółko i krzyżyk przy użyciu serwletów i JSP.

Ten projekt będzie trochę różnił się od poprzednich. Znajdą się w nim nie tylko zadania, ale również wyjaśnienia, jak je wykonać. Czyli będzie to projekt z cyklu „JAK TO…”.

Instrukcja:

  1. Widelec z repozytorium: https://github.com/CodeGymCC/project-servlet.git
  2. Pobierz swoją wersję projektu na swój komputer.
  3. Skonfiguruj uruchamianie aplikacji w IDEA:
    • Alt + Shift + F9 -> Edytuj konfiguracje… -> Alt + insert -> tom (w pasku wyszukiwania) -> Local.
    • Następnie musisz kliknąć „KONFIGURUJ” i wskazać, gdzie archiwum z Tomcatem zostało pobrane i rozpakowane.
    • W zakładce „Deployment”: Alt + insert -> Artifact… -> kółko i krzyżyk: wybuch wojny -> OK.
    • W polu „Kontekst aplikacji”: pozostaw tylko „/” (ukośnik).
    • Naciśnij „APLIKUJ”.
    • Zamknij okno ustawień.
    • Wykonaj pierwsze uruchomienie testowe dostosowanej konfiguracji. Jeśli wszystko zostanie wykonane poprawnie, otworzy się domyślna przeglądarka, w której będzie:
  4. Otwórz plik „pom.xml” . W bloku „dependencies” znajdują się 2 zależności .
    • javax.servlet-apiodpowiada za specyfikację serwletów. Zakres „dostarczony” jest potrzebny podczas programowania, ale nie jest potrzebny w czasie wykonywania (Tomcat ma już tę zależność w folderze lib).
    • jstl– można uznać za silnik szablonów.
  5. W folderze „webapp” znajdują się 3 pliki :
    • index.jsp- to jest nasz szablon (podobny do strony HTML). Będzie zawierał znaczniki i skrypty. Jest to plik o nazwie „index” , który jest podawany jako strona początkowa, jeśli nie ma konfiguracji, co widzieliśmy w kroku 3.
    • /static/main.css- plik dla stylów. Podobnie jak w poprzednim projekcie, tutaj wszystko zależy od Ciebie, maluj, jak chcesz.
    • /static/jquery-3.6.0.min.js- zależność frontendu, którą nasz serwer będzie dystrybuował jako statyczną.
  6. Pakiet „com.tictactoe” będzie zawierał cały kod Java. W tej chwili są 2 klasy:
    • Sign- enum, które odpowiada za "cross/zero/void" .
    • Fieldjest naszą dziedziną. Ta klasa ma mapę „pola” . Zasada przechowywania danych będzie następująca: komórki pola kółko i krzyżyk są numerowane od zera. W pierwszym wierszu 0, 1 i 2. W drugim: 3, 4 i 5. I tak dalej. Istnieją również 3 metody. „getEmptyFieldIndex” szuka pierwszej pustej komórki (tak, nasz przeciwnik nie będzie zbyt sprytny). "checkWin" sprawdza, czy gra się skończyła. Jeśli jest rząd trzech krzyżyków, zwraca krzyżyk; jeśli jest rząd trzech zer, zwraca zero. W przeciwnym razie jest pusty. "getFieldData" - zwraca wartości mapy "field" jako listę posortowaną rosnąco według indeksu.
  7. Wyjaśnienia dotyczące szablonu są zakończone, teraz możesz rozpocząć zadanie. Zacznijmy od narysowania tabeli 3 na 3. Aby to zrobić, dodaj następujący kod do „index.jsp” :
    <table>
    	<tr>
    		<td>0</td>
    		<td>1</td>
    		<td>2</td>
    	</tr>
    	<tr>
    		<td>3</td>
    		<td>4</td>
    		<td>5</td>
    	</tr>
    	<tr>
    		<td>6</td>
    		<td>7</td>
    		<td>8</td>
    	</tr>
    </table>
    Następnie usuniemy liczby z tabeli i zastąpimy je krzyżykiem, zerem lub pustym polem. Ponadto w tagu „head” umieść plik stylu. Aby to zrobić, dodaj linię:<link href="static/main.css" rel="stylesheet">

    Zawartość pliku stylu zależy od Ciebie. Użyłem tego:
    td {
        border: 3px solid black;
        padding: 10px;
        border-collapse: separate;
        margin: 10px;
        width: 100px;
        height: 100px;
        font-size: 50px;
        text-align: center;
        empty-cells: show;
    }
    Po uruchomieniu mój wynik wygląda tak:
  8. Dodajmy teraz następującą funkcjonalność: po kliknięciu komórki zostanie wysłane żądanie do serwera, w którym jako parametr przekażemy indeks klikniętej komórki. To zadanie można podzielić na dwie części: wyślij żądanie z frontu, zaakceptuj żądanie na serwerze. Zacznijmy dla odmiany od przodu.

    Dodajmy parametr „onclick” do każdego tagu „d” . W wartości wskazujemy zmianę aktualnej strony na podany adres URL. Serwlet odpowiedzialny za logikę będzie miał adres URL „/logic” . I zajmie parametr o nazwie „click” . Przekażemy więc indeks komórki, którą kliknął użytkownik.
    <table>
        <tr>
            <td onclick="window.location='/logic?click=0'">0</td>
            <td onclick="window.location='/logic?click=1'">1</td>
            <td onclick="window.location='/logic?click=2'">2</td>
        </tr>
        <tr>
            <td onclick="window.location='/logic?click=3'">3</td>
            <td onclick="window.location='/logic?click=4'">4</td>
            <td onclick="window.location='/logic?click=5'">5</td>
        </tr>
        <tr>
            <td onclick="window.location='/logic?click=6'">6</td>
            <td onclick="window.location='/logic?click=7'">7</td>
            <td onclick="window.location='/logic?click=8'">8</td>
        </tr>
    </table>
    Możesz sprawdzić, czy wszystko jest zrobione poprawnie poprzez panel programisty w przeglądarce. Na przykład w przeglądarce Chrome otwiera się za pomocą przycisku F12 . W wyniku kliknięcia na komórkę o indeksie 4, obraz będzie wyglądał następująco: Otrzymujemy błąd, ponieważ nie stworzyliśmy jeszcze serwletu, który może wysłać serwer na adres „logika” .
  9. W paczce "com.tictactoe" utwórz klasę "LogicServlet" , która powinna być pochodną klasy "javax.servlet.http.HttpServlet" . W klasie zastąp metodę „doGet” .

    I dodajmy metodę, która pobierze indeks klikniętej komórki. Musisz także dodać mapowanie (adres, pod którym ten aplet przechwyci żądanie). Sugeruję zrobić to za pomocą adnotacji (ale jeśli lubisz trudności, możesz również użyć web.xml). Ogólny kod serwletu:
    package com.tictactoe;
    
    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 java.io.IOException;
    
    
    @WebServlet(name = "LogicServlet", value = "/logic")
    public class LogicServlet extends HttpServlet {
        @Override
    	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            int index = getSelectedIndex(req);
            resp.sendRedirect("/index.jsp");
        }
    
    
        private int getSelectedIndex(HttpServletRequest request) {
            String click = request.getParameter("click");
            boolean isNumeric = click.chars().allMatch(Character::isDigit);
            return isNumeric ? Integer.parseInt(click) : 0;
        }
    
    }
    Teraz po kliknięciu w dowolną komórkę otrzymamy indeks tej komórki na serwerze (możesz się upewnić uruchamiając serwer w trybie debugowania). I nastąpi przekierowanie do tej samej strony, z której dokonano kliknięcia.
  10. Teraz możemy klikać, ale to jeszcze nie jest gra. Aby gra miała logikę, musisz zapisywać stan gry (gdzie są krzyżyki, gdzie są zera) pomiędzy żądaniami. Najprostszym sposobem na to jest przechowywanie tych danych w sesji. Dzięki takiemu podejściu sesja zostanie zapisana na serwerze, a klient otrzyma identyfikator sesji w pliku cookie o nazwie „JSESSIONID” . Ale sesja nie musi być tworzona za każdym razem, ale tylko na początku gry. Uruchommy w tym celu inny serwlet, który nazwiemy „InitServlet” . Nadpiszemy w nim metodę „doGet” , w której utworzymy nową sesję, stworzymy pole gry, wstawimy to pole gry i listę typu Sign w atrybuty sesji, i wyślemy „forward” do pliku index.jsp strona. Kod:
    package com.tictactoe;
    
    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;
    import java.util.List;
    import java.util.Map;
    
    @WebServlet(name = "InitServlet", value = "/start")
    public class InitServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // Create a new session
            HttpSession currentSession = req.getSession(true);
    
            // Create a playing field
            Field field = new Field();
            Map<Integer, Sign> fieldData = field.getField();
    
            // Get a list of field values
            List<Sign> data = field.getFieldData();
    
            // Adding field parameters to the session (needed to store state between requests)
            currentSession.setAttribute("field", field);
            // and field values ​​sorted by index (required for drawing crosses and zeroes)
            currentSession.setAttribute("data", data);
    
            // Redirect request to index.jsp page via server
            getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
        }
    }
    I nie zapominajmy, zmieńmy stronę startową, która otwiera się w przeglądarce po uruchomieniu serwera na „/start” : Teraz po ponownym uruchomieniu serwera i kliknięciu dowolnej komórki pola w menu programisty przeglądarki w sekcji „Nagłówki żądań” , pojawi się plik cookie z identyfikatorem sesji:
  11. Gdy mamy już repozytorium, w którym możemy przechowywać stan pomiędzy żądaniami klienta (przeglądarki), możemy przystąpić do pisania logiki gry. Logika, którą mamy, znajduje się w „LogicServlet” . Musimy pracować metodą „doGet” . Dodajmy to zachowanie do metody:
    • otrzymamy z sesji obiekt „field” typu Field (przeniesiemy go do metody „extractField” ).
    • umieść krzyżyk w miejscu, w którym użytkownik kliknął (do tej pory bez żadnych kontroli).
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();
    
        // Get the playfield object from the session
        Field field = extractField(currentSession);
    
        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
    
        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);
    
        // Read the list of icons
        List<Sign> data = field.getFieldData();
    
        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);
    
        resp.sendRedirect("/index.jsp");
    }
    
    
    
    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
    Zachowanie jeszcze się nie zmieniło, ale jeśli uruchomisz serwer w trybie debugowania i ustawisz punkt przerwania na linii, do której wysyłane jest przekierowanie, możesz zobaczyć „wnętrzności” obiektu „data . Rzeczywiście, pod klikniętym indeksem pojawia się „KRZYŻ” .
  12. Teraz czas na wyświetlenie krzyżyka na frontendzie. W tym celu będziemy pracować z plikiem „index.jsp” i technologią „JSTL” .
    • W sekcji <head> dodaj:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • W tabeli wewnątrz każdego bloku <td> zmień indeks na konstrukcję, która umożliwia obliczanie wartości. Na przykład dla indeksu zero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Teraz, gdy klikniesz na komórkę, pojawi się tam krzyżyk:
  13. Zrobiliśmy swój ruch, teraz kolej na „zero”. I dodajmy tutaj kilka czeków, aby znaki nie były umieszczane w już zajętych komórkach.
    • Musisz sprawdzić, czy kliknięta komórka jest pusta. W przeciwnym razie nic nie robimy i odsyłamy użytkownika na tę samą stronę bez zmiany parametrów sesji.
    • Ponieważ liczba pól na polu jest nieparzysta, możliwe, że został postawiony krzyżyk, ale nie ma miejsca na zero. Dlatego po postawieniu krzyżyka staramy się uzyskać indeks niezajętej komórki (metoda getEmptyFieldIndex klasy Field). Jeśli indeks nie jest ujemny, umieść tam zero. Kod:
      package com.tictactoe;
      
      import javax.servlet.RequestDispatcher;
      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;
      import java.util.List;
      
      @WebServlet(name = "LogicServlet", value = "/logic")
      public class LogicServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // Get the current session
              HttpSession currentSession = req.getSession();
      
              // Get the playfield object from the session
              Field field = extractField(currentSession);
      
              // get the index of the cell that was clicked
              int index = getSelectedIndex(req);
              Sign currentSign = field.getField().get(index);
      
              // Check if the clicked cell is empty.
              // Otherwise, we do nothing and send the user to the same page without changes
              // parameters in the session
              if (Sign.EMPTY != currentSign) {
                  RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
                  dispatcher.forward(req, resp);
                  return;
              }
      
              // put a cross in the cell that the user clicked on
              field.getField().put(index, Sign.CROSS);
      
              // Get an empty field cell
              int emptyFieldIndex = field.getEmptyFieldIndex();
      
              if (emptyFieldIndex >= 0) {
                  field.getField().put(emptyFieldIndex, Sign.NOUGHT);
              }
      
              // Read the list of icons
              List<Sign> data = field.getFieldData();
      
              // Update field object and icon list in session
              currentSession.setAttribute("data", data);
              currentSession.setAttribute("field", field);
      
              resp.sendRedirect("/index.jsp");
          }
      
          private int getSelectedIndex(HttpServletRequest request) {
              String click = request.getParameter("click");
              boolean isNumeric = click.chars().allMatch(Character::isDigit);
              return isNumeric ? Integer.parseInt(click) : 0;
          }
      
          private Field extractField(HttpSession currentSession) {
              Object fieldAttribute = currentSession.getAttribute("field");
              if (Field.class != fieldAttribute.getClass()) {
                  currentSession.invalidate();
                  throw new RuntimeException("Session is broken, try one more time");
              }
              return (Field) fieldAttribute;
          }
      }
  14. Na tym etapie można stawiać krzyżyki, AI odpowiada zerami. Ale nie ma sprawdzenia, kiedy zatrzymać grę. Może to nastąpić w trzech przypadkach:
    • po kolejnym ruchu krzyża utworzyła się linia trzech krzyży;
    • po kolejnym ruchu powrotnym z zerem utworzyła się linia trzech zer;
    • po kolejnym ruchu krzyża kończyły się puste cele.
    Dodajmy metodę, która sprawdza, czy są trzy krzyżyki/zera w rzędzie:
    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);
    
            // Read the list of icons
            List<Sign> data = field.getFieldData();
    
            // Update this list in session
            currentSession.setAttribute("data", data);
    
            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }
    Osobliwością tej metody jest to, że w przypadku znalezienia zwycięzcy dodajemy do sesji kolejny parametr, za pomocą którego zmienimy wyświetlanie w „index.jsp” w kolejnych akapitach.
  15. Dodajmy dwukrotnie wywołanie metody „checkWin ” do metody „doGet” . Pierwszy raz po ustawieniu krzyżyka, drugi po ustawieniu zera.
    // Check if the cross won after adding the user's last click
    if (checkWin(resp, currentSession, field)) {
        return;
    }
    if (emptyFieldIndex >= 0) {
        field.getField().put(emptyFieldIndex, Sign.NOUGHT);
        // Check if the zero won after adding the last zero
        if (checkWin(resp, currentSession, field)) {
            return;
        }
    }
  16. Jeśli chodzi o zachowanie, prawie nic się nie zmieniło (poza tym, że jeśli jeden ze znaków wygrywa, zera nie są już wstawiane. Użyjmy parametru „winner” w „index.jsp” i wyświetlmy zwycięzcę. Używamy dyrektyw po tabeli: c:setc:if
    <hr>
    <c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
    <c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>
    
    <c:if test="${winner == CROSSES}">
        <h1>CROSSES WIN!</h1>
    </c:if>
    <c:if test="${winner == NOUGHTS}">
        <h1>NOUGHTS WIN!</h1>
    </c:if>
    Jeśli wygrają krzyże, pojawi się komunikat „KRZYŻE WYGRYWAJĄ!” , jeśli zera to „NOUGHTS WIN!” . W efekcie możemy otrzymać jeden z dwóch napisów:
  17. Jeśli jest zwycięzca, musisz być w stanie się zemścić. Aby to zrobić, potrzebujesz przycisku, który wyśle ​​​​żądanie do serwera. A serwer unieważni bieżącą sesję i przekieruje żądanie z powrotem do „/ start” .
    • W „index.jsp” w sekcji „head” wpisz skrypt „jquery” . Za pomocą tej biblioteki wyślemy żądanie do serwera.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • W „index.jsp” w sekcji „skrypt” dodaj funkcję, która może wysłać żądanie POST do serwera. Sprawimy, że funkcja będzie synchroniczna, a gdy przyjdzie odpowiedź z serwera, przeładuje bieżącą stronę.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Wewnątrz bloków „c:if” dodaj przycisk, który po kliknięciu wywołuje funkcję, którą właśnie napisaliśmy:
      <c:if test="${winner == CROSSES}">
          <h1>CROSSES WIN!</h1>
          <button onclick="restart()">Start again</button>
      </c:if>
      <c:if test="${winner == NOUGHTS}">
          <h1>NOUGHTS WIN!</h1>
          <button onclick="restart()">Start again</button>
      </c:if>
    • Stwórzmy nowy serwlet, który będzie obsługiwał adres URL „/restart” .
      package com.tictactoe;
      
      import javax.servlet.annotation.WebServlet;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      
      @WebServlet(name = "RestartServlet", value = "/restart")
      public class RestartServlet extends HttpServlet {
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
              req.getSession().invalidate();
              resp.sendRedirect("/start");
          }
      }
      Po zwycięstwie pojawi się przycisk „Zacznij od nowa” . Po kliknięciu na nią pole zostanie całkowicie wyczyszczone, a gra rozpocznie się od nowa.
  18. Pozostaje rozważyć ostatnią sytuację. Co jeśli użytkownik postawił krzyżyk, nie było zwycięstwa i nie ma miejsca na zero? To jest remis i teraz go przetworzymy:
    • W sesji „LogicServlet” dodaj kolejny parametr „draw” , zaktualizuj pole „data” i wyślij przekierowanie do „index.jsp” :
      // If such a cell exists
      if (emptyFieldIndex >= 0) {}
      // If there is no empty cell and no one wins, then it's a draw
      else {
          // Add a flag to the session that signals that a draw has occurred
          currentSession.setAttribute("draw", true);
      
          // Read the list of icons
          List<Sign> data = field.getFieldData();
      
          // Update this list in session
          currentSession.setAttribute("data", data);
      
          // helmet redirect
          response.sendRedirect("/index.jsp");
          return;
      }
    • W „index.jsp” przetworzymy ten parametr:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      W wyniku losowania otrzymamy odpowiednią wiadomość i propozycję rozpoczęcia od nowa:

To kończy pisanie gry.

Kod klas i plików, z którymi pracowały

InitServlet

package com.tictactoe;

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;
import java.util.List;
import java.util.Map;

@WebServlet(name = "InitServlet", value = "/start")
public class InitServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Create a new session
        HttpSession currentSession = req.getSession(true);

        // Create a playing field
        Field field = new Field();
        Map<Integer, Sign> fieldData = field.getField();

        // Get a list of field values
        List<Sign> data = field.getFieldData();

        // Adding field parameters to the session (needed to store state between requests)
        currentSession.setAttribute("field", field);
        // and field values ​​sorted by index (required for drawing crosses and zeroes)
        currentSession.setAttribute("data", data);

        // Redirect request to index.jsp page via server
        getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
    }
}

serwlet logiczny

package com.tictactoe;

import javax.servlet.RequestDispatcher;
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;
import java.util.List;

@WebServlet(name = "LogicServlet", value = "/logic")
public class LogicServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();

        // Get the playfield object from the session
        Field field = extractField(currentSession);

        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
        Sign currentSign = field.getField().get(index);

        // Check if the clicked cell is empty.
        // Otherwise, we do nothing and send the user to the same page without changes
        // parameters in the session
        if (Sign.EMPTY != currentSign) {
            RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
            dispatcher.forward(req, resp);
            return;
        }

        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);

        // Check if the cross has won after adding the user's last click
        if (checkWin(resp, currentSession, field)) {
            return;
        }

        // Get an empty field cell
        int emptyFieldIndex = field.getEmptyFieldIndex();

        if (emptyFieldIndex >= 0) {
            field.getField().put(emptyFieldIndex, Sign.NOUGHT);
            // Check if the zero won after adding the last zero
            if (checkWin(resp, currentSession, field)) {
                return;
            }
        }
        // If there is no empty cell and no one wins, then it's a draw
        else {
            // Add a flag to the session that signals that a draw has occurred
            currentSession.setAttribute("draw", true);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            resp.sendRedirect("/index.jsp");
            return;
        }

        // Read the list of icons
        List<Sign> data = field.getFieldData();

        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);

        resp.sendRedirect("/index.jsp");
    }

    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }

    private int getSelectedIndex(HttpServletRequest request) {
        String click = request.getParameter("click");
        boolean isNumeric = click.chars().allMatch(Character::isDigit);
        return isNumeric ? Integer.parseInt(click) : 0;
    }

    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
}

Uruchom ponownie serwlet

package com.tictactoe;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "RestartServlet", value = "/restart")
public class RestartServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        req.getSession().invalidate();
        resp.sendRedirect("/start");
    }
}

index.jsp _

<%@ page import="com.tictactoe.Sign" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<!DOCTYPE html>
<html>
<head>
    <link href="static/main.css" rel="stylesheet">
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    <title>Tic-Tac-Toe</title>
</head>
<body>
<h1>Tic-Tac-Toe</h1>

<table>
    <tr>
        <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td>
        <td onclick="window.location='/logic?click=1'">${data.get(1).getSign()}</td>
        <td onclick="window.location='/logic?click=2'">${data.get(2).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=3'">${data.get(3).getSign()}</td>
        <td onclick="window.location='/logic?click=4'">${data.get(4).getSign()}</td>
        <td onclick="window.location='/logic?click=5'">${data.get(5).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=6'">${data.get(6).getSign()}</td>
        <td onclick="window.location='/logic?click=7'">${data.get(7).getSign()}</td>
        <td onclick="window.location='/logic?click=8'">${data.get(8).getSign()}</td>
    </tr>
</table>

<hr>
<c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
<c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>

<c:if test="${winner == CROSSES}">
    <h1>CROSSES WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${winner == NOUGHTS}">
    <h1>NOUGHTS WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${draw}">
    <h1>IT'S A DRAW</h1>
    <button onclick="restart()">Start again</button>
</c:if>

<script>
    function restart() {
        $.ajax({
            url: '/restart',
            type: 'POST',
            contentType: 'application/json;charset=UTF-8',
            async: false,
            success: function () {
                location.reload();
            }
        });
    }
</script>

</body>
</html>

main.css _

td {
    border: 3px solid black;
    padding: 10px;
    border-collapse: separate;
    margin: 10px;
    width: 100px;
    height: 100px;
    font-size: 50px;
    text-align: center;
    empty-cells: show;
   }