Hoy escribiremos un juego de tres en raya usando servlets y JSP.

Este proyecto será un poco diferente a los anteriores. Contendrá no solo tareas, sino también explicaciones de cómo hacerlas. Es decir, será un proyecto de la serie "CÓMO...".

Instrucción:

  1. Bifurcación del repositorio: https://github.com/CodeGymCC/project-servlet.git
  2. Descarga tu versión del proyecto a tu computadora.
  3. Configure el inicio de la aplicación en IDEA:
    • Alt + Shift + F9 -> Editar configuraciones… -> Alt + insertar -> tom (en la barra de búsqueda) -> Local.
    • Después de eso, debe hacer clic en "CONFIGURAR" e indicar dónde se descargó y descomprimió el archivo con Tomcat.
    • En la pestaña "Despliegue": Alt + insertar -> Artefacto… -> tres en raya: guerra explotada -> Aceptar.
    • En el campo "Contexto de la aplicación": deje solo "/" (barra).
    • Presione "APLICAR".
    • Cierra la ventana de configuración.
    • Realice la primera ejecución de prueba de la configuración personalizada. Si todo se hace correctamente, se abrirá su navegador predeterminado, en el que será:
  4. Abra el archivo "pom.xml" . Hay 2 dependencias en el bloque "dependencias" .
    • javax.servlet-apies responsable de la especificación de los servlets. El alcance "proporcionado" es necesario durante el desarrollo, pero no en tiempo de ejecución (Tomcat ya tiene esta dependencia en la carpeta lib).
    • jstl– puede ser considerado como un motor de plantillas.
  5. Hay 3 archivos en la carpeta "webapp" :
    • index.jsp- esta es nuestra plantilla (similar a la página HTML). Contendrá marcado y scripts. Es el archivo llamado “index” que se da como página inicial, si no hay configuraciones, que vimos en el paso 3.
    • /static/main.css- archivo para estilos. Como en el proyecto anterior, aquí todo depende de ti, pinta como quieras.
    • /static/jquery-3.6.0.min.js- Dependencia frontend que nuestro servidor distribuirá como estática.
  6. El paquete "com.tictactoe" contendrá todo el código Java. Ahora mismo hay 2 clases:
    • Sign- enum, que es responsable de la "cruz/cero/vacío" .
    • Fieldes nuestro campo. Esta clase tiene un mapa de "campo" . El principio de almacenamiento de datos será el siguiente: las celdas del campo tic-tac-toe se numeran desde cero. En la primera línea 0, 1 y 2. En la segunda: 3, 4 y 5. Y así sucesivamente. También hay 3 métodos. “getEmptyFieldIndex” busca la primera celda vacía (sí, nuestro oponente no será muy inteligente). "checkWin" comprueba si el juego ha terminado. Si hay una fila de tres cruces, devuelve una cruz; si hay una fila de tres ceros, devuelve un cero. De lo contrario, está vacío. "getFieldData" : devuelve los valores del mapa de "campo" como una lista ordenada en orden de índice ascendente.
  7. Las explicaciones sobre la plantilla han terminado, ahora puede comenzar la tarea. Comencemos dibujando una tabla de 3 por 3. Para hacer esto, agregue el siguiente código a “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>
    Luego eliminaremos los números de la tabla y los reemplazaremos con una cruz, un cero o un campo vacío. Además, dentro de la etiqueta "head", incluye el archivo de estilo. Para hacer esto, agregue una línea:<link href="static/main.css" rel="stylesheet">

    El contenido del archivo de estilo depende de usted. Usé este:
    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;
    }
    Después de ejecutar, mi resultado se ve así:
  8. Ahora agreguemos la siguiente funcionalidad: cuando se hace clic en una celda, se enviará una solicitud al servidor, en la que pasaremos el índice de la celda en la que se hizo clic como parámetro. Esta tarea se puede dividir en dos partes: enviar una solicitud desde el frente, aceptar una solicitud en el servidor. Empecemos por la parte delantera para variar.

    Agreguemos un parámetro "onclick" a cada etiqueta "d" . En el valor, indicamos el cambio de la página actual a la URL especificada. El servlet que será responsable de la lógica tendrá la URL “/logic” . Y tomará un parámetro llamado “clic” . Entonces pasaremos el índice de la celda en la que el usuario hizo clic.
    <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>
    Puedes comprobar que todo se hace correctamente a través del panel de desarrollador del navegador. Por ejemplo, en Chrome, se abre con el botón F12 . Como resultado de hacer clic en una celda con índice 4, la imagen será la siguiente: Obtenemos un error porque aún no hemos creado un servlet que pueda enviar el servidor a la dirección "lógica" .
  9. En el paquete "com.tictactoe", cree una clase "LogicServlet" que debe derivarse de la clase "javax.servlet.http.HttpServlet" . En la clase, invalide el método "doGet" .

    Y agreguemos un método que obtendrá el índice de la celda en la que se hizo clic. También debe agregar una asignación (la dirección en la que este servlet interceptará la solicitud). Sugiero hacerlo a través de una anotación (pero si le gustan las dificultades, también puede usar web.xml). Código de servlet general:
    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;
        }
    
    }
    Ahora, al hacer clic en cualquier celda, obtendremos el índice de esta celda en el servidor (puede asegurarse ejecutando el servidor en depuración). Y habrá una redirección a la misma página desde la que se hizo el clic.
  10. Ahora podemos hacer clic, pero todavía no es un juego. Para que el juego tenga lógica, debe guardar el estado del juego (dónde están las cruces, dónde están los ceros) entre solicitudes. La forma más fácil de hacer esto es almacenar estos datos en la sesión. Con este enfoque, la sesión se almacenará en el servidor y el cliente recibirá una ID de sesión en una cookie denominada "JSESSIONID" . Pero no es necesario crear la sesión cada vez, sino solo al comienzo del juego. Iniciemos otro servlet para esto, al que llamaremos "InitServlet" . Anularemos el método "doGet" en él , en el que crearemos una nueva sesión, crearemos un campo de juego, pondremos este campo de juego y una lista de tipo Iniciar sesión en los atributos de la sesión y enviar " reenviar" a index.jsp página. Código:
    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);
        }
    }
    Y no olvidemos, cambiemos la página de inicio que se abre en el navegador después de iniciar el servidor a “/start” : ahora, después de reiniciar el servidor y hacer clic en cualquier celda del campo en el menú del desarrollador del navegador en la sección “Solicitar encabezados” , habrá una cookie con el ID de sesión:
  11. Cuando tenemos un repositorio en el que podemos almacenar el estado entre las solicitudes del cliente (navegador), podemos comenzar a escribir la lógica del juego. La lógica que tenemos está en “LogicServlet” . Necesitamos trabajar con el método “doGet” . Agreguemos este comportamiento al método:
    • obtendremos el objeto “field” del tipo Field de la sesión (lo sacaremos al método “extractField” ).
    • coloque una cruz donde el usuario hizo clic (hasta ahora sin ningún control).
    @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;
    }
    El comportamiento aún no ha cambiado, pero si inicia el servidor en depuración y establece un punto de interrupción en la línea donde se envía la redirección, puede ver las "entradas" del objeto "datos " . Allí, de hecho, aparece "CROSS" debajo del índice en el que se hizo clic.
  12. Ahora es el momento de mostrar la cruz en la interfaz. Para ello, trabajaremos con el archivo “index.jsp” y la tecnología “JSTL” .
    • En la sección <head> agregue:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • En la tabla dentro de cada bloque <td>, cambie el índice a una construcción que le permita calcular valores. Por ejemplo, para el índice cero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Ahora, cuando haga clic en una celda, aparecerá una cruz allí:
  13. Hemos hecho nuestra movida, ahora es el turno del "cero". Y agreguemos un par de controles aquí, para que los letreros no se coloquen en celdas ya ocupadas.
    • Debe verificar que la celda en la que se hizo clic esté vacía. De lo contrario, no hacemos nada y enviamos al usuario a la misma página sin cambiar los parámetros de la sesión.
    • Dado que el número de celdas en el campo es impar, es posible que se haya colocado una cruz, pero no hay lugar para un cero. Por lo tanto, después de poner una cruz, tratamos de obtener el índice de una celda desocupada (el método getEmptyFieldIndex de la clase Field). Si el índice no es negativo, entonces ponga un cero allí. Código:
      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. En esta etapa, puede poner cruces, AI responde con ceros. Pero no hay verificación de cuándo detener el juego. Esto puede ser en tres casos:
    • después del siguiente movimiento de la cruz, se formó una línea de tres cruces;
    • después del siguiente movimiento de regreso con un cero, se formó una línea de tres ceros;
    • después del siguiente movimiento de la cruz, las celdas vacías terminaron.
    Agreguemos un método que verifique si hay tres cruces/ceros seguidos:
    /**
     * 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;
    }
    La peculiaridad de este método es que si se encuentra el ganador, agregamos otro parámetro a la sesión, con el cual cambiaremos la visualización en “index.jsp” en los siguientes párrafos.
  15. Agreguemos una llamada al método "checkWin " dos veces al método "doGet" . La primera vez después de establecer la cruz, la segunda, después de establecer el cero.
    // 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. En términos de comportamiento, casi nada ha cambiado (excepto que si uno de los signos gana, ya no se colocan ceros. Usemos el parámetro "ganador" en "index.jsp" y mostremos al ganador. Usamos directivas después de la tabla: 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>
    Si ganan los cruces, el mensaje “¡GANAN LOS CRUCES!” , si los ceros son “¡CERO GANAN!” . Como resultado, podemos obtener una de dos inscripciones:
  17. Si hay un ganador, debes poder vengarte. Para hacer esto, necesita un botón que envíe una solicitud al servidor. Y el servidor invalidará la sesión actual y redirigirá la solicitud a "/start" .
    • En “index.jsp” en la sección “head” , escriba el script “jquery” . Usando esta biblioteca, enviaremos una solicitud al servidor.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • En "index.jsp" en la sección "script" , agregue una función que pueda enviar una solicitud POST al servidor. Haremos que la función sea sincrónica, y cuando llegue una respuesta del servidor, volverá a cargar la página actual.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Dentro de los bloques "c:if" , agrega un botón que, cuando se hace clic, llama a la función que acabamos de escribir:
      <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>
    • Vamos a crear un nuevo servlet que sirva la 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");
          }
      }
      Tras la victoria, aparecerá el botón “Empezar de nuevo” . Después de hacer clic en él, el campo se borrará por completo y el juego comenzará de nuevo.
  18. Queda por considerar la última situación. ¿Qué pasa si el usuario puso una cruz, no hubo victoria y no hay lugar para un cero? Entonces esto es un empate, y lo procesaremos ahora:
    • En la sesión "LogicServlet" , agregue otro parámetro "draw" , actualice el campo "data" y envíe una redirección a "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;
      }
    • En "index.jsp" procesaremos este parámetro:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Como resultado de un sorteo, recibiremos el mensaje correspondiente y una oferta para empezar de nuevo:

Esto completa la escritura del juego.

Código de clases y archivos con los que trabajaron

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

ReiniciarServlet

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

índice.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>

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