Astăzi vom scrie un joc Tic-Tac-Toe folosind servlet-uri și JSP.

Acest proiect va fi puțin diferit de cele anterioare. Acesta va conține nu numai sarcini, ci și explicații despre cum să le faci. Adică va fi un proiect din seria „CUM SE...”.

Instructiuni:

  1. Furcă din depozit: https://github.com/CodeGymCC/project-servlet.git
  2. Descărcați versiunea dvs. a proiectului pe computer.
  3. Configurați lansarea aplicației în IDEA:
    • Alt + Shift + F9 -> Editare configurații... -> Alt + inserare -> tom (în bara de căutare) -> Local.
    • După aceea, trebuie să faceți clic pe „CONFIGURĂ” și să indicați unde a fost descărcată și dezambalată arhiva cu Tomcat.
    • În fila „Deployment”: Alt + insert -> Artefact... -> tic-tac-toe:war exploded -> OK.
    • În câmpul „Contextul aplicației”: lăsați doar „/” (bară oblică).
    • Apăsați „APLICA”.
    • Închideți fereastra de setări.
    • Efectuați prima rulare de probă a configurației personalizate. Dacă totul este făcut corect, se va deschide browserul implicit, în care va fi:
  4. Deschideți fișierul „pom.xml” . Există 2 dependențe în blocul „dependențe” .
    • javax.servlet-apieste responsabil pentru specificarea servlet-urilor. Domeniul de aplicare „furnizat” este necesar în timpul dezvoltării, dar nu este necesar în timpul execuției (Tomcat are deja această dependență în folderul lib).
    • jstl– poate fi considerat ca un motor de șablon.
  5. Există 3 fișiere în folderul „webapp” :
    • index.jsp- acesta este șablonul nostru (similar cu pagina HTML). Va conține markup și scripturi. Este fișierul numit „index” care este dat ca pagină inițială, dacă nu există configurații, pe care le-am văzut la pasul 3.
    • /static/main.css- fișier pentru stiluri. Ca și în proiectul anterior, totul aici depinde de tine, pictează după cum dorești.
    • /static/jquery-3.6.0.min.js- dependență de front-end pe care serverul nostru o va distribui ca static.
  6. Pachetul „com.tictactoe” va conține tot codul Java. Momentan sunt 2 clase:
    • Sign- enum, care este responsabil pentru "cruce / zero / gol" .
    • Fieldeste domeniul nostru. Această clasă are o hartă „de câmp” . Principiul stocării datelor va fi următorul: celulele câmpului tic-tac-toe sunt numerotate de la zero. În prima linie 0, 1 și 2. În a doua: 3, 4 și 5. Și așa mai departe. Există și 3 metode. „getEmptyFieldIndex” caută prima celulă goală (da, adversarul nostru nu va fi foarte inteligent). „checkWin” verifică dacă jocul sa încheiat. Dacă există un rând de trei cruci, returnează o cruce; dacă există un rând de trei zerouri, returnează un zero. Altfel, e gol. „getFieldData” - returnează valorile hărții „câmp” ca o listă sortată în ordine crescătoare a indexului.
  7. Explicațiile despre șablon s-au terminat, acum puteți începe sarcina. Să începem prin a desena un tabel 3 cu 3. Pentru a face acest lucru, adăugați următorul cod la „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>
    Apoi vom elimina numerele din tabel și le vom înlocui cu o cruce, zero sau un câmp gol. De asemenea, în interiorul etichetei „head”, includeți fișierul de stil. Pentru a face acest lucru, adăugați o linie:<link href="static/main.css" rel="stylesheet">

    Conținutul fișierului de stil depinde de dvs. Am folosit asta:
    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;
    }
    După alergare, rezultatul meu arată astfel:
  8. Acum să adăugăm următoarea funcționalitate: atunci când se face clic pe o celulă, va fi trimisă o solicitare către server, în care vom trece ca parametru indexul celulei pe care s-a făcut clic. Această sarcină poate fi împărțită în două părți: trimiteți o solicitare din față, acceptați o solicitare pe server. Să începem din față pentru o schimbare.

    Să adăugăm un parametru „onclick” la fiecare etichetă „d” . În valoare, indicăm schimbarea paginii curente la adresa URL specificată. Servletul care va fi responsabil pentru logică va avea adresa URL „/logic” . Și va fi nevoie de un parametru numit „clic” . Deci vom trece indexul celulei pe care a făcut clic utilizatorul.
    <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>
    Puteți verifica dacă totul este făcut corect prin panoul pentru dezvoltatori din browser. De exemplu, în Chrome, se deschide cu butonul F12 . Ca urmare a clicului pe o celulă cu index 4, imaginea va fi după cum urmează: Primim o eroare deoarece nu am creat încă un servlet care să poată trimite serverul la adresa „logică” .
  9. În pachetul "com.tictactoe" creați o clasă "LogicServlet" care ar trebui să fie derivată din clasa "javax.servlet.http.HttpServlet" . În clasă, suprascrieți metoda „doGet” .

    Și să adăugăm o metodă care va obține indexul celulei pe care s-a făcut clic. De asemenea, trebuie să adăugați o mapare (adresa la care acest servlet va intercepta cererea). Vă sugerez să faceți acest lucru printr-o adnotare (dar dacă vă plac dificultățile, puteți utiliza și web.xml). Cod 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;
        }
    
    }
    Acum, când facem clic pe orice celulă, vom obține indexul acestei celule pe server (vă puteți asigura rulând serverul în depanare). Și va exista o redirecționare către aceeași pagină de pe care a fost făcut clicul.
  10. Acum putem face clic, dar încă nu este un joc. Pentru ca jocul să aibă logică, trebuie să salvați starea jocului (unde sunt crucile, unde sunt zerourile) între cereri. Cel mai simplu mod de a face acest lucru este stocarea acestor date în sesiune. Cu această abordare, sesiunea va fi stocată pe server, iar clientul va primi un ID de sesiune într-un cookie numit „JSESSIONID” . Dar sesiunea nu trebuie creată de fiecare dată, ci doar la începutul jocului. Să începem un alt servlet pentru aceasta, pe care îl vom numi „InitServlet” . Vom suprascrie metoda „doGet” în ea , în care vom crea o nouă sesiune, vom crea un teren de joc, vom pune acest teren de joc și o listă de tip Sign in atributele sesiunii și vom trimite „ forward” la index.jsp pagină. Cod:
    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);
        }
    }
    Și să nu uităm, să schimbăm pagina de pornire care se deschide în browser după pornirea serverului în „/start” : Acum, după repornirea serverului și făcând clic pe orice celulă a câmpului din meniul dezvoltatorului browserului din secțiunea „Request Headers” , va exista un cookie cu ID-ul sesiunii:
  11. Când avem un depozit în care putem stoca starea dintre solicitările de la client (browser), putem începe să scriem logica jocului. Logica pe care o avem este în „LogicServlet” . Trebuie să lucrăm cu metoda „doGet” . Să adăugăm acest comportament la metodă:
    • vom obține obiectul „field” de tip Field din sesiune (il vom scoate la metoda „extractField” ).
    • puneți o cruce acolo unde utilizatorul a dat clic (până acum fără nicio verificare).
    @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;
    }
    Comportamentul nu s-a schimbat încă, dar dacă porniți serverul în depanare și setați un punct de întrerupere pe linia în care este trimisă redirecționarea, puteți vedea „interiorul” obiectului „date . Acolo, într-adevăr , „CROSS” apare sub indexul pe care s-a făcut clic.
  12. Acum este timpul să afișați crucea pe front. Pentru a face acest lucru, vom lucra cu fișierul „index.jsp” și tehnologia „JSTL” .
    • În secțiunea <head> adăugați:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • În tabelul din interiorul fiecărui bloc <td>, schimbați indexul într-un construct care vă permite să calculați valori. De exemplu, pentru indexul zero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Acum, când faceți clic pe o celulă, acolo va apărea o cruce:
  13. Ne-am făcut mișcarea, acum a venit rândul pentru „zero”. Și să adăugăm câteva verificări aici, astfel încât semnele să nu fie plasate în celulele deja ocupate.
    • Trebuie să verificați dacă celula pe care s-a făcut clic este goală. În caz contrar, nu facem nimic și trimitem utilizatorul pe aceeași pagină fără a modifica parametrii sesiunii.
    • Deoarece numărul de celule de pe câmp este impar, este posibil să fi fost plasată o cruce, dar nu există loc pentru un zero. Prin urmare, după ce punem o cruce, încercăm să obținem indexul unei celule neocupate (metoda getEmptyFieldIndex a clasei Field). Dacă indicele nu este negativ, atunci puneți un zero acolo. Cod:
      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. În această etapă, puteți pune cruci, AI răspunde cu zerouri. Dar nu există nicio verificare când să opriți jocul. Acest lucru poate fi în trei cazuri:
    • după următoarea mutare a crucii s-a format o linie de trei cruci;
    • după următoarea mișcare de întoarcere cu un zero, s-a format o linie de trei zerouri;
    • după următoarea mutare a crucii, celulele goale s-au încheiat.
    Să adăugăm o metodă care verifică dacă există trei cruci / zerouri pe rând:
    /**
     * 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;
    }
    Particularitatea acestei metode este că, în cazul în care este găsit câștigătorul, adăugăm un alt parametru la sesiune, folosind care vom schimba afișarea în „index.jsp” în paragrafele următoare.
  15. Să adăugăm de două ori un apel la metoda „checkWin ” la metoda „doGet” . Prima dată după setarea crucii, a doua - după setarea zeroului.
    // 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. În ceea ce privește comportamentul, aproape nimic nu s-a schimbat (cu excepția faptului că dacă unul dintre semne câștigă, nu se mai pun zerouri. Să folosim parametrul „winner” în „index.jsp” și să afișăm câștigătorul. Folosim directive după tabel: 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>
    Dacă încrucișările câștigă, mesajul „CROSSES WIN!” , dacă zerourile sunt „NOUGHTS WIN!” . Ca rezultat, putem obține una dintre cele două inscripții:
  17. Dacă există un câștigător, trebuie să poți să te răzbuni. Pentru a face acest lucru, aveți nevoie de un buton care va trimite o solicitare către server. Și serverul va invalida sesiunea curentă și va redirecționa cererea înapoi la „/start” .
    • În „index.jsp” în secțiunea „head” , scrieți scriptul „jquery” . Folosind această bibliotecă, vom trimite o cerere către server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • În „index.jsp” în secțiunea „script” , adăugați o funcție care poate trimite o solicitare POST către server. Vom face funcția sincronă, iar când vine un răspuns de la server, acesta va reîncărca pagina curentă.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • În blocurile „c:if” , adăugați un buton care, atunci când este apăsat, apelează funcția pe care tocmai am scris-o:
      <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>
    • Să creăm un nou servlet care va difuza adresa 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");
          }
      }
      După victorie, va apărea butonul „Începe din nou” . După ce faceți clic pe el, câmpul va fi complet golit, iar jocul va începe de la capăt.
  18. Rămâne de luat în considerare ultima situație. Ce se întâmplă dacă utilizatorul pune o cruce, nu a existat nicio victorie și nu există loc pentru un zero? Atunci aceasta este o remiză și o vom procesa acum:
    • În sesiunea „LogicServlet” , adăugați un alt parametru „draw” , actualizați câmpul „date” și trimiteți o redirecționare către „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;
      }
    • În „index.jsp” vom procesa acest parametru:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      În urma unei trageri la sorți, vom primi mesajul corespunzător și o ofertă de a începe de la capăt:

Acest lucru completează scrierea jocului.

Codul claselor și fișierelor cu care au lucrat

InitServlet

package com.tictactoe;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;
import java.util.Map;

@WebServlet(name = "InitServlet", value = "/start")
public class InitServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Create a new session
        HttpSession currentSession = req.getSession(true);

        // Create a playing field
        Field field = new Field();
        Map<Integer, Sign> fieldData = field.getField();

        // Get a list of field values
        List<Sign> data = field.getFieldData();

        // Adding field parameters to the session (needed to store state between requests)
        currentSession.setAttribute("field", field);
        // and field values ​​sorted by index (required for drawing crosses and zeroes)
        currentSession.setAttribute("data", data);

        // Redirect request to index.jsp page via server
        getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
    }
}

LogicServlet

package com.tictactoe;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "LogicServlet", value = "/logic")
public class LogicServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();

        // Get the playfield object from the session
        Field field = extractField(currentSession);

        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
        Sign currentSign = field.getField().get(index);

        // Check if the clicked cell is empty.
        // Otherwise, we do nothing and send the user to the same page without changes
        // parameters in the session
        if (Sign.EMPTY != currentSign) {
            RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
            dispatcher.forward(req, resp);
            return;
        }

        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);

        // Check if the cross has won after adding the user's last click
        if (checkWin(resp, currentSession, field)) {
            return;
        }

        // Get an empty field cell
        int emptyFieldIndex = field.getEmptyFieldIndex();

        if (emptyFieldIndex >= 0) {
            field.getField().put(emptyFieldIndex, Sign.NOUGHT);
            // Check if the zero won after adding the last zero
            if (checkWin(resp, currentSession, field)) {
                return;
            }
        }
        // If there is no empty cell and no one wins, then it's a draw
        else {
            // Add a flag to the session that signals that a draw has occurred
            currentSession.setAttribute("draw", true);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            resp.sendRedirect("/index.jsp");
            return;
        }

        // Read the list of icons
        List<Sign> data = field.getFieldData();

        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);

        resp.sendRedirect("/index.jsp");
    }

    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }

    private int getSelectedIndex(HttpServletRequest request) {
        String click = request.getParameter("click");
        boolean isNumeric = click.chars().allMatch(Character::isDigit);
        return isNumeric ? Integer.parseInt(click) : 0;
    }

    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
}

RestartServlet

package com.tictactoe;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "RestartServlet", value = "/restart")
public class RestartServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        req.getSession().invalidate();
        resp.sendRedirect("/start");
    }
}

index.jsp _

<%@ page import="com.tictactoe.Sign" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<!DOCTYPE html>
<html>
<head>
    <link href="static/main.css" rel="stylesheet">
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    <title>Tic-Tac-Toe</title>
</head>
<body>
<h1>Tic-Tac-Toe</h1>

<table>
    <tr>
        <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td>
        <td onclick="window.location='/logic?click=1'">${data.get(1).getSign()}</td>
        <td onclick="window.location='/logic?click=2'">${data.get(2).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=3'">${data.get(3).getSign()}</td>
        <td onclick="window.location='/logic?click=4'">${data.get(4).getSign()}</td>
        <td onclick="window.location='/logic?click=5'">${data.get(5).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=6'">${data.get(6).getSign()}</td>
        <td onclick="window.location='/logic?click=7'">${data.get(7).getSign()}</td>
        <td onclick="window.location='/logic?click=8'">${data.get(8).getSign()}</td>
    </tr>
</table>

<hr>
<c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
<c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>

<c:if test="${winner == CROSSES}">
    <h1>CROSSES WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${winner == NOUGHTS}">
    <h1>NOUGHTS WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${draw}">
    <h1>IT'S A DRAW</h1>
    <button onclick="restart()">Start again</button>
</c:if>

<script>
    function restart() {
        $.ajax({
            url: '/restart',
            type: 'POST',
            contentType: 'application/json;charset=UTF-8',
            async: false,
            success: function () {
                location.reload();
            }
        });
    }
</script>

</body>
</html>

main.css _

td {
    border: 3px solid black;
    padding: 10px;
    border-collapse: separate;
    margin: 10px;
    width: 100px;
    height: 100px;
    font-size: 50px;
    text-align: center;
    empty-cells: show;
   }