State.java

package dev.plagarizers.klotski.game.state;


import com.google.gson.Gson;
import dev.plagarizers.klotski.game.block.*;
import dev.plagarizers.klotski.game.util.Coordinate;
import dev.plagarizers.klotski.game.util.Direction;
import dev.plagarizers.klotski.game.util.Level;
import dev.plagarizers.klotski.game.util.SavesManager;

import java.util.*;
import java.util.stream.Collectors;

public class State implements Cloneable {

    public final static int ROWS = 5, COLS = 4, NUM_PIECES = 10;
    public final static Coordinate GOAL = Coordinate.of(3, 1);
    private static final SavesManager savesManager = new SavesManager();
    private final HashMap<Coordinate, Block> blocks;
    private int moves = 0;


    /**
     * Constructs a new State object with an empty block collection.
     */
    private State() {
        this.blocks = new HashMap<>();
    }

    /**
     * Creates a new State object with the default configuration of blocks.
     *
     * @return a new State object with the default block configuration
     */
    public static State fromDefaultConfiguration() {
        Block[] base = new Block[NUM_PIECES];
        base[0] = new BigBlock(new Coordinate(0, 1));
        base[1] = new HorizontalBlock(new Coordinate(2, 1));
        base[2] = new VerticalBlock(new Coordinate(0, 3));
        base[3] = new VerticalBlock(new Coordinate(0, 0));
        base[4] = new VerticalBlock(new Coordinate(3, 0));
        base[5] = new VerticalBlock(new Coordinate(3, 3));
        base[6] = new SmallBlock(new Coordinate(3, 1));
        base[7] = new SmallBlock(new Coordinate(3, 2));
        base[8] = new SmallBlock(new Coordinate(4, 1));
        base[9] = new SmallBlock(new Coordinate(4, 2));

        State state = new State();

        state.setBlocks(base);
        return state;
    }

    /**
     * Creates a new State object with a random configuration loaded from the levels file.
     *
     * @return a new State object with a random configuration
     */
    public static State fromRandomConfiguration() {
        return fromRandomLevel().toState();
    }

    public static Level fromRandomLevel() {
        List<Level> levels = savesManager.loadLevelsFromDefaultPath();

        Random random = new Random();
        int index = random.nextInt(levels.size());
        return levels.get(index);
    }

    /**
     * Checks if the given coordinate is a valid coordinate within the board boundaries.
     *
     * @param coordinate The coordinate to check.
     * @return {@code true} if the coordinate is valid, {@code false} otherwise.
     */
    public static boolean isValidCoordinate(Coordinate coordinate) {
        return coordinate.getX() >= 0 && coordinate.getX() < ROWS && coordinate.getY() >= 0 && coordinate.getY() < COLS;
    }

    /**
     * Creates a new State object from a JSON string representation.
     *
     * @param json the JSON string representation of the state
     * @return a new State object created from the JSON string representation
     */
    @SuppressWarnings("unchecked")
    public static State fromJson(String json) {
        Gson gson = new Gson();

        HashMap<String, Object> save = gson.fromJson(json, HashMap.class);
        int moves = ((Double) save.get("moves")).intValue();
        List<Block> blockList = ((ArrayList<Block>) save.get("blocks"));
        State state = new State();

        Block[] blocks = gson.fromJson(gson.toJson(blockList), Block[].class);
        state.setBlocks(blocks);
        state.setMoves(moves);
        return state;
    }

    /**
     * Converts an array of bit board representations to a State object.
     *
     * @param bitBoard The array of bit board representations.
     * @return The corresponding State object.
     */
    public static State fromBitBoard(int[] bitBoard) {
        State state = new State();

        Block[] blocks = new Block[NUM_PIECES];
        for (int i = 0; i < NUM_PIECES; i++) {
            blocks[i] = createBlockFromBitMask(bitBoard[i]);
        }
        state.setBlocks(blocks);
        return state;
    }

    /**
     * Converts a block object to a bit mask representation.
     *
     * @param block The block object to convert.
     * @return The bit mask representation of the block.
     */
    public static int createBitMaskForBlock(Block block) {

        int mask = 0b0000_0000_0000_0000_0000;
        int x = block.getLocation().getX();
        int y = block.getLocation().getY();
        for (int row = x; row < x + block.getHeight(); row++) {
            for (int col = y; col < y + block.getWidth(); col++) {
                int index = 16 - (row * State.COLS) + col;
                // Set the corresponding bit to 1
                mask |= (1 << index);
            }
        }
        return mask;
    }

    /**
     * Converts a bit mask representation to a block object.
     *
     * @param mask The bit mask representation of the block.
     * @return The corresponding block object.
     */

    public static Block createBlockFromBitMask(int mask) {


        if (mask <= 0) {
            return null;
        }
        int y = -1, x = -1, width = -1, height = -1;

        for (int row = 0; row < State.ROWS; row++) {
            for (int col = 0; col < State.COLS; col++) {
                int bit = (mask >> (State.ROWS - row - 1) * State.COLS + col) & 1;

                if (bit == 1) {
                    if (y == -1) {
                        // Found the first cell of the block
                        y = col;
                        x = row;
                        width = 1;
                        height = 1;
                    } else {
                        // Expand the dimensions of the block if necessary
                        width = Math.max(width, col - y + 1);
                        height = Math.max(height, row - x + 1);
                    }
                }
            }
        }

        return new Block(Coordinate.of(x, y), height, width);
    }

