feature/wilson-algorithm #10
					 23 changed files with 557 additions and 396 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> | ||||||
|  |  | ||||||
|  | @ -1,312 +0,0 @@ | ||||||
| package ch.fritteli.maze.generator.algorithm; |  | ||||||
| 
 |  | ||||||
| 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.Tuple2; |  | ||||||
| import io.vavr.collection.Iterator; |  | ||||||
| import io.vavr.collection.Seq; |  | ||||||
| import io.vavr.collection.Stream; |  | ||||||
| import io.vavr.collection.Vector; |  | ||||||
| import io.vavr.control.Option; |  | ||||||
| import org.jetbrains.annotations.NotNull; |  | ||||||
| import org.jetbrains.annotations.Nullable; |  | ||||||
| 
 |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.EnumSet; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Random; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * An implementation of <a href="https://en.wikipedia.org/wiki/Maze_generation_algorithm#Wilson's_algorithm">Wilson's Algorithm</a>. |  | ||||||
|  */ |  | ||||||
| public class Wilson extends AbstractMazeGeneratorAlgorithm { |  | ||||||
|     public Wilson(@NotNull final Maze maze) { |  | ||||||
|         super(maze, "Wilson"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void run() { |  | ||||||
|         final MyMaze myMaze = new MyMaze(this.maze.getWidth(), this.maze.getHeight()); |  | ||||||
|         // 1. Initialization: pick random location, add to maze |  | ||||||
|         final Position position = myMaze.getRandomAvailablePosition(this.random).get(); |  | ||||||
|         myMaze.setPartOfMaze(position); |  | ||||||
| 
 |  | ||||||
|         // 2. while locations-not-in-maze exist, repeat: |  | ||||||
| 
 |  | ||||||
|         final List<MyMaze.Path> pths = new ArrayList<>(); |  | ||||||
|         Position startPosition; |  | ||||||
|         while ((startPosition = myMaze.getRandomAvailablePosition(this.random).getOrNull()) != null) { |  | ||||||
|             final MyMaze.Path path = myMaze.createPath(startPosition); |  | ||||||
|             while (true) { |  | ||||||
|                 final Position nextPosition = path.nextRandomPosition(this.random); |  | ||||||
|                 if (path.contains(nextPosition)) { |  | ||||||
|                     path.removeLoopUpTo(nextPosition); |  | ||||||
|                 } else { |  | ||||||
|                     path.append(nextPosition); |  | ||||||
|                     if (myMaze.isPartOfMaze(nextPosition)) { |  | ||||||
|                         myMaze.setPartOfMaze(path); |  | ||||||
|                         pths.add(path); |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         applyToMaze(pths, maze); |  | ||||||
|         solve(maze); |  | ||||||
|         //    1. pick random location not in maze |  | ||||||
|         //    2. perform random walk until you reach the maze (*) |  | ||||||
|         //    3. add path to maze |  | ||||||
|         // (*): random walk: |  | ||||||
|         //    1. advance in random direction |  | ||||||
|         //    2. if loop with current path is formed, remove loop, continue from last location before loop |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void applyToMaze(@NotNull final List<MyMaze.Path> pths, @NotNull final Maze maze) { |  | ||||||
|         pths.forEach(path -> { |  | ||||||
|             final Iterator<Direction> moves = Stream.ofAll(path.path) |  | ||||||
|                     .sliding(2) |  | ||||||
|                     .flatMap(poss -> poss.head().getDirectionTo(poss.get(1))); |  | ||||||
|             Position position = path.path.getFirst(); |  | ||||||
|             while (moves.hasNext()) { |  | ||||||
|                 Tile tile = maze.getTileAt(position).get(); |  | ||||||
|                 final Direction move = moves.next(); |  | ||||||
|                 tile.digTo(move); |  | ||||||
|                 position = position.move(move); |  | ||||||
|                 maze.getTileAt(position).forEach(t -> t.digTo(move.invert())); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         Direction direction = determineDirectionForDigging(maze.getStart()); |  | ||||||
|         Tile t = maze.getStartTile(); |  | ||||||
|         this.digTo(t, direction); |  | ||||||
|         direction = determineDirectionForDigging(maze.getEnd()); |  | ||||||
|         t = maze.getEndTile(); |  | ||||||
|         this.digTo(t, direction); |  | ||||||
|         // seal all walls, mark all as visited |  | ||||||
|         for (int x = 0; x < maze.getWidth(); x++) { |  | ||||||
|             for (int y = 0; y < maze.getHeight(); y++) { |  | ||||||
|                 maze.getTileAt(x, y).forEach(tile -> { |  | ||||||
|                     Stream.of(Direction.values()) |  | ||||||
|                             .forEach(d -> { |  | ||||||
|                                 if (tile.hasWallAt(d)) { |  | ||||||
|                                     tile.preventDiggingToOrFrom(d); |  | ||||||
|                                 } else { |  | ||||||
|                                     tile.digFrom(d); |  | ||||||
|                                 } |  | ||||||
|                             }); |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     private Direction determineDirectionForDigging(@NotNull final Position position) { |  | ||||||
|         if (position.y() == 0) { |  | ||||||
|             return Direction.TOP; |  | ||||||
|         } |  | ||||||
|         if (position.x() == 0) { |  | ||||||
|             return Direction.LEFT; |  | ||||||
|         } |  | ||||||
|         if (position.y() == this.maze.getHeight() - 1) { |  | ||||||
|             return Direction.BOTTOM; |  | ||||||
|         } |  | ||||||
|         if (position.x() == this.maze.getWidth() - 1) { |  | ||||||
|             return Direction.RIGHT; |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void digTo(@NotNull final Tile tile, @NotNull final Direction direction) { |  | ||||||
|         tile.enableDiggingToOrFrom(direction); |  | ||||||
|         tile.digTo(direction); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void solve(@NotNull final Maze maze) { |  | ||||||
|         EnumSet<Direction>[][] remainingDirs = new EnumSet[maze.getWidth()][maze.getHeight()]; |  | ||||||
|         for (int x = 0; x < remainingDirs.length; x++) { |  | ||||||
|             for (int y = 0; y < remainingDirs[x].length; y++) { |  | ||||||
|                 remainingDirs[x][y] = EnumSet.allOf(Direction.class); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         Position p = maze.getStart(); |  | ||||||
|         final Direction direction = this.determineDirectionForDigging(p); |  | ||||||
|         remainingDirs[p.x()][p.y()].remove(direction); |  | ||||||
|         LinkedList<Position> solution = new LinkedList<>(); |  | ||||||
|         solution.add(p); |  | ||||||
|         while (!p.equals(maze.getEnd())) { |  | ||||||
|             final Tile tile = maze.getTileAt(p).get(); |  | ||||||
|             EnumSet<Direction> dirs = remainingDirs[p.x()][p.y()]; |  | ||||||
|             dirs.removeIf(tile::hasWallAt); |  | ||||||
|             if (dirs.isEmpty()) { |  | ||||||
|                 solution.pop(); |  | ||||||
|                 p = solution.peek(); |  | ||||||
|             } else { |  | ||||||
|                 final Direction nextDir = dirs.iterator().next(); |  | ||||||
|                 final Position nextPos = p.move(nextDir); |  | ||||||
|                 solution.push(nextPos); |  | ||||||
|                 remainingDirs[p.x()][p.y()].remove(nextDir); |  | ||||||
|                 remainingDirs[nextPos.x()][nextPos.y()].remove(nextDir.invert()); |  | ||||||
|                 p = nextPos; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         solution.forEach(s -> maze.getTileAt(s).forEach(Tile::setSolution)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static class MyMaze { |  | ||||||
|         private final int width; |  | ||||||
|         private final int height; |  | ||||||
|         private final boolean[][] partOfMaze; |  | ||||||
|         private final boolean[] completeColumns; |  | ||||||
| 
 |  | ||||||
|         MyMaze(final int width, final int height) { |  | ||||||
|             this.width = width; |  | ||||||
|             this.height = height; |  | ||||||
|             this.partOfMaze = new boolean[this.width][this.height]; |  | ||||||
|             for (int x = 0; x < this.width; x++) { |  | ||||||
|                 this.partOfMaze[x] = new boolean[this.height]; |  | ||||||
|             } |  | ||||||
|             this.completeColumns = new boolean[this.width]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         boolean isPartOfMaze(final int x, final int y) { |  | ||||||
|             return this.partOfMaze[x][y]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         boolean isPartOfMaze(@NotNull final Position position) { |  | ||||||
|             return this.isPartOfMaze(position.x(), position.y()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         void setPartOfMaze(final int x, final int y) { |  | ||||||
|             this.partOfMaze[x][y] = true; |  | ||||||
|             this.checkCompleteColumn(x); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         void setPartOfMaze(@NotNull final Position position) { |  | ||||||
|             this.setPartOfMaze(position.x(), position.y()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         void setPartOfMaze(@NotNull final Path path) { |  | ||||||
|             path.path.forEach(this::setPartOfMaze); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         void checkCompleteColumn(final int x) { |  | ||||||
|             if (this.completeColumns[x]) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             for (int y = 0; y < this.height; y++) { |  | ||||||
|                 if (!this.isPartOfMaze(x, y)) { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             this.completeColumns[x] = true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Option<Position> getRandomAvailablePosition(@NotNull final Random random) { |  | ||||||
|             final Seq<Integer> allowedColumns = Vector.ofAll(this.completeColumns) |  | ||||||
|                     .zipWithIndex() |  | ||||||
|                     .reject(Tuple2::_1) |  | ||||||
|                     .map(Tuple2::_2); |  | ||||||
|             if (allowedColumns.isEmpty()) { |  | ||||||
|                 return Option.none(); |  | ||||||
|             } |  | ||||||
|             final int x = allowedColumns.get(random.nextInt(allowedColumns.size())); |  | ||||||
|             final boolean[] column = partOfMaze[x]; |  | ||||||
| 
 |  | ||||||
|             final Seq<Integer> allowedRows = Vector.ofAll(column) |  | ||||||
|                     .zipWithIndex() |  | ||||||
|                     .reject(Tuple2::_1) |  | ||||||
|                     .map(Tuple2::_2); |  | ||||||
|             if (allowedRows.isEmpty()) { |  | ||||||
|                 return Option.none(); |  | ||||||
|             } |  | ||||||
|             final int y = allowedRows.get(random.nextInt(allowedRows.size())); |  | ||||||
|             return Option.some(new Position(x, y)); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public Path createPath(@NotNull final Position position) { |  | ||||||
|             return new Path(position); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private class Path { |  | ||||||
|             @NotNull |  | ||||||
|             private final List<Position> path = new LinkedList<>(); |  | ||||||
| 
 |  | ||||||
|             Path(@NotNull final Position position) { |  | ||||||
|                 this.path.add(position); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @NotNull |  | ||||||
|             Position nextRandomPosition(@NotNull final Random random) { |  | ||||||
|                 final Direction direction = this.getRandomDirectionFromLastPosition(random); |  | ||||||
|                 return this.path.getLast().move(direction); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             boolean contains(@NotNull final Position position) { |  | ||||||
|                 return this.path.contains(position); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             void removeLoopUpTo(@NotNull final Position position) { |  | ||||||
|                 while (!this.path.removeLast().equals(position)) { |  | ||||||
|                 } |  | ||||||
|                 this.path.add(position); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             public void append(@NotNull final Position nextPosition) { |  | ||||||
|                 this.path.add(nextPosition); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @NotNull |  | ||||||
|             private Direction getRandomDirectionFromLastPosition(@NotNull final Random random) { |  | ||||||
|                 final EnumSet<Direction> validDirections = this.getValidDirectionsFromLastPosition(); |  | ||||||
|                 if (validDirections.isEmpty()) { |  | ||||||
|                     throw new IllegalStateException("WE MUST NOT GET HERE! analyze why it happened!!!"); |  | ||||||
|                 } |  | ||||||
|                 if (validDirections.size() == 1) { |  | ||||||
|                     return validDirections.iterator().next(); |  | ||||||
|                 } |  | ||||||
|                 final Direction[] directionArray = validDirections.toArray(Direction[]::new); |  | ||||||
|                 final int index = random.nextInt(directionArray.length); |  | ||||||
|                 return directionArray[index]; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @NotNull |  | ||||||
|             private EnumSet<Direction> getValidDirectionsFromLastPosition() { |  | ||||||
|                 final Position fromPosition = this.path.getLast(); |  | ||||||
|                 final EnumSet<Direction> validDirections = EnumSet.allOf(Direction.class); |  | ||||||
|                 if (this.path.size() > 1) { |  | ||||||
|                     final Position prevP = this.path.get(this.path.size() - 2); |  | ||||||
|                     fromPosition.getDirectionTo(prevP) |  | ||||||
|                             .forEach(validDirections::remove); |  | ||||||
|                 } |  | ||||||
|                 boolean canLeft = fromPosition.x() > 0; |  | ||||||
|                 boolean canRight = fromPosition.x() < width - 1; |  | ||||||
|                 boolean canUp = fromPosition.y() > 0; |  | ||||||
|                 boolean canDown = fromPosition.y() < height - 1; |  | ||||||
|                 if (!canLeft) { |  | ||||||
|                     validDirections.remove(Direction.LEFT); |  | ||||||
|                 } |  | ||||||
|                 if (!canRight) { |  | ||||||
|                     validDirections.remove(Direction.RIGHT); |  | ||||||
|                 } |  | ||||||
|                 if (!canUp) { |  | ||||||
|                     validDirections.remove(Direction.TOP); |  | ||||||
|                 } |  | ||||||
|                 if (!canDown) { |  | ||||||
|                     validDirections.remove(Direction.BOTTOM); |  | ||||||
|                 } |  | ||||||
|                 return validDirections; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public String toString() { |  | ||||||
|                 return Stream.ofAll(this.path) |  | ||||||
|                         .map(position -> "(%s,%s)".formatted(position.x(), position.y())) |  | ||||||
|                         .mkString("Path[", "->", "]"); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -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,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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  | } | ||||||
|  | @ -4,6 +4,7 @@ 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.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 { | ||||||
|  | @ -18,11 +19,11 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { | ||||||
|         // 01 0xb1 magic |         // 01 0xb1 magic | ||||||
|         // 02 0x01 version |         // 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(); | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ 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.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 { | ||||||
|  | @ -13,8 +14,8 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream { | ||||||
|         // 00 0x1a magic |         // 00 0x1a magic | ||||||
|         // 01 0xb1 magic |         // 01 0xb1 magic | ||||||
|         // 02 0x02 version |         // 02 0x02 version | ||||||
|         this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1); |         this.writeByte(MazeConstants.MAGIC_BYTE_1); | ||||||
|         this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2); |         this.writeByte(MazeConstants.MAGIC_BYTE_2); | ||||||
|         this.writeByte(SerializerDeserializerV1.VERSION_BYTE); |         this.writeByte(SerializerDeserializerV1.VERSION_BYTE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,9 +24,7 @@ import java.lang.reflect.InvocationTargetException; | ||||||
|  */ |  */ | ||||||
| @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; |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Serializes the {@code maze} into a byte array. |      * Serializes the {@code maze} into a byte array. | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ 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.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 { | ||||||
|  | @ -19,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(); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ 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.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 { | ||||||
|  | @ -14,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); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,10 +29,7 @@ import java.lang.reflect.InvocationTargetException; | ||||||
|  */ |  */ | ||||||
| @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; |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Serializes the {@code maze} into a byte array. |      * Serializes the {@code maze} into a byte array. | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ 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.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  | @ -22,11 +23,11 @@ public class MazeInputStreamV3 extends AbstractMazeInputStream { | ||||||
|         // 01 0xb1 magic |         // 01 0xb1 magic | ||||||
|         // 02 0x03 version |         // 02 0x03 version | ||||||
|         final byte magic1 = this.readByte(); |         final byte magic1 = this.readByte(); | ||||||
|         if (magic1 != SerializerDeserializerV3.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 != SerializerDeserializerV3.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(); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ 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.CommonTileHandler; | ||||||
|  | import ch.fritteli.maze.generator.serialization.MazeConstants; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| 
 | 
 | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
|  | @ -16,8 +17,8 @@ public class MazeOutputStreamV3 extends AbstractMazeOutputStream { | ||||||
|         // 00 0x1a magic |         // 00 0x1a magic | ||||||
|         // 01 0xb1 magic |         // 01 0xb1 magic | ||||||
|         // 02 0x03 version |         // 02 0x03 version | ||||||
|         this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_1); |         this.writeByte(MazeConstants.MAGIC_BYTE_1); | ||||||
|         this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_2); |         this.writeByte(MazeConstants.MAGIC_BYTE_2); | ||||||
|         this.writeByte(SerializerDeserializerV3.VERSION_BYTE); |         this.writeByte(SerializerDeserializerV3.VERSION_BYTE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -32,10 +32,7 @@ import java.lang.reflect.InvocationTargetException; | ||||||
|  */ |  */ | ||||||
| @UtilityClass | @UtilityClass | ||||||
| public class SerializerDeserializerV3 { | public class SerializerDeserializerV3 { | ||||||
| 
 |     public final byte VERSION_BYTE = 0x03; | ||||||
|     final byte MAGIC_BYTE_1 = 0x1a; |  | ||||||
|     final byte MAGIC_BYTE_2 = (byte) 0xb1; |  | ||||||
|     final byte VERSION_BYTE = 0x03; |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Serializes the {@code maze} into a byte array. |      * Serializes the {@code maze} into a byte array. | ||||||
|  |  | ||||||
|  | @ -1,44 +0,0 @@ | ||||||
| package ch.fritteli.maze.generator.algorithm; |  | ||||||
| 
 |  | ||||||
| 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 foo() { |  | ||||||
|         // 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(""" |  | ||||||
|                 ╷ ╶─────┬─────┬───┬─┐ |  | ||||||
|                 │       │     │   │ │ |  | ||||||
|                 │ ╷ ╶─┬─┴─┬─╴ ╵ ╷ │ │ |  | ||||||
|                 │ │   │   │     │ │ │ |  | ||||||
|                 │ └─┐ │ ╶─┼─┬─┬─┘ │ │ |  | ||||||
|                 │   │ │   │ │ │   │ │ |  | ||||||
|                 │ ╷ │ └─╴ │ ╵ ╵ ╶─┘ │ |  | ||||||
|                 │ │ │     │         │ |  | ||||||
|                 ├─┴─┘ ╷ ┌─┴───┐ ╷ ╶─┤ |  | ||||||
|                 │     │ │     │ │   │ |  | ||||||
|                 │ ╷ ┌─┘ └───╴ │ └─┬─┤ |  | ||||||
|                 │ │ │         │   │ │ |  | ||||||
|                 ├─┴─┘ ┌───╴ ╷ └─┐ ╵ │ |  | ||||||
|                 │     │     │   │   │ |  | ||||||
|                 │ ╶─┬─┴─╴ ┌─┘ ╶─┘ ╶─┤ |  | ||||||
|                 │   │     │         │ |  | ||||||
|                 ├─╴ ├─╴ ┌─┘ ╶─┐ ╶───┤ |  | ||||||
|                 │   │   │     │     │ |  | ||||||
|                 ├───┴─╴ └─┐ ╶─┴───┐ │ |  | ||||||
|                 │         │       │ │ |  | ||||||
|                 └─────────┴───────┘ ╵"""); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -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(); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| package ch.fritteli.maze.generator.serialization.v2; | package ch.fritteli.maze.generator.serialization.v2; | ||||||
| 
 | 
 | ||||||
| import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; | import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; | ||||||
| import ch.fritteli.maze.generator.algorithm.Wilson; | import ch.fritteli.maze.generator.algorithm.wilson.Wilson; | ||||||
| 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 org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| package ch.fritteli.maze.generator.serialization.v3; | package ch.fritteli.maze.generator.serialization.v3; | ||||||
| 
 | 
 | ||||||
| import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; | import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; | ||||||
| import ch.fritteli.maze.generator.algorithm.Wilson; | import ch.fritteli.maze.generator.algorithm.wilson.Wilson; | ||||||
| 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 org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
|  | @ -37,4 +37,13 @@ class SerializerDeserializerV3Test { | ||||||
|         final Maze result = SerializerDeserializerV3.deserialize(bytes); |         final Maze result = SerializerDeserializerV3.deserialize(bytes); | ||||||
|         assertThat(result).isEqualTo(expected); |         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