Merge pull request 'feature/wilson-algorithm' (#10) from feature/wilson-algorithm into master
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Reviewed-on: #10
This commit is contained in:
		
						commit
						05ee55822a
					
				
					 34 changed files with 1257 additions and 340 deletions
				
			
		
							
								
								
									
										5
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -74,6 +74,11 @@ | ||||||
|             <groupId>io.vavr</groupId> |             <groupId>io.vavr</groupId> | ||||||
|             <artifactId>vavr</artifactId> |             <artifactId>vavr</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>com.google.guava</groupId> | ||||||
|  |             <artifactId>guava</artifactId> | ||||||
|  |             <version>33.2.1-jre</version> | ||||||
|  |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>org.apache.pdfbox</groupId> |             <groupId>org.apache.pdfbox</groupId> | ||||||
|             <artifactId>pdfbox</artifactId> |             <artifactId>pdfbox</artifactId> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,20 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.util.Random; | ||||||
|  | 
 | ||||||
|  | public abstract class AbstractMazeGeneratorAlgorithm implements MazeGeneratorAlgorithm { | ||||||
|  |     @NotNull | ||||||
|  |     protected final Maze maze; | ||||||
|  |     @NotNull | ||||||
|  |     protected final Random random; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     protected AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze, @NotNull final String algorithmName) { | ||||||
|  |         this.maze = maze; | ||||||
|  |         this.random = new Random(maze.getRandomSeed()); | ||||||
|  |         this.maze.setAlgorithm(algorithmName); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm; | ||||||
|  | 
 | ||||||
|  | public interface MazeGeneratorAlgorithm { | ||||||
|  |     void run(); | ||||||
|  | } | ||||||
|  | @ -10,20 +10,13 @@ import org.jetbrains.annotations.Nullable; | ||||||
| 
 | 
 | ||||||
| import java.util.Deque; | import java.util.Deque; | ||||||
| import java.util.LinkedList; | import java.util.LinkedList; | ||||||
| import java.util.Random; |  | ||||||
| 
 | 
 | ||||||
| public class RandomDepthFirst { | public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm { | ||||||
| 
 |  | ||||||
|     @NotNull |  | ||||||
|     private final Maze maze; |  | ||||||
|     @NotNull |  | ||||||
|     private final Random random; |  | ||||||
|     @NotNull |     @NotNull | ||||||
|     private final Deque<Position> positions = new LinkedList<>(); |     private final Deque<Position> positions = new LinkedList<>(); | ||||||
| 
 | 
 | ||||||
|     public RandomDepthFirst(@NotNull final Maze maze) { |     public RandomDepthFirst(@NotNull final Maze maze) { | ||||||
|         this.maze = maze; |         super(maze, "Random Depth First"); | ||||||
|         this.random = new Random(maze.getRandomSeed()); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void run() { |     public void run() { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,62 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm.wilson; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Direction; | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.util.EnumSet; | ||||||
|  | import java.util.Iterator; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Stack; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | public class MazeSolver { | ||||||
|  |     @NotNull | ||||||
|  |     private final Maze maze; | ||||||
|  | 
 | ||||||
|  |     MazeSolver(@NotNull final Maze maze) { | ||||||
|  |         this.maze = maze; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void solve() { | ||||||
|  |         final Direction directionToOuterWall = Wilson.getDirectionToOuterWall( | ||||||
|  |                 this.maze.getStart(), | ||||||
|  |                 this.maze.getWidth(), | ||||||
|  |                 this.maze.getHeight() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         final List<Position> solution = this.getSolution(this.maze.getStart(), directionToOuterWall); | ||||||
|  |         for (Position position : solution) { | ||||||
|  |             this.maze.getTileAt(position).get().setSolution(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private List<Position> getSolution(@NotNull Position position, | ||||||
|  |                                        @NotNull Direction forbidden) { | ||||||
|  |         record PathElement(@NotNull Position position, | ||||||
|  |                            @NotNull EnumSet<Direction> possibleDirections) { | ||||||
|  |         } | ||||||
|  |         final Stack<PathElement> solution = new Stack<>(); | ||||||
|  |         final EnumSet<Direction> directions = this.maze.getTileAt(position).get().getOpenDirections(); | ||||||
|  |         directions.remove(forbidden); | ||||||
|  |         PathElement head = new PathElement(position, directions); | ||||||
|  |         solution.push(head); | ||||||
|  |         while (!head.position.equals(this.maze.getEnd())) { | ||||||
|  |             if (head.possibleDirections.isEmpty()) { | ||||||
|  |                 solution.pop(); | ||||||
|  |                 head = solution.peek(); | ||||||
|  |             } else { | ||||||
|  |                 final Iterator<Direction> iterator = head.possibleDirections.iterator(); | ||||||
|  |                 final Direction direction = iterator.next(); | ||||||
|  |                 iterator.remove(); | ||||||
|  |                 final Position next = head.position.move(direction); | ||||||
|  |                 final EnumSet<Direction> openDirections = this.maze.getTileAt(next).get().getOpenDirections(); | ||||||
|  |                 openDirections.remove(direction.invert()); | ||||||
|  |                 head = new PathElement(next, openDirections); | ||||||
|  |                 solution.push(head); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return solution.stream().map(PathElement::position).collect(Collectors.toList()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,91 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm.wilson; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Direction; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import io.vavr.collection.List; | ||||||
|  | import io.vavr.collection.Stream; | ||||||
|  | import io.vavr.collection.Traversable; | ||||||
|  | import io.vavr.control.Option; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.util.Random; | ||||||
|  | 
 | ||||||
|  | class Path { | ||||||
|  |     private final int width; | ||||||
|  |     private final int height; | ||||||
|  |     @NotNull | ||||||
|  |     private List<Position> positions; | ||||||
|  | 
 | ||||||
|  |     Path(@NotNull final Position start, int width, int height) { | ||||||
|  |         this.positions = List.of(start); | ||||||
|  |         this.width = width; | ||||||
|  |         this.height = height; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     Position growRandom(@NotNull final Random random) { | ||||||
|  |         final Position position = this.nextRandomPosition(random); | ||||||
|  |         if (this.contains(position)) { | ||||||
|  |             this.removeLoopUpTo(position); | ||||||
|  |             return this.growRandom(random); | ||||||
|  |         } | ||||||
|  |         this.positions = this.positions.prepend(position); | ||||||
|  |         return position; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     List<Position> getPositions() { | ||||||
|  |         return this.positions; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     Position getStart() { | ||||||
|  |         return this.positions.last(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     Traversable<Direction> getMovesFromStart() { | ||||||
|  |         return this.positions.reverse().sliding(2) | ||||||
|  |                 .flatMap(positions1 -> Option.when( | ||||||
|  |                         positions1.size() == 2, | ||||||
|  |                         // DEV-NOTE: .get() is safe here, because in the context of a path, there MUST be a direction | ||||||
|  |                         // from one position to the next. | ||||||
|  |                         () -> positions1.head().getDirectionTo(positions1.last()).get() | ||||||
|  |                 )); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     private Position nextRandomPosition(@NotNull final Random random) { | ||||||
|  |         final Direction randomDirection = this.getRandomDirection(random); | ||||||
|  |         final Position nextPosition = this.positions.head().move(randomDirection); | ||||||
|  |         if (this.isWithinBounds(nextPosition) && !nextPosition.equals(this.positions.head())) { | ||||||
|  |             return nextPosition; | ||||||
|  |         } | ||||||
|  |         return this.nextRandomPosition(random); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isWithinBounds(@NotNull final Position position) { | ||||||
|  |         return position.x() >= 0 && position.x() < this.width && position.y() >= 0 && position.y() < this.height; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean contains(@NotNull final Position position) { | ||||||
|  |         return this.positions.contains(position); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void removeLoopUpTo(@NotNull final Position position) { | ||||||
|  |         this.positions = this.positions.dropUntil(position::equals); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     private Direction getRandomDirection(@NotNull final Random random) { | ||||||
|  |         final Direction[] array = Direction.values(); | ||||||
|  |         return array[random.nextInt(array.length)]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String toString() { | ||||||
|  |         return Stream.ofAll(this.positions) | ||||||
|  |                 .map(position -> "(%s,%s)".formatted(position.x(), position.y())) | ||||||
|  |                 .mkString("Path[", "->", "]"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,131 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm.wilson; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import com.google.common.collect.HashMultimap; | ||||||
|  | import com.google.common.collect.Multimap; | ||||||
|  | import io.vavr.Tuple; | ||||||
|  | import io.vavr.collection.Stream; | ||||||
|  | import io.vavr.collection.Traversable; | ||||||
|  | import io.vavr.control.Option; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.util.Random; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This class will build paths such that in the end all fields of the maze are covered by exactly one path. | ||||||
|  |  */ | ||||||
|  | class PathsBuilder { | ||||||
|  |     private final int width; | ||||||
|  |     private final int height; | ||||||
|  |     @NotNull | ||||||
|  |     private final Random random; | ||||||
|  |     @NotNull | ||||||
|  |     private final Multimap<Integer, Integer> availablePositions; | ||||||
|  | 
 | ||||||
|  |     PathsBuilder(@NotNull final Maze maze, | ||||||
|  |                  @NotNull final Random random) { | ||||||
|  |         this.width = maze.getWidth(); | ||||||
|  |         this.height = maze.getHeight(); | ||||||
|  |         this.random = random; | ||||||
|  |         this.availablePositions = HashMultimap.create(this.width, this.height); | ||||||
|  | 
 | ||||||
|  |         // Initialize the available positions. | ||||||
|  |         for (int x = 0; x < this.width; x++) { | ||||||
|  |             for (int y = 0; y < this.height; y++) { | ||||||
|  |                 this.availablePositions.put(x, y); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create all the paths such that the maze will be completely filled and every field of it will be covered by | ||||||
|  |      * exactly one path. | ||||||
|  |      * | ||||||
|  |      * @return A {@link Traversable} of generated {@link Path Paths}. | ||||||
|  |      */ | ||||||
|  |     @NotNull | ||||||
|  |     Traversable<Path> buildPaths() { | ||||||
|  |         this.initializeWithRandomStartingPosition(); | ||||||
|  | 
 | ||||||
|  |         return Stream.unfoldLeft( | ||||||
|  |                 this, | ||||||
|  |                 builder -> builder.buildPath() | ||||||
|  |                         .map(path -> { | ||||||
|  |                             builder.setPartOfMaze(path); | ||||||
|  |                             return Tuple.of(builder, path); | ||||||
|  |                         }) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void initializeWithRandomStartingPosition() { | ||||||
|  |         this.popRandomPosition(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Creates one new path, if possible. If the maze is already filled, {@link io.vavr.control.Option.None} is | ||||||
|  |      * returned. | ||||||
|  |      * | ||||||
|  |      * @return An {@link Option} of a new {@link Path} instance. | ||||||
|  |      */ | ||||||
|  |     @NotNull | ||||||
|  |     private Option<Path> buildPath() { | ||||||
|  |         return this.initializeNewPath() | ||||||
|  |                 .map(this::growPath); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     private Option<Path> initializeNewPath() { | ||||||
|  |         return this.popRandomPosition() | ||||||
|  |                 .map(position -> new Path(position, this.width, this.height)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Randomly grow the {@code path} until it reaches a field that is part of the maze and return it. The resulting | ||||||
|  |      * path will contain no loops. | ||||||
|  |      * | ||||||
|  |      * @param path The {@link Path} to grow. | ||||||
|  |      * @return The final {@link Path} that reaches the maze. | ||||||
|  |      */ | ||||||
|  |     @NotNull | ||||||
|  |     private Path growPath(@NotNull final Path path) { | ||||||
|  |         Position lastPosition; | ||||||
|  |         do { | ||||||
|  |             lastPosition = path.growRandom(this.random); | ||||||
|  |         } while (this.isNotPartOfMaze(lastPosition)); | ||||||
|  |         return path; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isNotPartOfMaze(@NotNull final Position position) { | ||||||
|  |         return this.availablePositions.containsEntry(position.x(), position.y()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setPartOfMaze(@NotNull final Position position) { | ||||||
|  |         this.availablePositions.remove(position.x(), position.y()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setPartOfMaze(@NotNull final Path path) { | ||||||
|  |         path.getPositions().forEach(this::setPartOfMaze); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Finds a random {@link Position}, that is not yet part of the maze, marks it as being part of the maze and returns | ||||||
|  |      * it. If no position is available, {@link io.vavr.control.Option.None} is returned. | ||||||
|  |      * | ||||||
|  |      * @return An available position or {@link io.vavr.control.Option.None}. | ||||||
|  |      */ | ||||||
|  |     @NotNull | ||||||
|  |     private Option<Position> popRandomPosition() { | ||||||
|  |         if (this.availablePositions.isEmpty()) { | ||||||
|  |             return Option.none(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final Integer[] keys = this.availablePositions.keySet().toArray(Integer[]::new); | ||||||
|  |         final int key = keys[this.random.nextInt(keys.length)]; | ||||||
|  |         final Integer[] values = this.availablePositions.get(key).toArray(Integer[]::new); | ||||||
|  |         final int value = values[this.random.nextInt(values.length)]; | ||||||
|  | 
 | ||||||
|  |         this.availablePositions.remove(key, value); | ||||||
|  |         return Option.some(new Position(key, value)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,96 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm.wilson; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.algorithm.AbstractMazeGeneratorAlgorithm; | ||||||
|  | import ch.fritteli.maze.generator.model.Direction; | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import ch.fritteli.maze.generator.model.Tile; | ||||||
|  | import io.vavr.collection.Traversable; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | import org.jetbrains.annotations.Nullable; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * An implementation of <a href="https://en.wikipedia.org/wiki/Maze_generation_algorithm#Wilson's_algorithm">Wilson's Algorithm</a>. | ||||||
|  |  * In short: | ||||||
|  |  * <ol> | ||||||
|  |  *     <li>Pick random location, add to maze</li> | ||||||
|  |  *     <li>While locations that are not part of the maze exist, loop: | ||||||
|  |  *     <ol> | ||||||
|  |  *         <li>Pick random location that's not part of the maze</li> | ||||||
|  |  *         <li>Randomly walk from this location, until ... | ||||||
|  |  *         <ul> | ||||||
|  |  *             <li>... either you hit the current path, forming a loop. Then remove the entire loop and continue | ||||||
|  |  *             walking.</li> | ||||||
|  |  *             <li>... or you hit a position that is part of the maze. Then add the path to the maze and start the next | ||||||
|  |  *             walk.</li> | ||||||
|  |  *         </ul></li> | ||||||
|  |  *     </ol></li> | ||||||
|  |  * </ol> | ||||||
|  |  */ | ||||||
|  | public class Wilson extends AbstractMazeGeneratorAlgorithm { | ||||||
|  | 
 | ||||||
|  |     public Wilson(@NotNull final Maze maze) { | ||||||
|  |         super(maze, "Wilson"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     static Direction getDirectionToOuterWall(@NotNull final Position position, | ||||||
|  |                                              final int width, | ||||||
|  |                                              final int height) { | ||||||
|  |         if (position.y() == 0) { | ||||||
|  |             return Direction.TOP; | ||||||
|  |         } | ||||||
|  |         if (position.y() == height - 1) { | ||||||
|  |             return Direction.BOTTOM; | ||||||
|  |         } | ||||||
|  |         if (position.x() == 0) { | ||||||
|  |             return Direction.LEFT; | ||||||
|  |         } | ||||||
|  |         if (position.x() == width - 1) { | ||||||
|  |             return Direction.RIGHT; | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void run() { | ||||||
|  |         final Traversable<Path> paths = new PathsBuilder(this.maze, this.random) | ||||||
|  |                 .buildPaths(); | ||||||
|  | 
 | ||||||
|  |         this.applyPathsToMaze(paths); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void applyPathsToMaze(@NotNull final Traversable<Path> paths) { | ||||||
|  |         this.openStartAndEndWalls(); | ||||||
|  |         paths.forEach(path -> path.getMovesFromStart() | ||||||
|  |                 .foldLeft( | ||||||
|  |                         path.getStart(), | ||||||
|  |                         (position, direction) -> { | ||||||
|  |                             this.maze.getTileAt(position).get() | ||||||
|  |                                     .digTo(direction); | ||||||
|  |                             final Position next = position.move(direction); | ||||||
|  |                             this.maze.getTileAt(next).get() | ||||||
|  |                                     .digTo(direction.invert()); | ||||||
|  |                             return next; | ||||||
|  |                         })); | ||||||
|  | 
 | ||||||
|  |         final MazeSolver solver = new MazeSolver(this.maze); | ||||||
|  |         solver.solve(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void openStartAndEndWalls() { | ||||||
|  |         this.openWall(this.maze.getStart(), this.maze.getStartTile()); | ||||||
|  |         this.openWall(this.maze.getEnd(), this.maze.getEndTile()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void openWall(@NotNull final Position position, @NotNull final Tile tile) { | ||||||
|  |         final Direction direction = this.getDirectionToOuterWall(position); | ||||||
|  |         tile.enableDiggingToOrFrom(direction); | ||||||
|  |         tile.digTo(direction); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     private Direction getDirectionToOuterWall(@NotNull final Position position) { | ||||||
|  |         return getDirectionToOuterWall(position, this.maze.getWidth(), this.maze.getHeight()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| package ch.fritteli.maze.generator.model; | package ch.fritteli.maze.generator.model; | ||||||
| 
 | 
 | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
| public enum Direction { | public enum Direction { | ||||||
|     TOP, |     TOP, | ||||||
|     RIGHT, |     RIGHT, | ||||||
|     BOTTOM, |     BOTTOM, | ||||||
|     LEFT; |     LEFT; | ||||||
| 
 | 
 | ||||||
|  |     @NotNull | ||||||
|     public Direction invert() { |     public Direction invert() { | ||||||
|         return switch (this) { |         return switch (this) { | ||||||
|             case TOP -> BOTTOM; |             case TOP -> BOTTOM; | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package ch.fritteli.maze.generator.model; | ||||||
| import io.vavr.control.Option; | import io.vavr.control.Option; | ||||||
| import lombok.EqualsAndHashCode; | import lombok.EqualsAndHashCode; | ||||||
| import lombok.Getter; | import lombok.Getter; | ||||||
|  | import lombok.Setter; | ||||||
| import lombok.ToString; | import lombok.ToString; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
|  | @ -21,6 +22,9 @@ public class Maze { | ||||||
|     private final Position start; |     private final Position start; | ||||||
|     @Getter |     @Getter | ||||||
|     private final Position end; |     private final Position end; | ||||||
|  |     @Getter | ||||||
|  |     @Setter | ||||||
|  |     private String algorithm; | ||||||
| 
 | 
 | ||||||
|     public Maze(final int width, final int height) { |     public Maze(final int width, final int height) { | ||||||
|         this(width, height, System.nanoTime()); |         this(width, height, System.nanoTime()); | ||||||
|  | @ -34,7 +38,11 @@ public class Maze { | ||||||
|         this(width, height, randomSeed, new Position(0, 0), new Position(width - 1, height - 1)); |         this(width, height, randomSeed, new Position(0, 0), new Position(width - 1, height - 1)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Maze(final int width, final int height, final long randomSeed, @NotNull final Position start, @NotNull final Position end) { |     public Maze(final int width, | ||||||
|  |                 final int height, | ||||||
|  |                 final long randomSeed, | ||||||
|  |                 @NotNull final Position start, | ||||||
|  |                 @NotNull final Position end) { | ||||||
|         if (width <= 1 || height <= 1) { |         if (width <= 1 || height <= 1) { | ||||||
|             throw new IllegalArgumentException("width and height must be >1"); |             throw new IllegalArgumentException("width and height must be >1"); | ||||||
|         } |         } | ||||||
|  | @ -71,7 +79,12 @@ public class Maze { | ||||||
|     /** |     /** | ||||||
|      * INTERNAL API. Exists only for deserialization. Not to be called from user code. |      * INTERNAL API. Exists only for deserialization. Not to be called from user code. | ||||||
|      */ |      */ | ||||||
|     private Maze(@NotNull final Tile[][] field, final int width, final int height, @NotNull final Position start, @NotNull final Position end, final long randomSeed) { |     private Maze(@NotNull final Tile[][] field, | ||||||
|  |                  final int width, | ||||||
|  |                  final int height, | ||||||
|  |                  @NotNull final Position start, | ||||||
|  |                  @NotNull final Position end, | ||||||
|  |                  final long randomSeed) { | ||||||
|         this.field = field; |         this.field = field; | ||||||
|         this.width = width; |         this.width = width; | ||||||
|         this.height = height; |         this.height = height; | ||||||
|  | @ -80,6 +93,25 @@ public class Maze { | ||||||
|         this.end = end; |         this.end = end; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * INTERNAL API. Exists only for deserialization. Not to be called from user code. | ||||||
|  |      */ | ||||||
|  |     private Maze(@NotNull final Tile[][] field, | ||||||
|  |                  final int width, | ||||||
|  |                  final int height, | ||||||
|  |                  @NotNull final Position start, | ||||||
|  |                  @NotNull final Position end, | ||||||
|  |                  final long randomSeed, | ||||||
|  |                  @NotNull final String algorithm) { | ||||||
|  |         this.field = field; | ||||||
|  |         this.width = width; | ||||||
|  |         this.height = height; | ||||||
|  |         this.randomSeed = randomSeed; | ||||||
|  |         this.algorithm = algorithm; | ||||||
|  |         this.start = start; | ||||||
|  |         this.end = end; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @NotNull |     @NotNull | ||||||
|     public Option<Tile> getTileAt(@NotNull final Position position) { |     public Option<Tile> getTileAt(@NotNull final Position position) { | ||||||
|         return this.getTileAt(position.x(), position.y()); |         return this.getTileAt(position.x(), position.y()); | ||||||
|  |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| package ch.fritteli.maze.generator.model; | package ch.fritteli.maze.generator.model; | ||||||
| 
 | 
 | ||||||
|  | import io.vavr.control.Option; | ||||||
| import lombok.With; | import lombok.With; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| @With | @With | ||||||
| public record Position(int x, int y) { | public record Position(int x, int y) { | ||||||
|  |     @NotNull | ||||||
|     public Position move(@NotNull final Direction direction) { |     public Position move(@NotNull final Direction direction) { | ||||||
|         return switch (direction) { |         return switch (direction) { | ||||||
|             case BOTTOM -> this.withY(this.y + 1); |             case BOTTOM -> this.withY(this.y + 1); | ||||||
|  | @ -13,4 +15,20 @@ public record Position(int x, int y) { | ||||||
|             case TOP -> this.withY(this.y - 1); |             case TOP -> this.withY(this.y - 1); | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     public Option<Direction> getDirectionTo(@NotNull final Position position) { | ||||||
|  |         final int xDiff = position.x - this.x; | ||||||
|  |         final int yDiff = position.y - this.y; | ||||||
|  |         return switch (xDiff) { | ||||||
|  |             case -1 -> Option.when(yDiff == 0, Direction.LEFT); | ||||||
|  |             case 0 -> switch (yDiff) { | ||||||
|  |                 case -1 -> Option.some(Direction.TOP); | ||||||
|  |                 case 1 -> Option.some(Direction.BOTTOM); | ||||||
|  |                 default -> Option.none(); | ||||||
|  |             }; | ||||||
|  |             case 1 -> Option.when(yDiff == 0, Direction.RIGHT); | ||||||
|  |             default -> Option.none(); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| package ch.fritteli.maze.generator.model; | package ch.fritteli.maze.generator.model; | ||||||
| 
 | 
 | ||||||
| import io.vavr.collection.Stream; | import io.vavr.collection.Vector; | ||||||
| import io.vavr.control.Option; | import io.vavr.control.Option; | ||||||
| import lombok.AccessLevel; | import lombok.AccessLevel; | ||||||
| import lombok.EqualsAndHashCode; | import lombok.EqualsAndHashCode; | ||||||
|  | @ -17,6 +17,7 @@ import java.util.Random; | ||||||
| @ToString | @ToString | ||||||
| public class Tile { | public class Tile { | ||||||
|     final Walls walls = new Walls(); |     final Walls walls = new Walls(); | ||||||
|  |     @EqualsAndHashCode.Exclude | ||||||
|     boolean visited = false; |     boolean visited = false; | ||||||
|     @Getter |     @Getter | ||||||
|     boolean solution = false; |     boolean solution = false; | ||||||
|  | @ -65,13 +66,23 @@ public class Tile { | ||||||
|         this.walls.set(direction); |         this.walls.set(direction); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @NotNull | ||||||
|  |     public EnumSet<Direction> getOpenDirections() { | ||||||
|  |         return this.walls.getOpen(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|     public Option<Direction> getRandomAvailableDirection(@NotNull final Random random) { |     public Option<Direction> getRandomAvailableDirection(@NotNull final Random random) { | ||||||
|         final Stream<Direction> availableDirections = this.walls.getUnsealedSet(); |         final EnumSet<Direction> availableDirections = this.walls.getUnsealedSet(); | ||||||
|         if (availableDirections.isEmpty()) { |         if (availableDirections.isEmpty()) { | ||||||
|             return Option.none(); |             return Option.none(); | ||||||
|         } |         } | ||||||
|         final int index = random.nextInt(availableDirections.length()); |         if (availableDirections.size() == 1) { | ||||||
|         return Option.of(availableDirections.get(index)); |             return Option.some(availableDirections.iterator().next()); | ||||||
|  |         } | ||||||
|  |         final Vector<Direction> directions = Vector.ofAll(availableDirections); | ||||||
|  |         final int index = random.nextInt(directions.size()); | ||||||
|  |         return Option.of(directions.get(index)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean hasWallAt(@NotNull final Direction direction) { |     public boolean hasWallAt(@NotNull final Direction direction) { | ||||||
|  |  | ||||||
|  | @ -1,21 +1,17 @@ | ||||||
| package ch.fritteli.maze.generator.model; | package ch.fritteli.maze.generator.model; | ||||||
| 
 | 
 | ||||||
| import io.vavr.collection.Stream; |  | ||||||
| import lombok.EqualsAndHashCode; | import lombok.EqualsAndHashCode; | ||||||
| import lombok.ToString; | import lombok.ToString; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| import java.util.EnumSet; | import java.util.EnumSet; | ||||||
| import java.util.HashSet; |  | ||||||
| import java.util.Set; |  | ||||||
| import java.util.SortedSet; |  | ||||||
| import java.util.TreeSet; |  | ||||||
| 
 | 
 | ||||||
| @EqualsAndHashCode | @EqualsAndHashCode | ||||||
| @ToString | @ToString | ||||||
| public class Walls { | public class Walls { | ||||||
|     private final SortedSet<Direction> directions = new TreeSet<>(); |     private final EnumSet<Direction> directions = EnumSet.noneOf(Direction.class); | ||||||
|     private final Set<Direction> sealed = new HashSet<>(); |     @EqualsAndHashCode.Exclude | ||||||
|  |     private final EnumSet<Direction> sealed = EnumSet.noneOf(Direction.class); | ||||||
| 
 | 
 | ||||||
|     public void set(@NotNull final Direction direction) { |     public void set(@NotNull final Direction direction) { | ||||||
|         this.directions.add(direction); |         this.directions.add(direction); | ||||||
|  | @ -36,9 +32,11 @@ public class Walls { | ||||||
|         return this.directions.contains(direction); |         return this.directions.contains(direction); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Stream<Direction> getUnsealedSet() { |     @NotNull | ||||||
|         return Stream.ofAll(this.directions) |     public EnumSet<Direction> getUnsealedSet() { | ||||||
|                 .removeAll(this.sealed); |         final EnumSet<Direction> result = EnumSet.copyOf(this.directions); | ||||||
|  |         result.removeAll(this.sealed); | ||||||
|  |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void seal(@NotNull final Direction direction) { |     public void seal(@NotNull final Direction direction) { | ||||||
|  | @ -51,4 +49,9 @@ public class Walls { | ||||||
|     public void unseal(@NotNull final Direction direction) { |     public void unseal(@NotNull final Direction direction) { | ||||||
|         this.sealed.remove(direction); |         this.sealed.remove(direction); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     public EnumSet<Direction> getOpen() { | ||||||
|  |         return EnumSet.complementOf(this.directions); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,108 +6,109 @@ import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| public class HTMLRenderer implements Renderer<String> { | public class HTMLRenderer implements Renderer<String> { | ||||||
| 
 | 
 | ||||||
|     private static final String POSTAMBLE = "<script>" |     private static final String POSTAMBLE = """ | ||||||
|             + "let userPath = [];" |             <script> | ||||||
|             + "const DIR_UNDEF = -1;" |             let userPath = []; | ||||||
|             + "const DIR_SAME = 0;" |             const DIR_UNDEF = -1; | ||||||
|             + "const DIR_UP = 1;" |             const DIR_SAME = 0; | ||||||
|             + "const DIR_RIGHT = 2;" |             const DIR_UP = 1; | ||||||
|             + "const DIR_DOWN = 3;" |             const DIR_RIGHT = 2; | ||||||
|             + "const DIR_LEFT = 4;" |             const DIR_DOWN = 3; | ||||||
|             + "function getCoords(cell) {" |             const DIR_LEFT = 4; | ||||||
|             + "    return {" |             function getCoords(cell) { | ||||||
|             + "        x: cell.cellIndex," |                 return { | ||||||
|             + "        y: cell.parentElement.rowIndex" |                     x: cell.cellIndex, | ||||||
|             + "    };" |                     y: cell.parentElement.rowIndex | ||||||
|             + "}" |                 }; | ||||||
|             + "function distance(prev, next) {" |             } | ||||||
|             + "    return Math.abs(prev.x - next.x) + Math.abs(prev.y - next.y);" |             function distance(prev, next) { | ||||||
|             + "}" |                 return Math.abs(prev.x - next.x) + Math.abs(prev.y - next.y); | ||||||
|             + "function direction(prev, next) {" |             } | ||||||
|             + "    const dist = distance(prev, next);" |             function direction(prev, next) { | ||||||
|             + "    if (dist === 0) {" |                 const dist = distance(prev, next); | ||||||
|             + "        return DIR_SAME;" |                 if (dist === 0) { | ||||||
|             + "    }" |                     return DIR_SAME; | ||||||
|             + "    if (dist !== 1) {" |                 } | ||||||
|             + "        return DIR_UNDEF;" |                 if (dist !== 1) { | ||||||
|             + "    }" |                     return DIR_UNDEF; | ||||||
|             + "    if (next.x === prev.x) {" |                 } | ||||||
|             + "        if (next.y === prev.y + 1) {" |                 if (next.x === prev.x) { | ||||||
|             + "            return DIR_DOWN;" |                     if (next.y === prev.y + 1) { | ||||||
|             + "        }" |                         return DIR_DOWN; | ||||||
|             + "        return DIR_UP;" |                     } | ||||||
|             + "    }" |                     return DIR_UP; | ||||||
|             + "    if (next.x === prev.x + 1) {" |                 } | ||||||
|             + "        return DIR_RIGHT;" |                 if (next.x === prev.x + 1) { | ||||||
|             + "    }" |                     return DIR_RIGHT; | ||||||
|             + "    return DIR_LEFT;" |                 } | ||||||
|             + "}" |                 return DIR_LEFT; | ||||||
|             + "(function () {" |             } | ||||||
|             + "    const labyrinthTable = document.getElementById(\"labyrinth\");" |             (function () { | ||||||
|             + "    const labyrinthCells = labyrinthTable.getElementsByTagName(\"td\");" |                 const labyrinthTable = document.getElementById("labyrinth"); | ||||||
|             + "    const start = {x: 0, y: 0};" |                 const labyrinthCells = labyrinthTable.getElementsByTagName("td"); | ||||||
|             + "    const end = {" |                 const start = {x: 0, y: 0}; | ||||||
|             + "        x: labyrinthTable.getElementsByTagName(\"tr\")[0].getElementsByTagName(\"td\").length - 1," |                 const end = { | ||||||
|             + "        y: labyrinthTable.getElementsByTagName(\"tr\").length - 1" |                     x: labyrinthTable.getElementsByTagName("tr")[0].getElementsByTagName("td").length - 1, | ||||||
|             + "    };" |                     y: labyrinthTable.getElementsByTagName("tr").length - 1 | ||||||
|             + "    for (let i = 0; i < labyrinthCells.length; i++) {" |                 }; | ||||||
|             + "        let cell = labyrinthCells.item(i);" |                 for (let i = 0; i < labyrinthCells.length; i++) { | ||||||
|             + "        cell.onclick = (event) => {" |                     let cell = labyrinthCells.item(i); | ||||||
|             + "            let target = event.target;" |                     cell.onclick = (event) => { | ||||||
|             + "            const coords = getCoords(target);" |                         let target = event.target; | ||||||
|             + "            if (coords.x === end.x && coords.y === end.y) {" |                         const coords = getCoords(target); | ||||||
|             + "                alert(\"HOORAY! You did it! Congratulations!\")" |                         if (coords.x === end.x && coords.y === end.y) { | ||||||
|             + "            }" |                             alert("HOORAY! You did it! Congratulations!") | ||||||
|             + "            if (userPath.length === 0) {" |                         } | ||||||
|             + "                if (coords.x === start.x && coords.y === start.y) {" |                         if (userPath.length === 0) { | ||||||
|             + "                    userPath.push(coords);" |                             if (coords.x === start.x && coords.y === start.y) { | ||||||
|             + "                    target.classList.toggle(\"user\");" |                                 userPath.push(coords); | ||||||
|             + "                }" |                                 target.classList.toggle("user"); | ||||||
|             + "            } else {" |                             } | ||||||
|             + "                const dir = direction(userPath[userPath.length - 1], coords);" |                         } else { | ||||||
|             + "                switch (dir) {" |                             const dir = direction(userPath[userPath.length - 1], coords); | ||||||
|             + "                    case DIR_UNDEF:" |                             switch (dir) { | ||||||
|             + "                        return;" |                                 case DIR_UNDEF: | ||||||
|             + "                    case DIR_SAME:" |                                     return; | ||||||
|             + "                        userPath.pop();" |                                 case DIR_SAME: | ||||||
|             + "                        target.classList.toggle(\"user\");" |                                     userPath.pop(); | ||||||
|             + "                        return;" |                                     target.classList.toggle("user"); | ||||||
|             + "                    default:" |                                     return; | ||||||
|             + "                        if (userPath.find(value => value.x === coords.x && value.y === coords.y)) {" |                                 default: | ||||||
|             + "                            return;" |                                     if (userPath.find(value => value.x === coords.x && value.y === coords.y)) { | ||||||
|             + "                        } else {" |                                         return; | ||||||
|             + "                            switch (dir) {" |                                     } else { | ||||||
|             + "                                case DIR_UP:" |                                         switch (dir) { | ||||||
|             + "                                    if (target.classList.contains(\"bottom\")) {" |                                             case DIR_UP: | ||||||
|             + "                                        return;" |                                                 if (target.classList.contains("bottom")) { | ||||||
|             + "                                    }" |                                                     return; | ||||||
|             + "                                    break;" |                                                 } | ||||||
|             + "                                case DIR_RIGHT:" |                                                 break; | ||||||
|             + "                                    if (target.classList.contains(\"left\")) {" |                                             case DIR_RIGHT: | ||||||
|             + "                                        return;" |                                                 if (target.classList.contains("left")) { | ||||||
|             + "                                    }" |                                                     return; | ||||||
|             + "                                    break;" |                                                 } | ||||||
|             + "                                case DIR_DOWN:" |                                                 break; | ||||||
|             + "                                    if (target.classList.contains(\"top\")) {" |                                             case DIR_DOWN: | ||||||
|             + "                                        return;" |                                                 if (target.classList.contains("top")) { | ||||||
|             + "                                    }" |                                                     return; | ||||||
|             + "                                    break;" |                                                 } | ||||||
|             + "                                case DIR_LEFT:" |                                                 break; | ||||||
|             + "                                    if (target.classList.contains(\"right\")) {" |                                             case DIR_LEFT: | ||||||
|             + "                                        return;" |                                                 if (target.classList.contains("right")) { | ||||||
|             + "                                    }" |                                                     return; | ||||||
|             + "                                    break;" |                                                 } | ||||||
|             + "                            }" |                                                 break; | ||||||
|             + "                            userPath.push(coords);" |                                         } | ||||||
|             + "                            target.classList.toggle(\"user\");" |                                         userPath.push(coords); | ||||||
|             + "                            return;" |                                         target.classList.toggle("user"); | ||||||
|             + "                        }" |                                         return; | ||||||
|             + "                }" |                                     } | ||||||
|             + "            }" |                             } | ||||||
|             + "        };" |                         } | ||||||
|             + "    }" |                     }; | ||||||
|             + "})();" |                 } | ||||||
|             + "</script></body></html>"; |             })(); | ||||||
|  |             </script></body></html>"""; | ||||||
| 
 | 
 | ||||||
|     private HTMLRenderer() { |     private HTMLRenderer() { | ||||||
|     } |     } | ||||||
|  | @ -135,33 +136,42 @@ public class HTMLRenderer implements Renderer<String> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private String getPreamble(@NotNull final Maze maze) { |     private String getPreamble(@NotNull final Maze maze) { | ||||||
|         return "<!DOCTYPE html><html lang=\"en\">" + |         return """ | ||||||
|                 "<head>" + |                 <!DOCTYPE html> | ||||||
|                 "<title>Maze " + maze.getWidth() + "x" + maze.getHeight() + ", ID " + maze.getRandomSeed() + "</title>" + |                 <html lang="en"> | ||||||
|                 "<meta charset=\"utf-8\">" + |                     <head> | ||||||
|                 "<style>" + |                         <title>Maze %dx%d, ID %d, Algorithm %s</title> | ||||||
|                 "table{border-collapse:collapse;}" + |                         <meta charset="utf-8"> | ||||||
|                 "td{border:0 solid black;height:1em;width:1em;cursor:pointer;}" + |                         <style> | ||||||
|                 "td.top{border-top-width:1px;}" + |                             table{border-collapse:collapse;} | ||||||
|                 "td.right{border-right-width:1px;}" + |                             td{border:0 solid black;height:1em;width:1em;cursor:pointer;} | ||||||
|                 "td.bottom{border-bottom-width:1px;}" + |                             td.top{border-top-width:1px;} | ||||||
|                 "td.left{border-left-width:1px;}" + |                             td.right{border-right-width:1px;} | ||||||
|                 "td.user{background:hotpink;}" + |                             td.bottom{border-bottom-width:1px;} | ||||||
|                 "</style>" + |                             td.left{border-left-width:1px;} | ||||||
|                 "<script>" + |                             td.user{background:hotpink;} | ||||||
|                 "let solution = false;" + |                         </style> | ||||||
|                 "function toggleSolution() {" + |                         <script> | ||||||
|                 "let stylesheet = document.styleSheets[0];" + |                             let solution = false; | ||||||
|                 "if(solution){" + |                             function toggleSolution() { | ||||||
|                 "stylesheet.deleteRule(0);" + |                                 let stylesheet = document.styleSheets[0]; | ||||||
|                 "}else{" + |                                 if (solution) { | ||||||
|                 "stylesheet.insertRule(\"td.solution{background-color:lightgray;}\", 0);" + |                                     stylesheet.deleteRule(0); | ||||||
|                 "}" + |                                 } else { | ||||||
|                 "solution = !solution;" + |                                     stylesheet.insertRule("td.solution{background-color:lightgray;}", 0); | ||||||
|                 "}" + |                                 } | ||||||
|                 "</script>" + |                                 solution = !solution; | ||||||
|                 "</head>" + |                             } | ||||||
|                 "<body>" + |                         </script> | ||||||
|                 "<input id=\"solutionbox\" type=\"checkbox\" onclick=\"toggleSolution()\"/><label for=\"solutionbox\">show solution</label>"; |                     </head> | ||||||
|  |                     <body> | ||||||
|  |                         <input id="solutionbox" type="checkbox" onclick="toggleSolution()"/> | ||||||
|  |                         <label for="solutionbox">show solution</label>""" | ||||||
|  |                 .formatted( | ||||||
|  |                         maze.getWidth(), | ||||||
|  |                         maze.getHeight(), | ||||||
|  |                         maze.getRandomSeed(), | ||||||
|  |                         maze.getAlgorithm() | ||||||
|  |                 ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ class Generator { | ||||||
|     JsonMaze generate() { |     JsonMaze generate() { | ||||||
|         final JsonMaze result = new JsonMaze(); |         final JsonMaze result = new JsonMaze(); | ||||||
|         result.setId(String.valueOf(this.maze.getRandomSeed())); |         result.setId(String.valueOf(this.maze.getRandomSeed())); | ||||||
|  |         result.setAlgorithm(this.maze.getAlgorithm()); | ||||||
|         result.setWidth(this.maze.getWidth()); |         result.setWidth(this.maze.getWidth()); | ||||||
|         result.setHeight(this.maze.getHeight()); |         result.setHeight(this.maze.getHeight()); | ||||||
|         final List<List<JsonCell>> rows = new ArrayList<>(); |         final List<List<JsonCell>> rows = new ArrayList<>(); | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ class Generator { | ||||||
| 
 | 
 | ||||||
|         final PDDocument pdDocument = new PDDocument(); |         final PDDocument pdDocument = new PDDocument(); | ||||||
|         final PDDocumentInformation info = new PDDocumentInformation(); |         final PDDocumentInformation info = new PDDocumentInformation(); | ||||||
|         info.setTitle("Maze %sx%s, ID %s".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed())); |         info.setTitle("Maze %sx%s, ID %s (%s)".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed(), this.maze.getAlgorithm())); | ||||||
|         pdDocument.setDocumentInformation(info); |         pdDocument.setDocumentInformation(info); | ||||||
|         final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight)); |         final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight)); | ||||||
|         final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight)); |         final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight)); | ||||||
|  | @ -233,7 +233,7 @@ class Generator { | ||||||
|                 if (position.equals(previousPosition)) { |                 if (position.equals(previousPosition)) { | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
|                 if (tileAtPosition.map(Tile::isSolution).getOrElse(false)) { |                 if (tileAtPosition.exists(Tile::isSolution)) { | ||||||
|                     return position; |                     return position; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| import java.io.ByteArrayInputStream; | import java.io.ByteArrayInputStream; | ||||||
|  | import java.io.IOException; | ||||||
| 
 | 
 | ||||||
| public abstract class AbstractMazeInputStream extends ByteArrayInputStream { | public abstract class AbstractMazeInputStream extends ByteArrayInputStream { | ||||||
| 
 | 
 | ||||||
|  | @ -14,7 +15,7 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream { | ||||||
|     public abstract void checkHeader(); |     public abstract void checkHeader(); | ||||||
| 
 | 
 | ||||||
|     @NotNull |     @NotNull | ||||||
|     public abstract Maze readMazeData(); |     public abstract Maze readMazeData() throws IOException; | ||||||
| 
 | 
 | ||||||
|     public byte readByte() { |     public byte readByte() { | ||||||
|         final int read = this.read(); |         final int read = this.read(); | ||||||
|  | @ -25,6 +26,10 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream { | ||||||
|         return (byte) read; |         return (byte) read; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public int readByteAsInt() { | ||||||
|  |         return 0xff & this.readByte(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public int readInt() { |     public int readInt() { | ||||||
|         int result = 0; |         int result = 0; | ||||||
|         result |= (0xff & this.readByte()) << 24; |         result |= (0xff & this.readByte()) << 24; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Direction; | ||||||
|  | import ch.fritteli.maze.generator.model.Tile; | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.lang.reflect.Constructor; | ||||||
|  | import java.lang.reflect.InvocationTargetException; | ||||||
|  | import java.util.EnumSet; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Binary format description of a {@link Tile}.<br> | ||||||
|  |  * A tile is stored in one byte: | ||||||
|  |  * <ul> | ||||||
|  |  *     <li>bits 0..2: always 0</li> | ||||||
|  |  *     <li>bit 3: 1=solution, 0=not solution</li> | ||||||
|  |  *     <li>bits 4..7: encode walls</li> | ||||||
|  |  * </ul> | ||||||
|  |  * The values for bits 4..7 are as follows: | ||||||
|  |  * <pre> | ||||||
|  |  * decimal hex bin  border | ||||||
|  |  *       0   0 0000 no border | ||||||
|  |  *       1   1 0001 top | ||||||
|  |  *       2   2 0010 right | ||||||
|  |  *       3   3 0011 top+right | ||||||
|  |  *       4   4 0100 bottom | ||||||
|  |  *       5   5 0101 top+bottom | ||||||
|  |  *       6   6 0110 right+bottom | ||||||
|  |  *       7   7 0111 top+right+bottom | ||||||
|  |  *       8   8 1000 left | ||||||
|  |  *       9   9 1001 top+left | ||||||
|  |  *      10   a 1010 right+left | ||||||
|  |  *      11   b 1011 top+right+left | ||||||
|  |  *      12   c 1100 bottom+left | ||||||
|  |  *      13   d 1101 top+bottom+left | ||||||
|  |  *      14   e 1110 right+bottom+left | ||||||
|  |  *      15   f 1111 top+right+bottom+left | ||||||
|  |  * </pre> | ||||||
|  |  */ | ||||||
|  | @UtilityClass | ||||||
|  | public class CommonTileHandler { | ||||||
|  |     private final byte TOP_BIT = 0b0000_0001; | ||||||
|  |     private final byte RIGHT_BIT = 0b0000_0010; | ||||||
|  |     private final byte BOTTOM_BIT = 0b0000_0100; | ||||||
|  |     private final byte LEFT_BIT = 0b0000_1000; | ||||||
|  |     private final byte SOLUTION_BIT = 0b0001_0000; | ||||||
|  | 
 | ||||||
|  |     public byte getBitmaskForTile(@NotNull final Tile tile) { | ||||||
|  |         byte bitmask = 0; | ||||||
|  |         if (tile.hasWallAt(Direction.TOP)) { | ||||||
|  |             bitmask |= TOP_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.hasWallAt(Direction.RIGHT)) { | ||||||
|  |             bitmask |= RIGHT_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.hasWallAt(Direction.BOTTOM)) { | ||||||
|  |             bitmask |= BOTTOM_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.hasWallAt((Direction.LEFT))) { | ||||||
|  |             bitmask |= LEFT_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.isSolution()) { | ||||||
|  |             bitmask |= SOLUTION_BIT; | ||||||
|  |         } | ||||||
|  |         return bitmask; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     public Tile getTileForBitmask(final byte bitmask) { | ||||||
|  |         final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class); | ||||||
|  |         if ((bitmask & TOP_BIT) == TOP_BIT) { | ||||||
|  |             walls.add(Direction.TOP); | ||||||
|  |         } | ||||||
|  |         if ((bitmask & RIGHT_BIT) == RIGHT_BIT) { | ||||||
|  |             walls.add(Direction.RIGHT); | ||||||
|  |         } | ||||||
|  |         if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) { | ||||||
|  |             walls.add(Direction.BOTTOM); | ||||||
|  |         } | ||||||
|  |         if ((bitmask & LEFT_BIT) == LEFT_BIT) { | ||||||
|  |             walls.add(Direction.LEFT); | ||||||
|  |         } | ||||||
|  |         final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT; | ||||||
|  |         return createTile(walls, solution); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) { | ||||||
|  |         try { | ||||||
|  |             final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE); | ||||||
|  |             constructor.setAccessible(true); | ||||||
|  |             return constructor.newInstance(walls, solution); | ||||||
|  |         } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException | | ||||||
|  |                                 InvocationTargetException e) { | ||||||
|  |             throw new RuntimeException("Can not deserialize Tile from maze data.", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization; | ||||||
|  | 
 | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
|  | 
 | ||||||
|  | @UtilityClass | ||||||
|  | public class MazeConstants { | ||||||
|  |     public final byte MAGIC_BYTE_1 = 0x1a; | ||||||
|  |     public final byte MAGIC_BYTE_2 = (byte) 0xb1; | ||||||
|  | } | ||||||
|  | @ -3,6 +3,8 @@ package ch.fritteli.maze.generator.serialization.v1; | ||||||
| import ch.fritteli.maze.generator.model.Maze; | import ch.fritteli.maze.generator.model.Maze; | ||||||
| import ch.fritteli.maze.generator.model.Tile; | import ch.fritteli.maze.generator.model.Tile; | ||||||
| import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream; | import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream; | ||||||
|  | import ch.fritteli.maze.generator.serialization.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| public class MazeInputStreamV1 extends AbstractMazeInputStream { | public class MazeInputStreamV1 extends AbstractMazeInputStream { | ||||||
|  | @ -13,12 +15,15 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void checkHeader() { |     public void checkHeader() { | ||||||
|  |         // 00 0x1a magic | ||||||
|  |         // 01 0xb1 magic | ||||||
|  |         // 02 0x01 version | ||||||
|         final byte magic1 = this.readByte(); |         final byte magic1 = this.readByte(); | ||||||
|         if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) { |         if (magic1 != MazeConstants.MAGIC_BYTE_1) { | ||||||
|             throw new IllegalArgumentException("Invalid maze data."); |             throw new IllegalArgumentException("Invalid maze data."); | ||||||
|         } |         } | ||||||
|         final byte magic2 = this.readByte(); |         final byte magic2 = this.readByte(); | ||||||
|         if (magic2 != SerializerDeserializerV1.MAGIC_BYTE_2) { |         if (magic2 != MazeConstants.MAGIC_BYTE_2) { | ||||||
|             throw new IllegalArgumentException("Invalid maze data."); |             throw new IllegalArgumentException("Invalid maze data."); | ||||||
|         } |         } | ||||||
|         final int version = this.readByte(); |         final int version = this.readByte(); | ||||||
|  | @ -30,6 +35,10 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { | ||||||
|     @NotNull |     @NotNull | ||||||
|     @Override |     @Override | ||||||
|     public Maze readMazeData() { |     public Maze readMazeData() { | ||||||
|  |         // 03..10  random seed number (long) | ||||||
|  |         // 11..14  width (int) | ||||||
|  |         // 15..18  height (int) | ||||||
|  |         // 19..    tiles | ||||||
|         final long randomSeed = this.readLong(); |         final long randomSeed = this.readLong(); | ||||||
|         final int width = this.readInt(); |         final int width = this.readInt(); | ||||||
|         final int height = this.readInt(); |         final int height = this.readInt(); | ||||||
|  | @ -42,7 +51,7 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { | ||||||
|         for (int y = 0; y < height; y++) { |         for (int y = 0; y < height; y++) { | ||||||
|             for (int x = 0; x < width; x++) { |             for (int x = 0; x < width; x++) { | ||||||
|                 final byte bitmask = this.readByte(); |                 final byte bitmask = this.readByte(); | ||||||
|                 tiles[x][y] = SerializerDeserializerV1.getTileForBitmask(bitmask); |                 tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,19 +3,28 @@ package ch.fritteli.maze.generator.serialization.v1; | ||||||
| import ch.fritteli.maze.generator.model.Maze; | import ch.fritteli.maze.generator.model.Maze; | ||||||
| import ch.fritteli.maze.generator.model.Tile; | import ch.fritteli.maze.generator.model.Tile; | ||||||
| import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream; | import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream; | ||||||
|  | import ch.fritteli.maze.generator.serialization.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| public class MazeOutputStreamV1 extends AbstractMazeOutputStream { | public class MazeOutputStreamV1 extends AbstractMazeOutputStream { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void writeHeader() { |     public void writeHeader() { | ||||||
|         this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1); |         // 00 0x1a magic | ||||||
|         this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2); |         // 01 0xb1 magic | ||||||
|  |         // 02 0x02 version | ||||||
|  |         this.writeByte(MazeConstants.MAGIC_BYTE_1); | ||||||
|  |         this.writeByte(MazeConstants.MAGIC_BYTE_2); | ||||||
|         this.writeByte(SerializerDeserializerV1.VERSION_BYTE); |         this.writeByte(SerializerDeserializerV1.VERSION_BYTE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void writeMazeData(@NotNull final Maze maze) { |     public void writeMazeData(@NotNull final Maze maze) { | ||||||
|  |         // 03..10  random seed number (long) | ||||||
|  |         // 11..14  width (int) | ||||||
|  |         // 15..18  height (int) | ||||||
|  |         // 19..    tiles | ||||||
|         final long randomSeed = maze.getRandomSeed(); |         final long randomSeed = maze.getRandomSeed(); | ||||||
|         final int width = maze.getWidth(); |         final int width = maze.getWidth(); | ||||||
|         final int height = maze.getHeight(); |         final int height = maze.getHeight(); | ||||||
|  | @ -27,7 +36,7 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream { | ||||||
|             for (int x = 0; x < width; x++) { |             for (int x = 0; x < width; x++) { | ||||||
|                 // We .get() it, because we want to crash hard if it is not available. |                 // We .get() it, because we want to crash hard if it is not available. | ||||||
|                 final Tile tile = maze.getTileAt(x, y).get(); |                 final Tile tile = maze.getTileAt(x, y).get(); | ||||||
|                 final byte bitmask = SerializerDeserializerV1.getBitmaskForTile(tile); |                 final byte bitmask = CommonTileHandler.getBitmaskForTile(tile); | ||||||
|                 this.writeByte(bitmask); |                 this.writeByte(bitmask); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| package ch.fritteli.maze.generator.serialization.v1; | package ch.fritteli.maze.generator.serialization.v1; | ||||||
| 
 | 
 | ||||||
| import ch.fritteli.maze.generator.model.Direction; |  | ||||||
| import ch.fritteli.maze.generator.model.Maze; | import ch.fritteli.maze.generator.model.Maze; | ||||||
| import ch.fritteli.maze.generator.model.Tile; | import ch.fritteli.maze.generator.model.Tile; | ||||||
| import lombok.experimental.UtilityClass; | import lombok.experimental.UtilityClass; | ||||||
|  | @ -8,53 +7,24 @@ import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| import java.lang.reflect.Constructor; | import java.lang.reflect.Constructor; | ||||||
| import java.lang.reflect.InvocationTargetException; | import java.lang.reflect.InvocationTargetException; | ||||||
| import java.util.EnumSet; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * <pre> |  * Header bytes are: | ||||||
|  * decimal hex bin  border |  | ||||||
|  *       0   0 0000 no border |  | ||||||
|  *       1   1 0001 top |  | ||||||
|  *       2   2 0010 right |  | ||||||
|  *       3   3 0011 top+right |  | ||||||
|  *       4   4 0100 bottom |  | ||||||
|  *       5   5 0101 top+bottom |  | ||||||
|  *       6   6 0110 right+bottom |  | ||||||
|  *       7   7 0111 top+right+bottom |  | ||||||
|  *       8   8 1000 left |  | ||||||
|  *       9   9 1001 top+left |  | ||||||
|  *      10   a 1010 right+left |  | ||||||
|  *      11   b 1011 top+right+left |  | ||||||
|  *      12   c 1100 bottom+left |  | ||||||
|  *      13   d 1101 top+bottom+left |  | ||||||
|  *      14   e 1110 right+bottom+left |  | ||||||
|  *      15   f 1111 top+right+bottom+left |  | ||||||
|  * </pre> |  | ||||||
|  * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls |  | ||||||
|  * ==> first bytes are: |  | ||||||
|  * <pre> |  * <pre> | ||||||
|  *   byte  hex meaning |  *   byte  hex meaning | ||||||
|  *     00 0x1a magic |  *     00 0x1a magic | ||||||
|  *     01 0xb1 magic |  *     01 0xb1 magic | ||||||
|  *     02 0x01 version (0x00 -> dev, 0x01 -> stable) |  *     02 0x01 version (0x00 -> dev, 0x01 -> stable) | ||||||
|  * 03..06      width (int) |  * 03..10      random seed number (long) | ||||||
|  * 07..10      height (int) |  * 11..14      width (int) | ||||||
|  * 11..18      random seed number (long) |  * 15..18      height (int) | ||||||
|  * 19..        tiles |  * 19..        tiles | ||||||
|  * </pre> |  * </pre> | ||||||
|  * Extraneous space (poss. last nibble) is ignored. |  * Extraneous space (poss. last nibble) is ignored. | ||||||
|  */ |  */ | ||||||
| @UtilityClass | @UtilityClass | ||||||
| public class SerializerDeserializerV1 { | public class SerializerDeserializerV1 { | ||||||
|     final byte MAGIC_BYTE_1 = 0x1a; |     public final byte VERSION_BYTE = 0x01; | ||||||
|     final byte MAGIC_BYTE_2 = (byte) 0xb1; |  | ||||||
|     final byte VERSION_BYTE = 0x01; |  | ||||||
| 
 |  | ||||||
|     private final byte TOP_BIT = 0b0000_0001; |  | ||||||
|     private final byte RIGHT_BIT = 0b0000_0010; |  | ||||||
|     private final byte BOTTOM_BIT = 0b0000_0100; |  | ||||||
|     private final byte LEFT_BIT = 0b0000_1000; |  | ||||||
|     private final byte SOLUTION_BIT = 0b0001_0000; |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Serializes the {@code maze} into a byte array. |      * Serializes the {@code maze} into a byte array. | ||||||
|  | @ -94,55 +64,4 @@ public class SerializerDeserializerV1 { | ||||||
|             throw new RuntimeException("Can not deserialize Maze from maze data.", e); |             throw new RuntimeException("Can not deserialize Maze from maze data.", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     @NotNull |  | ||||||
|     private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) { |  | ||||||
|         try { |  | ||||||
|             final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE); |  | ||||||
|             constructor.setAccessible(true); |  | ||||||
|             return constructor.newInstance(walls, solution); |  | ||||||
|         } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException | |  | ||||||
|                                 InvocationTargetException e) { |  | ||||||
|             throw new RuntimeException("Can not deserialize Tile from maze data.", e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     byte getBitmaskForTile(@NotNull final Tile tile) { |  | ||||||
|         byte bitmask = 0; |  | ||||||
|         if (tile.hasWallAt(Direction.TOP)) { |  | ||||||
|             bitmask |= TOP_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.hasWallAt(Direction.RIGHT)) { |  | ||||||
|             bitmask |= RIGHT_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.hasWallAt(Direction.BOTTOM)) { |  | ||||||
|             bitmask |= BOTTOM_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.hasWallAt((Direction.LEFT))) { |  | ||||||
|             bitmask |= LEFT_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.isSolution()) { |  | ||||||
|             bitmask |= SOLUTION_BIT; |  | ||||||
|         } |  | ||||||
|         return bitmask; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NotNull |  | ||||||
|     Tile getTileForBitmask(final byte bitmask) { |  | ||||||
|         final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class); |  | ||||||
|         if ((bitmask & TOP_BIT) == TOP_BIT) { |  | ||||||
|             walls.add(Direction.TOP); |  | ||||||
|         } |  | ||||||
|         if ((bitmask & RIGHT_BIT) == RIGHT_BIT) { |  | ||||||
|             walls.add(Direction.RIGHT); |  | ||||||
|         } |  | ||||||
|         if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) { |  | ||||||
|             walls.add(Direction.BOTTOM); |  | ||||||
|         } |  | ||||||
|         if ((bitmask & LEFT_BIT) == LEFT_BIT) { |  | ||||||
|             walls.add(Direction.LEFT); |  | ||||||
|         } |  | ||||||
|         final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT; |  | ||||||
|         return createTile(walls, solution); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ import ch.fritteli.maze.generator.model.Maze; | ||||||
| import ch.fritteli.maze.generator.model.Position; | import ch.fritteli.maze.generator.model.Position; | ||||||
| import ch.fritteli.maze.generator.model.Tile; | import ch.fritteli.maze.generator.model.Tile; | ||||||
| import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream; | import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream; | ||||||
|  | import ch.fritteli.maze.generator.serialization.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| public class MazeInputStreamV2 extends AbstractMazeInputStream { | public class MazeInputStreamV2 extends AbstractMazeInputStream { | ||||||
|  | @ -18,11 +20,11 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream { | ||||||
|         // 01 0xb1 magic |         // 01 0xb1 magic | ||||||
|         // 02 0x02 version |         // 02 0x02 version | ||||||
|         final byte magic1 = this.readByte(); |         final byte magic1 = this.readByte(); | ||||||
|         if (magic1 != SerializerDeserializerV2.MAGIC_BYTE_1) { |         if (magic1 != MazeConstants.MAGIC_BYTE_1) { | ||||||
|             throw new IllegalArgumentException("Invalid maze data."); |             throw new IllegalArgumentException("Invalid maze data."); | ||||||
|         } |         } | ||||||
|         final byte magic2 = this.readByte(); |         final byte magic2 = this.readByte(); | ||||||
|         if (magic2 != SerializerDeserializerV2.MAGIC_BYTE_2) { |         if (magic2 != MazeConstants.MAGIC_BYTE_2) { | ||||||
|             throw new IllegalArgumentException("Invalid maze data."); |             throw new IllegalArgumentException("Invalid maze data."); | ||||||
|         } |         } | ||||||
|         final int version = this.readByte(); |         final int version = this.readByte(); | ||||||
|  | @ -58,7 +60,7 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream { | ||||||
|         for (int y = 0; y < height; y++) { |         for (int y = 0; y < height; y++) { | ||||||
|             for (int x = 0; x < width; x++) { |             for (int x = 0; x < width; x++) { | ||||||
|                 final byte bitmask = this.readByte(); |                 final byte bitmask = this.readByte(); | ||||||
|                 tiles[x][y] = SerializerDeserializerV2.getTileForBitmask(bitmask); |                 tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ import ch.fritteli.maze.generator.model.Maze; | ||||||
| import ch.fritteli.maze.generator.model.Position; | import ch.fritteli.maze.generator.model.Position; | ||||||
| import ch.fritteli.maze.generator.model.Tile; | import ch.fritteli.maze.generator.model.Tile; | ||||||
| import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream; | import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream; | ||||||
|  | import ch.fritteli.maze.generator.serialization.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| public class MazeOutputStreamV2 extends AbstractMazeOutputStream { | public class MazeOutputStreamV2 extends AbstractMazeOutputStream { | ||||||
|  | @ -13,8 +15,8 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream { | ||||||
|         // 00 0x1a magic |         // 00 0x1a magic | ||||||
|         // 01 0xb1 magic |         // 01 0xb1 magic | ||||||
|         // 02 0x02 version |         // 02 0x02 version | ||||||
|         this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_1); |         this.writeByte(MazeConstants.MAGIC_BYTE_1); | ||||||
|         this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_2); |         this.writeByte(MazeConstants.MAGIC_BYTE_2); | ||||||
|         this.writeByte(SerializerDeserializerV2.VERSION_BYTE); |         this.writeByte(SerializerDeserializerV2.VERSION_BYTE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -45,7 +47,7 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream { | ||||||
|             for (int x = 0; x < width; x++) { |             for (int x = 0; x < width; x++) { | ||||||
|                 // We .get() it, because we want to crash hard if it is not available. |                 // We .get() it, because we want to crash hard if it is not available. | ||||||
|                 final Tile tile = maze.getTileAt(x, y).get(); |                 final Tile tile = maze.getTileAt(x, y).get(); | ||||||
|                 final byte bitmask = SerializerDeserializerV2.getBitmaskForTile(tile); |                 final byte bitmask = CommonTileHandler.getBitmaskForTile(tile); | ||||||
|                 this.writeByte(bitmask); |                 this.writeByte(bitmask); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| package ch.fritteli.maze.generator.serialization.v2; | package ch.fritteli.maze.generator.serialization.v2; | ||||||
| 
 | 
 | ||||||
| import ch.fritteli.maze.generator.model.Direction; |  | ||||||
| import ch.fritteli.maze.generator.model.Maze; | import ch.fritteli.maze.generator.model.Maze; | ||||||
| import ch.fritteli.maze.generator.model.Position; | import ch.fritteli.maze.generator.model.Position; | ||||||
| import ch.fritteli.maze.generator.model.Tile; | import ch.fritteli.maze.generator.model.Tile; | ||||||
|  | @ -9,29 +8,9 @@ import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| import java.lang.reflect.Constructor; | import java.lang.reflect.Constructor; | ||||||
| import java.lang.reflect.InvocationTargetException; | import java.lang.reflect.InvocationTargetException; | ||||||
| import java.util.EnumSet; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * <pre> |  * Header bytes are: | ||||||
|  * decimal hex bin  border |  | ||||||
|  *       0   0 0000 no border |  | ||||||
|  *       1   1 0001 top |  | ||||||
|  *       2   2 0010 right |  | ||||||
|  *       3   3 0011 top+right |  | ||||||
|  *       4   4 0100 bottom |  | ||||||
|  *       5   5 0101 top+bottom |  | ||||||
|  *       6   6 0110 right+bottom |  | ||||||
|  *       7   7 0111 top+right+bottom |  | ||||||
|  *       8   8 1000 left |  | ||||||
|  *       9   9 1001 top+left |  | ||||||
|  *      10   a 1010 right+left |  | ||||||
|  *      11   b 1011 top+right+left |  | ||||||
|  *      12   c 1100 bottom+left |  | ||||||
|  *      13   d 1101 top+bottom+left |  | ||||||
|  *      14   e 1110 right+bottom+left |  | ||||||
|  *      15   f 1111 top+right+bottom+left |  | ||||||
|  * </pre> |  | ||||||
|  * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first bytes are: |  | ||||||
|  * <pre> |  * <pre> | ||||||
|  *   byte  hex meaning |  *   byte  hex meaning | ||||||
|  *     00 0x1a magic |  *     00 0x1a magic | ||||||
|  | @ -50,16 +29,7 @@ import java.util.EnumSet; | ||||||
|  */ |  */ | ||||||
| @UtilityClass | @UtilityClass | ||||||
| public class SerializerDeserializerV2 { | public class SerializerDeserializerV2 { | ||||||
| 
 |     public final byte VERSION_BYTE = 0x02; | ||||||
|     final byte MAGIC_BYTE_1 = 0x1a; |  | ||||||
|     final byte MAGIC_BYTE_2 = (byte) 0xb1; |  | ||||||
|     final byte VERSION_BYTE = 0x02; |  | ||||||
| 
 |  | ||||||
|     private final byte TOP_BIT = 0b0000_0001; |  | ||||||
|     private final byte RIGHT_BIT = 0b0000_0010; |  | ||||||
|     private final byte BOTTOM_BIT = 0b0000_0100; |  | ||||||
|     private final byte LEFT_BIT = 0b0000_1000; |  | ||||||
|     private final byte SOLUTION_BIT = 0b0001_0000; |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Serializes the {@code maze} into a byte array. |      * Serializes the {@code maze} into a byte array. | ||||||
|  | @ -99,55 +69,4 @@ public class SerializerDeserializerV2 { | ||||||
|             throw new RuntimeException("Can not deserialize Maze from maze data.", e); |             throw new RuntimeException("Can not deserialize Maze from maze data.", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     @NotNull |  | ||||||
|     private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) { |  | ||||||
|         try { |  | ||||||
|             final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE); |  | ||||||
|             constructor.setAccessible(true); |  | ||||||
|             return constructor.newInstance(walls, solution); |  | ||||||
|         } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException | |  | ||||||
|                                 InvocationTargetException e) { |  | ||||||
|             throw new RuntimeException("Can not deserialize Tile from maze data.", e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     byte getBitmaskForTile(@NotNull final Tile tile) { |  | ||||||
|         byte bitmask = 0; |  | ||||||
|         if (tile.hasWallAt(Direction.TOP)) { |  | ||||||
|             bitmask |= TOP_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.hasWallAt(Direction.RIGHT)) { |  | ||||||
|             bitmask |= RIGHT_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.hasWallAt(Direction.BOTTOM)) { |  | ||||||
|             bitmask |= BOTTOM_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.hasWallAt((Direction.LEFT))) { |  | ||||||
|             bitmask |= LEFT_BIT; |  | ||||||
|         } |  | ||||||
|         if (tile.isSolution()) { |  | ||||||
|             bitmask |= SOLUTION_BIT; |  | ||||||
|         } |  | ||||||
|         return bitmask; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NotNull |  | ||||||
|     Tile getTileForBitmask(final byte bitmask) { |  | ||||||
|         final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class); |  | ||||||
|         if ((bitmask & TOP_BIT) == TOP_BIT) { |  | ||||||
|             walls.add(Direction.TOP); |  | ||||||
|         } |  | ||||||
|         if ((bitmask & RIGHT_BIT) == RIGHT_BIT) { |  | ||||||
|             walls.add(Direction.RIGHT); |  | ||||||
|         } |  | ||||||
|         if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) { |  | ||||||
|             walls.add(Direction.BOTTOM); |  | ||||||
|         } |  | ||||||
|         if ((bitmask & LEFT_BIT) == LEFT_BIT) { |  | ||||||
|             walls.add(Direction.LEFT); |  | ||||||
|         } |  | ||||||
|         final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT; |  | ||||||
|         return createTile(walls, solution); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,79 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization.v3; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import ch.fritteli.maze.generator.model.Tile; | ||||||
|  | import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream; | ||||||
|  | import ch.fritteli.maze.generator.serialization.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | 
 | ||||||
|  | public class MazeInputStreamV3 extends AbstractMazeInputStream { | ||||||
|  | 
 | ||||||
|  |     public MazeInputStreamV3(@NotNull final byte[] buf) { | ||||||
|  |         super(buf); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void checkHeader() { | ||||||
|  |         // 00 0x1a magic | ||||||
|  |         // 01 0xb1 magic | ||||||
|  |         // 02 0x03 version | ||||||
|  |         final byte magic1 = this.readByte(); | ||||||
|  |         if (magic1 != MazeConstants.MAGIC_BYTE_1) { | ||||||
|  |             throw new IllegalArgumentException("Invalid maze data."); | ||||||
|  |         } | ||||||
|  |         final byte magic2 = this.readByte(); | ||||||
|  |         if (magic2 != MazeConstants.MAGIC_BYTE_2) { | ||||||
|  |             throw new IllegalArgumentException("Invalid maze data."); | ||||||
|  |         } | ||||||
|  |         final int version = this.readByte(); | ||||||
|  |         if (version != SerializerDeserializerV3.VERSION_BYTE) { | ||||||
|  |             throw new IllegalArgumentException("Unknown maze data version: " + version); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     @Override | ||||||
|  |     public Maze readMazeData() throws IOException { | ||||||
|  |         // 03..06   width (int) | ||||||
|  |         // 07..10   height (int) | ||||||
|  |         // 11..14   start-x (int) | ||||||
|  |         // 15..18   start-y (int) | ||||||
|  |         // 19..22   end-x (int) | ||||||
|  |         // 23..26   end-y (int) | ||||||
|  |         // 27..34   random seed number (long) | ||||||
|  |         // 35       length of the algorithm's name (unsigned byte) | ||||||
|  |         // 36..+len name (bytes of String) | ||||||
|  |         // +len+1.. tiles | ||||||
|  |         final int width = this.readInt(); | ||||||
|  |         final int height = this.readInt(); | ||||||
|  |         final int startX = this.readInt(); | ||||||
|  |         final int startY = this.readInt(); | ||||||
|  |         final int endX = this.readInt(); | ||||||
|  |         final int endY = this.readInt(); | ||||||
|  |         final long randomSeed = this.readLong(); | ||||||
|  |         final int algorithmLength = this.readByteAsInt(); | ||||||
|  | 
 | ||||||
|  |         final String algorithm = new String(this.readNBytes(algorithmLength), StandardCharsets.UTF_8); | ||||||
|  | 
 | ||||||
|  |         final Tile[][] tiles = new Tile[width][height]; | ||||||
|  |         for (int x = 0; x < width; x++) { | ||||||
|  |             tiles[x] = new Tile[height]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (int y = 0; y < height; y++) { | ||||||
|  |             for (int x = 0; x < width; x++) { | ||||||
|  |                 final byte bitmask = this.readByte(); | ||||||
|  |                 tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final Position start = new Position(startX, startY); | ||||||
|  |         final Position end = new Position(endX, endY); | ||||||
|  |         return SerializerDeserializerV3.createMaze(tiles, width, height, start, end, randomSeed, algorithm); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization.v3; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import ch.fritteli.maze.generator.model.Tile; | ||||||
|  | import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream; | ||||||
|  | import ch.fritteli.maze.generator.serialization.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | 
 | ||||||
|  | public class MazeOutputStreamV3 extends AbstractMazeOutputStream { | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void writeHeader() { | ||||||
|  |         // 00 0x1a magic | ||||||
|  |         // 01 0xb1 magic | ||||||
|  |         // 02 0x03 version | ||||||
|  |         this.writeByte(MazeConstants.MAGIC_BYTE_1); | ||||||
|  |         this.writeByte(MazeConstants.MAGIC_BYTE_2); | ||||||
|  |         this.writeByte(SerializerDeserializerV3.VERSION_BYTE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void writeMazeData(@NotNull final Maze maze) { | ||||||
|  |         // 03..06   width (int) | ||||||
|  |         // 07..10   height (int) | ||||||
|  |         // 11..14   start-x (int) | ||||||
|  |         // 15..18   start-y (int) | ||||||
|  |         // 19..22   end-x (int) | ||||||
|  |         // 23..26   end-y (int) | ||||||
|  |         // 27..34   random seed number (long) | ||||||
|  |         // 35       length of the algorithm's name (unsigned byte) | ||||||
|  |         // 36..+len name (bytes of String) | ||||||
|  |         // +len+1.. tiles | ||||||
|  |         final long randomSeed = maze.getRandomSeed(); | ||||||
|  |         final AlgorithmWrapper algorithm = this.getAlgorithmWrapper(maze.getAlgorithm()); | ||||||
|  |         final int width = maze.getWidth(); | ||||||
|  |         final int height = maze.getHeight(); | ||||||
|  |         final Position start = maze.getStart(); | ||||||
|  |         final Position end = maze.getEnd(); | ||||||
|  |         this.writeInt(width); | ||||||
|  |         this.writeInt(height); | ||||||
|  |         this.writeInt(start.x()); | ||||||
|  |         this.writeInt(start.y()); | ||||||
|  |         this.writeInt(end.x()); | ||||||
|  |         this.writeInt(end.y()); | ||||||
|  |         this.writeLong(randomSeed); | ||||||
|  |         this.writeByte(algorithm.length()); | ||||||
|  |         this.writeBytes(algorithm.name()); | ||||||
|  | 
 | ||||||
|  |         for (int y = 0; y < height; y++) { | ||||||
|  |             for (int x = 0; x < width; x++) { | ||||||
|  |                 // We .get() it, because we want to crash hard if it is not available. | ||||||
|  |                 final Tile tile = maze.getTileAt(x, y).get(); | ||||||
|  |                 final byte bitmask = CommonTileHandler.getBitmaskForTile(tile); | ||||||
|  |                 this.writeByte(bitmask); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     private AlgorithmWrapper getAlgorithmWrapper(@NotNull final String algorithm) { | ||||||
|  |         final byte[] bytes = algorithm.getBytes(StandardCharsets.UTF_8); | ||||||
|  |         if (bytes.length < 256) { | ||||||
|  |             // Phew, that's the easy case! | ||||||
|  |             return new AlgorithmWrapper(bytes, (byte) bytes.length); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Let's use a very primitive, brute-force approach | ||||||
|  |         int strLen = Math.min(255, algorithm.length()); | ||||||
|  |         int len; | ||||||
|  |         byte[] name; | ||||||
|  |         do { | ||||||
|  |             name = algorithm.substring(0, strLen).getBytes(StandardCharsets.UTF_8); | ||||||
|  |             len = name.length; | ||||||
|  |             strLen--; | ||||||
|  |         } while (len > 255); | ||||||
|  | 
 | ||||||
|  |         return new AlgorithmWrapper(name, (byte) len); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private record AlgorithmWrapper(@NotNull byte[] name, byte length) { | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,89 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization.v3; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import ch.fritteli.maze.generator.model.Tile; | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.lang.reflect.Constructor; | ||||||
|  | import java.lang.reflect.InvocationTargetException; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Header bytes are: | ||||||
|  |  * <pre> | ||||||
|  |  *   byte     hex    meaning | ||||||
|  |  *     00     0x1a   magic | ||||||
|  |  *     01     0xb1   magic | ||||||
|  |  *     02     0x03   version (0x00 -> dev, 0x01, 0x02 -> deprecated, 0x03 -> stable) | ||||||
|  |  *     03..06        width (int) | ||||||
|  |  *     07..10        height (int) | ||||||
|  |  *     11..14        start-x (int) | ||||||
|  |  *     15..18        start-y (int) | ||||||
|  |  *     19..22        end-x (int) | ||||||
|  |  *     23..26        end-y (int) | ||||||
|  |  *     27..34        random seed number (long) | ||||||
|  |  *     35            length of the algorithm's name (number of bytes of the Java String) (unsigned byte) | ||||||
|  |  *     36..(36+len)  algorithm's name (bytes of the Java String) (byte...) | ||||||
|  |  *     36+len+1..    tiles | ||||||
|  |  * </pre> | ||||||
|  |  * Extraneous space (poss. last nibble) is ignored. | ||||||
|  |  */ | ||||||
|  | @UtilityClass | ||||||
|  | public class SerializerDeserializerV3 { | ||||||
|  |     public final byte VERSION_BYTE = 0x03; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Serializes the {@code maze} into a byte array. | ||||||
|  |      * | ||||||
|  |      * @param maze The {@link Maze} to be serialized. | ||||||
|  |      * @return The resulting byte array. | ||||||
|  |      */ | ||||||
|  |     @NotNull | ||||||
|  |     public byte[] serialize(@NotNull final Maze maze) { | ||||||
|  |         final MazeOutputStreamV3 stream = new MazeOutputStreamV3(); | ||||||
|  |         stream.writeHeader(); | ||||||
|  |         stream.writeMazeData(maze); | ||||||
|  |         return stream.toByteArray(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Deserializes the byte array into an instance of {@link Maze}. | ||||||
|  |      * | ||||||
|  |      * @param bytes The byte array to be deserialized. | ||||||
|  |      * @return An instance of {@link Maze}. | ||||||
|  |      */ | ||||||
|  |     @NotNull | ||||||
|  |     public Maze deserialize(@NotNull final byte[] bytes) throws IOException { | ||||||
|  |         final MazeInputStreamV3 stream = new MazeInputStreamV3(bytes); | ||||||
|  |         stream.checkHeader(); | ||||||
|  |         return stream.readMazeData(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NotNull | ||||||
|  |     Maze createMaze(@NotNull final Tile[][] field, | ||||||
|  |                     final int width, | ||||||
|  |                     final int height, | ||||||
|  |                     @NotNull final Position start, | ||||||
|  |                     @NotNull final Position end, | ||||||
|  |                     final long randomSeed, | ||||||
|  |                     @NotNull final String algorithm) { | ||||||
|  |         try { | ||||||
|  |             final Constructor<Maze> constructor = Maze.class.getDeclaredConstructor( | ||||||
|  |                     Tile[][].class, | ||||||
|  |                     Integer.TYPE, | ||||||
|  |                     Integer.TYPE, | ||||||
|  |                     Position.class, | ||||||
|  |                     Position.class, | ||||||
|  |                     Long.TYPE, | ||||||
|  |                     String.class | ||||||
|  |             ); | ||||||
|  |             constructor.setAccessible(true); | ||||||
|  |             return constructor.newInstance(field, width, height, start, end, randomSeed, algorithm); | ||||||
|  |         } catch (@NotNull final NoSuchMethodException | IllegalAccessException | InstantiationException | | ||||||
|  |                                 InvocationTargetException e) { | ||||||
|  |             throw new RuntimeException("Can not deserialize Maze from maze data.", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
|   "additionalProperties": false, |   "additionalProperties": false, | ||||||
|   "required": [ |   "required": [ | ||||||
|     "id", |     "id", | ||||||
|  |     "algorithm", | ||||||
|     "width", |     "width", | ||||||
|     "height", |     "height", | ||||||
|     "start", |     "start", | ||||||
|  | @ -17,6 +18,10 @@ | ||||||
|       "type": "string", |       "type": "string", | ||||||
|       "description": "64 bit precision signed integer value. Transmitted as string, because ECMAScript (browsers) don't normally handle 64 bit integers well, as the ECMAScript 'number' type is a 64 bit signed double value, leaving only 53 bits for the integer part, thus losing precision." |       "description": "64 bit precision signed integer value. Transmitted as string, because ECMAScript (browsers) don't normally handle 64 bit integers well, as the ECMAScript 'number' type is a 64 bit signed double value, leaving only 53 bits for the integer part, thus losing precision." | ||||||
|     }, |     }, | ||||||
|  |     "algorithm": { | ||||||
|  |       "type": "string", | ||||||
|  |       "description": "The name of the algorithm used to generate the maze." | ||||||
|  |     }, | ||||||
|     "width": { |     "width": { | ||||||
|       "type": "integer", |       "type": "integer", | ||||||
|       "minimum": 1 |       "minimum": 1 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | package ch.fritteli.maze.generator.algorithm.wilson; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.renderer.text.TextRenderer; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | 
 | ||||||
|  | class WilsonTest { | ||||||
|  |     @Test | ||||||
|  |     void testTinyGeneration() { | ||||||
|  |         // arrange | ||||||
|  |         final Maze maze = new Maze(3, 3, 0); | ||||||
|  |         final Wilson wilson = new Wilson(maze); | ||||||
|  | 
 | ||||||
|  |         // act | ||||||
|  |         wilson.run(); | ||||||
|  | 
 | ||||||
|  |         // assert | ||||||
|  |         final String textRepresentation = TextRenderer.newInstance().render(maze); | ||||||
|  |         assertThat(textRepresentation).isEqualTo(""" | ||||||
|  |                 ╷ ╶───┐ | ||||||
|  |                 │     │ | ||||||
|  |                 │ ┌─┐ │ | ||||||
|  |                 │ │ │ │ | ||||||
|  |                 │ ╵ │ │ | ||||||
|  |                 │   │ │ | ||||||
|  |                 └───┘ ╵"""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSimpleGeneration() { | ||||||
|  |         // arrange | ||||||
|  |         final Maze maze = new Maze(10, 10, 0); | ||||||
|  |         final Wilson wilson = new Wilson(maze); | ||||||
|  | 
 | ||||||
|  |         // act | ||||||
|  |         wilson.run(); | ||||||
|  | 
 | ||||||
|  |         // assert | ||||||
|  |         final String textRepresentation = TextRenderer.newInstance().render(maze); | ||||||
|  |         assertThat(textRepresentation).isEqualTo(""" | ||||||
|  |                 ╷ ╶─────┬─────┬─────┐ | ||||||
|  |                 │       │     │     │ | ||||||
|  |                 ├─╴ ╷ ╶─┴─┬─╴ ├─┐ ╶─┤ | ||||||
|  |                 │   │     │   │ │   │ | ||||||
|  |                 │ ╷ └─┐ ╶─┘ ┌─┘ ╵ ╷ │ | ||||||
|  |                 │ │   │     │     │ │ | ||||||
|  |                 │ │ ╶─┤ ╶─┐ ╵ ╷ ╶─┤ │ | ||||||
|  |                 │ │   │   │   │   │ │ | ||||||
|  |                 │ ├─┐ ├─╴ └─┐ │ ╶─┼─┤ | ||||||
|  |                 │ │ │ │     │ │   │ │ | ||||||
|  |                 ├─┘ └─┘ ╶─┐ └─┤ ╶─┤ │ | ||||||
|  |                 │         │   │   │ │ | ||||||
|  |                 ├───┐ ╷ ╶─┴───┴─┐ │ │ | ||||||
|  |                 │   │ │         │ │ │ | ||||||
|  |                 ├─╴ └─┼─╴ ╷ ╷ ┌─┴─┘ │ | ||||||
|  |                 │     │   │ │ │     │ | ||||||
|  |                 │ ╷ ╶─┴─┬─┤ ├─┘ ╶─┐ │ | ||||||
|  |                 │ │     │ │ │     │ │ | ||||||
|  |                 │ ├─╴ ╷ ╵ ╵ ╵ ┌─╴ │ │ | ||||||
|  |                 │ │   │       │   │ │ | ||||||
|  |                 └─┴───┴───────┴───┘ ╵"""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSimpleGenerationWithSolution() { | ||||||
|  |         // arrange | ||||||
|  |         final Maze maze = new Maze(10, 10, 0); | ||||||
|  |         final Wilson wilson = new Wilson(maze); | ||||||
|  | 
 | ||||||
|  |         // act | ||||||
|  |         wilson.run(); | ||||||
|  | 
 | ||||||
|  |         // assert | ||||||
|  |         final String textRepresentation = TextRenderer.newInstance().setRenderSolution(true).render(maze); | ||||||
|  |         assertThat(textRepresentation).isEqualTo(""" | ||||||
|  |                 ╷│╶─────┬─────┬─────┐ | ||||||
|  |                 │╰───╮  │     │     │ | ||||||
|  |                 ├─╴ ╷│╶─┴─┬─╴ ├─┐ ╶─┤ | ||||||
|  |                 │   │╰─╮  │   │ │   │ | ||||||
|  |                 │ ╷ └─┐│╶─┘ ┌─┘ ╵ ╷ │ | ||||||
|  |                 │ │   ││    │     │ │ | ||||||
|  |                 │ │ ╶─┤│╶─┐ ╵ ╷ ╶─┤ │ | ||||||
|  |                 │ │   │╰─╮│   │   │ │ | ||||||
|  |                 │ ├─┐ ├─╴│└─┐ │ ╶─┼─┤ | ||||||
|  |                 │ │ │ │╭─╯  │ │   │ │ | ||||||
|  |                 ├─┘ └─┘│╶─┐ └─┤ ╶─┤ │ | ||||||
|  |                 │      │  │   │   │ │ | ||||||
|  |                 ├───┐ ╷│╶─┴───┴─┐ │ │ | ||||||
|  |                 │   │ │╰───╮    │ │ │ | ||||||
|  |                 ├─╴ └─┼─╴ ╷│╷ ┌─┴─┘ │ | ||||||
|  |                 │     │   │││ │╭───╮│ | ||||||
|  |                 │ ╷ ╶─┴─┬─┤│├─┘│╶─┐││ | ||||||
|  |                 │ │     │ │││╭─╯  │││ | ||||||
|  |                 │ ├─╴ ╷ ╵ ╵│╵│┌─╴ │││ | ||||||
|  |                 │ │   │    ╰─╯│   │││ | ||||||
|  |                 └─┴───┴───────┴───┘│╵"""); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,6 +3,8 @@ package ch.fritteli.maze.generator.model; | ||||||
| import io.vavr.collection.Stream; | import io.vavr.collection.Stream; | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| 
 | 
 | ||||||
|  | import java.util.EnumSet; | ||||||
|  | 
 | ||||||
| import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||||
| import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||||||
| 
 | 
 | ||||||
|  | @ -113,7 +115,7 @@ class WallsTest { | ||||||
|         final Walls sut = new Walls(); |         final Walls sut = new Walls(); | ||||||
| 
 | 
 | ||||||
|         // act |         // act | ||||||
|         Stream<Direction> result = sut.getUnsealedSet(); |         EnumSet<Direction> result = sut.getUnsealedSet(); | ||||||
| 
 | 
 | ||||||
|         // assert |         // assert | ||||||
|         assertThat(result).isEmpty(); |         assertThat(result).isEmpty(); | ||||||
|  |  | ||||||
|  | @ -13,6 +13,8 @@ class SerializerDeserializerV1Test { | ||||||
|         new RandomDepthFirst(expected).run(); |         new RandomDepthFirst(expected).run(); | ||||||
|         final byte[] bytes = SerializerDeserializerV1.serialize(expected); |         final byte[] bytes = SerializerDeserializerV1.serialize(expected); | ||||||
|         final Maze result = SerializerDeserializerV1.deserialize(bytes); |         final Maze result = SerializerDeserializerV1.deserialize(bytes); | ||||||
|  |         assertThat(result.getAlgorithm()).isNull(); | ||||||
|  |         expected.setAlgorithm(null); | ||||||
|         assertThat(result).isEqualTo(expected); |         assertThat(result).isEqualTo(expected); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -22,6 +24,8 @@ class SerializerDeserializerV1Test { | ||||||
|         new RandomDepthFirst(expected).run(); |         new RandomDepthFirst(expected).run(); | ||||||
|         final byte[] bytes = SerializerDeserializerV1.serialize(expected); |         final byte[] bytes = SerializerDeserializerV1.serialize(expected); | ||||||
|         final Maze result = SerializerDeserializerV1.deserialize(bytes); |         final Maze result = SerializerDeserializerV1.deserialize(bytes); | ||||||
|  |         assertThat(result.getAlgorithm()).isNull(); | ||||||
|  |         expected.setAlgorithm(null); | ||||||
|         assertThat(result).isEqualTo(expected); |         assertThat(result).isEqualTo(expected); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -31,6 +35,8 @@ class SerializerDeserializerV1Test { | ||||||
|         new RandomDepthFirst(expected).run(); |         new RandomDepthFirst(expected).run(); | ||||||
|         final byte[] bytes = SerializerDeserializerV1.serialize(expected); |         final byte[] bytes = SerializerDeserializerV1.serialize(expected); | ||||||
|         final Maze result = SerializerDeserializerV1.deserialize(bytes); |         final Maze result = SerializerDeserializerV1.deserialize(bytes); | ||||||
|  |         assertThat(result.getAlgorithm()).isNull(); | ||||||
|  |         expected.setAlgorithm(null); | ||||||
|         assertThat(result).isEqualTo(expected); |         assertThat(result).isEqualTo(expected); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization.v2; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; | ||||||
|  | import ch.fritteli.maze.generator.algorithm.wilson.Wilson; | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | 
 | ||||||
|  | class SerializerDeserializerV2Test { | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeTiny() throws IOException { | ||||||
|  |         final Maze expected = new Maze(2, 2, 255, new Position(0, 0), new Position(1, 1)); | ||||||
|  |         new RandomDepthFirst(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV2.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV2.deserialize(bytes); | ||||||
|  |         assertThat(result.getAlgorithm()).isNull(); | ||||||
|  |         expected.setAlgorithm(null); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeMedium() throws IOException { | ||||||
|  |         final Maze expected = new Maze(20, 20, -271828182846L); | ||||||
|  |         new Wilson(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV2.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV2.deserialize(bytes); | ||||||
|  |         assertThat(result.getAlgorithm()).isNull(); | ||||||
|  |         expected.setAlgorithm(null); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeLarge() throws IOException { | ||||||
|  |         final Maze expected = new Maze(200, 320, 3141592653589793238L); | ||||||
|  |         new Wilson(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV2.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV2.deserialize(bytes); | ||||||
|  |         assertThat(result.getAlgorithm()).isNull(); | ||||||
|  |         expected.setAlgorithm(null); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,49 @@ | ||||||
|  | package ch.fritteli.maze.generator.serialization.v3; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; | ||||||
|  | import ch.fritteli.maze.generator.algorithm.wilson.Wilson; | ||||||
|  | import ch.fritteli.maze.generator.model.Maze; | ||||||
|  | import ch.fritteli.maze.generator.model.Position; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | 
 | ||||||
|  | class SerializerDeserializerV3Test { | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeTiny() throws IOException { | ||||||
|  |         final Maze expected = new Maze(2, 2, 255, new Position(0, 0), new Position(1, 1)); | ||||||
|  |         new RandomDepthFirst(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV3.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV3.deserialize(bytes); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeMedium() throws IOException { | ||||||
|  |         final Maze expected = new Maze(20, 20, -271828182846L); | ||||||
|  |         new Wilson(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV3.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV3.deserialize(bytes); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeLarge() throws IOException { | ||||||
|  |         final Maze expected = new Maze(200, 320, 3141592653589793238L); | ||||||
|  |         new Wilson(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV3.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV3.deserialize(bytes); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeLargeRandom() throws IOException { | ||||||
|  |         final Maze expected = new Maze(200, 320, 3141592653589793238L); | ||||||
|  |         new RandomDepthFirst(expected).run(); | ||||||
|  |         final byte[] bytes = SerializerDeserializerV3.serialize(expected); | ||||||
|  |         final Maze result = SerializerDeserializerV3.deserialize(bytes); | ||||||
|  |         assertThat(result).isEqualTo(expected); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue