Bugün servletler ve JSP kullanarak bir Tic-Tac-Toe oyunu yazacağız.

Bu proje öncekilerden biraz farklı olacak. Sadece görevleri değil, aynı zamanda bunların nasıl yapılacağına dair açıklamaları da içerecektir. Yani "NASIL YAPILIR ..." serisinden bir proje olacak.

Talimat:

  1. Depodan çatal: https://github.com/CodeGymCC/project-servlet.git
  2. Projenizin sürümünü bilgisayarınıza indirin.
  3. IDEA'da uygulama başlatmayı ayarlayın:
    • Alt + Shift + F9 -> Yapılandırmaları Düzenle… -> Alt + ekle -> tom (arama çubuğuna) -> Yerel.
    • Bundan sonra, "YAPILANDIR" ı tıklamanız ve Tomcat ile arşivin nereye indirildiğini ve paketten çıkarıldığını belirtmeniz gerekir.
    • "Dağıtım" sekmesinde: Alt + ekle -> Artifact… -> tic-tac-toe:war patladı -> Tamam.
    • "Uygulama bağlamı" alanında: yalnızca "/" (eğik çizgi) bırakın.
    • "UYGULA"ya basın.
    • Ayarlar penceresini kapatın.
    • Özelleştirilmiş yapılandırmanın ilk deneme çalıştırmasını yapın. Her şey doğru yapılırsa, varsayılan tarayıcınız açılır ve içinde şöyle olur:
  4. "pom.xml" dosyasını açın . “bağımlılıklar” bloğunda 2 bağımlılık vardır .
    • javax.servlet-apiservlet'lerin belirtilmesinden sorumludur. Geliştirme sırasında "sağlanan" kapsam gereklidir, ancak çalışma zamanında gerekli değildir (Tomcat zaten lib klasöründe bu bağımlılığa sahiptir).
    • jstl– bir şablon motoru olarak kabul edilebilir.
  5. “webapp” klasöründe 3 dosya vardır :
    • index.jsp- bu bizim şablonumuz (HTML sayfasına benzer). İşaretleme ve komut dosyaları içerecektir. 3. adımda gördüğümüz konfigürasyonlar yoksa ilk sayfa olarak verilen “index” adlı dosyadır .
    • /static/main.css- stiller için dosya. Bir önceki projede olduğu gibi burada da her şey size kalmış, dilediğiniz gibi boyayın.
    • /static/jquery-3.6.0.min.js- sunucumuzun statik olarak dağıtacağı ön uç bağımlılığı.
  6. "com.tictactoe" paketi tüm Java kodunu içerecektir. Şu anda 2 sınıf var:
    • Sign- "çapraz / sıfır / boşluktan" sorumlu olan numaralandırma .
    • Fieldbizim alanımızdır. Bu sınıfın bir "alan" haritası vardır . Veri depolama prensibi şu şekilde olacaktır: tic-tac-toe alanının hücreleri sıfırdan numaralandırılır. İlk satırda 0, 1 ve 2. İkinci satırda: 3, 4 ve 5. Ve böyle devam eder. Ayrıca 3 yöntem var. “getEmptyFieldIndex” ilk boş hücreyi arar (evet, rakibimiz pek akıllı olmayacaktır). "checkWin" oyunun bitip bitmediğini kontrol eder. Üç çarpıdan oluşan bir satır varsa, bir çarpı döndürür; üç sıfırdan oluşan bir satır varsa, sıfır döndürür. Aksi halde boştur. "getFieldData" - "alan" haritasının değerlerini artan dizin sırasına göre sıralanmış bir liste olarak döndürür.
  7. Şablonla ilgili açıklamalar bitti, artık göreve başlayabilirsiniz. 3'e 3'lük bir tablo çizerek başlayalım, bunun için “index.jsp” dosyasına aşağıdaki kodu ekleyin :
    <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>
    Daha sonra tablodaki sayıları kaldıracağız ve bunları artı, sıfır veya boş bir alanla değiştireceğiz. Ayrıca, "head" etiketinin içine stil dosyasını ekleyin. Bunu yapmak için bir satır ekleyin:<link href="static/main.css" rel="stylesheet">

    Stil dosyasının içeriği size kalmış. Bunu kullandım:
    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;
    }
    Çalıştırdıktan sonra sonucum şöyle görünüyor:
  8. Şimdi şu işlevi ekleyelim: Bir hücre tıklandığında sunucuya, tıklanan hücrenin dizinini parametre olarak geçireceğimiz bir istek gönderilecek. Bu görev iki bölüme ayrılabilir: önden bir istek gönderin, sunucuda bir isteği kabul edin. Bir değişiklik için önden başlayalım.

    Her "d" etiketine bir "onclick" parametresi ekleyelim . Değerde, geçerli sayfanın belirtilen URL'ye değişimini belirtiriz. Mantıktan sorumlu olacak sunucu uygulamasının URL'si “/logic” olacaktır . Ve "tıkla" adlı bir parametre alacaktır . Böylece kullanıcının tıkladığı hücrenin indeksini geçeceğiz.
    <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>
    Her şeyin doğru yapılıp yapılmadığını tarayıcıdaki geliştirici panelinden kontrol edebilirsiniz. Örneğin Chrome'da F12 butonu ile açılır . İndeks 4'e sahip bir hücreye tıklanması sonucunda resim aşağıdaki gibi olacaktır: Henüz “logic” adresine sunucu gönderebilecek bir servlet oluşturamadığımız için hata alıyoruz .
  9. "com.tictactoe" paketinde, "javax.servlet.http.HttpServlet " sınıfından türetilmesi gereken bir "LogicServlet" sınıfı oluşturun . Sınıfta "doGet" yöntemini geçersiz kılın .

    Ve tıklanan hücrenin indeksini alacak bir metot ekleyelim. Ayrıca bir eşleme (bu servlet'in isteği yakalayacağı adres) eklemeniz gerekir. Bunu bir ek açıklama aracılığıyla yapmanızı öneririm (ancak zorlukları seviyorsanız web.xml'yi de kullanabilirsiniz). Genel sunucu uygulaması kodu:
    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;
        }
    
    }
    Şimdi herhangi bir hücreye tıkladığımızda sunucuda bu hücrenin indeksini alacağız (sunucuyu debugda çalıştırarak emin olabilirsiniz). Ve tıklamanın yapıldığı aynı sayfaya bir yönlendirme olacaktır.
  10. Şimdi tıklayabiliriz, ancak bu henüz bir oyun değil. Oyunun mantık sahibi olması için istekler arasında oyunun durumunu (hataların nerede, sıfırların nerede olduğunu) kaydetmeniz gerekiyor. Bunu yapmanın en kolay yolu, bu verileri oturumda depolamaktır. Bu yaklaşımla, oturum sunucuda depolanacak ve istemci "JSESSIONID" adlı bir çerezde bir oturum kimliği alacaktır . Ancak oturumun her seferinde oluşturulması gerekmez, yalnızca oyunun başında oluşturulur. Bunun için "InitServlet" adını vereceğimiz başka bir servlet başlatalım . İçinde yeni bir oturum oluşturacağımız, bir oyun alanı oluşturacağımız, bu oyun alanını ve Sign türünde bir listeyi oturum özniteliklerine koyacağımız ve index.jsp'ye " ilet " göndereceğimiz "doGet" yöntemini geçersiz kılacağız. sayfa. kod:
    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);
        }
    }
    Ve unutmamak için sunucuyu başlattıktan sonra tarayıcıda açılan başlangıç ​​sayfasını “/start” olarak değiştirelim : Şimdi sunucuyu yeniden başlattıktan sonra tarayıcı geliştirici menüsündeki “Request Headers” bölümündeki alanın herhangi bir hücresine tıkladıktan sonra , oturum kimliğine sahip bir çerez olacaktır:
  11. İstemciden (tarayıcıdan) gelen istekler arasında durumu saklayabileceğimiz bir havuza sahip olduğumuzda, oyun mantığı yazmaya başlayabiliriz. Sahip olduğumuz mantık “LogicServlet” içindedir . “doGet” metodu ile çalışmamız gerekiyor . Bu davranışı metoda ekleyelim:
    • oturumdan Field tipindeki “field” nesnesini alacağız ( “extractField” metoduna çıkaracağız ).
    • kullanıcının tıkladığı yere bir çarpı koyun (şimdiye kadar herhangi bir kontrol yapılmadı).
    @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;
    }
    Davranış henüz değişmedi, ancak sunucuyu hata ayıklamada başlatır ve yönlendirmenin gönderildiği satırda bir kesme noktası ayarlarsanız, “ data” nesnesinin “iç kısımlarını” görebilirsiniz . Orada gerçekten de tıklanan dizinin altında “CROSS” görünüyor .
  12. Şimdi ön uçta haçı göstermenin zamanı geldi. Bunun için “index.jsp” dosyası ve “JSTL” teknolojisi ile çalışacağız .
    • <head> bölümüne şunu ekleyin:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Her <td> bloğunun içindeki tabloda, dizini, değerleri hesaplamanıza izin veren bir yapıya değiştirin. Örneğin, sıfır indeksi için: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Şimdi, bir hücreye tıkladığınızda, orada bir çarpı işareti görünecektir:
  13. Hamlemizi yaptık, sıra “sıfıra” geldi. Ve buraya birkaç kontrol ekleyelim, böylece işaretler zaten dolu olan hücrelere yerleştirilmez.
    • Tıklanan hücrenin boş olup olmadığını kontrol etmeniz gerekir. Aksi halde hiçbir şey yapmıyoruz ve oturum parametrelerini değiştirmeden kullanıcıyı aynı sayfaya gönderiyoruz.
    • Alandaki hücre sayısı tek olduğundan, bir çarpı konmuş olabilir, ancak sıfıra yer yoktur. Bu nedenle çarpı işareti koyduktan sonra boş bir hücrenin indeksini almaya çalışıyoruz (Field sınıfının getEmptyFieldIndex metodu). İndeks negatif değilse, oraya bir sıfır koyun. kod:
      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. Bu aşamada artı işareti koyabilirsiniz, yapay zeka sıfırlarla yanıt verir. Ancak oyunun ne zaman durdurulacağının kontrolü yoktur. Bu üç durumda olabilir:
    • haçın bir sonraki hareketinden sonra, üç çaprazdan oluşan bir çizgi oluştu;
    • sıfır ile bir sonraki dönüş hareketinden sonra, üç sıfırdan oluşan bir çizgi oluşturuldu;
    • haçın bir sonraki hareketinden sonra boş hücreler sona erdi.
    Arka arkaya üç artı / sıfır olup olmadığını kontrol eden bir yöntem ekleyelim:
    /**
     * 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;
    }
    Bu yöntemin özelliği, kazanan bulunursa, oturuma aşağıdaki paragraflarda "index.jsp" görüntüsünü değiştireceğimiz başka bir parametre eklememizdir .
  15. “DoGet” metoduna iki kez “checkWin ” metoduna bir çağrı ekleyelim . Haçı ayarladıktan sonra ilk kez, ikincisi - sıfırı ayarladıktan sonra.
    // 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. Davranış açısından neredeyse hiçbir şey değişmedi (işaretlerden biri kazanırsa artık sıfırlar yerleştirilmemesi dışında. “index.jsp” içinde “winner” parametresini kullanalım ve kazananı gösterelim. Tablodan c:setsonra yönergeler kullanıyoruz:c: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>
    Ortalar kazanırsa, "HARİÇLER KAZANDI!" , sıfırlar "HİÇ KAZANDI!" . Sonuç olarak, iki yazıttan birini alabiliriz:
  17. Bir kazanan varsa, intikam alabilmeniz gerekir. Bunu yapmak için, sunucuya istek gönderecek bir düğmeye ihtiyacınız var. Ve sunucu geçerli oturumu geçersiz kılacak ve isteği tekrar “/start” a yönlendirecektir .
    • “index.jsp” içindeki “head” bölümüne “jquery” scriptini yazın . Bu kütüphaneyi kullanarak sunucuya bir istek göndereceğiz.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • "script" bölümündeki "index.jsp" içinde , sunucuya POST isteği gönderebilen bir işlev ekleyin. Fonksiyonu senkronize hale getireceğiz ve sunucudan bir cevap geldiğinde mevcut sayfayı yeniden yükleyecektir.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • "c:if" bloklarının içine , tıklandığında az önce yazdığımız işlevi çağıran bir düğme ekleyin:
      <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>
    • "/restart" URL'sini sunacak yeni bir sunucu uygulaması oluşturalım .
      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");
          }
      }
      Zaferden sonra, "Yeniden başla" düğmesi görünecektir . Üzerine tıkladıktan sonra alan tamamen temizlenecek ve oyun baştan başlayacaktır.
  18. Son durumu düşünmeye devam ediyor. Ya kullanıcı artı işareti koyarsa, zafer yoksa ve sıfıra yer yoksa? O zaman bu bir beraberlik ve bunu şimdi işleyeceğiz:
    • "LogicServlet" oturumunda , başka bir "draw" parametresi ekleyin , "data" alanını güncelleyin ve "index.jsp" ye bir yönlendirme gönderin :
      // 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;
      }
    • "index.jsp" içinde şu parametreyi işleyeceğiz:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Çekiliş sonucunda ilgili mesajı ve yeniden başlama teklifini alacağız:

Bu, oyunun yazımını tamamlar.

Çalıştıkları sınıfların ve dosyaların kodu

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

Mantık Sunucu Uygulaması

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

Yeniden BaşlatServlet

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>

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