Hoje vamos escrever um jogo Tic-Tac-Toe usando servlets e JSP.

Este projeto será um pouco diferente dos anteriores. Ele conterá não apenas tarefas, mas também explicações sobre como realizá-las. Ou seja, será um projeto da série "COMO FAZER...".

Instrução:

  1. Fork do repositório: https://github.com/CodeGymCC/project-servlet.git
  2. Baixe sua versão do projeto para o seu computador.
  3. Configure a inicialização do aplicativo no IDEA:
    • Alt + Shift + F9 -> Editar configurações… -> Alt + inserir -> tom (na barra de pesquisa) -> Local.
    • Depois disso, você precisa clicar em “CONFIGURAR” e indicar onde o arquivo com o Tomcat foi baixado e descompactado.
    • Na aba “Deployment”: Alt + insert -> Artifact… -> tic-tac-toe:war explodid -> OK.
    • No campo “Application context”: deixe apenas “/” (barra).
    • Pressione "APLICAR".
    • Feche a janela de configurações.
    • Faça o primeiro teste da configuração personalizada. Se tudo for feito corretamente, seu navegador padrão será aberto, no qual estará:
  4. Abra o arquivo "pom.xml" . Existem 2 dependências no bloco “dependências” .
    • javax.servlet-apié responsável pela especificação de servlets. O escopo "provided" é necessário durante o desenvolvimento, mas não é necessário em tempo de execução (o Tomcat já possui essa dependência na pasta lib).
    • jstl– pode ser considerado como um mecanismo de modelo.
  5. Existem 3 arquivos na pasta “webapp” :
    • index.jsp- este é o nosso modelo (semelhante à página HTML). Ele conterá marcação e scripts. É o arquivo chamado “index” que é dado como página inicial, caso não haja configurações, que vimos no passo 3.
    • /static/main.css- arquivo para estilos. Como no projeto anterior, tudo aqui é com você, pinte como quiser.
    • /static/jquery-3.6.0.min.js- dependência de frontend que nosso servidor distribuirá como estático.
  6. O pacote "com.tictactoe" conterá todo o código Java. Neste momento existem 2 classes:
    • Sign- enum, que é responsável pelo "cross / zero / void" .
    • Fieldé o nosso campo. Esta classe tem um mapa de "campo" . O princípio de armazenamento de dados será o seguinte: as células do campo jogo da velha são numeradas a partir de zero. Na primeira linha 0, 1 e 2. Na segunda: 3, 4 e 5. E assim por diante. Existem também 3 métodos. “getEmptyFieldIndex” procura a primeira célula vazia (sim, nosso oponente não será muito esperto). "checkWin" verifica se o jogo acabou. Se houver uma linha de três cruzes, ele retorna uma cruz; se houver uma linha de três zeros, ele retorna um zero. Caso contrário, está vazio. "getFieldData" - retorna os valores do mapa "campo" como uma lista classificada em ordem crescente de índice.
  7. As explicações sobre o modelo estão concluídas, agora você pode iniciar a tarefa. Vamos começar desenhando uma tabela 3 por 3. Para fazer isso, adicione o seguinte código 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>
    Em seguida, removeremos os números da tabela e os substituiremos por uma cruz, zero ou um campo vazio. Além disso, dentro da tag “head”, inclua o arquivo de estilo. Para fazer isso, adicione uma linha:<link href="static/main.css" rel="stylesheet">

    O conteúdo do arquivo de estilo depende de você. Eu usei este:
    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;
    }
    Depois de executar, meu resultado ficou assim:
  8. Agora vamos adicionar a seguinte funcionalidade: quando uma célula for clicada, uma solicitação será enviada ao servidor, na qual passaremos como parâmetro o índice da célula que foi clicada. Essa tarefa pode ser dividida em duas partes: enviar uma solicitação pela frente, aceitar uma solicitação no servidor. Vamos começar na frente para variar.

    Vamos adicionar um parâmetro "onclick" a cada tag "d" . No valor, indicamos a alteração da página atual para o URL especificado. O servlet que será responsável pela lógica terá a URL “/logic” . E vai levar um parâmetro chamado “click” . Então vamos passar o índice da célula que o usuário clicou.
    <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>
    Você pode verificar se tudo foi feito corretamente por meio do painel do desenvolvedor no navegador. Por exemplo, no Chrome, abre com o botão F12 . Como resultado de clicar em uma célula com índice 4, a imagem será a seguinte: Obtemos um erro porque ainda não criamos um servlet que possa enviar o servidor para o endereço “logic” .
  9. No pacote "com.tictactoe" crie uma classe "LogicServlet" que deve ser derivada da classe "javax.servlet.http.HttpServlet" . Na classe, sobrescreva o método “doGet” .

    E vamos adicionar um método que vai pegar o índice da célula que foi clicada. Você também precisa adicionar um mapeamento (o endereço no qual este servlet interceptará a solicitação). Sugiro fazer isso através de uma anotação (mas se você gosta de dificuldades, também pode usar o web.xml). Código geral do servlet:
    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;
        }
    
    }
    Agora, ao clicar em qualquer célula, obteremos o índice desta célula no servidor (você pode ter certeza executando o servidor em debug). E haverá um redirecionamento para a mesma página de onde foi feito o clique.
  10. Agora podemos clicar, mas ainda não é um jogo. Para que o jogo tenha lógica, você precisa salvar o estado do jogo (onde estão as cruzes, onde estão os zeros) entre as solicitações. A maneira mais fácil de fazer isso é armazenar esses dados na sessão. Com essa abordagem, a sessão será armazenada no servidor e o cliente receberá um ID de sessão em um cookie denominado “JSESSIONID” . Mas a sessão não precisa ser criada toda vez, mas apenas no início do jogo. Vamos iniciar outro servlet para isso, que chamaremos de "InitServlet" . Vamos sobrescrever o método “doGet” nele , no qual iremos criar uma nova sessão, criar um campo de jogo, colocar este campo de jogo e uma lista do tipo Sign nos atributos da sessão, e enviar “ forward” para o index.jsp página. Código:
    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 para não esquecer, vamos mudar a página inicial que abre no navegador após iniciar o servidor para “/start” : Agora após reiniciar o servidor e clicar em qualquer célula do campo no menu do desenvolvedor do navegador na seção “Request Headers” , haverá um cookie com o ID da sessão:
  11. Quando temos um repositório no qual podemos armazenar o estado entre as solicitações do cliente (navegador), podemos começar a escrever a lógica do jogo. A lógica que temos está em “LogicServlet” . Precisamos trabalhar com o método “doGet” . Vamos adicionar este comportamento ao método:
    • vamos pegar o objeto “field” do tipo Field da sessão (vamos retirá-lo para o método “extractField” ).
    • coloque uma cruz onde o usuário clicou (até agora sem nenhuma verificação).
    @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;
    }
    O comportamento ainda não mudou, mas se você iniciar o servidor em depuração e definir um breakpoint na linha para onde o redirecionamento é enviado, poderá ver as “entranhas” do objeto “dados . Ali, de fato, aparece “CROSS” abaixo do índice que foi clicado.
  12. Agora é hora de exibir a cruz no frontend. Para isso, trabalharemos com o arquivo “index.jsp” e a tecnologia “JSTL” .
    • Na seção <head> adicione:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Na tabela dentro de cada bloco <td>, altere o índice para uma construção que permite calcular valores. Por exemplo, para o índice zero: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Agora, ao clicar em uma célula, aparecerá uma cruz ali:
  13. Fizemos a nossa jogada, agora é a vez do "zero". E vamos adicionar algumas verificações aqui, para que os sinais não sejam colocados em células já ocupadas.
    • Você precisa verificar se a célula que foi clicada está vazia. Caso contrário, não fazemos nada e enviamos o usuário para a mesma página sem alterar os parâmetros da sessão.
    • Como o número de células no campo é ímpar, é possível que tenha sido colocada uma cruz, mas não há espaço para um zero. Portanto, após colocarmos uma cruz, tentamos obter o índice de uma célula desocupada (o método getEmptyFieldIndex da classe Field). Se o índice não for negativo, coloque um zero lá. Código:
      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. Nesta fase, você pode colocar cruzes, a IA responde com zeros. Mas não há verificação de quando parar o jogo. Isso pode ocorrer em três casos:
    • após o próximo movimento da cruz, uma linha de três cruzes foi formada;
    • após o próximo movimento de retorno com um zero, uma linha de três zeros foi formada;
    • após o próximo movimento da cruz, as células vazias terminaram.
    Vamos adicionar um método que verifica se há três cruzes/zeros seguidos:
    /**
     * 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;
    }
    A peculiaridade deste método é que, se o vencedor for encontrado, adicionamos outro parâmetro à sessão, com o qual alteraremos a exibição em “index.jsp” nos parágrafos seguintes.
  15. Vamos adicionar uma chamada ao método “checkWin ” duas vezes ao método “doGet” . A primeira vez após definir a cruz, a segunda - após definir o 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. Em termos de comportamento, quase nada mudou (exceto que se um dos sinais vencer, os zeros não serão mais colocados. Vamos usar o parâmetro “winner” em “index.jsp” e exibir o vencedor. Usamos diretivas após a tabela: 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 os cruzamentos vencerem, a mensagem “CROSSES WIN!” , se os zeros forem “NOUGTS WIN!” . Como resultado, podemos obter uma das duas inscrições:
  17. Se houver um vencedor, você precisa se vingar. Para fazer isso, você precisa de um botão que enviará uma solicitação ao servidor. E o servidor invalidará a sessão atual e redirecionará a solicitação de volta para “/start” .
    • Em “index.jsp” na seção “head” , escreva o script “jquery” . Usando esta biblioteca, enviaremos uma solicitação ao servidor.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Em “index.jsp” na seção “script” , adicione uma função que pode enviar uma solicitação POST ao servidor. Faremos a função síncrona e, quando vier uma resposta do servidor, ela recarregará a página atual.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Dentro dos blocos “c:if” , adicione um botão que, ao ser clicado, chama a função que acabamos de escrever:
      <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>
    • Vamos criar um novo servlet que servirá a 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");
          }
      }
      Após a vitória, aparecerá o botão “Começar de novo” . Depois de clicar nele, o campo será totalmente limpo e o jogo será reiniciado.
  18. Resta considerar a última situação. E se o usuário colocar uma cruz, não houve vitória e não há lugar para zero? Então este é um empate e vamos processá-lo agora:
    • Na sessão "LogicServlet" , adicione outro parâmetro "draw" , atualize o campo "data" e envie um redirecionamento para "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;
      }
    • Em "index.jsp" iremos processar este parâmetro:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Como resultado de um sorteio, receberemos a mensagem correspondente e uma oferta para recomeçar:

Isso completa a escrita do jogo.

Código das classes e arquivos com os quais trabalharam

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