Dina iki kita bakal nulis game Tic-Tac-Toe nggunakake servlets lan JSP.

Proyek iki bakal rada beda karo sing sadurunge. Bakal ngemot ora mung tugas, nanging uga panjelasan babagan carane nindakake. Yaiku, bakal dadi proyek saka seri "CARA ...".

instruksi:

  1. Garpu saka repositori: https://github.com/CodeGymCC/project-servlet.git
  2. Download versi proyek menyang komputer.
  3. Setel peluncuran aplikasi ing IDEA:
    • Alt + Shift + F9 -> Edit Konfigurasi… -> Alt + insert -> tom (menyang bar telusuran) -> Lokal.
    • Sawisé iku, sampeyan kudu ngeklik "CONFIGURE" lan nuduhake ing ngendi arsip karo Tomcat diundhuh lan dibongkar.
    • Ing tab "Panyebaran": Alt + insert -> Artefak... -> tic-tac-toe:war mbledhos -> OK.
    • Ing lapangan "Konteks aplikasi": mung ninggalake "/" (slash).
    • Tekan "APPLY".
    • Nutup jendhela setelan.
    • Nggawe test run pisanan saka konfigurasi selaras. Yen kabeh wis rampung kanthi bener, browser gawan sampeyan bakal mbukak, yaiku:
  4. Bukak file "pom.xml" . Ana 2 dependensi ing blok "dependensi" .
    • javax.servlet-apitanggung jawab kanggo specification saka servlets. Lingkup "kasedhiya" dibutuhake sajrone pembangunan, nanging ora dibutuhake nalika runtime (Tomcat wis duwe ketergantungan iki ing folder lib).
    • jstl- bisa dianggep minangka mesin cithakan.
  5. Ana 3 file ing folder "webapp" :
    • index.jsp- iki cithakan kita (padha karo kaca HTML). Bakal ngemot markup lan skrip. Iki minangka file sing diarani "indeks" sing diwenehake minangka kaca wiwitan, yen ora ana konfigurasi, sing kita deleng ing langkah 3.
    • /static/main.css- file kanggo gaya. Kaya ing proyek sadurunge, kabeh ing kene terserah sampeyan, cat kaya sing dikarepake.
    • /static/jquery-3.6.0.min.js- dependensi frontend sing bakal disebarake server kita minangka statis.
  6. Paket "com.tictactoe" bakal ngemot kabeh kode Jawa. Saiki ana 2 kelas:
    • Sign- enum, kang tanggung jawab kanggo "salib / nul / roso sepi" .
    • Fieldiku lapangan kita. Kelas iki duwe peta "lapangan" . Prinsip panyimpenan data bakal kaya ing ngisor iki: sel kolom tic-tac-toe dinomer saka nol. Ing baris pisanan 0, 1 lan 2. Ing kaloro: 3, 4 lan 5. Lan sateruse. Ana uga 3 cara. "getEmptyFieldIndex" nggoleki sel kosong pisanan (ya, mungsuh kita ora bakal pinter banget). "checkWin" mriksa yen game wis rampung. Yen ana baris saka telung salib, iku ngasilake salib; yen ana baris saka telung nol, iku ngasilake nol. Yen ora, iku kosong. "getFieldData" - ngasilake nilai peta "lapangan" minangka dhaptar sing diurutake kanthi urutan indeks munggah.
  7. Panjelasan babagan cithakan wis rampung, saiki sampeyan bisa miwiti tugas. Ayo diwiwiti kanthi nggambar tabel 3 nganti 3. Kanggo nindakake iki, tambahake kode ing ngisor iki menyang "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>
    Kita banjur bakal mbusak nomer ing meja lan ngganti karo salib, nul utawa kolom kosong. Uga, ing jero tag "kepala", kalebu file gaya. Kanggo nindakake iki, nambah baris:<link href="static/main.css" rel="stylesheet">

    Isi file gaya terserah sampeyan. Aku nggunakake iki:
    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;
    }
    Sawise mlaku, asilku katon kaya iki:
  8. Saiki ayo nambah fungsi ing ngisor iki: nalika sel diklik, panjaluk bakal dikirim menyang server, ing ngendi kita bakal ngliwati indeks sel sing diklik minangka parameter. Tugas iki bisa dipérang dadi rong bagéan: ngirim panjalukan saka ngarep, nampa panjalukan ing server. Ayo dadi miwiti ing ngarep kanggo owah-owahan.

    Ayo tambahake parameter "onclick" ing saben tag "d" . Ing nilai kasebut, kita nuduhake owah-owahan kaca saiki menyang URL sing ditemtokake. Servlet sing bakal tanggung jawab kanggo logika bakal duwe URL "/logic" . Lan bakal njupuk parameter sing disebut "klik" . Dadi, kita bakal ngliwati indeks sel sing diklik pangguna.
    <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>
    Sampeyan bisa mriksa manawa kabeh wis rampung kanthi bener liwat panel pangembang ing browser. Contone, ing Chrome, mbukak nganggo tombol F12 . Minangka asil ngeklik sel kanthi indeks 4, gambar kasebut bakal kaya mangkene: Kita entuk kesalahan amarga durung nggawe servlet sing bisa ngirim server menyang alamat "logika" .
  9. Ing paket "com.tictactoe" nggawe kelas "LogicServlet" kang kudu asalé saka kelas "javax.servlet.http.HttpServlet" . Ing kelas, ganti metode "doGet" .

    Lan ayo nambah metode sing bakal entuk indeks sel sing diklik. Sampeyan uga kudu nambah pemetaan (alamat ing ngendi servlet iki bakal nyegat panjalukan). Aku suggest nindakake iki liwat anotasi (nanging yen sampeyan seneng kangelan, sampeyan uga bisa nggunakake web.xml). Kode servlet umum:
    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;
        }
    
    }
    Saiki, nalika ngeklik sel apa wae, kita bakal entuk indeks sel iki ing server (sampeyan bisa nggawe manawa kanthi mbukak server ing debug). Lan bakal ana pangalihan menyang kaca sing padha saka ngendi klik digawe.
  10. Saiki kita bisa ngeklik, nanging iki dudu game. Supaya game duwe logika, sampeyan kudu nyimpen negara game (ngendi salib, ngendi nul) antarane panjalukan. Cara paling gampang kanggo nindakake iki yaiku nyimpen data kasebut ing sesi kasebut. Kanthi pendekatan iki, sesi bakal disimpen ing server, lan klien bakal nampa ID sesi ing cookie jenenge "JSESSIONID" . Nanging sesi ora perlu digawe saben wektu, nanging mung ing awal game. Ayo dadi miwiti servlet liyane kanggo iki, sing bakal kita sebut "InitServlet" . Kita bakal ngilangi cara "doGet" ing kono , ing ngendi kita bakal nggawe sesi anyar, nggawe lapangan dolanan, sijine lapangan dolanan iki lan dhaptar jinis Mlebu atribut sesi, lan ngirim "maju" menyang index.jsp kaca. 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);
        }
    }
    Lan aja lali, ayo ngganti kaca wiwitan sing mbukak ing browser sawise miwiti server dadi "/start" : Saiki sawise miwiti maneh server lan ngeklik ing sembarang sel lapangan ing menu pangembang browser ing bagean "Request Headers" , bakal ana cookie karo ID sesi:
  11. Nalika kita duwe gudang ing ngendi kita bisa nyimpen negara antarane panjalukan saka klien (browser), kita bisa miwiti nulis logika game. Logika sing ana ing "LogicServlet" . Kita kudu nggarap metode "doGet" . Ayo nambahake prilaku iki menyang metode:
    • kita bakal entuk obyek "lapangan" saka jinis Field saka sesi (kita bakal njupuk menyang cara "extractField" ).
    • sijine salib ngendi pangguna ngeklik (dadi adoh tanpa mriksa).
    @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;
    }
    Prilaku durung diganti, nanging yen sampeyan miwiti server ing debug lan nyetel breakpoint ing baris ngendi pangalihan dikirim, sampeyan bisa ndeleng "innards" saka obyek "data " . Ing kono, "CROSS" katon ing indeks sing diklik.
  12. Saiki wektune kanggo nampilake salib ing ngarep. Kanggo nindakake iki, kita bakal nggarap file "index.jsp" lan teknologi "JSTL" .
    • Ing bagean <head> tambahake:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Ing tabel ing saben blok <td>, ganti indeks dadi konstruksi sing ngidini sampeyan ngetung nilai. Contone, kanggo indeks nol: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Saiki, nalika sampeyan ngeklik sel, salib bakal katon ing kono:
  13. Kita wis nggawe pamindhahan, saiki giliran "nol". Lan ayo ditambahake sawetara cek ing kene, supaya tandha-tandha ora diselehake ing sel sing wis dikuwasani.
    • Sampeyan kudu mriksa yen sel sing diklik kosong. Yen ora, kita ora nindakake apa-apa lan ngirim pangguna menyang kaca sing padha tanpa ngganti paramèter sesi.
    • Wiwit nomer sel ing lapangan aneh, bisa salib iki diselehake, nanging ora ana kamar kanggo nul a. Mulane, sawise kita sijine salib, kita nyoba kanggo njaluk indeks saka sel unoccupied (metode getEmptyFieldIndex saka kelas Field). Yen indeks ora negatif, banjur sijine nol ana. 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. Ing tataran iki, sampeyan bisa sijine salib, AI jawaban karo nul. Nanging ora ana mriksa nalika kanggo mungkasi game. Iki bisa ing telung kasus:
    • sawise pamindhahan sabanjure salib, baris saka telung salib dibentuk;
    • sawise pamindhahan bali sabanjuré karo nul, kawangun baris saka telung nul;
    • sawise pamindhahan sabanjure salib, sel kosong rampung.
    Ayo nambahake metode sing mriksa yen ana telung salib / nol berturut-turut:
    /**
     * 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;
    }
    Keanehan saka metode iki yaiku yen pemenang ditemokake, kita nambah parameter liyane ing sesi kasebut, nggunakake sing bakal ngganti tampilan ing "index.jsp" ing paragraf ing ngisor iki.
  15. Ayo dadi nambah telpon kanggo "checkWin " cara kaping pindho kanggo "doGet" cara . Kaping pisanan sawise nyetel salib, kaloro - sawise nyetel 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. Ing babagan prilaku, meh ora ana owah-owahan (kajaba yen salah siji pratandha menang, nul ora diselehake maneh. Ayo nggunakake parameter "menang" ing "index.jsp" lan nampilake pemenang. Kita nggunakake arahan sawise meja: 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>
    Yen salib menang, pesen "CROSSES WIN!" , yen nul "NOUGHTS WIN!" . Akibaté, kita bisa njaluk siji saka rong prasasti:
  17. Yen ana sing menang, sampeyan kudu bisa mbales dendam. Kanggo nindakake iki, sampeyan butuh tombol sing bakal ngirim panjalukan menyang server. Lan server bakal mbatalake sesi saiki lan pangalihan panyuwunan bali menyang "/start" .
    • Ing "index.jsp" ing bagean "kepala" , tulis script "jquery" . Nggunakake perpustakaan iki, kita bakal ngirim panjalukan menyang server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Ing "index.jsp" ing "skrip" bagean , nambah fungsi sing bisa ngirim request POST menyang server. Kita bakal nggawe fungsi sinkron, lan nalika respon teka saka server, bakal mbukak maneh kaca saiki.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Ing blok "c:if" , tambahake tombol sing, nalika diklik, nelpon fungsi sing lagi wae kita tulis:
      <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>
    • Ayo nggawe servlet anyar sing bakal ngawula 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");
          }
      }
      Sawise kamenangan, tombol "Mulai maneh" bakal katon . Sawise ngeklik ing, lapangan bakal rampung ngankat, lan game bakal miwiti maneh.
  18. Iku tetep kanggo nimbang kahanan pungkasan. Apa yen pangguna sijine salib, ora ana kamenangan , lan ora ana panggonan kanggo nul a? Banjur iki imbang, lan kita bakal ngolah saiki:
    • Ing sesi "LogicServlet" , tambahake parameter liyane "gambar" , nganyari kolom "data" lan ngirim pangalihan menyang "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;
      }
    • Ing "index.jsp" kita bakal ngolah parameter iki:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Minangka asil imbang, kita bakal nampa pesen sing cocog lan tawaran kanggo miwiti maneh:

Iki ngrampungake nulis game kasebut.

Kode kelas lan file sing digunakake

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

Wiwiti manehServlet

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