From cdfa1f4476d9eef3893bf0a403947f34b3532409 Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Sat, 14 Dec 2024 04:30:35 +0100
Subject: [PATCH 1/6] Initial - and extremely ugly - version of Wilson's maze
 algorithm.

---
 .../AbstractMazeGeneratorAlgorithm.java       |  19 ++
 .../algorithm/MazeGeneratorAlgorithm.java     |   5 +
 .../generator/algorithm/RandomDepthFirst.java |  11 +-
 .../maze/generator/algorithm/Wilson.java      | 297 ++++++++++++++++++
 .../maze/generator/model/Direction.java       |   3 +
 .../maze/generator/model/Position.java        |  18 ++
 .../generator/renderer/pdf/Generator.java     |   2 +-
 .../maze/generator/algorithm/WilsonTest.java  |  15 +
 8 files changed, 360 insertions(+), 10 deletions(-)
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
 create mode 100644 src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java

diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java
new file mode 100644
index 0000000..330f9e7
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java
@@ -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());
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java b/src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java
new file mode 100644
index 0000000..0cc091a
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java
@@ -0,0 +1,5 @@
+package ch.fritteli.maze.generator.algorithm;
+
+public interface MazeGeneratorAlgorithm {
+    void run();
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java
index a65b15e..21aa2d1 100644
--- a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java
@@ -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() {
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
new file mode 100644
index 0000000..86e4fd6
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
@@ -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[", "->", "]");
+            }
+        }
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/model/Direction.java b/src/main/java/ch/fritteli/maze/generator/model/Direction.java
index e50ba6c..75ee1c4 100644
--- a/src/main/java/ch/fritteli/maze/generator/model/Direction.java
+++ b/src/main/java/ch/fritteli/maze/generator/model/Direction.java
@@ -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;
diff --git a/src/main/java/ch/fritteli/maze/generator/model/Position.java b/src/main/java/ch/fritteli/maze/generator/model/Position.java
index 4b91543..a6fa788 100644
--- a/src/main/java/ch/fritteli/maze/generator/model/Position.java
+++ b/src/main/java/ch/fritteli/maze/generator/model/Position.java
@@ -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();
+        };
+    }
 }
diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java
index 0de27a9..547f3da 100644
--- a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java
+++ b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java
@@ -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;
                 }
             }
diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
new file mode 100644
index 0000000..8e46ba5
--- /dev/null
+++ b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
@@ -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));
+    }
+}

From 9579066cb5e407c4d57ca8d4137d4c107fdf123d Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Wed, 18 Dec 2024 00:24:55 +0100
Subject: [PATCH 2/6] Add Serialization V3: Include the name of the algorithm.

---
 .../AbstractMazeGeneratorAlgorithm.java       |   3 +-
 .../generator/algorithm/RandomDepthFirst.java |   2 +-
 .../maze/generator/algorithm/Wilson.java      |   2 +-
 .../fritteli/maze/generator/model/Maze.java   |  36 ++-
 .../generator/renderer/html/HTMLRenderer.java | 270 +++++++++---------
 .../generator/renderer/json/Generator.java    |   1 +
 .../generator/renderer/pdf/Generator.java     |   2 +-
 .../AbstractMazeInputStream.java              |   7 +-
 .../v1/SerializerDeserializerV1.java          |   3 +-
 .../serialization/v3/MazeInputStreamV3.java   |  77 +++++
 .../serialization/v3/MazeOutputStreamV3.java  |  84 ++++++
 .../v3/SerializerDeserializerV3.java          | 170 +++++++++++
 src/main/resources/maze.schema.json           |   5 +
 13 files changed, 523 insertions(+), 139 deletions(-)
 create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java

diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java
index 330f9e7..2037f76 100644
--- a/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java
@@ -12,8 +12,9 @@ public abstract class AbstractMazeGeneratorAlgorithm implements MazeGeneratorAlg
     protected final Random random;
 
 
