Today we will write a Tic-Tac-Toe game using servlets and JSP.

This project will be a little different from the previous ones. It will contain not only tasks, but also explanations of how to do them. That is, it will be a project from the "HOW TO ..." series.

Instruction:

  1. Fork from the repository: https://github.com/CodeGymCC/project-servlet.git
  2. Download your version of the project to your computer.
  3. Set up application launch in IDEA:
    • Alt + Shift + F9 -> Edit Configurations… -> Alt + insert -> tom (into the search bar) -> Local.
    • After that, you need to click “CONFIGURE” and indicate where the archive with Tomcat was downloaded and unpacked.
    • In the “Deployment” tab: Alt + insert -> Artifact… -> tic-tac-toe:war exploded -> OK.
    • In the “Application context” field: leave only “/” (slash).
    • Press "APPLY".
    • Close the settings window.
    • Make the first test run of the customized configuration. If everything is done correctly, your default browser will open, in which it will be:
  4. Open the "pom.xml" file . There are 2 dependencies in the “dependencies” block .
    • javax.servlet-apiis responsible for the specification of servlets. Scope "provided" is needed during development, but not needed at runtime (Tomcat already has this dependency in the lib folder).
    • jstl– can be considered as a template engine.
  5. There are 3 files in the “webapp” folder :
    • index.jsp- this is our template (similar to the HTML page). It will contain markup and scripts. It is the file called “index” that is given as the initial page, if there are no configurations, which we saw in step 3.
    • /static/main.css- file for styles. As in the previous project, everything here is up to you, paint as you wish.
    • /static/jquery-3.6.0.min.js- frontend dependency that our server will distribute as static.
  6. The "com.tictactoe" package will contain all the Java code. Right now there are 2 classes:
    • Sign- enum, which is responsible for the "cross / zero / void" .
    • Fieldis our field. This class has a "field" map . The principle of data storage will be as follows: the cells of the tic-tac-toe field are numbered from zero. In the first line 0, 1 and 2. In the second: 3, 4 and 5. And so on. There are also 3 methods. “getEmptyFieldIndex” looks for the first empty cell (yes, our opponent will not be very smart). "checkWin" checks if the game is over. If there is a row of three crosses, it returns a cross; if there is a row of three zeroes, it returns a zero. Otherwise, it's empty. "getFieldData" - returns the values ​​of the "field" map as a list sorted in ascending index order.
  7. The explanations about the template are finished, now you can start the task. Let's start by drawing a 3 by 3 table. To do this, add the following code to “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>
    We will then remove the numbers in the table and replace them with a cross, zero or an empty field. Also, inside the “head” tag, include the style file. To do this, add a line:<link href="static/main.css" rel="stylesheet">

    The content of the style file is up to you. I used this one:
    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;
    }
    After running, my result looks like this:
  8. Now let's add the following functionality: when a cell is clicked, a request will be sent to the server, in which we will pass the index of the cell that was clicked as a parameter. This task can be divided into two parts: send a request from the front, accept a request on the server. Let's start at the front for a change.

    Let's add an "onclick" parameter to each "d" tag . In the value, we indicate the change of the current page to the specified URL. The servlet that will be responsible for the logic will have the URL “/logic” . And it will take a parameter called “click” . So we will pass the index of the cell that the user clicked on.
    <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>
    You can check that everything is done correctly through the developer panel in the browser. For example, in Chrome, it opens with the F12 button . As a result of clicking on a cell with index 4, the picture will be as follows: We get an error because we have not yet created a servlet that can send the server to the address “logic” .
  9. In the package "com.tictactoe" create a class "LogicServlet" which should be derived from the class "javax.servlet.http.HttpServlet" . In the class, override the “doGet” method .

    And let's add a method that will get the index of the cell that was clicked. You also need to add a mapping (the address at which this servlet will intercept the request). I suggest doing this through an annotation (but if you like difficulties, you can also use web.xml). General servlet code:
    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;
        }
    
    }
    Now, when clicking on any cell, we will get the index of this cell on the server (you can make sure by running the server in debug). And there will be a redirect to the same page from which the click was made.
  10. Now we can click, but it's not a game yet. In order for the game to have logic, you need to save the state of the game (where the crosses are, where the zeros are) between requests. The easiest way to do this is to store this data in the session. With this approach, the session will be stored on the server, and the client will receive a session ID in a cookie named “JSESSIONID” . But the session does not need to be created every time, but only at the beginning of the game. Let's start another servlet for this, which we will call "InitServlet" . We will override the “doGet” method in it , in which we will create a new session, create a playing field, put this playing field and a list of type Sign in the session attributes, and send “forward” to the index.jsp page. Code:
    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);
        }
    }
    And not to forget, let's change the start page that opens in the browser after starting the server to “/start” : Now after restarting the server and clicking on any cell of the field in the browser developer menu in the “Request Headers” section , there will be a cookie with the session ID :
  11. When we have a repository in which we can store state between requests from the client (browser), we can start writing game logic. The logic we have is in “LogicServlet” . We need to work with the “doGet” method . Let's add this behavior to the method:
    • we will get the “field” object of the Field type from the session (we will take it out to the “extractField” method ).
    • put a cross where the user clicked (so far without any checks).
    @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;
    }
    The behavior has not changed yet, but if you start the server in debug and set a breakpoint on the line where the redirect is sent, you can see the “innards” of the “ data” object . There, indeed , “CROSS” appears under the index that was clicked.
  12. Now it's time to display the cross on the frontend. To do this, we will work with the “index.jsp” file and the “JSTL” technology .
    • In the <head> section add:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • In the table inside each <td> block, change the index to a construct that allows you to calculate values. For example, for index zero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Now, when you click on a cell, a cross will appear there:
  13. We have made our move, now it's the turn for the "zero". And let's add a couple of checks here, so that the signs are not placed in already occupied cells.
    • You need to check that the cell that was clicked is empty. Otherwise, we do nothing and send the user to the same page without changing the session parameters.
    • Since the number of cells on the field is odd, it is possible that a cross was placed, but there is no room for a zero. Therefore, after we put a cross, we try to get the index of an unoccupied cell (the getEmptyFieldIndex method of the Field class). If the index is not negative, then put a zero there. Code:
      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. At this stage, you can put crosses, AI answers with zeros. But there is no check when to stop the game. This can be in three cases:
    • after the next move of the cross, a line of three crosses was formed;
    • after the next return move with a zero, a line of three zeros was formed;
    • after the next move of the cross, empty cells ended.
    Let's add a method that checks if there are three crosses / zeroes in a row:
    /**
     * 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;
    }
    The peculiarity of this method is that if the winner is found, we add another parameter to the session, using which we will change the display in “index.jsp” in the following paragraphs.
  15. Let's add a call to the “checkWin ” method twice to the “doGet” method . The first time after setting the cross, the second - after setting the zero.
    // 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. In terms of behavior, almost nothing has changed (except that if one of the signs wins, zeros are no longer placed. Let's use the “winner” parameter in “index.jsp” and display the winner. We use directives after the table: 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>
    If crosses win, the message “CROSSES WIN!” , if the zeros are “NOUGHTS WIN!” . As a result, we can get one of two inscriptions:
  17. If there is a winner, you need to be able to take revenge. To do this, you need a button that will send a request to the server. And the server will invalidate the current session and redirect the request back to “/start” .
    • In “index.jsp” in the “head” section , write the script “jquery” . Using this library, we will send a request to the server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • In “index.jsp” in the “script” section , add a function that can send a POST request to the server. We will make the function synchronous, and when the response comes from the server, it will reload the current page.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Inside the “c:if” blocks , add a button that, when clicked, calls the function we just wrote:
      <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>
    • Let's create a new servlet that will serve the "/restart" URL .
      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");
          }
      }
      After the victory, the “Start again” button will appear . After clicking on it, the field will be completely cleared, and the game will start over.
  18. It remains to consider the last situation. What if the user put a cross, there was no victory, and there is no place for a zero? Then this is a draw, and we will process it now:
    • In the "LogicServlet" session, add another parameter "draw" , update the "data" field and send a redirect to "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;
      }
    • In "index.jsp" we will process this parameter:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      As a result of a draw, we will receive the corresponding message and an offer to start over:

This completes the writing of the game.

Code of classes and files with which they worked

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

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

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