Hôm nay chúng ta sẽ viết một trò chơi Tic-Tac-Toe sử dụng servlet và JSP.

Dự án này sẽ khác một chút so với những dự án trước. Nó sẽ không chỉ chứa các nhiệm vụ mà còn có giải thích về cách thực hiện chúng. Tức là nó sẽ là một dự án trong sê-ri "HOW TO ...".

Chỉ dẫn:

  1. Ngã ba từ kho lưu trữ: https://github.com/CodeGymCC/project-servlet.git
  2. Tải phiên bản dự án về máy tính của bạn.
  3. Thiết lập khởi chạy ứng dụng trong IDEA:
    • Alt + Shift + F9 -> Chỉnh sửa cấu hình… -> Alt + insert -> tom (vào thanh tìm kiếm) -> Cục bộ.
    • Sau đó, bạn cần nhấp vào “CẤU HÌNH” và cho biết nơi lưu trữ với Tomcat đã được tải xuống và giải nén.
    • Trong tab “Triển khai”: Alt + insert -> Cổ vật… -> tic-tac-toe:chiến tranh bùng nổ -> OK.
    • Trong trường “Bối cảnh ứng dụng”: chỉ để lại dấu “/” (dấu gạch chéo).
    • Nhấn "ÁP DỤNG".
    • Đóng cửa sổ cài đặt.
    • Thực hiện lần chạy thử đầu tiên của cấu hình tùy chỉnh. Nếu mọi thứ được thực hiện chính xác, trình duyệt mặc định của bạn sẽ mở ra, trong đó sẽ là:
  4. Mở tệp "pom.xml" . Có 2 phụ thuộc trong khối "phụ thuộc" .
    • javax.servlet-apichịu trách nhiệm về đặc điểm kỹ thuật của servlet. Phạm vi "được cung cấp" là cần thiết trong quá trình phát triển, nhưng không cần thiết trong thời gian chạy (Tomcat đã có phần phụ thuộc này trong thư mục lib).
    • jstl– có thể được coi là một công cụ mẫu.
  5. Có 3 tệp trong thư mục “webapp” :
    • index.jsp- đây là mẫu của chúng tôi (tương tự như trang HTML). Nó sẽ chứa đánh dấu và tập lệnh. Đó là tệp có tên là "chỉ mục" được cung cấp làm trang ban đầu, nếu không có cấu hình mà chúng ta đã thấy ở bước 3.
    • /static/main.css- tập tin cho phong cách. Như trong dự án trước, mọi thứ ở đây tùy thuộc vào bạn, hãy vẽ theo ý muốn.
    • /static/jquery-3.6.0.min.js- phần phụ thuộc giao diện người dùng mà máy chủ của chúng tôi sẽ phân phối dưới dạng tĩnh.
  6. Gói "com.tictactoe" sẽ chứa tất cả mã Java. Hiện tại có 2 lớp:
    • Sign- enum, chịu trách nhiệm về "cross / zero / void" .
    • Fieldlà lĩnh vực của chúng tôi. Lớp này có một bản đồ “trường” . Nguyên tắc lưu trữ dữ liệu sẽ như sau: các ô của trường tic-tac-toe được đánh số từ 0. Trong dòng đầu tiên 0, 1 và 2. Trong dòng thứ hai: 3, 4 và 5. Và cứ thế. Ngoài ra còn có 3 phương pháp. “getEmptyFieldIndex” tìm kiếm ô trống đầu tiên (vâng, đối thủ của chúng ta sẽ không thông minh lắm). "checkWin" kiểm tra xem trò chơi đã kết thúc chưa. Nếu có một hàng gồm ba chữ thập, nó sẽ trả về một chữ thập; nếu có một hàng gồm ba số 0, nó sẽ trả về một số không. Nếu không, nó trống rỗng. "getFieldData" - trả về các giá trị của bản đồ "trường" dưới dạng danh sách được sắp xếp theo thứ tự chỉ số tăng dần.
  7. Các giải thích về mẫu đã kết thúc, bây giờ bạn có thể bắt đầu nhiệm vụ. Hãy bắt đầu bằng cách vẽ một bảng 3 x 3. Để làm điều này, hãy thêm đoạn mã sau vào “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>
    Sau đó, chúng tôi sẽ xóa các số trong bảng và thay thế chúng bằng một chữ thập, số 0 hoặc một trường trống. Ngoài ra, bên trong thẻ “head”, hãy bao gồm tệp kiểu. Để làm điều này, thêm một dòng:<link href="static/main.css" rel="stylesheet">

    Nội dung của tệp kiểu tùy thuộc vào bạn. Tôi đã sử dụng cái này:
    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;
    }
    Sau khi chạy, kết quả của tôi trông như thế này:
  8. Bây giờ, hãy thêm chức năng sau: khi một ô được nhấp, một yêu cầu sẽ được gửi đến máy chủ, trong đó chúng tôi sẽ chuyển chỉ mục của ô được nhấp làm tham số. Nhiệm vụ này có thể được chia thành hai phần: gửi yêu cầu từ phía trước, chấp nhận yêu cầu trên máy chủ. Hãy bắt đầu từ phía trước để thay đổi.

    Hãy thêm tham số "onclick" vào mỗi thẻ "d" . Trong giá trị, chúng tôi chỉ ra sự thay đổi của trang hiện tại thành URL được chỉ định. servlet chịu trách nhiệm logic sẽ có URL “/logic” . Và nó sẽ nhận một tham số gọi là “click” . Vì vậy, chúng tôi sẽ chuyển chỉ mục của ô mà người dùng đã nhấp vào.
    <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>
    Bạn có thể kiểm tra xem mọi thứ đã được thực hiện chính xác hay chưa thông qua bảng điều khiển dành cho nhà phát triển trong trình duyệt. Ví dụ: trong Chrome, nó sẽ mở bằng nút F12 . Kết quả là khi nhấp vào ô có chỉ số 4, hình ảnh sẽ như sau: Chúng tôi gặp lỗi vì chúng tôi chưa tạo một servlet có thể gửi máy chủ đến địa chỉ “logic” .
  9. Trong gói "com.tictactoe" tạo một lớp "LogicServlet" sẽ được bắt nguồn từ lớp "javax.servlet.http.HttpServlet" . Trong lớp, hãy ghi đè phương thức “doGet” .

    Và hãy thêm một phương thức sẽ lấy chỉ mục của ô đã được nhấp. Bạn cũng cần thêm một ánh xạ (địa chỉ mà servlet này sẽ chặn yêu cầu). Tôi khuyên bạn nên thực hiện việc này thông qua một chú thích (nhưng nếu bạn thích những khó khăn, bạn cũng có thể sử dụng web.xml). Mã servlet chung:
    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;
        }
    
    }
    Bây giờ, khi nhấp vào bất kỳ ô nào, chúng tôi sẽ nhận được chỉ mục của ô này trên máy chủ (bạn có thể chắc chắn bằng cách chạy máy chủ trong gỡ lỗi). Và sẽ có một chuyển hướng đến cùng một trang mà từ đó nhấp chuột được thực hiện.
  10. Bây giờ chúng ta có thể nhấp chuột, nhưng nó vẫn chưa phải là một trò chơi. Để trò chơi có logic, bạn cần lưu trạng thái của trò chơi (nơi có dấu thập, nơi có số 0) giữa các yêu cầu. Cách dễ nhất để làm điều này là lưu trữ dữ liệu này trong phiên. Với phương pháp này, phiên sẽ được lưu trữ trên máy chủ và máy khách sẽ nhận được ID phiên trong cookie có tên “JSESSIONID” . Nhưng phiên không cần phải được tạo mọi lúc mà chỉ khi bắt đầu trò chơi. Hãy bắt đầu một servlet khác cho việc này, mà chúng ta sẽ gọi là "InitServlet" . Chúng tôi sẽ ghi đè phương thức “doGet” trong đó , trong đó chúng tôi sẽ tạo một phiên mới, tạo một sân chơi, đặt sân chơi này và một danh sách loại Đăng nhập vào các thuộc tính phiên và gửi “ chuyển tiếp” tới index.jsp trang. Mã số:
    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);
        }
    }
    Và đừng quên, hãy thay đổi trang bắt đầu mở trong trình duyệt sau khi khởi động máy chủ thành “/start” : Bây giờ, sau khi khởi động lại máy chủ và nhấp vào bất kỳ ô nào của trường trong menu nhà phát triển trình duyệt trong phần “Yêu cầu tiêu đề” , sẽ có một cookie với ID phiên:
  11. Khi chúng tôi có một kho lưu trữ trong đó chúng tôi có thể lưu trữ trạng thái giữa các yêu cầu từ máy khách (trình duyệt), chúng tôi có thể bắt đầu viết logic trò chơi. Logic mà chúng tôi có là trong “LogicServlet” . Chúng ta cần làm việc với phương thức “doGet” . Hãy thêm hành vi này vào phương thức:
    • chúng ta sẽ lấy đối tượng “trường” của loại Trường từ phiên (chúng ta sẽ đưa nó ra phương thức “extractField” ).
    • đặt một dấu thập nơi người dùng đã nhấp vào (cho đến nay không có bất kỳ kiểm tra nào).
    @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;
    }
    Hành vi vẫn chưa thay đổi, nhưng nếu bạn khởi động máy chủ ở chế độ gỡ lỗi và đặt điểm ngắt trên dòng nơi chuyển hướng được gửi, bạn có thể thấy “các bộ phận bên trong” của đối tượng “dữ liệu . Trên thực tế , “CROSS” xuất hiện dưới chỉ mục đã được nhấp vào.
  12. Bây giờ là lúc hiển thị chữ thập trên giao diện người dùng. Để làm điều này, chúng tôi sẽ làm việc với tệp “index.jsp” và công nghệ “JSTL” .
    • Trong phần <head> thêm:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Trong bảng bên trong mỗi khối <td>, hãy thay đổi chỉ mục thành một cấu trúc cho phép bạn tính toán các giá trị. Ví dụ: đối với chỉ số 0: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Bây giờ, khi bạn bấm vào một ô, một dấu thập sẽ xuất hiện ở đó:
  13. Chúng tôi đã hành động, bây giờ đến lượt "số không". Và hãy thêm một vài kiểm tra ở đây để các dấu hiệu không được đặt trong các ô đã được sử dụng.
    • Bạn cần kiểm tra xem ô được nhấp có trống không. Mặt khác, chúng tôi không làm gì cả và đưa người dùng đến cùng một trang mà không thay đổi tham số phiên.
    • Vì số lượng ô trên trường là số lẻ nên có thể có một dấu thập đã được đặt, nhưng không có chỗ cho số không. Do đó, sau khi đặt dấu chéo, chúng tôi cố gắng lấy chỉ mục của một ô trống (phương thức getEmptyFieldIndex của lớp Trường). Nếu chỉ số không âm, hãy đặt số 0 ở đó. Mã số:
      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. Ở giai đoạn này, bạn có thể đặt các chữ thập, AI trả lời bằng số không. Nhưng không có kiểm tra khi dừng trò chơi. Điều này có thể xảy ra trong ba trường hợp:
    • sau lần di chuyển tiếp theo của cây thánh giá, một hàng gồm ba cây thánh giá được hình thành;
    • sau lần quay lại tiếp theo di chuyển với số không, một dòng gồm ba số không được hình thành;
    • sau lần di chuyển tiếp theo của chữ thập, các ô trống kết thúc.
    Hãy thêm một phương thức để kiểm tra xem có ba dấu thập/số không liên tiếp hay không:
    /**
     * 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;
    }
    Điểm đặc biệt của phương pháp này là nếu tìm thấy người chiến thắng, chúng tôi sẽ thêm một tham số khác vào phiên, sử dụng tham số này, chúng tôi sẽ thay đổi hiển thị trong “index.jsp” trong các đoạn sau.
  15. Hãy thêm lệnh gọi phương thức “checkWin ” hai lần vào phương thức “doGet” . Lần đầu tiên sau khi đặt chữ thập, lần thứ hai - sau khi đặt số không.
    // 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. Về mặt hành vi, hầu như không có gì thay đổi (ngoại trừ việc nếu một trong các dấu hiệu chiến thắng, số 0 sẽ không còn được đặt nữa. Hãy sử dụng tham số “người chiến thắng” trong “index.jsp” và hiển thị người chiến thắng. Chúng tôi sử dụng các lệnh sau bảng: 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>
    Nếu các chữ thập thắng, thông báo "Crosses WIN!" , nếu số không là "KHÔNG THẮNG!" . Kết quả là, chúng ta có thể nhận được một trong hai dòng chữ:
  17. Nếu có một người chiến thắng, bạn cần có khả năng trả thù. Để làm điều này, bạn cần một nút sẽ gửi yêu cầu đến máy chủ. Và máy chủ sẽ vô hiệu hóa phiên hiện tại và chuyển hướng yêu cầu trở lại “/start” .
    • Trong “index.jsp” trong phần “head” , hãy viết tập lệnh “jquery” . Sử dụng thư viện này, chúng tôi sẽ gửi yêu cầu đến máy chủ.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Trong “index.jsp” trong phần “script” , hãy thêm một chức năng có thể gửi yêu cầu POST đến máy chủ. Chúng tôi sẽ làm cho chức năng đồng bộ và khi phản hồi đến từ máy chủ, nó sẽ tải lại trang hiện tại.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Bên trong các khối “c:if” , hãy thêm một nút mà khi được nhấp vào sẽ gọi hàm mà chúng ta vừa viết:
      <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>
    • Hãy tạo một servlet mới sẽ phục vụ 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");
          }
      }
      Sau khi chiến thắng, nút "Bắt đầu lại" sẽ xuất hiện . Sau khi nhấp vào nó, trường sẽ bị xóa hoàn toàn và trò chơi sẽ bắt đầu lại.
  18. Nó vẫn còn để xem xét tình hình cuối cùng. Điều gì sẽ xảy ra nếu người dùng đặt một dấu thập, không có chiến thắng và không có chỗ cho số không? Sau đó, đây là một trận hòa và chúng tôi sẽ xử lý nó ngay bây giờ:
    • Trong phiên "LogicServlet" , thêm một tham số khác "draw" , cập nhật trường "data" và gửi chuyển hướng đến "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;
      }
    • Trong "index.jsp", chúng tôi sẽ xử lý tham số này:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Kết quả là một trận hòa, chúng tôi sẽ nhận được tin nhắn tương ứng và đề nghị bắt đầu lại:

Điều này hoàn thành việc viết của trò chơi.

Mã của các lớp và tệp mà chúng đã làm việc

khởi tạoServlet

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

Khởi động lạiServlet

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>

chính.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;
   }