Aujourd'hui, nous allons écrire un jeu Tic-Tac-Toe en utilisant des servlets et JSP.

Ce projet sera un peu différent des précédents. Il contiendra non seulement des tâches, mais aussi des explications sur la façon de les faire. Autrement dit, ce sera un projet de la série "HOW TO ...".

Instruction:

  1. Fork du référentiel : https://github.com/CodeGymCC/project-servlet.git
  2. Téléchargez votre version du projet sur votre ordinateur.
  3. Configurez le lancement de l'application dans IDEA :
    • Alt + Maj + F9 -> Modifier les configurations… -> Alt + insérer -> tom (dans la barre de recherche) -> Local.
    • Après cela, vous devez cliquer sur "CONFIGURER" et indiquer où l'archive avec Tomcat a été téléchargée et décompressée.
    • Dans l'onglet "Déploiement" : Alt + insert -> Artefact… -> tic-tac-toe:war éclaté -> OK.
    • Dans le champ « Application context » : ne laissez que « / » (slash).
    • Appuyez sur "APPLIQUER".
    • Fermez la fenêtre des paramètres.
    • Effectuez le premier test de la configuration personnalisée. Si tout est fait correctement, votre navigateur par défaut s'ouvrira, dans lequel ce sera :
  4. Ouvrez le fichier "pom.xml" . Il y a 2 dépendances dans le bloc "dépendances" .
    • javax.servlet-apiest responsable de la spécification des servlets. La portée "fournie" est nécessaire pendant le développement, mais pas au moment de l'exécution (Tomcat a déjà cette dépendance dans le dossier lib).
    • jstl– peut être considéré comme un moteur de template.
  5. Il y a 3 fichiers dans le dossier « webapp » :
    • index.jsp- c'est notre modèle (similaire à la page HTML). Il contiendra le balisage et les scripts. C'est le fichier appelé "index" qui est donné comme page initiale, s'il n'y a pas de configurations, ce que nous avons vu à l'étape 3.
    • /static/main.css- fichier pour les styles. Comme dans le projet précédent, tout ici dépend de vous, peignez comme vous le souhaitez.
    • /static/jquery-3.6.0.min.js- dépendance frontale que notre serveur distribuera en tant que statique.
  6. Le package "com.tictactoe" contiendra tout le code Java. Il y a actuellement 2 classes :
    • Sign- enum, qui est responsable de la "croix/zéro/vide" .
    • Fieldest notre domaine. Cette classe a une carte "champ" . Le principe de stockage des données sera le suivant : les cellules du champ tic-tac-toe sont numérotées à partir de zéro. Dans la première ligne 0, 1 et 2. Dans la seconde : 3, 4 et 5. Et ainsi de suite. Il existe également 3 méthodes. « getEmptyFieldIndex » cherche la première cellule vide (oui, notre adversaire ne sera pas très malin). "checkWin" vérifie si le jeu est terminé. S'il y a une ligne de trois croix, il renvoie une croix ; s'il y a une ligne de trois zéros, il renvoie un zéro. Sinon, c'est vide. "getFieldData" - renvoie les valeurs de la carte "champ" sous forme de liste triée par ordre d'index croissant.
  7. Les explications sur le modèle sont terminées, vous pouvez maintenant commencer la tâche. Commençons par dessiner un tableau 3 par 3. Pour cela, ajoutez le code suivant à « 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>
    Nous supprimerons alors les chiffres du tableau et les remplacerons par une croix, un zéro ou un champ vide. De plus, à l'intérieur de la balise "head", incluez le fichier de style. Pour cela, ajoutez une ligne :<link href="static/main.css" rel="stylesheet">

    Le contenu du fichier de style dépend de vous. J'ai utilisé celui-ci :
    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;
    }
    Après avoir exécuté, mon résultat ressemble à ceci:
  8. Ajoutons maintenant la fonctionnalité suivante : lorsqu'une cellule est cliquée, une requête sera envoyée au serveur, dans laquelle nous passerons l'index de la cellule qui a été cliquée en paramètre. Cette tâche peut être divisée en deux parties : envoyer une requête depuis le front, accepter une requête sur le serveur. Commençons par le devant pour changer.

    Ajoutons un paramètre "onclick" à chaque balise "d" . Dans la valeur, nous indiquons le changement de la page actuelle vers l'URL spécifiée. La servlet qui sera responsable de la logique aura l'URL "/logic" . Et il faudra un paramètre appelé "clic" . Nous allons donc passer l'index de la cellule sur laquelle l'utilisateur a cliqué.
    <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>
    Vous pouvez vérifier que tout est fait correctement via le panneau de développement du navigateur. Par exemple, dans Chrome, il s'ouvre avec le bouton F12 . À la suite d'un clic sur une cellule avec l'index 4, l'image sera la suivante : Nous obtenons une erreur car nous n'avons pas encore créé de servlet pouvant envoyer le serveur à l'adresse « logique » .
  9. Dans le package "com.tictactoe" créez une classe "LogicServlet" qui doit être dérivée de la classe "javax.servlet.http.HttpServlet" . Dans la classe, remplacez la méthode "doGet" .

    Et ajoutons une méthode qui obtiendra l'index de la cellule sur laquelle on a cliqué. Vous devez également ajouter un mapping (l'adresse à laquelle cette servlet interceptera la requête). Je suggère de le faire via une annotation (mais si vous aimez les difficultés, vous pouvez également utiliser web.xml). Code de servlet général :
    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;
        }
    
    }
    Maintenant, en cliquant sur n'importe quelle cellule, nous obtiendrons l'index de cette cellule sur le serveur (vous pouvez vous en assurer en exécutant le serveur en débogage). Et il y aura une redirection vers la même page à partir de laquelle le clic a été effectué.
  10. Maintenant, nous pouvons cliquer, mais ce n'est pas encore un jeu. Pour que le jeu ait une logique, vous devez enregistrer l'état du jeu (où se trouvent les croix, où se trouvent les zéros) entre les requêtes. Le moyen le plus simple consiste à stocker ces données dans la session. Avec cette approche, la session sera stockée sur le serveur et le client recevra un identifiant de session dans un cookie nommé "JSESSIONID" . Mais la session n'a pas besoin d'être créée à chaque fois, mais seulement au début du jeu. Démarrons une autre servlet pour cela, que nous appellerons "InitServlet" . Nous allons remplacer la méthode "doGet" dedans , dans laquelle nous allons créer une nouvelle session, créer un terrain de jeu, mettre ce terrain de jeu et une liste de type Sign dans les attributs de session, et envoyer " forward" au 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);
        }
    }
    Et pour ne pas oublier, changeons la page de démarrage qui s'ouvre dans le navigateur après le démarrage du serveur en "/start" : Maintenant, après avoir redémarré le serveur et cliqué sur n'importe quelle cellule du champ dans le menu développeur du navigateur dans la section "Request Headers" , il y aura un cookie avec l'identifiant de session :
  11. Lorsque nous avons un référentiel dans lequel nous pouvons stocker l'état entre les requêtes du client (navigateur), nous pouvons commencer à écrire la logique du jeu. La logique que nous avons est dans "LogicServlet" . Nous devons travailler avec la méthode "doGet" . Ajoutons ce comportement à la méthode :
    • nous obtiendrons l' objet "field" du type Field de la session (nous le sortirons de la méthode "extractField" ).
    • mettre une croix là où l'utilisateur a cliqué (jusqu'à présent sans aucune vérification).
    @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;
    }
    Le comportement n'a pas encore changé, mais si vous démarrez le serveur en débogage et définissez un point d'arrêt sur la ligne où la redirection est envoyée, vous pouvez voir les "entrailles" de l'objet "données " . Là, en effet , "CROSS" apparaît sous l'index qui a été cliqué.
  12. Il est maintenant temps d'afficher la croix sur le frontend. Pour ce faire, nous allons travailler avec le fichier « index.jsp » et la technologie « JSTL » .
    • Dans la section <head> ajoutez :<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Dans le tableau à l'intérieur de chaque bloc <td>, remplacez l'index par une construction qui vous permet de calculer des valeurs. Par exemple, pour l'index zéro : <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Désormais, lorsque vous cliquerez sur une cellule, une croix y apparaîtra :
  13. Nous avons fait notre coup, maintenant c'est au tour du "zéro". Et ajoutons quelques vérifications ici, afin que les panneaux ne soient pas placés dans des cellules déjà occupées.
    • Vous devez vérifier que la cellule sur laquelle vous avez cliqué est vide. Sinon, nous ne faisons rien et renvoyons l'utilisateur vers la même page sans modifier les paramètres de session.
    • Puisque le nombre de cellules sur le terrain est impair, il est possible qu'une croix ait été placée, mais il n'y a pas de place pour un zéro. Par conséquent, après avoir mis une croix, nous essayons d'obtenir l'index d'une cellule inoccupée (la méthode getEmptyFieldIndex de la classe Field). Si l'indice n'est pas négatif, mettez-y un zéro. 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. A ce stade, vous pouvez mettre des croix, l'IA répond par des zéros. Mais il n'y a pas de contrôle quand arrêter le jeu. Cela peut être dans trois cas :
    • après le mouvement suivant de la croix, une ligne de trois croix a été formée;
    • après le prochain mouvement de retour avec un zéro, une ligne de trois zéros s'est formée;
    • après le prochain mouvement de la croix, les cellules vides se sont terminées.
    Ajoutons une méthode qui vérifie s'il y a trois croix/zéros d'affilée :
    /**
     * 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 particularité de cette méthode est que si le gagnant est trouvé, nous ajoutons un autre paramètre à la session, à l'aide duquel nous modifierons l'affichage dans « index.jsp » dans les paragraphes suivants.
  15. Ajoutons deux fois un appel à la méthode « checkWin » à la méthode « doGet » . La première fois après avoir réglé la croix, la seconde - après avoir réglé le zéro.
    // 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 termes de comportement, presque rien n'a changé (sauf que si l'un des signes gagne, les zéros ne sont plus placés. Utilisons le paramètre "winner" dans "index.jsp" et affichons le gagnant. Nous utilisons des directives après le tableau : 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 les croix gagnent, le message "CROSSES WIN!" , si les zéros sont « NOUGHTS WIN ! . En conséquence, nous pouvons obtenir l'une des deux inscriptions:
  17. S'il y a un gagnant, vous devez pouvoir vous venger. Pour ce faire, vous avez besoin d'un bouton qui enverra une requête au serveur. Et le serveur invalidera la session en cours et redirigera la requête vers "/start" .
    • Dans « index.jsp » dans la section « head » , écrivez le script « jquery » . En utilisant cette bibliothèque, nous enverrons une requête au serveur.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Dans « index.jsp » dans la section « script » , ajoutez une fonction qui peut envoyer une requête POST au serveur. Nous allons rendre la fonction synchrone, et lorsqu'une réponse viendra du serveur, elle rechargera la page en cours.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • À l'intérieur des blocs "c:if" , ajoutez un bouton qui, lorsqu'il est cliqué, appelle la fonction que nous venons d'écrire :
      <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>
    • Créons un nouveau servlet qui servira l' 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");
          }
      }
      Après la victoire, le bouton "Recommencer" apparaîtra . Après avoir cliqué dessus, le champ sera complètement effacé et le jeu recommencera.
  18. Il reste à considérer la dernière situation. Et si l'utilisateur mettait une croix, qu'il n'y avait pas de victoire et qu'il n'y avait pas de place pour un zéro ? Alors c'est un tirage au sort, et nous allons le traiter maintenant :
    • Dans la session "LogicServlet" , ajoutez un autre paramètre "draw" , mettez à jour le champ "data" et envoyez une redirection vers "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;
      }
    • Dans "index.jsp" nous traiterons ce paramètre :
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      A la suite d'un tirage au sort, nous recevrons le message correspondant et une offre pour recommencer :

Ceci termine l'écriture du jeu.

Code des classes et fichiers avec lesquels ils ont travaillé

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

RedémarrerServlet

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>

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