From 948fbdafbeb28b0162f10f40b456f6de42371660 Mon Sep 17 00:00:00 2001 From: Jonathan Neufeld Date: Sat, 20 Jun 2020 10:44:12 -0700 Subject: [PATCH] Initial commit of the Hydrazine path-finding engine (taken from 7654f9f1229f6c7bb20ba4927c3bcea6e3ebd9d4 in MMM repo) --- .gitignore | 2 + LICENSE | 25 + NOTICE | 5 + build.gradle | 16 + .../gaming/ai/path/HydrazinePathFinder.java | 999 ++++++++++++++++++ .../extollit/gaming/ai/path/IConfigModel.java | 147 +++ .../gaming/ai/path/SchedulingPriority.java | 46 + .../ai/path/model/AreaOcclusionProvider.java | 140 +++ .../gaming/ai/path/model/Element.java | 99 ++ .../ai/path/model/HydrazinePathPoint.java | 167 +++ .../ai/path/model/IBlockDescription.java | 115 ++ .../gaming/ai/path/model/IBlockObject.java | 28 + .../gaming/ai/path/model/IColumnarSpace.java | 68 ++ .../ai/path/model/IDynamicMovableObject.java | 44 + .../gaming/ai/path/model/IInstanceSpace.java | 51 + .../ai/path/model/IOcclusionProvider.java | 38 + .../gaming/ai/path/model/IPathingEntity.java | 139 +++ .../extollit/gaming/ai/path/model/Logic.java | 110 ++ .../gaming/ai/path/model/OcclusionField.java | 849 +++++++++++++++ .../gaming/ai/path/model/Passibility.java | 40 + .../gaming/ai/path/model/PathObject.java | 361 +++++++ .../ai/path/model/SortedPointQueue.java | 174 +++ .../AbstractHydrazinePathFinderTests.java | 157 +++ .../ai/path/FatHydrazinePathFinderTests.java | 64 ++ .../gaming/ai/path/FatIntegrationTests.java | 39 + .../ai/path/HydrazinePathFinderTests.java | 450 ++++++++ .../gaming/ai/path/IntegrationTests.java | 392 +++++++ .../ai/path/TestableHydrazinePathFinder.java | 16 + .../gaming/ai/path/TestingBlocks.java | 179 ++++ ...AbstractAreaInitOcclusionFieldTesting.java | 75 ++ .../model/AbstractOcclusionFieldTesting.java | 45 + .../AbstractOcclusionProviderTesting.java | 46 + .../model/AreaInitOcclusionFieldTests.java | 279 +++++ .../model/AreaSetOcclusionFieldTests.java | 302 ++++++ .../gaming/ai/path/model/DirectLineTests.java | 305 ++++++ .../path/model/DoorOcclusionFieldTests.java | 60 ++ .../ai/path/model/IntegrationTests.java | 101 ++ .../ai/path/model/OcclusionFieldTests.java | 541 ++++++++++ .../gaming/ai/path/model/PathObjectTests.java | 281 +++++ .../gaming/ai/path/model/PathObjectUtil.java | 15 + .../ai/path/model/TestOcclusionField.java | 17 + 41 files changed, 7027 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 build.gradle create mode 100644 src/main/java/com/extollit/gaming/ai/path/HydrazinePathFinder.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/IConfigModel.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/SchedulingPriority.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/AreaOcclusionProvider.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/Element.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/HydrazinePathPoint.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IBlockDescription.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IBlockObject.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IColumnarSpace.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IDynamicMovableObject.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IInstanceSpace.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IOcclusionProvider.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/IPathingEntity.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/Logic.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/OcclusionField.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/Passibility.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/PathObject.java create mode 100644 src/main/java/com/extollit/gaming/ai/path/model/SortedPointQueue.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/AbstractHydrazinePathFinderTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/FatHydrazinePathFinderTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/FatIntegrationTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/HydrazinePathFinderTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/IntegrationTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/TestableHydrazinePathFinder.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/TestingBlocks.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/AbstractAreaInitOcclusionFieldTesting.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionFieldTesting.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionProviderTesting.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/AreaInitOcclusionFieldTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/AreaSetOcclusionFieldTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/DirectLineTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/DoorOcclusionFieldTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/IntegrationTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/OcclusionFieldTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/PathObjectTests.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/PathObjectUtil.java create mode 100644 src/test/java/com/extollit/gaming/ai/path/model/TestOcclusionField.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fe56f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out +build diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f618dcf --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ + Copyright 2020 Jonathan B. Neufeld (extollIT Enterprises) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + HYDRAZINE PATH-FINDING ENGINE SUBCOMPONENTS: + + Hydrazine Path-Finding Engine includes a number of subcomponents with + separate copyright notices and license terms. Your use of the source + code for these subcomponents is subject to the terms and + conditions of the following licenses. + + This product bundles extollIT Data Structures 2.7, + copyright Jonathan B. Neufeld, extollIT Enterprises, + which is also available under the Apache 2.0 license. + diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..2d415ac --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Hydrazine Path-Finding Engine +Copyright 2020 Jonathan B. Neufeld, extollIT Enterprises + +This product includes software developed at +extollIT Enterprises (http://www.extollit.com/). diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..855a9d0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +group 'com.extollit.gaming' +version '1.02' + +apply plugin: 'java' + +compileJava { + sourceCompatibility = '1.7' + targetCompatibility = '1.7' +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: "org.mockito", name: "mockito-core", version: "1.10.19" + + compile project(':data-structures') +} diff --git a/src/main/java/com/extollit/gaming/ai/path/HydrazinePathFinder.java b/src/main/java/com/extollit/gaming/ai/path/HydrazinePathFinder.java new file mode 100644 index 0000000..73f7f0a --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/HydrazinePathFinder.java @@ -0,0 +1,999 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.collect.SparseSpatialMap; +import com.extollit.gaming.ai.path.model.*; +import com.extollit.linalg.immutable.AxisAlignedBBox; +import com.extollit.linalg.immutable.IntAxisAlignedBox; +import com.extollit.linalg.immutable.Vec3i; +import com.extollit.linalg.mutable.Vec3d; +import com.extollit.num.FastMath; +import com.extollit.num.FloatRange; + +import java.text.MessageFormat; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +import static com.extollit.num.FastMath.ceil; +import static com.extollit.num.FastMath.floor; +import static java.lang.Math.round; + +public class HydrazinePathFinder { + private static final AxisAlignedBBox FULL_BOUNDS = new AxisAlignedBBox(0, 0, 0, 1, 1, 1); + + private static double DOT_THRESHOLD = 0.6; + private static int + MAX_SAFE_FALL_DISTANCE = 4, + MAX_SURVIVE_FALL_DISTANCE = 20, + CESA_LIMIT = 16; + private static FloatRange + PROBATIONARY_TIME_LIMIT = new FloatRange(12, 18), + PASSIBLE_POINT_TIME_LIMIT = new FloatRange(4, 9); + + private static byte FAILURE_COUNT_THRESHOLD = 3; + + private final SortedPointQueue queue = new SortedPointQueue(); + private final SparseSpatialMap nodeMap = new SparseSpatialMap<>(3); + private final Set unreachableFromSource = new HashSet<>(3); + private final IPathingEntity subject; + private final IInstanceSpace instanceSpace; + + private com.extollit.linalg.mutable.Vec3d sourcePosition, destinationPosition; + private IDynamicMovableObject destinationEntity; + + private IOcclusionProvider occlusionProvider; + private PathObject currentPath; + private IPathingEntity.Capabilities capabilities; + private HydrazinePathPoint source, target, closest; + private int cx0, cxN, cz0, czN, discreteSize, tall, initComputeIterations, periodicComputeIterations; + private int failureCount; + private float searchRangeSquared, passiblePointPathTimeLimit, nextGraphCacheReset, actualSize; + private Random random = new Random(); + + public static void configureFrom(IConfigModel configModel) { + FAILURE_COUNT_THRESHOLD = configModel.failureCountThreshold(); + PROBATIONARY_TIME_LIMIT = configModel.probationaryTimeLimit(); + PASSIBLE_POINT_TIME_LIMIT = configModel.passiblePointTimeLimit(); + DOT_THRESHOLD = configModel.dotThreshold(); + MAX_SAFE_FALL_DISTANCE = configModel.safeFallDistance(); + MAX_SURVIVE_FALL_DISTANCE = configModel.surviveFallDistance(); + CESA_LIMIT = configModel.cesaLimit(); + } + + public HydrazinePathFinder(IPathingEntity entity, IInstanceSpace instanceSpace) { + this.subject = entity; + this.instanceSpace = instanceSpace; + + applySubject(); + schedulingPriority(SchedulingPriority.low); + + this.passiblePointPathTimeLimit = PASSIBLE_POINT_TIME_LIMIT.next(this.random); + } + + public void setRandomNumberGenerator(Random random) { + this.random = random; + } + + public void schedulingPriority(SchedulingPriority schedulingPriority) { + this.initComputeIterations = schedulingPriority.initComputeIterations; + this.periodicComputeIterations = schedulingPriority.periodicComputeIterations; + } + + public final Vec3i trackingDestination() { + if (this.destinationEntity != null && this.destinationPosition != null) { + final HydrazinePathPoint pointAtDestination = pointAtDestination(); + if (impassible(pointAtDestination)) + return null; + else + return pointAtDestination.key; + } else + return null; + } + public final Vec3i currentTarget() { return this.target == null ? null : this.target.key; } + + public PathObject trackPathTo(IDynamicMovableObject target) { + this.destinationEntity = target; + return initiatePathTo(target.coordinates()); + } + + public PathObject initiatePathTo(com.extollit.linalg.immutable.Vec3d coordinates) { + return initiatePathTo(coordinates.x, coordinates.y, coordinates.z); + } + + public PathObject initiatePathTo(double x, double y, double z) { + updateSourcePosition(); + applySubject(); + + final float rangeSquared = this.searchRangeSquared; + final com.extollit.linalg.immutable.Vec3d sourcePos = new com.extollit.linalg.immutable.Vec3d(this.sourcePosition); + if (sourcePos.subOf(x, y, z).mg2() > rangeSquared) + return null; + + updateDestination(x, y, z); + + if (!graphTimeout() && (reachedTarget() || triageTimeout() || destinationDeviatedFromTarget())) + resetTriage(); + + return triage(this.initComputeIterations); + } + + public PathObject update() { + if (this.destinationEntity != null) + updateDestination(this.destinationEntity.coordinates()); + + if (this.destinationPosition == null) + return this.currentPath; + + graphTimeout(); + + updateSourcePosition(); + if (reachedTarget() || triageTimeout() || destinationDeviatedFromTarget()) + resetTriage(); + + return triage(this.periodicComputeIterations); + } + + private boolean destinationDeviatedFromTarget() { + final com.extollit.linalg.mutable.Vec3d + dt = new com.extollit.linalg.mutable.Vec3d(this.target.key), + dd = new com.extollit.linalg.mutable.Vec3d(destinationPosition); + + dd.x = floor(dd.x); + dd.y = ceil(dd.y); + dd.z = floor(dd.z); + + final Vec3i source = this.source.key; + dt.sub(source); + dd.sub(source); + + if (dt.mg2() > dd.mg2()) + return true; + + dt.normalize(); + dd.normalize(); + + return dt.dot(dd) < DOT_THRESHOLD; + } + + private boolean triageTimeout() { + final PathObject currentPath = this.currentPath; + final boolean status = + currentPath != null && + !currentPath.done() && + currentPath.length() > 0 && + currentPath.stagnantFor(this.subject) > this.passiblePointPathTimeLimit; + + if (status) { + if (++this.failureCount == 1) + this.nextGraphCacheReset = pathTimeAge() + PROBATIONARY_TIME_LIMIT.next(this.random); + + final Vec3i culprit = currentPath.at(currentPath.i); + this.nodeMap.remove(culprit); + } + + return status; + } + + private boolean graphTimeout() { + if (this.failureCount >= FAILURE_COUNT_THRESHOLD + && pathTimeAge() > this.nextGraphCacheReset) + { + resetGraph(); + return true; + } + + return false; + } + + private void adjustPathPosition(PathObject formerPath, PathObject currentPath) { + double minSquareDistFromSource = Double.MAX_VALUE; + final Vec3i + sourceKey = this.source.key, + currentPoint = formerPath.current(); + + final int + sourceX = sourceKey.x, + sourceY = sourceKey.y, + sourceZ = sourceKey.z; + + int c = -1; + while(++c < currentPath.length()) { + final Vec3i p = currentPath.at(c); + if (p.equals(currentPoint)) { + currentPath.i = c; + break; + } + + final int + dx = p.x - sourceX, + dy = p.y - sourceY, + dz = p.z - sourceZ, + + squareDelta = dx * dx + dy * dy + dz * dz; + + if (squareDelta < minSquareDistFromSource) { + minSquareDistFromSource = squareDelta; + currentPath.i = c; + } + } + } + + protected final void resetTriage() { + final Vec3d + sourcePosition = this.sourcePosition, + destinationPosition = this.destinationPosition; + + updateFieldWindow( + floor(sourcePosition.x), + floor(sourcePosition.z), + + floor(destinationPosition.x), + floor(destinationPosition.z) + ); + + applySubject(); + + final HydrazinePathPoint source = this.source = pointAtSource(); + source.length(0); + source.orphan(); + + refinePassibility(source.key); + + setTargetFor(source); + + for (HydrazinePathPoint p : this.nodeMap.values()) + p.first(false); + + this.queue.clear(); + this.queue.add(source); + this.closest = source; + this.passiblePointPathTimeLimit = PASSIBLE_POINT_TIME_LIMIT.next(this.random); + } + + protected final boolean refinePassibility(Vec3i sourcePoint) { + this.unreachableFromSource.clear(); + + if (!fuzzyPassibility(sourcePoint.x, sourcePoint.y, sourcePoint.z)) + return false; + + final IBlockObject blockObject = this.instanceSpace.blockObjectAt(sourcePoint.x, sourcePoint.y, sourcePoint.z); + if (!blockObject.isImpeding()) + return false; + + final AxisAlignedBBox bounds = blockObject.bounds(); + final Vec3d + c = new Vec3d(subject.coordinates()); + + c.sub(sourcePoint); + final com.extollit.linalg.immutable.Vec3d delta = new com.extollit.linalg.immutable.Vec3d(c); + + c.sub(bounds.center()); + + boolean mutated = false; + + if (delta.z >= bounds.min.z && delta.z <= bounds.max.z) { + final int x = sourcePoint.x + (c.x < 0 ? +1 : -1); + + for (int dz = -1; dz <= +1; ++dz) + this.unreachableFromSource.add( + new Vec3i(x, sourcePoint.y, sourcePoint.z + dz) + ); + + mutated = true; + } + + if (delta.x >= bounds.min.x && delta.x <= bounds.max.x) { + final int z = sourcePoint.z + (c.z < 0 ? +1 : -1); + + for (int dx = -1; dx <= +1; ++dx) + this.unreachableFromSource.add( + new Vec3i(sourcePoint.x + dx, sourcePoint.y, z) + ); + + mutated = true; + } + + return mutated; + } + + private void applySubject() { + this.discreteSize = FastMath.floor((this.actualSize = this.subject.width()) + 1); + this.tall = FastMath.floor(this.subject.height() + 1); + final float pathSearchRange = this.subject.searchRange(); + this.searchRangeSquared = pathSearchRange*pathSearchRange; + this.capabilities = this.subject.capabilities(); + } + + private void setTargetFor(HydrazinePathPoint source) { + final Vec3d + destinationPosition = this.destinationPosition; + + int distance = 0x40; + this.target = pointAtDestination(); + while (distance > 0 && !source.target(this.target)) { + final Vec3d + v = new Vec3d(destinationPosition), + init = new Vec3d(source.key); + + distance--; + + v.sub(init); + v.normalize(); + v.mul(distance); + v.add(init); + this.target = cachedPointAt( + floor(v.x), + ceil(v.y), + floor(v.z) + ); + } + + if (distance == 0) + source.target(this.target = this.source); + } + + private void resetGraph() { + this.nodeMap.clear(); + resetTriage(); + this.failureCount = 0; + this.nextGraphCacheReset = 0; + } + + private void updateFieldWindow(PathObject path) { + Vec3i pp = path.last(); + if (pp == null) + return; + + final com.extollit.linalg.mutable.Vec3i + min = new com.extollit.linalg.mutable.Vec3i(pp.x, pp.y, pp.z), + max = new com.extollit.linalg.mutable.Vec3i(pp.x, pp.y, pp.z); + + if (!path.done()) + for (int c = path.i; c < path.length(); ++c) { + pp = path.at(c); + if (pp.x < min.x) + min.x = pp.x; + if (pp.y < min.y) + min.y = pp.y; + if (pp.z < min.z) + min.z = pp.z; + + if (pp.x > max.x) + max.x = pp.x; + if (pp.y > max.y) + max.y = pp.y; + if (pp.z > max.z) + max.z = pp.z; + } + + updateFieldWindow(min.x, min.z, max.x, max.z); + } + private void updateFieldWindow(int sourceX, int sourceZ, int targetX, int targetZ) { + int x0, xN, z0, zN; + + if (sourceX > targetX) { + x0 = targetX; + xN = sourceX; + } else { + x0 = sourceX; + xN = targetX; + } + + if (sourceZ > targetZ) { + z0 = targetZ; + zN = sourceZ; + } else { + z0 = sourceZ; + zN = targetZ; + } + + final IDynamicMovableObject destinationEntity = this.destinationEntity; + final float entityWidth = this.subject.width(); + int entitySize = ceil( + destinationEntity != null ? + Math.max(entityWidth, destinationEntity.width()) : + entityWidth + ); + + final float searchAreaRange = this.subject.searchRange(); + x0 -= searchAreaRange + entitySize; + z0 -= searchAreaRange + entitySize; + xN += searchAreaRange + entitySize; + zN += searchAreaRange + entitySize; + + updateFieldWindow(x0, z0, xN, zN, true); + } + + private void updateFieldWindow(int x0, int z0, int xN, int zN, boolean cull) { + final int + cx0 = x0 >> 4, + cz0 = z0 >> 4, + cxN = xN >> 4, + czN = zN >> 4; + + final IOcclusionProvider aop = this.occlusionProvider; + final boolean windowTest; + + if (cull) + windowTest = cx0 != this.cx0 || cz0 != this.cz0 || cxN != this.cxN || czN != this.czN; + else + windowTest = cx0 < this.cx0 || cz0 < this.cz0 || cxN > this.cxN || czN > this.czN; + + if (aop == null || windowTest) { + this.occlusionProvider = this.instanceSpace.occlusionProviderFor(cx0, cz0, cxN, czN); + this.cx0 = cx0; + this.cz0 = cz0; + this.cxN = cxN; + this.czN = czN; + + if (cull) + this.nodeMap.cullOutside(new IntAxisAlignedBox(x0, Integer.MIN_VALUE, z0, xN, Integer.MAX_VALUE, zN)); + } + } + + private HydrazinePathPoint pointAtDestination() { + final Vec3d destinationPosition = this.destinationPosition; + if (destinationPosition == null) + return null; + + return cachedPointAt( + floor(destinationPosition.x), + ceil(destinationPosition.y), + floor(destinationPosition.z) + ); + } + + private HydrazinePathPoint pointAtSource() { + final Vec3d sourcePosition = this.sourcePosition; + final int + x = floor(sourcePosition.x), + y = floor(sourcePosition.y), + z = floor(sourcePosition.z); + + HydrazinePathPoint candidate = cachedPassiblePointNear(x, y, z, null); + if (candidate == null) { + candidate = cachedPointAt(x, y, z); + candidate.passibility(Passibility.impassible); + } + return candidate; + } + + private void updateDestination(double x, double y, double z) { + if (this.destinationPosition != null) { + final Vec3d destinationPosition = this.destinationPosition; + destinationPosition.x = x; + destinationPosition.y = y; + destinationPosition.z = z; + } else + this.destinationPosition = new Vec3d(x, y, z); + } + + private void updateDestination(com.extollit.linalg.immutable.Vec3d coordinates) { + if (this.destinationPosition != null) + this.destinationPosition.set(coordinates); + else + this.destinationPosition = new Vec3d(coordinates); + } + + private boolean reachedTarget() { + final boolean flag = this.target == null || this.source == this.target || pointAtSource() == this.target; + if (flag) { + this.failureCount = 0; + this.passiblePointPathTimeLimit = PASSIBLE_POINT_TIME_LIMIT.next(this.random); + } + return flag; + } + + private void updateSourcePosition() { + final com.extollit.linalg.immutable.Vec3d coordinates = this.subject.coordinates(); + if (this.sourcePosition != null) { + final Vec3d sourcePosition; + sourcePosition = this.sourcePosition; + sourcePosition.x = coordinates.x; + sourcePosition.y = coordinates.y; + sourcePosition.z = coordinates.z; + } else + this.sourcePosition = new Vec3d(coordinates.x, coordinates.y, coordinates.z); + } + + public void reset() { + this.currentPath = null; + this.queue.clear(); + this.nodeMap.clear(); + this.unreachableFromSource.clear(); + this.target = + this.source = + this.closest = null; + this.sourcePosition = + this.destinationPosition = null; + this.destinationEntity = null; + this.occlusionProvider = null; + this.failureCount = 0; + this.nextGraphCacheReset = 0; + this.passiblePointPathTimeLimit = PASSIBLE_POINT_TIME_LIMIT.next(this.random); + } + + private PathObject updatePath(PathObject newPath) { + if (newPath != null && !newPath.done()) { + if (this.currentPath != null) { + if (this.currentPath.sameAs(newPath)) + return this.currentPath; + else if (!this.currentPath.done()) + adjustPathPosition(this.currentPath, newPath); + } + + if (this.occlusionProvider == null) + updateFieldWindow(newPath); + + return this.currentPath = newPath; + } else + return this.currentPath = null; + } + + private PathObject triage(int iterations) { + PathObject currentPath = this.currentPath; + + final SortedPointQueue queue = this.queue; + + if (queue.isEmpty()) + if (currentPath == null) + return null; + else if (!currentPath.done()) + return currentPath; + else + resetTriage(); + + currentPath = null; + while (!queue.isEmpty() && iterations-- > 0) { + final HydrazinePathPoint current = queue.dequeue(); + + if (this.closest == null || HydrazinePathPoint.squareDelta(current, this.target) < HydrazinePathPoint.squareDelta(this.closest, this.target)) + this.closest = current; + + if (current == this.target) { + currentPath = PathObject.fromHead(this.capabilities.speed(), this.target); + if (currentPath != null && !currentPath.done()) { + currentPath.setRandomNumberGenerator(this.random); + this.queue.clear(); + break; + } + + resetTriage(); + continue; + } + + processNode(current); + } + + if (currentPath == null && this.closest != null && !queue.isEmpty()) + currentPath = PathObject.fromHead(this.capabilities.speed(), this.closest); + + return updatePath(currentPath); + } + + private void processNode(HydrazinePathPoint current) { + current.first(true); + + final Vec3i coords = current.key; + final HydrazinePathPoint + west = cachedPassiblePointNear(coords.x - 1, coords.y, coords.z, coords), + east = cachedPassiblePointNear(coords.x + 1, coords.y, coords.z, coords), + north = cachedPassiblePointNear(coords.x, coords.y, coords.z - 1, coords), + south = cachedPassiblePointNear(coords.x, coords.y, coords.z + 1, coords); + + final boolean found = applyPointOptions(current, west, east, north, south); + + if (!found) { + com.extollit.linalg.mutable.AxisAlignedBBox + southBounds = blockBounds(coords, 0, 0, +1), + northBounds = blockBounds(coords, 0, 0, -1), + eastBounds = blockBounds(coords, +1, 0, 0), + westBounds = blockBounds(coords, -1, 0, 0); + + final float actualSizeSquared = this.actualSize * this.actualSize; + final HydrazinePathPoint[] pointOptions = { + westBounds == null || northBounds == null || westBounds.mg2(northBounds) >= actualSizeSquared ? cachedPassiblePointNear(coords.x - 1, coords.y, coords.z - 1, coords) : null, + eastBounds == null || southBounds == null || eastBounds.mg2(southBounds) >= actualSizeSquared ? cachedPassiblePointNear(coords.x + 1, coords.y, coords.z + 1, coords) : null, + eastBounds == null || northBounds == null || eastBounds.mg2(northBounds) >= actualSizeSquared ? cachedPassiblePointNear(coords.x + 1, coords.y, coords.z - 1, coords) : null, + westBounds == null || southBounds == null || westBounds.mg2(southBounds) >= actualSizeSquared ? cachedPassiblePointNear(coords.x - 1, coords.y, coords.z + 1, coords) : null + }; + + applyPointOptions(current, pointOptions); + } + } + + private com.extollit.linalg.mutable.AxisAlignedBBox blockBounds(Vec3i coords, int dx, int dy, int dz) { + final int + x = coords.x + dx, + y = coords.y + dy, + z = coords.z + dz; + + final AxisAlignedBBox bounds; + final byte flags = this.occlusionProvider.elementAt(x, y, z); + if (fuzzyPassibility(flags)) { + final IBlockObject block = instanceSpace.blockObjectAt(x, y, z); + if (!block.isImpeding()) + return null; + + bounds = block.bounds(); + } else if (impassible(flags)) + bounds = FULL_BOUNDS; + else + return null; + + final com.extollit.linalg.mutable.AxisAlignedBBox result = new com.extollit.linalg.mutable.AxisAlignedBBox(bounds); + result.add(dx, dy, dz); + return result; + } + + private boolean applyPointOptions(HydrazinePathPoint current, HydrazinePathPoint... pointOptions) { + boolean found = false; + for (HydrazinePathPoint alternative : pointOptions) { + if (impassible(alternative) || alternative.first() || HydrazinePathPoint.squareDelta(alternative, this.target) >= this.searchRangeSquared) + continue; + + found = true; + this.queue.appendTo(alternative, current, target); + } + return found; + } + + private boolean impassible(HydrazinePathPoint alternative) { + return alternative == null || impassible(alternative.passibility()); + } + + private HydrazinePathPoint cachedPointAt(int x, int y, int z) + { + final Vec3i coords = new Vec3i(x, y, z); + HydrazinePathPoint point = this.nodeMap.get(coords); + + if (point == null) + this.nodeMap.put(coords, point = new HydrazinePathPoint(coords)); + + return point; + } + + protected final HydrazinePathPoint cachedPassiblePointNear(final int x0, final int y0, final int z0, final Vec3i origin) { + final Vec3i coords0 = new Vec3i(x0, y0, z0); + HydrazinePathPoint point = this.nodeMap.get(coords0); + + if (point == null) { + point = passiblePointNear(coords0, origin); + if (point != null) + this.nodeMap.put(coords0, point); + } + + if (point != null && origin != null && unreachableFromSource(origin, coords0)) + return null; + + return point; + } + + protected HydrazinePathPoint passiblePointNear(Vec3i coords0, Vec3i origin) { + final HydrazinePathPoint point; + final IOcclusionProvider op = this.occlusionProvider; + final int + x0 = coords0.x, + y0 = coords0.y, + z0 = coords0.z; + + final Vec3i d; + + if (origin != null) + d = coords0.subOf(origin); + else + d = Vec3i.ZERO; + + final boolean hasOrigin = d != Vec3i.ZERO && !d.equals(Vec3i.ZERO); + + final boolean + climbsLadders = this.capabilities.climber(); + + Passibility passibility = Passibility.passible; + + int minY = Integer.MIN_VALUE; + float minPartY = 0; + + for (int r = this.discreteSize / 2, + x = x0 - r, + xN = x0 + this.discreteSize - r; + + x < xN; + + ++x + ) + for (int z = z0 - r, + zN = z0 + this.discreteSize - r; + + z < zN; + + ++z + ) { + int y = y0; + + float partY = topOffsetAt( + x - d.x, + y - d.y - 1, + z - d.z + ); + + byte flags = op.elementAt(x, y, z); + if (impassible(flags)) { + final float partialDisparity = partY - topOffsetAt(flags, x, y++, z); + flags = op.elementAt(x, y, z); + + if (partialDisparity < 0 || impassible(flags)) { + if (!hasOrigin) + return null; + + if (d.x * d.x + d.z * d.z <= 1) { + y -= d.y + 1; + + do + flags = op.elementAt(x - d.x, y++, z - d.z); + while (climbsLadders && Logic.climbable(flags)); + } + + if (impassible(flags = op.elementAt(x, --y, z)) && (impassible(flags = op.elementAt(x, ++y, z)) || partY < 0)) + return null; + } + } + partY = topOffsetAt(x, y - 1, z); + final int ys; + passibility = verticalClearanceAt(this.tall, flags, passibility, d, x, ys = y, z, partY); + + boolean swimable = false; + for (int j = 0; unstable(flags) && !(swimable = swimable(flags)) && j <= MAX_SURVIVE_FALL_DISTANCE; j++) + flags = op.elementAt(x, --y, z); + + if (swimable) { + final int cesaLimit = y + CESA_LIMIT; + final byte flags00 = flags; + byte flags0; + do { + flags0 = flags; + flags = op.elementAt(x, ++y, z); + } while (swimable(flags) && unstable(flags) && y < cesaLimit); + if (y >= cesaLimit) { + y -= CESA_LIMIT + 1; + flags = flags00; + } else { + y--; + flags = flags0; + } + } + + partY = topOffsetAt(flags, x, y++, z); + passibility = verticalClearanceAt(ys - y, op.elementAt(x, y, z), passibility, d, x, y, z, partY); + + if (y > minY) { + minY = y; + minPartY = partY; + } else if (y == minY && partY > minPartY) + minPartY = partY; + + passibility = passibility.between(passibility(op.elementAt(x, y, z))); + if (impassible(passibility)) + return null; + } + + if (hasOrigin && !impassible(passibility)) + passibility = originHeadClearance(passibility, origin, minY, minPartY); + + passibility = fallingSafety(passibility, y0, minY); + + if (impassible(passibility)) + return null; + + point = new HydrazinePathPoint(new Vec3i(x0, minY + round(minPartY), z0)); + point.passibility(passibility); + + return point; + } + + protected final boolean unreachableFromSource(Vec3i current, Vec3i target) { + final HydrazinePathPoint source = this.source; + return source != null && current.equals(source.key) && this.unreachableFromSource.contains(target); + } + + private Passibility fallingSafety(Passibility passibility, int y0, int minY) { + final int dy = y0 - minY; + if (dy > 1) + passibility = passibility.between( + dy > MAX_SAFE_FALL_DISTANCE ? + Passibility.dangerous : + Passibility.risky + ); + return passibility; + } + + private Passibility verticalClearanceAt(int max, byte flags, Passibility passibility, Vec3i d, int x, int y, int z, float partY) { + final IOcclusionProvider op = this.occlusionProvider; + byte clearanceFlags = flags; + final int + yMax = y + max, + yN = Math.max(y, y - d.y) + this.tall; + int yt = y; + + for (int yNa = yN + floor(partY); + + yt < yNa && yt < yMax; + + clearanceFlags = op.elementAt(x, ++yt, z) + ) + passibility = passibility.between(clearance(clearanceFlags)); + + if (yt < yN && yt < yMax && (insufficientHeadClearance(clearanceFlags, partY, x, yt, z))) + passibility = passibility.between(clearance(clearanceFlags)); + + return passibility; + } + + private Passibility originHeadClearance(Passibility passibility, Vec3i origin, int minY, float minPartY) { + final IOcclusionProvider op = this.occlusionProvider; + final int + yN = minY + this.tall, + yNa = yN + floor(minPartY); + + for (int x = origin.x, xN = origin.x + this.discreteSize; x < xN; ++x) + for (int z = origin.z, zN = origin.z + this.discreteSize; z < zN; ++z) + for (int y = origin.y + this.tall; y < yNa; ++y) + passibility = passibility.between(clearance(op.elementAt(x, y, z))); + + if (yNa < yN) + for (int x = origin.x, xN = origin.x + this.discreteSize; x < xN; ++x) + for (int z = origin.z, zN = origin.z + this.discreteSize; z < zN; ++z) { + final byte flags = op.elementAt(x, yNa, z); + if (insufficientHeadClearance(flags, minPartY, x, yNa, z)) + passibility = passibility.between(clearance(flags)); + } + + return passibility; + } + + private boolean insufficientHeadClearance(byte flags, float partialY0, int x, int yN, int z) { + return bottomOffsetAt(flags, x, yN, z) + partialY0 > 0; + } + + private float topOffsetAt(int x, int y, int z) { + return topOffsetAt(occlusionProvider.elementAt(x, y, z), x, y, z); + } + + private float topOffsetAt(byte flags, int x, int y, int z) { + if (Element.air.in(flags) + || Logic.climbable(flags) + || Element.earth.in(flags) && Logic.nothing.in(flags) + ) + return 0; + + if (swimmingRequiredFor(flags)) + return -0.5f; + + final IBlockObject block = this.instanceSpace.blockObjectAt(x, y, z); + if (!block.isImpeding()) + return 0; + + return (float)block.bounds().max.y - 1; + } + + private float bottomOffsetAt(byte flags, int x, int y, int z) { + if (Element.air.in(flags) + || Logic.climbable(flags) + || Element.earth.in(flags) && Logic.nothing.in(flags) + || swimmingRequiredFor(flags) + ) + return 0; + + final IBlockObject block = this.instanceSpace.blockObjectAt(x, y, z); + if (!block.isImpeding()) + return 0; + + return (float) block.bounds().min.y; + } + + private boolean impassible(Passibility passibility) { + return passibility == Passibility.impassible + || (this.capabilities.cautious() && passibility.worseThan(Passibility.passible)); + } + + private boolean swimable(byte flags) { + return this.capabilities.swimmer() && swimmingRequiredFor(flags) && (Element.water.in(flags) || this.capabilities.fireResistant()); + } + + private boolean swimmingRequiredFor(byte flags) { + return Element.water.in(flags) || (Element.fire.in(flags) && !Logic.fuzzy.in(flags)); + } + + private boolean unstable(byte flags) { + return (!Element.earth.in(flags) || Logic.ladder.in(flags)); + } + + private boolean impassible(byte flags) { + return (Element.earth.in(flags) && !(Logic.doorway.in(flags) && this.capabilities.opensDoors()) && !Logic.ladder.in(flags)) + || (Element.air.in(flags) && Logic.doorway.in(flags) && this.capabilities.avoidsDoorways()); + } + + private boolean fuzzyPassibility(int x, int y, int z) { + return fuzzyPassibility(this.occlusionProvider.elementAt(x, y, z)); + } + + private boolean fuzzyPassibility(byte flags) { + return impassible(flags) && (Logic.fuzzy.in(flags) || Logic.doorway.in(flags)); + } + + private Passibility clearance(byte flags) { + if (Element.earth.in(flags)) + if (Logic.ladder.in(flags)) + return Passibility.passible; + else if (Logic.fuzzy.in(flags)) + return Passibility.risky; + else + return Passibility.impassible; + else if (Element.water.in(flags)) + return Passibility.risky; + else if (Element.fire.in(flags)) + return Passibility.dangerous; + else + return Passibility.passible; + } + + private Passibility passibility(byte flags) { + final Element kind = Element.of(flags); + switch (kind) { + case earth: + if (Logic.ladder.in(flags) || (Logic.doorway.in(flags) && this.capabilities.opensDoors())) + return Passibility.passible; + else + return Passibility.impassible; + + case air: + if (Logic.doorway.in(flags) && capabilities.avoidsDoorways()) + return Passibility.impassible; + else + return Passibility.passible; + + case water: + if (this.capabilities.aquaphobic() || !this.capabilities.swimmer()) + return Passibility.dangerous; + else + return Passibility.risky; + + case fire: + if (!this.capabilities.fireResistant()) + return Passibility.dangerous; + else + return Passibility.risky; + } + + throw new IllegalArgumentException(MessageFormat.format("Unhandled element type ''{0}''", kind)); + } + + public IPathingEntity subject() { + return this.subject; + } + + void occlusionProvider(IOcclusionProvider occlusionProvider) { + this.occlusionProvider = occlusionProvider; + } + + public boolean sameDestination(PathObject delegate, com.extollit.linalg.immutable.Vec3d target) { + if (this.currentPath == null) + return false; + + if (this.currentPath != delegate && !this.currentPath.sameAs(delegate)) + return false; + + final Vec3d dest = this.destinationPosition; + if (dest == null) + return false; + + return + floor(target.x) == floor(dest.x) && + floor(target.y) == floor(dest.y) && + floor(target.z) == floor(dest.z); + } + + private float pathTimeAge() { + return this.subject.age() * this.capabilities.speed(); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/IConfigModel.java b/src/main/java/com/extollit/gaming/ai/path/IConfigModel.java new file mode 100644 index 0000000..93d715f --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/IConfigModel.java @@ -0,0 +1,147 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.num.FloatRange; + +/** + * Data abstraction used for globally configuring (once per class-loader) the Hydrazine path-finding engine. It contains + * configurations for influencing the co-routine-like scheduling of the A* triage process, timeouts for cache invalidation + * to aid entities toward their destinations, and some gravity-related limits for all pathing entities. + * + * Some of the properties here use a term called "path time" which is a time value relative to the pathing entity's + * dynamic movement speed. A single unit of path time is the time it takes (in server ticks) for the pathing entity to + * move one block with its current movement speed. Properties measured in path time are typically used to aid the engine + * in determining whether the pathing entity is stuck and countermeasures necessary to get it unstuck. + */ +public interface IConfigModel { + /** + * This is the maximum number of blocks an entity may fall safely without incurring any damage from falling + * (independent of any other hazards) + * + * @return the maximum fall distance measured in blocks + */ + short safeFallDistance(); + + /** + * This is the maximum number of blocks an entity may fall without dying from incurred falling damage + * (independent of any other hazards). Entities that fall further than the safe falling distance but less than + * or equal to the survival falling distance will incur at least some damage from falling. + * + * @return the maximum survive fall distance measured in blocks. + * @see #surviveFallDistance() + */ + short surviveFallDistance(); + + /** + * Inspired by a term from the Professional Association of Diving Instructors (PADI), the CESA limit + * (Controlled Emergency Swimming Ascent) here is the maximum number of blocks an entity submerged in fluid + * (typically water) would search upward for a passible point at the surface before the path-finding engine + * writes-off the entity as drowning and chooses a path-point along the bottom instead. + * + * @return the preconfigured CESA limit for saving drowning entities out of air. + */ + short cesaLimit(); + + /** + * This is used by an entity's path-finding engine instance to determine cache invalidation and recalculation of a + * target point. In general, it expresses an angular variance between the pathing entity's starting point, + * it's current target, and it's destination. + * + * During path-finding, the engine often chooses a target suitably close to the destination. The destination may be + * an entity that is constantly on the move. When the angle between this destination entity and the pathing entity + * is too great, then the triage is reset, a new target is determined, and a new path is computed. Refer to the + * ASCII art diagram for an example of this angular variance: + * + * s------e----t + * \ + * \ + * \ + * d + * + * In the above example, the formula would be: (s -> t) dot (s -> d) < threshold + * + * @return the threshold value the dot product between the destination position and the current target with the + * initial source position must be less than for an A* triage reset to occur. + */ + float dotThreshold(); + + /** + * The minimum and maximum amount of path time that an entity may remain 'stuck' at a point along its path + * until the engine increments the failure counter. A random value in this range is picked each time a path-finding + * session is reset. + * + * @return minium and maximum path time period before stuck before the entity's engine increases its failure count. + * @see #failureCountThreshold() + */ + FloatRange passiblePointTimeLimit(); + + /** + * The number of failures permitted before a pathing entity's engine proceeds to perform additional steps to aid the + * entity along its path (typically cache invalidation). The engine's probationary time-limit must have also been + * exceeded, so it is possible that the actual failure count may be higher than this threshold depending on the + * situation. + * + * @return the pre-configured failure count + * @see #passiblePointTimeLimit() + */ + byte failureCountThreshold(); + + /** + * The minimum and maximum required path time that an entity may remain in failure state + * (at least one failure count) before a pathing entity's engine proceeds to perform additional states to aid the + * entity along its path (typically cache invalidation). The engine's failure count must also have exceeded + * its threshold, so it is possible that the actual time may be higher than this maximum depending on the situation. + * + * A random value in this range is picked each time a pathing entity enters failure state (transitions from having + * a zero failure count to a failure count of one). + * + * @return the pre-configured minimum and maximum possible time-limit in path time units + * @see #failureCountThreshold() + */ + FloatRange probationaryTimeLimit(); + + /** + * Hydrazine tries to optimize pathing by telling entities to move to the furthest path point in a path from its + * current position that is a straight and continuous direct line. Sometimes entities will still get stuck because + * Hydrazine only checks for taxi-cab clearance between path points, it's reasonable to assume (for most casess) + * that if the taxi-cab path is clear then the associated direct line covering them is also clear. Furthermore, + * this assumption is made in real-life scenarios as well and suffers the same erroneous (and acceptable) judgement. + * + * This property is the minimum and maximum path time that an entity may remain stuck at some path-point until + * the engine removes the direct path shortcut and tells the entity to move to the path point immediately adjacent + * to its current position instead. + * + * @return the pre-configured minimum and maximum time-limit in path time units to cope with direct-line optimization errors + */ + FloatRange directLineTimeLimit(); + + /** + * Determines the dynamically configured co-routine-like schedule for the given priority rating. + * + * @param priority the requested priority + * @return an A* triage schedule for the given priority + */ + Schedule scheduleFor(SchedulingPriority priority); + + /** + * A data class used to configure the co-routine-like behavior of the A* triage process. + */ + final class Schedule { + + /** + * Number of cycles to dedicate to path-finding triage when a path-finding operation is first initiated for an + * entity. + */ + public final int init; + + /** + * Number of cycles to dedicate to path-finding triage for each sub-sequent cycle after a path-finding operation + * has been initiated for an entity. + */ + public final int period; + + public Schedule(int init, int period) { + this.init = init; + this.period = period; + } + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/SchedulingPriority.java b/src/main/java/com/extollit/gaming/ai/path/SchedulingPriority.java new file mode 100644 index 0000000..e176b0f --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/SchedulingPriority.java @@ -0,0 +1,46 @@ +package com.extollit.gaming.ai.path; + +/** + * Preset scheduling priorities for the co-routine-like behavior of the engine's A* triage process. This determines + * how many iterations (per cycle) a path-finding engine instance for an entity dedicates to the A* algorithm. + * + * @see IConfigModel.Schedule + */ +public enum SchedulingPriority { + /** + * Indicates high-priority scheduling, entities with engines configured for this rating complete path-finding sooner. + * This is initialized with default values: + * - 12 initial compute iterations + * - 7 subsequent compute iterations + */ + high (12, 7), + + /** + * Indicates low-priority scheduling, entities with engines configured for this rating complete path-finding later. + * This is initialized with default values: + * - 3 initial compute iterations + * - 2 subsequent compute iterations + */ + low (3, 2); + + protected int initComputeIterations, periodicComputeIterations; + + SchedulingPriority(int initComputeIterations, int periodicComputeIterations) { + this.initComputeIterations = initComputeIterations; + this.periodicComputeIterations = periodicComputeIterations; + } + + /** + * Used to configure the co-routine-like compute cycles for each of these priority ratings. + * + * @param IConfigModel source containing the appropriate configuration parameters + * @see IConfigModel#scheduleFor(SchedulingPriority) + */ + public static void configureFrom(IConfigModel IConfigModel) { + for (SchedulingPriority priority : SchedulingPriority.values()) { + final IConfigModel.Schedule schedule = IConfigModel.scheduleFor(priority); + priority.initComputeIterations = schedule.init; + priority.periodicComputeIterations = schedule.period; + } + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/AreaOcclusionProvider.java b/src/main/java/com/extollit/gaming/ai/path/model/AreaOcclusionProvider.java new file mode 100644 index 0000000..a98e769 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/AreaOcclusionProvider.java @@ -0,0 +1,140 @@ +package com.extollit.gaming.ai.path.model; + +public class AreaOcclusionProvider implements IOcclusionProvider { + private final IColumnarSpace[][] columnarSpaces; + + private final int cx0, cz0, cxN, czN; + + public AreaOcclusionProvider(IColumnarSpace[][] columnarSpaces, int cx0, int cz0) { + this.columnarSpaces = columnarSpaces; + this.cx0 = cx0; + this.cz0 = cz0; + this.cxN = columnarSpaces[0].length + cx0 - 1; + this.czN = columnarSpaces.length + cz0 - 1; + } + + @Override + public byte elementAt(int x, int y, int z) { + final IColumnarSpace[][] columnarSpaces = this.columnarSpaces; + final int + cx = x >> 4, + cz = z >> 4, + cy = y >> 4; + + if (cx >= cx0 && cx <= cxN && cz >= cz0 && cz <= czN && cy >= 0 && cy < OcclusionField.DIMENSION_SIZE) { + final int + + czz = cz - cz0, + cxx = cx - cx0; + + final IColumnarSpace columnarSpace = columnarSpaces[czz][cxx]; + if (columnarSpace != null) { + final OcclusionField field = columnarSpace.occlusionFieldAt(cx, cy, cz); + + if (!field.areaInitFull()) + areaInit(field, x, y, z); + + return field.elementAt(x & OcclusionField.DIMENSION_MASK, y & OcclusionField.DIMENSION_MASK, z & OcclusionField.DIMENSION_MASK); + } + } + + return 0; + } + + private void areaInit(OcclusionField field, int x, int y, int z) { + final IColumnarSpace[][] columnarSpaces = this.columnarSpaces; + final int + cx = x >> 4, + cy = y >> 4, + cz = z >> 4, + + cxN = columnarSpaces[0].length - 1, + czN = columnarSpaces.length - 1, + + czz = cz - cz0, + cxx = cx - cx0, + xx = x & OcclusionField.DIMENSION_MASK, + yy = y & OcclusionField.DIMENSION_MASK, + zz = z & OcclusionField.DIMENSION_MASK; + + final IColumnarSpace + centerColumnarSpace = columnarSpaces[czz][cxx]; + + if (xx == 0 && zz == 0 && !field.areaInitAt(OcclusionField.AreaInit.northWest) && cxx > 0 && czz > 0) { + final IColumnarSpace + westColumnarSpace = columnarSpaces[czz - 1][cxx], + northColumnarSpace = columnarSpaces[czz][cxx - 1]; + + if (northColumnarSpace != null && westColumnarSpace != null) + field.areaInitNorthWest( + northColumnarSpace.occlusionFieldAt(cx - 1, cy, cz), + westColumnarSpace.occlusionFieldAt(cx, cy, cz - 1) + ); + } else if (xx == OcclusionField.DIMENSION_EXTENT && zz == 0 && !field.areaInitAt(OcclusionField.AreaInit.northEast) && cxx < cxN && czz > 0) { + final IColumnarSpace + westColumnarSpace = columnarSpaces[czz - 1][cxx], + eastColumnarSpace = columnarSpaces[czz][cxx + 1]; + + if (eastColumnarSpace != null && westColumnarSpace != null) + field.areaInitNorthEast( + eastColumnarSpace.occlusionFieldAt(cx + 1, cy, cz), + westColumnarSpace.occlusionFieldAt(cx, cy, cz - 1) + ); + } else if (xx == 0 && zz == OcclusionField.DIMENSION_EXTENT && !field.areaInitAt(OcclusionField.AreaInit.southWest) && cxx > 0 && czz < czN) { + final IColumnarSpace + northColumnarSpace = columnarSpaces[czz][cxx - 1], + southColumnarSpace = columnarSpaces[czz + 1][cxx]; + + if (northColumnarSpace != null && southColumnarSpace != null) + field.areaInitSouthWest( + northColumnarSpace.occlusionFieldAt(cx - 1, cy, cz), + southColumnarSpace.occlusionFieldAt(cx, cy, cz + 1) + ); + } else if (xx == OcclusionField.DIMENSION_EXTENT && zz == OcclusionField.DIMENSION_EXTENT && !field.areaInitAt(OcclusionField.AreaInit.southEast) && cxx < cxN && czz < czN) { + final IColumnarSpace + eastColumnarSpace = columnarSpaces[czz][cxx + 1], + southColumnarSpace = columnarSpaces[czz + 1][cxx]; + + if (eastColumnarSpace != null && southColumnarSpace != null) + field.areaInitSouthEast( + eastColumnarSpace.occlusionFieldAt(cx + 1, cy, cz), + southColumnarSpace.occlusionFieldAt(cx, cy, cz + 1) + ); + } else if (xx == 0 && !field.areaInitAt(OcclusionField.AreaInit.west) && cxx > 0) { + final IColumnarSpace + northColumnarSpace = columnarSpaces[czz][cxx - 1]; + + if (northColumnarSpace != null) + field.areaInitWest(northColumnarSpace.occlusionFieldAt(cx - 1, cy, cz)); + } else if (xx == OcclusionField.DIMENSION_EXTENT && !field.areaInitAt(OcclusionField.AreaInit.east) && cxx < cxN) { + final IColumnarSpace + eastColumnarSpace = columnarSpaces[czz][cxx + 1]; + + if (eastColumnarSpace != null) + field.areaInitEast(eastColumnarSpace.occlusionFieldAt(cx + 1, cy, cz)); + } else if (zz == 0 && !field.areaInitAt(OcclusionField.AreaInit.north) && czz > 0) { + final IColumnarSpace + westColumnarSpace = columnarSpaces[czz - 1][cxx]; + + if (westColumnarSpace != null) + field.areaInitNorth(westColumnarSpace.occlusionFieldAt(cx, cy, cz - 1)); + } else if (zz == OcclusionField.DIMENSION_EXTENT && !field.areaInitAt(OcclusionField.AreaInit.south) && czz < czN) { + final IColumnarSpace + southColumnarSpace = columnarSpaces[czz + 1][cxx]; + + if (southColumnarSpace != null) + field.areaInitSouth(southColumnarSpace.occlusionFieldAt(cx, cy, cz + 1)); + } + + if (yy == OcclusionField.DIMENSION_EXTENT && !field.areaInitAt(OcclusionField.AreaInit.up)) { + field.areaInitUp(centerColumnarSpace, cy, cy < OcclusionField.DIMENSION_EXTENT ? centerColumnarSpace.occlusionFieldAt(cx, cy + 1, cz) : null); + } else if (yy == 0 && !field.areaInitAt(OcclusionField.AreaInit.down)) { + field.areaInitDown(centerColumnarSpace, cy, cy > 0 ? centerColumnarSpace.occlusionFieldAt(cx, cy - 1, cz) : null); + } + } + + @Override + public String visualizeAt(int y) { + return OcclusionField.visualizeAt(this, y, cx0 << 4, cz0 << 4, (cxN + 1) << 4, (czN + 1) << 4); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/Element.java b/src/main/java/com/extollit/gaming/ai/path/model/Element.java new file mode 100644 index 0000000..8f5912a --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/Element.java @@ -0,0 +1,99 @@ +package com.extollit.gaming.ai.path.model; + +/** + * Passibility of blocks is stored in nibbles by an occlusion field. The lower two bits of this nibble indicates + * the material and basic passibility of the block. This is determined from the flags on {@link IBlockDescription}. + * + * @see IBlockDescription + * @see Logic + */ +public enum Element { + /** + * Represents a passible block having no collision bounds. + * A block description that is not impeding, a liquid or incinerating produces an 'air' element. + * + * @see IBlockDescription#isImpeding() + * @see IBlockDescription#isLiquid() + * @see IBlockDescription#isIncinerating() + */ + air, + + /** + * Represents an impassible block having at least some collision bounds. + * A block description that is impeding produces an 'earth' element with the following additional conditions: + * - If the block is a door, {@link IBlockObject#isImpeding()} is called to determine dynamic collision bounds + * from the specific block's state inside the instance. If the door is open then the element 'air' results + * instead. + * - Climable blocks with no collision bounds (i.e. vines and ladders) also produce an 'earth' element. This is + * a special provision due to bit restrictions. This is acceptable because something that has collision bounds + * cannot also be a climbable ladder or vine due to how these work in the Notchian implementation. + * + * @see Logic#climbable(byte) + * @see IBlockObject#isImpeding() + * @see IBlockDescription#isDoor() + * @see #air + */ + earth, + + /** + * Represents a fluid block having no collision bounds but can potentially drown an entity or requires swimming. + * This is used only for non-incinerating fluids such as water, quicksand or mud. Lava or magma is expressed + * differently. + * + * A block description that is liquid and not incinerating produces a 'water' element. + * + * @see IBlockDescription#isLiquid() + * @see IBlockDescription#isIncinerating() + */ + water, + + /** + * Represents a block that can burn entities that pass through it (if they are not fire resistant). This is also + * used to represent lava or magma blocks with the additional {@link Logic#fuzzy} flag. + * + * A block description that is incinerating produces a 'fire' element. + * + * @see IBlockDescription#isIncinerating() + * @see Logic#fuzzy + */ + fire; + + public static final int MASK = 4 - 1; + + public final byte mask = (byte)ordinal(); + + /** + * Helper function for determining whether this element is represented by the specified nibble. The nibble may + * contain {@link Logic} flags in the high two bits, these will be ignored. + * + * @param flags A four-bit nibble containing a lower two bit element representation + * + * @return true if the lower two bits map to this element + */ + public boolean in(byte flags) { + return (flags & MASK) == this.mask; + } + + /** + * Helper function for determining the element represented by the specified nibble. The nibble may contain + * {@link Logic} flags in the high two bits, these will be ignored. + * + * @param flags A four-bit nibble containing a lower two bit element representation + * + * @return the element represented by the nibble + */ + public static Element of(byte flags) { + return Element.values()[flags & MASK]; + } + + /** + * Helper function for setting the lower two bits of the specified nibble to represent this element. The nibble + * may contain {@link Logic} flags in the high two bits, these bits will not be affected. + * + * @param flags A four-bit nibble that will be modified to represent this element + * @return the passed-in flags modified to represent this element + */ + public byte to(byte flags) { + return (byte)((flags & ~MASK) | this.mask); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/HydrazinePathPoint.java b/src/main/java/com/extollit/gaming/ai/path/model/HydrazinePathPoint.java new file mode 100644 index 0000000..044eb71 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/HydrazinePathPoint.java @@ -0,0 +1,167 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.Vec3i; + +import java.text.MessageFormat; +import java.util.Objects; + +public class HydrazinePathPoint { + + private static final byte + BitWidth_1K = 10, + BitWidth_64 = 6, + Mask_Passibility = (byte)(3), + Index_BitOffs = 2, + Length_BitOffs = (byte)(Index_BitOffs + BitWidth_1K), + Delta_BitOffs = (byte)(Length_BitOffs + BitWidth_64), + Journey_BitOffs = (byte)(Delta_BitOffs + BitWidth_64), + First_BitOffs = (byte)(Journey_BitOffs + BitWidth_64); + + private static final int + Mask_64 = (1 << BitWidth_64) - 1, + Mask_1K = (1 << BitWidth_1K) - 1; + + public final Vec3i key; + + private int word; + private HydrazinePathPoint previous; + + public HydrazinePathPoint(Vec3i key) { + this.key = key; + unassign(); + } + + public final byte length() { + return (byte)((this.word >> Length_BitOffs) & Mask_64); + } + public final byte delta() { + return (byte)((this.word >> Delta_BitOffs) & Mask_64); + } + public final byte journey() { + return (byte)((this.word >> Journey_BitOffs) & Mask_64); + } + public final HydrazinePathPoint up() { + return this.previous; + } + + public final Passibility passibility() { + return Passibility.values()[(int)(this.word & Mask_Passibility)]; + } + public final void passibility(Passibility passibility) { + if (this.previous != null) + passibility = passibility.between(this.previous.passibility()); + this.word = (this.word & ~Mask_Passibility) | passibility.ordinal(); + } + public final boolean length(int length) { + if (length > Mask_64 || length < 0) + return false; + + this.word = (this.word & ~(Mask_64 << Length_BitOffs)) | (length << Length_BitOffs); + return true; + } + final boolean delta(int delta) { + if (delta > Mask_64 || delta < 0) + return false; + + this.word = (this.word & ~(Mask_64 << Delta_BitOffs)) | (delta << Delta_BitOffs); + return true; + } + final boolean journey(int journey) { + if (journey > Mask_64 || journey < 0) + return false; + + this.word = (this.word & ~(Mask_64 << Journey_BitOffs)) | (journey << Journey_BitOffs); + return true; + } + final short index() { + short index = (short) ((this.word >> Index_BitOffs) & Mask_1K); + return index == Mask_1K ? -1 : index; + } + final boolean index(int index) { + if (index >= Mask_1K || index < -1) + return false; + + this.word = (this.word & ~(Mask_1K << Index_BitOffs)) | ((index & Mask_1K) << Index_BitOffs); + return true; + } + public final boolean first() { + return ((this.word >> First_BitOffs) & 1) == 1; + } + public final void first(boolean flag) { + this.word = (this.word & ~(1 << First_BitOffs)) | ((flag ? 1 : 0) << First_BitOffs); + } + + public final boolean assigned() { + return index() != -1; + } + + public boolean target(HydrazinePathPoint target) { + final int distance = (int) Math.sqrt(squareDelta(this, target)); + if (distance > Mask_64) + return false; + + this.word = (this.word & ~((Mask_64 << Journey_BitOffs) | (Mask_64 << Delta_BitOffs))) | ((distance << Journey_BitOffs) | (distance << Delta_BitOffs)); + return true; + } + + public void orphan() { + this.previous = null; + } + final void unassign() { + index(-1); + first(false); + } + final boolean appendTo(final HydrazinePathPoint parent, final int delta, final HydrazinePathPoint target) { + this.previous = parent; + passibility(passibility()); + return length(parent.length() + delta) + && delta((int)Math.sqrt(squareDelta(this, target))); + } + + public static int squareDelta(HydrazinePathPoint left, HydrazinePathPoint right) { + final Vec3i + leftCoords = left.key, + rightCoords = right.key; + + final int + dx = leftCoords.x - rightCoords.x, + dy = leftCoords.y - rightCoords.y, + dz = leftCoords.z - rightCoords.z; + + return dx*dx + dy*dy*2 + dz*dz; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(this.key.toString()); + final short index = index(); + if (first()) + sb.append('V'); + + if (index == -1) + sb.append(" (unassigned)"); + else { + sb.append(" @ "); + sb.append(index); + } + + return sb.toString() + MessageFormat.format(" ({0}) : length={1}, delta={2}, journey={3}", passibility(), length(), delta(), journey()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HydrazinePathPoint pathPoint = (HydrazinePathPoint) o; + return Objects.equals(key, pathPoint.key); + } + + @Override + public final int hashCode() { + final Vec3i key = this.key; + int result = key.x >> 4; + result = 31 * result + (key.y >> 4); + result = 31 * result + (key.z >> 4); + return result; + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IBlockDescription.java b/src/main/java/com/extollit/gaming/ai/path/model/IBlockDescription.java new file mode 100644 index 0000000..d643be2 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IBlockDescription.java @@ -0,0 +1,115 @@ +package com.extollit.gaming.ai.path.model; + +/** + * Description of a block independent of instance, this is the most basic type that is used primarily for populating + * the occlusion fields. + */ +public interface IBlockDescription { + /** + * Whether or not this is a fence, wall, or fence gate. Blocks like this have irregular maximum bounds + * y-component of 1.5. + * + * This signals to the engine that entities should not attempt to path over these blocks. + * + * @return true if this describes a block that is a fence, wall or fence gate . + */ + boolean isFenceLike(); + + /** + * Whether this block is used for climbing (typically by players, but now by mobs who can climb too!) + * + * If this block is climbable (i.e. vines or ladder) then the engine looks for adjacent solid blocks to support + * climbing (most often necessary for vines since free-hanging vines cannot be climbed). The engine looks for + * the next adjacent block at the top of the continuous vertical string of ladders of vines. + * + * @return true if this is either a ladder or a vine block. + */ + boolean isClimbable(); + + /** + * Whether the block is a door that can be manually opened or broken-down by zombies. + * + * In the Notchian implementation villagers and zombies interact with doors in different ways. Zombies always try + * to path through doors that are considered breakable (wooden doors). Villagers will always go through doors + * whether closed or not (if closed they open the door). Under certain circumstances (usually when the villager is + * threatened) villagers treat open doors as not passable. This combined with other AI logic has the effect of + * keeping villagers indoors during times of threat (zombie invasions) even when the doors to their houses are + * open while still allowing them to wander inside their homes. + * + * @return true if this is a wooden door, wooden trap door or wooden fence gate. + */ + boolean isDoor(); + + /** + * Does this block impede movement in at least one direction? + * + * This is used to determine if a block impacts passibility at all and has at least some collision bounds that + * prevent any movement. + * + * Examples of such blocks include: + * - Solid stone and ore + * - Slab or plank + * - Wall + * - Anvil + * - Yes, even lily-pads + * + * Examples of blocks that do NOT impede: + * - Air + * - Grass + * - Water + * - Fire + * - Lava + * - Quicksand + * - Flowers + * - Vines + * - Ladders + * + * @return true if this block has at least some collision bounds. + */ + boolean isImpeding(); + + /** + * This is a special sort of block that "impedes" with full 1x1x1 collision bounds effectively filling the entire + * volume of the block. + * + * This flag is used to signal to the engine whether additional and potentially CPU-intensive calcuations are + * necessary to determine how this block affects path-finding. If the block is fully bounded then no additional + * comprehensive calculations are necessary. + * + * Examples of such blocks include: + * - Stone + * - Ore + * - Lapis compressed block + * - Command cube + * + * Examples of blocks that are NOT fully bounded, but do impede: + * - Anvil + * - Wall + * - Lily-pad + * + * @return true if the block has min/max bounds of < 0, 0, 0 > to < 1, 1, 1 >. + */ + boolean isFullyBounded(); + + /** + * Whether this represents a liquid block, either flowing or static, harmful or benign. Blocks like this will + * signal to pathing entities that they need to swim through it. + * + * Swimming is conducted by the Notchian implementation (and by Hydrazine path-finding engine) by constantly telling + * the mob to jump as if holding the space-bar steady and attempting to walk on-top of the fluid. This is to + * counteract gravity from pulling the entity downward and drowning it. + * + * @return true if this is a liquid such as water, lava, mud or quicksand. + */ + boolean isLiquid(); + + /** + * Does this block burn entities that come into contact with it? + * + * Blocks with this flag signal that only entities with fire-resistance (either with the potion effect running or + * natural fire resistance as with the Lava Monsters mod) can path through this. + * + * @return true if this is lava or fire or something similar that burns due to high heat. + */ + boolean isIncinerating(); +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IBlockObject.java b/src/main/java/com/extollit/gaming/ai/path/model/IBlockObject.java new file mode 100644 index 0000000..bbb496a --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IBlockObject.java @@ -0,0 +1,28 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.AxisAlignedBBox; + +/** + * Description of a block at a specific location in an instance, this is a more comprehensive type than the + * super interface IBlockDescription. The engine uses this as a last resort to determine passibility with + * certain complex blocks. + * + * A block object at a specific location in an instance may have dynamic collision bounds that depend on its + * state. That is why this type is necessary. + */ +public interface IBlockObject extends IBlockDescription { + /** + * The maximum collision bounds of the block, pathing entities should remain outside of these bounds. This method + * only applies to blocks that are impeding, it is not called for blocks that do not impede. + * + * This represents the superset of the actual collision bounds, which may be more complex and/or include more than + * one bounded region and not necessarily axis-aligned. This is used by the engine to determine which direction an + * entity can path from if the entity is in this block based on its coordinates. + * It is primarily inspired by the phenomenon of animals and creatures getting stuck at fences, which have partial + * collision bounds. + * + * @return The maximum axis-aligned bounds for the block, must be non-null + * @see IBlockDescription#isImpeding() + */ + AxisAlignedBBox bounds(); +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IColumnarSpace.java b/src/main/java/com/extollit/gaming/ai/path/model/IColumnarSpace.java new file mode 100644 index 0000000..c3832fb --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IColumnarSpace.java @@ -0,0 +1,68 @@ +package com.extollit.gaming.ai.path.model; + +import java.util.Iterator; + +/** + * Represents a logical columnar division of space 16x16 blocks in size on the x/z plane with full extent along the y-axis + */ +public interface IColumnarSpace { + /** + * Returns a basic description of the block located at the specified coordinates relative to the columnar space. + * + * This is only the basic block description, the result is never downcasted to a {@link IBlockObject} object by + * the engine. + * + * @param x relative x-coordinate + * @param y relative y-coordinate + * @param z relative z-coordinate + * @return invariant concrete type of {@link IBlockDescription} at the aforementioned relative coordinates + */ + IBlockDescription blockAt(int x, int y, int z); + + /** + * Returns the Notchian meta-data of the block located at the specified coordinates relative to the columnar space. + * + * This is currently not used. + * + * @param x relative x-coordinate + * @param y relative y-coordinate + * @param z relative z-coordinate + * @return Notchian block meta-data nibble for the block at the aforementioned relative coordinates + */ + int metaDataAt(int x, int y, int z); + + /** + * Retrieves the occlusion field located at the specified absolute chunk coordinates (relative to the instance, + * not the columnar space). Populates the field from block data if not yet loaded. + * + * @param cx absolute chunk x-coordinate + * @param cy absolute chunk y coordinate + * @param cz absolute chunk z-coordinate + * @return the occlusion field at the specified absolute chunk coordinates in the parent instance. + */ + OcclusionField occlusionFieldAt(int cx, int cy, int cz); + + /** + * Retrieves the occlusion field located at the specified y chunk coordinate in the columnar space. If the + * field is not yet loaded at that chunk coordinate then no work is done. + * + * @param cy absolute y chunk coordinate of the field to retrieve + * @return a pre-existing occlusion field in the columnar space at the aforementioned y chunk coordinate or null if + * there is not one yet loaded there + */ + OcclusionField optOcclusionFieldAt(int cy); + + /** + * Provides an iterator of all the loaded / populated occlusion fields in a columnar space. + * + * @return iterator of pre-existing occlusion fields in the columnar space + */ + Iterator iterateOcclusionFields(); + + /** + * Each columnar space belongs to an instance, this is its parent. + * + * @return parent instance that contains this columnar space + */ + IInstanceSpace instance(); +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IDynamicMovableObject.java b/src/main/java/com/extollit/gaming/ai/path/model/IDynamicMovableObject.java new file mode 100644 index 0000000..eaf9ebb --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IDynamicMovableObject.java @@ -0,0 +1,44 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.Vec3d; + +/** + * Abstraction for entities, this could potentially be a covariant pathing entity or an entity that a pathing + * entity is following or pathing to. + * + * @see IPathingEntity + */ +public interface IDynamicMovableObject { + /** + * Absolute (relative to the instance) three-dimensional coordinates of the entity in the instance it is hosted + * measured in blocks. + * + * Since entities take-up three-dimensional space themselves, these coordinates must meet the following conditions + * for effective path-finding operation: + * - The x and z components must point to the center of the bounding box of the entity. In other words, they must + * be offset by half the value of {@link #width()} from the minimum x and z extent of the entity's bounding + * region. + * - The y-component must point to the minimum y-extent of the entity's bounding region. + * + * @return a three-dimensional double floating-point vector of the entity's position. + */ + Vec3d coordinates(); + + /** + * The width and depth of the entity along the x and z axes measured in blocks. + * + * This value must match the space occupied by the bounding box of the entity on the x/z plane along either axis. + * + * @return width or depth of the entity + */ + float width(); + + /** + * The height of the entity along the y-axis measured in blocks. + * + * This value must match the space occupied by the bounding box of the entity along the y-axis. + * + * @return height of the entity + */ + float height(); +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IInstanceSpace.java b/src/main/java/com/extollit/gaming/ai/path/model/IInstanceSpace.java new file mode 100644 index 0000000..2a401e4 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IInstanceSpace.java @@ -0,0 +1,51 @@ +package com.extollit.gaming.ai.path.model; + +/** + * Abstraction for a world instance capable of yielding smaller virtual units of space. It also yields occlusion + * fields representing those spaces. + * + * Many of these functions use chunk coordinates, which are at intervals of 16 blocks (cx = x >> 4). + */ +public interface IInstanceSpace { + /** + * Yields a comprehensive block object at the specified coordinates in the instance. This is called only + * as a last resort by the engine if it cannot obtain sufficient information regarding the passibility of a block + * from the occlusion field provider. + * + * @param x x-coordinate + * @param y y-coordinate + * @param z z-coordinate + * @return an object identifying and describing the block at the aforementioned coordinates + */ + IBlockObject blockObjectAt(int x, int y, int z); + + /** + * Looks-up the occlusion field at the specified chunk coordinates if it has been initialized. + * Occlusion fields ought to be lazy-loaded and only populated when path-finding is requested through + * those areas using an occlusion provider. + * + * @param cx x chunk coordinate + * @param cy y chunk coordinate, no less than 0, no more than 15 + * @param cz z chunk coordinate + * @return the pre-existing occlusion field at the aforementioned chunk coordinates, null otherwise + * @see IOcclusionProvider + */ + OcclusionField optOcclusionFieldAt(int cx, int cy, int cz); + + /** + * Produces a facade to an array of occlusion fields defined by the specified chunk coordinate range. + * This is used for computing a path through an instance, as information is requested the provider should + * initialize / load the respective occlusion fields. + * + * In the Notchian implementation, a world (instance) is sub-divided into columnar chunk objects that each contain + * all the block information in a 16x16 block column along the y-axis at some x/z coordinates in the world. This is + * based on that approach, while occlusion fields along the x and z axis are bounded, those along the y-axis are not. + * + * @param cx0 Minimum x chunk coordinate of the bounded range + * @param cz0 Minimum z chunk coordinate of the bounded range + * @param cxN Maximum x chunk coordinate of the bounded range + * @param czN Maximum z chunk coordinate of the bounded range + * @return the populated occlusion field representing the chunk at the aforementioned chunk coordinates + */ + IOcclusionProvider occlusionProviderFor(int cx0, int cz0, int cxN, int czN); +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IOcclusionProvider.java b/src/main/java/com/extollit/gaming/ai/path/model/IOcclusionProvider.java new file mode 100644 index 0000000..68c251a --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IOcclusionProvider.java @@ -0,0 +1,38 @@ +package com.extollit.gaming.ai.path.model; + +/** + * Provides simplified low-latency information about a block at a location in an instance. This is not meant to be + * implemented by API consumers. + * + * @see AreaOcclusionProvider + * @see OcclusionField + */ +public interface IOcclusionProvider { + /** + * Returns a nibble that describes the passibility of the block at the specified coordinates in the instance. + * + * The low two bits of the nibble are used to identify the {@link Element} of the block and the high two bits of the + * nibble are used to identify the {@link Logic} of the block. There are utility methods on those types for + * conveniently extracting that information from a nibble. + * + * @param x absolute (relative to the instance) x-coordinate + * @param y absolute (relative to the instance) y-coordinate + * @param z absolute (relative to the instance) z-coordinate + * @return a nibble containing the {@link Element} and {@link Logic} information about a block in the instance at + * the aforementioned coordinates. + * @see Element + * @see Logic + */ + byte elementAt(int x, int y, int z); + + /** + * Provides a visualization of an x/z plane of the occlusion field at the specified y coordinate using ASCII art. + * + * This method was used for troubleshooting and is not used by the engine itself. + * + * @param y absolute y-coordinate (relative to the instance) to render an x/z plane snapshot of the field + * @return a string that conveniently provides a visualization of the field, especially if used as a watch expression + * during a debugging session. + */ + String visualizeAt(int y); +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/IPathingEntity.java b/src/main/java/com/extollit/gaming/ai/path/model/IPathingEntity.java new file mode 100644 index 0000000..63d7059 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/IPathingEntity.java @@ -0,0 +1,139 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.gaming.ai.path.IConfigModel; +import com.extollit.linalg.immutable.Vec3d; + +/** + * Abstraction for an entity that requires path-finding support. + */ +public interface IPathingEntity extends IDynamicMovableObject { + /** + * Ticks since the entity was spawned / created. This is typically one server cycle, in the Notchian implementation + * there are twenty ticks per second. + * + * This is used to determine how long a pathing entity has been stuck at the same spot to signal to the engine + * whether some cache invalidation is necessary to aid the entity toward its target. If this value exceeds a + * pre-configured threshold then the engine proceeds to perform some cache invalidation. + * + * @return age of the entity in ticks + * @see IConfigModel#passiblePointTimeLimit() + */ + int age(); + + /** + * The maximum path-finding range for the entity. Although the current engine is limited to computing paths no more + * than 32 blocks in length, if this value is greater than that then the engine will attempt to compute a path toward + * the destination and refine periodically. + * + * @return the maximum search distance for the entity to path-find measured in blocks + */ + float searchRange(); + + /** + * The pathing capabilities of this entity. This method is only called once per path-finding entity per path-finding + * session. If an entity's path is cancelled and a new one requested then this property is retrieved again. + * + * @return the pathing capabilities of this entity + */ + Capabilities capabilities(); + + /** + * Given a valid path, this is called by the path-finding engine to tell this entity to move in a continuous line from + * its current position to the specified position. This may be an adjacent block (no more than one block away) or a + * distant block if the engine determines that the area between the entity's current position and the target position + * is clear and safe to traverse. + * + * @param position absolute (relative to the instance) target position to move toward + */ + void moveTo(Vec3d position); + + /** + * Expresses the movement capabilities of an entity, the engine loads these flags once per path-finding session and + * uses them to rate path-point candidates. This structure is yielded by pathing entities. + * + * @see IPathingEntity + */ + interface Capabilities { + /** + * The absolute movement speed of the entity as it paths from block to block measured in blocks per tick. For + * example, if it takes an entity approximately one second to pass from one block to another then this value will + * be ~0.05. This value is used by the path-finding engine to help it determine if an entity is stuck and not + * progressing along their path. + * + * @return the blocks per tick land movement speed of the pathing entity + */ + float speed(); + + /** + * Can this entity navigate through fire or lava or other burning / high-heat blocks without sustaining any damage? + * + * @return true if this mob is fire resistant + */ + boolean fireResistant(); + + /** + * Is this entity daring or cautious? does it take chances in order to reach its target? This flag typically + * applies to animals and not to hostile mobs. + * + * An entity that is cautious will only use pristine high-quality "passible" paths. Paths that contain points + * rated as either "risky", "dangerous" or "impassible" shall all be treated as "impassible" by this entity. + * + * @return true if this entity cannot take any risks during path-finding. + * @see Passibility + */ + boolean cautious(); + + /** + * Can this entity climb ladders or vines? + * + * @return true if this entity should attempt to path up ladders or vines + */ + boolean climber(); + + /** + * Is this entity capable of swimming? This flag determines where the engine looks for path-points through fluid + * for the entity. It also influences the rating of path points discovered in fluid. Fluid path-points for swimmers + * are not considered dangerous (unless it's lava and the entity is not fire resistant, then it's impassible). + * + * Type type of entities that are not swimmers are typically the following: + * - Puppies + * - Kittens + * - Villager golems + * + * @return true if the entity can swim and typically survive in fluid, false if the entity always drowns in fluid. + */ + boolean swimmer(); + + /** + * Does this entity avoid water and other fluids? This also influences the rating of path points discovered in fluid. + * For aquaphobic entities, fluid path points are considered dangerous. Entities that are aquaphobic (avoids water) + * will typically look for a way around the fluid (unless there is no other alternative). + * + * It is important to note that this flag does not imply that the entity cannot swim, rather that the entity should + * void having to. The type of entity that is a swimmer and not aquaphobic would be a fish or a whale. + * + * @return true if the entity should avoid pathing through fluid. + */ + boolean aquaphobic(); + + /** + * This flag is used only by villagers in the Notchian implementation, it tells the path-finding engine whether to + * consider open doorways as passible. When a villager is under threat it should stay indoors and not attempt to path + * outside where the threat exists. However, they should still be able to wander around inside the room where they are + * situated. + * + * @return true if this entity should treat open doorways as impassible + */ + boolean avoidsDoorways(); + + /** + * Does this entity either open doors carefully or smash them down forcefully? In the Notchian implementation, + * villagers open doors carefully and zombies knock doors down. This flag typically only applies to wooden doors + * since iron doors require a button, lever or other redstone mechanism and that would involve AI outside the scope + * of Hydrazine path-finding engine. + * + * @return true if this entity should treat closed (typically wooden) doors as passible + */ + boolean opensDoors(); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/Logic.java b/src/main/java/com/extollit/gaming/ai/path/model/Logic.java new file mode 100644 index 0000000..23a7874 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/Logic.java @@ -0,0 +1,110 @@ +package com.extollit.gaming.ai.path.model; + +/** + * Passibility of blocks is stored in nibbles by an occlusion field. The higher two bits of this nibble supplement + * the lower two bits by refining the nature of the {@link Element} expressed by the lower two bits for passibility. + * + * @see Element + */ +public enum Logic { + /** + * No special information that refines the nature of the associated element. + */ + nothing, + + /** + * This indicates, in general, that the associated element isn't precisely what it seems and additional calculations + * and queries may be necessary to determine the passibility of a block represented by the associated nibble. + * + * Below describes the meaning of this flag applied to each element: + * + * - {@link Element#fire} - The block is flames, without this flag the block is lava + * - {@link Element#earth} - The block does not have full collision bounds and possibly has dynamic collision bounds + * - {@link Element#air} - The block is openly passible (but not an open door) and there is at least one block in its + * Von Neumann neighborhood that is dissimilar and not also fuzzy + * - {@link Element#water} - Reserved for future use + * + * @see IBlockDescription#isFullyBounded() + */ + fuzzy, + + /** + * This indicates that the block is climbable (i.e. either a ladder or vine) and resides adjacent to a solid block + * in its Von Neumann neighborhood that supports it (e.g. free-hanging vines cannot be climbed, they must be up + * against something solid). + * + * Presently this flag only applies to the {@link Element#earth} element. Pairing this flag with any of the other + * three elements is reserved for future use. + * + * @see IBlockDescription#isClimbable() + */ + ladder, + + /** + * Indicates that the block is a door that is either open or closed. This requires performing an additional query on + * the particular block inside the instance to determine whether the door is open or closed from its dynamic state. + * + * Below describes the meaning of this flag applied to each element: + * + * - {@link Element#earth} - The door is closed + * - {@link Element#air} - The door is open + * - {@link Element#fire} - Reserved for future use + * - {@link Element#water} - Reserved for future use + * + * @see IBlockDescription#isDoor() + */ + doorway; + + public static final int + BIT_OFFSET = 2, + MASK = 4 - 1; + + public final byte mask = (byte)(ordinal() << BIT_OFFSET); + + /** + * Helper function for determining whether this logic indicator is represented by the specified nibble. + * The nibble may contain {@link Element} flags in the low two bits, these will be ignored. + * + * @param flags A four-bit nibble containing a higher two-bit logic representation + * + * @return true if the higher two bits map to this logic indicator + */ + public boolean in(byte flags) { + return (flags & (MASK << BIT_OFFSET)) == this.mask; + } + + /** + * Helper function for determining the logic indicator represented by the specified nibble. The nibble may contain + * {@link Element} flags in the low two bits, these will be ignored. + * + * @param flags A four-bit nibble containing a higher two-bit logic representation + * + * @return the logic indicator represented by the nibble + */ + public static Logic of(byte flags) { + return Logic.values()[(flags & (MASK << BIT_OFFSET)) >> BIT_OFFSET]; + } + + /** + * Helper function for setting the higher two bits of the specified nibble to represent this logic indicator. + * The nibble may contain {@link Element} flags in the low two bits, these bits will not be affected. + * + * @param flags A four-bit nibble that will be modified to represent this logic indicator + * @return the passed-in flags modified to represent this logic indicator + */ + public byte to(byte flags) { + return (byte)((flags & ~(MASK << BIT_OFFSET)) | this.mask); + } + + /** + * Helper function for determining whether the entire nibble (both high and low sets of bit pairs) represents a + * climbable ladder (i.e. vines or ladder). + * + * @param flags A four-bit nibble that contains a high two-bit logic representation and a low two-bit element + * representation. + * @return whether the flags map to both {@link #ladder} and {@link Element#earth} + */ + public static boolean climbable(byte flags) { + return ladder.in(flags) && Element.earth.in(flags); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/OcclusionField.java b/src/main/java/com/extollit/gaming/ai/path/model/OcclusionField.java new file mode 100644 index 0000000..ea5caf5 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/OcclusionField.java @@ -0,0 +1,849 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.VertexOffset; + +public class OcclusionField implements IOcclusionProvider { + public enum AreaInit { + north (0, -1), + south (0, +1), + west (-1, 0), + east (+1, 0), + + northEast (+1, -1), + northWest (-1, -1), + southEast (+1, +1), + southWest (-1, +1), + + up (1), + down (-1); + + public final VertexOffset offset; + public final short mask = (short)(1 << ordinal()); + + AreaInit(int dx, int dz) { + this.offset = new VertexOffset(dx, 0, dz); + } + AreaInit(int dy) { + this.offset = new VertexOffset(0, dy, 0); + } + + public boolean in(short flags) { return (flags & mask) != 0; } + public short to(short flags) { return (short)(flags | mask); } + } + + private static final byte + ELEMENT_LENGTH_SHL = 2, + ELEMENT_LENGTH = 1 << ELEMENT_LENGTH_SHL, + WORD_LENGTH = 64, + ELEMENTS_PER_WORD = WORD_LENGTH / ELEMENT_LENGTH, + WORD_LAST_OFFSET = ELEMENTS_PER_WORD - 1, + COORDINATE_TO_INDEX_SHR = 4, + DIMENSION_ORDER = 4; + + public static final int + DIMENSION_SIZE = 1 << DIMENSION_ORDER; + + static final int + DIMENSION_MASK = (1 << DIMENSION_ORDER) - 1, + DIMENSION_EXTENT = DIMENSION_SIZE - 1; + + private static final int + DIMENSION_SQUARE_SIZE = DIMENSION_SIZE * DIMENSION_SIZE, + LAST_INDEX = (DIMENSION_SIZE * DIMENSION_SQUARE_SIZE - 1) >> COORDINATE_TO_INDEX_SHR; + + private static final long + ELEMENT_MASK = (1 << ELEMENT_LENGTH) - 1; + + private static final short + FULLY_AREA_INIT = 0x3FF; + + private long [] words; + private byte singleton; + private short areaInit; + + public OcclusionField() {} + + public boolean areaInitFull() { + return this.areaInit == FULLY_AREA_INIT; + } + public boolean areaInitAt(AreaInit direction) { + return direction.in(this.areaInit); + } + + public static boolean fuzzyOpenIn(byte element) { + return Element.air.in(element) || (Element.earth.in(element) && Logic.fuzzy.in(element)); + } + + public void loadFrom(IColumnarSpace columnarSpace, int cx, int cy, int cz) { + this.singleton = 0; + this.words = new long[DIMENSION_SQUARE_SIZE * DIMENSION_SIZE * ELEMENT_LENGTH / WORD_LENGTH]; + + boolean compress = true; + byte lastFlags = this.singleton; + + final int + x0 = cx << DIMENSION_ORDER, + y0 = cy << DIMENSION_ORDER, yN = y0 + DIMENSION_SIZE, + z0 = cz << DIMENSION_ORDER; + + final long[] words = this.words; + + final int yNi = yN - 1; + for (int y = yNi, i = LAST_INDEX; y >= y0; --y) + for (int z = DIMENSION_EXTENT; z >= 0; --z) + for (int x = DIMENSION_SIZE - ELEMENTS_PER_WORD; x >= 0; x -= ELEMENTS_PER_WORD) { + long word = 0; + for (int b = WORD_LAST_OFFSET; b >= 0; --b) { + final int xx = x + b; + final IBlockDescription blockDescription = columnarSpace.blockAt(xx, y, z); + final byte flags = flagsFor(columnarSpace, x0 + xx, y, z0 + z, blockDescription); + compress &= (lastFlags == flags) || (i == LAST_INDEX && b == WORD_LAST_OFFSET); + lastFlags = flags; + word <<= (1 << ELEMENT_LENGTH_SHL); + word |= (long)flags; + + if (blockDescription.isFenceLike() && y < yNi) { + final int indexUp = i + (DIMENSION_SQUARE_SIZE >> COORDINATE_TO_INDEX_SHR); + words[indexUp] = modifyWord(words[indexUp], b, flags); + } + } + words[i--] = word; + } + + if (compress) { + this.words = null; + this.singleton = lastFlags; + } else + areaInit(); + } + + private boolean fenceOrDoorLike(byte flags) { + return (Element.earth.in(flags) && Logic.fuzzy.in(flags)) || (Logic.doorway.in(flags)); + } + + private void decompress() { + long word = singletonWord(); + final long[] words = this.words = new long[DIMENSION_SQUARE_SIZE * DIMENSION_SIZE * ELEMENT_LENGTH / WORD_LENGTH]; + for (int i = 0; i < words.length; ++i) + words[i] = word; + + this.singleton = 0; + } + + private long singletonWord() { + final byte singleton = this.singleton; + long word = 0; + for (int b = ELEMENTS_PER_WORD; b > 0; --b) { + word <<= 1 << ELEMENT_LENGTH_SHL; + word |= singleton; + } + return word; + } + + private void areaInit() { + final long[] words = this.words; + if (words == null) + return; + + for (int y = 0, index = DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR; y < DIMENSION_SIZE; ++y) { + for (int z = 1; z < DIMENSION_EXTENT; ++z) + for (int x = 0; x < DIMENSION_SIZE; x += ELEMENTS_PER_WORD) + { + long + word = words[index]; + final long + northWord = words[index - (DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR)], + southWord = words[index + (DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR)]; + + for ( + int b = x == 0 ? 1 : 0, + bN = ELEMENTS_PER_WORD - (x + ELEMENTS_PER_WORD >= DIMENSION_SIZE ? 1 : 0); + b < bN; + ++b + ) { + + final long + westWord = words[index - ((b - 1) >> COORDINATE_TO_INDEX_SHR)], + eastWord = words[index + ((b + 1) >> COORDINATE_TO_INDEX_SHR)]; + + word = areaWordFor(word, b, northWord, eastWord, southWord, westWord); + } + words[index++] = word; + } + + index += ((2 * DIMENSION_SIZE) >> COORDINATE_TO_INDEX_SHR); + } + } + + void areaInitNorth(OcclusionField other) { + areaInitZPlane(other, false); + this.areaInit = AreaInit.north.to(this.areaInit); + } + void areaInitSouth(OcclusionField other) { + areaInitZPlane(other, true); + this.areaInit = AreaInit.south.to(this.areaInit); + } + void areaInitWest(OcclusionField other) { + areaInitXPlane(other, false); + this.areaInit = AreaInit.west.to(this.areaInit); + } + void areaInitEast(OcclusionField other) { + areaInitXPlane(other, true); + this.areaInit = AreaInit.east.to(this.areaInit); + } + void areaInitNorthEast(OcclusionField horizontal, OcclusionField depth) { + areaInitVerticalEdge(horizontal, depth, true, false); + this.areaInit = AreaInit.northEast.to(this.areaInit); + } + void areaInitSouthEast(OcclusionField horizontal, OcclusionField depth) { + areaInitVerticalEdge(horizontal, depth, true, true); + this.areaInit = AreaInit.southEast.to(this.areaInit); + } + void areaInitNorthWest(OcclusionField horizontal, OcclusionField depth) { + areaInitVerticalEdge(horizontal, depth, false, false); + this.areaInit = AreaInit.northWest.to(this.areaInit); + } + void areaInitSouthWest(OcclusionField horizontal, OcclusionField depth) { + areaInitVerticalEdge(horizontal, depth, false, true); + this.areaInit = AreaInit.southWest.to(this.areaInit); + } + void areaInitUp(IColumnarSpace columnarSpace, int cy, OcclusionField other) { + resolveTruncatedFencesAndDoors(columnarSpace, cy, other, true); + this.areaInit = AreaInit.up.to(this.areaInit); + } + void areaInitDown(IColumnarSpace columnarSpace, int cy, OcclusionField other) { + resolveTruncatedFencesAndDoors(columnarSpace, cy, other, false); + this.areaInit = AreaInit.down.to(this.areaInit); + } + + private void resolveTruncatedFencesAndDoors(IColumnarSpace columnarSpace, int cy, OcclusionField other, final boolean end) { + if (other == null) + return; + + int i = LAST_INDEX - (DIMENSION_SQUARE_SIZE >> COORDINATE_TO_INDEX_SHR) + 1; + final OcclusionField subject, object; + + if (end) + { + subject = this; + object = other; + cy++; + } else { + subject = other; + object = this; + } + + final int y = (cy << DIMENSION_ORDER) - 1; + + final long[] words = subject.words; + final byte singleton = subject.singleton; + long word = 0; + + for (int z = 0; z < DIMENSION_SIZE; ++z) + for (int x = 0; x < DIMENSION_SIZE; x += ELEMENTS_PER_WORD) { + if (words != null) + word = words[i++]; + for (int b = 0; b < ELEMENTS_PER_WORD; ++b) { + final int xx = x + b; + final byte flags; + + if (words != null) + flags = (byte)(word & ELEMENT_MASK); + else + flags = singleton; + + final boolean + fenceLike = Element.earth.in(flags) && Logic.fuzzy.in(flags), + doorLike = Logic.doorway.in(flags); + + if (fenceLike || doorLike) { + final IBlockDescription block = columnarSpace.blockAt(xx, y, z); + if (fenceLike && block.isFenceLike() || doorLike && block.isDoor()) + object.set(xx, 0, z, flags); + } + + word >>= 1 << ELEMENT_LENGTH_SHL; + } + } + } + + private void areaInitZPlane(OcclusionField neighbor, final boolean end) { + long[] words = this.words; + final int + z0 = end ? DIMENSION_EXTENT : 0, + disposition = ((z0 / (DIMENSION_EXTENT)) << 1) - 1; + final long + neighborWords[] = neighbor.words, + singletonWord = words == null ? singletonWord() : 0, + neighborSingletonWord = neighborWords == null ? neighbor.singletonWord() : 0; + for (int y = 0, + index = z0 * DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR, + northIndex = (DIMENSION_SIZE - z0 - 1) * DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR; + + y < DIMENSION_SIZE; + + ++y + ) { + for (int x = 0; x < DIMENSION_SIZE; x += ELEMENTS_PER_WORD) { + long word = words == null ? singletonWord : words[index]; + final long + northWord, + southWord; + + { + final long + primary = words == null ? singletonWord : words[index + -disposition * (DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR)], + secondary = neighborWords == null ? neighborSingletonWord : neighborWords[northIndex]; + + if (disposition < 0) { + northWord = secondary; + southWord = primary; + } else { + northWord = primary; + southWord = secondary; + } + } + + for ( + int b = x == 0 ? 1 : 0, + bN = ELEMENTS_PER_WORD - (x + ELEMENTS_PER_WORD >= DIMENSION_SIZE ? 1 : 0); + b < bN; + ++b + ) { + + final long westWord, eastWord; + + if (words == null) + eastWord = westWord = singletonWord; + else { + westWord = words[index - ((b - 1) >> COORDINATE_TO_INDEX_SHR)]; + eastWord = words[index + ((b + 1) >> COORDINATE_TO_INDEX_SHR)]; + } + + word = areaWordFor(word, b, northWord, eastWord, southWord, westWord); + } + if (words == null && word != singletonWord) { + decompress(); + words = this.words; + } + + if (words != null) + words[index] = word; + + index++; + northIndex++; + } + + final int di = ((DIMENSION_EXTENT) * DIMENSION_SIZE) >> COORDINATE_TO_INDEX_SHR; + index += di; + northIndex += di; + } + } + + private void areaInitXPlane(OcclusionField neighbor, final boolean end) { + long[] words = this.words; + final int + x0 = end ? DIMENSION_EXTENT : 0, + disposition = ((x0 / (DIMENSION_EXTENT)) << 1) - 1, + offset = ((disposition + 1) >> 1) * WORD_LAST_OFFSET; + + final long + neighborWords[] = neighbor.words, + singletonWord = words == null ? singletonWord() : 0, + neighborSingletonWord = neighborWords == null ? neighbor.singletonWord() : 0; + for (int y = 0, + index = x0 + DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR, + neighborIndex = (DIMENSION_SIZE - x0 - 1) + DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR; + + y < DIMENSION_SIZE; + + ++y + ) { + for (int z = 1; z < DIMENSION_EXTENT; ++z) { + long word = words == null ? singletonWord : words[index]; + final long + westWord, + eastWord, + northWord, + southWord; + + { + final long + primary = words == null ? singletonWord : words[index], + secondary = neighborWords == null ? neighborSingletonWord : neighborWords[neighborIndex]; + + if (disposition < 0) { + westWord = secondary; + eastWord = primary; + } else { + westWord = primary; + eastWord = secondary; + } + } + + if (words == null) + southWord = northWord = singletonWord; + else { + northWord = words[index - (DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR)]; + southWord = words[index + (DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR)]; + } + + word = areaWordFor(word, offset, northWord, eastWord, southWord, westWord); + + if (words == null && word != singletonWord) { + decompress(); + words = this.words; + } + + if (words != null) + words[index] = word; + + final int di = DIMENSION_SIZE >> COORDINATE_TO_INDEX_SHR; + index += di; + neighborIndex += di; + } + + final int di = (DIMENSION_SIZE * 2) >> COORDINATE_TO_INDEX_SHR; + index += di; + neighborIndex += di; + } + } + + private void areaInitVerticalEdge(OcclusionField horizNeighbor, OcclusionField depthNeighbor, final boolean horizEnd, final boolean depthEnd) { + long[] words = this.words; + final int + x0 = horizEnd ? DIMENSION_EXTENT : 0, + z0 = depthEnd ? DIMENSION_EXTENT : 0, + xd = ((x0 / (DIMENSION_EXTENT)) << 1) - 1, + zd = ((z0 / (DIMENSION_EXTENT)) << 1) - 1, + offset = ((xd + 1) >> 1) * WORD_LAST_OFFSET; + + final long + horizNeighborWords[] = horizNeighbor.words, + depthNeighborWords[] = depthNeighbor.words, + singletonWord = words == null ? singletonWord() : 0, + horizNeighborSingletonWord = horizNeighborWords == null ? horizNeighbor.singletonWord() : 0, + depthNeighborSingletonWord = depthNeighborWords == null ? depthNeighbor.singletonWord() : 0; + + for (int y = 0, + index = z0 * DIMENSION_SIZE + x0 >> COORDINATE_TO_INDEX_SHR, + horizNeighborIndex = z0 * DIMENSION_SIZE + (DIMENSION_SIZE - x0 - 1) >> COORDINATE_TO_INDEX_SHR, + depthNeighborIndex = (DIMENSION_SIZE - z0 - 1) * DIMENSION_SIZE + x0 >> COORDINATE_TO_INDEX_SHR; + + y < DIMENSION_SIZE; + + ++y + ) { + long word = words == null ? singletonWord : words[index]; + final long + westWord, + eastWord, + northWord, + southWord; + + { + final long + horizSecondary = horizNeighborWords == null ? horizNeighborSingletonWord : horizNeighborWords[horizNeighborIndex], + depthSecondary = depthNeighborWords == null ? depthNeighborSingletonWord : depthNeighborWords[depthNeighborIndex]; + + if (xd < 0) { + westWord = horizSecondary; + eastWord = word; + } else { + westWord = word; + eastWord = horizSecondary; + } + + if (zd < 0) { + northWord = depthSecondary; + southWord = word; + } else { + northWord = word; + southWord = depthSecondary; + } + } + + word = areaWordFor(word, offset, northWord, eastWord, southWord, westWord); + + if (words == null && word != singletonWord) { + decompress(); + words = this.words; + } + + if (words != null) + words[index] = word; + + final int di = (DIMENSION_SIZE * DIMENSION_SIZE) >> COORDINATE_TO_INDEX_SHR; + index += di; + horizNeighborIndex += di; + depthNeighborIndex += di; + } + } + + private long areaWordFor(long centerWord, int offset, long northWord, long eastWord, long southWord, long westWord) { + byte + centerFlags = elementAt(centerWord, offset); + + final byte + northFlags = elementAt(northWord, offset), + southFlags = elementAt(southWord, offset), + westFlags = westFlags(offset, westWord), + eastFlags = eastFlags(offset, eastWord); + + centerFlags = areaFlagsFor(centerFlags, northFlags, eastFlags, southFlags, westFlags); + centerWord = modifyWord(centerWord, offset, centerFlags); + return centerWord; + } + + private long fenceAndDoorAreaWordFor(IColumnarSpace columnarSpace, int dx, int y, int dz, long centerWord, int offset, long upWord, long downWord, boolean handlingFenceTops) { + byte + centerFlags = elementAt(centerWord, offset); + final byte + upFlags = elementAt(upWord, offset), + downFlags = elementAt(downWord, offset); + + centerFlags = fenceAndDoorAreaFlagsFor(columnarSpace, dx, y, dz, centerFlags, upFlags, downFlags, handlingFenceTops); + centerWord = modifyWord(centerWord, offset, centerFlags); + return centerWord; + } + + private byte eastFlags(int offset, long eastWord) { + return elementAt(eastWord, (offset + 1) % ELEMENTS_PER_WORD); + } + + private byte westFlags(int offset, long westWord) { + return elementAt(westWord, (offset + ELEMENTS_PER_WORD - 1) % ELEMENTS_PER_WORD); + } + + private byte areaFlagsFor(byte centerFlags, byte northFlags, byte eastFlags, byte southFlags, byte westFlags) { + final Element + northElem = Element.of(northFlags), + southElem = Element.of(southFlags), + westElem = Element.of(westFlags), + eastElem = Element.of(eastFlags), + centerElem = Element.of(centerFlags); + + if (Logic.ladder.in(centerFlags) && (northElem == Element.earth || eastElem == Element.earth || southElem == Element.earth || westElem == Element.earth)) + centerFlags = Element.earth.to(centerFlags); + else if (centerElem == Element.air && Logic.nothing.in(centerFlags) + && ( + fuzziable(centerElem, northFlags) || + fuzziable(centerElem, eastFlags) || + fuzziable(centerElem, southFlags) || + fuzziable(centerElem, westFlags) + ) + ) + centerFlags = Logic.fuzzy.to(centerFlags); + + return centerFlags; + } + + private byte fenceAndDoorAreaFlagsFor(IColumnarSpace columnarSpace, int dx, int y, int dz, byte centerFlags, byte upFlags, byte downFlags, boolean handlingFenceTops) { + final boolean + downFenceOrDoorLike = fenceOrDoorLike(downFlags), + centerFenceOrDoorLike = fenceOrDoorLike(centerFlags); + + final IBlockDescription + centerBlock = columnarSpace.blockAt(dx, y, dz), + downBlock = columnarSpace.blockAt(dx, y - 1, dz); + + if (!centerBlock.isImpeding()) { + + if (downFenceOrDoorLike && !centerFenceOrDoorLike && downBlock.isFenceLike() + || + !handlingFenceTops && !downFenceOrDoorLike && centerFenceOrDoorLike && !centerBlock.isFenceLike() + || + downFenceOrDoorLike && centerFenceOrDoorLike && + ( + Logic.doorway.in(downFlags) && (downBlock.isFenceLike() && downBlock.isDoor() && (Element.earth.in(downFlags) || !(centerBlock.isDoor() && centerBlock.isFenceLike()))) + || + Logic.doorway.in(centerFlags) && !(downBlock.isFenceLike() && downBlock.isDoor()) + ) + ) + return downFlags; + } else if ( + downFenceOrDoorLike && centerFenceOrDoorLike && + Logic.doorway.in(downFlags) && Logic.doorway.in(centerFlags) && + downBlock.isDoor() && centerBlock.isDoor() + ) + return downFlags; + + return centerFlags; + } + + private boolean fuzziable(Element centerElem, byte otherFlags) { + final Element otherElement = Element.of(otherFlags); + return centerElem != otherElement && !(otherElement == Element.earth && Logic.fuzzy.in(otherFlags)); + } + + private long modifyWord(long word, int offset, byte flags) { + final int shl = offset << ELEMENT_LENGTH_SHL; + return (word & ~(ELEMENT_MASK << shl)) | (((long)flags) << shl); + } + + @SuppressWarnings("unused") + public void set(IColumnarSpace columnarSpace, int x, int y, int z, IBlockDescription blockDescription) { + final int + dx = x & DIMENSION_MASK, + dy = y & DIMENSION_MASK, + dz = z & DIMENSION_MASK; + + final byte flags = flagsFor(columnarSpace, x, y, z, blockDescription); + + if (set(dx, dy, dz, flags)) + { + final boolean + dzb = dz > 0 && dz < DIMENSION_EXTENT, + dxb = dx > 0 && dx < DIMENSION_EXTENT, + dyb = dy > 0 && dy < DIMENSION_EXTENT; + + if (dzb && dxb && dyb) + areaComputeAt(dx, dy, dz); + else + greaterAreaComputeAt(columnarSpace, x, y, z); + + if (dx > 1 && dzb) + areaComputeAt(dx - 1, dy, dz); + else + greaterAreaComputeAt(columnarSpace, x - 1, y, z); + + if (dx < DIMENSION_EXTENT - 1 && dzb) + areaComputeAt(dx + 1, dy, dz); + else + greaterAreaComputeAt(columnarSpace, x + 1, y, z); + + if (dz > 1 && dxb) + areaComputeAt(dx, dy, dz - 1); + else + greaterAreaComputeAt(columnarSpace, x, y, z - 1); + + if (dz < DIMENSION_EXTENT - 1 && dxb) + areaComputeAt(dx, dy, dz + 1); + else + greaterAreaComputeAt(columnarSpace, x, y, z + 1); + + if (dy > 0 && dy < DIMENSION_EXTENT) + fencesAndDoorsComputeAt(columnarSpace, dx, y, dz, true); + else + greaterFencesAndDoorsComputeAt(columnarSpace, x, y, z, true); + + if (dy > 1) + fencesAndDoorsComputeAt(columnarSpace, dx, y - 1, dz, false); + else + greaterFencesAndDoorsComputeAt(columnarSpace, x, y - 1, z, false); + + if (dy < DIMENSION_EXTENT - 1) + fencesAndDoorsComputeAt(columnarSpace, dx, y + 1, dz, false); + else + greaterFencesAndDoorsComputeAt(columnarSpace, x, y + 1, z, false); + } + } + + private boolean set(int dx, int dy, int dz, byte flags) { + if (this.words == null && flags != this.singleton) + decompress(); + + if (this.words != null) { + final int index = index(dx, dy, dz); + final long word = this.words[index]; + this.words[index] = modifyWord(word, dx % ELEMENTS_PER_WORD, flags); + + return true; + } + + return false; + } + + private int index(int dx, int dy, int dz) { + return (dy * DIMENSION_SQUARE_SIZE + dz * DIMENSION_SIZE + dx) >> COORDINATE_TO_INDEX_SHR; + } + + private void areaComputeAt(int dx, int dy, int dz) { + final long[] words = this.words; + final int + offset = dx % ELEMENTS_PER_WORD, + index = index(dx, dy, dz); + final long + northWord = words[index(dx, dy, dz - 1)], + southWord = words[index(dx, dy, dz + 1)], + westWord = words[index(dx - 1, dy, dz)], + eastWord = words[index(dx + 1, dy, dz)], + centerWord = words[index]; + + words[index] = areaWordFor(centerWord, offset, northWord, eastWord, southWord, westWord); + } + + private void fencesAndDoorsComputeAt(IColumnarSpace columnarSpace, int dx, int y, int dz, boolean handlingFenceTops) { + final long[] words = this.words; + final int + dy = y & DIMENSION_MASK, + offset = dx % ELEMENTS_PER_WORD, + index = index(dx, dy, dz); + final long + downWord = words[index(dx, dy - 1, dz)], + upWord = words[index(dx, dy + 1, dz)], + centerWord = words[index]; + + words[index] = fenceAndDoorAreaWordFor(columnarSpace, dx, y, dz, centerWord, offset, upWord, downWord, handlingFenceTops); + } + + private void greaterAreaComputeAt(IColumnarSpace columnarSpace, int x, int y, int z) { + final int + dx = x & DIMENSION_MASK, + dy = y & DIMENSION_MASK, + dz = z & DIMENSION_MASK, + cx = x >> DIMENSION_ORDER, + cy = y >> DIMENSION_ORDER, + cz = z >> DIMENSION_ORDER; + + final IInstanceSpace instance = columnarSpace.instance(); + + final OcclusionField + center = instance.optOcclusionFieldAt(cx, cy, cz); + + if (center == null) + return; + + final OcclusionField + north = instance.optOcclusionFieldAt(cx, cy, (z - 1) >> DIMENSION_ORDER), + east = instance.optOcclusionFieldAt((x + 1) >> DIMENSION_ORDER, cy, cz), + south = instance.optOcclusionFieldAt(cx, cy, (z + 1) >> DIMENSION_ORDER), + west = instance.optOcclusionFieldAt((x - 1) >> DIMENSION_ORDER, cy, cz); + + byte + centerFlags = center.elementAt(dx, dy, dz), + northFlags = north == null ? 0 : north.elementAt(dx, dy, (dz - 1) & DIMENSION_MASK), + southFlags = south == null ? 0 : south.elementAt(dx, dy, (dz + 1) & DIMENSION_MASK), + westFlags = west == null ? 0 : west.elementAt((dx - 1) & DIMENSION_MASK, dy, dz), + eastFlags = east == null ? 0 : east.elementAt((dx + 1) & DIMENSION_MASK, dy, dz); + + final byte flags = areaFlagsFor(centerFlags, northFlags, eastFlags, southFlags, westFlags); + + center.set(dx, dy, dz, flags); + } + + private void greaterFencesAndDoorsComputeAt(IColumnarSpace columnarSpace, int x, int y, int z, boolean handlingFenceTops) { + final int + dx = x & DIMENSION_MASK, + dy = y & DIMENSION_MASK, + dz = z & DIMENSION_MASK, + cx = x >> DIMENSION_ORDER, + cy = y >> DIMENSION_ORDER, + cz = z >> DIMENSION_ORDER; + + final IInstanceSpace instance = columnarSpace.instance(); + + final OcclusionField + center = instance.optOcclusionFieldAt(cx, cy, cz); + + if (center == null) + return; + + final OcclusionField + up = instance.optOcclusionFieldAt(cx, (y + 1) >> DIMENSION_ORDER, cz), + down = instance.optOcclusionFieldAt(cx, (y - 1) >> DIMENSION_ORDER, cz); + + byte + centerFlags = center.elementAt(dx, dy, dz), + upFlags = up == null ? 0 : up.elementAt(dx, (dy + 1) & DIMENSION_MASK, dz), + downFlags = down == null ? 0 : down.elementAt(dx, (dy - 1) & DIMENSION_MASK, dz); + + final byte flags = fenceAndDoorAreaFlagsFor(columnarSpace, dx, y, dz, centerFlags, upFlags, downFlags, handlingFenceTops); + + center.set(dx, dy, dz, flags); + } + + @Override + public byte elementAt(int x, int y, int z) { + byte element; + if (this.words != null) { + long word = this.words[index(x, y, z)]; + element = elementAt(word, x % ELEMENTS_PER_WORD); + } else + element = this.singleton; + return element; + } + + private byte elementAt(long word, final int offset) { + byte element; + element = (byte) (word >> (offset << ELEMENT_LENGTH_SHL)); + element &= ELEMENT_MASK; + return element; + } + + @Override + public String visualizeAt(int dy) { + return visualizeAt(this, dy, 0, 0, DIMENSION_SIZE, DIMENSION_SIZE); + } + + private byte flagsFor(IColumnarSpace columnarSpace, int x, int y, int z, IBlockDescription block) { + byte flags = 0; + + final IInstanceSpace instance = columnarSpace.instance(); + final boolean doorway = block.isDoor(); + + if (doorway) + flags |= (instance.blockObjectAt(x, y, z).isImpeding() ? Element.earth : Element.air).mask; + else if (!block.isImpeding()) + if (block.isLiquid()) + if (block.isIncinerating()) + flags |= Element.fire.mask; + else + flags |= Element.water.mask; + else if (block.isIncinerating()) + flags |= Element.fire.mask | Logic.fuzzy.mask; + else + flags |= Element.air.mask; + else + flags |= Element.earth.mask; + + if (doorway) + flags = Logic.doorway.to(flags); + else if (block.isClimbable()) + flags = Logic.ladder.to(flags); + else if (Element.earth.in(flags) && !block.isFullyBounded()) + flags = Logic.fuzzy.to(flags); + + return flags; + } + + static String visualizeAt(IOcclusionProvider provider, int dy, final int x0, final int z0, final int xN, final int zN) { + final StringBuilder sb = new StringBuilder(); + + for (int z = z0; z < zN; ++z) { + for (int x = x0; x < xN; ++x) + { + final char ch; + final byte flags = provider.elementAt(x, dy, z); + switch (Element.of(flags)) { + case air: + ch = Logic.fuzzy.in(flags) ? '░' : ' '; + break; + case earth: + if (Logic.climbable(flags)) + ch = '#'; + else if (Logic.fuzzy.in(flags)) + ch = '▄'; + else + ch = '█'; + break; + case fire: + ch = 'X'; + break; + case water: + ch = '≋'; + break; + + default: + ch = '?'; + break; + } + + sb.append(ch); + } + sb.append(System.lineSeparator()); + } + + return sb.toString(); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/Passibility.java b/src/main/java/com/extollit/gaming/ai/path/model/Passibility.java new file mode 100644 index 0000000..fc1acb3 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/Passibility.java @@ -0,0 +1,40 @@ +package com.extollit.gaming.ai.path.model; + +/** + * Expresses ratings for traversal into a particular path-point according to increasing risk. + * + * This is used to rate path points visited during A* triage according to the type of block. + */ +public enum Passibility { + /** + * Pristine, fully-passible, no risk to the entity + */ + passible, + + /** + * Mild risk pathing into this point, it could be close to lava or through water. + */ + risky, + + /** + * High risk pathing into this point, these points are usually over cliffs or on-fire + */ + dangerous, + + /** + * Impossible (or completely impractical) pathing into this point, usually impeded by collision bounds of + * the block. This also applies to lava since the chances of survival pathing through even one block of + * lava (when not fire-resistant) is effectively zero. + */ + impassible; + + public Passibility between(Passibility other) { + return values()[Math.max(ordinal(), other.ordinal())]; + } + public boolean betterThan(Passibility other) { + return ordinal() < other.ordinal(); + } + public boolean worseThan(Passibility other) { + return ordinal() > other.ordinal(); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/PathObject.java b/src/main/java/com/extollit/gaming/ai/path/model/PathObject.java new file mode 100644 index 0000000..1ddbf04 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/PathObject.java @@ -0,0 +1,361 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.collect.ArrayIterable; +import com.extollit.gaming.ai.path.IConfigModel; +import com.extollit.linalg.immutable.Vec3i; +import com.extollit.linalg.mutable.Vec3d; +import com.extollit.num.FloatRange; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; + +import static com.extollit.num.FastMath.*; + +public final class PathObject implements Iterable { + private static FloatRange DIRECT_LINE_TIME_LIMIT = new FloatRange(2, 4); + + final Vec3i[] points; + private final float speed; + + public int i; + + private Random random = new Random(); + private int + taxiUntil = 0, + adjacentIndex = 0, + length; + + private float nextDirectLineTimeout, lastMutationTime = -1; + + public static void configureFrom(IConfigModel configModel) { + DIRECT_LINE_TIME_LIMIT = configModel.directLineTimeLimit(); + } + + protected PathObject(float speed, Vec3i... points) { + this.points = points; + this.length = points.length; + this.speed = speed; + this.nextDirectLineTimeout = DIRECT_LINE_TIME_LIMIT.next(this.random); + } + + public void setRandomNumberGenerator(Random random) { + this.random = random; + } + + public static PathObject fromHead(float speed, HydrazinePathPoint head) { + int i = 1; + + for (HydrazinePathPoint p = head; p.up() != null; p = p.up()) + ++i; + + final Vec3i[] result = new Vec3i[i]; + final Vec3i key = head.key; + result[--i] = key; + + for (HydrazinePathPoint p = head; p.up() != null; result[--i] = p.key) + p = p.up(); + + if (result.length <= 1) + return null; + else + return new PathObject(speed, result); + } + + public void truncateTo(int length) { + if (length < 0 || length >= this.points.length) + throw new ArrayIndexOutOfBoundsException( + MessageFormat.format("Length is out of bounds 0 <= length < {0} but length = {1}", this.points.length, length) + ); + + this.length = length; + } + + public void untruncate() { + this.length = this.points.length; + } + + @Override + public Iterator iterator() { + return new ArrayIterable.Iter(this.points, this.length); + } + public final int length() { return this.length; } + public final Vec3i at(int i) { return this.points[i]; } + public final Vec3i current() { + return this.points[this.i]; + } + public final Vec3i last() { + final Vec3i[] points = this.points; + final int length = this.length; + if (length > 0) + return points[length - 1]; + else + return null; + } + + public final boolean done() { return this.i >= this.length; } + + public void update(final IPathingEntity subject) { + boolean mutated = false; + + try { + if (done()) + return; + + final int unlevelIndex = unlevelIndex(this.i, subject.coordinates()); + + int adjacentIndex; + double minDistanceSquared = Double.MAX_VALUE; + + { + final com.extollit.linalg.immutable.Vec3d currentPosition = subject.coordinates(); + final float width = subject.width(); + final float offset = pointToPositionOffset(width); + final Vec3d d = new Vec3d(currentPosition); + final int end = unlevelIndex + 1; + + for (int i = adjacentIndex = this.adjacentIndex; i < this.length && i < end; ++i) { + final Vec3i pp = at(i); + d.sub(pp); + d.sub(offset, 0, offset); + d.y = 0; + + final double distanceSquared = d.mg2(); + + if (distanceSquared < minDistanceSquared) { + adjacentIndex = i; + minDistanceSquared = distanceSquared; + } + + d.set(currentPosition); + } + } + + int targetIndex = this.i; + if (minDistanceSquared <= 0.25) { + int advanceTargetIndex; + + targetIndex = adjacentIndex; + if (targetIndex >= this.taxiUntil && (advanceTargetIndex = directLine(targetIndex, unlevelIndex)) > targetIndex) + targetIndex = advanceTargetIndex; + else + targetIndex = adjacentIndex + 1; + } else if (minDistanceSquared > 0.5) + targetIndex = adjacentIndex; + + mutated = this.adjacentIndex != adjacentIndex; + this.adjacentIndex = adjacentIndex; + this.i = Math.max(adjacentIndex, targetIndex); + + if (stagnantFor(subject) > this.nextDirectLineTimeout) { + if (this.taxiUntil < adjacentIndex) + this.taxiUntil = adjacentIndex + 1; + else + this.taxiUntil++; + + this.nextDirectLineTimeout = DIRECT_LINE_TIME_LIMIT.next(this.random); + } + + final Vec3i point = done() ? last() : current(); + if (point != null) + subject.moveTo(positionFor(subject, point)); + } finally { + if (mutated || this.lastMutationTime < 0) + this.lastMutationTime = subject.age() * this.speed; + } + } + + public boolean taxiing() { + return this.taxiUntil >= this.adjacentIndex; + } + + public void taxiUntil(int index) { + this.taxiUntil = index; + } + + public static com.extollit.linalg.immutable.Vec3d positionFor(IPathingEntity subject, Vec3i point) { + final float offset = pointToPositionOffset(subject.width()); + return new com.extollit.linalg.immutable.Vec3d( + point.x + offset, + point.y, + point.z + offset + ); + } + + private static float pointToPositionOffset(final float subjectWidth) { + final float dw = (float)(ceil(subjectWidth)) / 2; + return dw - floor(dw); + } + + public float stagnantFor(IPathingEntity pathingEntity) { return this.lastMutationTime < 0 ? 0 : pathingEntity.age() * this.speed - this.lastMutationTime; } + + protected int directLine(final int from, final int until) { + final int levelDistance = until - from + 1; + if (levelDistance < 2) + return from; + + int [] xis = new int[3], + zis = new int[3]; + int ii = 0, + i = from, + i0 = i, + xi00 = 0, + zi00 = 0, + sq; + + final int n = until - 1; + + final Vec3i[] points = this.points; + Vec3i p0 = points[i]; + + while (i++ < n) { + final Vec3i p = points[i]; + final int + dx = p.x - p0.x, + dz = p.z - p0.z; + + int + xi = xis[ii], + zi = zis[ii]; + + xi += dx; + zi += dz; + + if (xi * zi != 0) { + final int + xi0 = xis[ii], + zi0 = zis[ii]; + + xi00 = xi0; + zi00 = zi0; + + xi -= xi0; + zi -= zi0; + + if (++ii >= 3) { + --ii; + break; + } + + i0 = i - 1; + } + + xis[ii] = xi; + zis[ii] = zi; + + sq = xi * zi00 + zi * xi00; + sq *= sq; + + if (sq > (zi00 + xi00) * (zi00 + xi00)) + break; + + p0 = p; + } + i = i0; + + xi00 = xis[0]; + zi00 = zis[0]; + + final int iiN = ii; + int xi = 0, + zi = 0, + + axi00 = abs(xi00), + azi00 = abs(zi00); + + ii = 0; + p0 = points[i]; + while (i++ < n) { + final Vec3i p = points[i]; + final int + dx = p.x - p0.x, + dz = p.z - p0.z, + + xi0 = xi, + zi0 = zi; + + xi += dx; + zi += dz; + + if (abs(xi0) > axi00 || (abs(zi0) > azi00)) { + --i; + break; + } + + if (xi * zi != 0) { + if (xi0 != xi00 || zi0 != zi00) + break; + + xi -= xi00; + zi -= zi00; + + ii = (ii + 1) % iiN; + + xi00 = xis[ii]; + zi00 = zis[ii]; + + axi00 = abs(xi00); + azi00 = abs(zi00); + } + + if (dx * xi00 < 0 || dz * zi00 < 0) + break; + + p0 = p; + } + + return --i; + } + + private int unlevelIndex(int from, com.extollit.linalg.immutable.Vec3d position) { + final int y0 = floor(position.y); + int levelIndex = length(); + + for (int i = from; i < length(); ++i) + { + final Vec3i pp = at(i); + if (pp.y != y0) + { + levelIndex = i; + break; + } + } + return levelIndex; + } + + public boolean sameAs(PathObject other) { + return Arrays.equals(points, other.points); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PathObject that = (PathObject) o; + return i == that.i && sameAs(that); + } + + @Override + public int hashCode() { + final Vec3i last = last(); + return last == null ? 0 : last.hashCode(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + int index = 0; + sb.append("Last Mutation: "); + sb.append(this.lastMutationTime); + sb.append(System.lineSeparator()); + for (Vec3i pp : this.points) { + if (index++ == i) + sb.append('*'); + + sb.append(pp); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/extollit/gaming/ai/path/model/SortedPointQueue.java b/src/main/java/com/extollit/gaming/ai/path/model/SortedPointQueue.java new file mode 100644 index 0000000..7fa0674 --- /dev/null +++ b/src/main/java/com/extollit/gaming/ai/path/model/SortedPointQueue.java @@ -0,0 +1,174 @@ +package com.extollit.gaming.ai.path.model; + +import java.util.ArrayList; +import java.util.ListIterator; + +public class SortedPointQueue { + private static final float CULL_THRESHOLD = 0.25f; + + private final ArrayList list = new ArrayList<>(8); + + protected boolean fastAdd(HydrazinePathPoint point) { + if (point.assigned()) + throw new IllegalStateException("Point is already assigned"); + + if (!point.index(this.list.size())) + return false; + + this.list.add(point); + sortBack(point.index()); + return true; + } + + public final void clear() { + for (HydrazinePathPoint point : this.list) + point.unassign(); + this.list.clear(); + } + + public final boolean isEmpty() { + return this.list.isEmpty(); + } + + public HydrazinePathPoint top() { + return this.list.get(0); + } + public HydrazinePathPoint dequeue() { + final ArrayList list = this.list; + final HydrazinePathPoint point; + if (list.size() == 1) + point = list.remove(0); + else { + point = list.set(0, list.remove(list.size() - 1)); + sortForward(0); + } + + point.unassign(); + return point; + } + + public boolean modifyDistance(HydrazinePathPoint point, int distance) { + final int distance0 = point.journey(); + + if (point.journey(distance)) { + if (distance < distance0) + sortBack(point.index()); + else + sortForward(point.index()); + + return true; + } else + return false; + } + + private void sortBack(int index) { + final ArrayList list = this.list; + final HydrazinePathPoint originalPoint = list.get(index); + final int distanceRemaining = originalPoint.journey(); + final Passibility originalPassibility = originalPoint.passibility(); + while (index > 0) { + final int i = (index - 1) >> 1; + final HydrazinePathPoint point = list.get(i); + + final Passibility passibility = point.passibility(); + if ((distanceRemaining >= point.journey() && originalPassibility == passibility) || originalPassibility.worseThan(passibility)) + break; + + list.set(index, point); + point.index(index); + + index = i; + } + + list.set(index, originalPoint); + originalPoint.index(index); + } + + private void sortForward(int index) { + final ArrayList list = this.list; + HydrazinePathPoint originalPoint = list.get(index); + final int distanceRemaining = originalPoint.journey(); + final Passibility originalPassibility = originalPoint.passibility(); + + do { + final int i = 1 + (index << 1); + final int j = i + 1; + + if (i >= list.size()) + break; + + final HydrazinePathPoint pointAlpha = list.get(i); + final int distAlpha = pointAlpha.journey(); + final Passibility passibilityAlpha = pointAlpha.passibility(); + final HydrazinePathPoint pointBeta; + final int distBeta; + final Passibility passibilityBeta; + + if (j >= list.size()) { + pointBeta = null; + distBeta = Integer.MIN_VALUE; + passibilityBeta = Passibility.passible; + } else { + pointBeta = list.get(j); + distBeta = pointBeta.journey(); + passibilityBeta = pointBeta.passibility(); + } + + if ((distAlpha < distBeta && passibilityAlpha == passibilityBeta) + || passibilityAlpha.betterThan(passibilityBeta)) { + if ((distAlpha >= distanceRemaining && passibilityAlpha == originalPassibility) + || passibilityAlpha.worseThan(originalPassibility)) + break; + + list.set(index, pointAlpha); + pointAlpha.index(index); + index = i; + } else { + if (pointBeta == null || (distBeta >= distanceRemaining && passibilityAlpha == originalPassibility) + || passibilityBeta.worseThan(originalPassibility)) + break; + + list.set(index, pointBeta); + pointBeta.index(index); + index = j; + } + } while (true); + + list.set(index, originalPoint); + originalPoint.index(index); + } + + public boolean appendTo(HydrazinePathPoint point, HydrazinePathPoint parent, HydrazinePathPoint target) { + final int squareDelta = HydrazinePathPoint.squareDelta(parent, point); + + final byte length = point.length(); + if (!point.assigned() || (parent.length() + squareDelta < length*length && !point.passibility().betterThan(parent.passibility()))) { + if (point.appendTo(parent, (int)Math.sqrt(squareDelta), target)) { + final int distance = point.length() + point.delta(); + if (point.assigned()) + return modifyDistance(point, distance); + else if (point.journey(distance)) + add(point); + } else + point.orphan(); + } + + return false; + } + + public void add(HydrazinePathPoint point) { + if (fastAdd(point)) + return; + + final ArrayList list = this.list; + final int size = list.size(); + + final ListIterator i = list.listIterator(size); + for (int amount = (int)Math.ceil((float)size * CULL_THRESHOLD); amount > 0 && i.hasPrevious(); --amount) { + i.previous().unassign(); + i.remove(); + } + + fastAdd(point); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/AbstractHydrazinePathFinderTests.java b/src/test/java/com/extollit/gaming/ai/path/AbstractHydrazinePathFinderTests.java new file mode 100644 index 0000000..9e44d3a --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/AbstractHydrazinePathFinderTests.java @@ -0,0 +1,157 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.*; +import com.extollit.linalg.immutable.AxisAlignedBBox; +import com.extollit.linalg.immutable.Vec3d; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Before; +import org.mockito.Mock; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +abstract class AbstractHydrazinePathFinderTests { + protected static final Vec3i ORIGIN = new Vec3i(0, 0, 0); + + protected HydrazinePathFinder pathFinder; + + @Mock protected IInstanceSpace instanceSpace; + @Mock protected IOcclusionProvider occlusionProvider; + @Mock protected IDynamicMovableObject destinationEntity; + @Mock protected IPathingEntity pathingEntity; + + @Mock protected IPathingEntity.Capabilities capabilities; + + @Before + public void setup() { + setup(pathingEntity); + + when(destinationEntity.width()).thenReturn(0.6f); + when(destinationEntity.height()).thenReturn(1.8f); + + when(instanceSpace.occlusionProviderFor(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(occlusionProvider); + + pathFinder = new HydrazinePathFinder(pathingEntity, instanceSpace); + pathFinder.schedulingPriority(SchedulingPriority.high); + pathFinder.occlusionProvider(occlusionProvider); + when(capabilities.cautious()).thenReturn(true); + when(capabilities.aquaphobic()).thenReturn(true); + when(capabilities.climber()).thenReturn(true); + when(capabilities.opensDoors()).thenReturn(false); + when(capabilities.avoidsDoorways()).thenReturn(true); + } + + protected void setup(final IPathingEntity pathingEntity) { + when(pathingEntity.capabilities()).thenReturn(capabilities); + when(pathingEntity.searchRange()).thenReturn(32f); + + when(pathingEntity.width()).thenReturn(0.6f); + when(pathingEntity.height()).thenReturn(1.8f); + } + + protected void cautious(boolean flag) { + when(capabilities.cautious()).thenReturn(flag); + } + + protected void pos(IDynamicMovableObject mockedMob, double x, double y, double z) { + final Vec3d pos = new Vec3d(x, y, z); + when(mockedMob.coordinates()).thenReturn(pos); + } + + protected void pos(double x, double y, double z) { + final Vec3d pos = new Vec3d(x, y, z); + when(pathingEntity.coordinates()).thenReturn(pos); + } + + protected void pos(IPathingEntity mockedPathing, double x, double y, double z) { + pos((IDynamicMovableObject)pathingEntity, x, y, z); + } + + protected void solid(final int x, final int y, final int z) { + when(occlusionProvider.elementAt(x, y, z)).thenReturn(Element.earth.mask); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(TestingBlocks.stone); + } + + protected void fuzzy(final int x, final int y, final int z) { + final IBlockObject block = mock(IBlockObject.class); + + when(occlusionProvider.elementAt(x, y, z)).thenReturn(Logic.fuzzy.to(Element.earth.mask)); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(block); + when(block.bounds()).thenReturn(new AxisAlignedBBox(0, 0, 0, 1, 1, 1)); + when(block.isImpeding()).thenReturn(true); + } + protected void slabDown(final int x, final int y, final int z) { + fuzzy(x, y, z); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(TestingBlocks.slabDown); + } + protected void slabUp(final int x, final int y, final int z) { + fuzzy(x, y, z); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(TestingBlocks.slabUp); + } + + protected void climb(final int x, final int y, final int z) { + when(occlusionProvider.elementAt(x, y, z)).thenReturn(Logic.ladder.to(Element.earth.mask)); + } + + protected void clear(final int x, final int y, final int z) { + when(occlusionProvider.elementAt(x, y, z)).thenReturn(Element.air.mask); + } + + protected void water(final int x, final int y, final int z) { + when(occlusionProvider.elementAt(x, y, z)).thenReturn(Element.water.mask); + } + + protected void diver() { + when(capabilities.cautious()).thenReturn(false); + when(capabilities.swimmer()).thenReturn(true); + when(capabilities.aquaphobic()).thenReturn(false); + } + + protected void door(final int x, final int y, final int z, boolean open) { + byte mask = Logic.doorway.mask; + if (!open) + mask = Element.earth.to(mask); + + when(occlusionProvider.elementAt(x, y, z)).thenReturn(mask); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(TestingBlocks.door); + } + + protected void latFence(final int x, final int y, final int z) { + fence(new AxisAlignedBBox(0, 0, 0.45f, 1, 1.5f, 0.55f), x, y, z); + } + + protected void longFence(final int x, final int y, final int z) { + fence(new AxisAlignedBBox(0.45f, 0, 0, 0.55f, 1.5, 1), x, y, z); + } + + protected void cornerFenceSouthEast(final int x, final int y, final int z) { + fence(new AxisAlignedBBox(0.45f, 0, 0.45f, 1, 1.5f, 1), x, y, z); + } + + protected void cornerFenceSouthWest(final int x, final int y, final int z) { + fence(new AxisAlignedBBox(0, 0, 0.45f, 0.55f, 1.5f, 1), x, y, z); + } + + protected void cornerFenceNorthEast(final int x, final int y, final int z) { + fence(new AxisAlignedBBox(0.45f, 0, 0, 1, 1.5f, 0.55f), x, y, z); + } + + protected void cornerFenceNorthWest(final int x, final int y, final int z) { + fence(new AxisAlignedBBox(0, 0, 0, 0.55f, 1.5f, 0.55f), x, y, z); + } + + protected void fence(AxisAlignedBBox bounds, final int x, final int y, final int z) { + IBlockObject block = mock(IBlockObject.class); + + fuzzy(x, y, z); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(block); + when(block.bounds()).thenReturn(bounds); + when(block.isImpeding()).thenReturn(true); + + fuzzy(x, y + 1, z); + when(instanceSpace.blockObjectAt(x, y + 1, z)).thenReturn(block); + when(block.bounds()).thenReturn(bounds); + when(block.isImpeding()).thenReturn(true); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/FatHydrazinePathFinderTests.java b/src/test/java/com/extollit/gaming/ai/path/FatHydrazinePathFinderTests.java new file mode 100644 index 0000000..98606f2 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/FatHydrazinePathFinderTests.java @@ -0,0 +1,64 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.HydrazinePathPoint; +import com.extollit.gaming.ai.path.model.IPathingEntity; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class FatHydrazinePathFinderTests extends AbstractHydrazinePathFinderTests { + public void setup(IPathingEntity pathingEntity) { + super.setup(pathingEntity); + when(pathingEntity.width()).thenReturn(1.4f); + } + + @Test + public void stepUpEast() { + solid(0, -1, 0); + solid(1, -1, 0); + solid(2, 0, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(0, actual.key.y); + } + + @Test + public void stepUpWest() { + solid(0, -1, 0); + solid(-1, -1, 0); + solid(-2, 0, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(-1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(1, actual.key.y); + } + + @Test + public void stepUpSouth() { + solid(0, -1, 0); + solid(0, -1, 1); + solid(0, 0, 2); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(0, 0, 1), ORIGIN); + assertNotNull(actual); + assertEquals(0, actual.key.y); + } + + @Test + public void stepUpNorth() { + solid(0, -1, 0); + solid(0, -1, -1); + solid(0, 0, -2); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(0, 0, -1), ORIGIN); + assertNotNull(actual); + assertEquals(1, actual.key.y); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/FatIntegrationTests.java b/src/test/java/com/extollit/gaming/ai/path/FatIntegrationTests.java new file mode 100644 index 0000000..f282ff2 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/FatIntegrationTests.java @@ -0,0 +1,39 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.PathObject; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static com.extollit.gaming.ai.path.model.PathObjectUtil.assertPath; +import static org.junit.Assert.assertNotNull; + +@RunWith(MockitoJUnitRunner.class) +public class FatIntegrationTests extends AbstractHydrazinePathFinderTests { + @Test + public void cornerStepDown() { + diver(); + + solid(0, -1, 0); + solid(-1, -1, 0); + solid(+1, -1, 0); + solid(0, -2, -1); + solid(-1, -2, -1); + solid(+1, -1, -1); + solid(0, -2, -2); + solid(-1, -2, -2); + solid(+1, -2, -2); + + pos(super.pathingEntity, 0, 0, 0); + + final PathObject path = pathFinder.initiatePathTo(0, 0, -2); + assertNotNull(path); + final Vec3i [] expectedPath = { + ORIGIN, + new Vec3i(0, -1, -1), + new Vec3i(0, -1, -2) + }; + assertPath(path, expectedPath); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/HydrazinePathFinderTests.java b/src/test/java/com/extollit/gaming/ai/path/HydrazinePathFinderTests.java new file mode 100644 index 0000000..edc3bbf --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/HydrazinePathFinderTests.java @@ -0,0 +1,450 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.*; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class HydrazinePathFinderTests extends AbstractHydrazinePathFinderTests { + + @Test + public void stepUp() { + solid(0, -1, 0); + solid(1, 0, 0); + solid(1, 3, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(Passibility.passible, actual.passibility()); + assertEquals(1, actual.key.y); + } + + @Test + public void noStepUp() { + solid(0, -1, 0); + solid(1, -1, 0); + solid(1, 0, 0); + solid(1, 1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNull(actual); + } + + @Test + public void narrowAtHead() { + solid(1, 0, 0); + solid(1, 2, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNull(actual); + } + + @Test + public void narrowAtFeet() { + clear(1, 0, 0); + slabUp(1, 1, 0); + solid(1, -1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNull(actual); + } + + @Test + public void narrowAtFeetDown1() { + solid(0, -1, 0); + clear(1, 0, 0); + slabUp(1, 1, 0); + clear(1, -1, 0); + solid(1, -2, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNull(actual); + } + + @Test + public void barelyHeadSpace() { + solid(0, -1, 0); + clear(0, 0, 0); + clear(0, 1, 0); + slabDown(0, 0, 1); + clear(0, 1, 1); + slabUp(0, 2, 1); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(0, 0, 1), ORIGIN); + assertEquals(1, actual.key.y); + } + + @Test + public void climbLadder() { + solid(1, 0, 0); + solid(1, 1, 0); + solid(1, 2, 0); + solid(1, 3, 0); + climb(0, 0, 0); + climb(0, 1, 0); + climb(0, 2, 0); + climb(0, 3, 0); + solid(0, -1, 0); + solid(1, -1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + + assertNotNull(actual); + assertEquals(4, actual.key.y); + assertEquals(1, actual.key.x); + } + + @Test + public void noClimbDiagonal() { + solid(1, 0, 1); + solid(1, 1, 1); + solid(1, 2, 1); + solid(1, 3, 1); + climb(0, 0, 0); + climb(0, 1, 0); + climb(0, 2, 0); + climb(0, 3, 0); + solid(0, -1, 0); + solid(1, -1, 1); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 1), ORIGIN); + + assertNull(actual); + } + + @Test + public void diagonalFences() { + solid(0, 0, 0); + solid(1, 0, 0); + solid(0, 0, 1); + solid(1, 0, 1); + longFence(0, 1, 0); + longFence(1, 1, 1); + pos(0, 1, 1); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 1, 0), new Vec3i(0, 1, 1)); + + assertNotNull(actual); + assertEquals(new Vec3i(1, 1, 0), actual.key); + } + + @Test + public void ladderOneUp() { + solid(1, 0, 0); + solid(1, 1, 0); + solid(1, 2, 0); + solid(1, 3, 0); + climb(0, 0, 0); + climb(0, 1, 0); + climb(0, 2, 0); + solid(0, -1, 0); + solid(1, -1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + + assertNotNull(actual); + assertEquals(4, actual.key.y); + assertEquals(1, actual.key.x); + } + + @Test + public void ohSoHoppySlab() { + slabDown(0, 0, 0); + solid(0, 0, 1); + solid(0, 1, 1); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(0, 1, 1), new Vec3i(0, 1, 0)); + + assertNull(actual); + } + + @Test + public void noHoppySlab() { + slabDown(0, 0, 0); + solid(0, 0, 1); + slabDown(0, 1, 1); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(0, 1, 1), new Vec3i(0, 1, 0)); + + assertNotNull(actual); + assertEquals(2, actual.key.y); + } + + @Test + public void ladderOneTooManyUp() { + solid(1, 0, 0); + solid(1, 1, 0); + solid(1, 2, 0); + solid(1, 3, 0); + slabDown(1, 4, 0); + climb(0, 0, 0); + climb(0, 1, 0); + climb(0, 2, 0); + solid(0, -1, 0); + solid(1, -1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + + assertNull(actual); + } + + @Test + public void ladderHalfUp() { + solid(1, 0, 0); + solid(1, 1, 0); + solid(1, 2, 0); + slabDown(1, 3, 0); + climb(0, 0, 0); + climb(0, 1, 0); + climb(0, 2, 0); + solid(0, -1, 0); + solid(1, -1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + + assertNotNull(actual); + assertEquals(4, actual.key.y); + assertEquals(1, actual.key.x); + } + + @Test + public void slabUpDown() { + cautious(false); + + slabDown(0, 0, 0); + solid(0, -1, 0); + solid(1, -2, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 1, 0), new Vec3i(0, 1, 0)); + + assertNotNull(actual); + assertEquals(Passibility.risky, actual.passibility()); + assertEquals(-1, actual.key.y); + } + + @Test + public void safeFall() { + solid(1, -1, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(Passibility.passible, actual.passibility()); + } + + @Test + public void riskyFall() { + solid(1, -3, 0); + + cautious(false); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(Passibility.risky, actual.passibility()); + } + + @Test + public void veryRiskyFall() { + solid(1, -5, 0); + + cautious(false); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(Passibility.risky, actual.passibility()); + } + + @Test + public void dangerousFall() { + solid(1, -21, 0); + + cautious(false); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(Passibility.dangerous, actual.passibility()); + } + + @Test + public void swimming() { + solid(1, -3, 0); + water(1, -2, 0); + water(1, -1, 0); + solid(0, -3, 0); + water(0, -2, 0); + water(0, -1, 0); + + diver(); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertNotNull(actual); + assertEquals(0, actual.key.y); + } + + @Test + public void drown() { + for (int y = 20; y > -20; --y) + for (int z = -1; z <= +1; ++z) + for (int x = -1; x <= +1; ++x) + water(x, y, z); + + diver(); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertEquals(Passibility.risky, actual.passibility()); + assertEquals(0, actual.key.y); + } + + @Test + public void cesa() { + for (int y = 12; y > -20; --y) + for (int z = -1; z <= 1; ++z) + for (int x = -1; x <= 1; ++x) + water(x, y, z); + + diver(); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertEquals(Passibility.risky, actual.passibility()); + assertEquals(13, actual.key.y); + } + + @Test + public void dive() { + for (int y = -5; y > -10; --y) + for (int z = -1; z <= 1; ++z) + for (int x = -1; x <= 1; ++x) + water(x, y, z); + + diver(); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + assertEquals(Passibility.risky, actual.passibility()); + assertEquals(-4, actual.key.y); + } + + @Test + public void outPool() { + water(0, -1, 0); + clear(0, 0, 0); + clear(0, 1, 0); + clear(0, 2, 0); + + solid(1, -1, 0); + solid(1, 0, 0); + clear(1, 1, 0); + clear(1, 2, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + + assertNull(actual); + } + + @Test + public void climbOutPool() { + water(0, -1, 0); + climb(0, 0, 0); + climb(0, 1, 0); + climb(0, 2, 0); + + solid(1, -1, 0); + solid(1, 0, 0); + clear(1, 1, 0); + clear(1, 2, 0); + + final HydrazinePathPoint actual = pathFinder.passiblePointNear(new Vec3i(1, 0, 0), ORIGIN); + + assertEquals(Passibility.passible, actual.passibility()); + assertEquals(new Vec3i(1, 1, 0), actual.key); + } + + @Test + public void refinePassibilityGate() { + solid(0, -1, 0); + solid(1, -1, 0); + solid(-1, -1, 0); + pos(0.2f, 0, 0); + + longFence(0, 0, 0); + when(occlusionProvider.elementAt(0, 0, 0)).thenReturn(Logic.doorway.to(Element.earth.mask)); + + final PathObject path = pathFinder.initiatePathTo(1, 0, 0); + + assertNull(path); + + assertTrue(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(+1, 0, 0))); + assertFalse(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(-1, 0, 0))); + } + + @Test + public void refinePassibilityEast() { + solid(0, -1, 0); + solid(1, -1, 0); + solid(-1, -1, 0); + pos(0.2f, 0, 0); + + longFence(0, 0, 0); + + final PathObject path = pathFinder.initiatePathTo(1, 0, 0); + + assertNull(path); + + assertTrue(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(+1, 0, 0))); + assertFalse(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(-1, 0, 0))); + } + + @Test + public void refinePassibilityWest() { + solid(0, -1, 0); + solid(1, -1, 0); + solid(-1, -1, 0); + pos(0.8f, 0, 0); + + longFence(0, 0, 0); + + final PathObject path = pathFinder.initiatePathTo(-1, 0, 0); + + assertNull(path); + + assertTrue(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(-1, 0, 0))); + assertFalse(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(+1, 0, 0))); + } + + @Test + public void refinePassibilitySouth() { + solid(0, -1, 0); + solid(0, -1, +1); + solid(0, -1, -1); + pos(0, 0, 0.2f); + + latFence(0, 0, 0); + + final PathObject path = pathFinder.initiatePathTo(0, 0, 1); + + assertNull(path); + + assertTrue(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(0, 0, +1))); + assertFalse(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(0, 0, -1))); + } + + @Test + public void refinePassibilityNorth() { + solid(0, -1, 0); + solid(0, -1, +1); + solid(0, -1, -1); + pos(0, 0, 0.8f); + + latFence(0, 0, 0); + + final PathObject path = pathFinder.initiatePathTo(0, 0, -1); + + assertNull(path); + + assertTrue(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(0, 0, -1))); + assertFalse(pathFinder.unreachableFromSource(Vec3i.ZERO, new Vec3i(0, 0, +1))); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/IntegrationTests.java b/src/test/java/com/extollit/gaming/ai/path/IntegrationTests.java new file mode 100644 index 0000000..1022be0 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/IntegrationTests.java @@ -0,0 +1,392 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.PathObject; +import com.extollit.linalg.immutable.Vec3d; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static com.extollit.gaming.ai.path.model.PathObjectUtil.assertPath; +import static org.junit.Assert.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class IntegrationTests extends AbstractHydrazinePathFinderTests { + @Test + public void pathRiskyDown() { + when(super.capabilities.cautious()).thenReturn(false); + + solid(-4, -1, 0); + solid(-4, 0, 0); + solid(-4, 1, 0); + solid(-4, 2, 0); + solid(-5, 2, 0); + solid(-3, -1, 0); + solid(-2, -1, 0); + solid(-1, -1, 0); + solid(0, -1, 0); + + pos(super.destinationEntity, 0.5, 0, 0.5); + pos(super.pathingEntity, -4.5, 3, 0.5); + + PathObject path = pathFinder.trackPathTo(destinationEntity); + + final Vec3i[] expectedPath = { + new Vec3i(-5, 3, 0), + new Vec3i(-4, 3, 0), + new Vec3i(-3, 0, 0), + new Vec3i(-2, 0, 0), + new Vec3i(-1, 0, 0), + new Vec3i(0, 0, 0) + }; + assertPath(path, expectedPath); + } + + @Test + public void bruteOverTrapdoor() { + when(super.capabilities.opensDoors()).thenReturn(true); + when(super.capabilities.avoidsDoorways()).thenReturn(false); + + solid(0, -1, 0); + door(1, -1, 0, false); + solid(2, -1, 0); + + pos(super.pathingEntity, 0, 0, 0); + + PathObject path = pathFinder.initiatePathTo(2, 0, 0); + + assertPath(path, new Vec3i(0, 0, 0), new Vec3i(1, 0, 0), new Vec3i(2, 0, 0)); + } + + @Test + public void trapdoor() { + when(super.capabilities.opensDoors()).thenReturn(false); + when(super.capabilities.avoidsDoorways()).thenReturn(false); + + solid(0, -1, 0); + door(1, -1, 0, false); + solid(2, -1, 0); + + pos(super.pathingEntity, 0, 0, 0); + + PathObject path = pathFinder.initiatePathTo(2, 0, 0); + + assertPath(path, new Vec3i(0, 0, 0), new Vec3i(1, 0, 0), new Vec3i(2, 0, 0)); + } + + @Test + public void openTrapdoor() { + when(super.capabilities.opensDoors()).thenReturn(false); + when(super.capabilities.avoidsDoorways()).thenReturn(false); + + solid(0, -1, 0); + door(1, -1, 0, true); + solid(2, -1, 0); + + pos(super.pathingEntity, 0, 0, 0); + + PathObject path = pathFinder.initiatePathTo(2, 0, 0); + assertNull(path); + } + + @Test + public void solidStart() { + solid(0, 0, 0); + solid(0, 1, 0); + + pos(super.pathingEntity, 0, 0, 0); + + final PathObject path = pathFinder.initiatePathTo(1, 0, 0); + + assertPath(path, new Vec3i(0, 0, 0), new Vec3i(1, 0, 0)); + } + + @Test + public void groundedStart() { + when(super.capabilities.cautious()).thenReturn(false); + pos(super.pathingEntity, 0, 5, 0); + + solid(0, 0, 0); + solid(1, 0, 0); + + + final PathObject path = pathFinder.initiatePathTo(1, 1, 0); + + assertPath(path, new Vec3i(0, 1, 0), new Vec3i(1, 1, 0)); + } + + @Test + public void divingStart() { + when(super.capabilities.cautious()).thenReturn(false); + when(super.capabilities.swimmer()).thenReturn(true); + + water(0, 0, 0); + water(1, 0, 0); + water(0, -1, 0); + water(1, -1, 0); + water(0, -2, 0); + water(1, -2, 0); + solid(0, -3, 0); + solid(1, -3, 0); + solid(2, 0, 0); + solid(3, 0, 0); + + pos(super.pathingEntity, 0, 5, 0); + + final PathObject path = pathFinder.initiatePathTo(3, 1, 0); + + assertPath(path, + new Vec3i(0, 1, 0), + new Vec3i(1, 1, 0), + new Vec3i(2, 1, 0), + new Vec3i(3, 1, 0) + ); + + path.update(super.pathingEntity); + + assertEquals(0, path.i); + + pos(super.pathingEntity, 0.5, 1, 0.5); + path.update(super.pathingEntity); + + assertEquals(3, path.i); + } + + @Test + public void fatOutOfPool() { + when(super.capabilities.cautious()).thenReturn(false); + when(super.capabilities.swimmer()).thenReturn(true); + when(super.pathingEntity.width()).thenReturn(1.4f); + + solid(1, 1, 0); + solid(1, 1, +1); + solid(1, 1, -1); + solid(1, 0, 0); + solid(1, 0, +1); + solid(1, 0, -1); + solid(1, -1, 0); + solid(1, -1, +1); + solid(1, -1, -1); + solid(-2, -1, -1); + solid(-1, -1, -1); + solid(0, -1, -1); + solid(-2, -1, 0); + solid(-1, -1, 0); + solid(0, -1, 0); + solid(-2, -1, 1); + water(1, 0, -1); + water(0, 0, -1); + water(-1, 0, -1); + water(1, 0, 0); + water(0, 0, 0); + water(-1, 0, 0); + water(-2, 0, 1); + solid(1, 0, 1); + solid(1, 1, 1); + solid(0, 0, 1); + solid(0, 0, 2); + solid(1, 0, 2); + solid(0, 1, 1); + solid(0, 1, 2); + solid(1, 1, 2); + solid(0, 1, 3); + solid(1, 1, 3); + + pos(super.pathingEntity, 0, 0.5, 0); + + final PathObject path = pathFinder.initiatePathTo(0, 2, 3); + + assertPath( + path, + new Vec3i(0, 1, 0), + new Vec3i(-1, 1, 0), + new Vec3i(-1, 1, 1), + new Vec3i(-1, 1, 2), + new Vec3i(0, 2, 2), + new Vec3i(0, 2, 3) + ); + + path.update(pathingEntity); + assertEquals(1, path.i); + + pos(super.pathingEntity, -0.9, 0.5, 0.2); + path.update(pathingEntity); + assertEquals(2, path.i); + + pos(super.pathingEntity, -0.9, 0.5, 1.1); + path.update(pathingEntity); + assertEquals(3, path.i); + + pos(super.pathingEntity, -0.9, 1, 1.9); + path.update(pathingEntity); + assertEquals(4, path.i); + + pos(super.pathingEntity, 0.05, 2, 3.1); + path.update(pathingEntity); + assertTrue(path.done()); + } + + @Test + public void trackEntity() { + when(pathingEntity.coordinates()).thenReturn(new Vec3d(1, 10, 1)); + when(destinationEntity.coordinates()).thenReturn(new Vec3d(3, 10, 1)); + solid(1, 9, -1); + solid(1, 9, 0); + solid(1, 9, 1); + solid(2, 9, 1); + solid(3, 9, 1); + + PathObject pathObject = pathFinder.trackPathTo(destinationEntity); + + assertNotNull(pathObject); + assertEquals(new Vec3i(3, 10, 1), pathObject.last()); + + solid(1, 9, 0); + solid(1, 9, -1); + + when(destinationEntity.coordinates()).thenReturn(new Vec3d(1, 10, -1)); + pathObject = pathFinder.update(); + + assertNotNull(pathObject); + assertEquals(new Vec3i(1, 10, -1), pathObject.last()); + } + + @Test + public void stuckFence() { + pos(1.8f, 1, 1); + solid(1, 0, 1); + solid(0, 0, 1); + longFence(1, 1, 1); + + final PathObject path = pathFinder.initiatePathTo(0, 1, 1); + + assertNull(path); + } + + @Test + public void unstuckFenceCorner() { + cautious(true); + + pos(0.8f, 1, 0.8f); + solid(1, 0, 1); + solid(0, 0, 1); + solid(1, 0, 0); + solid(0, 0, 0); + + solid(0, 0, 2); + solid(1, 0, 2); + solid(2, 0, 2); + solid(2, 0, 0); + solid(2, 0, 1); + + cornerFenceSouthEast(0, 1, 0); + latFence(1, 1, 0); + longFence(0, 1, 1); + + final PathObject path = pathFinder.initiatePathTo(2, 1, 2); + + assertPath( + path, + + new Vec3i(0, 1, 0), + new Vec3i(1, 1, 1), + new Vec3i(2, 1, 1), + new Vec3i(2, 1, 2) + ); + } + + @Test + public void stuckFenceCorner() { + cautious(true); + + solid(1, 0, 1); + solid(0, 0, 1); + solid(1, 0, 0); + solid(0, 0, 0); + + solid(0, 0, 2); + solid(1, 0, 2); + solid(2, 0, 2); + solid(2, 0, 0); + solid(2, 0, 1); + + solid(0, 0, -1); + solid(1, 0, -1); + solid(2, 0, -1); + + cornerFenceSouthEast(0, 1, 0); + latFence(1, 1, 0); + latFence(2, 1, 0); + longFence(0, 1, 1); + + pos(0.8f, 1, 0.8f); + + final PathObject path = pathFinder.initiatePathTo(1, 1, -1); + + assertNull(path); + } + + @Test + public void fencedOut() { + solid(0, -1, 0); + solid(0, -1, -1); + solid(0, -1, +1); + solid(1, -1, 0); + solid(1, -1, -1); + solid(1, -1, +1); + solid(2, -1, 0); + solid(2, -1, -1); + solid(2, -1, +1); + pos(0, 0, 0); + + longFence(1, 0, 0); + longFence(1, 0, -1); + longFence(1, 0, +1); + + final PathObject path = pathFinder.initiatePathTo(2, 0, 0); + + assertNull(path); + } + + @Test + public void stuckSoTaxi() { + solid(0, -1, 0); + solid(0, -1, 1); + solid(0, -1, 2); + solid(0, -1, 3); + + when(capabilities.speed()).thenReturn(1.0f); + pos(0.5, 0, 0.5); + PathObject path = pathFinder.initiatePathTo(0, 0, 3); + + solid(0, 0, 2); + solid(0, 1, 2); + + path.update(pathingEntity); + + verify(pathingEntity).moveTo(new Vec3d(0.5, 0, 3.5)); + pos(0.5, 0, 1.5); + + PathObject path2 = pathFinder.update(); + + assertSame(path, path2); + + when(pathingEntity.age()).thenReturn(100); + + path2 = pathFinder.update(); + + assertNotSame(path, path2); + path = path2; + + path.update(pathingEntity); + when(pathingEntity.age()).thenReturn(200); + + path2 = pathFinder.update(); + assertSame(path, path2); + path2.update(pathingEntity); + + assertTrue(path2.taxiing()); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/TestableHydrazinePathFinder.java b/src/test/java/com/extollit/gaming/ai/path/TestableHydrazinePathFinder.java new file mode 100644 index 0000000..f60b223 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/TestableHydrazinePathFinder.java @@ -0,0 +1,16 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.IInstanceSpace; +import com.extollit.gaming.ai.path.model.IOcclusionProvider; +import com.extollit.gaming.ai.path.model.IPathingEntity; + +public class TestableHydrazinePathFinder extends HydrazinePathFinder { + public TestableHydrazinePathFinder(IPathingEntity entity, IInstanceSpace instanceSpace) { + super(entity, instanceSpace); + } + + @Override + public void occlusionProvider(IOcclusionProvider occlusionProvider) { + super.occlusionProvider(occlusionProvider); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/TestingBlocks.java b/src/test/java/com/extollit/gaming/ai/path/TestingBlocks.java new file mode 100644 index 0000000..f7f260a --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/TestingBlocks.java @@ -0,0 +1,179 @@ +package com.extollit.gaming.ai.path; + +import com.extollit.gaming.ai.path.model.IBlockObject; +import com.extollit.linalg.immutable.AxisAlignedBBox; + +public class TestingBlocks { + public static final IBlockObject + stone = new Stone(), + wall = new Wall(), + lava = new Lava(), + air = new Air(), + ladder = new Ladder(), + slabUp = new SlabUp(), + slabDown = new SlabDown(), + torch = air; + + public static final Door + door = new Door(); + public static final FenceGate + fenceGate = new FenceGate(); + + private static class AbstractBlockDescription implements IBlockObject { + @Override + public boolean isFenceLike() { + return false; + } + + @Override + public boolean isClimbable() { + return false; + } + + @Override + public boolean isDoor() { + return false; + } + + @Override + public boolean isImpeding() { + return false; + } + + @Override + public boolean isFullyBounded() { + return false; + } + + @Override + public boolean isLiquid() { + return false; + } + + @Override + public boolean isIncinerating() { + return false; + } + + @Override + public AxisAlignedBBox bounds() { + return new AxisAlignedBBox( + 0, 0, 0, + 1, isFenceLike() ? 1.5f : 0, 1 + ); + } + } + + public static class Stone extends AbstractBlockDescription { + @Override + public boolean isImpeding() { + return true; + } + + @Override + public boolean isFullyBounded() { + return true; + } + + @Override + public AxisAlignedBBox bounds() { + return new AxisAlignedBBox( + 0, 0, 0, + 1, 1, 1 + ); + } + } + + public static class Wall extends Stone { + @Override + public boolean isFenceLike() { + return true; + } + + @Override + public boolean isFullyBounded() { + return false; + } + } + + public static final class Lava extends AbstractBlockDescription { + @Override + public boolean isLiquid() { + return true; + } + + @Override + public boolean isIncinerating() { + return true; + } + } + + public static final class Air extends AbstractBlockDescription {} + + private static abstract class AbstractDoor extends AbstractBlockDescription { + public boolean open; + + @Override + public final boolean isDoor() { + return true; + } + + @Override + public final boolean isImpeding() { + return !open; + } + + @Override + public AxisAlignedBBox bounds() { + return new AxisAlignedBBox( + 0, 0, 0, + 1, 1, 1 + ); + } + } + + public static final class FenceGate extends AbstractDoor { + @Override + public boolean isFenceLike() { + return true; + } + } + + public static final class Door extends AbstractDoor {} + + public static final class Ladder extends AbstractBlockDescription { + @Override + public boolean isClimbable() { + return true; + } + } + + public static final class SlabDown extends AbstractBlockDescription { + @Override + public boolean isImpeding() { + return true; + } + @Override + public AxisAlignedBBox bounds() { + return new AxisAlignedBBox( + 0, 0, 0, + 1, 0.5, 1 + ); + } + } + + public static final class SlabUp extends AbstractBlockDescription { + @Override + public boolean isImpeding() { + return true; + } + + @Override + public AxisAlignedBBox bounds() { + return new AxisAlignedBBox( + 0, 0.5, 0, + 1, 1, 1 + ); + } + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/AbstractAreaInitOcclusionFieldTesting.java b/src/test/java/com/extollit/gaming/ai/path/model/AbstractAreaInitOcclusionFieldTesting.java new file mode 100644 index 0000000..326183e --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/AbstractAreaInitOcclusionFieldTesting.java @@ -0,0 +1,75 @@ +package com.extollit.gaming.ai.path.model; + +import org.junit.Before; + +import static com.extollit.gaming.ai.path.TestingBlocks.air; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public abstract class AbstractAreaInitOcclusionFieldTesting extends AbstractOcclusionProviderTesting { + private final IColumnarSpace [] columnarSpaces = new IColumnarSpace[OcclusionField.AreaInit.values().length - 2]; + private final OcclusionField [] fields = new OcclusionField[OcclusionField.AreaInit.values().length]; + + protected OcclusionField centerField; + protected AreaOcclusionProvider areaOcclusionProvider; + + protected final int cy0; + + protected AbstractAreaInitOcclusionFieldTesting(int cy0) { + this.cy0 = cy0; + } + + @Before + public void setup () { + super.setup(); + + this.centerField = new TestOcclusionField(); + + for (OcclusionField.AreaInit area : OcclusionField.AreaInit.values()) { + final IColumnarSpace columnarSpace = area == OcclusionField.AreaInit.up || area == OcclusionField.AreaInit.down ? centerSpace : (columnarSpaces[area.ordinal()] = mock(IColumnarSpace.class)); + final OcclusionField field = fields[area.ordinal()] = new TestOcclusionField(area); + + when(columnarSpace.instance()).thenReturn(instanceSpace); + when(columnarSpace.blockAt(anyInt(), anyInt(), anyInt())).thenReturn(air); + when(columnarSpace.toString()).thenReturn("Chunk to the " + area.name()); + + when(columnarSpace.optOcclusionFieldAt(area.offset.dy)).thenReturn(field); + when(columnarSpace.occlusionFieldAt(area.offset.dx, area.offset.dy + cy0, area.offset.dz)).thenReturn(field); + when(instanceSpace.optOcclusionFieldAt(area.offset.dx, area.offset.dy + cy0, area.offset.dz)).thenReturn(field); + } + when(centerSpace.blockAt(anyInt(), anyInt(), anyInt())).thenReturn(air); + when(centerSpace.toString()).thenReturn("Center space"); + + when(centerSpace.optOcclusionFieldAt(cy0)).thenReturn(centerField); + when(centerSpace.occlusionFieldAt(0, cy0, 0)).thenReturn(centerField); + when(instanceSpace.optOcclusionFieldAt(0, cy0, 0)).thenReturn(centerField); + + this.areaOcclusionProvider = new AreaOcclusionProvider( + new IColumnarSpace[][] { + new IColumnarSpace[] { columnarSpaces[OcclusionField.AreaInit.northWest.ordinal()], columnarSpaces[OcclusionField.AreaInit.north.ordinal()], columnarSpaces[OcclusionField.AreaInit.northEast.ordinal()] }, + new IColumnarSpace[] { columnarSpaces[OcclusionField.AreaInit.west.ordinal()], centerSpace, columnarSpaces[OcclusionField.AreaInit.east.ordinal()] }, + new IColumnarSpace[] { columnarSpaces[OcclusionField.AreaInit.southWest.ordinal()], columnarSpaces[OcclusionField.AreaInit.south.ordinal()], columnarSpaces[OcclusionField.AreaInit.southEast.ordinal()] } + }, + -1, + -1 + ); + } + + protected final OcclusionField field(final OcclusionField.AreaInit area) { + return fields[area.ordinal()]; + } + + protected final IColumnarSpace columnarSpace(final OcclusionField.AreaInit area) { + return area == OcclusionField.AreaInit.up || area == OcclusionField.AreaInit.down ? centerSpace : columnarSpaces[area.ordinal()]; + } + + protected final void set(final OcclusionField.AreaInit area, final int x, final int y, final int z, final IBlockObject block) { + field(area).set(columnarSpace(area), x, y, z, block); + when(instanceSpace.blockObjectAt(x, y, z)).thenReturn(block); + } + protected final void set(final int x, final int y, final int z, final IBlockObject block) { + blockAt(x, y, z, block); + centerField.set(centerSpace, x, y, z, block); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionFieldTesting.java b/src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionFieldTesting.java new file mode 100644 index 0000000..5464ec5 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionFieldTesting.java @@ -0,0 +1,45 @@ +package com.extollit.gaming.ai.path.model; + +import org.junit.Before; + +import static com.extollit.gaming.ai.path.TestingBlocks.air; +import static com.extollit.gaming.ai.path.TestingBlocks.fenceGate; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.when; + +public class AbstractOcclusionFieldTesting extends AbstractOcclusionProviderTesting { + protected OcclusionField occlusionField; + + @Before + public void setup () { + super.setup(); + + this.occlusionField = new TestOcclusionField(); + when(centerSpace.blockAt(anyInt(), anyInt(), anyInt())).thenReturn(air); + when(centerSpace.optOcclusionFieldAt(anyInt())).thenReturn(occlusionField); + } + + protected void verifyNeighborhood(final int x, final int y, final int z, + final Element test, + final Element westElem, final Element eastElem, + final Element northElem, final Element southElem) { + final byte element = this.occlusionField.elementAt(x, y, z); + final byte + west = this.occlusionField.elementAt(x - 1, y, z), + east = this.occlusionField.elementAt(x + 1, y, z), + north = this.occlusionField.elementAt(x, y, z - 1), + south = this.occlusionField.elementAt(x, y, z + 1); + + assertTrue(test.in(element)); + assertTrue(westElem.in(west)); + assertTrue(eastElem.in(east)); + assertTrue(northElem.in(north)); + assertTrue(southElem.in(south)); + } + + protected void fenceGate(boolean open, final int x, final int y, final int z) { + blockAt(x, y, z, fenceGate); + fenceGate.open = open; + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionProviderTesting.java b/src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionProviderTesting.java new file mode 100644 index 0000000..f8baa88 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/AbstractOcclusionProviderTesting.java @@ -0,0 +1,46 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.gaming.ai.path.TestingBlocks; +import org.junit.Before; +import org.mockito.Mock; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +public class AbstractOcclusionProviderTesting { + @Mock + protected IColumnarSpace centerSpace; + @Mock + protected IInstanceSpace instanceSpace; + + public static void assertDoorway(IOcclusionProvider occlusionProvider, boolean open, int x, int y, int z) { + final byte + bottom = occlusionProvider.elementAt(x, y, z), + top = occlusionProvider.elementAt(x, y + 1, z); + + assertTrue(Logic.doorway.in(top)); + assertTrue(Logic.doorway.in(bottom)); + final Element expectedElement = open ? Element.air : Element.earth; + assertTrue(expectedElement.in(top)); + assertTrue(expectedElement.in(bottom)); + } + + @Before + public void setup() { + when(centerSpace.instance()).thenReturn(instanceSpace); + } + + protected void door(boolean open, final int x, final int y, final int z) { + final TestingBlocks.Door door = TestingBlocks.door; + + blockAt(x, y, z, door); + blockAt(x, y + 1, z, door); + + door.open = open; + } + + protected void blockAt(int x, int y, int z, IBlockObject blockObject) { + when(this.centerSpace.blockAt(x, y, z)).thenReturn(blockObject); + when(this.instanceSpace.blockObjectAt(x, y, z)).thenReturn(blockObject); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/AreaInitOcclusionFieldTests.java b/src/test/java/com/extollit/gaming/ai/path/model/AreaInitOcclusionFieldTests.java new file mode 100644 index 0000000..0a49d2f --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/AreaInitOcclusionFieldTests.java @@ -0,0 +1,279 @@ +package com.extollit.gaming.ai.path.model; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static com.extollit.gaming.ai.path.TestingBlocks.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AreaInitOcclusionFieldTests extends AbstractAreaInitOcclusionFieldTesting { + public AreaInitOcclusionFieldTests() { + super(3); + } + + @Test + public void center() { + final IColumnarSpace columnarSpace = centerSpace; + when(columnarSpace.blockAt(5, 5, 5)).thenReturn(lava); + + final OcclusionField main = centerField; + main.loadFrom(columnarSpace, 0, 0, 0); + + assertFalse(Logic.fuzzy.in(main.elementAt(5, 5, 5))); + assertTrue(Logic.fuzzy.in(main.elementAt(6, 5, 5))); + assertTrue(Logic.fuzzy.in(main.elementAt(4, 5, 5))); + assertTrue(Logic.fuzzy.in(main.elementAt(5, 5, 6))); + assertTrue(Logic.fuzzy.in(main.elementAt(5, 5, 4))); + assertFalse(Logic.fuzzy.in(main.elementAt(6, 5, 6))); + assertFalse(Logic.fuzzy.in(main.elementAt(4, 5, 4))); + assertFalse(Logic.fuzzy.in(main.elementAt(4, 5, 6))); + assertFalse(Logic.fuzzy.in(main.elementAt(6, 5, 4))); + } + + @Test + public void west() { + set(OcclusionField.AreaInit.west, -1, 5, 5, lava); + final OcclusionField field = centerField; + field.areaInitWest(field(OcclusionField.AreaInit.west)); + + assertTrue(Logic.fuzzy.in(field.elementAt(0, 5, 5))); + assertFalse(Logic.fuzzy.in(field.elementAt(0, 5, 4))); + assertFalse(Logic.fuzzy.in(field.elementAt(0, 5, 6))); + assertFalse(Logic.fuzzy.in(field.elementAt(1, 5, 5))); + } + + @Test + public void north() { + set(OcclusionField.AreaInit.north, 5, 5, -1, lava); + final OcclusionField field = centerField; + field.areaInitNorth(field(OcclusionField.AreaInit.north)); + + assertTrue(Logic.fuzzy.in(field.elementAt(5, 5, 0))); + assertFalse(Logic.fuzzy.in(field.elementAt(4, 5, 0))); + assertFalse(Logic.fuzzy.in(field.elementAt(6, 5, 0))); + assertFalse(Logic.fuzzy.in(field.elementAt(5, 5, 1))); + } + + @Test + public void east() { + set(OcclusionField.AreaInit.east, 16, 5, 5, lava); + final OcclusionField main = centerField; + main.areaInitEast(field(OcclusionField.AreaInit.east)); + + assertTrue(Logic.fuzzy.in(main.elementAt(15, 5, 5))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 5, 4))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 5, 6))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 5, 5))); + } + + @Test + public void south() { + set(OcclusionField.AreaInit.south, 5, 5, 16, lava); + final OcclusionField main = centerField; + main.areaInitSouth(field(OcclusionField.AreaInit.south)); + + assertTrue(Logic.fuzzy.in(main.elementAt(5, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(4, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(6, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(5, 5, 14))); + } + + @Test + public void northWest() { + set(OcclusionField.AreaInit.west, -1, 5, 0, lava); + set(OcclusionField.AreaInit.north, 0, 5, -1, lava); + set(OcclusionField.AreaInit.west, -1, 6, 0, lava); + set(OcclusionField.AreaInit.north, 0, 7, -1, lava); + final OcclusionField main = centerField; + main.areaInitNorthWest(field(OcclusionField.AreaInit.west), field(OcclusionField.AreaInit.north)); + + assertTrue(Logic.fuzzy.in(main.elementAt(0, 5, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(1, 5, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(0, 5, 1))); + assertTrue(Logic.fuzzy.in(main.elementAt(0, 6, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(1, 6, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(0, 6, 1))); + assertTrue(Logic.fuzzy.in(main.elementAt(0, 7, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(1, 7, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(0, 7, 1))); + + assertFalse(Logic.fuzzy.in(main.elementAt(0, 8, 0))); + } + + @Test + public void northEast() { + set(OcclusionField.AreaInit.east, 16, 5, 0, lava); + set(OcclusionField.AreaInit.north, 15, 5, -1, lava); + set(OcclusionField.AreaInit.east, 16, 6, 0, lava); + set(OcclusionField.AreaInit.north, 15, 7, -1, lava); + final OcclusionField main = centerField; + main.areaInitNorthEast(field(OcclusionField.AreaInit.east), field(OcclusionField.AreaInit.north)); + + assertTrue(Logic.fuzzy.in(main.elementAt(15, 5, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 5, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 5, 1))); + assertTrue(Logic.fuzzy.in(main.elementAt(15, 6, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 6, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 6, 1))); + assertTrue(Logic.fuzzy.in(main.elementAt(15, 7, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 7, 0))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 7, 1))); + + assertFalse(Logic.fuzzy.in(main.elementAt(15, 8, 0))); + } + + @Test + public void southWest() { + set(OcclusionField.AreaInit.west, -1, 5, 15, lava); + set(OcclusionField.AreaInit.south, 0, 5, 16, lava); + set(OcclusionField.AreaInit.west, -1, 6, 15, lava); + set(OcclusionField.AreaInit.south, 0, 7, 16, lava); + final OcclusionField main = centerField; + main.areaInitSouthWest(field(OcclusionField.AreaInit.west), field(OcclusionField.AreaInit.south)); + + assertTrue(Logic.fuzzy.in(main.elementAt(0, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(1, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(0, 5, 14))); + assertTrue(Logic.fuzzy.in(main.elementAt(0, 6, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(1, 6, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(0, 6, 14))); + assertTrue(Logic.fuzzy.in(main.elementAt(0, 7, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(1, 7, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(0, 7, 14))); + + assertFalse(Logic.fuzzy.in(main.elementAt(0, 8, 15))); + } + + @Test + public void southEast() { + set(OcclusionField.AreaInit.east, 16, 5, 15, lava); + set(OcclusionField.AreaInit.south, 15, 5, 16, lava); + set(OcclusionField.AreaInit.east, 16, 6, 15, lava); + set(OcclusionField.AreaInit.south, 15, 7, 16, lava); + final OcclusionField main = centerField; + main.areaInitSouthEast(field(OcclusionField.AreaInit.east), field(OcclusionField.AreaInit.south)); + + assertTrue(Logic.fuzzy.in(main.elementAt(15, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 5, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 5, 14))); + assertTrue(Logic.fuzzy.in(main.elementAt(15, 6, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 6, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 6, 14))); + assertTrue(Logic.fuzzy.in(main.elementAt(15, 7, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(14, 7, 15))); + assertFalse(Logic.fuzzy.in(main.elementAt(15, 7, 14))); + + assertFalse(Logic.fuzzy.in(main.elementAt(15, 8, 15))); + } + + @Test + public void truncatedFence() { + final IColumnarSpace columnarSpace = centerSpace; + blockAt(5, 63, 5, wall); + + final OcclusionField main = centerField, up = field(OcclusionField.AreaInit.up); + main.loadFrom(columnarSpace, 0, 3, 0); + up.loadFrom(columnarSpace, 0, 4, 0); + + assertFalse(main.areaInitAt(OcclusionField.AreaInit.up)); + + final byte [] wall = { + areaOcclusionProvider.elementAt(5, 63, 5), + areaOcclusionProvider.elementAt(5, 64, 5) + }; + + assertTrue(main.areaInitAt(OcclusionField.AreaInit.up)); + + assertTrue(Element.earth.in(wall[0])); + assertTrue(Element.earth.in(wall[1])); + assertTrue(Logic.fuzzy.in(wall[0])); + assertTrue(Logic.fuzzy.in(wall[1])); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5, 62, 5))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5, 65, 5))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5 + 1, 63, 5))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5 - 1, 63, 5))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5, 63, 5 + 1))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5, 63, 5 - 1))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5 + 1, 64, 5))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5 - 1, 64, 5))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5, 64, 5 + 1))); + assertFalse(Element.earth.in(areaOcclusionProvider.elementAt(5, 64, 5 - 1))); + } + + @Test + public void truncatedFenceReverseQuery() { + final IColumnarSpace columnarSpace = centerSpace; + blockAt(5, 63, 5, wall); + + final OcclusionField up = field(OcclusionField.AreaInit.up); + up.loadFrom(columnarSpace, 0, 4, 0); + centerField.loadFrom(centerSpace, 0, 3, 0); + + final byte flags = areaOcclusionProvider.elementAt(5, 64, 5); + assertTrue(Element.earth.in(flags) && Logic.fuzzy.in(flags)); + } + + @Test + public void tooLow() { + final OcclusionField bottomField = new OcclusionField(); + when(centerSpace.occlusionFieldAt(0, 0, 0)).thenReturn(bottomField); + areaOcclusionProvider.elementAt(5, 0, 5); + assertTrue(bottomField.areaInitAt(OcclusionField.AreaInit.down)); + } + + @Test + public void tooHigh() { + final OcclusionField topField = new OcclusionField(); + when(centerSpace.occlusionFieldAt(0, 15, 0)).thenReturn(topField); + areaOcclusionProvider.elementAt(5, 255, 5); + assertTrue(topField.areaInitAt(OcclusionField.AreaInit.up)); + } + + @Test + public void doorOpened() { + door(true, 4, 63, 6); + + final OcclusionField main = centerField, up = field(OcclusionField.AreaInit.up); + final IColumnarSpace columnarSpace = this.centerSpace; + + main.loadFrom(columnarSpace, 0, 3, 0); + up.loadFrom(columnarSpace, 0, 4, 0); + + assertDoorway(areaOcclusionProvider, true, 4, 63, 6); + } + + @Test + public void doorClosed() { + door(false, 4, 63, 6); + + final OcclusionField main = centerField, up = field(OcclusionField.AreaInit.up); + final IColumnarSpace columnarSpace = this.centerSpace; + + main.loadFrom(columnarSpace, 0, 3, 0); + up.loadFrom(columnarSpace, 0, 4, 0); + + assertDoorway(areaOcclusionProvider, false, 4, 63, 6); + } + + @Test + public void swimmableLava() { + for (int k = 0; k < 3; ++k) + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) + blockAt(i, j, k, stone); + + blockAt(1, 1, 1, lava); + + final IColumnarSpace columnarSpace = centerSpace; + final OcclusionField main = centerField; + main.loadFrom(columnarSpace, 0, 0, 0); + + final byte lava = main.elementAt(1, 1, 1); + assertTrue(Element.fire.in(lava)); + assertFalse(Logic.fuzzy.in(lava)); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/AreaSetOcclusionFieldTests.java b/src/test/java/com/extollit/gaming/ai/path/model/AreaSetOcclusionFieldTests.java new file mode 100644 index 0000000..db05a70 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/AreaSetOcclusionFieldTests.java @@ -0,0 +1,302 @@ +package com.extollit.gaming.ai.path.model; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static com.extollit.gaming.ai.path.TestingBlocks.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class AreaSetOcclusionFieldTests extends AbstractAreaInitOcclusionFieldTesting { + public AreaSetOcclusionFieldTests() { + super(3); + } + + @Test + public void center () { + set(5, 50, 5, lava); + + assertNeighborhood(5, 50, 5); + } + + @Test + public void west() { + set(0, 50, 5, lava); + + verify(instanceSpace, times(7)).optOcclusionFieldAt(-1, 3, 0); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(0, 50, 5); + } + + @Test + public void east() { + set(15, 50, 5, lava); + + verify(instanceSpace, times(7)).optOcclusionFieldAt(+1, 3, 0); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(15, 50, 5); + } + + @Test + public void north() { + set(5, 50, 0, lava); + + verify(instanceSpace, times(7)).optOcclusionFieldAt(0, 3, -1); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(5, 50, 0); + } + + @Test + public void south() { + set(5, 50, 15, lava); + + verify(instanceSpace, times(7)).optOcclusionFieldAt(0, 3, +1); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(5, 50, 15); + } + + @Test + public void northWest() { + set(0, 50, 0, lava); + + verify(instanceSpace, times(2)).optOcclusionFieldAt(-1, 3, -1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(0, 3, -1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(-1, 3, 0); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(0, 50, 0); + } + + @Test + public void northEast() { + set(15, 50, 0, lava); + + verify(instanceSpace, times(2)).optOcclusionFieldAt(+1, 3, -1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(0, 3, -1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(+1, 3, 0); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(15, 50, 0); + } + + @Test + public void southWest() { + set(0, 50, 15, lava); + + verify(instanceSpace, times(2)).optOcclusionFieldAt(-1, 3, +1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(0, 3, +1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(-1, 3, 0); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(0, 50, 15); + } + + @Test + public void southEast() { + set(15, 50, 15, lava); + + verify(instanceSpace, times(2)).optOcclusionFieldAt(+1, 3, +1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(0, 3, +1); + verify(instanceSpace, times(5)).optOcclusionFieldAt(+1, 3, 0); + verify(instanceSpace, times(13)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(15, 50, 15); + } + + @Test + public void westInner() { + set(1, 50, 5, lava); + + verify(instanceSpace).optOcclusionFieldAt(-1, 3, 0); + verify(instanceSpace, times(4)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(1, 50, 5); + } + + @Test + public void eastInner() { + set(14, 50, 5, lava); + + verify(instanceSpace).optOcclusionFieldAt(+1, 3, 0); + verify(instanceSpace, times(4)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(14, 50, 5); + } + + @Test + public void northInner() { + set(5, 50, 1, lava); + + verify(instanceSpace).optOcclusionFieldAt(0, 3, -1); + verify(instanceSpace, times(4)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(5, 50, 1); + } + + @Test + public void southInner() { + set(5, 50, 14, lava); + + verify(instanceSpace).optOcclusionFieldAt(0, 3, +1); + verify(instanceSpace, times(4)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(5, 50, 14); + } + + @Test + public void northWestInner() { + set(1, 50, 1, lava); + + verify(instanceSpace, never()).optOcclusionFieldAt(-1, 3, -1); + verify(instanceSpace).optOcclusionFieldAt(0, 3, -1); + verify(instanceSpace).optOcclusionFieldAt(-1, 3, 0); + verify(instanceSpace, times(8)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(1, 50, 1); + } + + @Test + public void northEastInner() { + set(14, 50, 1, lava); + + verify(instanceSpace, never()).optOcclusionFieldAt(+1, 3, -1); + verify(instanceSpace).optOcclusionFieldAt(0, 3, -1); + verify(instanceSpace).optOcclusionFieldAt(+1, 3, 0); + verify(instanceSpace, times(8)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(14, 50, 1); + } + + @Test + public void southWestInner() { + set(1, 50, 14, lava); + + verify(instanceSpace, never()).optOcclusionFieldAt(-1, 3, +1); + verify(instanceSpace).optOcclusionFieldAt(0, 3, +1); + verify(instanceSpace).optOcclusionFieldAt(-1, 3, 0); + verify(instanceSpace, times(8)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(1, 50, 14); + } + + @Test + public void southEastInner() { + set(14, 50, 14, lava); + + verify(instanceSpace, never()).optOcclusionFieldAt(+1, 3, +1); + verify(instanceSpace).optOcclusionFieldAt(0, 3, +1); + verify(instanceSpace).optOcclusionFieldAt(+1, 3, 0); + verify(instanceSpace, times(8)).optOcclusionFieldAt(0, 3, 0); + + assertNeighborhood(14, 50, 14); + } + + @Test + public void wallUp() { + set(5, 63, 5, wall); + + verify(instanceSpace, atLeastOnce()).optOcclusionFieldAt(0, +4, 0); + + final byte top = areaOcclusionProvider.elementAt(5, 64, 5); + + assertTrue(Element.earth.in(top) && Logic.fuzzy.in(top)); + } + + @Test + public void undoWallUp() { + blockAt(5, 63, 5, wall); + centerField.loadFrom(centerSpace, 0, 3, 0); + + final byte pre = areaOcclusionProvider.elementAt(5, 64, 5); + assertTrue(Element.earth.in(pre) && Logic.fuzzy.in(pre)); + + set(5, 63, 5, air); + + verify(instanceSpace, atLeastOnce()).optOcclusionFieldAt(0, +4, 0); + + final byte top = areaOcclusionProvider.elementAt(5, 64, 5); + + assertFalse(Element.earth.in(top) || Logic.fuzzy.in(top)); + } + + @Test + public void wallDown() { + set(5, 48, 5, wall); + + blockAt(5, 48, 5, wall); + + verify(instanceSpace, never()).optOcclusionFieldAt(0, +4, 0); + + final byte top = areaOcclusionProvider.elementAt(5, 49, 5); + + assertTrue(Element.earth.in(top) && Logic.fuzzy.in(top)); + } + + @Test + public void undoWallDown() { + blockAt(5, 48, 5, wall); + + centerField.loadFrom(centerSpace, 0, 3, 0); + + final byte pre = areaOcclusionProvider.elementAt(5, 49, 5); + assertTrue(Element.earth.in(pre) && Logic.fuzzy.in(pre)); + + set(5, 48, 5, air); + + verify(instanceSpace, never()).optOcclusionFieldAt(0, +4, 0); + + final byte top = areaOcclusionProvider.elementAt(5, 49, 5); + + assertFalse(Element.earth.in(top) || Logic.fuzzy.in(top)); + } + + @Test + public void ladderClimable() { + set(5, 50, 5, stone); + set(6, 50, 5, ladder); + + assertTrue(Logic.climbable(areaOcclusionProvider.elementAt(6, 50, 5))); + assertFalse(Logic.climbable(areaOcclusionProvider.elementAt(5, 50, 5))); + assertFalse(Logic.climbable(areaOcclusionProvider.elementAt(7, 50, 5))); + assertFalse(Logic.climbable(areaOcclusionProvider.elementAt(6, 50, 4))); + assertFalse(Logic.climbable(areaOcclusionProvider.elementAt(6, 50, 6))); + } + + private void assertNeighborhood(final int dx, final int dy, final int dz) { + assertTrue(Logic.fuzzy.in(areaOcclusionProvider.elementAt((dx - 1), dy, dz))); + assertTrue(Logic.fuzzy.in(areaOcclusionProvider.elementAt((dx + 1), dy, dz))); + assertTrue(Logic.fuzzy.in(areaOcclusionProvider.elementAt(dx, dy, (dz - 1)))); + assertTrue(Logic.fuzzy.in(areaOcclusionProvider.elementAt(dx, dy, (dz + 1)))); + assertFalse(Logic.fuzzy.in(areaOcclusionProvider.elementAt((dx + 1), dy, (dz + 1)))); + assertFalse(Logic.fuzzy.in(areaOcclusionProvider.elementAt((dx - 1), dy, (dz - 1)))); + assertFalse(Logic.fuzzy.in(areaOcclusionProvider.elementAt((dx - 1), dy, (dz + 1)))); + assertFalse(Logic.fuzzy.in(areaOcclusionProvider.elementAt((dx + 1), dy, (dz - 1)))); + } + + @Test + public void doorOpened() { + door(false, 4, 63, 6); + centerField.loadFrom(centerSpace, 0, 3, 0); + door(true, 4, 63, 6); + + centerField.set(centerSpace, 4, 63, 6, door); + assertDoorway(areaOcclusionProvider, true, 4, 63, 6); + } + + @Test + public void doorClosed() { + door(true, 4, 63, 6); + centerField.loadFrom(centerSpace, 0, 3, 0); + door(false, 4, 63, 6); + + centerField.set(centerSpace, 4, 63, 6, door); + assertDoorway(areaOcclusionProvider, false, 4, 63, 6); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/DirectLineTests.java b/src/test/java/com/extollit/gaming/ai/path/model/DirectLineTests.java new file mode 100644 index 0000000..d5d4673 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/DirectLineTests.java @@ -0,0 +1,305 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class DirectLineTests { + @Test + public void advanceDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(5, 0, 2), + new Vec3i(4, 0, 2), + new Vec3i(3, 0, 3) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(7, i); + } + + @Test + public void fatAdvanceDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(3, 0, 2), + new Vec3i(3, 0, 3), + new Vec3i(4, 0, 3) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(6, i); + } + + @Test + public void veryFatAdvanceDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(4, 0, 3), + new Vec3i(3, 0, 4), + new Vec3i(3, 0, 3), + new Vec3i(3, 0, 4) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(4, i); + } + + @Test + public void fullyAdvanceDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(5, 0, 2), + new Vec3i(6, 0, 2), + new Vec3i(6, 0, 3) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(9, i); + } + + @Test + public void smallAdvanceDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(1, 0, 0), + new Vec3i(1, 0, -1), + new Vec3i(2, 0, -1), + new Vec3i(2, 0, 0), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(5, 0, 3), + new Vec3i(6, 0, 4), + new Vec3i(5, 0, 5), + new Vec3i(4, 0, 6) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(2, i); + } + + @Test + public void noAdvanceDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, -1), + new Vec3i(2, 0, -1), + new Vec3i(2, 0, 0), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(5, 0, 3), + new Vec3i(6, 0, 4), + new Vec3i(5, 0, 5), + new Vec3i(4, 0, 6) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(1, i); + } + + @Test + public void angleThenUp() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(0, 0, -1), + new Vec3i(0, 0, -2), + new Vec3i(-1, 0, -2), + new Vec3i(-1, 0, -3), + new Vec3i(-1, 0, -4), + new Vec3i(-2, 0, -4), + new Vec3i(-2, 0, -5), + new Vec3i(-2, 0, -6), + new Vec3i(-2, 0, -7), + new Vec3i(-2, 0, -8), + new Vec3i(-2, 0, -9), + new Vec3i(-2, 0, -10), + new Vec3i(-2, 0, -11), + new Vec3i(-2, 0, -12) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(8, i); + } + + @Test + public void softAngleThenHardAngle() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(0, 0, -1), + new Vec3i(0, 0, -2), + new Vec3i(-1, 0, -2), + new Vec3i(-1, 0, -3), + new Vec3i(-1, 0, -4), + new Vec3i(-2, 0, -4), + new Vec3i(-2, 0, -5), + new Vec3i(-2, 0, -6), + new Vec3i(-3, 0, -6), + new Vec3i(-3, 0, -7), + new Vec3i(-4, 0, -7), + new Vec3i(-4, 0, -8), + new Vec3i(-5, 0, -8), + new Vec3i(-5, 0, -9) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(10, i); + } + + @Test + public void noAdvanceEll() { + final PathObject path = new PathObject( + 1, + new Vec3i(-2, 4, 9), + new Vec3i(-2, 4, 8), + new Vec3i(-2, 4, 7), + new Vec3i(-2, 4, 6), + new Vec3i(-2, 4, 5), + new Vec3i(-3, 4, 5), + new Vec3i(-4, 4, 5), + new Vec3i(-5, 4, 5), + new Vec3i(-6, 4, 5) + ); + + final int i = path.directLine(0, 8); + assertEquals(4, i); + } + + @Test + public void nonTaxiCab() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(1, 0, 1), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 2), + new Vec3i(4, 0, 2), + new Vec3i(5, 0, 3), + new Vec3i(6, 0, 4), + new Vec3i(7, 0, 5) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(4, i); + } + + @Test + public void diagonal() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(1, 0, 1), + new Vec3i(2, 0, 2), + new Vec3i(3, 0, 3), + new Vec3i(4, 0, 4), + new Vec3i(4, 0, 5), + new Vec3i(5, 0, 6) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(2, i); + } + + @Test + public void slenderDirectLine() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(+1, 0, 0), + new Vec3i(0, 0, 0), + new Vec3i(-1, 0, 0), + new Vec3i(-2, 0, 0), + new Vec3i(-3, 0, 0), + new Vec3i(-3, 0, 1), + new Vec3i(-4, 0, 1), + new Vec3i(-5, 0, 1), + new Vec3i(-6, 0, 1), + new Vec3i(-7, 0, 1), + new Vec3i(-7, 0, 2), + new Vec3i(-8, 0, 2), + new Vec3i(-9, 0, 2), + new Vec3i(-10, 0, 2), + new Vec3i(-10, 0, 3), + new Vec3i(-9, 0, 3), + new Vec3i(-8, 0, 3), + new Vec3i(-7, 0, 3) + ); + + final int i = pathObject.directLine(0, pathObject.length()); + + assertEquals(13, i); + } + + @Test + public void horseshoe() { + final PathObject path = new PathObject( + 1, + new Vec3i(1, 0, -1), + new Vec3i(1, 0, 0), + new Vec3i(1, 0, 1), + new Vec3i(0, 0, 1), + new Vec3i(0, 0, 0), + new Vec3i(0, 0, -1), + new Vec3i(0, 0, -2) + ); + + final int i = path.directLine(0, path.length()); + + assertEquals(3, i); + } + + @Test + public void subtleEll () { + final PathObject path = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(2, 0, 2), + new Vec3i(3, 0, 2), + new Vec3i(4, 0, 2) + ); + + final int i = path.directLine(0, path.length()); + + assertEquals(2, i); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/DoorOcclusionFieldTests.java b/src/test/java/com/extollit/gaming/ai/path/model/DoorOcclusionFieldTests.java new file mode 100644 index 0000000..11a2765 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/DoorOcclusionFieldTests.java @@ -0,0 +1,60 @@ +package com.extollit.gaming.ai.path.model; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static com.extollit.gaming.ai.path.TestingBlocks.door; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class DoorOcclusionFieldTests extends AbstractOcclusionFieldTesting { + @Test + public void offset() { + final int + x = 208, y = 64, z = -1389, + dx = x & 0xF, dy = y & 0xF, dz = z & 0xF; + + door(true, x, y, z); + + occlusionField.set(centerSpace, x, y, z, door); + final byte element = occlusionField.elementAt(dx, dy, dz); + + verify(instanceSpace, atLeastOnce()).optOcclusionFieldAt(12, 4, -87); + + assertTrue(Logic.doorway.in(element)); + } + + @Test + public void initOpen() { + door(true, 4, 5, 6); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + assertDoorway(true, 4, 5, 6); + } + + @Test + public void initClosed() { + door(false, 4, 5, 6); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + assertDoorway(false, 4, 5, 6); + } + + @Test + public void opened() { + door(false, 4, 5, 6); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + door(true, 4, 5, 6); + + occlusionField.set(centerSpace, 4, 5, 6, door); + assertDoorway(true, 4, 5, 6); + } + + private void assertDoorway(boolean open, final int x, final int y, final int z) { + final OcclusionField occlusionField = this.occlusionField; + assertDoorway(occlusionField, open, x, y, z); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/IntegrationTests.java b/src/test/java/com/extollit/gaming/ai/path/model/IntegrationTests.java new file mode 100644 index 0000000..24d4589 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/IntegrationTests.java @@ -0,0 +1,101 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.Vec3d; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class IntegrationTests { + @Mock + private IPathingEntity pathingEntity; + + @Before + public void setup() { + when(pathingEntity.width()).thenReturn(0.6f); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(0.5, 0, 0.5)); + } + + @Test + public void jackknife() { + PathObject path = new PathObject( + 1, + new Vec3i(-1, 4, 10), + new Vec3i(-2, 4, 11), + new Vec3i(-3, 4, 11), + new Vec3i(-4, 4, 11) + ); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(-0.5, 4, 10.5)); + + path.update(pathingEntity); + assertEquals(1, path.i); + + verify(pathingEntity).moveTo(new Vec3d(-1.5, 4, 11.5)); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(-1.5, 4, 11.5)); + + path = new PathObject( + 1, + new Vec3i(-1, 4, 10), + new Vec3i(-2, 4, 11), + new Vec3i(-3, 4, 11), + new Vec3i(-4, 4, 11), + new Vec3i(-4, 4, 10), + new Vec3i(-4, 4, 9), + new Vec3i(-3, 4, 9), + new Vec3i(-2, 4, 9), + new Vec3i(-1, 4, 9), + new Vec3i(0, 4, 9), + new Vec3i(1, 4, 9) + ); + + path.update(pathingEntity); + assertEquals(3, path.i); + + verify(pathingEntity).moveTo(new Vec3d(-3.5, 4, 11.5)); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(-3.5, 4, 11.5)); + path.update(pathingEntity); + assertEquals(5, path.i); + + verify(pathingEntity).moveTo(new Vec3d(-3.5, 4, 9.5)); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(-3.5, 4, 9.5)); + path.update(pathingEntity); + assertEquals(10, path.i); + } + + @Test + public void taxi() { + PathObject path = new PathObject( + 1, + new Vec3i(0, 4, 2), + new Vec3i(0, 4, 3), + new Vec3i(1, 4, 3), + new Vec3i(1, 4, 4), + new Vec3i(2, 4, 4) + ); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(0.4, 4.5, 2.4)); + + path.taxiUntil(2); + + path.update(pathingEntity); + assertEquals(1, path.i); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(0.4, 4.5, 3.4)); + path.update(pathingEntity); + assertEquals(2, path.i); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(1.4, 4.5, 3.4)); + path.update(pathingEntity); + assertEquals(4, path.i); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/OcclusionFieldTests.java b/src/test/java/com/extollit/gaming/ai/path/model/OcclusionFieldTests.java new file mode 100644 index 0000000..c65385c --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/OcclusionFieldTests.java @@ -0,0 +1,541 @@ +package com.extollit.gaming.ai.path.model; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static com.extollit.gaming.ai.path.TestingBlocks.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalMatchers.leq; +import static org.mockito.AdditionalMatchers.lt; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class OcclusionFieldTests extends AbstractOcclusionFieldTesting { + @Test + public void control() { + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + assertTrue(Element.air.in(occlusionField.elementAt(1, 2, 3))); + } + + @Test + public void point() { + when(centerSpace.blockAt(anyInt(), leq(7), anyInt())).thenReturn(stone); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + occlusionField.set(centerSpace, 4, 14, 8, stone); + verifyNeighborhood(4, 14, 8, + Element.earth, + Element.air, + Element.air, + Element.air, + Element.air + ); + } + + @Test + public void point2() { + when(centerSpace.blockAt(anyInt(), lt(4), anyInt())).thenReturn(stone); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + occlusionField.set(centerSpace, 15, 4, 3, lava); + verifyNeighborhood(15, 4,3, + Element.fire, + Element.air, + Element.air, + Element.air, + Element.air + ); + } + + @Test + public void xPlane() { + when(centerSpace.blockAt(leq(7), anyInt(), anyInt())).thenReturn(stone); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + for (int z = 0; z < 16; ++z) + for (int y = 0; y < 16; ++y) + for (int x = 0; x < 16; ++x) { + final byte element = occlusionField.elementAt(x, y, z); + if (x <= 7) + assertFalse(Element.air.in(element)); + else + assertTrue(Element.air.in(element)); + } + } + + + @Test + public void yPlane() { + when(centerSpace.blockAt(anyInt(), leq(7), anyInt())).thenReturn(stone); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + for (int z = 0; z < 16; ++z) + for (int y = 0; y < 16; ++y) + for (int x = 0; x < 16; ++x) { + final byte element = occlusionField.elementAt(x, y, z); + if (y <= 7) + assertFalse(Element.air.in(element)); + else + assertTrue(Element.air.in(element)); + } + } + + @Test + public void zPlane() { + when(centerSpace.blockAt(anyInt(), anyInt(), leq(7))).thenReturn(stone); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + for (int z = 0; z < 16; ++z) + for (int y = 0; y < 16; ++y) + for (int x = 0; x < 16; ++x) { + final byte element = occlusionField.elementAt(x, y, z); + if (z <= 7) + assertFalse(Element.air.in(element)); + else + assertTrue(Element.air.in(element)); + } + } + + @Test + public void lava() { + when(centerSpace.blockAt(0, 0, 0)).thenReturn(stone); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + occlusionField.set(centerSpace, 3, 9, 2, lava); + final byte element = this.occlusionField.elementAt(3, 9, 2); + assertTrue(Element.fire.in(element)); + } + + @Test + public void wall() { + blockAt(5, 5, 5, wall); + + occlusionField.loadFrom(centerSpace, 0, 0, 0); + final byte [] wall = { + occlusionField.elementAt(5, 5, 5), + occlusionField.elementAt(5, 6, 5), + }; + assertTrue(Element.earth.in(wall[0])); + assertTrue(Element.earth.in(wall[1])); + assertTrue(Logic.fuzzy.in(wall[0])); + assertTrue(Logic.fuzzy.in(wall[1])); + assertFalse(Element.earth.in(occlusionField.elementAt(5, 4, 5))); + assertFalse(Element.earth.in(occlusionField.elementAt(5, 7, 5))); + for (int y = 5; y <= 6; ++y) { + assertFalse(Element.earth.in(occlusionField.elementAt(5 + 1, y, 5))); + assertFalse(Element.earth.in(occlusionField.elementAt(5 - 1, y, 5))); + assertFalse(Element.earth.in(occlusionField.elementAt(5, y, 5 + 1))); + assertFalse(Element.earth.in(occlusionField.elementAt(5, y, 5 - 1))); + } + } + + @Test + public void placeFenceGate() { + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + fenceGate(false, 5, 5, 5); + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + } + + @Test + public void removeFenceGate() { + fenceGate(false, 5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + + when(centerSpace.blockAt(5, 5, 5)).thenReturn(air); + occlusionField.set(centerSpace, 5, 5, 5, air); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.air.in(bottom)); + assertTrue(Element.air.in(top)); + assertTrue(Logic.nothing.in(bottom)); + assertTrue(Logic.nothing.in(top)); + } + + @Test + public void openFenceGate() { + fenceGate(false, 5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + + fenceGate(true, 5, 5, 5); + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.air.in(bottom)); + assertTrue(Element.air.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + } + + @Test + public void closeFenceGate() { + fenceGate(true, 5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.air.in(bottom)); + assertTrue(Element.air.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + + fenceGate.open = false; + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + } + + @Test + public void openCappedFenceGate() { + fenceGate(false, 5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(stone); + occlusionField.set(centerSpace, 5, 6, 5, stone); + + assertTrue(Element.earth.in(occlusionField.elementAt(5, 6, 5))); + + fenceGate.open = true; + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.air.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.nothing.in(top)); + } + + @Test + public void closeCappedFenceGate() { + fenceGate(true, 5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(stone); + occlusionField.set(centerSpace, 5, 6, 5, stone); + + assertTrue(Element.earth.in(occlusionField.elementAt(5, 6, 5))); + + fenceGate.open = false; + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.nothing.in(top)); + } + + @Test + public void removeClosedCappedFenceGate() { + fenceGate(false, 5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(stone); + occlusionField.set(centerSpace, 5, 6, 5, stone); + + when(centerSpace.blockAt(5, 5, 5)).thenReturn(air); + occlusionField.set(centerSpace, 5, 5, 5, air); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.air.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.nothing.in(bottom)); + assertTrue(Logic.nothing.in(top)); + } + + @Test + public void placeClosedCappedFenceGate() { + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(stone); + occlusionField.set(centerSpace, 5, 6, 5, stone); + + fenceGate(false, 5, 5, 5); + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.nothing.in(top)); + } + + @Test + public void fenceGateToWall() { + fenceGate(true,5, 5, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + when(centerSpace.blockAt(5, 5, 5)).thenReturn(wall); + occlusionField.set(centerSpace, 5, 5, 5, wall); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.fuzzy.in(bottom)); + assertTrue(Logic.fuzzy.in(top)); + } + + @Test + public void wallToFenceGate() { + blockAt(5, 5, 5, wall); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + fenceGate(false, 5, 5, 5); + occlusionField.set(centerSpace, 5, 5, 5, fenceGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + assertTrue(Logic.doorway.in(bottom)); + assertTrue(Logic.doorway.in(top)); + } + + @Test + public void torchUp() { + blockAt(5, 5, 5, wall); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + + occlusionField.set(centerSpace, 5, 6, 5, torch); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + } + + @Test + public void torchDown() { + blockAt(5, 5, 5, wall); + blockAt(5, 6, 5, torch); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + + occlusionField.set(centerSpace, 5, 6, 5, air); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(bottom)); + assertTrue(Element.earth.in(top)); + } + + @Test + @Ignore("Insufficient information in the occlusion field to determine this reliably, will need to refactor. Currently this means that two fence gates, the top one closed, will be considered as if both are open. This is an acceptable trade-off.") + public void stackedFenceGates() { + fenceGate.open = false; + + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + final FenceGate + topGate = new FenceGate(), + bottomGate = new FenceGate(); + + bottomGate.open = true; + topGate.open = false; + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(fenceGate); + when(centerSpace.blockAt(5, 5, 5)).thenReturn(fenceGate); + when(instanceSpace.blockObjectAt(5, 6, 5)).thenReturn(topGate); + when(instanceSpace.blockObjectAt(5, 5, 5)).thenReturn(bottomGate); + + occlusionField.set(centerSpace, 5, 6, 5, topGate); + occlusionField.set(centerSpace, 5, 5, 5, bottomGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(top)); + assertFalse(Element.earth.in(bottom)); + + topGate.open = true; + occlusionField.set(centerSpace, 5, 6, 5, topGate); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertFalse(Element.earth.in(top)); + assertFalse(Element.earth.in(bottom)); + } + + @Test + public void invertedStackedFenceGates() { + fenceGate.open = false; + + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + final FenceGate + topGate = new FenceGate(), + bottomGate = new FenceGate(); + + bottomGate.open = false; + topGate.open = true; + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(fenceGate); + when(centerSpace.blockAt(5, 5, 5)).thenReturn(fenceGate); + when(instanceSpace.blockObjectAt(5, 6, 5)).thenReturn(topGate); + when(instanceSpace.blockObjectAt(5, 5, 5)).thenReturn(bottomGate); + + occlusionField.set(centerSpace, 5, 6, 5, topGate); + occlusionField.set(centerSpace, 5, 5, 5, bottomGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(top)); + assertTrue(Element.earth.in(bottom)); + + topGate.open = false; + occlusionField.set(centerSpace, 5, 6, 5, topGate); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(top)); + assertTrue(Element.earth.in(bottom)); + } + + + @Test + public void stackedFenceGateTopOpen() { + fenceGate(false, 5, 5, 5); + fenceGate(false, 5, 6, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + final FenceGate + topGate = new FenceGate(), + bottomGate = new FenceGate(); + + bottomGate.open = false; + topGate.open = true; + + fenceGate.open = false; + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(fenceGate); + when(centerSpace.blockAt(5, 5, 5)).thenReturn(fenceGate); + when(instanceSpace.blockObjectAt(5, 6, 5)).thenReturn(topGate); + when(instanceSpace.blockObjectAt(5, 5, 5)).thenReturn(bottomGate); + + occlusionField.set(centerSpace, 5, 6, 5, topGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(top)); + assertTrue(Element.earth.in(bottom)); + + topGate.open = false; + occlusionField.set(centerSpace, 5, 6, 5, topGate); + + top = occlusionField.elementAt(5, 6, 5); + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.earth.in(top)); + assertTrue(Element.earth.in(bottom)); + } + + @Test + public void stackedFenceGateOpen() { + fenceGate(false, 5, 5, 5); + fenceGate(false, 5, 6, 5); + occlusionField.loadFrom(centerSpace, 0, 0, 0); + + fenceGate.open = false; + + final FenceGate + topGate = new FenceGate(), + bottomGate = new FenceGate(); + + bottomGate.open = true; + topGate.open = true; + + when(centerSpace.blockAt(5, 6, 5)).thenReturn(fenceGate); + when(centerSpace.blockAt(5, 5, 5)).thenReturn(fenceGate); + when(instanceSpace.blockObjectAt(5, 6, 5)).thenReturn(topGate); + when(instanceSpace.blockObjectAt(5, 5, 5)).thenReturn(bottomGate); + + occlusionField.set(centerSpace, 5, 6, 5, topGate); + occlusionField.set(centerSpace, 5, 5, 5, topGate); + + byte + top = occlusionField.elementAt(5, 6, 5), + bottom = occlusionField.elementAt(5, 5, 5); + + assertTrue(Element.air.in(top)); + assertTrue(Element.air.in(bottom)); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/PathObjectTests.java b/src/test/java/com/extollit/gaming/ai/path/model/PathObjectTests.java new file mode 100644 index 0000000..48763e1 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/PathObjectTests.java @@ -0,0 +1,281 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.collect.CollectionsExt; +import com.extollit.linalg.immutable.Vec3d; +import com.extollit.linalg.immutable.Vec3i; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PathObjectTests { + @Mock private IPathingEntity pathingEntity; + + @Before + public void setup() { + when(pathingEntity.width()).thenReturn(0.6f); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(0.5, 0, 0.5)); + } + + @Test + public void updateMutationState() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1) + ); + when(pathingEntity.age()).thenReturn(42); + assertEquals(0, pathObject.stagnantFor(pathingEntity), 0.01); + + pathObject.update(pathingEntity); + when(pathingEntity.age()).thenReturn(94); + assertEquals(94 - 42, pathObject.stagnantFor(pathingEntity), 0.01); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(2.5, 0.2, 1.5)); + + pathObject.update(pathingEntity); + + assertEquals(0, pathObject.stagnantFor(pathingEntity), 0.01); + } + + @Test + public void update() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 1), + new Vec3i(4, 1, 1), + new Vec3i(4, 1, 2), + new Vec3i(5, 1, 2), + new Vec3i(4, 1, 2), + new Vec3i(3, 1, 3) + ); + pathObject.update(pathingEntity); + + assertEquals(4, pathObject.i); + } + + @Test + public void updateLateStage() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(2, 0, 1), + new Vec3i(3, 0, 1), + new Vec3i(4, 0, 1), + new Vec3i(4, 0, 2), + new Vec3i(5, 0, 2), + new Vec3i(6, 0, 2), + new Vec3i(6, 0, 3) + ); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(4.5, 0, 1.5)); + + pathObject.update(pathingEntity); + + assertEquals(6, pathObject.i); + } + + @Test + public void waterStuck() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(6, 4, 7), + new Vec3i(7, 4, 7), + new Vec3i(8, 4, 7), + new Vec3i(9, 4, 7) + ); + when(pathingEntity.width()).thenReturn(0.3f); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(6.5, 4.1, 7.5)); + + pathObject.update(pathingEntity); + + assertEquals(3, pathObject.i); + } + + @Test + public void truncation() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(2, 5, 3), + new Vec3i(7, 8, 2), + new Vec3i(9, 2, 6), + new Vec3i(5, 7, 9), + new Vec3i(1, 5, 3) + ); + + pathObject.truncateTo(3); + + assertEquals(3, pathObject.length()); + assertEquals( + Arrays.asList( + new Vec3i(2, 5, 3), + new Vec3i(7, 8, 2), + new Vec3i(9, 2, 6) + ), + CollectionsExt.toList(pathObject) + ); + } + + @Test + public void untruncation() { + final PathObject pathObject = new PathObject( + 1, + new Vec3i(2, 5, 3), + new Vec3i(7, 8, 2), + new Vec3i(9, 2, 6), + new Vec3i(5, 7, 9), + new Vec3i(1, 5, 3) + ); + + pathObject.truncateTo(3); + pathObject.untruncate(); + + assertEquals(5, pathObject.length()); + assertEquals( + Arrays.asList(pathObject.points), + CollectionsExt.toList(pathObject) + ); + } + + @Test + public void positionFor1() { + when(pathingEntity.width()).thenReturn(0.8f); + + final Vec3d pos = PathObject.positionFor(pathingEntity, new Vec3i(1, 2, 3)); + + assertEquals(new Vec3d(1.5, 2, 3.5), pos); + } + + + @Test + public void positionFor2() { + when(pathingEntity.width()).thenReturn(1.4f); + + final Vec3d pos = PathObject.positionFor(pathingEntity, new Vec3i(1, 2, 3)); + + assertEquals(new Vec3d(1, 2, 3), pos); + } + + @Test + public void positionFor3() { + when(pathingEntity.width()).thenReturn(2.3f); + + final Vec3d pos = PathObject.positionFor(pathingEntity, new Vec3i(1, 2, 3)); + + assertEquals(new Vec3d(1.5, 2, 3.5), pos); + } + + @Test + public void dontAdvanceBigBoiTooMuch() { + when(pathingEntity.width()).thenReturn(1.4f); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(-0.45f, 0, 0)); + final PathObject pathObject = new PathObject( + 1, + new Vec3i(0, 0, 0), + new Vec3i(-1, 0, 0), + new Vec3i(-1, 0, 1), + new Vec3i(0, 1, 1) + ); + + pathObject.update(pathingEntity); + + assertEquals(2, pathObject.i); + } + + @Test + public void dontDoubleBack() { + PathObject pathObject = new PathObject( + 1, + new Vec3i(1, 0, 0), + new Vec3i(2, 0, 0), + new Vec3i(3, 0, 0), + new Vec3i(4, 0, 0), + new Vec3i(5, 0, 0) + ); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(3.5, 0 ,0.5)); + pathObject.update(pathingEntity); + + assertEquals(4, pathObject.i); + verify(pathingEntity).moveTo(new Vec3d(5.5, 0, 0.5)); + + when(pathingEntity.age()).thenReturn(100); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(3.5, 0 ,1.5)); + + pathObject.update(pathingEntity); + + assertEquals(2, pathObject.i); + verify(pathingEntity).moveTo(new Vec3d(3.5, 0, 0.5)); + } + + + @Test + public void nonRepudiantUpdate() { + PathObject path = new PathObject( + 1, + new Vec3i(-2, 4, 11), + new Vec3i(-3, 4, 11), + new Vec3i(-4, 4, 11), + new Vec3i(-5, 4, 11), + new Vec3i(-5, 4, 10), + new Vec3i(-4, 4, 10), + new Vec3i(-3, 4, 10), + new Vec3i(-2, 4, 10), + new Vec3i(-1, 4, 10), + new Vec3i(0, 4, 10) + ); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(-1.5, 4, 11.5)); + path.update(pathingEntity); + final int first = path.i; + + path.update(pathingEntity); + assertEquals(first, path.i); + } + + @Test + public void approximateAdjacent() { + PathObject path = new PathObject( + 1, + new Vec3i(0, 1, 0), + new Vec3i(1, 1, 0) + ); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(0.4, 0.5, 0.4)); + + path.update(pathingEntity); + assertEquals(1, path.i); + } + + @Test + public void stairMaster() { + PathObject path = new PathObject( + 1, + new Vec3i(13, 4, 6), + new Vec3i(12, 5, 6), + new Vec3i(11, 5, 6), + new Vec3i(11, 4, 7), + new Vec3i(10, 4, 7) + ); + when(pathingEntity.coordinates()).thenReturn(new Vec3d(13.5, 4, 6.5)); + path.update(pathingEntity); + + when(pathingEntity.coordinates()).thenReturn(new Vec3d(11.4, 5, 7.3)); + path.update(pathingEntity); + + assertEquals(4, path.i); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/PathObjectUtil.java b/src/test/java/com/extollit/gaming/ai/path/model/PathObjectUtil.java new file mode 100644 index 0000000..5695232 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/PathObjectUtil.java @@ -0,0 +1,15 @@ +package com.extollit.gaming.ai.path.model; + +import com.extollit.linalg.immutable.Vec3i; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; + +public class PathObjectUtil { + public static void assertPath(PathObject path, Vec3i... coordinates) { + if (coordinates == null || coordinates.length == 0) + assertNull(path); + + assertArrayEquals(coordinates, path.points); + } +} diff --git a/src/test/java/com/extollit/gaming/ai/path/model/TestOcclusionField.java b/src/test/java/com/extollit/gaming/ai/path/model/TestOcclusionField.java new file mode 100644 index 0000000..abc9f20 --- /dev/null +++ b/src/test/java/com/extollit/gaming/ai/path/model/TestOcclusionField.java @@ -0,0 +1,17 @@ +package com.extollit.gaming.ai.path.model; + +public class TestOcclusionField extends OcclusionField { + public final AreaInit area; + + public TestOcclusionField() { + this(null); + } + public TestOcclusionField(AreaInit area) { + this.area = area; + } + + @Override + public String toString() { + return this.area == null ? "center" : this.area.name(); + } +}