Initial - and extremely ugly - version of Wilson's maze algorithm.
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		
							parent
							
								
									cf405fbc98
								
							
						
					
					
						commit
						cdfa1f4476
					
				
					 8 changed files with 360 additions and 10 deletions
				
			
		|  | @ -0,0 +1,19 @@ | ||||||
|  | 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; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze) { | ||||||
|  |         this.maze = maze; | ||||||
|  |         this.random = new Random(maze.getRandomSeed()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|         this.random = new Random(maze.getRandomSeed()); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void run() { |     public void run() { | ||||||
|  |  | ||||||
							
								
								
									
										297
									
								
								src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,297 @@ | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @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[", "->", "]"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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(); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | 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; | ||||||
|  | 
 | ||||||
|  | class WilsonTest { | ||||||
|  |     @Test | ||||||
|  |     void foo() { | ||||||
|  |         final Maze maze = new Maze(50, 10, 0); | ||||||
|  |         final Wilson wilson = new Wilson(maze); | ||||||
|  |         wilson.run(); | ||||||
|  |         System.out.println(TextRenderer.newInstance().render(maze)); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue