I dag vil vi skrive et Tic-Tac-Toe-spil ved hjælp af servlets og JSP.

Dette projekt vil være lidt anderledes end de tidligere. Det vil ikke kun indeholde opgaver, men også forklaringer på, hvordan de udføres. Det vil sige, at det bliver et projekt fra "HOW TO ..."-serien.

Instruktion:

  1. Fork fra depotet: https://github.com/CodeGymCC/project-servlet.git
  2. Download din version af projektet til din computer.
  3. Konfigurer applikationsstart i IDEA:
    • Alt + Shift + F9 -> Rediger konfigurationer... -> Alt + indsæt -> tom (i søgelinjen) -> Lokal.
    • Derefter skal du klikke på "CONFIGURE" og angive, hvor arkivet med Tomcat blev downloadet og pakket ud.
    • På fanen "Deployment": Alt + indsæt -> Artifact... -> tic-tac-toe:war eksploderede -> OK.
    • I feltet "Applikationskontekst": lad kun "/" (skråstreg).
    • Tryk på "APPLY".
    • Luk indstillingsvinduet.
    • Foretag den første testkørsel af den tilpassede konfiguration. Hvis alt er gjort korrekt, åbnes din standardbrowser, hvor den vil være:
  4. Åbn filen "pom.xml" . Der er 2 afhængigheder i blokken "afhængigheder" .
    • javax.servlet-apier ansvarlig for specifikationen af ​​servlets. Omfang "forudsat" er påkrævet under udvikling, men ikke nødvendigt under kørsel (Tomcat har allerede denne afhængighed i lib-mappen).
    • jstl– kan betragtes som en skabelonmotor.
  5. Der er 3 filer i "webapp" -mappen :
    • index.jsp- dette er vores skabelon (svarende til HTML-siden). Det vil indeholde opmærkning og scripts. Det er filen kaldet "indeks" , der er angivet som startsiden, hvis der ikke er nogen konfigurationer, som vi så i trin 3.
    • /static/main.css- fil for stilarter. Som i det forrige projekt er alt her op til dig, mal som du ønsker.
    • /static/jquery-3.6.0.min.js- frontend-afhængighed, som vores server vil distribuere som statisk.
  6. "com.tictactoe" -pakken vil indeholde al Java-koden. Lige nu er der 2 klasser:
    • Sign- enum, som er ansvarlig for "krydset / nul / void" .
    • Fielder vores felt. Denne klasse har et "mark" -kort . Princippet for datalagring vil være som følger: cellerne i tic-tac-toe-feltet er nummereret fra nul. I den første linje 0, 1 og 2. I den anden: 3, 4 og 5. Og så videre. Der er også 3 metoder. "getEmptyFieldIndex" leder efter den første tomme celle (ja, vores modstander vil ikke være særlig smart). "checkWin" tjekker om spillet er slut. Hvis der er en række med tre kryds, returnerer det et kryds; hvis der er en række med tre nuller, returnerer det et nul. Ellers er den tom. "getFieldData" - returnerer værdierne af "felt" -kortet som en liste sorteret i stigende indeksrækkefølge.
  7. Forklaringerne om skabelonen er færdige, nu kan du starte opgaven. Lad os starte med at tegne en 3 gange 3 tabel. For at gøre dette skal du tilføje følgende kode til "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>
    Vi vil så fjerne tallene i tabellen og erstatte dem med et kryds, nul eller et tomt felt. Inkluder også stilfilen inde i "head"-tagget. For at gøre dette skal du tilføje en linje:<link href="static/main.css" rel="stylesheet">

    Indholdet af stilfilen er op til dig. Jeg brugte denne:
    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;
    }
    Efter løb ser mit resultat således ud:
  8. Lad os nu tilføje følgende funktionalitet: når der klikkes på en celle, vil en anmodning blive sendt til serveren, hvor vi sender indekset for den celle, der blev klikket på, som en parameter. Denne opgave kan opdeles i to dele: send en anmodning fra forsiden, accepter en anmodning på serveren. Lad os starte forfra for en forandring.

    Lad os tilføje en "onclick" -parameter til hvert "d" -tag . I værdien angiver vi ændringen af ​​den aktuelle side til den angivne URL. Den servlet, der vil være ansvarlig for logikken, vil have URL'en "/logic" . Og det vil tage en parameter kaldet "klik" . Så vi vil videregive indekset for den celle, som brugeren klikkede på.
    <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>
    Du kan kontrollere, at alt er udført korrekt gennem udviklerpanelet i browseren. For eksempel i Chrome åbnes den med F12 -knappen . Som et resultat af at klikke på en celle med indeks 4, bliver billedet som følger: Vi får en fejl, fordi vi endnu ikke har oprettet en servlet, der kan sende serveren til adressen "logic" .
  9. I pakken "com.tictactoe" skal du oprette en klasse "LogicServlet" , som skal være afledt af klassen "javax.servlet.http.HttpServlet" . I klassen skal du tilsidesætte "doGet" -metoden .

    Og lad os tilføje en metode, der får indekset for den celle, der blev klikket på. Du skal også tilføje en mapping (den adresse, hvor denne servlet vil opsnappe anmodningen). Jeg foreslår, at du gør dette gennem en annotering (men hvis du kan lide vanskeligheder, kan du også bruge web.xml). Generel servlet kode:
    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;
        }
    
    }
    Når vi nu klikker på en hvilken som helst celle, får vi indekset for denne celle på serveren (du kan sikre dig ved at køre serveren i debug). Og der vil være en omdirigering til den samme side, hvorfra klikket blev foretaget.
  10. Nu kan vi klikke, men det er ikke et spil endnu. For at spillet skal have logik, skal du gemme spillets tilstand (hvor krydsene er, hvor nullerne er) mellem anmodninger. Den nemmeste måde at gøre dette på er at gemme disse data i sessionen. Med denne tilgang vil sessionen blive gemt på serveren, og klienten vil modtage et sessions-id i en cookie med navnet "JSESSIONID" . Men sessionen skal ikke oprettes hver gang, men kun i begyndelsen af ​​spillet. Lad os starte en anden servlet til dette, som vi vil kalde "InitServlet" . Vi vil tilsidesætte "doGet" -metoden i den , hvor vi vil oprette en ny session, skabe en spilleplads, sætte denne spillebane og en liste over type Log ind i sessionsattributterne og sende " frem" til index.jsp side. Kode:
    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);
        }
    }
    Og ikke at forglemme, lad os ændre startsiden, der åbner i browseren efter start af serveren til "/start" : Nu efter genstart af serveren og klik på en hvilken som helst celle i feltet i browserens udviklermenu i sektionen "Request Headers" , vil der være en cookie med sessions-id'et:
  11. Når vi har et lager, hvori vi kan gemme tilstand mellem anmodninger fra klienten (browser), kan vi begynde at skrive spillogik. Den logik, vi har, er i "LogicServlet" . Vi skal arbejde med "doGet" -metoden . Lad os tilføje denne adfærd til metoden:
    • vi får "field" -objektet af Field-typen fra sessionen (vi vil tage det ud til "extractField" -metoden ).
    • sæt et kryds, hvor brugeren har klikket (indtil videre uden nogen kontrol).
    @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;
    }
    Adfærden har ikke ændret sig endnu, men hvis du starter serveren i debug og indstiller et brudpunkt på linjen, hvortil omdirigeringen sendes, kan du se "indmaden" af "data" -objektet . Der vises "CROSS" under det indeks, der blev klikket på.
  12. Nu er det tid til at vise krydset på frontenden. For at gøre dette vil vi arbejde med "index.jsp" -filen og "JSTL" -teknologien .
    • Tilføj i <head>-sektionen:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • I tabellen inde i hver <td>-blok skal du ændre indekset til en konstruktion, der giver dig mulighed for at beregne værdier. For eksempel, for indeks nul: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Nu, når du klikker på en celle, vil der vises et kryds der:
  13. Vi har lavet vores træk, nu er det turen til "nullen". Og lad os tilføje et par tjek her, så skiltene ikke placeres i allerede besatte celler.
    • Du skal kontrollere, at den celle, der blev klikket på, er tom. Ellers gør vi ingenting og sender brugeren til samme side uden at ændre sessionsparametrene.
    • Da antallet af celler på feltet er ulige, er det muligt, at der er sat et kryds, men der er ikke plads til et nul. Derfor, efter at vi har sat et kryds, forsøger vi at få indekset for en ledig celle (getEmptyFieldIndex-metoden i klassen Field). Hvis indekset ikke er negativt, så sæt et nul der. Kode:
      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. På dette stadie kan du sætte kryds, svarer AI med nuller. Men der er ingen kontrol, hvornår spillet skal stoppes. Dette kan være i tre tilfælde:
    • efter næste træk af korset dannedes en linje med tre kryds;
    • efter det næste returtræk med et nul, blev der dannet en linje med tre nuller;
    • efter næste træk af korset, sluttede tomme celler.
    Lad os tilføje en metode, der kontrollerer, om der er tre kryds/nuller i en række:
    /**
     * 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;
    }
    Det særlige ved denne metode er, at hvis vinderen er fundet, tilføjer vi en anden parameter til sessionen, hvorved vi vil ændre visningen i "index.jsp" i de følgende afsnit.
  15. Lad os tilføje et kald til "checkWin "-metoden to gange til "doGet"-metoden . Første gang efter at have sat krydset, den anden - efter at have sat nul.
    // 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. Med hensyn til adfærd har næsten intet ændret sig (bortset fra at hvis et af tegnene vinder, placeres nuller ikke længere. Lad os bruge parameteren "vinder" i "index.jsp" og vise vinderen. Vi bruger direktiver efter tabellen: 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>
    Hvis kryds vinder, meddelelsen "KRYDS VIND!" , hvis nullerne er "NOUGHTS WIN!" . Som et resultat kan vi få en af ​​to inskriptioner:
  17. Hvis der er en vinder, skal du være i stand til at tage revanche. For at gøre dette skal du bruge en knap, der sender en anmodning til serveren. Og serveren vil ugyldiggøre den aktuelle session og omdirigere anmodningen tilbage til "/start" .
    • I "index.jsp" i "head" -sektionen skal du skrive scriptet "jquery" . Ved at bruge dette bibliotek sender vi en anmodning til serveren.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • I "index.jsp" i "script" -sektionen skal du tilføje en funktion, der kan sende en POST-anmodning til serveren. Vi vil gøre funktionen synkron, og når der kommer et svar fra serveren, genindlæser den den aktuelle side.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Inde i "c:if" -blokkene skal du tilføje en knap, der, når den klikkes, kalder den funktion, vi lige skrev:
      <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>
    • Lad os oprette en ny servlet, der vil tjene "/genstart" URL'en .
      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");
          }
      }
      Efter sejren vises knappen "Start igen" . Når du har klikket på det, vil feltet være helt ryddet, og spillet starter forfra.
  18. Det er tilbage at overveje den sidste situation. Hvad hvis brugeren satte et kryds, var der ingen sejr, og der er ikke plads til et nul? Så er dette en lodtrækning, og vi behandler den nu:
    • I "LogicServlet" -sessionen skal du tilføje en anden parameter "draw" , opdatere "data" -feltet og sende en omdirigering til "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;
      }
    • I "index.jsp" behandler vi denne parameter:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Som et resultat af en lodtrækning modtager vi den tilsvarende besked og et tilbud om at starte forfra:

Dette fuldender skrivningen af ​​spillet.

Kode for klasser og filer, som de arbejdede med

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);
    }
}

LogicServlet

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;
    }
}

GenstartServlet

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;
   }