-    public AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze) {
+    protected AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze, @NotNull final String algorithmName) {
         this.maze = maze;
         this.random = new Random(maze.getRandomSeed());
+        this.maze.setAlgorithm(algorithmName);
     }
 }
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java
index 21aa2d1..eab53a9 100644
--- a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java
@@ -16,7 +16,7 @@ public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm {
     private final Deque<Position> positions = new LinkedList<>();
 
     public RandomDepthFirst(@NotNull final Maze maze) {
-        super(maze);
+        super(maze, "Random Depth First");
     }
 
     public void run() {
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
index 86e4fd6..e53acce 100644
--- a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
@@ -24,7 +24,7 @@ import java.util.Random;
  */
 public class Wilson extends AbstractMazeGeneratorAlgorithm {
     public Wilson(@NotNull final Maze maze) {
-        super(maze);
+        super(maze, "Wilson");
     }
 
     @Override
diff --git a/src/main/java/ch/fritteli/maze/generator/model/Maze.java b/src/main/java/ch/fritteli/maze/generator/model/Maze.java
index 14d7af6..633e1b3 100644
--- a/src/main/java/ch/fritteli/maze/generator/model/Maze.java
+++ b/src/main/java/ch/fritteli/maze/generator/model/Maze.java
@@ -3,6 +3,7 @@ package ch.fritteli.maze.generator.model;
 import io.vavr.control.Option;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.ToString;
 import org.jetbrains.annotations.NotNull;
 
@@ -21,6 +22,9 @@ public class Maze {
     private final Position start;
     @Getter
     private final Position end;
+    @Getter
+    @Setter
+    private String algorithm;
 
     public Maze(final int width, final int height) {
         this(width, height, System.nanoTime());
@@ -34,7 +38,11 @@ public class Maze {
         this(width, height, randomSeed, new Position(0, 0), new Position(width - 1, height - 1));
     }
 
-    public Maze(final int width, final int height, final long randomSeed, @NotNull final Position start, @NotNull final Position end) {
+    public Maze(final int width,
+                final int height,
+                final long randomSeed,
+                @NotNull final Position start,
+                @NotNull final Position end) {
         if (width <= 1 || height <= 1) {
             throw new IllegalArgumentException("width and height must be >1");
         }
@@ -71,7 +79,12 @@ public class Maze {
     /**
      * INTERNAL API. Exists only for deserialization. Not to be called from user code.
      */
-    private Maze(@NotNull final Tile[][] field, final int width, final int height, @NotNull final Position start, @NotNull final Position end, final long randomSeed) {
+    private Maze(@NotNull final Tile[][] field,
+                 final int width,
+                 final int height,
+                 @NotNull final Position start,
+                 @NotNull final Position end,
+                 final long randomSeed) {
         this.field = field;
         this.width = width;
         this.height = height;
@@ -80,6 +93,25 @@ public class Maze {
         this.end = end;
     }
 
+    /**
+     * INTERNAL API. Exists only for deserialization. Not to be called from user code.
+     */
+    private Maze(@NotNull final Tile[][] field,
+                 final int width,
+                 final int height,
+                 @NotNull final Position start,
+                 @NotNull final Position end,
+                 final long randomSeed,
+                 @NotNull final String algorithm) {
+        this.field = field;
+        this.width = width;
+        this.height = height;
+        this.randomSeed = randomSeed;
+        this.algorithm = algorithm;
+        this.start = start;
+        this.end = end;
+    }
+
     @NotNull
     public Option<Tile> getTileAt(@NotNull final Position position) {
         return this.getTileAt(position.x(), position.y());
diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java b/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java
index 1242ac3..a2de552 100644
--- a/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java
+++ b/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java
@@ -6,108 +6,109 @@ import org.jetbrains.annotations.NotNull;
 
 public class HTMLRenderer implements Renderer<String> {
 
-    private static final String POSTAMBLE = "<script>"
-            + "let userPath = [];"
-            + "const DIR_UNDEF = -1;"
-            + "const DIR_SAME = 0;"
-            + "const DIR_UP = 1;"
-            + "const DIR_RIGHT = 2;"
-            + "const DIR_DOWN = 3;"
-            + "const DIR_LEFT = 4;"
-            + "function getCoords(cell) {"
-            + "    return {"
-            + "        x: cell.cellIndex,"
-            + "        y: cell.parentElement.rowIndex"
-            + "    };"
-            + "}"
-            + "function distance(prev, next) {"
-            + "    return Math.abs(prev.x - next.x) + Math.abs(prev.y - next.y);"
-            + "}"
-            + "function direction(prev, next) {"
-            + "    const dist = distance(prev, next);"
-            + "    if (dist === 0) {"
-            + "        return DIR_SAME;"
-            + "    }"
-            + "    if (dist !== 1) {"
-            + "        return DIR_UNDEF;"
-            + "    }"
-            + "    if (next.x === prev.x) {"
-            + "        if (next.y === prev.y + 1) {"
-            + "            return DIR_DOWN;"
-            + "        }"
-            + "        return DIR_UP;"
-            + "    }"
-            + "    if (next.x === prev.x + 1) {"
-            + "        return DIR_RIGHT;"
-            + "    }"
-            + "    return DIR_LEFT;"
-            + "}"
-            + "(function () {"
-            + "    const labyrinthTable = document.getElementById(\"labyrinth\");"
-            + "    const labyrinthCells = labyrinthTable.getElementsByTagName(\"td\");"
-            + "    const start = {x: 0, y: 0};"
-            + "    const end = {"
-            + "        x: labyrinthTable.getElementsByTagName(\"tr\")[0].getElementsByTagName(\"td\").length - 1,"
-            + "        y: labyrinthTable.getElementsByTagName(\"tr\").length - 1"
-            + "    };"
-            + "    for (let i = 0; i < labyrinthCells.length; i++) {"
-            + "        let cell = labyrinthCells.item(i);"
-            + "        cell.onclick = (event) => {"
-            + "            let target = event.target;"
-            + "            const coords = getCoords(target);"
-            + "            if (coords.x === end.x && coords.y === end.y) {"
-            + "                alert(\"HOORAY! You did it! Congratulations!\")"
-            + "            }"
-            + "            if (userPath.length === 0) {"
-            + "                if (coords.x === start.x && coords.y === start.y) {"
-            + "                    userPath.push(coords);"
-            + "                    target.classList.toggle(\"user\");"
-            + "                }"
-            + "            } else {"
-            + "                const dir = direction(userPath[userPath.length - 1], coords);"
-            + "                switch (dir) {"
-            + "                    case DIR_UNDEF:"
-            + "                        return;"
-            + "                    case DIR_SAME:"
-            + "                        userPath.pop();"
-            + "                        target.classList.toggle(\"user\");"
-            + "                        return;"
-            + "                    default:"
-            + "                        if (userPath.find(value => value.x === coords.x && value.y === coords.y)) {"
-            + "                            return;"
-            + "                        } else {"
-            + "                            switch (dir) {"
-            + "                                case DIR_UP:"
-            + "                                    if (target.classList.contains(\"bottom\")) {"
-            + "                                        return;"
-            + "                                    }"
-            + "                                    break;"
-            + "                                case DIR_RIGHT:"
-            + "                                    if (target.classList.contains(\"left\")) {"
-            + "                                        return;"
-            + "                                    }"
-            + "                                    break;"
-            + "                                case DIR_DOWN:"
-            + "                                    if (target.classList.contains(\"top\")) {"
-            + "                                        return;"
-            + "                                    }"
-            + "                                    break;"
-            + "                                case DIR_LEFT:"
-            + "                                    if (target.classList.contains(\"right\")) {"
-            + "                                        return;"
-            + "                                    }"
-            + "                                    break;"
-            + "                            }"
-            + "                            userPath.push(coords);"
-            + "                            target.classList.toggle(\"user\");"
-            + "                            return;"
-            + "                        }"
-            + "                }"
-            + "            }"
-            + "        };"
-            + "    }"
-            + "})();"
-            + "</script></body></html>";
+    private static final String POSTAMBLE = """
+            <script>
+            let userPath = [];
+            const DIR_UNDEF = -1;
+            const DIR_SAME = 0;
+            const DIR_UP = 1;
+            const DIR_RIGHT = 2;
+            const DIR_DOWN = 3;
+            const DIR_LEFT = 4;
+            function getCoords(cell) {
+                return {
+                    x: cell.cellIndex,
+                    y: cell.parentElement.rowIndex
+                };
+            }
+            function distance(prev, next) {
+                return Math.abs(prev.x - next.x) + Math.abs(prev.y - next.y);
+            }
+            function direction(prev, next) {
+                const dist = distance(prev, next);
+                if (dist === 0) {
+                    return DIR_SAME;
+                }
+                if (dist !== 1) {
+                    return DIR_UNDEF;
+                }
+                if (next.x === prev.x) {
+                    if (next.y === prev.y + 1) {
+                        return DIR_DOWN;
+                    }
+                    return DIR_UP;
+                }
+                if (next.x === prev.x + 1) {
+                    return DIR_RIGHT;
+                }
+                return DIR_LEFT;
+            }
+            (function () {
+                const labyrinthTable = document.getElementById("labyrinth");
+                const labyrinthCells = labyrinthTable.getElementsByTagName("td");
+                const start = {x: 0, y: 0};
+                const end = {
+                    x: labyrinthTable.getElementsByTagName("tr")[0].getElementsByTagName("td").length - 1,
+                    y: labyrinthTable.getElementsByTagName("tr").length - 1
+                };
+                for (let i = 0; i < labyrinthCells.length; i++) {
+                    let cell = labyrinthCells.item(i);
+                    cell.onclick = (event) => {
+                        let target = event.target;
+                        const coords = getCoords(target);
+                        if (coords.x === end.x && coords.y === end.y) {
+                            alert("HOORAY! You did it! Congratulations!")
+                        }
+                        if (userPath.length === 0) {
+                            if (coords.x === start.x && coords.y === start.y) {
+                                userPath.push(coords);
+                                target.classList.toggle("user");
+                            }
+                        } else {
+                            const dir = direction(userPath[userPath.length - 1], coords);
+                            switch (dir) {
+                                case DIR_UNDEF:
+                                    return;
+                                case DIR_SAME:
+                                    userPath.pop();
+                                    target.classList.toggle("user");
+                                    return;
+                                default:
+                                    if (userPath.find(value => value.x === coords.x && value.y === coords.y)) {
+                                        return;
+                                    } else {
+                                        switch (dir) {
+                                            case DIR_UP:
+                                                if (target.classList.contains("bottom")) {
+                                                    return;
+                                                }
+                                                break;
+                                            case DIR_RIGHT:
+                                                if (target.classList.contains("left")) {
+                                                    return;
+                                                }
+                                                break;
+                                            case DIR_DOWN:
+                                                if (target.classList.contains("top")) {
+                                                    return;
+                                                }
+                                                break;
+                                            case DIR_LEFT:
+                                                if (target.classList.contains("right")) {
+                                                    return;
+                                                }
+                                                break;
+                                        }
+                                        userPath.push(coords);
+                                        target.classList.toggle("user");
+                                        return;
+                                    }
+                            }
+                        }
+                    };
+                }
+            })();
+            </script></body></html>""";
 
     private HTMLRenderer() {
     }
@@ -135,33 +136,42 @@ public class HTMLRenderer implements Renderer<String> {
     }
 
     private String getPreamble(@NotNull final Maze maze) {
-        return "<!DOCTYPE html><html lang=\"en\">" +
-                "<head>" +
-                "<title>Maze " + maze.getWidth() + "x" + maze.getHeight() + ", ID " + maze.getRandomSeed() + "</title>" +
-                "<meta charset=\"utf-8\">" +
-                "<style>" +
-                "table{border-collapse:collapse;}" +
-                "td{border:0 solid black;height:1em;width:1em;cursor:pointer;}" +
-                "td.top{border-top-width:1px;}" +
-                "td.right{border-right-width:1px;}" +
-                "td.bottom{border-bottom-width:1px;}" +
-                "td.left{border-left-width:1px;}" +
-                "td.user{background:hotpink;}" +
-                "</style>" +
-                "<script>" +
-                "let solution = false;" +
-                "function toggleSolution() {" +
-                "let stylesheet = document.styleSheets[0];" +
-                "if(solution){" +
-                "stylesheet.deleteRule(0);" +
-                "}else{" +
-                "stylesheet.insertRule(\"td.solution{background-color:lightgray;}\", 0);" +
-                "}" +
-                "solution = !solution;" +
-                "}" +
-                "</script>" +
-                "</head>" +
-                "<body>" +
-                "<input id=\"solutionbox\" type=\"checkbox\" onclick=\"toggleSolution()\"/><label for=\"solutionbox\">show solution</label>";
+        return """
+                <!DOCTYPE html>
+                <html lang="en">
+                    <head>
+                        <title>Maze %dx%d, ID %d, Algorithm %s</title>
+                        <meta charset="utf-8">
+                        <style>
+                            table{border-collapse:collapse;}
+                            td{border:0 solid black;height:1em;width:1em;cursor:pointer;}
+                            td.top{border-top-width:1px;}
+                            td.right{border-right-width:1px;}
+                            td.bottom{border-bottom-width:1px;}
+                            td.left{border-left-width:1px;}
+                            td.user{background:hotpink;}
+                        </style>
+                        <script>
+                            let solution = false;
+                            function toggleSolution() {
+                                let stylesheet = document.styleSheets[0];
+                                if (solution) {
+                                    stylesheet.deleteRule(0);
+                                } else {
+                                    stylesheet.insertRule("td.solution{background-color:lightgray;}", 0);
+                                }
+                                solution = !solution;
+                            }
+                        </script>
+                    </head>
+                    <body>
+                        <input id="solutionbox" type="checkbox" onclick="toggleSolution()"/>
+                        <label for="solutionbox">show solution</label>"""
+                .formatted(
+                        maze.getWidth(),
+                        maze.getHeight(),
+                        maze.getRandomSeed(),
+                        maze.getAlgorithm()
+                );
     }
 }
diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java b/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java
index c08ed1b..3970732 100644
--- a/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java
+++ b/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java
@@ -23,6 +23,7 @@ class Generator {
     JsonMaze generate() {
         final JsonMaze result = new JsonMaze();
         result.setId(String.valueOf(this.maze.getRandomSeed()));
+        result.setAlgorithm(this.maze.getAlgorithm());
         result.setWidth(this.maze.getWidth());
         result.setHeight(this.maze.getHeight());
         final List<List<JsonCell>> rows = new ArrayList<>();
diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java
index 547f3da..a4a103d 100644
--- a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java
+++ b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java
@@ -34,7 +34,7 @@ class Generator {
 
         final PDDocument pdDocument = new PDDocument();
         final PDDocumentInformation info = new PDDocumentInformation();
-        info.setTitle("Maze %sx%s, ID %s".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed()));
+        info.setTitle("Maze %sx%s, ID %s (%s)".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed(), this.maze.getAlgorithm()));
         pdDocument.setDocumentInformation(info);
         final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight));
         final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight));
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java b/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java
index 3803ef1..7e4ba88 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 
 public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
 
@@ -14,7 +15,7 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
     public abstract void checkHeader();
 
     @NotNull
-    public abstract Maze readMazeData();
+    public abstract Maze readMazeData() throws IOException;
 
     public byte readByte() {
         final int read = this.read();
@@ -25,6 +26,10 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
         return (byte) read;
     }
 
+    public int readByteAsInt() {
+        return 0xff & this.readByte();
+    }
+
     public int readInt() {
         int result = 0;
         result |= (0xff & this.readByte()) << 24;
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
index 24f7358..fb4a213 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
@@ -30,8 +30,7 @@ import java.util.EnumSet;
  *      14   e 1110 right+bottom+left
  *      15   f 1111 top+right+bottom+left
  * </pre>
- * ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls
- * ==&gt; first bytes are:
+ * ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==&gt; first bytes are:
  * <pre>
  *   byte  hex meaning
  *     00 0x1a magic
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
new file mode 100644
index 0000000..1814008
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
@@ -0,0 +1,77 @@
+package ch.fritteli.maze.generator.serialization.v3;
+
+import ch.fritteli.maze.generator.model.Maze;
+import ch.fritteli.maze.generator.model.Position;
+import ch.fritteli.maze.generator.model.Tile;
+import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class MazeInputStreamV3 extends AbstractMazeInputStream {
+
+    public MazeInputStreamV3(@NotNull final byte[] buf) {
+        super(buf);
+    }
+
+    @Override
+    public void checkHeader() {
+        // 00 0x1a magic
+        // 01 0xb1 magic
+        // 02 0x03 version
+        final byte magic1 = this.readByte();
+        if (magic1 != SerializerDeserializerV3.MAGIC_BYTE_1) {
+            throw new IllegalArgumentException("Invalid maze data.");
+        }
+        final byte magic2 = this.readByte();
+        if (magic2 != SerializerDeserializerV3.MAGIC_BYTE_2) {
+            throw new IllegalArgumentException("Invalid maze data.");
+        }
+        final int version = this.readByte();
+        if (version != SerializerDeserializerV3.VERSION_BYTE) {
+            throw new IllegalArgumentException("Unknown maze data version: " + version);
+        }
+    }
+
+    @NotNull
+    @Override
+    public Maze readMazeData() throws IOException {
+        // 03..06   width (int)
+        // 07..10   height (int)
+        // 11..14   start-x (int)
+        // 15..18   start-y (int)
+        // 19..22   end-x (int)
+        // 23..26   end-y (int)
+        // 27..34   random seed number (long)
+        // 35       length of the algorithm's name (unsigned byte)
+        // 36..+len name (bytes of String)
+        // +len+1.. tiles
+        final int width = this.readInt();
+        final int height = this.readInt();
+        final int startX = this.readInt();
+        final int startY = this.readInt();
+        final int endX = this.readInt();
+        final int endY = this.readInt();
+        final long randomSeed = this.readLong();
+        final int algorithmLength = this.readByteAsInt();
+
+        final String algorithm = new String(this.readNBytes(algorithmLength), StandardCharsets.UTF_8);
+
+        final Tile[][] tiles = new Tile[width][height];
+        for (int x = 0; x < width; x++) {
+            tiles[x] = new Tile[height];
+        }
+
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                final byte bitmask = this.readByte();
+                tiles[x][y] = SerializerDeserializerV3.getTileForBitmask(bitmask);
+            }
+        }
+
+        final Position start = new Position(startX, startY);
+        final Position end = new Position(endX, endY);
+        return SerializerDeserializerV3.createMaze(tiles, width, height, start, end, randomSeed, algorithm);
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
new file mode 100644
index 0000000..52b154a
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
@@ -0,0 +1,84 @@
+package ch.fritteli.maze.generator.serialization.v3;
+
+import ch.fritteli.maze.generator.model.Maze;
+import ch.fritteli.maze.generator.model.Position;
+import ch.fritteli.maze.generator.model.Tile;
+import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.charset.StandardCharsets;
+
+public class MazeOutputStreamV3 extends AbstractMazeOutputStream {
+
+    @Override
+    public void writeHeader() {
+        // 00 0x1a magic
+        // 01 0xb1 magic
+        // 02 0x03 version
+        this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_1);
+        this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_2);
+        this.writeByte(SerializerDeserializerV3.VERSION_BYTE);
+    }
+
+    @Override
+    public void writeMazeData(@NotNull final Maze maze) {
+        // 03..06   width (int)
+        // 07..10   height (int)
+        // 11..14   start-x (int)
+        // 15..18   start-y (int)
+        // 19..22   end-x (int)
+        // 23..26   end-y (int)
+        // 27..34   random seed number (long)
+        // 35       length of the algorithm's name (unsigned byte)
+        // 36..+len name (bytes of String)
+        // +len+1.. tiles
+        final long randomSeed = maze.getRandomSeed();
+        final AlgorithmWrapper algorithm = this.getAlgorithmWrapper(maze.getAlgorithm());
+        final int width = maze.getWidth();
+        final int height = maze.getHeight();
+        final Position start = maze.getStart();
+        final Position end = maze.getEnd();
+        this.writeInt(width);
+        this.writeInt(height);
+        this.writeInt(start.x());
+        this.writeInt(start.y());
+        this.writeInt(end.x());
+        this.writeInt(end.y());
+        this.writeLong(randomSeed);
+        this.writeByte(algorithm.length());
+        this.writeBytes(algorithm.name());
+
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                // We .get() it, because we want to crash hard if it is not available.
+                final Tile tile = maze.getTileAt(x, y).get();
+                final byte bitmask = SerializerDeserializerV3.getBitmaskForTile(tile);
+                this.writeByte(bitmask);
+            }
+        }
+    }
+
+    @NotNull
+    private AlgorithmWrapper getAlgorithmWrapper(@NotNull final String algorithm) {
+        final byte[] bytes = algorithm.getBytes(StandardCharsets.UTF_8);
+        if (bytes.length < 256) {
+            // Phew, that's the easy case!
+            return new AlgorithmWrapper(bytes, (byte) bytes.length);
+        }
+
+        // Let's use a very primitive, brute-force approach
+        int strLen = Math.min(255, algorithm.length());
+        int len;
+        byte[] name;
+        do {
+            name = algorithm.substring(0, strLen).getBytes(StandardCharsets.UTF_8);
+            len = name.length;
+            strLen--;
+        } while (len > 255);
+
+        return new AlgorithmWrapper(name, (byte) len);
+    }
+
+    private record AlgorithmWrapper(@NotNull byte[] name, byte length) {
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
new file mode 100644
index 0000000..839e588
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
@@ -0,0 +1,170 @@
+package ch.fritteli.maze.generator.serialization.v3;
+
+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 lombok.experimental.UtilityClass;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.EnumSet;
+
+/**
+ * <pre>
+ * decimal hex bin  border
+ *       0   0 0000 no border
+ *       1   1 0001 top
+ *       2   2 0010 right
+ *       3   3 0011 top+right
+ *       4   4 0100 bottom
+ *       5   5 0101 top+bottom
+ *       6   6 0110 right+bottom
+ *       7   7 0111 top+right+bottom
+ *       8   8 1000 left
+ *       9   9 1001 top+left
+ *      10   a 1010 right+left
+ *      11   b 1011 top+right+left
+ *      12   c 1100 bottom+left
+ *      13   d 1101 top+bottom+left
+ *      14   e 1110 right+bottom+left
+ *      15   f 1111 top+right+bottom+left
+ * </pre>
+ * ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==&gt; first bytes are:
+ * <pre>
+ *   byte     hex    meaning
+ *     00     0x1a   magic
+ *     01     0xb1   magic
+ *     02     0x03   version (0x00 -> dev, 0x01, 0x02 -> deprecated, 0x03 -> stable)
+ *     03..06        width (int)
+ *     07..10        height (int)
+ *     11..14        start-x (int)
+ *     15..18        start-y (int)
+ *     19..22        end-x (int)
+ *     23..26        end-y (int)
+ *     27..34        random seed number (long)
+ *     35            length of the algorithm's name (number of bytes of the Java String) (unsigned byte)
+ *     36..(36+len)  algorithm's name (bytes of the Java String) (byte...)
+ *     36+len+1..    tiles
+ * </pre>
+ * Extraneous space (poss. last nibble) is ignored.
+ */
+@UtilityClass
+public class SerializerDeserializerV3 {
+
+    final byte MAGIC_BYTE_1 = 0x1a;
+    final byte MAGIC_BYTE_2 = (byte) 0xb1;
+    final byte VERSION_BYTE = 0x03;
+
+    private final byte TOP_BIT = 0b0000_0001;
+    private final byte RIGHT_BIT = 0b0000_0010;
+    private final byte BOTTOM_BIT = 0b0000_0100;
+    private final byte LEFT_BIT = 0b0000_1000;
+    private final byte SOLUTION_BIT = 0b0001_0000;
+
+    /**
+     * Serializes the {@code maze} into a byte array.
+     *
+     * @param maze The {@link Maze} to be serialized.
+     * @return The resulting byte array.
+     */
+    @NotNull
+    public byte[] serialize(@NotNull final Maze maze) {
+        final MazeOutputStreamV3 stream = new MazeOutputStreamV3();
+        stream.writeHeader();
+        stream.writeMazeData(maze);
+        return stream.toByteArray();
+    }
+
+    /**
+     * Deserializes the byte array into an instance of {@link Maze}.
+     *
+     * @param bytes The byte array to be deserialized.
+     * @return An instance of {@link Maze}.
+     */
+    @NotNull
+    public Maze deserialize(@NotNull final byte[] bytes) throws IOException {
+        final MazeInputStreamV3 stream = new MazeInputStreamV3(bytes);
+        stream.checkHeader();
+        return stream.readMazeData();
+    }
+
+    @NotNull
+    Maze createMaze(@NotNull final Tile[][] field,
+                    final int width,
+                    final int height,
+                    @NotNull final Position start,
+                    @NotNull final Position end,
+                    final long randomSeed,
+                    @NotNull final String algorithm) {
+        try {
+            final Constructor<Maze> constructor = Maze.class.getDeclaredConstructor(
+                    Tile[][].class,
+                    Integer.TYPE,
+                    Integer.TYPE,
+                    Position.class,
+                    Position.class,
+                    Long.TYPE,
+                    String.class
+            );
+            constructor.setAccessible(true);
+            return constructor.newInstance(field, width, height, start, end, randomSeed, algorithm);
+        } catch (@NotNull final NoSuchMethodException | IllegalAccessException | InstantiationException |
+                                InvocationTargetException e) {
+            throw new RuntimeException("Can not deserialize Maze from maze data.", e);
+        }
+    }
+
+    @NotNull
+    private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
+        try {
+            final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
+            constructor.setAccessible(true);
+            return constructor.newInstance(walls, solution);
+        } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
+                                InvocationTargetException e) {
+            throw new RuntimeException("Can not deserialize Tile from maze data.", e);
+        }
+    }
+
+    byte getBitmaskForTile(@NotNull final Tile tile) {
+        byte bitmask = 0;
+        if (tile.hasWallAt(Direction.TOP)) {
+            bitmask |= TOP_BIT;
+        }
+        if (tile.hasWallAt(Direction.RIGHT)) {
+            bitmask |= RIGHT_BIT;
+        }
+        if (tile.hasWallAt(Direction.BOTTOM)) {
+            bitmask |= BOTTOM_BIT;
+        }
+        if (tile.hasWallAt((Direction.LEFT))) {
+            bitmask |= LEFT_BIT;
+        }
+        if (tile.isSolution()) {
+            bitmask |= SOLUTION_BIT;
+        }
+        return bitmask;
+    }
+
+    @NotNull
+    Tile getTileForBitmask(final byte bitmask) {
+        final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
+        if ((bitmask & TOP_BIT) == TOP_BIT) {
+            walls.add(Direction.TOP);
+        }
+        if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
+            walls.add(Direction.RIGHT);
+        }
+        if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
+            walls.add(Direction.BOTTOM);
+        }
+        if ((bitmask & LEFT_BIT) == LEFT_BIT) {
+            walls.add(Direction.LEFT);
+        }
+        final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
+        return createTile(walls, solution);
+    }
+}
diff --git a/src/main/resources/maze.schema.json b/src/main/resources/maze.schema.json
index 4b7a57e..8fbbb05 100644
--- a/src/main/resources/maze.schema.json
+++ b/src/main/resources/maze.schema.json
@@ -6,6 +6,7 @@
   "additionalProperties": false,
   "required": [
     "id",
+    "algorithm",
     "width",
     "height",
     "start",
@@ -17,6 +18,10 @@
       "type": "string",
       "description": "64 bit precision signed integer value. Transmitted as string, because ECMAScript (browsers) don't normally handle 64 bit integers well, as the ECMAScript 'number' type is a 64 bit signed double value, leaving only 53 bits for the integer part, thus losing precision."
     },
+    "algorithm": {
+      "type": "string",
+      "description": "The name of the algorithm used to generate the maze."
+    },
     "width": {
       "type": "integer",
       "minimum": 1

From 5a642b354b0c0f956c18cef8fa70893606ce9bfe Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Wed, 18 Dec 2024 00:45:33 +0100
Subject: [PATCH 3/6] Cleanup and refactoring: Eliminate duplicated code.

---
 .../serialization/CommonTileHandler.java      | 100 ++++++++++++++++++
 .../serialization/v1/MazeInputStreamV1.java   |  10 +-
 .../serialization/v1/MazeOutputStreamV1.java  |  10 +-
 .../v1/SerializerDeserializerV1.java          |  86 +--------------
 .../serialization/v2/MazeInputStreamV2.java   |   3 +-
 .../serialization/v2/MazeOutputStreamV2.java  |   3 +-
 .../v2/SerializerDeserializerV2.java          |  80 +-------------
 .../serialization/v3/MazeInputStreamV3.java   |   3 +-
 .../serialization/v3/MazeOutputStreamV3.java  |   3 +-
 .../v3/SerializerDeserializerV3.java          |  80 +-------------
 10 files changed, 132 insertions(+), 246 deletions(-)
 create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java

diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java b/src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java
new file mode 100644
index 0000000..b4051c8
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java
@@ -0,0 +1,100 @@
+package ch.fritteli.maze.generator.serialization;
+
+import ch.fritteli.maze.generator.model.Direction;
+import ch.fritteli.maze.generator.model.Tile;
+import lombok.experimental.UtilityClass;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.EnumSet;
+
+
+/**
+ * Binary format description of a {@link Tile}.<br>
+ * A tile is stored in one byte:
+ * <ul>
+ *     <li>bits 0..2: always 0</li>
+ *     <li>bit 3: 1=solution, 0=not solution</li>
+ *     <li>bits 4..7: encode walls</li>
+ * </ul>
+ * The values for bits 4..7 are as follows:
+ * <pre>
+ * decimal hex bin  border
+ *       0   0 0000 no border
+ *       1   1 0001 top
+ *       2   2 0010 right
+ *       3   3 0011 top+right
+ *       4   4 0100 bottom
+ *       5   5 0101 top+bottom
+ *       6   6 0110 right+bottom
+ *       7   7 0111 top+right+bottom
+ *       8   8 1000 left
+ *       9   9 1001 top+left
+ *      10   a 1010 right+left
+ *      11   b 1011 top+right+left
+ *      12   c 1100 bottom+left
+ *      13   d 1101 top+bottom+left
+ *      14   e 1110 right+bottom+left
+ *      15   f 1111 top+right+bottom+left
+ * </pre>
+ */
+@UtilityClass
+public class CommonTileHandler {
+    private final byte TOP_BIT = 0b0000_0001;
+    private final byte RIGHT_BIT = 0b0000_0010;
+    private final byte BOTTOM_BIT = 0b0000_0100;
+    private final byte LEFT_BIT = 0b0000_1000;
+    private final byte SOLUTION_BIT = 0b0001_0000;
+
+    public byte getBitmaskForTile(@NotNull final Tile tile) {
+        byte bitmask = 0;
+        if (tile.hasWallAt(Direction.TOP)) {
+            bitmask |= TOP_BIT;
+        }
+        if (tile.hasWallAt(Direction.RIGHT)) {
+            bitmask |= RIGHT_BIT;
+        }
+        if (tile.hasWallAt(Direction.BOTTOM)) {
+            bitmask |= BOTTOM_BIT;
+        }
+        if (tile.hasWallAt((Direction.LEFT))) {
+            bitmask |= LEFT_BIT;
+        }
+        if (tile.isSolution()) {
+            bitmask |= SOLUTION_BIT;
+        }
+        return bitmask;
+    }
+
+    @NotNull
+    public Tile getTileForBitmask(final byte bitmask) {
+        final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
+        if ((bitmask & TOP_BIT) == TOP_BIT) {
+            walls.add(Direction.TOP);
+        }
+        if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
+            walls.add(Direction.RIGHT);
+        }
+        if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
+            walls.add(Direction.BOTTOM);
+        }
+        if ((bitmask & LEFT_BIT) == LEFT_BIT) {
+            walls.add(Direction.LEFT);
+        }
+        final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
+        return createTile(walls, solution);
+    }
+
+    @NotNull
+    private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
+        try {
+            final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
+            constructor.setAccessible(true);
+            return constructor.newInstance(walls, solution);
+        } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
+                                InvocationTargetException e) {
+            throw new RuntimeException("Can not deserialize Tile from maze data.", e);
+        }
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java
index 3495e5b..5ec5c7c 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java
@@ -3,6 +3,7 @@ package ch.fritteli.maze.generator.serialization.v1;
 import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
+import ch.fritteli.maze.generator.serialization.CommonTileHandler;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeInputStreamV1 extends AbstractMazeInputStream {
@@ -13,6 +14,9 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
 
     @Override
     public void checkHeader() {
+        // 00 0x1a magic
+        // 01 0xb1 magic
+        // 02 0x01 version
         final byte magic1 = this.readByte();
         if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) {
             throw new IllegalArgumentException("Invalid maze data.");
@@ -30,6 +34,10 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
     @NotNull
     @Override
     public Maze readMazeData() {
+        // 03..10  random seed number (long)
+        // 11..14  width (int)
+        // 15..18  height (int)
+        // 19..    tiles
         final long randomSeed = this.readLong();
         final int width = this.readInt();
         final int height = this.readInt();
@@ -42,7 +50,7 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
         for (int y = 0; y < height; y++) {
             for (int x = 0; x < width; x++) {
                 final byte bitmask = this.readByte();
-                tiles[x][y] = SerializerDeserializerV1.getTileForBitmask(bitmask);
+                tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask);
             }
         }
 
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java
index 62ecf70..aa8d3c2 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java
@@ -3,12 +3,16 @@ package ch.fritteli.maze.generator.serialization.v1;
 import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
+import ch.fritteli.maze.generator.serialization.CommonTileHandler;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
 
     @Override
     public void writeHeader() {
+        // 00 0x1a magic
+        // 01 0xb1 magic
+        // 02 0x02 version
         this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1);
         this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2);
         this.writeByte(SerializerDeserializerV1.VERSION_BYTE);
@@ -16,6 +20,10 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
 
     @Override
     public void writeMazeData(@NotNull final Maze maze) {
+        // 03..10  random seed number (long)
+        // 11..14  width (int)
+        // 15..18  height (int)
+        // 19..    tiles
         final long randomSeed = maze.getRandomSeed();
         final int width = maze.getWidth();
         final int height = maze.getHeight();
@@ -27,7 +35,7 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
             for (int x = 0; x < width; x++) {
                 // We .get() it, because we want to crash hard if it is not available.
                 final Tile tile = maze.getTileAt(x, y).get();
-                final byte bitmask = SerializerDeserializerV1.getBitmaskForTile(tile);
+                final byte bitmask = CommonTileHandler.getBitmaskForTile(tile);
                 this.writeByte(bitmask);
             }
         }
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
index fb4a213..f9f02d9 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
@@ -1,6 +1,5 @@
 package ch.fritteli.maze.generator.serialization.v1;
 
-import ch.fritteli.maze.generator.model.Direction;
 import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Tile;
 import lombok.experimental.UtilityClass;
@@ -8,37 +7,17 @@ import org.jetbrains.annotations.NotNull;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
-import java.util.EnumSet;
 
 /**
- * <pre>
- * decimal hex bin  border
- *       0   0 0000 no border
- *       1   1 0001 top
- *       2   2 0010 right
- *       3   3 0011 top+right
- *       4   4 0100 bottom
- *       5   5 0101 top+bottom
- *       6   6 0110 right+bottom
- *       7   7 0111 top+right+bottom
- *       8   8 1000 left
- *       9   9 1001 top+left
- *      10   a 1010 right+left
- *      11   b 1011 top+right+left
- *      12   c 1100 bottom+left
- *      13   d 1101 top+bottom+left
- *      14   e 1110 right+bottom+left
- *      15   f 1111 top+right+bottom+left
- * </pre>
- * ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==&gt; first bytes are:
+ * Header bytes are:
  * <pre>
  *   byte  hex meaning
  *     00 0x1a magic
  *     01 0xb1 magic
  *     02 0x01 version (0x00 -> dev, 0x01 -> stable)
- * 03..06      width (int)
- * 07..10      height (int)
- * 11..18      random seed number (long)
+ * 03..10      random seed number (long)
+ * 11..14      width (int)
+ * 15..18      height (int)
  * 19..        tiles
  * </pre>
  * Extraneous space (poss. last nibble) is ignored.
@@ -49,12 +28,6 @@ public class SerializerDeserializerV1 {
     final byte MAGIC_BYTE_2 = (byte) 0xb1;
     final byte VERSION_BYTE = 0x01;
 
-    private final byte TOP_BIT = 0b0000_0001;
-    private final byte RIGHT_BIT = 0b0000_0010;
-    private final byte BOTTOM_BIT = 0b0000_0100;
-    private final byte LEFT_BIT = 0b0000_1000;
-    private final byte SOLUTION_BIT = 0b0001_0000;
-
     /**
      * Serializes the {@code maze} into a byte array.
      *
@@ -93,55 +66,4 @@ public class SerializerDeserializerV1 {
             throw new RuntimeException("Can not deserialize Maze from maze data.", e);
         }
     }
-
-    @NotNull
-    private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
-        try {
-            final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
-            constructor.setAccessible(true);
-            return constructor.newInstance(walls, solution);
-        } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
-                                InvocationTargetException e) {
-            throw new RuntimeException("Can not deserialize Tile from maze data.", e);
-        }
-    }
-
-    byte getBitmaskForTile(@NotNull final Tile tile) {
-        byte bitmask = 0;
-        if (tile.hasWallAt(Direction.TOP)) {
-            bitmask |= TOP_BIT;
-        }
-        if (tile.hasWallAt(Direction.RIGHT)) {
-            bitmask |= RIGHT_BIT;
-        }
-        if (tile.hasWallAt(Direction.BOTTOM)) {
-            bitmask |= BOTTOM_BIT;
-        }
-        if (tile.hasWallAt((Direction.LEFT))) {
-            bitmask |= LEFT_BIT;
-        }
-        if (tile.isSolution()) {
-            bitmask |= SOLUTION_BIT;
-        }
-        return bitmask;
-    }
-
-    @NotNull
-    Tile getTileForBitmask(final byte bitmask) {
-        final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
-        if ((bitmask & TOP_BIT) == TOP_BIT) {
-            walls.add(Direction.TOP);
-        }
-        if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
-            walls.add(Direction.RIGHT);
-        }
-        if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
-            walls.add(Direction.BOTTOM);
-        }
-        if ((bitmask & LEFT_BIT) == LEFT_BIT) {
-            walls.add(Direction.LEFT);
-        }
-        final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
-        return createTile(walls, solution);
-    }
 }
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java
index de982a6..bbe42b1 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
+import ch.fritteli.maze.generator.serialization.CommonTileHandler;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeInputStreamV2 extends AbstractMazeInputStream {
@@ -58,7 +59,7 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream {
         for (int y = 0; y < height; y++) {
             for (int x = 0; x < width; x++) {
                 final byte bitmask = this.readByte();
-                tiles[x][y] = SerializerDeserializerV2.getTileForBitmask(bitmask);
+                tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask);
             }
         }
 
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java
index b2305fb..c39ad3d 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
+import ch.fritteli.maze.generator.serialization.CommonTileHandler;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
@@ -45,7 +46,7 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
             for (int x = 0; x < width; x++) {
                 // We .get() it, because we want to crash hard if it is not available.
                 final Tile tile = maze.getTileAt(x, y).get();
-                final byte bitmask = SerializerDeserializerV2.getBitmaskForTile(tile);
+                final byte bitmask = CommonTileHandler.getBitmaskForTile(tile);
                 this.writeByte(bitmask);
             }
         }
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java
index 6e7b199..5b7753e 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java
@@ -1,6 +1,5 @@
 package ch.fritteli.maze.generator.serialization.v2;
 
-import ch.fritteli.maze.generator.model.Direction;
 import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
@@ -9,29 +8,9 @@ import org.jetbrains.annotations.NotNull;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
-import java.util.EnumSet;
 
 /**
- * <pre>
- * decimal hex bin  border
- *       0   0 0000 no border
- *       1   1 0001 top
- *       2   2 0010 right
- *       3   3 0011 top+right
- *       4   4 0100 bottom
- *       5   5 0101 top+bottom
- *       6   6 0110 right+bottom
- *       7   7 0111 top+right+bottom
- *       8   8 1000 left
- *       9   9 1001 top+left
- *      10   a 1010 right+left
- *      11   b 1011 top+right+left
- *      12   c 1100 bottom+left
- *      13   d 1101 top+bottom+left
- *      14   e 1110 right+bottom+left
- *      15   f 1111 top+right+bottom+left
- * </pre>
- * ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==&gt; first bytes are:
+ * Header bytes are:
  * <pre>
  *   byte  hex meaning
  *     00 0x1a magic
@@ -55,12 +34,6 @@ public class SerializerDeserializerV2 {
     final byte MAGIC_BYTE_2 = (byte) 0xb1;
     final byte VERSION_BYTE = 0x02;
 
-    private final byte TOP_BIT = 0b0000_0001;
-    private final byte RIGHT_BIT = 0b0000_0010;
-    private final byte BOTTOM_BIT = 0b0000_0100;
-    private final byte LEFT_BIT = 0b0000_1000;
-    private final byte SOLUTION_BIT = 0b0001_0000;
-
     /**
      * Serializes the {@code maze} into a byte array.
      *
@@ -99,55 +72,4 @@ public class SerializerDeserializerV2 {
             throw new RuntimeException("Can not deserialize Maze from maze data.", e);
         }
     }
-
-    @NotNull
-    private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
-        try {
-            final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
-            constructor.setAccessible(true);
-            return constructor.newInstance(walls, solution);
-        } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
-                                InvocationTargetException e) {
-            throw new RuntimeException("Can not deserialize Tile from maze data.", e);
-        }
-    }
-
-    byte getBitmaskForTile(@NotNull final Tile tile) {
-        byte bitmask = 0;
-        if (tile.hasWallAt(Direction.TOP)) {
-            bitmask |= TOP_BIT;
-        }
-        if (tile.hasWallAt(Direction.RIGHT)) {
-            bitmask |= RIGHT_BIT;
-        }
-        if (tile.hasWallAt(Direction.BOTTOM)) {
-            bitmask |= BOTTOM_BIT;
-        }
-        if (tile.hasWallAt((Direction.LEFT))) {
-            bitmask |= LEFT_BIT;
-        }
-        if (tile.isSolution()) {
-            bitmask |= SOLUTION_BIT;
-        }
-        return bitmask;
-    }
-
-    @NotNull
-    Tile getTileForBitmask(final byte bitmask) {
-        final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
-        if ((bitmask & TOP_BIT) == TOP_BIT) {
-            walls.add(Direction.TOP);
-        }
-        if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
-            walls.add(Direction.RIGHT);
-        }
-        if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
-            walls.add(Direction.BOTTOM);
-        }
-        if ((bitmask & LEFT_BIT) == LEFT_BIT) {
-            walls.add(Direction.LEFT);
-        }
-        final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
-        return createTile(walls, solution);
-    }
 }
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
index 1814008..af809b9 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
+import ch.fritteli.maze.generator.serialization.CommonTileHandler;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
@@ -66,7 +67,7 @@ public class MazeInputStreamV3 extends AbstractMazeInputStream {
         for (int y = 0; y < height; y++) {
             for (int x = 0; x < width; x++) {
                 final byte bitmask = this.readByte();
-                tiles[x][y] = SerializerDeserializerV3.getTileForBitmask(bitmask);
+                tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask);
             }
         }
 
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
index 52b154a..891f0bb 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
+import ch.fritteli.maze.generator.serialization.CommonTileHandler;
 import org.jetbrains.annotations.NotNull;
 
 import java.nio.charset.StandardCharsets;
@@ -52,7 +53,7 @@ public class MazeOutputStreamV3 extends AbstractMazeOutputStream {
             for (int x = 0; x < width; x++) {
                 // We .get() it, because we want to crash hard if it is not available.
                 final Tile tile = maze.getTileAt(x, y).get();
-                final byte bitmask = SerializerDeserializerV3.getBitmaskForTile(tile);
+                final byte bitmask = CommonTileHandler.getBitmaskForTile(tile);
                 this.writeByte(bitmask);
             }
         }
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
index 839e588..ac9375d 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
@@ -1,6 +1,5 @@
 package ch.fritteli.maze.generator.serialization.v3;
 
-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;
@@ -10,29 +9,9 @@ import org.jetbrains.annotations.NotNull;
 import java.io.IOException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
-import java.util.EnumSet;
 
 /**
- * <pre>
- * decimal hex bin  border
- *       0   0 0000 no border
- *       1   1 0001 top
- *       2   2 0010 right
- *       3   3 0011 top+right
- *       4   4 0100 bottom
- *       5   5 0101 top+bottom
- *       6   6 0110 right+bottom
- *       7   7 0111 top+right+bottom
- *       8   8 1000 left
- *       9   9 1001 top+left
- *      10   a 1010 right+left
- *      11   b 1011 top+right+left
- *      12   c 1100 bottom+left
- *      13   d 1101 top+bottom+left
- *      14   e 1110 right+bottom+left
- *      15   f 1111 top+right+bottom+left
- * </pre>
- * ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==&gt; first bytes are:
+ * Header bytes are:
  * <pre>
  *   byte     hex    meaning
  *     00     0x1a   magic
@@ -58,12 +37,6 @@ public class SerializerDeserializerV3 {
     final byte MAGIC_BYTE_2 = (byte) 0xb1;
     final byte VERSION_BYTE = 0x03;
 
-    private final byte TOP_BIT = 0b0000_0001;
-    private final byte RIGHT_BIT = 0b0000_0010;
-    private final byte BOTTOM_BIT = 0b0000_0100;
-    private final byte LEFT_BIT = 0b0000_1000;
-    private final byte SOLUTION_BIT = 0b0001_0000;
-
     /**
      * Serializes the {@code maze} into a byte array.
      *
@@ -116,55 +89,4 @@ public class SerializerDeserializerV3 {
             throw new RuntimeException("Can not deserialize Maze from maze data.", e);
         }
     }
-
-    @NotNull
-    private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
-        try {
-            final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
-            constructor.setAccessible(true);
-            return constructor.newInstance(walls, solution);
-        } catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
-                                InvocationTargetException e) {
-            throw new RuntimeException("Can not deserialize Tile from maze data.", e);
-        }
-    }
-
-    byte getBitmaskForTile(@NotNull final Tile tile) {
-        byte bitmask = 0;
-        if (tile.hasWallAt(Direction.TOP)) {
-            bitmask |= TOP_BIT;
-        }
-        if (tile.hasWallAt(Direction.RIGHT)) {
-            bitmask |= RIGHT_BIT;
-        }
-        if (tile.hasWallAt(Direction.BOTTOM)) {
-            bitmask |= BOTTOM_BIT;
-        }
-        if (tile.hasWallAt((Direction.LEFT))) {
-            bitmask |= LEFT_BIT;
-        }
-        if (tile.isSolution()) {
-            bitmask |= SOLUTION_BIT;
-        }
-        return bitmask;
-    }
-
-    @NotNull
-    Tile getTileForBitmask(final byte bitmask) {
-        final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
-        if ((bitmask & TOP_BIT) == TOP_BIT) {
-            walls.add(Direction.TOP);
-        }
-        if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
-            walls.add(Direction.RIGHT);
-        }
-        if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
-            walls.add(Direction.BOTTOM);
-        }
-        if ((bitmask & LEFT_BIT) == LEFT_BIT) {
-            walls.add(Direction.LEFT);
-        }
-        final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
-        return createTile(walls, solution);
-    }
 }

From 3bf438ffb92dadeb19e42140549bcad098eeb8d2 Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Wed, 18 Dec 2024 01:03:53 +0100
Subject: [PATCH 4/6] Add and fix tests for the Serializers.

---
 .../maze/generator/algorithm/Wilson.java      | 15 ++++++
 .../v1/SerializerDeserializerV1Test.java      |  6 +++
 .../v2/SerializerDeserializerV2Test.java      | 46 +++++++++++++++++++
 .../v3/SerializerDeserializerV3Test.java      | 40 ++++++++++++++++
 4 files changed, 107 insertions(+)
 create mode 100644 src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java
 create mode 100644 src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java

diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
index e53acce..af55d7e 100644
--- a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
@@ -86,6 +86,21 @@ public class Wilson extends AbstractMazeGeneratorAlgorithm {
         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
diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java
index 2fd819b..4d8a392 100644
--- a/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java
+++ b/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java
@@ -13,6 +13,8 @@ class SerializerDeserializerV1Test {
         new RandomDepthFirst(expected).run();
         final byte[] bytes = SerializerDeserializerV1.serialize(expected);
         final Maze result = SerializerDeserializerV1.deserialize(bytes);
+        assertThat(result.getAlgorithm()).isNull();
+        expected.setAlgorithm(null);
         assertThat(result).isEqualTo(expected);
     }
 
@@ -22,6 +24,8 @@ class SerializerDeserializerV1Test {
         new RandomDepthFirst(expected).run();
         final byte[] bytes = SerializerDeserializerV1.serialize(expected);
         final Maze result = SerializerDeserializerV1.deserialize(bytes);
+        assertThat(result.getAlgorithm()).isNull();
+        expected.setAlgorithm(null);
         assertThat(result).isEqualTo(expected);
     }
 
@@ -31,6 +35,8 @@ class SerializerDeserializerV1Test {
         new RandomDepthFirst(expected).run();
         final byte[] bytes = SerializerDeserializerV1.serialize(expected);
         final Maze result = SerializerDeserializerV1.deserialize(bytes);
+        assertThat(result.getAlgorithm()).isNull();
+        expected.setAlgorithm(null);
         assertThat(result).isEqualTo(expected);
     }
 }
diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java
new file mode 100644
index 0000000..c4898c7
--- /dev/null
+++ b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java
@@ -0,0 +1,46 @@
+package ch.fritteli.maze.generator.serialization.v2;
+
+import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
+import ch.fritteli.maze.generator.algorithm.Wilson;
+import ch.fritteli.maze.generator.model.Maze;
+import ch.fritteli.maze.generator.model.Position;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SerializerDeserializerV2Test {
+    @Test
+    void testSerializeDeserializeTiny() throws IOException {
+        final Maze expected = new Maze(2, 2, 255, new Position(0, 0), new Position(1, 1));
+        new RandomDepthFirst(expected).run();
+        final byte[] bytes = SerializerDeserializerV2.serialize(expected);
+        final Maze result = SerializerDeserializerV2.deserialize(bytes);
+        assertThat(result.getAlgorithm()).isNull();
+        expected.setAlgorithm(null);
+        assertThat(result).isEqualTo(expected);
+    }
+
+    @Test
+    void testSerializeDeserializeMedium() throws IOException {
+        final Maze expected = new Maze(20, 20, -271828182846L);
+        new Wilson(expected).run();
+        final byte[] bytes = SerializerDeserializerV2.serialize(expected);
+        final Maze result = SerializerDeserializerV2.deserialize(bytes);
+        assertThat(result.getAlgorithm()).isNull();
+        expected.setAlgorithm(null);
+        assertThat(result).isEqualTo(expected);
+    }
+
+    @Test
+    void testSerializeDeserializeLarge() throws IOException {
+        final Maze expected = new Maze(200, 320, 3141592653589793238L);
+        new Wilson(expected).run();
+        final byte[] bytes = SerializerDeserializerV2.serialize(expected);
+        final Maze result = SerializerDeserializerV2.deserialize(bytes);
+        assertThat(result.getAlgorithm()).isNull();
+        expected.setAlgorithm(null);
+        assertThat(result).isEqualTo(expected);
+    }
+}
diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java
new file mode 100644
index 0000000..fde2008
--- /dev/null
+++ b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java
@@ -0,0 +1,40 @@
+package ch.fritteli.maze.generator.serialization.v3;
+
+import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
+import ch.fritteli.maze.generator.algorithm.Wilson;
+import ch.fritteli.maze.generator.model.Maze;
+import ch.fritteli.maze.generator.model.Position;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SerializerDeserializerV3Test {
+    @Test
+    void testSerializeDeserializeTiny() throws IOException {
+        final Maze expected = new Maze(2, 2, 255, new Position(0, 0), new Position(1, 1));
+        new RandomDepthFirst(expected).run();
+        final byte[] bytes = SerializerDeserializerV3.serialize(expected);
+        final Maze result = SerializerDeserializerV3.deserialize(bytes);
+        assertThat(result).isEqualTo(expected);
+    }
+
+    @Test
+    void testSerializeDeserializeMedium() throws IOException {
+        final Maze expected = new Maze(20, 20, -271828182846L);
+        new Wilson(expected).run();
+        final byte[] bytes = SerializerDeserializerV3.serialize(expected);
+        final Maze result = SerializerDeserializerV3.deserialize(bytes);
+        assertThat(result).isEqualTo(expected);
+    }
+
+    @Test
+    void testSerializeDeserializeLarge() throws IOException {
+        final Maze expected = new Maze(200, 320, 3141592653589793238L);
+        new Wilson(expected).run();
+        final byte[] bytes = SerializerDeserializerV3.serialize(expected);
+        final Maze result = SerializerDeserializerV3.deserialize(bytes);
+        assertThat(result).isEqualTo(expected);
+    }
+}

From 9599be21b3eb9a965c3494930a44daec1800e69b Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Wed, 18 Dec 2024 01:06:33 +0100
Subject: [PATCH 5/6] Make the Wilson test actually test something.

---
 .../maze/generator/algorithm/WilsonTest.java  | 33 +++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
index 8e46ba5..a4f8c9f 100644
--- a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
+++ b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
@@ -4,12 +4,41 @@ 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() {
-        final Maze maze = new Maze(50, 10, 0);
+        // arrange
+        final Maze maze = new Maze(10, 10, 0);
         final Wilson wilson = new Wilson(maze);
+
+        // act
         wilson.run();
-        System.out.println(TextRenderer.newInstance().render(maze));
+
+        // assert
+        final String textRepresentation = TextRenderer.newInstance().render(maze);
+        assertThat(textRepresentation).isEqualTo("""
+                ╷ ╶─────┬─────┬───┬─┐
+                │       │     │   │ │
+                │ ╷ ╶─┬─┴─┬─╴ ╵ ╷ │ │
+                │ │   │   │     │ │ │
+                │ └─┐ │ ╶─┼─┬─┬─┘ │ │
+                │   │ │   │ │ │   │ │
+                │ ╷ │ └─╴ │ ╵ ╵ ╶─┘ │
+                │ │ │     │         │
+                ├─┴─┘ ╷ ┌─┴───┐ ╷ ╶─┤
+                │     │ │     │ │   │
+                │ ╷ ┌─┘ └───╴ │ └─┬─┤
+                │ │ │         │   │ │
+                ├─┴─┘ ┌───╴ ╷ └─┐ ╵ │
+                │     │     │   │   │
+                │ ╶─┬─┴─╴ ┌─┘ ╶─┘ ╶─┤
+                │   │     │         │
+                ├─╴ ├─╴ ┌─┘ ╶─┐ ╶───┤
+                │   │   │     │     │
+                ├───┴─╴ └─┐ ╶─┴───┐ │
+                │         │       │ │
+                └─────────┴───────┘ ╵""");
     }
 }

From 5e6965fa557196d436496bacde43c9c1a5af73ee Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Tue, 24 Dec 2024 02:50:45 +0100
Subject: [PATCH 6/6] Refactoring and clean-up. It is better organized, but the
 very basic data structures of the maze are not really suited for Wilson's
 algorithm.

Another refactoring will be required.
---
 pom.xml                                       |   5 +
 .../maze/generator/algorithm/Wilson.java      | 312 ------------------
 .../algorithm/wilson/MazeSolver.java          |  62 ++++
 .../maze/generator/algorithm/wilson/Path.java |  91 +++++
 .../algorithm/wilson/PathsBuilder.java        | 131 ++++++++
 .../generator/algorithm/wilson/Wilson.java    |  96 ++++++
 .../fritteli/maze/generator/model/Tile.java   |  19 +-
 .../fritteli/maze/generator/model/Walls.java  |  23 +-
 .../serialization/MazeConstants.java          |   9 +
 .../serialization/v1/MazeInputStreamV1.java   |   5 +-
 .../serialization/v1/MazeOutputStreamV1.java  |   5 +-
 .../v1/SerializerDeserializerV1.java          |   4 +-
 .../serialization/v2/MazeInputStreamV2.java   |   5 +-
 .../serialization/v2/MazeOutputStreamV2.java  |   5 +-
 .../v2/SerializerDeserializerV2.java          |   5 +-
 .../serialization/v3/MazeInputStreamV3.java   |   5 +-
 .../serialization/v3/MazeOutputStreamV3.java  |   5 +-
 .../v3/SerializerDeserializerV3.java          |   5 +-
 .../maze/generator/algorithm/WilsonTest.java  |  44 ---
 .../algorithm/wilson/WilsonTest.java          | 100 ++++++
 .../maze/generator/model/WallsTest.java       |   4 +-
 .../v2/SerializerDeserializerV2Test.java      |   2 +-
 .../v3/SerializerDeserializerV3Test.java      |  11 +-
 23 files changed, 557 insertions(+), 396 deletions(-)
 delete mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java
 create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java
 delete mode 100644 src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
 create mode 100644 src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java

diff --git a/pom.xml b/pom.xml
index dcdec02..54216db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,11 @@
             <groupId>io.vavr</groupId>
             <artifactId>vavr</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>33.2.1-jre</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.pdfbox</groupId>
             <artifactId>pdfbox</artifactId>
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
deleted file mode 100644
index af55d7e..0000000
--- a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
+++ /dev/null
@@ -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[", "->", "]");
-            }
-        }
-    }
-}
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java
new file mode 100644
index 0000000..f84a20c
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java
@@ -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());
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java
new file mode 100644
index 0000000..ab18ec2
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java
@@ -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[", "->", "]");
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java
new file mode 100644
index 0000000..098ed81
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java
@@ -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));
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java
new file mode 100644
index 0000000..1587e6e
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java
@@ -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());
+    }
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/model/Tile.java b/src/main/java/ch/fritteli/maze/generator/model/Tile.java
index c18ef5d..b4428f1 100644
--- a/src/main/java/ch/fritteli/maze/generator/model/Tile.java
+++ b/src/main/java/ch/fritteli/maze/generator/model/Tile.java
@@ -1,6 +1,6 @@
 package ch.fritteli.maze.generator.model;
 
-import io.vavr.collection.Stream;
+import io.vavr.collection.Vector;
 import io.vavr.control.Option;
 import lombok.AccessLevel;
 import lombok.EqualsAndHashCode;
@@ -17,6 +17,7 @@ import java.util.Random;
 @ToString
 public class Tile {
     final Walls walls = new Walls();
+    @EqualsAndHashCode.Exclude
     boolean visited = false;
     @Getter
     boolean solution = false;
@@ -65,13 +66,23 @@ public class Tile {
         this.walls.set(direction);
     }
 
+    @NotNull
+    public EnumSet<Direction> getOpenDirections() {
+        return this.walls.getOpen();
+    }
+
+    @NotNull
     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()) {
             return Option.none();
         }
-        final int index = random.nextInt(availableDirections.length());
-        return Option.of(availableDirections.get(index));
+        if (availableDirections.size() == 1) {
+            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) {
diff --git a/src/main/java/ch/fritteli/maze/generator/model/Walls.java b/src/main/java/ch/fritteli/maze/generator/model/Walls.java
index 8a4b340..4a899c7 100644
--- a/src/main/java/ch/fritteli/maze/generator/model/Walls.java
+++ b/src/main/java/ch/fritteli/maze/generator/model/Walls.java
@@ -1,21 +1,17 @@
 package ch.fritteli.maze.generator.model;
 
-import io.vavr.collection.Stream;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
 
 @EqualsAndHashCode
 @ToString
 public class Walls {
-    private final SortedSet<Direction> directions = new TreeSet<>();
-    private final Set<Direction> sealed = new HashSet<>();
+    private final EnumSet<Direction> directions = EnumSet.noneOf(Direction.class);
+    @EqualsAndHashCode.Exclude
+    private final EnumSet<Direction> sealed = EnumSet.noneOf(Direction.class);
 
     public void set(@NotNull final Direction direction) {
         this.directions.add(direction);
@@ -36,9 +32,11 @@ public class Walls {
         return this.directions.contains(direction);
     }
 
-    public Stream<Direction> getUnsealedSet() {
-        return Stream.ofAll(this.directions)
-                .removeAll(this.sealed);
+    @NotNull
+    public EnumSet<Direction> getUnsealedSet() {
+        final EnumSet<Direction> result = EnumSet.copyOf(this.directions);
+        result.removeAll(this.sealed);
+        return result;
     }
 
     public void seal(@NotNull final Direction direction) {
@@ -51,4 +49,9 @@ public class Walls {
     public void unseal(@NotNull final Direction direction) {
         this.sealed.remove(direction);
     }
+
+    @NotNull
+    public EnumSet<Direction> getOpen() {
+        return EnumSet.complementOf(this.directions);
+    }
 }
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java b/src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java
new file mode 100644
index 0000000..4b9c200
--- /dev/null
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java
@@ -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;
+}
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java
index 5ec5c7c..2532787 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
 import ch.fritteli.maze.generator.serialization.CommonTileHandler;
+import ch.fritteli.maze.generator.serialization.MazeConstants;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeInputStreamV1 extends AbstractMazeInputStream {
@@ -18,11 +19,11 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
         // 01 0xb1 magic
         // 02 0x01 version
         final byte magic1 = this.readByte();
-        if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) {
+        if (magic1 != MazeConstants.MAGIC_BYTE_1) {
             throw new IllegalArgumentException("Invalid maze data.");
         }
         final byte magic2 = this.readByte();
-        if (magic2 != SerializerDeserializerV1.MAGIC_BYTE_2) {
+        if (magic2 != MazeConstants.MAGIC_BYTE_2) {
             throw new IllegalArgumentException("Invalid maze data.");
         }
         final int version = this.readByte();
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java
index aa8d3c2..7085b04 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java
@@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
 import ch.fritteli.maze.generator.serialization.CommonTileHandler;
+import ch.fritteli.maze.generator.serialization.MazeConstants;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
@@ -13,8 +14,8 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
         // 00 0x1a magic
         // 01 0xb1 magic
         // 02 0x02 version
-        this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1);
-        this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2);
+        this.writeByte(MazeConstants.MAGIC_BYTE_1);
+        this.writeByte(MazeConstants.MAGIC_BYTE_2);
         this.writeByte(SerializerDeserializerV1.VERSION_BYTE);
     }
 
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
index f9f02d9..9f4a561 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java
@@ -24,9 +24,7 @@ import java.lang.reflect.InvocationTargetException;
  */
 @UtilityClass
 public class SerializerDeserializerV1 {
-    final byte MAGIC_BYTE_1 = 0x1a;
-    final byte MAGIC_BYTE_2 = (byte) 0xb1;
-    final byte VERSION_BYTE = 0x01;
+    public final byte VERSION_BYTE = 0x01;
 
     /**
      * Serializes the {@code maze} into a byte array.
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java
index bbe42b1..036d636 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java
@@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
 import ch.fritteli.maze.generator.serialization.CommonTileHandler;
+import ch.fritteli.maze.generator.serialization.MazeConstants;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeInputStreamV2 extends AbstractMazeInputStream {
@@ -19,11 +20,11 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream {
         // 01 0xb1 magic
         // 02 0x02 version
         final byte magic1 = this.readByte();
-        if (magic1 != SerializerDeserializerV2.MAGIC_BYTE_1) {
+        if (magic1 != MazeConstants.MAGIC_BYTE_1) {
             throw new IllegalArgumentException("Invalid maze data.");
         }
         final byte magic2 = this.readByte();
-        if (magic2 != SerializerDeserializerV2.MAGIC_BYTE_2) {
+        if (magic2 != MazeConstants.MAGIC_BYTE_2) {
             throw new IllegalArgumentException("Invalid maze data.");
         }
         final int version = this.readByte();
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java
index c39ad3d..1cb8f78 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java
@@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
 import ch.fritteli.maze.generator.serialization.CommonTileHandler;
+import ch.fritteli.maze.generator.serialization.MazeConstants;
 import org.jetbrains.annotations.NotNull;
 
 public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
@@ -14,8 +15,8 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
         // 00 0x1a magic
         // 01 0xb1 magic
         // 02 0x02 version
-        this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_1);
-        this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_2);
+        this.writeByte(MazeConstants.MAGIC_BYTE_1);
+        this.writeByte(MazeConstants.MAGIC_BYTE_2);
         this.writeByte(SerializerDeserializerV2.VERSION_BYTE);
     }
 
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java
index 5b7753e..99f8e22 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java
@@ -29,10 +29,7 @@ import java.lang.reflect.InvocationTargetException;
  */
 @UtilityClass
 public class SerializerDeserializerV2 {
-
-    final byte MAGIC_BYTE_1 = 0x1a;
-    final byte MAGIC_BYTE_2 = (byte) 0xb1;
-    final byte VERSION_BYTE = 0x02;
+    public final byte VERSION_BYTE = 0x02;
 
     /**
      * Serializes the {@code maze} into a byte array.
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
index af809b9..8a0dc26 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java
@@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
 import ch.fritteli.maze.generator.serialization.CommonTileHandler;
+import ch.fritteli.maze.generator.serialization.MazeConstants;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
@@ -22,11 +23,11 @@ public class MazeInputStreamV3 extends AbstractMazeInputStream {
         // 01 0xb1 magic
         // 02 0x03 version
         final byte magic1 = this.readByte();
-        if (magic1 != SerializerDeserializerV3.MAGIC_BYTE_1) {
+        if (magic1 != MazeConstants.MAGIC_BYTE_1) {
             throw new IllegalArgumentException("Invalid maze data.");
         }
         final byte magic2 = this.readByte();
-        if (magic2 != SerializerDeserializerV3.MAGIC_BYTE_2) {
+        if (magic2 != MazeConstants.MAGIC_BYTE_2) {
             throw new IllegalArgumentException("Invalid maze data.");
         }
         final int version = this.readByte();
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
index 891f0bb..c9da80e 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java
@@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
 import ch.fritteli.maze.generator.model.Tile;
 import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
 import ch.fritteli.maze.generator.serialization.CommonTileHandler;
+import ch.fritteli.maze.generator.serialization.MazeConstants;
 import org.jetbrains.annotations.NotNull;
 
 import java.nio.charset.StandardCharsets;
@@ -16,8 +17,8 @@ public class MazeOutputStreamV3 extends AbstractMazeOutputStream {
         // 00 0x1a magic
         // 01 0xb1 magic
         // 02 0x03 version
-        this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_1);
-        this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_2);
+        this.writeByte(MazeConstants.MAGIC_BYTE_1);
+        this.writeByte(MazeConstants.MAGIC_BYTE_2);
         this.writeByte(SerializerDeserializerV3.VERSION_BYTE);
     }
 
diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
index ac9375d..1da19cc 100644
--- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
+++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java
@@ -32,10 +32,7 @@ import java.lang.reflect.InvocationTargetException;
  */
 @UtilityClass
 public class SerializerDeserializerV3 {
-
-    final byte MAGIC_BYTE_1 = 0x1a;
-    final byte MAGIC_BYTE_2 = (byte) 0xb1;
-    final byte VERSION_BYTE = 0x03;
+    public final byte VERSION_BYTE = 0x03;
 
     /**
      * Serializes the {@code maze} into a byte array.
diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
deleted file mode 100644
index a4f8c9f..0000000
--- a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java
+++ /dev/null
@@ -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("""
-                ╷ ╶─────┬─────┬───┬─┐
-                │       │     │   │ │
-                │ ╷ ╶─┬─┴─┬─╴ ╵ ╷ │ │
-                │ │   │   │     │ │ │
-                │ └─┐ │ ╶─┼─┬─┬─┘ │ │
-                │   │ │   │ │ │   │ │
-                │ ╷ │ └─╴ │ ╵ ╵ ╶─┘ │
-                │ │ │     │         │
-                ├─┴─┘ ╷ ┌─┴───┐ ╷ ╶─┤
-                │     │ │     │ │   │
-                │ ╷ ┌─┘ └───╴ │ └─┬─┤
-                │ │ │         │   │ │
-                ├─┴─┘ ┌───╴ ╷ └─┐ ╵ │
-                │     │     │   │   │
-                │ ╶─┬─┴─╴ ┌─┘ ╶─┘ ╶─┤
-                │   │     │         │
-                ├─╴ ├─╴ ┌─┘ ╶─┐ ╶───┤
-                │   │   │     │     │
-                ├───┴─╴ └─┐ ╶─┴───┐ │
-                │         │       │ │
-                └─────────┴───────┘ ╵""");
-    }
-}
diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java
new file mode 100644
index 0000000..4457c42
--- /dev/null
+++ b/src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java
@@ -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("""
+                ╷│╶─────┬─────┬─────┐
+                │╰───╮  │     │     │
+                ├─╴ ╷│╶─┴─┬─╴ ├─┐ ╶─┤
+                │   │╰─╮  │   │ │   │
+                │ ╷ └─┐│╶─┘ ┌─┘ ╵ ╷ │
+                │ │   ││    │     │ │
+                │ │ ╶─┤│╶─┐ ╵ ╷ ╶─┤ │
+                │ │   │╰─╮│   │   │ │
+                │ ├─┐ ├─╴│└─┐ │ ╶─┼─┤
+                │ │ │ │╭─╯  │ │   │ │
+                ├─┘ └─┘│╶─┐ └─┤ ╶─┤ │
+                │      │  │   │   │ │
+                ├───┐ ╷│╶─┴───┴─┐ │ │
+                │   │ │╰───╮    │ │ │
+                ├─╴ └─┼─╴ ╷│╷ ┌─┴─┘ │
+                │     │   │││ │╭───╮│
+                │ ╷ ╶─┴─┬─┤│├─┘│╶─┐││
+                │ │     │ │││╭─╯  │││
+                │ ├─╴ ╷ ╵ ╵│╵│┌─╴ │││
+                │ │   │    ╰─╯│   │││
+                └─┴───┴───────┴───┘│╵""");
+    }
+}
diff --git a/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java b/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java
index 7be7269..d0f861f 100644
--- a/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java
+++ b/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java
@@ -3,6 +3,8 @@ package ch.fritteli.maze.generator.model;
 import io.vavr.collection.Stream;
 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.assertThatExceptionOfType;
 
@@ -113,7 +115,7 @@ class WallsTest {
         final Walls sut = new Walls();
 
         // act
-        Stream<Direction> result = sut.getUnsealedSet();
+        EnumSet<Direction> result = sut.getUnsealedSet();
 
         // assert
         assertThat(result).isEmpty();
diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java
index c4898c7..a89e947 100644
--- a/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java
+++ b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java
@@ -1,7 +1,7 @@
 package ch.fritteli.maze.generator.serialization.v2;
 
 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.Position;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java
index fde2008..801c677 100644
--- a/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java
+++ b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java
@@ -1,7 +1,7 @@
 package ch.fritteli.maze.generator.serialization.v3;
 
 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.Position;
 import org.junit.jupiter.api.Test;
@@ -37,4 +37,13 @@ class SerializerDeserializerV3Test {
         final Maze result = SerializerDeserializerV3.deserialize(bytes);
         assertThat(result).isEqualTo(expected);
     }
+
+    @Test
+    void testSerializeDeserializeLargeRandom() throws IOException {
+        final Maze expected = new Maze(200, 320, 3141592653589793238L);
+        new RandomDepthFirst(expected).run();
+        final byte[] bytes = SerializerDeserializerV3.serialize(expected);
+        final Maze result = SerializerDeserializerV3.deserialize(bytes);
+        assertThat(result).isEqualTo(expected);
+    }
 }