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