From 7f5f73f50030a46eb2584717886e5b59fbe47054 Mon Sep 17 00:00:00 2001 From: a_subscriber Date: Sat, 2 May 2026 10:51:32 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20HW12=20-=20=D0=A0=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B8=D0=B3=D1=80=D0=BE=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/ru/otus/main_patterns/App.java | 15 -- .../java/ru/otus/main_patterns/hw12/Cell.java | 12 ++ .../otus/main_patterns/hw12/GameObject.java | 59 +++++++ .../java/ru/otus/main_patterns/hw12/Grid.java | 53 +++++++ .../java/ru/otus/main_patterns/hw12/HW12.java | 111 +++++++++++++ .../hw12/command/CollisionCommand.java | 22 +++ .../hw12/command/MacroCommand.java | 28 ++++ .../hw12/command/UpdateObjectCellCommand.java | 61 +++++++ .../hw12/handler/CollisionCheckHandler.java | 19 +++ .../hw12/handler/DamageHandler.java | 12 ++ .../main_patterns/hw12/handler/Handler.java | 19 +++ .../hw12/interfaces/Command.java | 6 + .../hw12/command/CollisionCommandTest.java | 115 ++++++++++++++ .../hw12/command/MacroCommandTest.java | 144 +++++++++++++++++ .../command/UpdateObjectCellCommandTest.java | 150 ++++++++++++++++++ 15 files changed, 811 insertions(+), 15 deletions(-) delete mode 100644 app/src/main/java/ru/otus/main_patterns/App.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/Cell.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/GameObject.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/Grid.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/HW12.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/command/CollisionCommand.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/command/MacroCommand.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommand.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/handler/CollisionCheckHandler.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/handler/DamageHandler.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/handler/Handler.java create mode 100644 app/src/main/java/ru/otus/main_patterns/hw12/interfaces/Command.java create mode 100644 app/src/test/java/ru/otus/main_patterns/hw12/command/CollisionCommandTest.java create mode 100644 app/src/test/java/ru/otus/main_patterns/hw12/command/MacroCommandTest.java create mode 100644 app/src/test/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommandTest.java diff --git a/app/src/main/java/ru/otus/main_patterns/App.java b/app/src/main/java/ru/otus/main_patterns/App.java deleted file mode 100644 index 0d71726..0000000 --- a/app/src/main/java/ru/otus/main_patterns/App.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.otus.main_patterns; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class App { - private static final Logger logger = LoggerFactory.getLogger(App.class); - - public static void main(String[] args) { - logger.info( - "Java version: {}, Java vendor: {}", - System.getProperty("java.version"), - System.getProperty("java.vendor")); - } -} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/Cell.java b/app/src/main/java/ru/otus/main_patterns/hw12/Cell.java new file mode 100644 index 0000000..190ec99 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/Cell.java @@ -0,0 +1,12 @@ +package ru.otus.main_patterns.hw12; + +import java.util.ArrayList; +import java.util.List; + +public class Cell { + private final List objects = new ArrayList<>(); + + public List getObjects() { + return objects; + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/GameObject.java b/app/src/main/java/ru/otus/main_patterns/hw12/GameObject.java new file mode 100644 index 0000000..e60df6a --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/GameObject.java @@ -0,0 +1,59 @@ +package ru.otus.main_patterns.hw12; + +import java.util.HashSet; +import java.util.Set; + +public class GameObject { + private int id; + private int x, y; + private int size; + + private Set cells = new HashSet<>(); + + public GameObject(int id, int x, int y, int size) { + this.id = id; + this.x = x; + this.y = y; + this.size = size; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public Set getCells() { + return cells; + } + + public void setCells(Set cells) { + this.cells = cells; + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/Grid.java b/app/src/main/java/ru/otus/main_patterns/hw12/Grid.java new file mode 100644 index 0000000..b79c8e3 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/Grid.java @@ -0,0 +1,53 @@ +package ru.otus.main_patterns.hw12; + +import java.util.*; + +public class Grid { + private final int cellSize; + private final int offsetX; + private final int offsetY; + + private final Map cells = new HashMap<>(); + + public Grid(int cellSize, int offsetX, int offsetY) { + this.cellSize = cellSize; + this.offsetX = offsetX; + this.offsetY = offsetY; + } + + private String key(int x, int y) { + return x + "_" + y; + } + + private int cellX(int x) { + return (x + offsetX) / cellSize; + } + + private int cellY(int y) { + return (y + offsetY) / cellSize; + } + + public Set getCellsForObject(GameObject obj) { + Set result = new HashSet<>(); + + int minX = cellX(obj.getX() - obj.getSize()); + int maxX = cellX(obj.getX() + obj.getSize()); + int minY = cellY(obj.getY() - obj.getSize()); + int maxY = cellY(obj.getY() + obj.getSize()); + + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + result.add(getCell(x, y)); + } + } + return result; + } + + private Cell getCell(int x, int y) { + return cells.computeIfAbsent(key(x, y), k -> new Cell()); + } + + public Collection getAllCells() { + return cells.values(); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/HW12.java b/app/src/main/java/ru/otus/main_patterns/hw12/HW12.java new file mode 100644 index 0000000..f430361 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/HW12.java @@ -0,0 +1,111 @@ +package ru.otus.main_patterns.hw12; + +import java.util.Arrays; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.otus.main_patterns.hw12.command.MacroCommand; +import ru.otus.main_patterns.hw12.command.UpdateObjectCellCommand; + +/* + Домашнее задание + Реализация проверки коллизий игровых объектов + + Цель: + Научится применять цепочку обязанности + + Описание/Пошаговая инструкция выполнения домашнего задания: + Предположим, что у нас есть функция/процедура/метод, который умеет для двух игровых объектов + определять коллизию - столкнулись эти два объекта или нет. + Эту функцию/процедуру/метод реализовывать не нужно, так это достаточно большая задача. + Используя эту функцию, реализуем эффективный перебор всех игровых объектов, для определения всех возможных коллизий + в игре в каждый момент времени. + Для этого разобьем игровое поле на окрестности, предполагая, что объекты, находящиеся в разных окрестностях + не могут столкнуться друг с другом. Столкнуться друг с другом могут только объекты из одной окрестности. + + Необходимо: + 1. Реализовать команду, которая + - определяет окрестность, в которой присутствует игровой объект + - если объект попал в новую окрестность, то удаляет его из списка объектов старой окрестности и добавляет список + объектов новой окрестности. + - для каждого объекта новой окрестности и текущего движущегося объекта создает команду проверки коллизии этих двух объектов. + Все эти команды помещает в макрокоманду и эту макрокоманду записывает на место аналогичной макрокоманды для предыдущей окрестности. + 2. При подходе с окрестностями может оказаться, что объекты, находящиеся рядом, могут попасть в разные окрестности, + что не позволит определить коллизию между ними. + Для решения этой проблемы будем использовать две системы окрестностей - одни окрестности смещены относительно центра других окрестностей. + Для этого можно использовать несколько команд из п. 1 + 3. С помощью команды из 1. организовать проверку коллизий игровых объектов с помощью двух систем окрестностей. + + Критерии оценки: + 1. Задача сдана на проверку. 1 балл + 2. Задача оформлена в виде MR/PR. 1 балл + 3. CI - 1 балл + 4. Реализована команда из п.1 + 1 балл, если реализация правильно формирует список игровых объектов для каждой окрестности + 2 балла, если написана полная реализация + 5. Реализованы тесты к команде из п.1 + 1 балл, если написан хотя бы один тест + 2 балла, если преподаватель не может указать кейс, который не покрыт тестами. + 6. Реализован механизм с произвольным количеством систем окрестностей из п.3 2 балла + 7. Написаны тесты к п.3 1 балл + + Максимальная оценка за задание 10 баллов + Задание принимается, если было оценено не менее, чем в 7 баллов + + Решение: + Полный pipeline выполнения: + Game Tick + ↓ + UpdateObjectCellCommand (для каждого объекта) + ↓ + объект перемещается между клетками + ↓ + создаются CollisionCommand (для пар объектов) + ↓ + MacroCommand.execute() + ↓ + CollisionCommand.execute() + ↓ + handler.handle(a, b) -> Chain of Responsibility + +*/ +public class HW12 { + private static final Logger logger = LoggerFactory.getLogger(HW12.class); + + public static void main(String[] args) { + logger.info( + "Java version: {}, Java vendor: {}\nДомашнее задание#12: Реализация проверки коллизий игровых объектов", + System.getProperty("java.version"), + System.getProperty("java.vendor")); + int cellSize = 50; + + // 2 сетки (смещение!) + Grid grid1 = new Grid(cellSize, 0, 0); + Grid grid2 = new Grid(cellSize, cellSize / 2, cellSize / 2); + + List grids = Arrays.asList(grid1, grid2); + + MacroCommand macro = new MacroCommand(); + + List objects = + Arrays.asList( + new GameObject(1, 10, 10, 10), + new GameObject(2, 20, 20, 10), + new GameObject(3, 55, 55, 10), + new GameObject(4, 60, 60, 10), + new GameObject(5, 100, 100, 10), + new GameObject(6, 110, 105, 10), + new GameObject(7, 200, 200, 10), + new GameObject(8, 205, 205, 10), + new GameObject(9, 300, 300, 10), + new GameObject(10, 305, 300, 10)); + + // симуляция одного тика + for (GameObject obj : objects) { + new UpdateObjectCellCommand(obj, grids, macro).execute(); + } + + // выполнение всех коллизий + macro.execute(); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/command/CollisionCommand.java b/app/src/main/java/ru/otus/main_patterns/hw12/command/CollisionCommand.java new file mode 100644 index 0000000..560a22f --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/command/CollisionCommand.java @@ -0,0 +1,22 @@ +package ru.otus.main_patterns.hw12.command; + +import ru.otus.main_patterns.hw12.GameObject; +import ru.otus.main_patterns.hw12.handler.Handler; +import ru.otus.main_patterns.hw12.interfaces.Command; + +public class CollisionCommand implements Command { + private final GameObject gameObjectA; + private final GameObject gameObjectB; + private final Handler handler; + + public CollisionCommand(GameObject gameObjectA, GameObject gameObjectB, Handler handler) { + this.gameObjectA = gameObjectA; + this.gameObjectB = gameObjectB; + this.handler = handler; + } + + @Override + public void execute() { + handler.handle(gameObjectA, gameObjectB); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/command/MacroCommand.java b/app/src/main/java/ru/otus/main_patterns/hw12/command/MacroCommand.java new file mode 100644 index 0000000..c5ffb86 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/command/MacroCommand.java @@ -0,0 +1,28 @@ +package ru.otus.main_patterns.hw12.command; + +import java.util.ArrayList; +import java.util.List; +import ru.otus.main_patterns.hw12.interfaces.Command; + +public class MacroCommand implements Command { + private final List commands = new ArrayList<>(); + + public void add(Command c) { + commands.add(c); + } + + public void clear() { + commands.clear(); + } + + public List getCommands() { + return commands; + } + + @Override + public void execute() { + for (Command command : commands) { + command.execute(); + } + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommand.java b/app/src/main/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommand.java new file mode 100644 index 0000000..e0ab283 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommand.java @@ -0,0 +1,61 @@ +package ru.otus.main_patterns.hw12.command; + +import java.util.List; +import java.util.Set; +import ru.otus.main_patterns.hw12.Cell; +import ru.otus.main_patterns.hw12.GameObject; +import ru.otus.main_patterns.hw12.Grid; +import ru.otus.main_patterns.hw12.handler.CollisionCheckHandler; +import ru.otus.main_patterns.hw12.handler.DamageHandler; +import ru.otus.main_patterns.hw12.handler.Handler; +import ru.otus.main_patterns.hw12.interfaces.Command; + +/* + 1. Удаляет объект из старых клеток + 2. Добавляет в новые клетки (из двух grid) + 3. Генерирует CollisionCommand в MacroCommand +*/ +public class UpdateObjectCellCommand implements Command { + private final GameObject gameObject; + private final List grids; + private final MacroCommand macroCommand; + + public UpdateObjectCellCommand( + GameObject gameObject, List grids, MacroCommand macroCommand) { + this.gameObject = gameObject; + this.grids = grids; + this.macroCommand = macroCommand; + } + + @Override + public void execute() { + // 1. удалить из старых клеток + for (Cell cell : gameObject.getCells()) { + cell.getObjects().remove(gameObject); + } + gameObject.getCells().clear(); + + // 2. добавить в новые клетки (из всех grid) + for (Grid grid : grids) { + Set newCells = grid.getCellsForObject(gameObject); + for (Cell cell : newCells) { + // 3. создать команды коллизий + for (GameObject other : cell.getObjects()) { + // Chain of Responsibility + Handler handler = buildChain(this.gameObject); + macroCommand.add(new CollisionCommand(this.gameObject, other, handler)); + } + cell.getObjects().add(gameObject); + gameObject.getCells().add(cell); + } + } + } + + private Handler buildChain(GameObject gameObjectA) { + Handler handler = new CollisionCheckHandler(); + if (gameObjectA.getId() % 2 == 0) { + handler.setNext(new DamageHandler()); + } + return handler; + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/handler/CollisionCheckHandler.java b/app/src/main/java/ru/otus/main_patterns/hw12/handler/CollisionCheckHandler.java new file mode 100644 index 0000000..46ecc2f --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/handler/CollisionCheckHandler.java @@ -0,0 +1,19 @@ +package ru.otus.main_patterns.hw12.handler; + +import ru.otus.main_patterns.hw12.GameObject; + +public class CollisionCheckHandler extends Handler { + + @Override + protected boolean process(GameObject a, GameObject b) { + int dx = a.getX() - b.getX(); + int dy = a.getY() - b.getY(); + int r = a.getSize() + b.getSize(); + if (dx * dx + dy * dy <= r * r) { + System.out.println( + "\nCollisionCheckHandler, Collision detected: " + a.getId() + " & " + b.getId()); + return true; + } + return false; // остановка цепочки + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/handler/DamageHandler.java b/app/src/main/java/ru/otus/main_patterns/hw12/handler/DamageHandler.java new file mode 100644 index 0000000..3e6cb14 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/handler/DamageHandler.java @@ -0,0 +1,12 @@ +package ru.otus.main_patterns.hw12.handler; + +import ru.otus.main_patterns.hw12.GameObject; + +public class DamageHandler extends Handler { + + @Override + protected boolean process(GameObject a, GameObject b) { + System.out.println("DamageHandler, Damage applied to " + a.getId() + " and " + b.getId()); + return true; + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/handler/Handler.java b/app/src/main/java/ru/otus/main_patterns/hw12/handler/Handler.java new file mode 100644 index 0000000..0c09387 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/handler/Handler.java @@ -0,0 +1,19 @@ +package ru.otus.main_patterns.hw12.handler; + +import ru.otus.main_patterns.hw12.GameObject; + +public abstract class Handler { + private Handler next; + + public void setNext(Handler next) { + this.next = next; + } + + public void handle(GameObject gameObjectA, GameObject gameObjectB) { + if (process(gameObjectA, gameObjectB) && next != null) { + next.handle(gameObjectA, gameObjectB); + } + } + + protected abstract boolean process(GameObject a, GameObject b); +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw12/interfaces/Command.java b/app/src/main/java/ru/otus/main_patterns/hw12/interfaces/Command.java new file mode 100644 index 0000000..45d35c2 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw12/interfaces/Command.java @@ -0,0 +1,6 @@ +package ru.otus.main_patterns.hw12.interfaces; + +public interface Command { + + void execute(); +} diff --git a/app/src/test/java/ru/otus/main_patterns/hw12/command/CollisionCommandTest.java b/app/src/test/java/ru/otus/main_patterns/hw12/command/CollisionCommandTest.java new file mode 100644 index 0000000..482011b --- /dev/null +++ b/app/src/test/java/ru/otus/main_patterns/hw12/command/CollisionCommandTest.java @@ -0,0 +1,115 @@ +package ru.otus.main_patterns.hw12.command; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import ru.otus.main_patterns.hw12.GameObject; +import ru.otus.main_patterns.hw12.handler.CollisionCheckHandler; +import ru.otus.main_patterns.hw12.handler.DamageHandler; +import ru.otus.main_patterns.hw12.handler.Handler; + +class CollisionCommandTest { + + // Проверка: execute вызывает chain + @Test + void shouldCallChainHandle() { + // Arrange + GameObject a = new GameObject(1, 0, 0, 5); + GameObject b = new GameObject(2, 1, 1, 5); + Handler chain = mock(Handler.class); + CollisionCommand command = new CollisionCommand(a, b, chain); + + // Act + command.execute(); + + // Assert + verify(chain, times(1)).handle(a, b); + } + + // Проверка: передаются правильные объекты + @Test + void shouldPassCorrectObjectsToChain() { + + GameObject a = new GameObject(1, 10, 10, 5); + GameObject b = new GameObject(2, 20, 20, 5); + + Handler chain = mock(Handler.class); + + CollisionCommand command = new CollisionCommand(a, b, chain); + + command.execute(); + + verify(chain).handle(eq(a), eq(b)); + } + + // Проверка Chain (интеграционный тест) + @Test + void shouldExecuteChainHandlersOnCollision() { + + GameObject a = new GameObject(1, 0, 0, 10); + GameObject b = new GameObject(2, 5, 5, 10); // есть коллизия + + CollisionCheckHandler h1 = spy(new CollisionCheckHandler()); + DamageHandler h2 = spy(new DamageHandler()); + + h1.setNext(h2); + + CollisionCommand command = new CollisionCommand(a, b, h1); + + command.execute(); + + verify(h1, times(1)).handle(a, b); + verify(h2, times(1)).handle(a, b); + } + + // Проверка: цепочка останавливается + @Test + void shouldStopChainWhenNoCollision() { + + GameObject a = new GameObject(1, 0, 0, 5); + GameObject b = new GameObject(2, 1000, 1000, 5); // нет коллизии + + CollisionCheckHandler h1 = spy(new CollisionCheckHandler()); + DamageHandler h2 = spy(new DamageHandler()); + + h1.setNext(h2); + + CollisionCommand command = new CollisionCommand(a, b, h1); + + command.execute(); + + verify(h1, times(1)).handle(a, b); + verify(h2, never()).handle(any(), any()); + } + + // Edge case: null chain + @Test + void shouldThrowIfChainIsNull() { + + GameObject a = new GameObject(1, 0, 0, 5); + GameObject b = new GameObject(2, 1, 1, 5); + + CollisionCommand command = new CollisionCommand(a, b, null); + + assertThatThrownBy(command::execute).isInstanceOf(NullPointerException.class); + } + + // Проверка: команда не меняет состояние + @Test + void shouldNotModifyObjects() { + + GameObject a = new GameObject(1, 0, 0, 5); + GameObject b = new GameObject(2, 1, 1, 5); + + Handler chain = mock(Handler.class); + + CollisionCommand command = new CollisionCommand(a, b, chain); + + command.execute(); + + assertThat(0).isEqualTo(a.getX()); + assertThat(1).isEqualTo(b.getX()); + } +} diff --git a/app/src/test/java/ru/otus/main_patterns/hw12/command/MacroCommandTest.java b/app/src/test/java/ru/otus/main_patterns/hw12/command/MacroCommandTest.java new file mode 100644 index 0000000..9be4c1d --- /dev/null +++ b/app/src/test/java/ru/otus/main_patterns/hw12/command/MacroCommandTest.java @@ -0,0 +1,144 @@ +package ru.otus.main_patterns.hw12.command; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import ru.otus.main_patterns.hw12.interfaces.Command; + +class MacroCommandTest { + private MacroCommand macroCommand; + private Command command1; + private Command command2; + + @BeforeEach + void setUp() { + macroCommand = new MacroCommand(); + command1 = mock(Command.class); + command2 = mock(Command.class); + } + + @Test + void execute_shouldCallAllCommands() { + // Arrange + macroCommand.add(command1); + macroCommand.add(command2); + + // Act + macroCommand.execute(); + + // Assert + verify(command1, times(1)).execute(); + verify(command2, times(1)).execute(); + } + + @Test + void execute_shouldDoNothing_whenNoCommands() { + // Act + macroCommand.execute(); + + // Assert + // просто проверяем, что ничего не упало + // (взаимодействий нет) + verifyNoInteractions(command1, command2); + } + + @Test + void clear_shouldRemoveAllCommands() { + // Arrange + macroCommand.add(command1); + macroCommand.add(command2); + + // Act + macroCommand.clear(); + macroCommand.execute(); + + // Assert + verifyNoInteractions(command1, command2); + } + + @Test + void add_shouldAllowDuplicateCommands() { + // Arrange + macroCommand.add(command1); + macroCommand.add(command1); + + // Act + macroCommand.execute(); + + // Assert + verify(command1, times(2)).execute(); + } + + @Test + void execute_shouldStopOnFirstException() { + // Arrange + MacroCommand macro = new MacroCommand(); + Command ok = mock(Command.class); + Command fail = mock(Command.class); + Command never = mock(Command.class); + doThrow(new RuntimeException("boom")).when(fail).execute(); + + // Act + macro.add(ok); + macro.add(fail); + macro.add(never); + + // Assert + assertThrows(RuntimeException.class, macro::execute); + verify(ok).execute(); + verify(fail).execute(); + verify(never, never()).execute(); // важно! + } + + @Test + void execute_shouldPreserveOrder() { + // Arrange + MacroCommand macro = new MacroCommand(); + Command c1 = mock(Command.class); + Command c2 = mock(Command.class); + macro.add(c1); + macro.add(c2); + + // Act + macro.execute(); + InOrder inOrder = inOrder(c1, c2); + + // Assert + inOrder.verify(c1).execute(); + inOrder.verify(c2).execute(); + } + + @Test + void clear_thenAdd_shouldWorkCorrectly() { + // Arrange + MacroCommand macro = new MacroCommand(); + Command c = mock(Command.class); + macro.add(c); + macro.clear(); + macro.add(c); + + // Act + macro.execute(); + + // Assert + verify(c, times(1)).execute(); + } + + @Test + void execute_calledTwice_shouldExecuteTwice() { + // Arrange + MacroCommand macro = new MacroCommand(); + Command c = mock(Command.class); + macro.add(c); + + // Act + macro.execute(); + macro.execute(); + + // Assert + verify(c, times(2)).execute(); + } +} diff --git a/app/src/test/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommandTest.java b/app/src/test/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommandTest.java new file mode 100644 index 0000000..dae9056 --- /dev/null +++ b/app/src/test/java/ru/otus/main_patterns/hw12/command/UpdateObjectCellCommandTest.java @@ -0,0 +1,150 @@ +package ru.otus.main_patterns.hw12.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import org.junit.jupiter.api.Test; +import ru.otus.main_patterns.hw12.Cell; +import ru.otus.main_patterns.hw12.GameObject; +import ru.otus.main_patterns.hw12.Grid; +import ru.otus.main_patterns.hw12.handler.CollisionCheckHandler; +import ru.otus.main_patterns.hw12.handler.DamageHandler; + +class UpdateObjectCellCommandTest { + + // Базовый тест: объект попадает в клетки + @Test + void shouldAddObjectToCells() { + // Arrange + Grid grid = new Grid(50, 0, 0); + List grids = Arrays.asList(grid); + MacroCommand macro = new MacroCommand(); + GameObject obj = new GameObject(1, 10, 10, 5); + + // Act + new UpdateObjectCellCommand(obj, grids, macro).execute(); + + // Assert + assertFalse(obj.getCells().isEmpty(), "Object should be assigned to cells"); + } + + // Объект удаляется из старых клеток + @Test + void shouldRemoveObjectFromOldCells() { + // Arrange + Grid grid = new Grid(50, 0, 0); + List grids = Arrays.asList(grid); + MacroCommand macro = new MacroCommand(); + GameObject obj = new GameObject(1, 10, 10, 5); + // первый апдейт + new UpdateObjectCellCommand(obj, grids, macro).execute(); + Set oldCells = new HashSet<>(obj.getCells()); + // двигаем объект + obj.setX(200); + obj.setY(200); + + // Act + new UpdateObjectCellCommand(obj, grids, macro).execute(); + + // Assert + for (Cell c : oldCells) { + assertFalse(c.getObjects().contains(obj), "Old cell should not contain object"); + } + } + + // Объект добавляется в новые клетки + @Test + void shouldAddObjectToNewCellsAfterMove() { + // Arrange + Grid grid = new Grid(50, 0, 0); + List grids = Arrays.asList(grid); + MacroCommand macro = new MacroCommand(); + GameObject obj = new GameObject(1, 10, 10, 5); + // Act + new UpdateObjectCellCommand(obj, grids, macro).execute(); + obj.setX(200); + obj.setY(200); + new UpdateObjectCellCommand(obj, grids, macro).execute(); + boolean found = false; + for (Cell c : obj.getCells()) { + if (c.getObjects().contains(obj)) { + found = true; + } + } + + // Assert + assertTrue(found, "Object should be in new cells"); + } + + // Генерация collision-команд + @Test + void shouldCreateCollisionCommands() { + // Arrange + Grid grid = new Grid(50, 0, 0); + List grids = Arrays.asList(grid); + MacroCommand macro = new MacroCommand(); + GameObject obj1 = new GameObject(1, 10, 10, 5); + GameObject obj2 = new GameObject(2, 12, 12, 5); + + // Act + new UpdateObjectCellCommand(obj1, grids, macro).execute(); + new UpdateObjectCellCommand(obj2, grids, macro).execute(); + + // Assert + assertFalse(macro.getCommands().isEmpty(), "Collision commands should be created"); + } + + // Объект может быть в нескольких клетках + @Test + void shouldOccupyMultipleCellsIfLarge() { + // Arrange + Grid grid = new Grid(50, 0, 0); + List grids = Arrays.asList(grid); + MacroCommand macro = new MacroCommand(); + GameObject obj = new GameObject(1, 50, 50, 40); // большой радиус + + // Act + new UpdateObjectCellCommand(obj, grids, macro).execute(); + + // Assert + assertTrue(obj.getCells().size() > 1, "Object should occupy multiple cells"); + } + + // Две сетки (offset grid) + @Test + void shouldWorkWithTwoGrids() { + // Arrange + Grid grid1 = new Grid(50, 0, 0); + Grid grid2 = new Grid(50, 25, 25); + List grids = Arrays.asList(grid1, grid2); + MacroCommand macro = new MacroCommand(); + GameObject obj = new GameObject(1, 49, 49, 5); + + // Act + new UpdateObjectCellCommand(obj, grids, macro).execute(); + + // Assert + assertTrue(obj.getCells().size() >= 2, "Object should be in cells from both grids"); + } + + // Проверка Chain of Responsibility + @Test + void shouldStopChainIfNoCollision() { + // Arrange + CollisionCheckHandler h1 = new CollisionCheckHandler(); + DamageHandler h2 = spy(new DamageHandler()); + h1.setNext(h2); + + GameObject a = new GameObject(1, 0, 0, 5); + GameObject b = new GameObject(2, 1000, 1000, 5); + + // если chain работает правильно — DamageHandler НЕ вызовется + // Act + h1.handle(a, b); + + // Assert + verify(h2, never()).handle(any(), any()); + } +} From 9baf0f8921395fd4735b5a99db013ce15db8ac75 Mon Sep 17 00:00:00 2001 From: a_subscriber Date: Sat, 2 May 2026 11:46:32 +0300 Subject: [PATCH 2/2] feat: Update version --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c8ed8e3..7178b85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,7 @@ repositories { } group = "ru.otus.main_patterns" -version = "1.0.11-SNAPSHOT" +version = "1.0.12-SNAPSHOT" application { mainClass.set("ru.otus.main_patterns.App")