Ma egy Tic-Tac-Toe játékot írunk servletekkel és JSP-vel.

Ez a projekt kicsit más lesz, mint a korábbiak. Nemcsak feladatokat, hanem azok elvégzésének magyarázatát is tartalmazza. Azaz egy projekt lesz a "HOGYAN..." sorozatból.

Utasítás:

  1. Fork az adattárból: https://github.com/CodeGymCC/project-servlet.git
  2. Töltse le a projekt verzióját a számítógépére.
  3. Alkalmazásindítás beállítása az IDEA-ban:
    • Alt + Shift + F9 -> Konfigurációk szerkesztése… -> Alt + insert -> tom (a keresősávba) -> Local.
    • Ezt követően kattintson a „KONFIGURÁLÁS” gombra, és jelezze, hogy hol töltötte le és bontotta ki a Tomcat archívumát.
    • A „Bevezetés” lapon: Alt + insert -> Artifact… -> tic-tac-toe:war exploded -> OK.
    • Az „Alkalmazáskörnyezet” mezőben: hagyja csak a „/” jelet (perjel).
    • Nyomja meg az "ALKALMAZ" gombot.
    • Zárja be a beállítások ablakot.
    • Végezze el a testreszabott konfiguráció első tesztfutását. Ha minden megfelelően megtörtént, megnyílik az alapértelmezett böngésző, amelyben ez lesz:
  4. Nyissa meg a „pom.xml” fájlt . A „függőségek” blokkban 2 függőség található .
    • javax.servlet-apifelelős a szervletek specifikációjáért. A "feltéve" hatókörre szükség van a fejlesztés során, de nem szükséges futás közben (a Tomcat lib mappájában ez a függőség már megtalálható).
    • jstl– sablonmotornak tekinthető.
  5. 3 fájl van a „webapp” mappában :
    • index.jsp- ez a sablonunk (hasonlóan a HTML oldalhoz). Jelölést és szkripteket fog tartalmazni. Az „index” nevű fájlt adjuk meg kezdőoldalként, ha nincsenek konfigurációk, amit a 3. lépésben láttunk.
    • /static/main.css- fájl stílusokhoz. Az előző projekthez hasonlóan itt is minden Önön múlik, festse, ahogy akarja.
    • /static/jquery-3.6.0.min.js- előtér-függőség, amelyet a szerverünk statikusként terjeszt.
  6. A "com.tictactoe" csomag tartalmazza az összes Java kódot. Jelenleg 2 osztály van:
    • Sign- enum, amely a "cross / zero / void" ért felelős .
    • Fielda mi területünk. Ennek az osztálynak van "mező" térképe . Az adattárolás elve a következő lesz: a tic-tac-toe mező celláit nullától kezdve számozzuk. Az első sorban 0, 1 és 2. A másodikban: 3, 4 és 5. És így tovább. 3 módszer is létezik. A „getEmptyFieldIndex” az első üres cellát keresi (igen, ellenfelünk nem lesz túl okos). A "checkWin" ellenőrzi, hogy a játéknak vége-e. Ha van egy sorban három kereszt, akkor egy keresztet ad vissza, ha van egy három nullából álló sor, akkor nullát ad vissza. Ellenkező esetben üres. "getFieldData" - a "mező" térkép értékeit növekvő index szerinti listaként adja vissza.
  7. A sablonnal kapcsolatos magyarázatok befejeződtek, most már indulhat a feladat. Kezdjük egy 3:3 táblázat megrajzolásával. Ehhez adja hozzá a következő kódot az „index.jsp” fájlhoz :
    <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>
    Ezután eltávolítjuk a számokat a táblázatból, és helyettesítjük őket kereszttel, nullával vagy üres mezővel. Ezenkívül a „head” címkén belül helyezze el a stílusfájlt. Ehhez adjon hozzá egy sort:<link href="static/main.css" rel="stylesheet">

    A stílusfájl tartalma Önön múlik. Én ezt használtam:
    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;
    }
    Futás után így néz ki az eredményem:
  8. Most adjuk hozzá a következő funkciót: ha egy cellára kattintunk, akkor egy kérés érkezik a szerverhez, amelyben paraméterként adjuk át a kattintott cella indexét. Ez a feladat két részre osztható: kérés küldése elölről, kérés elfogadása a szerveren. A változatosság kedvéért kezdjük elölről.

    Adjunk hozzá egy "onclick" paramétert minden "d" címkéhez . Az értékben az aktuális oldal módosítását jelezzük a megadott URL-re. A logikáért felelős szervlet URL-je „/logic” lesz . És ehhez egy „click” nevű paraméter kell . Így átadjuk annak a cellának az indexét, amelyre a felhasználó rákattintott.
    <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>
    A böngésző fejlesztői paneljén ellenőrizheti, hogy minden megfelelően történt-e. Például a Chrome-ban az F12 gombbal nyílik meg . A 4-es indexű cellára való kattintás eredményeként a következőképpen alakul a kép: Hibaüzenetet kapunk, mert még nem készítettünk olyan servletet, amely a „logic” címre tudja küldeni a szervert .
  9. A "com.tictactoe" csomagban hozzon létre egy "LogicServlet" osztályt, amelynek a "javax.servlet.http.HttpServlet" osztályból kell származnia . Az osztályban felülbírálja a „doGet” metódust .

    És adjunk hozzá egy metódust, amely megkapja a kattintott cella indexét. Ezenkívül hozzá kell adnia egy leképezést (az a cím, amelyen ez a szervlet elfogja a kérést). Azt javaslom, hogy ezt annotáción keresztül tegye (de ha szereti a nehézségeket, használhatja a web.xml-t is). Általános szervlet kód:
    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;
        }
    
    }
    Most, ha bármelyik cellára kattintunk, ennek a cellának az indexét kapjuk meg a kiszolgálón (megbizonyosodhat arról, ha a szervert debug-ban futtatja). És lesz egy átirányítás ugyanarra az oldalra, ahonnan a kattintás történt.
  10. Most már kattinthatunk is, de ez még nem játék. Ahhoz, hogy a játéknak legyen logikája, el kell mentenie a játék állapotát (hol vannak a keresztek, hol a nullák) a kérések között. Ennek legegyszerűbb módja, ha ezeket az adatokat a munkamenetben tárolja. Ezzel a megközelítéssel a munkamenet a szerveren tárolódik, és az ügyfél egy munkamenet-azonosítót kap egy „JSESSIONID” nevű cookie-ban . De a munkamenetet nem kell minden alkalommal létrehozni, hanem csak a játék elején. Ehhez indítsunk el egy másik servletet, amit "InitServlet" -nek nevezünk . Felülírjuk benne a „doGet” metódust , amelyben új munkamenetet hozunk létre, játékteret hozunk létre, ezt a játékteret és egy Sign típusú listát helyezünk el a session attribútumok között, és elküldjük a „ forward”-ot az index.jsp fájlba. oldalon. Kód:
    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);
        }
    }
    És ne felejtsük el, változtassuk meg a böngészőben a szerver elindítása után megnyíló kezdőoldalt „/start” -ra : Most, miután újraindította a szervert, és a „Fejlécek kérése” részben a böngésző fejlesztői menüjében a mező bármelyik cellájára kattintott. , lesz egy cookie a következő munkamenet-azonosítóval:
  11. Ha van egy repository, amelyben a klienstől (böngészőtől) érkező kérések közötti állapotokat tárolhatjuk, akkor elkezdhetjük a játék logikáját írni. A logikánk a „LogicServlet” -ben található . A „doGet” módszerrel kell dolgoznunk . Adjuk hozzá ezt a viselkedést a metódushoz:
    • a munkamenetből megkapjuk a Field típusú „field” objektumot (az „extractField” metódusba visszük ki ).
    • tegyen egy keresztet oda, ahol a felhasználó rákattintott (eddig minden ellenőrzés nélkül).
    @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;
    }
    A viselkedés még nem változott, de ha elindítja a szervert a hibakeresésben, és beállít egy töréspontot azon a sorban, ahová az átirányítást küldi, akkor láthatja az „adat” objektum „belsejét . Ott valóban megjelenik a „KERESZT” az index alatt, amelyre kattintottak.
  12. Most itt az ideje, hogy megjelenítse a keresztet az előlapon. Ehhez az „index.jsp” fájllal és a „JSTL” technológiával fogunk dolgozni .
    • A <head> szakaszban adja hozzá:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Az egyes <td> blokkon belüli táblázatban módosítsa az indexet olyan konstrukcióra, amely lehetővé teszi az értékek kiszámítását. Például a nulla indexhez: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Ha most egy cellára kattint, egy kereszt jelenik meg ott:
  13. Megtettük a lépést, most a "nullakon" a sor. És tegyünk még ide egy-két csekket, hogy ne a már foglalt cellákba kerüljenek a táblák.
    • Ellenőriznie kell, hogy a kattintott cella üres-e. Ellenkező esetben nem teszünk semmit, és ugyanarra az oldalra küldjük a felhasználót anélkül, hogy megváltoztatnánk a munkamenet paramétereit.
    • Mivel a mezőn lévő cellák száma páratlan, lehetséges, hogy keresztet tettek, de nincs hely nullának. Ezért, miután keresztet tettünk, megpróbáljuk lekérni egy üres cella indexét (a Field osztály getEmptyFieldIndex metódusa). Ha az index nem negatív, akkor tegyen nullát. Kód:
      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. Ebben a szakaszban kereszteket tehet, az AI nullákkal válaszol. De nincs ellenőrzés, mikor kell leállítani a játékot. Ez három esetben történhet:
    • a következő keresztmozgás után három keresztből álló vonal alakult ki;
    • a következő nullával való visszalépés után egy három nullából álló vonal jött létre;
    • a kereszt következő mozdulata után az üres cellák véget értek.
    Adjunk hozzá egy módszert, amely ellenőrzi, hogy van-e három kereszt / nulla egy sorban:
    /**
     * 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;
    }
    Ennek a módszernek az a sajátossága, hogy a nyertes megtalálása esetén egy újabb paramétert adunk a munkamenethez, amivel a következő bekezdésekben megváltoztatjuk az „index.jsp” -ben megjelenő megjelenítést.
  15. Adjuk hozzá kétszer a „checkWin ” metódus hívását a „doGet” metódushoz . Az első alkalommal a kereszt beállítása után, a második - a nulla beállítása után.
    // 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. A viselkedést tekintve szinte semmi nem változott (kivéve, hogy ha valamelyik előjel nyer, akkor már nem kerülnek nullák. Használjuk az „index.jsp” -ben a „winner” paramétert, és jelenítsük meg a nyertest. A táblázat c:setután direktívákat használunk :c: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>
    Ha a keresztek nyernek, akkor a „KERESZTEK NYEREM” üzenet jelenik meg. , ha a nullák a „NOUGHTS WIN!” . Ennek eredményeként két felirat egyikét kaphatjuk:
  17. Ha van győztes, tudnod kell bosszút állni. Ehhez szüksége van egy gombra, amely kérést küld a szervernek. A szerver érvényteleníti az aktuális munkamenetet, és visszairányítja a kérést a „/start” címre .
    • Az „index.jsp” fájl „head” részébe írja be a „jquery” szkriptet . Ennek a könyvtárnak a használatával kérést küldünk a szervernek.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Az „index.jsp” fájl „script” szakaszában adjon hozzá egy függvényt, amely POST kérést küldhet a szervernek. A függvényt szinkronizáljuk, és amikor válasz érkezik a szervertől, újratölti az aktuális oldalt.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • A „c:if” blokkon belül adjon hozzá egy gombot, amelyre kattintva meghívja az imént írt függvényt:
      <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>
    • Hozzon létre egy új szervletet, amely a „/restart” URL-t fogja kiszolgálni .
      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");
          }
      }
      A győzelem után megjelenik az „Újrakezdés” gomb . A rákattintás után a mező teljesen kiürül, és a játék elölről indul.
  18. Már csak az utolsó helyzetet kell figyelembe venni. Mi van, ha a felhasználó keresztet tesz, nincs győzelem, és nincs helye nullának? Akkor ez a sorsolás, és most feldolgozzuk:
    • A "LogicServlet" munkamenetben adjon hozzá egy másik "draw" paramétert , frissítse a "data" mezőt , és küldjön átirányítást az "index.jsp" címre :
      // 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;
      }
    • Az "index.jsp" fájlban ezt a paramétert dolgozzuk fel:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      A sorsolás eredményeként megkapjuk a megfelelő üzenetet és egy ajánlatot az újrakezdésre:

Ezzel befejeződik a játék megírása.

Azon osztályok és fájlok kódja, amelyekkel dolgoztak

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

RestartServlet

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