diff --git a/pom.xml b/pom.xml
index cb226e88..34647798 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,34 @@
jstl
1.2
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.10.2
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.12.0
+ test
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.20.0
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.20.0
+
diff --git a/src/main/java/com/tictactoe/Field.java b/src/main/java/com/tictactoe/Field.java
index c52d2a0d..f0076344 100644
--- a/src/main/java/com/tictactoe/Field.java
+++ b/src/main/java/com/tictactoe/Field.java
@@ -52,7 +52,8 @@ public Sign checkWin() {
);
for (List winPossibility : winPossibilities) {
- if (field.get(winPossibility.get(0)) == field.get(winPossibility.get(1))
+ if (field.get(winPossibility.get(0)) != Sign.EMPTY
+ && field.get(winPossibility.get(0)) == field.get(winPossibility.get(1))
&& field.get(winPossibility.get(0)) == field.get(winPossibility.get(2))) {
return field.get(winPossibility.get(0));
}
diff --git a/src/main/java/com/tictactoe/InitServlet.java b/src/main/java/com/tictactoe/InitServlet.java
new file mode 100644
index 00000000..8ecfee10
--- /dev/null
+++ b/src/main/java/com/tictactoe/InitServlet.java
@@ -0,0 +1,33 @@
+package com.tictactoe;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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 = "InitServlet", value = "/start")
+public class InitServlet extends HttpServlet {
+ private static final Logger LOGGER = LogManager.getLogger(InitServlet.class);
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ HttpSession currentSession = req.getSession(true);
+
+ Field field = new Field();
+
+ List data = field.getFieldData();
+
+ currentSession.setAttribute("field", field);
+ currentSession.setAttribute("data", data);
+
+ LOGGER.info("Game started, field initialized");
+ getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
+ }
+}
diff --git a/src/main/java/com/tictactoe/LogicServlet.java b/src/main/java/com/tictactoe/LogicServlet.java
new file mode 100644
index 00000000..938fd0ae
--- /dev/null
+++ b/src/main/java/com/tictactoe/LogicServlet.java
@@ -0,0 +1,97 @@
+package com.tictactoe;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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 {
+ private static final Logger LOGGER = LogManager.getLogger(LogicServlet.class);
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ HttpSession currentSession = req.getSession(true);
+ Field field = extractField(currentSession);
+
+ int index = getSelectedIndex(req);
+ Sign currentSign = field.getField().get(index);
+
+ if (Sign.EMPTY != currentSign) {
+ LOGGER.debug("Clicked on not EMPTY sign");
+ RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
+ dispatcher.forward(req, resp);
+ return;
+ }
+
+ field.getField().put(index, Sign.CROSS);
+ if (checkWin(resp, currentSession, field)) {
+ LOGGER.info("Game ended");
+ LOGGER.debug("Player won");
+ return;
+ }
+
+ int emptyFieldIndex = field.getEmptyFieldIndex();
+ if (emptyFieldIndex >= 0) {
+ field.getField().put(emptyFieldIndex, Sign.NOUGHT);
+ if (checkWin(resp, currentSession, field)) {
+ LOGGER.info("Game ended");
+ LOGGER.debug("Computer won");
+ return;
+ }
+ } else {
+ currentSession.setAttribute("draw", true);
+
+ List data = field.getFieldData();
+
+ currentSession.setAttribute("data", data);
+ resp.sendRedirect("/index.jsp");
+ return;
+ }
+
+ List data = field.getFieldData();
+
+ currentSession.setAttribute("data", data);
+ currentSession.setAttribute("field", field);
+
+ resp.sendRedirect("/index.jsp");
+ }
+
+ private Field extractField(HttpSession currentSession) {
+ Object fieldAttribute = currentSession.getAttribute("field");
+ if (fieldAttribute.getClass() != Field.class) {
+ currentSession.invalidate();
+ LOGGER.error("Session is broken");
+ throw new RuntimeException("Session is broken, try one more time");
+ }
+ return (Field) fieldAttribute;
+ }
+
+ private int getSelectedIndex(HttpServletRequest request) {
+ String click = request.getParameter("click");
+ boolean isNumeric = click.chars().allMatch(Character::isDigit);
+ return isNumeric ? Integer.parseInt(click) : 0;
+ }
+
+ private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
+ Sign winner = field.checkWin();
+ if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
+ currentSession.setAttribute("winner", winner);
+
+ List data = field.getFieldData();
+
+ currentSession.setAttribute("data", data);
+ response.sendRedirect("/index.jsp");
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/tictactoe/RestartServlet.java b/src/main/java/com/tictactoe/RestartServlet.java
new file mode 100644
index 00000000..154ba3f9
--- /dev/null
+++ b/src/main/java/com/tictactoe/RestartServlet.java
@@ -0,0 +1,22 @@
+package com.tictactoe;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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 {
+ private static final Logger LOGGER = LogManager.getLogger(RestartServlet.class);
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ req.getSession().invalidate();
+ resp.sendRedirect("/start");
+ LOGGER.info("Game restarted");
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
new file mode 100644
index 00000000..0d7a893d
--- /dev/null
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +1,23 @@
+
+
+ %date %level %logger{10} [%file:%line] %msg%n
+ C:/Main/project-servlet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp
index 964cc071..98e3a1aa 100644
--- a/src/main/webapp/index.jsp
+++ b/src/main/webapp/index.jsp
@@ -1,17 +1,61 @@
+<%@ page import="com.tictactoe.Sign" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+ <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+ ">
+
Tic-Tac-Toe
-Tic-Tac-Toe
+ Tic-Tac-Toe
+
+
+ | ${data.get(0).getSign()} |
+ ${data.get(1).getSign()} |
+ ${data.get(2).getSign()} |
+
+
+ | ${data.get(3).getSign()} |
+ ${data.get(4).getSign()} |
+ ${data.get(5).getSign()} |
+
+
+ | ${data.get(6).getSign()} |
+ ${data.get(7).getSign()} |
+ ${data.get(8).getSign()} |
+
+
+
+
-
-
+
+ CROSSES WIN!
+
+
+
+ NOUGHTS WIN!
+
+
+
+ IT'S A DRAW
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/static/main.css b/src/main/webapp/static/main.css
index e69de29b..98fbad59 100644
--- a/src/main/webapp/static/main.css
+++ b/src/main/webapp/static/main.css
@@ -0,0 +1,22 @@
+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;
+}
+
+
+body {
+ text-align: center;
+}
+
+table {
+ position: relative;
+ left: 50%;
+ transform: translateX(-50%);
+}
\ No newline at end of file
diff --git a/src/test/java/com/tictactoe/FieldTest.java b/src/test/java/com/tictactoe/FieldTest.java
new file mode 100644
index 00000000..e5bfe39a
--- /dev/null
+++ b/src/test/java/com/tictactoe/FieldTest.java
@@ -0,0 +1,99 @@
+package com.tictactoe;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class FieldTest {
+
+ public Field field;
+ @BeforeEach
+ void init() {
+ field = new Field();
+ }
+
+ @Test
+ void allFieldCellsShouldBeEmptyWhenFieldCreated() {
+ Map fieldMap = field.getField();
+ fieldMap.forEach((integer, sign) -> assertSame(sign, Sign.EMPTY));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8})
+ void shouldReturnRightEmptyFieldIndex(int fieldIndex) {
+ field.getField().forEach((integer, sign) -> {
+ if (integer != fieldIndex) {
+ field.getField().put(integer, Sign.CROSS);
+ }
+ });
+
+ assertEquals(fieldIndex, field.getEmptyFieldIndex());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8})
+ void getFieldData(int fieldIndex) {
+ field.getField().forEach((integer, sign) -> {
+ if (integer != fieldIndex) {
+ field.getField().put(integer, integer % 2 == 0 ? Sign.NOUGHT : Sign.CROSS);
+ }
+ });
+
+ Map expectedField = new TreeMap<>();
+ for (int integer = 0; integer < 9; integer++) {
+ if (integer != fieldIndex) {
+ expectedField.put(integer, integer % 2 == 0 ? Sign.NOUGHT : Sign.CROSS);
+ } else {
+ expectedField.put(integer, Sign.EMPTY);
+ }
+ }
+
+ assertEquals(expectedField, field.getField());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "0, 1, 2",
+ "3, 4, 5",
+ "6, 7, 8",
+ "0, 3, 6",
+ "1, 4, 7",
+ "2, 5, 8",
+ "0, 4, 8",
+ "2, 4, 6"
+ })
+ void shouldReturnCROSSSignWhenCalledCheckWin(int firstFieldIndex, int secondFieldIndex, int thirdFieldIndex) {
+ field.getField().put(firstFieldIndex, Sign.CROSS);
+ field.getField().put(secondFieldIndex, Sign.CROSS);
+ field.getField().put(thirdFieldIndex, Sign.CROSS);
+
+ assertSame(Sign.CROSS, field.checkWin());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "0, 1, 2",
+ "3, 4, 5",
+ "6, 7, 8",
+ "0, 3, 6",
+ "1, 4, 7",
+ "2, 5, 8",
+ "0, 4, 8",
+ "2, 4, 6"
+ })
+ void shouldReturnNOUGHTSignWhenCalledCheckWin(int firstFieldIndex, int secondFieldIndex, int thirdFieldIndex) {
+ field.getField().put(firstFieldIndex, Sign.NOUGHT);
+ field.getField().put(secondFieldIndex, Sign.NOUGHT);
+ field.getField().put(thirdFieldIndex, Sign.NOUGHT);
+
+ assertSame(Sign.NOUGHT, field.checkWin());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/tictactoe/LogicServletTest.java b/src/test/java/com/tictactoe/LogicServletTest.java
new file mode 100644
index 00000000..5bf95154
--- /dev/null
+++ b/src/test/java/com/tictactoe/LogicServletTest.java
@@ -0,0 +1,111 @@
+package com.tictactoe;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class LogicServletTest {
+
+ @Mock
+ public HttpServletRequest request;
+ @Mock
+ public HttpServletResponse response;
+ @Mock
+ public HttpSession session;
+ @Mock
+ public RequestDispatcher requestDispatcher;
+ @Mock
+ public ServletContext servletContext;
+ @Mock
+ public ServletConfig servletConfig;
+
+ public LogicServlet logicServlet;
+ public Field field;
+
+ @BeforeEach
+ void setUp() throws ServletException {
+ logicServlet = new LogicServlet();
+ field = new Field();
+ logicServlet.init(servletConfig);
+ when(request.getSession(true)).thenReturn(session);
+ when(session.getAttribute("field")).thenReturn(field);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8})
+ void shouldNotChangeFieldWhenClickedNotOnEmptyCell_TestWithParams_doGet(int click) throws ServletException, IOException {
+ when(logicServlet.getServletContext()).thenReturn(servletContext);
+ when(logicServlet.getServletContext().getRequestDispatcher("/index.jsp")).thenReturn(requestDispatcher);
+ when(request.getParameter("click")).thenReturn(String.valueOf(click));
+
+ field.getField().put(click, Sign.CROSS);
+
+ Field expectedField = field;
+ logicServlet.doGet(request, response);
+
+ assertEquals(expectedField.getField(), field.getField());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "0, 1, 2",
+ "3, 4, 5",
+ "6, 7, 8",
+ "0, 3, 6",
+ "1, 4, 7",
+ "2, 5, 8",
+ "0, 4, 8",
+ "2, 4, 6"
+ })
+ void shouldStopGameWhenCrossWinnerExist_TestWithParams_doGet(int firstCell, int secondCell, int thirdCell) throws ServletException, IOException {
+ field.getField().put(firstCell, Sign.CROSS);
+ field.getField().put(secondCell, Sign.CROSS);
+
+ when(request.getParameter("click")).thenReturn(String.valueOf(thirdCell));
+
+ logicServlet.doGet(request, response);
+
+ verify(session).setAttribute("winner", Sign.CROSS);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "0, 1, 2",
+ "3, 4, 5",
+ "6, 7, 8",
+ "0, 3, 6",
+ "1, 4, 7",
+ "2, 5, 8",
+ "0, 4, 8",
+ "2, 4, 6"
+ })
+ void shouldStopGameWhenNoughtWinnerExist_TestWithParams_doGet(int firstCell, int secondCell, int thirdCell) throws ServletException, IOException {
+ field.getField().put(firstCell, Sign.NOUGHT);
+ field.getField().put(secondCell, Sign.NOUGHT);
+ field.getField().put(thirdCell, Sign.NOUGHT);
+
+ when(request.getParameter("click")).thenReturn(String.valueOf(field.getEmptyFieldIndex()));
+
+ logicServlet.doGet(request, response);
+
+ verify(session).setAttribute("winner", Sign.NOUGHT);
+ }
+}
\ No newline at end of file