> {
text = TEXT_RENDERER.setRenderSolution(false).render(labyrinth).strip();
solution = TEXT_RENDERER.setRenderSolution(true).render(labyrinth).strip();
}
+ final Path targetLabyrinthFile = this.targetLabyrinthFile.get();
+ final Path targetSolutionFile = this.targetSolutionFile.get();
try {
- Files.write(this.targetLabyrinthFile, text.getBytes(StandardCharsets.UTF_8));
- Files.write(this.targetSolutionFile, solution.getBytes(StandardCharsets.UTF_8));
+ Files.write(targetLabyrinthFile, text.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
- System.err.println("Failed writing to file " + this.targetLabyrinthFile.normalize().toString());
- e.printStackTrace();
+ log.error("Failed writing to file " + targetLabyrinthFile.normalize(), e);
}
- return List.of(this.targetLabyrinthFile, this.targetSolutionFile);
+ try {
+ Files.write(targetSolutionFile, solution.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ log.error("Failed writing to file " + targetSolutionFile.normalize());
+ }
+ return List.of(targetLabyrinthFile, targetSolutionFile);
}
}
diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthInputStream.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthInputStream.java
new file mode 100644
index 0000000..1d13101
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthInputStream.java
@@ -0,0 +1,36 @@
+package ch.fritteli.labyrinth.generator.serialization;
+
+import lombok.NonNull;
+
+import java.io.ByteArrayInputStream;
+
+public class LabyrinthInputStream extends ByteArrayInputStream {
+ public LabyrinthInputStream(@NonNull final byte[] buf) {
+ super(buf);
+ }
+
+ public byte readByte() {
+ final int read = this.read();
+ if (read == -1) {
+ // end of stream reached
+ throw new ArrayIndexOutOfBoundsException("End of stream reached. Cannot read more bytes.");
+ }
+ return (byte) read;
+ }
+
+ public int readInt() {
+ int result = 0;
+ result |= (0xff & this.readByte()) << 24;
+ result |= (0xff & this.readByte()) << 16;
+ result |= (0xff & this.readByte()) << 8;
+ result |= 0xff & this.readByte();
+ return result;
+ }
+
+ public long readLong() {
+ long result = 0;
+ result |= ((long) this.readInt()) << 32;
+ result |= 0xffffffffL & this.readInt();
+ return result;
+ }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java
new file mode 100644
index 0000000..6d2327f
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java
@@ -0,0 +1,21 @@
+package ch.fritteli.labyrinth.generator.serialization;
+
+import java.io.ByteArrayOutputStream;
+
+public class LabyrinthOutputStream extends ByteArrayOutputStream {
+ public void writeByte(final byte value) {
+ this.write(value);
+ }
+
+ public void writeInt(final int value) {
+ this.write(value >> 24);
+ this.write(value >> 16);
+ this.write(value >> 8);
+ this.write(value);
+ }
+
+ public void writeLong(final long value) {
+ this.writeInt((int) (value >> 32));
+ this.writeInt((int) value);
+ }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializer.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializer.java
new file mode 100644
index 0000000..ae4df70
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializer.java
@@ -0,0 +1,208 @@
+package ch.fritteli.labyrinth.generator.serialization;
+
+import ch.fritteli.labyrinth.generator.model.Direction;
+import ch.fritteli.labyrinth.generator.model.Labyrinth;
+import ch.fritteli.labyrinth.generator.model.Tile;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.EnumSet;
+
+/**
+ *
+ * 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
+ *
+ * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls
+ * ==> first bytes are:
+ *
+ * byte hex meaning
+ * 00 0x1a magic
+ * 01 0xb1 magic
+ * 02 0x00 version (0x00 -> dev / unstable; will be bumped to 0x01 once stabilized)
+ * 03..06 width (int)
+ * 07..10 height (int)
+ * 11..18 random seed number (long)
+ * 19.. tiles
+ *
+ * exteaneous space (poss. last nibble) is ignored.
+ */
+@UtilityClass
+public class SerializerDeserializer {
+ private final byte MAGIC_BYTE_1 = 0x1a;
+ private final byte MAGIC_BYTE_2 = (byte) 0xb1;
+ private final byte VERSION_BYTE = 0x01;
+
+ private final byte TOP_BIT = 0b0000_0001;
+ private final byte RIGHT_BIT = 0b0000_0010;
+ private final byte BOTTOM_BIT = 0b0000_0100;
+ private final byte LEFT_BIT = 0b0000_1000;
+ private final byte SOLUTION_BIT = 0b0001_0000;
+
+ /**
+ * Serializes the {@code labyrinth} into a byte array.
+ *
+ * @param labyrinth The labyrinth to be serialized.
+ * @return The resulting byte array.
+ */
+ @NonNull
+ public byte[] serialize(@NonNull final Labyrinth labyrinth) {
+ final LabyrinthOutputStream stream = new LabyrinthOutputStream();
+ writeHeader(stream);
+ writeLabyrinthData(stream, labyrinth);
+ return stream.toByteArray();
+ }
+
+ /**
+ * Deserializes the byte array into an instance of {@link Labyrinth}.
+ *
+ * @param bytes The byte array to be deserialized.
+ * @return An instance of {@link Labyrinth}.
+ */
+ @NonNull
+ public Labyrinth deserialize(@NonNull final byte[] bytes) {
+ final LabyrinthInputStream stream = new LabyrinthInputStream(bytes);
+ checkHeader(stream);
+ return readLabyrinthData(stream);
+ }
+
+ private static void writeHeader(@NonNull final LabyrinthOutputStream stream) {
+ stream.writeByte(MAGIC_BYTE_1);
+ stream.writeByte(MAGIC_BYTE_2);
+ stream.writeByte(VERSION_BYTE);
+
+ }
+
+ private static void checkHeader(@NonNull final LabyrinthInputStream stream) {
+ final byte magic1 = stream.readByte();
+ if (magic1 != MAGIC_BYTE_1) {
+ throw new IllegalArgumentException("Invalid labyrinth data.");
+ }
+ final byte magic2 = stream.readByte();
+ if (magic2 != MAGIC_BYTE_2) {
+ throw new IllegalArgumentException("Invalid labyrinth data.");
+ }
+ final int version = stream.readByte();
+ if (version != VERSION_BYTE) {
+ throw new IllegalArgumentException("Unknown Labyrinth data version: " + version);
+ }
+ }
+
+ private static void writeLabyrinthData(@NonNull final LabyrinthOutputStream stream, @NonNull final Labyrinth labyrinth) {
+ final long randomSeed = labyrinth.getRandomSeed();
+ final int width = labyrinth.getWidth();
+ final int height = labyrinth.getHeight();
+ stream.writeLong(randomSeed);
+ stream.writeInt(width);
+ stream.writeInt(height);
+
+ 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 = labyrinth.getTileAt(x, y).get();
+ final byte bitmask = getBitmaskForTile(tile);
+ stream.writeByte(bitmask);
+ }
+ }
+
+ }
+
+ @NonNull
+ private static Labyrinth readLabyrinthData(@NonNull final LabyrinthInputStream stream) {
+ final long randomSeed = stream.readLong();
+ final int width = stream.readInt();
+ final int height = stream.readInt();
+
+ 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 = stream.readByte();
+ tiles[x][y] = getTileForBitmask(bitmask);
+ }
+ }
+
+ return createLabyrinth(tiles, width, height, randomSeed);
+ }
+
+ @NonNull
+ private Labyrinth createLabyrinth(@NonNull final Tile[][] field, final int width, final int height, final long randomSeed) {
+ try {
+ @NonNull final Constructor constructor = Labyrinth.class.getDeclaredConstructor(Tile[][].class, Integer.TYPE, Integer.TYPE, Long.TYPE);
+ constructor.setAccessible(true);
+ return constructor.newInstance(field, width, height, randomSeed);
+ } catch (final NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
+ throw new RuntimeException("Can not deserialize Labyrinth from labyrinth data.", e);
+ }
+ }
+
+ @NonNull
+ private Tile createTile(@NonNull final EnumSet walls, boolean solution) {
+ try {
+ @NonNull final Constructor constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
+ constructor.setAccessible(true);
+ return constructor.newInstance(walls, solution);
+ } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Can not deserialize Tile from labyrinth data.", e);
+ }
+ }
+
+ private byte getBitmaskForTile(@NonNull 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;
+ }
+
+ @NonNull
+ private Tile getTileForBitmask(final byte bitmask) {
+ final EnumSet 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/main.iml b/src/main/main.iml
deleted file mode 100644
index 908ad4f..0000000
--- a/src/main/main.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..fd7cd55
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/src/test/java/ch/fritteli/labyrinth/generator/renderer/text/CharDefinitionTest.java b/src/test/java/ch/fritteli/labyrinth/generator/renderer/text/CharDefinitionTest.java
index 818771d..eefc1a9 100644
--- a/src/test/java/ch/fritteli/labyrinth/generator/renderer/text/CharDefinitionTest.java
+++ b/src/test/java/ch/fritteli/labyrinth/generator/renderer/text/CharDefinitionTest.java
@@ -44,4 +44,16 @@ class CharDefinitionTest {
assertEquals("┤", new CharDefinition(true, true, true, false, true).toString());
assertEquals("┼", new CharDefinition(true, true, true, true, true).toString());
}
+
+ @Test
+ void testBuilderMethods() {
+ assertEquals(new CharDefinition(true, false, false, false, false), new CharDefinition().up());
+ assertEquals(new CharDefinition(false, true, false, false, false), new CharDefinition().down());
+ assertEquals(new CharDefinition(false, false, true, false, false), new CharDefinition().left());
+ assertEquals(new CharDefinition(false, false, false, true, false), new CharDefinition().right());
+ assertEquals(new CharDefinition(false, false, false, false, true), new CharDefinition().solution());
+ assertEquals(new CharDefinition(true, true, false, false, false), new CharDefinition().vertical());
+ assertEquals(new CharDefinition(false, false, true, true, false), new CharDefinition().horizontal());
+ assertEquals(new CharDefinition(true, true, true, true, true), new CharDefinition().vertical().horizontal().solution());
+ }
}
diff --git a/src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java b/src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java
new file mode 100644
index 0000000..4839e9b
--- /dev/null
+++ b/src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java
@@ -0,0 +1,33 @@
+package ch.fritteli.labyrinth.generator.serialization;
+
+import ch.fritteli.labyrinth.generator.model.Labyrinth;
+import lombok.NonNull;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SerializerDeserializerTest {
+ @Test
+ void testSerializeDeserializeTiny() {
+ @NonNull final Labyrinth expected = new Labyrinth(2, 2, 255);
+ @NonNull final byte[] bytes = SerializerDeserializer.serialize(expected);
+ @NonNull final Labyrinth result = SerializerDeserializer.deserialize(bytes);
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testSerializeDeserializeMedium() {
+ @NonNull final Labyrinth expected = new Labyrinth(20, 20, -271828182846L);
+ @NonNull final byte[] bytes = SerializerDeserializer.serialize(expected);
+ @NonNull final Labyrinth result = SerializerDeserializer.deserialize(bytes);
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testSerializeDeserializeLarge() {
+ @NonNull final Labyrinth expected = new Labyrinth(200, 320, 3141592653589793238L);
+ @NonNull final byte[] bytes = SerializerDeserializer.serialize(expected);
+ @NonNull final Labyrinth result = SerializerDeserializer.deserialize(bytes);
+ assertEquals(expected, result);
+ }
+}
diff --git a/src/test/test.iml b/src/test/test.iml
deleted file mode 100644
index a0e49a3..0000000
--- a/src/test/test.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file