Add Serialization V3: Include the name of the algorithm.
This commit is contained in:
parent
cdfa1f4476
commit
9579066cb5
13 changed files with 523 additions and 139 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue