今天我們將使用 servlet 和 JSP 編寫一個井字遊戲。

該項目將與之前的項目略有不同。它不僅包含任務,還包含如何執行任務的說明。也就是說,它將是“HOW TO ...”系列中的一個項目。

操作說明:

  1. 從存儲庫中分叉:https ://github.com/vasylmalik/project-servlet.git
  2. 將您的項目版本下載到您的計算機。
  3. 在 IDEA 中設置應用程序啟動:
    • Alt + Shift + F9 -> 編輯配置... -> Alt + 插入 -> tom(進入搜索欄)-> Local。
    • 之後,您需要單擊“配置”並指明下載和解壓縮帶有 Tomcat 的存檔文件的位置。
    • 在“部署”選項卡中:Alt + insert -> Artifact ... -> tic-tac-toe:war exploded -> OK。
    • 在“應用程序上下文”字段中:僅保留“/”(斜杠)。
    • 按“應用”​​。
    • 關閉設置窗口。
    • 對自定義配置進行首次試運行。如果一切都正確完成,您的默認瀏覽器將打開,其中將是:
  4. 打開“pom.xml”文件。“依賴項”塊中有 2 個依賴項。
    • javax.servlet-api負責servlets的規範。在開發期間需要“提供”的範圍,但在運行時不需要(Tomcat 已經在 lib 文件夾中具有此依賴項)。
    • jstl– 可以被認為是一個模板引擎。
  5. “webapp”文件夾中有 3 個文件:
    • index.jsp- 這是我們的模板(類似於 HTML 頁面)。它將包含標記和腳本。如果沒有我們在第 3 步中看到的配置,它就是作為初始頁面給出的名為“index”的文件。
    • /static/main.css- 樣式文件。和之前的項目一樣,這裡的一切都由你決定,想怎麼畫就怎麼畫。
    • /static/jquery-3.6.0.min.js- 我們的服務器將作為靜態分發的前端依賴項。
  6. “com.tictactoe”包將包含所有 Java 代碼。現在有2個類:
    • Sign- 枚舉,負責“交叉/零/無效”
    • Field是我們的領域。這個類有一個“字段”映射。數據存儲的原則如下:井字遊戲字段的單元格從零開始編號。在第一行中是 0、1 和 2。在第二行中:3、4 和 5。依此類推。也有3種方法。“getEmptyFieldIndex”尋找第一個空單元格(是的,我們的對手不會很聰明)。“checkWin”檢查遊戲是否結束。如果有一排三個十字,它返回一個十字;如果一排三個零,它返回一個零。否則,它是空的。“getFieldData” ——返回“字段”映射的值作為按索引升序排序的列表。
  7. 關於模板的解釋已經完成,現在您可以開始任務了。讓我們從繪製一個 3×3 的表格開始。為此,將以下代碼添加到“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> 	
    				
    然後,我們將刪除表中的數字,並用叉號、零或空字段替換它們。此外,在“head”標籤內,包含樣式文件。為此,添加一行:<link href="static/main.css" rel="stylesheet">

    樣式文件的內容由您決定。我用過這個:
    
    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; 
    } 
    		
    運行後,我的結果是這樣的:
  8. 現在讓我們添加以下功能:單擊單元格時,將向服務器發送請求,在該請求中我們將傳遞被單擊的單元格的索引作為參數。這個任務可以分為兩個部分:從前端發送請求,在服務器上接受請求。讓我們從前面開始改變。

    讓我們為每個“d” 標籤添加一個“onclick”參數。在值中,我們指示當前頁面更改為指定的 URL。負責邏輯的 servlet 將具有 URL “/logic”。它需要一個名為“click”的參數。所以我們將傳遞用戶點擊的單元格的索引。
    
    <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> 			
    
    		
    您可以通過瀏覽器中的開發人員面板檢查一切是否正確完成。例如,在 Chrome 中,它使用F12按鈕打開。單擊索引為 4 的單元格後,圖片將如下所示: 我們得到一個錯誤,因為我們還沒有創建一個可以將服務器發送到地址“logic”的 servlet 。
  9. 在包“com.tictactoe”中創建一個類“LogicServlet”,它應該從類“javax.servlet.http.HttpServlet”派生。在類中,覆蓋“doGet”方法。

    讓我們添加一個方法來獲取被單擊的單元格的索引。您還需要添加一個映射(此 servlet 將攔截請求的地址)。我建議通過註釋來做到這一點(但如果你喜歡困難,你也可以使用 web.xml)。通用servlet代碼:
    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;
        }
    
    }
    現在,當單擊任何單元格時,我們將獲得該單元格在服務器上的索引(您可以通過在調試中運行服務器來確保)。並且將重定向到進行點擊的同一頁面。
  10. 現在我們可以點擊了,但這還不是遊戲。為了使遊戲具有邏輯性,您需要在請求之間保存遊戲狀態(叉在哪裡,零在哪裡)。最簡單的方法是將此數據存儲在會話中。使用這種方法,會話將存儲在服務器上,客戶端將在名為“JSESSIONID”的 cookie 中收到一個會話 ID 。但是session不需要每次都創建,只需要在遊戲開始的時候創建即可。讓我們為此啟動另一個 servlet,我們將其稱為“InitServlet”。我們將覆蓋其中的“doGet”方法,在該方法中我們將創建一個新會話,創建一個運動場,將這個運動場和一個 Sign 類型的列表放在會話屬性中,並將“ forward”發送到 index.jsp頁。代碼:
    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);
        }
    }
    不要忘記,讓我們將啟動服務器後在瀏覽器中打開的起始頁更改為“/start”: 現在,在重新啟動服務器並單擊“請求標頭” 部分的瀏覽器開發人員菜單中的字段的任何單元格後,將會有一個帶有會話 ID 的 cookie:
  11. 當我們有一個可以存儲來自客戶端(瀏覽器)的請求之間的狀態的存儲庫時,我們就可以開始編寫遊戲邏輯了。我們擁有的邏輯在“LogicServlet”中。我們需要使用“doGet”方法。讓我們將此行為添加到方法中:
    • 我們將從會話中獲取 Field 類型的“字段”對象(我們將其取出到“extractField”方法)。
    • 在用戶點擊的地方打叉(到目前為止沒有任何檢查)。
    @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;
    }
    行為尚未改變,但如果您在調試中啟動服務器並在發送重定向的行上設置斷點,您可以看到“數據”對象的“內部結構實際上,“CROSS”出現在被點擊的索引下方。
  12. 現在是時候在前端顯示十字了。為此,我們將使用“index.jsp”文件和“JSTL”技術。
    • 在 <head> 部分添加:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • 在每個 <td> 塊內的表中,將索引更改為允許您計算值的結構。例如,對於索引零: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> 現在,當您單擊一個單元格時,那裡會出現一個十字:
  13. 我們已經採取行動,現在輪到“零”了。讓我們在這裡添加一些檢查,以便標誌不會放置在已佔用的單元格中。
    • 您需要檢查單擊的單元格是否為空。否則,我們什麼也不做,將用戶發送到同一頁面而不更改會話參數。
    • 由於該字段上的單元格數量是奇數,因此可能放置了十字,但沒有空間放置零。因此,我們在打叉之後,嘗試獲取一個未被佔用的單元格的索引(Field類的getEmptyFieldIndex方法)。如果索引不是負數,則在那裡放一個零。 代碼:
      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. 在這個階段,你可以打叉,AI 的答案是零。但是沒有檢查何時停止遊戲。這可以分為三種情況:
    • 十字的下一步移動之後,形成了三個十字的線;
    • 在下一個帶零的返回移動之後,形成了一條由三個零組成的線;
    • 在十字的下一步移動之後,空單元格結束。
    讓我們添加一個方法來檢查一行中是否有三個叉號/零號:
    /**
     * 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;
    }
    此方法的特殊之處在於,如果找到獲勝者,我們會向會話添加另一個參數,我們將使用該參數更改以下段落中 “index.jsp” 中的顯示。
  15. 讓我們向“doGet”方法添加兩次對“checkWin ”方法的調用。第一次設置十字後,第二次 - 設置零後。
    // 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. 在行為方面,幾乎沒有任何變化(除瞭如果其中一個符號獲勝,則不再放置零。讓我們在“index.jsp”中使用“winner”參數並顯示獲勝者。我們在表格 c:set後面使用指令:c: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> 
    
    如果十字線獲勝,則顯示“十字線獲勝!” 的信息 ,如果零是“NOUGHTS WIN!” . 結果,我們可以獲得兩個銘文之一:
  17. 如果有贏家,你需要能夠報復。為此,您需要一個向服務器發送請求的按鈕。服務器將使當前會話無效並將請求重定向回“/start”
    • 在“head”部分的“index.jsp”中,編寫腳本“jquery”。使用這個庫,我們將向服務器發送請求。
      
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script> 
      
    • “index.jsp”“script”部分,添加一個可以向服務器發送POST請求的函數。我們將使功能同步,當響應來自服務器時,它將重新加載當前頁面。
      
      <script> 
          function restart() { 
              $.ajax({ 
                  url: '/restart', 
                  type: 'POST', 
                  contentType: 'application/json;charset=UTF-8', 
                  async: false, 
                  success: function () { 
                      location.reload(); 
                  } 
              }); 
          } 
      </script> 
      
    • “c:if”塊中,添加一個按鈕,單擊該按鈕將調用我們剛剛編寫的函數:
      
      <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> 
      
    • 讓我們創建一個將為“/restart” URL 提供服務的新 servlet 。
      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");
          }
      }
      勝利後會出現“重新開始”按鈕。點擊後,場地將被徹底清空,遊戲重新開始。
  18. 最後一種情況還有待考慮。如果用戶打叉,沒有勝利,也沒有零的位置怎麼辦?那麼這是一個平局,我們現在將處理它:
    • “LogicServlet”會話中,添加另一個參數“draw” ,更新“data”字段並將重定向發送到“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;
      }
    • “index.jsp”中我們將處理這個參數:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      作為平局的結果,我們將收到相應的消息和重新開始的提議:

這樣就完成了遊戲的編寫。

他們使用的類和文件的代碼

初始化Servlet

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

邏輯服務小程序

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

重啟Servlet

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

索引.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>

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