Ngayon ay magsusulat kami ng larong Tic-Tac-Toe gamit ang mga servlet at JSP.

Ang proyektong ito ay magiging kakaiba ng kaunti sa mga nauna. Maglalaman ito ng hindi lamang mga gawain, kundi pati na rin ang mga paliwanag kung paano gawin ang mga ito. Ibig sabihin, ito ay magiging isang proyekto mula sa seryeng "HOW TO ...".

Tagubilin:

  1. Fork mula sa repository: https://github.com/CodeGymCC/project-servlet.git
  2. I-download ang iyong bersyon ng proyekto sa iyong computer.
  3. I-set up ang paglulunsad ng application sa IDEA:
    • Alt + Shift + F9 -> Edit Configurations... -> Alt + insert -> tom (sa search bar) -> Lokal.
    • Pagkatapos nito, kailangan mong i-click ang "CONFIGURE" at ipahiwatig kung saan na-download at na-unpack ang archive na may Tomcat.
    • Sa tab na “Deployment”: Alt + insert -> Artifact... -> tic-tac-toe:war exploded -> OK.
    • Sa field na "Konteksto ng application": iwan lang ang "/" (slash).
    • Pindutin ang "APPLY".
    • Isara ang window ng mga setting.
    • Gawin ang unang test run ng customized na configuration. Kung nagawa nang tama ang lahat, magbubukas ang iyong default na browser, kung saan ito ay magiging:
  4. Buksan ang "pom.xml" file . Mayroong 2 dependencies sa "dependencies" block .
    • javax.servlet-apiay responsable para sa pagtutukoy ng mga servlet. Ang saklaw na "ibinigay" ay kinakailangan sa panahon ng pag-unlad, ngunit hindi kinakailangan sa runtime (Tomcat ay mayroon nang dependency na ito sa lib folder).
    • jstl– maaaring ituring bilang isang template engine.
  5. Mayroong 3 file sa folder na "webapp" :
    • index.jsp- ito ang aming template (katulad ng HTML page). Maglalaman ito ng markup at mga script. Ito ang file na tinatawag na "index" na ibinibigay bilang paunang pahina, kung walang mga pagsasaayos, na nakita namin sa hakbang 3.
    • /static/main.css- file para sa mga estilo. Tulad ng sa nakaraang proyekto, ang lahat dito ay nasa iyo, pintura ayon sa gusto mo.
    • /static/jquery-3.6.0.min.js- frontend dependency na ipapamahagi ng aming server bilang static.
  6. Ang package na "com.tictactoe" ay maglalaman ng lahat ng Java code. Sa ngayon, mayroong 2 klase:
    • Sign- enum, na responsable para sa "cross / zero / void" .
    • Fielday ang aming larangan. Ang klase na ito ay may "field" na mapa . Ang prinsipyo ng pag-iimbak ng data ay ang mga sumusunod: ang mga cell ng field ng tic-tac-toe ay binibilang mula sa zero. Sa unang linya 0, 1 at 2. Sa pangalawa: 3, 4 at 5. At iba pa. Mayroon ding 3 pamamaraan. Hinahanap ng “getEmptyFieldIndex” ang unang walang laman na cell (oo, hindi magiging matalino ang ating kalaban). Sinusuri ng "checkWin" kung tapos na ang laro. Kung mayroong isang hilera ng tatlong mga krus, ito ay nagbabalik ng isang krus; kung mayroong isang hilera ng tatlong mga zero, ito ay nagbabalik ng isang zero. Kung hindi, ito ay walang laman. "getFieldData" - ibinabalik ang mga halaga ng mapa ng "field" bilang isang listahan na pinagsunod-sunod sa pataas na pagkakasunud-sunod ng index.
  7. Ang mga paliwanag tungkol sa template ay tapos na, maaari mo na ngayong simulan ang gawain. Magsimula tayo sa pagguhit ng 3 by 3 table. Upang gawin ito, idagdag ang sumusunod na code sa “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>
    Pagkatapos ay aalisin namin ang mga numero sa talahanayan at papalitan ang mga ito ng isang krus, zero o isang walang laman na field. Gayundin, sa loob ng tag na "head", isama ang style file. Upang gawin ito, magdagdag ng isang linya:<link href="static/main.css" rel="stylesheet">

    Nasa iyo ang nilalaman ng style file. Ginamit ko ang isang ito:
    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;
    }
    Pagkatapos tumakbo, ang aking resulta ay ganito:
  8. Ngayon, idagdag natin ang sumusunod na pag-andar: kapag na-click ang isang cell, isang kahilingan ang ipapadala sa server, kung saan ipapasa natin ang index ng cell na na-click bilang isang parameter. Ang gawaing ito ay maaaring nahahati sa dalawang bahagi: magpadala ng kahilingan mula sa harap, tumanggap ng kahilingan sa server. Magsimula tayo sa harapan para sa pagbabago.

    Magdagdag tayo ng parameter na "onclick" sa bawat tag na "d" . Sa halaga, ipinapahiwatig namin ang pagbabago ng kasalukuyang pahina sa tinukoy na URL. Ang servlet na magiging responsable para sa logic ay magkakaroon ng URL na “/logic” . At kukuha ito ng parameter na tinatawag na “click” . Kaya ipapasa namin ang index ng cell na na-click ng user.
    <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>
    Maaari mong suriin kung ang lahat ay ginawa nang tama sa pamamagitan ng panel ng developer sa browser. Halimbawa, sa Chrome, bubukas ito gamit ang F12 button . Bilang resulta ng pag-click sa isang cell na may index 4, ang larawan ay magiging ganito: Nakakakuha kami ng isang error dahil hindi pa kami nakakagawa ng isang servlet na maaaring magpadala ng server sa address na "logic" .
  9. Sa package na "com.tictactoe" lumikha ng isang klase na "LogicServlet" na dapat ay hango sa klase na "javax.servlet.http.HttpServlet" . Sa klase, i-override ang "doGet" na paraan .

    At magdagdag tayo ng paraan na makakakuha ng index ng cell na na-click. Kailangan mo ring magdagdag ng pagmamapa (ang address kung saan haharangin ng servlet na ito ang kahilingan). Iminumungkahi kong gawin ito sa pamamagitan ng isang anotasyon (ngunit kung gusto mo ng mga paghihirap, maaari mo ring gamitin ang web.xml). Pangkalahatang servlet code:
    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;
        }
    
    }
    Ngayon, kapag nag-click sa anumang cell, makukuha namin ang index ng cell na ito sa server (makatitiyak ka sa pamamagitan ng pagpapatakbo ng server sa debug). At magkakaroon ng pag-redirect sa parehong pahina kung saan ginawa ang pag-click.
  10. Ngayon ay maaari na tayong mag-click, ngunit hindi pa ito laro. Upang magkaroon ng lohika ang laro, kailangan mong i-save ang estado ng laro (kung nasaan ang mga krus, kung nasaan ang mga zero) sa pagitan ng mga kahilingan. Ang pinakamadaling paraan upang gawin ito ay ang pag-imbak ng data na ito sa session. Sa diskarteng ito, maiimbak ang session sa server, at makakatanggap ang kliyente ng session ID sa isang cookie na pinangalanang “JSESSIONID” . Ngunit ang session ay hindi kailangang gawin sa bawat oras, ngunit sa simula lamang ng laro. Magsimula tayo ng isa pang servlet para dito, na tatawagin nating "InitServlet" . I-override namin ang "doGet" na paraan dito , kung saan gagawa kami ng bagong session, gagawa ng playing field, ilagay ang playing field na ito at isang listahan ng uri Mag-sign in sa mga attribute ng session, at ipapadala ang " forward" sa index.jsp pahina. Code:
    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);
        }
    }
    At huwag kalimutan, baguhin natin ang panimulang pahina na bubukas sa browser pagkatapos simulan ang server sa "/start" : Ngayon pagkatapos i-restart ang server at mag-click sa anumang cell ng field sa menu ng developer ng browser sa seksyong "Request Header" , magkakaroon ng cookie na may session ID :
  11. Kapag mayroon kaming repositoryo kung saan maaari kaming mag-imbak ng estado sa pagitan ng mga kahilingan mula sa kliyente (browser), maaari naming simulan ang pagsusulat ng logic ng laro. Ang logic na mayroon kami ay nasa "LogicServlet" . Kailangan nating magtrabaho kasama ang "doGet" na paraan . Idagdag natin ang pag-uugaling ito sa pamamaraan:
    • makukuha natin ang object na "field" ng uri ng Field mula sa session (ilalabas natin ito sa pamamaraang "extractField" ).
    • maglagay ng krus kung saan nag-click ang user (sa ngayon ay walang anumang mga tseke).
    @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;
    }
    Hindi pa nagbabago ang pag-uugali, ngunit kung sisimulan mo ang server sa pag-debug at magtatakda ng breakpoint sa linya kung saan ipinadala ang pag-redirect, makikita mo ang "innards" ng object na "data " . Doon, sa katunayan, ang "CROSS" ay lilitaw sa ilalim ng index na na-click.
  12. Ngayon ay oras na upang ipakita ang krus sa frontend. Upang gawin ito, gagana kami sa "index.jsp" file at sa "JSTL" na teknolohiya .
    • Sa seksyong <head> idagdag ang:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Sa talahanayan sa loob ng bawat <td> block, baguhin ang index sa isang construct na nagbibigay-daan sa iyong kalkulahin ang mga halaga. Halimbawa, para sa index zero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Ngayon, kapag nag-click ka sa isang cell, may lalabas na krus doon:
  13. Nakagawa na kami ng aming hakbang, ngayon na ang "zero". At magdagdag tayo ng ilang mga tseke dito, upang ang mga palatandaan ay hindi mailagay sa mga na-occupy na cell.
    • Kailangan mong suriin na ang cell na na-click ay walang laman. Kung hindi, wala kaming gagawin at ipapadala ang user sa parehong page nang hindi binabago ang mga parameter ng session.
    • Dahil kakaiba ang bilang ng mga cell sa field, posibleng may inilagay na krus, ngunit walang puwang para sa zero. Samakatuwid, pagkatapos naming maglagay ng krus, sinusubukan naming makuha ang index ng isang walang tao na cell (ang getEmptyFieldIndex na paraan ng klase ng Field). Kung ang index ay hindi negatibo, pagkatapos ay maglagay ng zero doon. Code:
      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. Sa yugtong ito, maaari kang maglagay ng mga krus, mga sagot ng AI na may mga zero. Ngunit walang check kung kailan ititigil ang laro. Ito ay maaaring sa tatlong kaso:
    • pagkatapos ng susunod na paglipat ng krus, isang linya ng tatlong krus ay nabuo;
    • pagkatapos ng susunod na return move na may zero, nabuo ang isang linya ng tatlong zero;
    • pagkatapos ng susunod na paglipat ng krus, ang mga walang laman na mga cell ay natapos.
    Magdagdag tayo ng isang paraan na nagsusuri kung mayroong tatlong mga krus / zero sa isang hilera:
    /**
     * 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;
    }
    Ang kakaiba ng pamamaraang ito ay kung ang nagwagi ay natagpuan, nagdaragdag kami ng isa pang parameter sa session, gamit kung saan babaguhin namin ang display sa "index.jsp" sa mga sumusunod na talata.
  15. Magdagdag tayo ng tawag sa pamamaraang “checkWin ” nang dalawang beses sa pamamaraang “doGet” . Ang unang pagkakataon pagkatapos itakda ang krus, ang pangalawa - pagkatapos itakda ang zero.
    // 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. Sa mga tuntunin ng pag-uugali, halos walang nagbago (maliban na kung manalo ang isa sa mga palatandaan, hindi na ilalagay ang mga zero. Gamitin natin ang parameter na “winner” sa “index.jsp” at ipakita ang nanalo. Gumagamit kami ng mga direktiba pagkatapos ng talahanayan: 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>
    Kung mananalo ang mga cross, ang mensahe ay "CROSSES WIN!" , kung ang mga zero ay “NOUGHTS WIN!” . Bilang resulta, makakakuha tayo ng isa sa dalawang inskripsiyon:
  17. Kung may nanalo, kailangan marunong kang maghiganti. Upang gawin ito, kailangan mo ng isang pindutan na magpapadala ng isang kahilingan sa server. At ang server ay magpapawalang-bisa sa kasalukuyang session at ire-redirect ang kahilingan pabalik sa “/start” .
    • Sa “index.jsp” sa seksyong “head” , isulat ang script na “jquery” . Gamit ang library na ito, magpapadala kami ng kahilingan sa server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Sa “index.jsp” sa seksyong “script ,” magdagdag ng function na maaaring magpadala ng POST request sa server. Gagawin naming magkasabay ang function, at kapag ang isang tugon ay nagmula sa server, ire-reload nito ang kasalukuyang pahina.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Sa loob ng “c:if” blocks , magdagdag ng button na, kapag na-click, ay tumatawag sa function na isinulat namin:
      <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>
    • Gumawa tayo ng bagong servlet na maghahatid ng "/restart" URL .
      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");
          }
      }
      Pagkatapos ng tagumpay, lilitaw ang "Start again" button . Pagkatapos ng pag-click dito, ang field ay ganap na malilinis, at ang laro ay magsisimulang muli.
  18. Ito ay nananatiling isaalang-alang ang huling sitwasyon. Paano kung ang gumagamit ay naglagay ng krus, walang tagumpay, at walang lugar para sa isang zero? Pagkatapos ito ay isang draw, at ipoproseso namin ito ngayon:
    • Sa session na "LogicServlet ," magdagdag ng isa pang parameter na "draw" , i-update ang field na "data" at magpadala ng redirect sa "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;
      }
    • Sa "index.jsp" ipoproseso namin ang parameter na ito:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Bilang resulta ng isang draw, matatanggap namin ang kaukulang mensahe at isang alok upang magsimulang muli:

Kinukumpleto nito ang pagsulat ng laro.

Code ng mga klase at file kung saan sila nagtrabaho

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

I-restart angServlet

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