From 6fea22dc9f8812c5ef9b6b91f72814db7b2cbbfc Mon Sep 17 00:00:00 2001 From: Leijurv Date: Thu, 16 Mar 2023 00:42:23 -0700 Subject: [PATCH] this is incredibly cool and it does figure out the staircase pattern like i hoped --- .../com/github/leijurv/BetterBlockPos.java | 24 +- .../com/github/leijurv/NavigableSurface.java | 147 ++++++++++ src/main/java/com/github/leijurv/Testing.java | 7 - .../github/leijurv/NavigableSurfaceTest.java | 276 ++++++++++++++++++ 4 files changed, 435 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/github/leijurv/NavigableSurface.java delete mode 100644 src/main/java/com/github/leijurv/Testing.java create mode 100644 src/test/java/com/github/leijurv/NavigableSurfaceTest.java diff --git a/src/main/java/com/github/leijurv/BetterBlockPos.java b/src/main/java/com/github/leijurv/BetterBlockPos.java index d1ea89313..8b4bdedd7 100644 --- a/src/main/java/com/github/leijurv/BetterBlockPos.java +++ b/src/main/java/com/github/leijurv/BetterBlockPos.java @@ -157,7 +157,7 @@ public final class BetterBlockPos { return false; } - public BetterBlockPos up() { + public BetterBlockPos upPlusY() { // this is unimaginably faster than blockpos.up // that literally calls // this.up(1) @@ -170,50 +170,50 @@ public final class BetterBlockPos { return new BetterBlockPos(x, y + 1, z); } - public BetterBlockPos up(int amt) { + public BetterBlockPos upPlusY(int amt) { // see comment in up() return amt == 0 ? this : new BetterBlockPos(x, y + amt, z); } - public BetterBlockPos down() { + public BetterBlockPos downMinusY() { // see comment in up() return new BetterBlockPos(x, y - 1, z); } - public BetterBlockPos down(int amt) { + public BetterBlockPos downMinusY(int amt) { // see comment in up() return amt == 0 ? this : new BetterBlockPos(x, y - amt, z); } - public BetterBlockPos north() { + public BetterBlockPos northMinusZ() { return new BetterBlockPos(x, y, z - 1); } - public BetterBlockPos north(int amt) { + public BetterBlockPos northMinusZ(int amt) { return amt == 0 ? this : new BetterBlockPos(x, y, z - amt); } - public BetterBlockPos south() { + public BetterBlockPos southPlusZ() { return new BetterBlockPos(x, y, z + 1); } - public BetterBlockPos south(int amt) { + public BetterBlockPos southPlusZ(int amt) { return amt == 0 ? this : new BetterBlockPos(x, y, z + amt); } - public BetterBlockPos east() { + public BetterBlockPos eastPlusX() { return new BetterBlockPos(x + 1, y, z); } - public BetterBlockPos east(int amt) { + public BetterBlockPos eastPlusX(int amt) { return amt == 0 ? this : new BetterBlockPos(x + amt, y, z); } - public BetterBlockPos west() { + public BetterBlockPos westMinusX() { return new BetterBlockPos(x - 1, y, z); } - public BetterBlockPos west(int amt) { + public BetterBlockPos westMinusX(int amt) { return amt == 0 ? this : new BetterBlockPos(x - amt, y, z); } diff --git a/src/main/java/com/github/leijurv/NavigableSurface.java b/src/main/java/com/github/leijurv/NavigableSurface.java new file mode 100644 index 000000000..784a4f7c8 --- /dev/null +++ b/src/main/java/com/github/leijurv/NavigableSurface.java @@ -0,0 +1,147 @@ +package com.github.leijurv; + +import com.github.btrekkie.connectivity.ConnGraph; + +import java.util.OptionalInt; + +public class NavigableSurface { + // the encapsulation / separation of concerns is not great, but this is better for testing purposes than the fully accurate stuff in https://github.com/cabaletta/baritone/tree/builder-2/src/main/java/baritone/builder lol + public final int sizeX; + public final int sizeY; + public final int sizeZ; + + private final boolean[][][] blocks; + + private final ConnGraph connGraph; + + public NavigableSurface(int x, int y, int z) { + this.sizeX = x; + this.sizeY = y; + this.sizeZ = z; + this.blocks = new boolean[x][y][z]; + this.connGraph = new ConnGraph((a, b) -> (Integer) a + (Integer) b); + } + + public OptionalInt surfaceSize(BetterBlockPos pos) { // how big is the navigable surface from here? how many distinct coordinates can i walk to (in the future, the augmentation will probably have a list of those coordinates or something?) + Object data = connGraph.getComponentAugmentation(pos.toLong()); + if (data != null) { // i disagree with the intellij suggestion here i think it makes it worse + return OptionalInt.of((Integer) data); + } else { + return OptionalInt.empty(); + } + } + + // so the idea is that as blocks are added and removed, we'll maintain where the player can stand, and what connections that has to other places + public void placeOrRemoveBlock(BetterBlockPos where, boolean place) { + // i think it makes sense to only have a single function, as both placing and breaking blocks can create/remove places the player could stand, as well as creating/removing connections between those places + boolean previously = getBlock(where); + if (previously == place) { + return; // this is already the case + } + blocks[where.x][where.y][where.z] = place; + // first let's set some vertex info for where the player can and cannot stand + for (int dy = -1; dy <= 1; dy++) { + BetterBlockPos couldHaveChanged = where.upPlusY(dy); + boolean currentlyAllowed = canPlayerStandIn(couldHaveChanged); + if (currentlyAllowed) { + // i'm sure this will get more complicated later + connGraph.setVertexAugmentation(couldHaveChanged.toLong(), 1); + } else { + connGraph.removeVertexAugmentation(couldHaveChanged.toLong()); + } + } + // then let's set the edges + for (int dy = -2; dy <= 1; dy++) { // -2 because of the jump condition for ascending + // i guess some of these can be skipped based on whether "place" is false or true, but, whatever this is just for testing + BetterBlockPos couldHaveChanged = where.upPlusY(dy); + computePossibleMoves(couldHaveChanged); + } + } + + public boolean canPlayerStandIn(BetterBlockPos where) { + return getBlockOrAir(where.downMinusY()) && !getBlockOrAir(where) && !getBlockOrAir(where.upPlusY()); + } + + public void computePossibleMoves(BetterBlockPos feet) { + boolean anySuccess = canPlayerStandIn(feet); + // even if all are fail, need to remove those edges from the graph, so don't return early + for (int[] move : MOVES) { + BetterBlockPos newFeet = feet.eastPlusX(move[0]).upPlusY(move[1]).southPlusZ(move[2]); + boolean thisSuccess = anySuccess; + thisSuccess &= canPlayerStandIn(newFeet); + if (move[1] == -1) { + // descend movement requires the player head to move through one extra block (newFeet must be 3 high not 2 high) + thisSuccess &= !getBlockOrAir(newFeet.upPlusY(2)); + } + if (move[1] == 1) { + // same idea but ascending instead of descending + thisSuccess &= !getBlockOrAir(feet.upPlusY(2)); + } + if (thisSuccess) { + if (connGraph.addEdge(feet.toLong(), newFeet.toLong())) { + //System.out.println("Player can now move between " + feet + " and " + newFeet); + } + } else { + if (connGraph.removeEdge(feet.toLong(), newFeet.toLong())) { + //System.out.println("Player can no longer move between " + feet + " and " + newFeet); + } + } + } + } + + public int requireSurfaceSize(int x, int y, int z) { + return surfaceSize(new BetterBlockPos(x, y, z)).getAsInt(); + } + + public boolean inRange(int x, int y, int z) { + return (x | y | z | (sizeX - (x + 1)) | (sizeY - (y + 1)) | (sizeZ - (z + 1))) >= 0; // ">= 0" is used here in the sense of "most significant bit is not set" + } + + public boolean getBlock(BetterBlockPos where) { + return blocks[where.x][where.y][where.z]; + } + + public boolean getBlockOrAir(BetterBlockPos where) { + if (!inRange(where.x, where.y, where.z)) { + return false; + } + return getBlock(where); + } + + public boolean connected(BetterBlockPos a, BetterBlockPos b) { + return connGraph.connected(a.toLong(), b.toLong()); + } + + public void placeBlock(BetterBlockPos where) { + placeOrRemoveBlock(where, true); + } + + public void placeBlock(int x, int y, int z) { + placeBlock(new BetterBlockPos(x, y, z)); + } + + public void removeBlock(BetterBlockPos where) { + placeOrRemoveBlock(where, false); + } + + public void removeBlock(int x, int y, int z) { + removeBlock(new BetterBlockPos(x, y, z)); + } + + private static final int[][] MOVES = { + {1, -1, 0}, + {-1, -1, 0}, + {0, -1, 1}, + {0, -1, -1}, + + {1, 0, 0}, + {-1, 0, 0}, + {0, 0, 1}, + {0, 0, -1}, + + {1, 1, 0}, + {-1, 1, 0}, + {0, 1, 1}, + {0, 1, -1}, + }; +} diff --git a/src/main/java/com/github/leijurv/Testing.java b/src/main/java/com/github/leijurv/Testing.java deleted file mode 100644 index 37a2372ba..000000000 --- a/src/main/java/com/github/leijurv/Testing.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.leijurv; - -public class Testing { - public static void main(String[] args) { - EulerTourForest.sanityCheck(); - } -} diff --git a/src/test/java/com/github/leijurv/NavigableSurfaceTest.java b/src/test/java/com/github/leijurv/NavigableSurfaceTest.java new file mode 100644 index 000000000..d3d5e4980 --- /dev/null +++ b/src/test/java/com/github/leijurv/NavigableSurfaceTest.java @@ -0,0 +1,276 @@ +package com.github.leijurv; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + +import static org.junit.Assert.assertEquals; + + +public class NavigableSurfaceTest { + @Test + public void testBasic() { + NavigableSurface surface = new NavigableSurface(10, 10, 10); + surface.placeBlock(0, 0, 0); + assertEquals(OptionalInt.empty(), surface.surfaceSize(new BetterBlockPos(0, 0, 0))); + assertEquals(1, surface.requireSurfaceSize(0, 1, 0)); + surface.placeBlock(1, 0, 0); + assertEquals(2, surface.requireSurfaceSize(0, 1, 0)); + surface.placeBlock(1, 0, 0); + surface.placeBlock(2, 0, 0); + surface.placeBlock(3, 0, 0); + surface.placeBlock(4, 0, 0); + surface.placeBlock(5, 0, 0); + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(2, 1, 0)); + + surface.placeBlock(2, 1, 0); + assertEquals(OptionalInt.empty(), surface.surfaceSize(new BetterBlockPos(2, 1, 0))); + assertEquals(6, surface.requireSurfaceSize(2, 2, 0)); + + surface.placeBlock(2, 2, 0); + // X + // X + // XXXXXX + assertEquals(2, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(1, surface.requireSurfaceSize(2, 3, 0)); + assertEquals(3, surface.requireSurfaceSize(3, 1, 0)); + + surface.placeBlock(1, 1, 0); + // X + // XX + // XXXXXX + assertEquals(3, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(3, surface.requireSurfaceSize(3, 1, 0)); + + surface.placeBlock(3, 2, 0); + // XX + // XX + // XXXXXX + assertEquals(4, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(2, surface.requireSurfaceSize(4, 1, 0)); + + surface.placeBlock(4, 1, 0); + // XX + // XX X + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(OptionalInt.empty(), surface.surfaceSize(new BetterBlockPos(3, 1, 0))); + + surface.removeBlock(2, 2, 0); + // X + // XX X + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(2, 2, 0)); + + surface.removeBlock(2, 1, 0); + // X + // X X + // XXXXXX + assertEquals(3, surface.requireSurfaceSize(1, 2, 0)); + assertEquals(3, surface.requireSurfaceSize(3, 3, 0)); + + surface.removeBlock(3, 2, 0); + // X X + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(0, 1, 0)); + + surface.removeBlock(1, 0, 0); + // X X + // X XXXX + assertEquals(6, surface.requireSurfaceSize(0, 1, 0)); + + surface.removeBlock(1, 1, 0); + // X + // X XXXX + assertEquals(1, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(4, surface.requireSurfaceSize(2, 1, 0)); + } + + private NavigableSurface makeFlatSurface(int SZ) { + NavigableSurface surface = new NavigableSurface(SZ, SZ, SZ); + for (int x = 0; x < SZ; x++) { + for (int z = 0; z < SZ; z++) { + surface.placeBlock(new BetterBlockPos(x, 0, z)); + } + } + assertEquals(SZ * SZ, surface.requireSurfaceSize(0, 1, 0)); + return surface; + } + + @Test + public void testSurfaceSmall() { + makeFlatSurface(10); + } + + @Test + public void testSurfaceMed() { + makeFlatSurface(100); + } + + /*@Test + public void testSurfaceBig() { // 10x more on each side, so 100x more nodes total. youd expect this to be about 100x slower than testSurfaceMed, but it's actually 200x slower. this is ideally just because each graph operation is O(log^2 n) so n operations is O(n log^2 n), and hopefully not because of something that otherwise scales superlinearly + makeFlatSurface(1000); + }*/ + // okay but its slow so we dont care + + @Test + public void testStep() { + int SZ = 100; + int lineAt = SZ / 2; + NavigableSurface surface = makeFlatSurface(SZ); + for (int x = 0; x < SZ; x++) { + surface.placeBlock(x, 1, lineAt); // doesn't block the player since you can step over 1 block + } + assertEquals(SZ * SZ, surface.requireSurfaceSize(0, 1, 0)); + } + + @Test + public void testBlocked() { + int SZ = 100; + int lineAt = SZ / 2; + NavigableSurface surface = makeFlatSurface(SZ); + for (int x = 0; x < SZ; x++) { + surface.placeBlock(x, 2, lineAt); // does block the player since you can't step over 2 blocks + } + assertEquals(SZ * lineAt, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(SZ * (SZ - lineAt - 1), surface.requireSurfaceSize(0, 1, SZ - 1)); + assertEquals(SZ, surface.requireSurfaceSize(0, 3, lineAt)); + } + + private void fillSurfaceInOrderMaintainingConnection(NavigableSurface surface, BetterBlockPos maintainConnectionTo, List iterationOrder) { + outer: + while (true) { + for (BetterBlockPos candidate : iterationOrder) { + if (surface.getBlock(candidate)) { + continue; // already placed + } + // let's try placing + surface.placeBlock(candidate); + if (surface.connected(candidate.upPlusY(), maintainConnectionTo)) { + // success, placed a block while retaining the path down to the ground + continue outer; + } + // fail :( + surface.removeBlock(candidate); + } + return; + } + } + + @Test + public void testCastleWall() { + // build a single wall, but, never place a block that disconnects the surface + // (we expect to see a triangle) + int SZ = 20; + NavigableSurface surface = makeFlatSurface(SZ); + BetterBlockPos someOtherBlock = new BetterBlockPos(0, 1, 1); // won't be involved in the wall (since z=1) + List order = new ArrayList<>(); + for (int y = 0; y < SZ; y++) { + for (int x = 0; x < SZ; x++) { + order.add(new BetterBlockPos(x, y, 0)); + } + } + + fillSurfaceInOrderMaintainingConnection(surface, someOtherBlock, order); + + String shouldBe = "" + + "XX | | | \n" + + "XXX | | | \n" + + "XXXX | | | \n" + + "XXXXX | | | \n" + + "XXXXXX | | | \n" + + "XXXXXXX | | | \n" + + "XXXXXXXX | | | \n" + + "XXXXXXXXX | | | \n" + + "XXXXXXXXXX | | | \n" + + "XXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXXXXX| | | \n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n"; // double row is because we started with a flat surface, so there is another flat row behind this one to step back into + assertEquals(shouldBe, reportAllFourWalls(surface)); + } + + @Test + public void testCastleFourWalls() { + // build four walls, but, never place a block that disconnects the surface + // (we expect to see a zigzag cut out) + int SZ = 20; + NavigableSurface surface = makeFlatSurface(SZ); + BetterBlockPos someOtherBlock = new BetterBlockPos(SZ / 2, 1, SZ / 2); // center of the courtyard + List order = new ArrayList<>(); + for (int y = 0; y < SZ; y++) { + for (int x = 0; x < SZ; x++) { + for (int z = 0; z < SZ; z++) { + boolean xOnEdge = x == 0 || x == SZ - 1; + boolean zOnEdge = z == 0 || z == SZ - 1; + if (!xOnEdge && !zOnEdge) { + continue; // in the courtyard + } + order.add(new BetterBlockPos(x, y, z)); + } + } + } + + fillSurfaceInOrderMaintainingConnection(surface, someOtherBlock, order); + + String shouldBe = "" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXX XXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXX XXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXX XXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXX XXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXX XXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXX XXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXX XXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXX XXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXX XXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXX XXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXX XXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XX XXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|X XXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX| XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX | XXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXX | XXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXX |XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXX X|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n"; + assertEquals(shouldBe, reportAllFourWalls(surface)); + } + + private String reportAllFourWalls(NavigableSurface surface) { + StringBuilder report = new StringBuilder(); + for (int y = surface.sizeY - 1; y >= 0; y--) { + // make a report of what all four walls look like + for (int x = 0; x < surface.sizeX; x++) { + report.append(surface.getBlock(new BetterBlockPos(x, y, 0)) ? 'X' : ' '); + } + report.append('|'); + // start at 1 not 0 so that we don't repeat the last iteration of the previous loop (that would make the report look bad because the staircase would repeat one column for no reason) + for (int z = 1; z < surface.sizeZ; z++) { + report.append(surface.getBlock(new BetterBlockPos(surface.sizeX - 1, y, z)) ? 'X' : ' '); + } + report.append('|'); + // same deal for starting at -2 rather than -1 + for (int x = surface.sizeX - 2; x >= 0; x--) { + report.append(surface.getBlock(new BetterBlockPos(x, y, surface.sizeZ - 1)) ? 'X' : ' '); + } + report.append('|'); + // and same again + for (int z = surface.sizeZ - 2; z > 0; z--) { + report.append(surface.getBlock(new BetterBlockPos(0, y, z)) ? 'X' : ' '); + } + report.append('\n'); + } + return report.toString(); + } +}