    /**
     * Returns the number of moves made in the current state.
     *
     * @return the number of moves made
     */
    public int getMoves() {
        return moves;
    }

    /**
     * Sets the number of moves made in the current state.
     *
     * @param moves the number of moves to set
     */
    public void setMoves(int moves) {
        this.moves = moves;
    }

    /**
     * Checks if the current state is solved, i.e., if the big block is in the goal position.
     *
     * @return true if the state is solved, false otherwise
     */
    public boolean isSolved() {
        if (!blocks.containsKey(GOAL)) return false;
        return blocks.get(GOAL).equals(new BigBlock(GOAL));
    }

    /**
     * Checks if a block can be moved in the given direction in the current state.
     *
     * @param block     the block to move
     * @param direction the direction to move the block
     * @return true if the block can be moved, false otherwise
     */
    public boolean canMoveBlock(Block block, Direction direction) {

        Coordinate newTopLeft = block.getLocation().move(direction);

        if (!isValidBlock(newTopLeft, block.getWidth(), block.getHeight())) {
            return false;
        }
        for (Coordinate coordinate : block.getOccupiedLocations(newTopLeft)) {
            if (!blocks.containsKey(coordinate)) continue;
            if (!blocks.get(coordinate).equals(block)) return false;
        }
        return true;
    }

    /**
     * Moves a block in the given direction in the current state.
     *
     * @param block     the block to move
     * @param direction the direction to move the block
     * @return true if the block was moved, false otherwise
     */
    public boolean moveBlock(Block block, Direction direction) {


        if (!canMoveBlock(block, direction)) return false;

        for (Coordinate coordinate : block.getOccupiedLocations()) {
            blocks.remove(coordinate);
        }

        Coordinate newTopLeft = block.getLocation().move(direction);
        block.setLocation(newTopLeft);

        for (Coordinate coordinate : block.getOccupiedLocations()) {
            blocks.put(coordinate, block);
        }

        moves++;

        return true;
    }

    /**
     * Checks if the given topLeft is a valid coordinate within the board boundaries for a block with the given width and height.
     *
     * @param topLeft the topLeft to check
     * @param width   the width of the block
     * @param height  the height of the block
     * @return true if the topLeft is valid for the block, false otherwise
     */
    private boolean isValidBlock(Coordinate topLeft, int width, int height) {
        int x = topLeft.getX();
        int y = topLeft.getY();

        // Check if the topLeft is within the boundaries of the puzzle
        if (x >= 0 && y >= 0 && x <= ROWS && y <= COLS) {
            // Check if the topLeft is within the boundaries of the block
            return y + width <= COLS && x + height <= ROWS;
        }
        return false;
    }

    /**
     * Gets an array of blocks representing the current state.
     *
     * @return an array of blocks representing the current state
     */
    @SuppressWarnings("NewApi")
    public Block[] getBlocks() {
        HashSet<Block> set = new HashSet<>(blocks.values());
        List<Block> list = set.stream().sorted(Comparator.comparing(Block::getWidth).thenComparing(Block::getHeight)).collect(Collectors.toList());
        Collections.reverse(list);
        return list.toArray(new Block[0]);
    }

    /**
     * Sets the blocks in the state based on the given array of blocks.
     *
     * @param base the array of blocks to set
     * @throws IllegalArgumentException if the number of blocks is invalid
     */
    public void setBlocks(Block[] base) {
        blocks.clear();
        if (base.length != NUM_PIECES) throw new IllegalArgumentException("Invalid number of blocks");
        for (Block block : base) {
            if (block == null) throw new IllegalArgumentException("Invalid block");
            for (Coordinate coordinate : block.getOccupiedLocations()) {
                this.blocks.put(coordinate, block);
            }
        }
    }

    /**
     * Converts the state to a JSON string representation.
     *
     * @return the JSON string representation of the state
     */
    public String toJson() {
        Gson gson = new Gson();

        HashMap<String, Object> save = new HashMap<>();

        save.put("moves", this.moves);
        save.put("blocks", this.getBlocks());
        return gson.toJson(save);
    }

    @Override
    @SuppressWarnings("CloneDoesntCallSuperClone")
    public State clone() {
        State clone = new State();

        clone.moves = this.moves;

        for (Map.Entry<Coordinate, Block> entry : this.blocks.entrySet()) {
            Coordinate coordinate = entry.getKey();
            Block block = entry.getValue();

            clone.blocks.put(coordinate.clone(), block.clone());
        }
        return clone;
    }

    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < ROWS; i++) {
            sb.append("|");
            for (int j = 0; j < COLS; j++) {
                Coordinate coordinate = Coordinate.of(i, j);
                if (blocks.containsKey(coordinate)) {
                    sb.append(blocks.get(coordinate).getIcon());
                } else {
                    sb.append(" ");
                }
                sb.append(" | ");
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    /**
     * Converts the state to a bit board representation.
     *
     * @return The bit board representation of the state.
     */
    public int[] toBitBoard() {
        int[] bitBoard = new int[NUM_PIECES];
        int j = 0;
        for (Block block : this.getBlocks()) {
            bitBoard[j++] = createBitMaskForBlock(block);
        }
        return bitBoard;
    }


    @Override
    public boolean equals(Object obj) {
        if (obj instanceof State) {
            State state = (State) obj;

            return Arrays.equals(this.getBlocks(), state.getBlocks());
        }
        return false;
    }

    public void incrementMoves() {
        this.moves++;
    }

    public void decrementMoves() {
        this.moves--;
        if (this.moves < 0) {
            this.moves = 0;
        }
    }
}