Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Boulder Solver #540

Merged
merged 2 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -7,6 +7,7 @@
import de.hysky.skyblocker.skyblock.*;
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 @@ -127,6 +128,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 @@ -674,6 +674,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,217 @@
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) {
RenderHelper.renderLinesFromPoints(context, linePoints, ORANGE_COLOR_COMPONENTS, alpha, lineWidth, true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lines need to be rendered one at a time for the line width and normals to not get messed up. (See the last part of Waterboard#render.)

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,127 @@
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 void printBoard() {
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() : ".";
System.out.print(displayChar);
}
System.out.println();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't really matter but maybe something like Waterboard#boardToString would be better since System.out.print is not hardcoded.


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.hysky.skyblocker.skyblock.dungeon.puzzle.boulder;

import net.minecraft.util.math.BlockPos;

public record BoulderObject(int x, int y, int z, String type) {
public BlockPos get3DPosition() {
return new BlockPos(x, y, z);
}
}
Loading
Loading