baritone/src/test/java/baritone/pathing/calc/openset/OpenSetsTest.java

171 lines
6.4 KiB
Java

/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.pathing.calc.openset;
import baritone.api.pathing.goals.Goal;
import baritone.pathing.calc.PathNode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.*;
import static org.junit.Assert.*;
@RunWith(Parameterized.class)
public class OpenSetsTest {
private final int size;
public OpenSetsTest(int size) {
this.size = size;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
ArrayList<Object[]> testSizes = new ArrayList<>();
for (int size = 1; size < 20; size++) {
testSizes.add(new Object[]{size});
}
for (int size = 100; size <= 1000; size += 100) {
testSizes.add(new Object[]{size});
}
testSizes.add(new Object[]{5000});
testSizes.add(new Object[]{10000});
return testSizes;
}
private static void removeAndTest(int amount, IOpenSet[] test, Optional<Collection<PathNode>> mustContain) {
double[][] results = new double[test.length][amount];
for (int i = 0; i < test.length; i++) {
long before = System.nanoTime() / 1000000L;
for (int j = 0; j < amount; j++) {
PathNode pn = test[i].removeLowest();
if (mustContain.isPresent() && !mustContain.get().contains(pn)) {
throw new IllegalStateException(mustContain.get() + " " + pn);
}
results[i][j] = pn.combinedCost;
}
System.out.println(test[i].getClass() + " " + (System.nanoTime() / 1000000L - before));
}
for (int j = 0; j < amount; j++) {
for (int i = 1; i < test.length; i++) {
assertEquals(results[i][j], results[0][j], 0);
}
}
for (int i = 0; i < amount - 1; i++) {
assertTrue(results[0][i] < results[0][i + 1]);
}
}
@Test
public void testSize() {
System.out.println("Testing size " + size);
// Include LinkedListOpenSet even though it's not performant because I absolutely trust that it behaves properly
// I'm really testing the heap implementations against it as the ground truth
IOpenSet[] test = new IOpenSet[]{new BinaryHeapOpenSet(), new LinkedListOpenSet()};
for (IOpenSet set : test) {
assertTrue(set.isEmpty());
}
// generate the pathnodes that we'll be testing the sets on
PathNode[] toInsert = new PathNode[size];
for (int i = 0; i < size; i++) {
// can't use an existing goal
// because they use Baritone.settings()
// and we can't do that because Minecraft itself isn't initted
PathNode pn = new PathNode(0, 0, 0, new Goal() {
@Override
public boolean isInGoal(int x, int y, int z) {
return false;
}
@Override
public double heuristic(int x, int y, int z) {
return 0;
}
});
pn.combinedCost = Math.random();
toInsert[i] = pn;
}
// create a list of what the first removals should be
ArrayList<PathNode> copy = new ArrayList<>(Arrays.asList(toInsert));
copy.sort(Comparator.comparingDouble(pn -> pn.combinedCost));
Set<PathNode> lowestQuarter = new HashSet<>(copy.subList(0, size / 4));
// all opensets should be empty; nothing has been inserted yet
for (IOpenSet set : test) {
assertTrue(set.isEmpty());
}
System.out.println("Insertion");
for (IOpenSet set : test) {
long before = System.nanoTime() / 1000000L;
for (int i = 0; i < size; i++)
set.insert(toInsert[i]);
System.out.println(set.getClass() + " " + (System.nanoTime() / 1000000L - before));
//all three take either 0 or 1ms to insert up to 10,000 nodes
//linkedlist takes 0ms most often (because there's no array resizing or allocation there, just pointer shuffling)
}
// all opensets should now be full
for (IOpenSet set : test) {
assertFalse(set.isEmpty());
}
System.out.println("Removal round 1");
// remove a quarter of the nodes and verify that they are indeed the size/4 lowest ones
removeAndTest(size / 4, test, Optional.of(lowestQuarter));
// none of them should be empty (sanity check)
for (IOpenSet set : test) {
assertFalse(set.isEmpty());
}
int cnt = 0;
for (int i = 0; cnt < size / 2 && i < size; i++) {
if (lowestQuarter.contains(toInsert[i])) { // these were already removed and can't be updated to test
continue;
}
toInsert[i].combinedCost *= Math.random();
// multiplying it by a random number between 0 and 1 is guaranteed to decrease it
for (IOpenSet set : test) {
// it's difficult to benchmark these individually because if you modify all at once then update then
// it breaks the internal consistency of the heaps.
// you have to call update every time you modify a node.
set.update(toInsert[i]);
}
cnt++;
}
//still shouldn't be empty
for (IOpenSet set : test) {
assertFalse(set.isEmpty());
}
System.out.println("Removal round 2");
// remove the remaining 3/4
removeAndTest(size - size / 4, test, Optional.empty());
// every set should now be empty
for (IOpenSet set : test) {
assertTrue(set.isEmpty());
}
}
}