feature/wilson-algorithm #10
					 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.LinkedList; | ||||
| import java.util.Random; | ||||
| 
 | ||||
| public class RandomDepthFirst { | ||||
| 
 | ||||
|     @NotNull | ||||
|     private final Maze maze; | ||||
|     @NotNull | ||||
|     private final Random random; | ||||
| public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm { | ||||
|     @NotNull | ||||
|     private final Deque<Position> positions = new LinkedList<>(); | ||||
| 
 | ||||
|     public RandomDepthFirst(@NotNull final Maze maze) { | ||||
|         this.maze = maze; | ||||
|         this.random = new Random(maze.getRandomSeed()); | ||||
|         super(maze); | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
| 
 | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| 
 | ||||
| public enum Direction { | ||||
|     TOP, | ||||
|     RIGHT, | ||||
|     BOTTOM, | ||||
|     LEFT; | ||||
| 
 | ||||
|     @NotNull | ||||
|     public Direction invert() { | ||||
|         return switch (this) { | ||||
|             case TOP -> BOTTOM; | ||||
|  |  | |||
|  | @ -1,10 +1,12 @@ | |||
| package ch.fritteli.maze.generator.model; | ||||
| 
 | ||||
| import io.vavr.control.Option; | ||||
| import lombok.With; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| 
 | ||||
| @With | ||||
| public record Position(int x, int y) { | ||||
|     @NotNull | ||||
|     public Position move(@NotNull final Direction direction) { | ||||
|         return switch (direction) { | ||||
|             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); | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     @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)) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (tileAtPosition.map(Tile::isSolution).getOrElse(false)) { | ||||
|                 if (tileAtPosition.exists(Tile::isSolution)) { | ||||
|                     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