Idag kommer vi att skriva ett Tic-Tac-Toe-spel med servlets och JSP.

Detta projekt kommer att skilja sig lite från de tidigare. Den kommer att innehålla inte bara uppgifter, utan också förklaringar av hur man gör dem. Det vill säga att det blir ett projekt från "HUR MAN ..."-serien.

Instruktion:

  1. Gaffel från förvaret: https://github.com/CodeGymCC/project-servlet.git
  2. Ladda ner din version av projektet till din dator.
  3. Konfigurera programstart i IDEA:
    • Alt + Shift + F9 -> Redigera konfigurationer... -> Alt + infoga -> tom (i sökfältet) -> Lokalt.
    • Efter det måste du klicka på "KONFIGURERA" och ange var arkivet med Tomcat laddades ner och packades upp.
    • På fliken "Deployment": Alt + infoga -> Artefakt... -> tic-tac-toe:war exploderade -> OK.
    • I fältet "Ansökningssammanhang": lämna endast "/" (snedstreck).
    • Tryck på "APPLY".
    • Stäng inställningsfönstret.
    • Gör den första testkörningen av den anpassade konfigurationen. Om allt är gjort korrekt öppnas din standardwebbläsare, där det kommer att vara:
  4. Öppna filen "pom.xml" . Det finns 2 beroenden i "beroenden" -blocket .
    • javax.servlet-apiansvarar för specifikationen av servlets. Omfattning "förutsatt" behövs under utveckling, men behövs inte under körning (Tomcat har redan detta beroende i lib-mappen).
    • jstl– kan betraktas som en mallmotor.
  5. Det finns 3 filer i mappen "webapp" :
    • index.jsp- detta är vår mall (liknar HTML-sidan). Den kommer att innehålla uppmärkning och skript. Det är filen som heter "index" som ges som startsida, om det inte finns några konfigurationer, vilket vi såg i steg 3.
    • /static/main.css- fil för stilar. Precis som i förra projektet är allt här upp till dig, måla som du vill.
    • /static/jquery-3.6.0.min.js- gränssnittsberoende som vår server kommer att distribuera som statisk.
  6. Paketet "com.tictactoe" kommer att innehålla all Java-kod. Just nu finns det 2 klasser:
    • Sign- enum, som är ansvarig för "kryss / noll / tomrum" .
    • Fieldär vårt område. Den här klassen har en "fält" -karta . Principen för datalagring kommer att vara som följer: cellerna i tic-tac-toe-fältet är numrerade från noll. På den första raden 0, 1 och 2. På den andra: 3, 4 och 5. Och så vidare. Det finns också 3 metoder. "getEmptyFieldIndex" letar efter den första tomma cellen (ja, vår motståndare kommer inte att vara särskilt smart). "checkWin" kontrollerar om spelet är över. Om det finns en rad med tre kryss returnerar det ett kryss, om det finns en rad med tre nollor returnerar det en nolla. Annars är det tomt. "getFieldData" - returnerar värdena för "fält" -kartan som en lista sorterad i stigande indexordning.
  7. Förklaringarna om mallen är klara, nu kan du påbörja uppgiften. Låt oss börja med att rita en tabell med 3 gånger 3. För att göra detta, lägg till följande kod till "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 kommer då att ta bort siffrorna i tabellen och ersätta dem med ett kryss, nolla eller ett tomt fält. Inkludera även stilfilen i taggen "head". För att göra detta, lägg till en rad:<link href="static/main.css" rel="stylesheet">

    Innehållet i stilfilen är upp till dig. Jag använde denna:
    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;
    }
    Efter att ha kört ser mitt resultat ut så här:
  8. Låt oss nu lägga till följande funktionalitet: när en cell klickas, kommer en begäran att skickas till servern, där vi skickar indexet för cellen som klickades på som en parameter. Denna uppgift kan delas upp i två delar: skicka en förfrågan från fronten, acceptera en förfrågan på servern. Låt oss börja längst fram för en förändring.

    Låt oss lägga till en "onclick" -parameter till varje "d" -tagg . I värdet anger vi ändringen av den aktuella sidan till den angivna webbadressen. Servleten som kommer att ansvara för logiken kommer att ha URL:en " /logic" . Och det kommer att ta en parameter som heter "klick" . Så vi skickar indexet för cellen som användaren klickade 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 kontrollera att allt görs korrekt genom utvecklarpanelen i webbläsaren. Till exempel i Chrome öppnas den med F12 -knappen . Som ett resultat av att klicka på en cell med index 4 blir bilden som följer: Vi får ett felmeddelande eftersom vi ännu inte har skapat en servlet som kan skicka servern till adressen " logic" .
  9. I paketet "com.tictactoe" skapa en klass "LogicServlet" som ska härledas från klassen "javax.servlet.http.HttpServlet" . Åsidosätt metoden "doGet" i klassen .

    Och låt oss lägga till en metod som kommer att få indexet för cellen som klickades på. Du måste också lägga till en mappning (adressen där denna servlet kommer att avlyssna begäran). Jag föreslår att du gör detta genom en anteckning (men om du gillar svårigheter kan du också använda web.xml). Allmän servletkod:
    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är vi nu klickar på valfri cell kommer vi att få indexet för denna cell på servern (du kan vara säker genom att köra servern i debug). Och det blir en omdirigering till samma sida som klicket gjordes från.
  10. Nu kan vi klicka, men det är inte ett spel än. För att spelet ska ha logik måste du spara spelets tillstånd (där kryssen är, där nollorna finns) mellan förfrågningar. Det enklaste sättet att göra detta är att lagra dessa data i sessionen. Med detta tillvägagångssätt kommer sessionen att lagras på servern och klienten kommer att få ett sessions-ID i en cookie som heter " JSESSIONID" . Men sessionen behöver inte skapas varje gång, utan bara i början av spelet. Låt oss starta en annan servlet för detta, som vi kommer att kalla "InitServlet" . Vi kommer att åsidosätta "doGet" -metoden i den , där vi kommer att skapa en ny session, skapa en spelplan, lägga denna spelplan och en lista med typ Logga in sessionsattributen och skicka " framåt" till index.jsp sida. Koda:
    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);
        }
    }
    Och inte att förglömma, låt oss ändra startsidan som öppnas i webbläsaren efter att ha startat servern till " /start" : Nu efter att ha startat om servern och klickat på valfri cell i fältet i webbläsarens utvecklarmeny i avsnittet " Begärrubriker" , kommer det att finnas en cookie med sessions-ID:
  11. När vi har ett arkiv där vi kan lagra tillstånd mellan förfrågningar från klienten (webbläsaren), kan vi börja skriva spellogik. Logiken vi har är i "LogicServlet" . Vi måste arbeta med "doGet" -metoden . Låt oss lägga till detta beteende till metoden:
    • vi kommer att hämta "field" -objektet av typen Field från sessionen (vi tar ut det till "extractField" -metoden ).
    • sätt ett kryss där användaren klickade (hittills utan några kontroller).
    @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;
    }
    Beteendet har inte ändrats ännu, men om du startar servern i debug och ställer in en brytpunkt på raden dit omdirigeringen skickas, kan du se "insidan" av "data" -objektet . Där visas faktiskt "CROSS" under indexet som klickades på.
  12. Nu är det dags att visa korset på fronten. För att göra detta kommer vi att arbeta med filen "index.jsp" och teknologin "JSTL" .
    • I avsnittet <head> lägg till:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • I tabellen inuti varje <td>-block ändrar du indexet till en konstruktion som låter dig beräkna värden. Till exempel för index noll: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> När du nu klickar på en cell kommer ett kryss att visas där:
  13. Vi har gjort vårt drag, nu är det tur till "nollan". Och låt oss lägga till ett par kontroller här, så att skyltarna inte placeras i redan upptagna celler.
    • Du måste kontrollera att cellen som klickades på är tom. Annars gör vi ingenting och skickar användaren till samma sida utan att ändra sessionsparametrarna.
    • Eftersom antalet celler på fältet är udda, är det möjligt att ett kryss placerats, men det finns inget utrymme för en nolla. Därför, efter att vi satt ett kryss, försöker vi få indexet för en ledig cell (getEmptyFieldIndex-metoden för klassen Field). Om indexet inte är negativt, sätt en nolla där. Koda:
      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. I det här skedet kan du sätta kryss, svarar AI med nollor. Men det finns ingen kontroll när spelet ska stoppas. Detta kan vara i tre fall:
    • efter nästa drag av korset bildades en linje med tre kors;
    • efter nästa returdrag med en nolla bildades en rad med tre nollor;
    • efter nästa drag av korset, slutade tomma celler.
    Låt oss lägga till en metod som kontrollerar om det finns tre kryss/nollor i 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 speciella med denna metod är att om vinnaren hittas lägger vi till en annan parameter till sessionen, med hjälp av vilken vi kommer att ändra visningen i " index.jsp" i följande stycken.
  15. Låt oss lägga till ett anrop till metoden "checkWin " två gånger till metoden "doGet" . Första gången efter att ha satt krysset, den andra - efter att ha satt nollan.
    // 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 gäller beteende har nästan ingenting förändrats (förutom att om ett av tecknen vinner placeras inte nollor längre. Låt oss använda parametern “winner” i “index.jsp” och visa vinnaren. Vi använder direktiv efter 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>
    Om kryssen vinner, meddelandet "KORSA VINNER!" , om nollorna är "NOUGHTS WIN!" . Som ett resultat kan vi få en av två inskriptioner:
  17. Om det finns en vinnare måste du kunna ta revansch. För att göra detta behöver du en knapp som skickar en begäran till servern. Och servern kommer att ogiltigförklara den aktuella sessionen och omdirigera begäran tillbaka till "/start" .
    • I "index.jsp" i "head" -avsnittet skriver du skriptet "jquery" . Med hjälp av detta bibliotek kommer vi att skicka en förfrågan till servern.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • I "index.jsp" i "script" -avsnittet lägger du till en funktion som kan skicka en POST-begäran till servern. Vi kommer att göra funktionen synkron, och när ett svar kommer från servern kommer den att ladda om den aktuella sidan.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Inuti "c:if" -blocken lägger du till en knapp som, när den klickas, anropar funktionen vi just 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>
    • Låt oss skapa en ny servlet som kommer att tjäna "/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");
          }
      }
      Efter segern visas knappen "Börja igen" . Efter att ha klickat på den kommer fältet att rensas helt och spelet börjar om.
  18. Det återstår att överväga den sista situationen. Tänk om användaren satte ett kryss, det blev ingen seger och det finns ingen plats för en nolla? Då är det här en dragning, och vi kommer att behandla den nu:
    • I "LogicServlet" -sessionen, lägg till en annan parameter "draw" , uppdatera "data" -fältet och skicka en omdirigering till "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" kommer vi att bearbeta denna parameter:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Som ett resultat av en dragning kommer vi att få motsvarande meddelande och ett erbjudande om att börja om:

Detta avslutar skrivningen av spelet.

Kod för klasser och filer som de arbetade 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;
    }
}

Starta omServlet

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