From cc167c70df9aa944a220200de5b46bdda7f57df3 Mon Sep 17 00:00:00 2001
From: jez04 <david.jezek@post.cz>
Date: Wed, 23 Oct 2024 22:33:04 +0200
Subject: [PATCH] feat: :tada: solution lab04

---
 src/main/java/lab/Background.java             |   9 +-
 src/main/java/lab/Boat.java                   |  32 ++-
 src/main/java/lab/Collisionable.java          |  13 +
 src/main/java/lab/DrawableSimulable.java      |   9 +
 src/main/java/lab/LochNess.java               |  25 +-
 src/main/java/lab/Rock.java                   |  12 +-
 src/main/java/lab/Scene.java                  |  44 ++--
 src/main/java/lab/WorldEntity.java            |  32 +++
 .../structure/test/ClassStructureTest.java    | 232 ++++++++++++++++++
 9 files changed, 363 insertions(+), 45 deletions(-)
 create mode 100644 src/main/java/lab/Collisionable.java
 create mode 100644 src/main/java/lab/DrawableSimulable.java
 create mode 100644 src/main/java/lab/WorldEntity.java
 create mode 100644 src/test/java/jez04/structure/test/ClassStructureTest.java

diff --git a/src/main/java/lab/Background.java b/src/main/java/lab/Background.java
index 0db811c..1e4b308 100644
--- a/src/main/java/lab/Background.java
+++ b/src/main/java/lab/Background.java
@@ -5,16 +5,15 @@ import javafx.scene.canvas.GraphicsContext;
 import javafx.scene.image.Image;
 import javafx.scene.paint.Color;
 
