I dag skal vi skrive et Tic-Tac-Toe-spill ved å bruke servlets og JSP.

Dette prosjektet vil være litt annerledes enn de forrige. Den vil ikke bare inneholde oppgaver, men også forklaringer på hvordan du gjør dem. Det vil si at det blir et prosjekt fra «HOW TO ...»-serien.

Instruksjon:

  1. Fork fra depotet: https://github.com/CodeGymCC/project-servlet.git
  2. Last ned din versjon av prosjektet til datamaskinen din.
  3. Konfigurer applikasjonsstart i IDEA:
    • Alt + Shift + F9 -> Rediger konfigurasjoner... -> Alt + sett inn -> tom (i søkefeltet) -> Lokalt.
    • Etter det må du klikke på "KONFIGURER" og angi hvor arkivet med Tomcat ble lastet ned og pakket ut.
    • I "Deployment"-fanen: Alt + insert -> Artifact... -> tic-tac-toe:war eksploderte -> OK.
    • I feltet "Applikasjonskontekst": la bare "/" (skråstrek).
    • Trykk "SØK".
    • Lukk innstillingsvinduet.
    • Gjør den første testkjøringen av den tilpassede konfigurasjonen. Hvis alt er gjort riktig, åpnes standardnettleseren din, der den vil være:
  4. Åpne "pom.xml" -filen . Det er 2 avhengigheter i blokken "avhengigheter" .
    • javax.servlet-apier ansvarlig for spesifikasjonen av servlets. Omfang "forutsatt" er nødvendig under utvikling, men ikke nødvendig under kjøring (Tomcat har allerede denne avhengigheten i lib-mappen).
    • jstl– kan betraktes som en malmotor.
  5. Det er 3 filer i "webapp" -mappen :
    • index.jsp- dette er malen vår (lik HTML-siden). Den vil inneholde markeringer og skript. Det er filen kalt "indeks" som er gitt som startside, hvis det ikke er noen konfigurasjoner, som vi så i trinn 3.
    • /static/main.css- fil for stiler. Som i forrige prosjekt er alt her opp til deg, mal som du ønsker.
    • /static/jquery-3.6.0.min.js- frontend-avhengighet som serveren vår vil distribuere som statisk.
  6. "com.tictactoe" -pakken vil inneholde all Java-koden. Akkurat nå er det 2 klasser:
    • Sign- enum, som er ansvarlig for "krysset / null / tomrommet" .
    • Fielder vårt felt. Denne klassen har et "felt" -kart . Prinsippet for datalagring vil være som følger: cellene i tic-tac-toe-feltet er nummerert fra null. I den første linjen 0, 1 og 2. I den andre: 3, 4 og 5. Og så videre. Det er også 3 metoder. "getEmptyFieldIndex" ser etter den første tomme cellen (ja, motstanderen vår vil ikke være særlig smart). "checkWin" sjekker om spillet er over. Hvis det er en rad med tre kryss, returnerer den et kryss; hvis det er en rad med tre nuller, returnerer den en null. Ellers er det tomt. "getFieldData" - returnerer verdiene til "felt" -kartet som en liste sortert i stigende indeksrekkefølge.
  7. Forklaringene om malen er ferdige, nå kan du starte oppgaven. La oss starte med å tegne en tabell på 3 x 3. For å gjøre dette, legg til følgende kode i "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>
    Vi vil da fjerne tallene i tabellen og erstatte dem med et kryss, null eller et tomt felt. Inne i "head"-taggen inkluderer du også stilfilen. For å gjøre dette, legg til en linje:<link href="static/main.css" rel="stylesheet">

    Innholdet i stilfilen er opp til deg. Jeg brukte denne:
    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;
    }
    Etter å ha løpt ser resultatet mitt slik ut:
  8. La oss nå legge til følgende funksjonalitet: når en celle klikkes, vil en forespørsel sendes til serveren, der vi sender indeksen til cellen som ble klikket som en parameter. Denne oppgaven kan deles inn i to deler: send en forespørsel fra forsiden, godta en forespørsel på serveren. La oss starte foran for en forandring.

    La oss legge til en "onclick" -parameter til hver "d" -tag . I verdien angir vi endringen av gjeldende side til den angitte URL-adressen. Servleten som vil være ansvarlig for logikken vil ha URLen "/logic" . Og det vil ta en parameter kalt "klikk" . Så vi sender indeksen til cellen som brukeren klikket på.
    <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>
    Du kan sjekke at alt er gjort riktig gjennom utviklerpanelet i nettleseren. For eksempel, i Chrome, åpnes den med F12 -knappen . Som et resultat av å klikke på en celle med indeks 4, vil bildet være som følger: Vi får en feilmelding fordi vi ennå ikke har opprettet en servlet som kan sende serveren til adressen "logikk" .
  9. I pakken "com.tictactoe" oppretter du en klasse "LogicServlet" som skal være avledet fra klassen "javax.servlet.http.HttpServlet" . I klassen, overstyr «doGet» -metoden .

    Og la oss legge til en metode som vil få indeksen til cellen som ble klikket på. Du må også legge til en tilordning (adressen der denne servleten vil avskjære forespørselen). Jeg foreslår at du gjør dette gjennom en merknad (men hvis du liker vanskeligheter, kan du også bruke web.xml). Generell servletkode:
    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;
        }
    
    }
    Nå, når du klikker på en hvilken som helst celle, vil vi få indeksen til denne cellen på serveren (du kan være sikker ved å kjøre serveren i feilsøking). Og det vil være en omdirigering til samme side som klikket ble gjort fra.
  10. Nå kan vi klikke, men det er ikke et spill ennå. For at spillet skal ha logikk, må du lagre tilstanden til spillet (hvor kryssene er, hvor nullene er) mellom forespørsler. Den enkleste måten å gjøre dette på er å lagre disse dataene i økten. Med denne tilnærmingen vil økten bli lagret på serveren, og klienten vil motta en sesjons-ID i en informasjonskapsel kalt "JSESSIONID" . Men økten trenger ikke å opprettes hver gang, men bare i begynnelsen av spillet. La oss starte en annen servlet for dette, som vi vil kalle "InitServlet" . Vi vil overstyre "doGet" -metoden i den , der vi vil opprette en ny økt, lage en spillebane, sette denne spillefeltet og en liste med type Logg inn sesjonsattributtene, og sende " forward" til index.jsp side. Kode:
    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);
        }
    }
    Og for ikke å glemme, la oss endre startsiden som åpnes i nettleseren etter å ha startet serveren til "/start" : Nå etter å ha startet serveren på nytt og klikket på en hvilken som helst celle i feltet i nettleserutviklermenyen i delen "Request Headers" , vil det være en informasjonskapsel med økt-IDen:
  11. Når vi har et depot der vi kan lagre tilstand mellom forespørsler fra klienten (nettleseren), kan vi begynne å skrive spilllogikk. Logikken vi har er i "LogicServlet" . Vi må jobbe med "doGet" -metoden . La oss legge til denne oppførselen til metoden:
    • vi får "field" -objektet av Field-typen fra sesjonen (vi tar det ut til "extractField" -metoden ).
    • sette et kryss der brukeren klikket (så langt uten noen haker).
    @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;
    }
    Atferden har ikke endret seg ennå, men hvis du starter serveren i feilsøking og setter et bruddpunkt på linjen der omdirigeringen sendes, kan du se "innmaten" til "data" -objektet . Der vises faktisk "CROSS" under indeksen som ble klikket på.
  12. Nå er det på tide å vise korset på fronten. For å gjøre dette, vil vi jobbe med «index.jsp» -filen og «JSTL» -teknologien .
    • Legg til i <head>-delen:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • I tabellen inne i hver <td>-blokk endrer du indeksen til en konstruksjon som lar deg beregne verdier. For eksempel, for indeks null: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Nå, når du klikker på en celle, vil et kryss vises der:
  13. Vi har tatt grep, nå er det tur til "nullen". Og la oss legge til et par sjekker her, slik at skiltene ikke plasseres i allerede okkuperte celler.
    • Du må sjekke at cellen som ble klikket på er tom. Ellers gjør vi ingenting og sender brukeren til samme side uten å endre sesjonsparametrene.
    • Siden antallet celler på feltet er oddetall, er det mulig at det ble plassert et kryss, men det er ikke plass til en null. Derfor, etter at vi har satt et kryss, prøver vi å få indeksen til en ledig celle (getEmptyFieldIndex-metoden til Field-klassen). Hvis indeksen ikke er negativ, sett en null der. Kode:
      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. På dette stadiet kan du sette kryss, svarer AI med nuller. Men det er ingen sjekk når du skal stoppe spillet. Dette kan være i tre tilfeller:
    • etter neste trekk av korset ble det dannet en linje med tre kors;
    • etter neste returtrekk med null, ble det dannet en linje med tre nuller;
    • etter neste trekk av korset, endte tomme celler.
    La oss legge til en metode som sjekker om det er tre kryss/nuller på rad:
    /**
     * 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;
    }
    Det særegne med denne metoden er at hvis vinneren blir funnet, legger vi til en annen parameter til økten, ved hjelp av denne vil vi endre visningen i "index.jsp" i de følgende avsnittene.
  15. La oss legge til et kall til "checkWin "-metoden to ganger til "doGet"-metoden . Første gang etter å ha satt krysset, den andre - etter å ha satt null.
    // 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år det gjelder oppførsel, har nesten ingenting endret seg (bortsett fra at hvis ett av tegnene vinner, plasseres ikke lenger nuller. La oss bruke «vinner» -parameteren i «index.jsp» og vise vinneren. Vi bruker direktiver etter tabellen: 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>
    Hvis kryss vinner, meldingen "KRYSS VINN!" , hvis nullene er "NOUGHTS WIN!" . Som et resultat kan vi få en av to inskripsjoner:
  17. Hvis det er en vinner, må du være i stand til å ta hevn. For å gjøre dette trenger du en knapp som sender en forespørsel til serveren. Og serveren vil ugyldiggjøre gjeldende økt og omdirigere forespørselen tilbake til "/start" .
    • I «index.jsp» i «head» -delen skriver du skriptet «jquery» . Ved å bruke dette biblioteket sender vi en forespørsel til serveren.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • I «index.jsp» i «script» -delen legger du til en funksjon som kan sende en POST-forespørsel til serveren. Vi vil gjøre funksjonen synkron, og når et svar kommer fra serveren, vil den laste inn gjeldende side på nytt.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Inne i "c:if" -blokkene legger du til en knapp som, når den klikkes, kaller funksjonen vi nettopp skrev:
      <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>
    • La oss lage en ny servlet som vil tjene "/restart" URL .
      package com.tictactoe;
      
      import javax.servlet.annotation.WebServlet;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      
      @WebServlet(name = "RestartServlet", value = "/restart")
      public class RestartServlet extends HttpServlet {
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
              req.getSession().invalidate();
              resp.sendRedirect("/start");
          }
      }
      Etter seieren vil "Start igjen" -knappen vises . Etter å ha klikket på det, vil feltet bli fullstendig tømt, og spillet vil starte på nytt.
  18. Det gjenstår å vurdere den siste situasjonen. Hva om brukeren setter et kryss, det ble ingen seier, og det er ikke plass til en null? Da er dette en trekning, og vi behandler den nå:
    • I "LogicServlet" -økten, legg til en annen parameter "draw" , oppdater "data" -feltet og send en omdirigering til "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;
      }
    • I "index.jsp" vil vi behandle denne parameteren:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Som et resultat av en trekning vil vi motta den tilsvarende meldingen og et tilbud om å starte på nytt:

Dette fullfører skrivingen av spillet.

Kode for klasser og filer som de jobbet med

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

StartServlet på nytt

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