Hari ini kita akan menulis permainan Tic-Tac-Toe menggunakan servlet dan JSP.

Projek ini akan berbeza sedikit daripada yang sebelumnya. Ia akan mengandungi bukan sahaja tugas, tetapi juga penjelasan tentang cara melakukannya. Iaitu, ia akan menjadi projek dari siri "HOW TO ...".

Arahan:

  1. Fork dari repositori: https://github.com/CodeGymCC/project-servlet.git
  2. Muat turun versi projek anda ke komputer anda.
  3. Sediakan pelancaran aplikasi dalam IDEA:
    • Alt + Shift + F9 -> Edit Konfigurasi... -> Alt + insert -> tom (ke dalam bar carian) -> Local.
    • Selepas itu, anda perlu mengklik "CONFIGURE" dan menunjukkan tempat arkib dengan Tomcat telah dimuat turun dan dibongkar.
    • Dalam tab "Pengerahan": Alt + sisip -> Artifak... -> tic-tac-toe:war meledak -> OK.
    • Dalam medan "Konteks aplikasi": tinggalkan "/" sahaja (slash).
    • Tekan "APPLY".
    • Tutup tetingkap tetapan.
    • Buat ujian pertama bagi konfigurasi tersuai. Jika semuanya dilakukan dengan betul, penyemak imbas lalai anda akan dibuka, di mana ia akan menjadi:
  4. Buka fail "pom.xml" . Terdapat 2 kebergantungan dalam blok "kebergantungan" .
    • javax.servlet-apibertanggungjawab untuk spesifikasi servlet. Skop "disediakan" diperlukan semasa pembangunan, tetapi tidak diperlukan semasa runtime (Tomcat sudah mempunyai kebergantungan ini dalam folder lib).
    • jstl– boleh dianggap sebagai enjin templat.
  5. Terdapat 3 fail dalam folder "webapp" :
    • index.jsp- ini adalah templat kami (serupa dengan halaman HTML). Ia akan mengandungi markup dan skrip. Ia adalah fail yang dipanggil "indeks" yang diberikan sebagai halaman awal, jika tiada konfigurasi, yang kami lihat dalam langkah 3.
    • /static/main.css- fail untuk gaya. Seperti dalam projek sebelumnya, semuanya di sini terpulang kepada anda, cat mengikut kehendak anda.
    • /static/jquery-3.6.0.min.js- pergantungan frontend yang pelayan kami akan edarkan sebagai statik.
  6. Pakej "com.tictactoe" akan mengandungi semua kod Java. Sekarang ada 2 kelas:
    • Sign- enum, yang bertanggungjawab untuk "silang / sifar / batal" .
    • Fieldadalah bidang kita. Kelas ini mempunyai peta "medan" . Prinsip penyimpanan data adalah seperti berikut: sel-sel medan tic-tac-toe dinomborkan dari sifar. Dalam baris pertama 0, 1 dan 2. Dalam baris kedua: 3, 4 dan 5. Dan seterusnya. Terdapat juga 3 kaedah. "getEmptyFieldIndex" mencari sel kosong pertama (ya, lawan kita tidak akan menjadi sangat pintar). "checkWin" menyemak sama ada permainan telah tamat. Jika terdapat satu baris tiga salib, ia mengembalikan salib; jika terdapat satu baris tiga sifar, ia mengembalikan sifar. Jika tidak, ia kosong. "getFieldData" - mengembalikan nilai peta "medan" sebagai senarai yang diisih dalam susunan indeks menaik.
  7. Penjelasan tentang templat telah selesai, kini anda boleh memulakan tugas. Mari kita mulakan dengan melukis jadual 3 dengan 3. Untuk melakukan ini, tambah kod berikut pada “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 kemudiannya akan mengalih keluar nombor dalam jadual dan menggantikannya dengan pangkah, sifar atau medan kosong. Juga, di dalam teg "kepala", sertakan fail gaya. Untuk melakukan ini, tambahkan baris:<link href="static/main.css" rel="stylesheet">

    Kandungan fail gaya terpulang kepada 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;
    }
    Selepas berjalan, keputusan saya kelihatan seperti ini:
  8. Sekarang mari kita tambahkan fungsi berikut: apabila sel diklik, permintaan akan dihantar ke pelayan, di mana kita akan lulus indeks sel yang diklik sebagai parameter. Tugas ini boleh dibahagikan kepada dua bahagian: menghantar permintaan dari hadapan, menerima permintaan pada pelayan. Mari kita mulakan di hadapan untuk perubahan.

    Mari tambahkan parameter "onclick" pada setiap teg "d" . Dalam nilai, kami menunjukkan perubahan halaman semasa kepada URL yang ditentukan. Servis yang akan bertanggungjawab untuk logik akan mempunyai URL “/logic” . Dan ia akan mengambil parameter yang dipanggil "klik" . Jadi kita akan lulus indeks sel yang pengguna klik.
    <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 boleh menyemak sama ada semuanya dilakukan dengan betul melalui panel pembangun dalam penyemak imbas. Contohnya, dalam Chrome, ia dibuka dengan butang F12 . Hasil daripada mengklik pada sel dengan indeks 4, gambar akan menjadi seperti berikut: Kami mendapat ralat kerana kami belum mencipta servlet yang boleh menghantar pelayan ke alamat "logik" .
  9. Dalam pakej "com.tictactoe" cipta kelas "LogicServlet" yang sepatutnya diperoleh daripada kelas "javax.servlet.http.HttpServlet" . Dalam kelas, ganti kaedah "doGet" .

    Dan mari tambah kaedah yang akan mendapatkan indeks sel yang diklik. Anda juga perlu menambah pemetaan (alamat di mana servlet ini akan memintas permintaan). Saya cadangkan melakukan ini melalui anotasi (tetapi jika anda suka kesukaran, anda juga boleh menggunakan web.xml). Kod servlet am:
    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, apabila mengklik pada mana-mana sel, kami akan mendapat indeks sel ini pada pelayan (anda boleh pastikan dengan menjalankan pelayan dalam nyahpepijat). Dan akan ada ubah hala ke halaman yang sama dari mana klik dibuat.
  10. Sekarang kita boleh klik, tetapi ia bukan permainan lagi. Agar permainan mempunyai logik, anda perlu menyimpan keadaan permainan (di mana salib berada, di mana sifar berada) antara permintaan. Cara paling mudah untuk melakukan ini ialah menyimpan data ini dalam sesi. Dengan pendekatan ini, sesi akan disimpan pada pelayan dan pelanggan akan menerima ID sesi dalam kuki bernama "JSESSIONID" . Tetapi sesi tidak perlu dibuat setiap kali, tetapi hanya pada permulaan permainan. Mari kita mulakan servlet lain untuk ini, yang akan kita panggil "InitServlet" . Kami akan mengatasi kaedah "doGet" di dalamnya , di mana kami akan membuat sesi baharu, mencipta medan permainan, meletakkan medan permainan ini dan senarai jenis Log masuk atribut sesi dan menghantar " ke hadapan" ke index.jsp muka surat. Kod:
    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 tidak lupa, mari tukar halaman permulaan yang dibuka dalam penyemak imbas selepas memulakan pelayan kepada "/mula" : Sekarang selepas memulakan semula pelayan dan mengklik mana-mana sel medan dalam menu pembangun penyemak imbas dalam bahagian "Permintaan Pengepala" , akan ada kuki dengan ID sesi :
  11. Apabila kita mempunyai repositori di mana kita boleh menyimpan keadaan antara permintaan daripada klien (pelayar), kita boleh mula menulis logik permainan. Logik yang kami ada adalah dalam "LogicServlet" . Kita perlu bekerja dengan kaedah "doGet" . Mari tambahkan tingkah laku ini pada kaedah:
    • kami akan mendapat objek "medan" jenis Medan daripada sesi (kami akan membawanya keluar ke kaedah "extractField" ).
    • letakkan pangkah di mana pengguna mengklik (setakat ini tanpa sebarang semakan).
    @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;
    }
    Tingkah laku masih belum berubah, tetapi jika anda memulakan pelayan dalam nyahpepijat dan menetapkan titik putus pada baris tempat ubah hala dihantar, anda boleh melihat "dalaman" objek "data " . Di sana, sememangnya , "CROSS" muncul di bawah indeks yang telah diklik.
  12. Kini tiba masanya untuk memaparkan salib pada bahagian hadapan. Untuk melakukan ini, kami akan bekerja dengan fail "index.jsp" dan teknologi "JSTL" .
    • Dalam bahagian <head> tambahkan:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Dalam jadual di dalam setiap blok <td>, tukar indeks kepada binaan yang membolehkan anda mengira nilai. Sebagai contoh, untuk indeks sifar: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Sekarang, apabila anda mengklik pada sel, salib akan muncul di sana:
  13. Kami telah membuat langkah kami, kini giliran untuk "sifar". Dan mari kita tambahkan beberapa cek di sini, supaya tanda-tanda tidak diletakkan di dalam sel yang sudah diduduki.
    • Anda perlu menyemak sama ada sel yang diklik itu kosong. Jika tidak, kami tidak melakukan apa-apa dan menghantar pengguna ke halaman yang sama tanpa mengubah parameter sesi.
    • Oleh kerana bilangan sel pada medan adalah ganjil, ada kemungkinan salib telah diletakkan, tetapi tiada ruang untuk sifar. Oleh itu, selepas kami meletakkan pangkah, kami cuba mendapatkan indeks sel yang tidak diduduki (kaedah getEmptyFieldIndex kelas Field). Jika indeks tidak negatif, maka letakkan sifar di sana. Kod:
      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 peringkat ini, anda boleh meletakkan pangkah, jawapan AI dengan sifar. Tetapi tidak ada pemeriksaan bila untuk menghentikan permainan. Ini boleh berlaku dalam tiga kes:
    • selepas langkah salib seterusnya, garisan tiga salib terbentuk;
    • selepas pergerakan pulangan seterusnya dengan sifar, garisan tiga sifar telah dibentuk;
    • selepas langkah salib seterusnya, sel kosong berakhir.
    Mari tambah kaedah yang menyemak sama ada terdapat tiga pangkah / sifar 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;
    }
    Keistimewaan kaedah ini ialah jika pemenang ditemui, kami menambah parameter lain pada sesi, yang menggunakan mana kami akan menukar paparan dalam "index.jsp" dalam perenggan berikut.
  15. Mari tambahkan panggilan ke kaedah “checkWin ” dua kali kepada kaedah “doGet” . Kali pertama selepas menetapkan salib, yang kedua - selepas menetapkan sifar.
    // 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. Dari segi tingkah laku, hampir tiada apa yang berubah (kecuali jika salah satu tanda menang, sifar tidak lagi diletakkan. Mari gunakan parameter "pemenang" dalam "index.jsp" dan paparkan pemenang. Kami menggunakan arahan selepas jadual: 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 silang menang, mesej “CROSSES WIN!” , jika sifar ialah “NOUGHTS WIN!” . Akibatnya, kita boleh mendapatkan satu daripada dua inskripsi:
  17. Jika ada pemenang, anda perlu boleh membalas dendam. Untuk melakukan ini, anda memerlukan butang yang akan menghantar permintaan kepada pelayan. Dan pelayan akan membatalkan sesi semasa dan mengubah hala permintaan kembali ke “/start” .
    • Dalam "index.jsp" dalam bahagian "head" , tulis skrip "jquery" . Menggunakan perpustakaan ini, kami akan menghantar permintaan kepada pelayan.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Dalam "index.jsp" dalam bahagian "skrip" , tambahkan fungsi yang boleh menghantar permintaan POST ke pelayan. Kami akan menjadikan fungsi itu segerak, dan apabila respons datang daripada pelayan, ia akan memuatkan semula halaman semasa.
      <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 butang yang, apabila diklik, memanggil fungsi yang baru kami 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 baharu yang akan menyediakan 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");
          }
      }
      Selepas kemenangan, butang “Mula semula” akan muncul . Selepas mengklik padanya, medan akan dikosongkan sepenuhnya, dan permainan akan bermula semula.
  18. Ia kekal untuk mempertimbangkan keadaan terakhir. Bagaimana jika pengguna meletakkan salib, tiada kemenangan, dan tiada tempat untuk sifar? Kemudian ini adalah cabutan, dan kami akan memprosesnya sekarang:
    • Dalam sesi "LogicServlet" , tambah parameter lain "draw" , kemas kini medan "data" dan hantar ubah hala 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;
      }
    • Dalam "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>
      Hasil daripada cabutan, kami akan menerima mesej yang sepadan dan tawaran untuk memulakan semula:

Ini melengkapkan penulisan permainan.

Kod kelas dan fail yang mereka gunakan

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

Mulakan semulaServlet

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