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> - * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls - * ==> first bytes are: + * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> 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> + * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> 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