Hari ini kami akan menulis game Tic-Tac-Toe menggunakan servlet dan JSP.

Proyek ini akan sedikit berbeda dari yang sebelumnya. Ini tidak hanya berisi tugas, tetapi juga penjelasan tentang cara melakukannya. Artinya, ini akan menjadi proyek dari seri "CARA ...".

Petunjuk:

  1. Garpu dari repositori: https://github.com/CodeGymCC/project-servlet.git
  2. Unduh versi proyek Anda ke komputer Anda.
  3. Siapkan peluncuran aplikasi di IDEA:
    • Alt + Shift + F9 -> Edit Konfigurasi… -> Alt + insert -> tom (ke dalam bilah pencarian) -> Lokal.
    • Setelah itu, Anda perlu mengklik "KONFIGURASI" dan menunjukkan di mana arsip dengan Tomcat diunduh dan dibongkar.
    • Di tab "Penerapan": Alt + sisipkan -> Artefak… -> tic-tac-toe: perang meledak -> OK.
    • Di bidang "Konteks aplikasi": biarkan hanya "/" (garis miring).
    • Tekan "TERAPKAN".
    • Tutup jendela pengaturan.
    • Lakukan uji coba pertama dari konfigurasi yang disesuaikan. Jika semuanya dilakukan dengan benar, browser default Anda akan terbuka, di mana itu adalah:
  4. Buka file "pom.xml" . Ada 2 dependensi di blok "dependensi" .
    • javax.servlet-apibertanggung jawab atas spesifikasi servlet. Lingkup "disediakan" diperlukan selama pengembangan, tetapi tidak diperlukan saat runtime (Tomcat sudah memiliki ketergantungan ini di folder lib).
    • jstl– dapat dianggap sebagai mesin template.
  5. Ada 3 file di folder “webapp” :
    • index.jsp- ini adalah template kami (mirip dengan halaman HTML). Ini akan berisi markup dan skrip. Ini adalah file bernama "indeks" yang diberikan sebagai halaman awal, jika tidak ada konfigurasi, yang kita lihat di langkah 3.
    • /static/main.css- file untuk gaya. Seperti pada proyek sebelumnya, semua yang ada di sini terserah Anda, cat sesuai keinginan.
    • /static/jquery-3.6.0.min.js- ketergantungan frontend yang akan didistribusikan oleh server kami sebagai statis.
  6. Paket "com.tictactoe" akan berisi semua kode Java. Saat ini ada 2 kelas:
    • Sign- enum, yang bertanggung jawab atas "cross / zero / void" .
    • Fieldadalah bidang kami. Kelas ini memiliki peta "lapangan" . Prinsip penyimpanan data adalah sebagai berikut: sel bidang tic-tac-toe diberi nomor dari nol. Di baris pertama 0, 1 dan 2. Di baris kedua: 3, 4 dan 5. Dan seterusnya. Ada juga 3 metode. "getEmptyFieldIndex" mencari sel kosong pertama (ya, lawan kita tidak akan terlalu pintar). "checkWin" memeriksa apakah permainan sudah berakhir. Jika ada deretan tiga persilangan, ia mengembalikan persilangan; jika ada deretan tiga nol, ia mengembalikan nol. Jika tidak, itu kosong. "getFieldData" - mengembalikan nilai peta "bidang" sebagai daftar yang diurutkan dalam urutan indeks menaik.
  7. Penjelasan tentang template sudah selesai, sekarang Anda dapat memulai tugas. Mari kita mulai dengan menggambar tabel berukuran 3 kali 3. Untuk melakukannya, tambahkan kode berikut ke “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>
    Kami kemudian akan menghapus angka dalam tabel dan menggantinya dengan tanda silang, nol atau bidang kosong. Juga, di dalam tag "head", sertakan file gaya. Untuk melakukan ini, tambahkan baris:<link href="static/main.css" rel="stylesheet">

    Konten file gaya terserah Anda. Saya menggunakan yang ini:
    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;
    }
    Setelah dijalankan, hasil saya terlihat seperti ini:
  8. Sekarang mari tambahkan fungsionalitas berikut: ketika sel diklik, permintaan akan dikirim ke server, di mana kita akan meneruskan indeks sel yang diklik sebagai parameter. Tugas ini dapat dibagi menjadi dua bagian: mengirim permintaan dari depan, menerima permintaan di server. Mari kita mulai dari depan untuk perubahan.

    Mari tambahkan parameter "onclick" ke setiap tag "d" . Dalam nilainya, kami menunjukkan perubahan halaman saat ini ke URL yang ditentukan. Servlet yang akan bertanggung jawab atas logika akan memiliki URL “/logic” . Dan itu akan membutuhkan parameter yang disebut "klik" . Jadi kami akan meneruskan indeks sel yang diklik pengguna.
    <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>
    Anda dapat memeriksa apakah semuanya dilakukan dengan benar melalui panel pengembang di browser. Misalnya, di Chrome, ini terbuka dengan tombol F12 . Sebagai hasil dari mengklik sel dengan indeks 4, gambarnya adalah sebagai berikut: Kami mendapatkan kesalahan karena kami belum membuat servlet yang dapat mengirim server ke alamat "logic" .
  9. Dalam paket "com.tictactoe" buat kelas "LogicServlet" yang harus diturunkan dari kelas "javax.servlet.http.HttpServlet" . Di kelas, timpa metode "doGet" .

    Dan mari tambahkan metode yang akan mendapatkan indeks sel yang diklik. Anda juga perlu menambahkan pemetaan (alamat di mana servlet ini akan mencegat permintaan). Saya sarankan melakukan ini melalui anotasi (tetapi jika Anda menyukai kesulitan, Anda juga dapat menggunakan 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;
        }
    
    }
    Sekarang, saat mengklik sel mana saja, kita akan mendapatkan indeks sel ini di server (Anda dapat memastikannya dengan menjalankan server dalam debug). Dan akan ada pengalihan ke halaman yang sama dari mana klik dilakukan.
  10. Sekarang kita bisa mengklik, tapi ini belum menjadi permainan. Agar gim memiliki logika, Anda harus menyimpan status gim (di mana persilangan, di mana nol) di antara permintaan. Cara termudah untuk melakukannya adalah dengan menyimpan data ini di sesi. Dengan pendekatan ini, sesi akan disimpan di server, dan klien akan menerima ID sesi dalam cookie bernama “JSESSIONID” . Namun sesi tersebut tidak perlu dibuat setiap saat, melainkan hanya di awal permainan. Mari kita mulai servlet lain untuk ini, yang akan kita sebut "InitServlet" . Kami akan mengganti metode "doGet" di dalamnya , di mana kami akan membuat sesi baru, membuat lapangan bermain, meletakkan lapangan bermain ini dan daftar jenis Masuk atribut sesi, dan kirim "maju" ke index.jsp halaman. 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);
        }
    }
    Dan jangan lupa, mari kita ubah halaman awal yang terbuka di browser setelah memulai server menjadi "/ mulai" : Sekarang setelah memulai ulang server dan mengklik sel mana saja di menu pengembang browser di bagian "Header Permintaan" , akan ada cookie dengan session ID :
  11. Saat kita memiliki repositori tempat kita dapat menyimpan status di antara permintaan dari klien (browser), kita dapat mulai menulis logika game. Logika yang kita miliki ada di “LogicServlet” . Kita perlu bekerja dengan metode "doGet" . Mari tambahkan perilaku ini ke metode:
    • kita akan mendapatkan objek "bidang" dari jenis Bidang dari sesi (kita akan membawanya ke metode "extractField" ).
    • beri tanda silang di mana pengguna mengklik (sejauh ini tanpa pemeriksaan apa pun).
    @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;
    }
    Perilaku belum berubah, tetapi jika Anda memulai server dalam debug dan menyetel breakpoint pada baris tempat pengalihan dikirim, Anda dapat melihat "jeroan" dari objek " data " . Di sana, memang , "CROSS" muncul di bawah indeks yang diklik.
  12. Sekarang saatnya menampilkan salib di bagian depan. Untuk melakukan ini, kami akan bekerja dengan file "index.jsp" dan teknologi "JSTL" .
    • Di bagian <head> tambahkan:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Pada tabel di dalam setiap blok <td>, ubah indeks menjadi konstruksi yang memungkinkan Anda menghitung nilai. Misalnya, untuk indeks nol: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Sekarang, saat Anda mengklik sel, tanda silang akan muncul di sana:
  13. Kami telah bergerak, sekarang giliran "nol". Dan mari tambahkan beberapa tanda centang di sini, agar tanda tidak ditempatkan di sel yang sudah ditempati.
    • Anda perlu memeriksa apakah sel yang diklik kosong. Jika tidak, kami tidak melakukan apa pun dan mengarahkan pengguna ke halaman yang sama tanpa mengubah parameter sesi.
    • Karena jumlah sel di lapangan ganjil, ada kemungkinan salib ditempatkan, tetapi tidak ada ruang untuk nol. Oleh karena itu, setelah kami memberi tanda silang, kami mencoba mendapatkan indeks dari sel kosong (metode getEmptyFieldIndex dari kelas Field). Jika indeksnya tidak negatif, beri nol di sana. 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. Pada tahap ini, Anda dapat memberi tanda silang, jawaban AI dengan nol. Tetapi tidak ada pemeriksaan kapan harus menghentikan permainan. Ini bisa terjadi dalam tiga kasus:
    • setelah gerakan salib berikutnya, garis tiga salib terbentuk;
    • setelah gerakan kembali berikutnya dengan nol, garis tiga nol terbentuk;
    • setelah gerakan salib berikutnya, sel-sel kosong berakhir.
    Mari tambahkan metode yang memeriksa apakah ada tiga persilangan/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;
    }
    Keunikan dari metode ini adalah jika pemenang ditemukan, kami menambahkan parameter lain ke sesi, yang dengannya kami akan mengubah tampilan di "index.jsp" di paragraf berikut.
  15. Mari tambahkan panggilan ke metode "checkWin " dua kali ke metode "doGet" . Pertama kali setelah mengatur salib, yang kedua - setelah mengatur nol.
    // 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. Dalam hal perilaku, hampir tidak ada yang berubah (kecuali jika salah satu tanda menang, nol tidak lagi ditempatkan. Mari gunakan parameter "pemenang" di "index.jsp" dan tampilkan pemenangnya. Kami menggunakan arahan setelah tabel: 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>
    Jika salib menang, pesan “CROSSES WIN!” , jika angka nolnya adalah “NOUGHTS WIN!” . Hasilnya, kita bisa mendapatkan salah satu dari dua prasasti:
  17. Jika ada pemenang, Anda harus bisa membalas dendam. Untuk melakukan ini, Anda memerlukan tombol yang akan mengirimkan permintaan ke server. Dan server akan membatalkan sesi saat ini dan mengalihkan permintaan kembali ke “/ start” .
    • Pada “index.jsp” pada bagian “head” , tulis script “jquery” . Menggunakan perpustakaan ini, kami akan mengirimkan permintaan ke server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Di "index.jsp" di bagian "skrip" , tambahkan fungsi yang dapat mengirim permintaan POST ke server. Kami akan membuat fungsi sinkron, dan ketika respons datang dari server, itu akan memuat ulang halaman saat ini.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Di dalam blok "c:if" , tambahkan tombol yang, ketika diklik, memanggil fungsi yang baru saja 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>
    • Mari buat servlet baru yang akan melayani 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");
          }
      }
      Setelah kemenangan, tombol “Mulai lagi” akan muncul . Setelah mengkliknya, bidang akan sepenuhnya dihapus, dan permainan akan dimulai kembali.
  18. Tetap mempertimbangkan situasi terakhir. Bagaimana jika pengguna memberi tanda silang, tidak ada kemenangan, dan tidak ada tempat untuk nol? Maka ini undian, dan kami akan memprosesnya sekarang:
    • Di sesi "LogicServlet" , tambahkan parameter lain "draw" , perbarui bidang "data" dan kirim pengalihan ke "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;
      }
    • Di "index.jsp" kami akan memproses parameter ini:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Sebagai hasil pengundian, kami akan menerima pesan terkait dan tawaran untuk memulai kembali:

Ini melengkapi penulisan game.

Kode kelas dan file tempat mereka bekerja

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

LogikaServlet

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

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