-public class Background {
+public class Background extends WorldEntity {
 
-	private Scene scene;
 	private Point2D[] imagePosition;
 	private Image[] image;
 	private Point2D speed;
 	private static final double[] speedMultiplier = new double[] { 0.6, 0.8, 1, 1 };
 
 	public Background(Scene scene) {
-		this.scene = scene;
+		super(scene, new Point2D(0, 0));
 		image = new Image[4];
 		imagePosition = new Point2D[4];
 		image[0] = new Image(Background.class.getResourceAsStream("cityfar400.png"));
@@ -28,7 +27,8 @@ public class Background {
 		speed = new Point2D(100, 0);
 	}
 
-	public void draw(GraphicsContext gc) {
+	@Override
+	public void drawInternal(GraphicsContext gc) {
 		gc.setFill(Color.DARKBLUE);
 		gc.fillRect(0, 0, scene.getSize().getWidth(), scene.getSize().getHeight());
 		for (int i = 0; i < image.length; i++) {
@@ -44,6 +44,7 @@ public class Background {
 		}
 	}
 
+	@Override
 	public void simulate(double deltaTime) {
 		Point2D baseSpeed = speed.multiply(deltaTime / 1_000_000_000);
 		for (int i = 0; i < image.length; i++) {
diff --git a/src/main/java/lab/Boat.java b/src/main/java/lab/Boat.java
index 574c194..ea0561c 100644
--- a/src/main/java/lab/Boat.java
+++ b/src/main/java/lab/Boat.java
@@ -1,32 +1,52 @@
 package lab;
 
+import java.util.Random;
+
 import javafx.geometry.Point2D;
 import javafx.geometry.Rectangle2D;
 import javafx.scene.canvas.GraphicsContext;
 import javafx.scene.image.Image;
 
-public class Boat {
+public class Boat extends WorldEntity implements Collisionable {
 
-	private Scene scene;
-	private Point2D position;
+	private static final Random RANDOM = new Random();
+	private Point2D speed;
 	private Image image;
 
 	public Boat(Scene scene, Point2D position) {
-		this.scene = scene;
-		this.position = position;
+		super(scene, position);
 		image = new Image(Boat.class.getResourceAsStream("ship-boat.gif"));
+		speed = new Point2D(0, 0);
 	}
 
-	public void draw(GraphicsContext gc) {
+	@Override
+	public void drawInternal(GraphicsContext gc) {
 		gc.drawImage(image, position.getX(), position.getY());
 		gc.strokeRect(position.getX(), position.getY(), image.getWidth(), image.getHeight());
 	}
 
+	@Override
 	public void simulate(double deltaTime) {
+		position = position.add(speed.multiply(deltaTime / 1_000_000_000));
+		speed = speed.multiply(0.99);
 	}
 
+	@Override
 	public Rectangle2D getBoundingBox() {
 		return new Rectangle2D(position.getX(), position.getY(), image.getWidth(), image.getHeight());
 	}
 
+	@Override
+	public boolean intersect(Rectangle2D another) {
+		return getBoundingBox().intersects(another);
+	}
+
+	@Override
+	public void hitBy(Collisionable another) {
+		if(another instanceof LochNess lochNess) {
+			int direction = RANDOM.nextBoolean()?-1:1;
+			speed = new Point2D(-20, direction * RANDOM.nextDouble(50, 100));
+		}
+	}
+
 }
diff --git a/src/main/java/lab/Collisionable.java b/src/main/java/lab/Collisionable.java
new file mode 100644
index 0000000..40a8157
--- /dev/null
+++ b/src/main/java/lab/Collisionable.java
@@ -0,0 +1,13 @@
+package lab;
+
+import javafx.geometry.Rectangle2D;
+
+public interface Collisionable {
+
+	Rectangle2D getBoundingBox();
+
+	boolean intersect(Rectangle2D another);
+
+	void hitBy(Collisionable another);
+
+}
diff --git a/src/main/java/lab/DrawableSimulable.java b/src/main/java/lab/DrawableSimulable.java
new file mode 100644
index 0000000..9930e7e
--- /dev/null
+++ b/src/main/java/lab/DrawableSimulable.java
@@ -0,0 +1,9 @@
+package lab;
+
+import javafx.scene.canvas.GraphicsContext;
+
+public interface DrawableSimulable {
+	void draw(GraphicsContext gc);
+
+	void simulate(double deltaTime);
+}
diff --git a/src/main/java/lab/LochNess.java b/src/main/java/lab/LochNess.java
index e27d7e0..79edf18 100644
--- a/src/main/java/lab/LochNess.java
+++ b/src/main/java/lab/LochNess.java
@@ -7,16 +7,15 @@ import javafx.geometry.Rectangle2D;
 import javafx.scene.canvas.GraphicsContext;
 import javafx.scene.image.Image;
 
-public class LochNess {
+public class LochNess extends WorldEntity implements Collisionable {
 
 	private static final Random RANDOM = new Random();
 
-	private Scene scene;
-	private Point2D position;
 	private Point2D speed;
 	private Image image;
 
 	public LochNess(Scene scene) {
+		super(scene, new Point2D(0, 0));
 		this.scene = scene;
 		image = new Image(LochNess.class.getResourceAsStream("LochNess.gif"));
 		position = new Point2D(RANDOM.nextDouble(scene.getSize().getWidth() * 0.3, scene.getSize().getWidth()),
@@ -24,22 +23,25 @@ public class LochNess {
 		speed = new Point2D(-RANDOM.nextDouble(50, 150), 0);
 	}
 
-	public void draw(GraphicsContext gc) {
+	@Override
+	public void drawInternal(GraphicsContext gc) {
 		gc.drawImage(image, position.getX(), position.getY());
 		gc.strokeRect(position.getX(), position.getY(), image.getWidth(), image.getHeight());
 	}
 
+	@Override
 	public void simulate(double deltaTime) {
 		position = position.add(speed.multiply(deltaTime / 1_000_000_000));
 		if (position.getX() + image.getWidth() < 0) {
 			position = new Point2D(scene.getSize().getWidth(), position.getY());
 		}
-		if (position.getX() > scene.getSize().getWidth()+5) {
+		if (position.getX() > scene.getSize().getWidth() + 5) {
 			position = new Point2D(scene.getSize().getWidth(), position.getY());
 			speed = speed.multiply(-1);
 		}
 	}
 
+	@Override
 	public Rectangle2D getBoundingBox() {
 		return new Rectangle2D(position.getX(), position.getY(), image.getWidth(), image.getHeight());
 	}
@@ -47,4 +49,17 @@ public class LochNess {
 	public void changeDirection() {
 		speed = speed.multiply(-1);
 	}
+
+	@Override
+	public boolean intersect(Rectangle2D another) {
+		return getBoundingBox().intersects(another);
+	}
+
+	@Override
+	public void hitBy(Collisionable another) {
+		if (another instanceof Boat) {
+			changeDirection();
+		}
+
+	}
 }
diff --git a/src/main/java/lab/Rock.java b/src/main/java/lab/Rock.java
index b5a1b43..82c6a27 100644
--- a/src/main/java/lab/Rock.java
+++ b/src/main/java/lab/Rock.java
@@ -7,10 +7,8 @@ import javafx.scene.paint.Color;
 import javafx.scene.transform.Affine;
 import javafx.scene.transform.Rotate;
 
-public class Rock {
+public class Rock  extends WorldEntity {
 
-	private Scene scene;
-	private Point2D position;
 	private Dimension2D size;
 	private double angle = 0;
 
@@ -19,13 +17,12 @@ public class Rock {
 	}
 
 	public Rock(Scene scene, Point2D position, Dimension2D size) {
-		this.scene = scene;
-		this.position = position;
+		super(scene, position);
 		this.size = size;
 	}
 
-	public void draw(GraphicsContext gc) {
-		gc.save();
+	@Override
+	public void drawInternal(GraphicsContext gc) {
 		gc.setFill(Color.GRAY);
 		gc.setStroke(Color.GREEN);
 		Point2D center = position.add(size.getWidth() / 2, size.getHeight() / 2);
@@ -33,7 +30,6 @@ public class Rock {
 		gc.setTransform(new Affine(rotateMatrix));
 		gc.fillRect(position.getX(), position.getY(), size.getWidth(), size.getHeight());
 		gc.strokeRect(position.getX(), position.getY(), size.getWidth(), size.getHeight());
-		gc.restore();
 	}
 
 	public void simulate(double deltaTime) {
diff --git a/src/main/java/lab/Scene.java b/src/main/java/lab/Scene.java
index eaf6e25..fd6798a 100644
--- a/src/main/java/lab/Scene.java
+++ b/src/main/java/lab/Scene.java
@@ -7,19 +7,16 @@ import javafx.scene.canvas.GraphicsContext;
 public class Scene {
 
 	private Dimension2D size;
-	private Background background;
-	private Rock rock;
-	private Boat boat;
-	private LochNess[] lochNesses;
+	private DrawableSimulable[] sceneEntitites;
 
 	public Scene(double width, double height) {
 		size = new Dimension2D(width, height);
-		background = new Background(this);
-		rock = new Rock(this, new Point2D(300, 300), new Dimension2D(30, 50));
-		boat = new Boat(this, new Point2D(20, 200));
-		lochNesses = new LochNess[5];
-		for (int i = 0; i < lochNesses.length; i++) {
-			lochNesses[i] = new LochNess(this);
+		sceneEntitites = new DrawableSimulable[8];
+		sceneEntitites[0] = new Background(this);
+		sceneEntitites[1] = new Rock(this, new Point2D(300, 300), new Dimension2D(30, 50));
+		sceneEntitites[2] = new Boat(this, new Point2D(20, 200));
+		for (int i = 3; i < sceneEntitites.length; i++) {
+			sceneEntitites[i] = new LochNess(this);
 		}
 	}
 
@@ -28,22 +25,25 @@ public class Scene {
 	}
 
 	public void draw(GraphicsContext gc) {
-		background.draw(gc);
-		rock.draw(gc);
-		boat.draw(gc);
-		for (LochNess lochNess : lochNesses) {
-			lochNess.draw(gc);
+		for (DrawableSimulable entity : sceneEntitites) {
+			entity.draw(gc);
 		}
 	}
 
 	public void simulate(double deltaTime) {
-		background.simulate(deltaTime);
-		rock.simulate(deltaTime);
-		boat.simulate(deltaTime);
-		for (LochNess lochNess : lochNesses) {
-			lochNess.simulate(deltaTime);
-			if (lochNess.getBoundingBox().intersects(boat.getBoundingBox())) {
-				lochNess.changeDirection();
+		for (DrawableSimulable drawableSimulable : sceneEntitites) {
+			drawableSimulable.simulate(deltaTime);
+		}
+		for (int i = 0; i < sceneEntitites.length; i++) {
+			if (sceneEntitites[i] instanceof Collisionable c1) {
+				for (int j = i + 1; j < sceneEntitites.length; j++) {
+					if (sceneEntitites[j] instanceof Collisionable c2) {
+						if (c1.intersect(c2.getBoundingBox())) {
+							c1.hitBy(c2);
+							c2.hitBy(c1);
+						}
+					}
+				}
 			}
 		}
 	}
diff --git a/src/main/java/lab/WorldEntity.java b/src/main/java/lab/WorldEntity.java
new file mode 100644
index 0000000..b5ae6f5
--- /dev/null
+++ b/src/main/java/lab/WorldEntity.java
@@ -0,0 +1,32 @@
+package lab;
+
+import javafx.geometry.Point2D;
+import javafx.scene.canvas.GraphicsContext;
+
+public abstract class WorldEntity implements DrawableSimulable {
+
+	protected Scene scene;
+	protected Point2D position;
+
+	public WorldEntity(Scene scene, Point2D position) {
+		this.scene = scene;
+		this.position = position;
+	}
+
+	public Point2D getPosition() {
+		return position;
+	}
+
+	public Scene getScene() {
+		return scene;
+	}
+
+	@Override
+	public final void draw(GraphicsContext gc) {
+		gc.save();
+		drawInternal(gc);
+		gc.restore();
+	}
+
+	public abstract void drawInternal(GraphicsContext gc);
+}
diff --git a/src/test/java/jez04/structure/test/ClassStructureTest.java b/src/test/java/jez04/structure/test/ClassStructureTest.java
new file mode 100644
index 0000000..c5984fb
--- /dev/null
+++ b/src/test/java/jez04/structure/test/ClassStructureTest.java
@@ -0,0 +1,232 @@
+package jez04.structure.test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.reflections.Configuration;
+import org.reflections.Reflections;
+import org.reflections.scanners.Scanners;
+import org.reflections.util.ConfigurationBuilder;
+
+import javafx.geometry.Rectangle2D;
+import javafx.scene.canvas.GraphicsContext;
+
+class ClassStructureTest {
+
+	private static final String drawableSimulableName = "DrawableSimulable";
+	private static final String collisionableName = "Collisionable";
+
+	Set<String> allClasses = getNameOfAllClasses();
+
+	@Test
+	void interfaceExistencePropertiesTest() {
+		classExist(drawableSimulableName);
+		Class<?> c = getClass(drawableSimulableName);
+		isInterface(c);
+		hasMethod(c, "draw", void.class, GraphicsContext.class);
+		hasMethod(c, "simulate", void.class);
+	}
+
+	@Test
+	void interface2ExistencePropertiesTest() {
+		classExist(collisionableName);
+		Class<?> c = getClass(collisionableName);
+		isInterface(c);
+		hasMethod(c, "getBoundingBox", Rectangle2D.class);
+		hasMethod(c, "intersect", boolean.class, Rectangle2D.class);
+		hasMethod(c, "hitBy", void.class, c);
+	}
+
+	@Test
+	void movingClassTest() {
+		classExist("LochNess");
+		Class<?> c = getClass("LochNess");
+		hasImplements(c, collisionableName);
+		hasExtends(c, "WorldEntity");
+
+		hasMethod(c, "draw", void.class, GraphicsContext.class);
+		hasMethod(c, "drawInternal", void.class, GraphicsContext.class);
+	}
+
+	@Test
+	void abstractClassTest() {
+		classExist("WorldEntity");
+		Class<?> c = getClass("WorldEntity");
+		hasImplements(c, drawableSimulableName);
+
+		hasMethod(c, true, false, "draw", void.class, GraphicsContext.class);
+		hasMethod(c, false, true, "drawInternal", void.class, GraphicsContext.class);
+	}
+
+	@Test
+	void groupClassPropertyTest() {
+		classExist("lab.Scene");
+		Class<?> c = getClass("lab.Scene");
+		hasMethod(c, "draw");
+		hasMethod(c, "simulate");
+		Class<?> drawable = getClass(drawableSimulableName);
+		hasProperty(c, ".*", drawable, true);
+	}
+
+	@Test
+	void drawingClassExistenceTest() {
+		Set<String> allClasses = getNameOfAllClasses();
+		int detectedClassCount = 0;
+		for (String className : allClasses) {
+			try {
+				Class<?> clazz = Class.forName(className);
+				List<Constructor<?>> constructorList = Arrays.asList(clazz.getConstructors());
+				List<Method> methodList = Arrays.asList(clazz.getMethods());
+				long drawMethodCount = methodList.stream().filter(m -> m.getName().contains("draw")).count();
+				long simulateMethodCount = methodList.stream().filter(m -> m.getName().contains("sim")).count();
+				if (!constructorList.isEmpty() && (drawMethodCount > 0 || simulateMethodCount > 0)) {
+					System.out.println("DETECT:" + className);
+					detectedClassCount++;
+				}
+			} catch (ClassNotFoundException e) {
+				System.out.println(String.format("Class '%s' cannot be loaded: %s", className, e.getMessage()));
+			}
+		}
+		assertTrue(detectedClassCount >= 2, "");
+	}
+
+	private void isInterface(Class<?> c) {
+		assertTrue(c.isInterface(), c.getName() + " have to be interface.");
+	}
+
+	private void classExist(String name) {
+		assertTrue(allClasses.stream().anyMatch(c -> c.endsWith(name)), "Interface " + name + " not found");
+	}
+
+	private Class<?> getClass(String name) {
+		String className = allClasses.stream().filter(c -> c.endsWith(name)).findAny().orElse(null);
+		if (className == null) {
+			Assertions.fail("Class " + name + " not found.");
+		}
+		try {
+			return Class.forName(className);
+		} catch (ClassNotFoundException e) {
+			final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			try (PrintStream ps = new PrintStream(baos, true)) {
+				e.printStackTrace(ps);
+			} catch (Exception e2) {
+				Assertions.fail(e2.getMessage());
+			}
+			String stackTrace = baos.toString();
+			Assertions.fail("Class " + name + " not found.\n" + stackTrace);
+			return null;
+		}
+	}
+
+	private void hasProperty(Class<?> classDef, String propertyNameRegexp, Class<?> type, boolean array) {
+		List<Field> fields = Arrays.asList(classDef.getDeclaredFields());
+		assertTrue(fields.stream().anyMatch(f -> {
+			if (f.getName().matches(propertyNameRegexp)) {
+				if (array) {
+					return f.getType().isArray() && f.getType().getComponentType().equals(type);
+				} else {
+					return f.getType().equals(type);
+				}
+			}
+			return false;
+		}), "No field " + propertyNameRegexp + " of type " + type.getName() + " (is array " + array + ") in class "
+				+ classDef.getName());
+	}
+
+	private void hasMethod(Class<?> interfaceDef, String methodName, Class<?> returnType) {
+		List<Method> methods = Arrays.asList(interfaceDef.getMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().contains(methodName))
+						.anyMatch(m -> m.getReturnType().equals(returnType)),
+				"Method " + methodName + " not return " + returnType.getName());
+	}
+
+	private void hasMethod(Class<?> interfaceDef, String methodName, Class<?> returnType, Class<?>... params) {
+		List<Method> methods = Arrays.asList(interfaceDef.getMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().contains(methodName))
+						.filter(m -> m.getReturnType().equals(returnType))
+						.anyMatch(m -> Arrays.asList(m.getParameterTypes()).containsAll(Arrays.asList(params))),
+				"Method " + methodName + " has no all parrams:"
+						+ Arrays.asList(params).stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+
+	private void hasMethod(Class<?> interfaceDef, boolean finalTag, boolean abstractTag, String methodName,
+			Class<?> returnType, Class<?>... params) {
+		List<Method> methods = Arrays.asList(interfaceDef.getDeclaredMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().contains(methodName))
+						.filter(m -> m.getReturnType().equals(returnType)
+								&& (Modifier.isAbstract(m.getModifiers()) == abstractTag)
+								&& (Modifier.isFinal(m.getModifiers()) == finalTag))
+						.anyMatch(m -> Arrays.asList(m.getParameterTypes()).containsAll(Arrays.asList(params))),
+				"Method " + methodName + " has no all params:"
+						+ Arrays.asList(params).stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+
+	private void hasImplements(Class<?> clazz, String... interfaceNames) {
+		List<Class<?>> interfaces = new ArrayList<>();
+		Arrays.asList(interfaceNames).stream().map(name -> getClass(name)).forEach(c -> interfaces.add(c));
+		assertTrue(Arrays.asList(clazz.getInterfaces()).containsAll(interfaces), "Class not implements all interfaces:"
+				+ interfaces.stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+
+	private void hasExtends(Class<?> clazz, String parentName) {
+		Class<?> parent = getClass(parentName);
+		assertTrue(clazz.getSuperclass().equals(parent),
+				"Class " + clazz.getName() + " not extends class " + parentName);
+	}
+
+	private void hasMethod(Class<?> interfaceDef, String methodName) {
+		List<Method> methods = Arrays.asList(interfaceDef.getMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+	}
+
+	private Set<String> getNameOfAllClasses() {
+		List<String> initClassesName = Arrays.asList("lab.Routines", "lab.App", "lab.DrawingThread");
+		for (String className : initClassesName) {
+			try {
+				Class.forName(className);
+			} catch (ClassNotFoundException e) {
+				System.out.println(String.format("Class '%s' cannot be loaded: %s", className, e.getMessage()));
+			}
+		}
+		Set<String> allClasses = new HashSet<>();
+		for (Package p : Package.getPackages()) {
+			if (p.getName().startsWith("java.") || p.getName().startsWith("com.") || p.getName().startsWith("jdk.")
+					|| p.getName().startsWith("javafx.") || p.getName().startsWith("org.")
+					|| p.getName().startsWith("sun.") || p.getName().startsWith("javax.")
+					|| p.getName().startsWith("javassist")) {
+				continue;
+			}
+			System.out.println(p.getName());
+			Configuration conf = new ConfigurationBuilder().addScanners(Scanners.SubTypes.filterResultsBy(pc -> true))
+					.forPackages(p.getName());
+			Reflections reflections = new Reflections(conf);
+			allClasses.addAll(reflections.getAll(Scanners.SubTypes.filterResultsBy(c -> {
+				System.out.println(c);
+				return true;
+			})));
+		}
+		return allClasses;
+	}
+}
-- 
GitLab