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] 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