Oggi scriveremo un gioco Tic-Tac-Toe utilizzando servlet e JSP.

Questo progetto sarà un po' diverso dai precedenti. Conterrà non solo compiti, ma anche spiegazioni su come eseguirli. Cioè, sarà un progetto della serie "HOW TO ...".

Istruzione:

  1. Fork dal repository: https://github.com/CodeGymCC/project-servlet.git
  2. Scarica la tua versione del progetto sul tuo computer.
  3. Impostare l'avvio dell'applicazione in IDEA:
    • Alt + Maiusc + F9 -> Modifica configurazioni… -> Alt + inserisci -> tom (nella barra di ricerca) -> Locale.
    • Successivamente, è necessario fare clic su "CONFIGURA" e indicare dove è stato scaricato e decompresso l'archivio con Tomcat.
    • Nella scheda "Dispiegamento": Alt + insert -> Artefatto... -> tic-tac-toe:war esploso -> OK.
    • Nel campo "Contesto applicazione": lasciare solo "/" (barra).
    • Premere "APPLICA".
    • Chiudi la finestra delle impostazioni.
    • Eseguire la prima esecuzione di prova della configurazione personalizzata. Se tutto è fatto correttamente, si aprirà il tuo browser predefinito, in cui sarà:
  4. Apri il file "pom.xml" . Ci sono 2 dipendenze nel blocco "dipendenze" .
    • javax.servlet-apiè responsabile della specifica dei servlet. L'ambito "fornito" è necessario durante lo sviluppo, ma non necessario in fase di esecuzione (Tomcat ha già questa dipendenza nella cartella lib).
    • jstl– può essere considerato come un motore di template.
  5. Ci sono 3 file nella cartella "webapp" :
    • index.jsp- questo è il nostro modello (simile alla pagina HTML). Conterrà markup e script. È il file chiamato “index” che viene dato come pagina iniziale, se non ci sono configurazioni, che abbiamo visto nel passaggio 3.
    • /static/main.css- file per gli stili. Come nel progetto precedente, tutto qui dipende da te, dipingi come desideri.
    • /static/jquery-3.6.0.min.js- dipendenza frontend che il nostro server distribuirà come statica.
  6. Il pacchetto "com.tictactoe" conterrà tutto il codice Java. Al momento ci sono 2 classi:
    • Sign- enum, che è responsabile del "cross / zero / void" .
    • Fieldè il nostro campo Questa classe ha una mappa "campo" . Il principio dell'archiviazione dei dati sarà il seguente: le celle del campo tris sono numerate da zero. Nella prima riga 0, 1 e 2. Nella seconda: 3, 4 e 5. E così via. Ci sono anche 3 metodi. "getEmptyFieldIndex" cerca la prima cella vuota (sì, il nostro avversario non sarà molto intelligente). "checkWin" controlla se il gioco è finito. Se c'è una riga di tre croci, restituisce una croce; se c'è una riga di tre zeri, restituisce uno zero. Altrimenti, è vuoto. "getFieldData" - restituisce i valori della mappa "campo" come un elenco ordinato in ordine crescente di indice.
  7. Le spiegazioni sul modello sono terminate, ora puoi iniziare l'attività. Iniziamo disegnando una tabella 3 per 3. Per fare ciò, aggiungi il seguente codice 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>
    Rimuoveremo quindi i numeri nella tabella e li sostituiremo con una croce, zero o un campo vuoto. Inoltre, all'interno del tag "head", includi il file di stile. Per fare ciò, aggiungi una riga:<link href="static/main.css" rel="stylesheet">

    Il contenuto del file di stile dipende da te. io ho usato questo:
    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;
    }
    Dopo l'esecuzione, il mio risultato è simile a questo:
  8. Ora aggiungiamo la seguente funzionalità: quando si fa clic su una cella, verrà inviata una richiesta al server, in cui passeremo l'indice della cella che è stata cliccata come parametro. Questa attività può essere suddivisa in due parti: inviare una richiesta dal fronte, accettare una richiesta sul server. Cominciamo dalla parte anteriore per un cambiamento.

    Aggiungiamo un parametro "onclick" a ciascun tag "d" . Nel valore, indichiamo la modifica della pagina corrente all'URL specificato. Il servlet che sarà responsabile della logica avrà l'URL “/logic” . E ci vorrà un parametro chiamato "click" . Quindi passeremo l'indice della cella su cui l'utente ha fatto 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>
    Puoi verificare che tutto sia stato eseguito correttamente tramite il pannello degli sviluppatori nel browser. Ad esempio, in Chrome, si apre con il pulsante F12 . Come risultato del clic su una cella con indice 4, l'immagine sarà la seguente: Otteniamo un errore perché non abbiamo ancora creato un servlet che possa inviare il server all'indirizzo "logica" .
  9. Nel pacchetto "com.tictactoe" creare una classe "LogicServlet" che dovrebbe derivare dalla classe "javax.servlet.http.HttpServlet" . Nella classe, eseguire l'override del metodo "doGet" .

    E aggiungiamo un metodo che otterrà l'indice della cella su cui è stato fatto clic. È inoltre necessario aggiungere una mappatura (l'indirizzo a cui questo servlet intercetterà la richiesta). Suggerisco di farlo attraverso un'annotazione (ma se ti piacciono le difficoltà, puoi anche usare web.xml). Codice servlet generale:
    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;
        }
    
    }
    Ora, quando si fa clic su qualsiasi cella, otterremo l'indice di questa cella sul server (puoi assicurarti eseguendo il server in debug). E ci sarà un reindirizzamento alla stessa pagina da cui è stato effettuato il clic.
  10. Ora possiamo fare clic, ma non è ancora un gioco. Affinché il gioco abbia una logica, devi salvare lo stato del gioco (dove sono le croci, dove sono gli zeri) tra le richieste. Il modo più semplice per farlo è memorizzare questi dati nella sessione. Con questo approccio, la sessione verrà archiviata sul server e il client riceverà un ID di sessione in un cookie denominato "JSESSIONID" . Ma la sessione non deve essere creata ogni volta, ma solo all'inizio del gioco. Iniziamo un altro servlet per questo, che chiameremo "InitServlet" . Sovrascriveremo il metodo "doGet" in esso , in cui creeremo una nuova sessione, creeremo un campo di gioco, inseriremo questo campo di gioco e un elenco di tipo Accedi agli attributi della sessione e invieremo "avanti " a index.jsp pagina. Codice:
    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);
        }
    }
    E per non dimenticare, cambiamo la pagina iniziale che si apre nel browser dopo aver avviato il server in "/start" : Ora dopo aver riavviato il server e fatto clic su qualsiasi cella del campo nel menu sviluppatore del browser nella sezione "Richiedi intestazioni" , ci sarà un cookie con l'ID di sessione:
  11. Quando disponiamo di un repository in cui possiamo memorizzare lo stato tra le richieste del client (browser), possiamo iniziare a scrivere la logica del gioco. La logica che abbiamo è in "LogicServlet" . Dobbiamo lavorare con il metodo "doGet" . Aggiungiamo questo comportamento al metodo:
    • otterremo dalla sessione l' oggetto “field” di tipo Field (lo porteremo al metodo “extractField” ).
    • mettere una croce dove l'utente ha cliccato (finora senza alcun controllo).
    @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;
    }
    Il comportamento non è ancora cambiato, ma se si avvia il server in debug e si imposta un punto di interruzione sulla riga in cui viene inviato il reindirizzamento, è possibile vedere le "viscere" dell'oggetto "dati " . Lì, infatti, appare “CROSS” sotto l'indice che è stato cliccato.
  12. Ora è il momento di visualizzare la croce sul frontend. Per fare ciò, lavoreremo con il file "index.jsp" e la tecnologia "JSTL" .
    • Nella sezione <head> aggiungi:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Nella tabella all'interno di ciascun blocco <td>, modificare l'indice in un costrutto che consente di calcolare i valori. Ad esempio, per l'indice zero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> ora, quando fai clic su una cella, apparirà una croce:
  13. Abbiamo fatto la nostra mossa, ora tocca allo "zero". E aggiungiamo qui un paio di controlli, in modo che i segni non vengano posizionati in celle già occupate.
    • È necessario verificare che la cella su cui è stato fatto clic sia vuota. In caso contrario, non facciamo nulla e inviamo l'utente alla stessa pagina senza modificare i parametri della sessione.
    • Poiché il numero di celle sul campo è dispari, è possibile che sia stata inserita una croce, ma non c'è spazio per uno zero. Pertanto, dopo aver messo una crocetta, proviamo ad ottenere l'indice di una cella non occupata (il metodo getEmptyFieldIndex della classe Field). Se l'indice non è negativo, metti uno zero lì. Codice:
      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 questo punto, puoi mettere delle crocette, AI risponde con zeri. Ma non c'è controllo quando fermare il gioco. Questo può avvenire in tre casi:
    • dopo il successivo movimento della croce, si formò una linea di tre croci;
    • dopo la successiva mossa di ritorno con uno zero, si è formata una linea di tre zeri;
    • dopo la prossima mossa della croce, le celle vuote finirono.
    Aggiungiamo un metodo che controlla se ci sono tre croci/zero di fila:
    /**
     * 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 particolarità di questo metodo è che se viene trovato il vincitore, aggiungiamo un altro parametro alla sessione, utilizzando il quale cambieremo la visualizzazione in "index.jsp" nei paragrafi successivi.
  15. Aggiungiamo due volte una chiamata al metodo "checkWin " al metodo "doGet" . La prima volta dopo aver impostato la croce, la seconda dopo aver impostato lo 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 termini di comportamento, non è cambiato quasi nulla (tranne che se uno dei segni vince, gli zeri non vengono più posizionati. Usiamo il parametro "winner" in "index.jsp" e mostriamo il vincitore. Usiamo le direttive dopo la tabella: 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>
    Se i cross vincono, il messaggio "CROSSES WIN!" , se gli zeri sono "NOUGHTS WIN!" . Di conseguenza, possiamo ottenere una delle due iscrizioni:
  17. Se c'è un vincitore, devi essere in grado di vendicarti. Per fare ciò, è necessario un pulsante che invierà una richiesta al server. E il server invaliderà la sessione corrente e reindirizzerà la richiesta a "/start" .
    • In “index.jsp” nella sezione “head” , scrivi lo script “jquery” . Utilizzando questa libreria, invieremo una richiesta al server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • In "index.jsp" nella sezione "script" , aggiungi una funzione che può inviare una richiesta POST al server. Renderemo la funzione sincrona e quando arriverà una risposta dal server, ricaricherà la pagina corrente.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • All'interno dei blocchi “c:if” , aggiungiamo un pulsante che, se cliccato, chiama la funzione che abbiamo appena scritto:
      <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>
    • Creiamo un nuovo servlet che servirà 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");
          }
      }
      Dopo la vittoria, apparirà il pulsante "Ricomincia" . Dopo aver fatto clic su di esso, il campo verrà completamente cancellato e il gioco ricomincerà.
  18. Resta da considerare l'ultima situazione. Cosa succede se l'utente mette una croce, non c'è stata vittoria e non c'è posto per uno zero? Quindi questo è un pareggio e lo elaboreremo ora:
    • Nella sessione "LogicServlet" , aggiungi un altro parametro "draw" , aggiorna il campo "data" e invia un reindirizzamento 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;
      }
    • In "index.jsp" elaboreremo questo parametro:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      A seguito di un sorteggio, riceveremo il messaggio corrispondente e un'offerta per ricominciare:

Questo completa la scrittura del gioco.

Codice di classi e file con cui hanno lavorato

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

Servlet logico

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

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

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