Today we will write a Tic-Tac-Toe game using servlets and JSP.
This project will be a little different from the previous ones. It will contain not only tasks, but also explanations of how to do them. That is, it will be a project from the "HOW TO ..." series.
Instruction:
- Fork from the repository: https://github.com/CodeGymCC/project-servlet.git
- Download your version of the project to your computer.
- Set up application launch in IDEA:
- Alt + Shift + F9 -> Edit Configurations… -> Alt + insert -> tom (into the search bar) -> Local.
- After that, you need to click “CONFIGURE” and indicate where the archive with Tomcat was downloaded and unpacked.
- In the “Deployment” tab: Alt + insert -> Artifact… -> tic-tac-toe:war exploded -> OK.
- In the “Application context” field: leave only “/” (slash).
- Press "APPLY".
- Close the settings window.
- Make the first test run of the customized configuration. If everything is done correctly, your default browser will open, in which it will be:
- Open the "pom.xml" file . There are 2 dependencies in the “dependencies” block .
javax.servlet-api
is responsible for the specification of servlets. Scope "provided" is needed during development, but not needed at runtime (Tomcat already has this dependency in the lib folder).jstl
– can be considered as a template engine.- There are 3 files in the “webapp” folder :
index.jsp
- this is our template (similar to the HTML page). It will contain markup and scripts. It is the file called “index” that is given as the initial page, if there are no configurations, which we saw in step 3./static/main.css
- file for styles. As in the previous project, everything here is up to you, paint as you wish./static/jquery-3.6.0.min.js
- frontend dependency that our server will distribute as static.- The "com.tictactoe" package will contain all the Java code. Right now there are 2 classes:
Sign
- enum, which is responsible for the "cross / zero / void" .Field
is our field. This class has a "field" map . The principle of data storage will be as follows: the cells of the tic-tac-toe field are numbered from zero. In the first line 0, 1 and 2. In the second: 3, 4 and 5. And so on. There are also 3 methods. “getEmptyFieldIndex” looks for the first empty cell (yes, our opponent will not be very smart). "checkWin" checks if the game is over. If there is a row of three crosses, it returns a cross; if there is a row of three zeroes, it returns a zero. Otherwise, it's empty. "getFieldData" - returns the values of the "field" map as a list sorted in ascending index order.- The explanations about the template are finished, now you can start the task. Let's start by drawing a 3 by 3 table. To do this, add the following code to “index.jsp” :
We will then remove the numbers in the table and replace them with a cross, zero or an empty field. Also, inside the “head” tag, include the style file. To do this, add a line:<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>
<link href="static/main.css" rel="stylesheet">
The content of the style file is up to you. I used this one:
After running, my result looks like this: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; }
- Now let's add the following functionality: when a cell is clicked, a request will be sent to the server, in which we will pass the index of the cell that was clicked as a parameter. This task can be divided into two parts: send a request from the front, accept a request on the server. Let's start at the front for a change.
Let's add an "onclick" parameter to each "d" tag . In the value, we indicate the change of the current page to the specified URL. The servlet that will be responsible for the logic will have the URL “/logic” . And it will take a parameter called “click” . So we will pass the index of the cell that the user clicked on.
You can check that everything is done correctly through the developer panel in the browser. For example, in Chrome, it opens with the F12 button . As a result of clicking on a cell with index 4, the picture will be as follows: We get an error because we have not yet created a servlet that can send the server to the address “logic” .<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>
- In the package "com.tictactoe" create a class "LogicServlet" which should be derived from the class "javax.servlet.http.HttpServlet" . In the class, override the “doGet” method .
And let's add a method that will get the index of the cell that was clicked. You also need to add a mapping (the address at which this servlet will intercept the request). I suggest doing this through an annotation (but if you like difficulties, you can also use web.xml). General servlet code:
Now, when clicking on any cell, we will get the index of this cell on the server (you can make sure by running the server in debug). And there will be a redirect to the same page from which the click was made.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; } }
- Now we can click, but it's not a game yet. In order for the game to have logic, you need to save the state of the game (where the crosses are, where the zeros are) between requests. The easiest way to do this is to store this data in the session. With this approach, the session will be stored on the server, and the client will receive a session ID in a cookie named “JSESSIONID” . But the session does not need to be created every time, but only at the beginning of the game. Let's start another servlet for this, which we will call "InitServlet" . We will override the “doGet” method in it , in which we will create a new session, create a playing field, put this playing field and a list of type Sign in the session attributes, and send “forward” to the index.jsp page. Code:
And not to forget, let's change the start page that opens in the browser after starting the server to “/start” : Now after restarting the server and clicking on any cell of the field in the browser developer menu in the “Request Headers” section , there will be a cookie with the session ID :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); } }
- When we have a repository in which we can store state between requests from the client (browser), we can start writing game logic. The logic we have is in “LogicServlet” . We need to work with the “doGet” method . Let's add this behavior to the method:
- we will get the “field” object of the Field type from the session (we will take it out to the “extractField” method ).
- put a cross where the user clicked (so far without any checks).
The behavior has not changed yet, but if you start the server in debug and set a breakpoint on the line where the redirect is sent, you can see the “innards” of the “ data” object . There, indeed , “CROSS” appears under the index that was clicked.@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; }
- Now it's time to display the cross on the frontend. To do this, we will work with the “index.jsp” file and the “JSTL” technology .
- In the <head> section add:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
- In the table inside each <td> block, change the index to a construct that allows you to calculate values. For example, for index zero:
<td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td>
Now, when you click on a cell, a cross will appear there:
- In the <head> section add:
- We have made our move, now it's the turn for the "zero". And let's add a couple of checks here, so that the signs are not placed in already occupied cells.
- You need to check that the cell that was clicked is empty. Otherwise, we do nothing and send the user to the same page without changing the session parameters.
- Since the number of cells on the field is odd, it is possible that a cross was placed, but there is no room for a zero. Therefore, after we put a cross, we try to get the index of an unoccupied cell (the getEmptyFieldIndex method of the Field class). If the index is not negative, then put a zero there. Code:
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; } }
- At this stage, you can put crosses, AI answers with zeros. But there is no check when to stop the game. This can be in three cases:
- after the next move of the cross, a line of three crosses was formed;
- after the next return move with a zero, a line of three zeros was formed;
- after the next move of the cross, empty cells ended.
The peculiarity of this method is that if the winner is found, we add another parameter to the session, using which we will change the display in “index.jsp” in the following paragraphs./** * 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; }
- Let's add a call to the “checkWin ” method twice to the “doGet” method . The first time after setting the cross, the second - after setting the 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; } }
- In terms of behavior, almost nothing has changed (except that if one of the signs wins, zeros are no longer placed. Let's use the “winner” parameter in “index.jsp” and display the winner. We use directives after the table:
c:set
c:if
If crosses win, the message “CROSSES WIN!” , if the zeros are “NOUGHTS WIN!” . As a result, we can get one of two inscriptions:<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>
- If there is a winner, you need to be able to take revenge. To do this, you need a button that will send a request to the server. And the server will invalidate the current session and redirect the request back to “/start” .
- In “index.jsp” in the “head” section , write the script “jquery” . Using this library, we will send a request to the server.
<script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
- In “index.jsp” in the “script” section , add a function that can send a POST request to the server. We will make the function synchronous, and when the response comes from the server, it will reload the current page.
<script> function restart() { $.ajax({ url: '/restart', type: 'POST', contentType: 'application/json;charset=UTF-8', async: false, success: function () { location.reload(); } }); } </script>
- Inside the “c:if” blocks , add a button that, when clicked, calls the function we just wrote:
<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>
- Let's create a new servlet that will serve the "/restart" URL .
After the victory, the “Start again” button will appear . After clicking on it, the field will be completely cleared, and the game will start over.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"); } }
- In “index.jsp” in the “head” section , write the script “jquery” . Using this library, we will send a request to the server.
- It remains to consider the last situation. What if the user put a cross, there was no victory, and there is no place for a zero? Then this is a draw, and we will process it now:
- In the "LogicServlet" session, add another parameter "draw" , update the "data" field and send a redirect to "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; }
- In "index.jsp" we will process this parameter:
As a result of a draw, we will receive the corresponding message and an offer to start over:<c:if test="${draw}"> <h1>IT'S A DRAW</h1> <br> <button onclick="restart()">Start again</button> </c:if>
- In the "LogicServlet" session, add another parameter "draw" , update the "data" field and send a redirect to "index.jsp" :
This completes the writing of the game.
Code of classes and files with which they worked
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;
}
GO TO FULL VERSION