Skip to content

Commit

Permalink
Boulder Solver (#540)
Browse files Browse the repository at this point in the history
  • Loading branch information
LifeIsAParadox authored Feb 15, 2024
1 parent 9c10606 commit f11f206
Show file tree
Hide file tree
Showing 8 changed files with 577 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/main/java/de/hysky/skyblocker/SkyblockerMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra;
import de.hysky.skyblocker.skyblock.dungeon.*;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.boulder.Boulder;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.CreeperBeams;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonBlaze;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.TicTacToe;
Expand Down Expand Up @@ -137,6 +138,7 @@ public void onInitializeClient() {
SpecialEffects.init();
ItemProtection.init();
CreeperBeams.init();
Boulder.init();
ItemRarityBackgrounds.init();
MuseumItemCache.init();
SecretsTracker.init();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,9 @@ public static class Dungeons {
@SerialEntry
public boolean solveWaterboard = true;

@SerialEntry
public boolean solveBoulder = true;

@SerialEntry
public boolean fireFreezeStaffTimer = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,14 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig
newValue -> config.locations.dungeons.solveWaterboard = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
.option(Option.<Boolean>createBuilder()
.name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveBoulder"))
.description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveBoulder.@Tooltip")))
.binding(defaults.locations.dungeons.solveBoulder,
() -> config.locations.dungeons.solveBoulder,
newValue -> config.locations.dungeons.solveBoulder = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
.option(Option.<Boolean>createBuilder()
.name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.fireFreezeStaffTimer"))
.description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.fireFreezeStaffTimer.@Tooltip")))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package de.hysky.skyblocker.skyblock.dungeon.puzzle.boulder;

import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonPuzzle;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.dungeon.secrets.Room;
import de.hysky.skyblocker.utils.render.RenderHelper;
import de.hysky.skyblocker.utils.render.title.Title;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.util.DyeColor;
import net.minecraft.util.Formatting;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.BlockView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;

public class Boulder extends DungeonPuzzle {
private static final Logger LOGGER = LoggerFactory.getLogger(Boulder.class.getName());
private static final Boulder INSTANCE = new Boulder();
private static final float[] RED_COLOR_COMPONENTS = DyeColor.RED.getColorComponents();
private static final float[] ORANGE_COLOR_COMPONENTS = DyeColor.ORANGE.getColorComponents();
private static final int BASE_Y = 65;
static Vec3d[] linePoints;
static Box boundingBox;

private Boulder() {
super("boulder", "boxes-room");
}

public static void init() {
}

@Override
public void tick(MinecraftClient client) {

if (!shouldSolve() || !SkyblockerConfigManager.get().locations.dungeons.solveBoulder || client.world == null || !DungeonManager.isCurrentRoomMatched()) {
return;
}

Room room = DungeonManager.getCurrentRoom();

BlockPos chestPos = new BlockPos(15, BASE_Y, 29);
BlockPos start = new BlockPos(25, BASE_Y, 25);
BlockPos end = new BlockPos(5, BASE_Y, 8);
// Create a target BoulderObject for the puzzle
BoulderObject target = new BoulderObject(chestPos.getX(), chestPos.getX(), chestPos.getZ(), "T");
// Create a BoulderBoard representing the puzzle's grid
BoulderBoard board = new BoulderBoard(8, 7, target);

// Populate the BoulderBoard grid with BoulderObjects based on block types in the room
int column = 1;
for (int z = start.getZ(); z > end.getZ(); z--) {
int row = 0;
for (int x = start.getX(); x > end.getX(); x--) {
if (Math.abs(start.getX() - x) % 3 == 1 && Math.abs(start.getZ() - z) % 3 == 1) {
String blockType = getBlockType(client.world, x, BASE_Y, z);
board.placeObject(column, row, new BoulderObject(x, BASE_Y, z, blockType));
row++;
}
}
if (row == board.getWidth()) {
column++;
}
}

// Generate initial game states for the A* solver
char[][] boardArray = board.getBoardCharArray();
List<BoulderSolver.GameState> initialStates = Arrays.asList(
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 0),
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 1),
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 2),
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 3),
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 4),
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 5),
new BoulderSolver.GameState(boardArray, board.getHeight() - 1, 6)
);

// Solve the puzzle using the A* algorithm
List<int[]> solution = BoulderSolver.aStarSolve(initialStates);

if (solution != null) {
linePoints = new Vec3d[solution.size()];
int index = 0;
// Convert solution coordinates to Vec3d points for rendering
for (int[] coord : solution) {
int x = coord[0];
int y = coord[1];
// Convert relative coordinates to actual coordinates
linePoints[index++] = Vec3d.ofCenter(room.relativeToActual(board.getObject3DPosition(x, y)));
}

BlockPos button = null;
if (linePoints != null && linePoints.length > 0) {
// Check for buttons along the path of the solution
for (int i = 0; i < linePoints.length - 1; i++) {
Vec3d point1 = linePoints[i];
Vec3d point2 = linePoints[i + 1];
button = checkForButtonBlocksOnLine(client.world, point1, point2);
if (button != null) {
// If a button is found, calculate its bounding box
boundingBox = getBlockBoundingBox(client.world, button);
break;
}
}
if (button == null){
// If no button is found along the path the puzzle is solved; reset the puzzle
reset();
}
}
} else {
// If no solution is found, display a title message and reset the puzzle
Title title = new Title("skyblocker.dungeons.puzzle.boulder.noSolution", Formatting.GREEN);
RenderHelper.displayInTitleContainerAndPlaySound(title, 15);
reset();
}
}

/**
* Retrieves the type of block at the specified position in the world.
* If the block is Birch or Jungle plank, it will return "B"; otherwise, it will return ".".
*
* @param world The client world.
* @param x The x-coordinate of the block.
* @param y The y-coordinate of the block.
* @param z The z-coordinate of the block.
* @return The type of block at the specified position.
*/
public static String getBlockType(ClientWorld world, int x, int y, int z) {
Block block = world.getBlockState(DungeonManager.getCurrentRoom().relativeToActual(new BlockPos(x, y, z))).getBlock();
return (block == Blocks.BIRCH_PLANKS || block == Blocks.JUNGLE_PLANKS) ? "B" : ".";
}

/**
* Checks for blocks along the line between two points in the world.
* Returns the position of a block if it found a button on the line, if any.
*
* @param world The client world.
* @param point1 The starting point of the line.
* @param point2 The ending point of the line.
* @return The position of the block found on the line, or null if no block is found.
*/
private static BlockPos checkForButtonBlocksOnLine(ClientWorld world, Vec3d point1, Vec3d point2) {
double x1 = point1.getX();
double y1 = point1.getY() + 1;
double z1 = point1.getZ();

double x2 = point2.getX();
double y2 = point2.getY() + 1;
double z2 = point2.getZ();

int steps = (int) Math.max(Math.abs(x2 - x1), Math.max(Math.abs(y2 - y1), Math.abs(z2 - z1)));

double xStep = (x2 - x1) / steps;
double yStep = (y2 - y1) / steps;
double zStep = (z2 - z1) / steps;


for (int step = 0; step <= steps; step++) {
double currentX = x1 + step * xStep;
double currentY = y1 + step * yStep;
double currentZ = z1 + step * zStep;

BlockPos blockPos = BlockPos.ofFloored(currentX, currentY, currentZ);
Block block = world.getBlockState(blockPos).getBlock();

if (block == Blocks.STONE_BUTTON) {
return blockPos;
}

}
return null;
}

/**
* Retrieves the bounding box of a block in the world.
*
* @param world The client world.
* @param pos The position of the block.
* @return The bounding box of the block.
*/
public static Box getBlockBoundingBox(BlockView world, BlockPos pos) {
BlockState blockState = world.getBlockState(pos);
return blockState.getOutlineShape(world, pos).getBoundingBox().offset(pos);
}

@Override
public void render(WorldRenderContext context) {
if (!shouldSolve() || !SkyblockerConfigManager.get().locations.dungeons.solveBoulder || !DungeonManager.isCurrentRoomMatched())
return;
float alpha = 1.0f;
float lineWidth = 5.0f;

if (linePoints != null && linePoints.length > 0) {
for (int i = 0; i < linePoints.length - 1; i++) {
Vec3d startPoint = linePoints[i];
Vec3d endPoint = linePoints[i + 1];
RenderHelper.renderLinesFromPoints(context, new Vec3d[]{startPoint, endPoint}, ORANGE_COLOR_COMPONENTS, alpha, lineWidth, true);
}
if (boundingBox != null) {
RenderHelper.renderOutline(context, boundingBox, RED_COLOR_COMPONENTS, 5, false);
}
}
}

@Override
public void reset() {
super.reset();
linePoints = null;
boundingBox = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package de.hysky.skyblocker.skyblock.dungeon.puzzle.boulder;

import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;

/**
* Represents the game board for the Boulder puzzle, managing the grid of BoulderObjects.
* This class handles operations such as placing objects on the board, retrieving objects,
* and generating a character representation of the game board.
*/
public class BoulderBoard {
private final int height;
private final int width;
private final BoulderObject[][] grid;

/**
* Constructs a BoulderBoard with the specified height, width, and target BoulderObject.
*
* @param height The height of the board.
* @param width The width of the board.
* @param target The target BoulderObject that needs to be reached to solve the puzzle.
*/
public BoulderBoard(int height, int width, BoulderObject target) {
this.height = height;
this.width = width;
this.grid = new BoulderObject[height][width];

int offsetX = target.x() - 23;
int y = 65;

for (int z = 0; z < width; z++) {
if (z == width / 2) {
grid[0][z] = target;
} else {
grid[0][z] = new BoulderObject(offsetX, y, z, "B");
}
grid[height - 1][z] = new BoulderObject(24 - (3 * z), y, 6, "P");
}
}

/**
* Retrieves the BoulderObject at the specified position on the board.
*
* @param x The x-coordinate of the position.
* @param y The y-coordinate of the position.
* @return The BoulderObject at the specified position, or null if no object is present.
*/
public BoulderObject getObjectAtPosition(int x, int y) {
return isValidPosition(x, y) ? grid[x][y] : null;
}

/**
* Retrieves the 3D position of the BoulderObject at the specified position on the board.
*
* @param x The x-coordinate of the position.
* @param y The y-coordinate of the position.
* @return The BlockPos representing the 3D position of the BoulderObject,
* or null if no object is present at the specified position.
*/
public BlockPos getObject3DPosition(int x, int y) {
BoulderObject object = getObjectAtPosition(x, y);
return (object != null) ? object.get3DPosition().offset(Direction.Axis.Y, -1) : null;
}

/**
* Places a BoulderObject at the specified position on the board.
*
* @param x The x-coordinate of the position.
* @param y The y-coordinate of the position.
* @param object The BoulderObject to place on the board.
*/
public void placeObject(int x, int y, BoulderObject object) {
grid[x][y] = object;
}

public int getHeight() {
return height;
}

public int getWidth() {
return width;
}

/**
* Checks whether the specified position is valid within the bounds of the game board.
*
* @param x The x-coordinate of the position to check.
* @param y The y-coordinate of the position to check.
* @return {@code true} if the position is valid within the bounds of the board, {@code false} otherwise.
*/
private boolean isValidPosition(int x, int y) {
return x >= 0 && y >= 0 && x < height && y < width;
}

/**
* Generates a character array representation of the game board.
* Each character represents a type of BoulderObject or an empty space.
*
* @return A 2D character array representing the game board.
*/
public char[][] getBoardCharArray() {
char[][] boardCharArray = new char[height][width];
for (int x = 0; x < height; x++) {
for (int y = 0; y < width; y++) {
BoulderObject boulderObject = grid[x][y];
boardCharArray[x][y] = (boulderObject != null) ? boulderObject.type().charAt(0) : '.';
}
}
return boardCharArray;
}

/**
* Prints the current state of the game board to the console.
* Each character represents a type of BoulderObject or an empty space.
*/
public String boardToString() {
StringBuilder sb = new StringBuilder();
for (int x = 0; x < height; x++) {
for (int y = 0; y < width; y++) {
BoulderObject boulderObject = grid[x][y];
String displayChar = (boulderObject != null) ? boulderObject.type() : ".";
sb.append(displayChar);
}
sb.append("\n");
}
return sb.toString();
}

}
Loading

0 comments on commit f11f206

Please sign in to comment.