From 6302aaa5e833857c3cd04ebd9c46923f79548584 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 16 Apr 2023 23:38:01 +0200 Subject: [PATCH] Feat: Make start and end of a labyrinth configurable. Also, small refactoring. --- .../labyrinth/generator/Generator.java | 98 ++++++++++++ .../ch/fritteli/labyrinth/generator/Main.java | 1 + .../labyrinth/generator/model/Labyrinth.java | 108 ++++--------- .../generator/renderer/json/Generator.java | 9 ++ .../AbstractLabyrinthInputStream.java | 42 +++++ .../AbstractLabyrinthOutputStream.java | 28 ++++ .../serialization/LabyrinthInputStream.java | 74 --------- .../serialization/LabyrinthOutputStream.java | 50 ------ .../v1/LabyrinthInputStreamV1.java | 51 ++++++ .../v1/LabyrinthOutputStreamV1.java | 35 ++++ .../SerializerDeserializerV1.java} | 8 +- .../v2/LabyrinthInputStreamV2.java | 69 ++++++++ .../v2/LabyrinthOutputStreamV2.java | 53 +++++++ .../v2/SerializerDeserializerV2.java | 150 ++++++++++++++++++ src/main/resources/labyrinth.schema.json | 29 +++- .../SerializerDeserializerTest.java | 32 ---- .../v1/SerializerDeserializerV1Test.java | 36 +++++ 17 files changed, 639 insertions(+), 234 deletions(-) create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/Generator.java create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthInputStream.java create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthOutputStream.java delete mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthInputStream.java delete mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthInputStreamV1.java create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthOutputStreamV1.java rename src/main/java/ch/fritteli/labyrinth/generator/serialization/{SerializerDeserializer.java => v1/SerializerDeserializerV1.java} (95%) create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthInputStreamV2.java create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthOutputStreamV2.java create mode 100644 src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/SerializerDeserializerV2.java delete mode 100644 src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java create mode 100644 src/test/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1Test.java diff --git a/src/main/java/ch/fritteli/labyrinth/generator/Generator.java b/src/main/java/ch/fritteli/labyrinth/generator/Generator.java new file mode 100644 index 0000000..fa6c687 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/Generator.java @@ -0,0 +1,98 @@ +package ch.fritteli.labyrinth.generator; + +import ch.fritteli.labyrinth.generator.model.Direction; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Position; +import ch.fritteli.labyrinth.generator.model.Tile; +import io.vavr.control.Option; +import java.util.Deque; +import java.util.LinkedList; +import java.util.Random; +import lombok.NonNull; + +public class Generator { + + @NonNull + private final Labyrinth labyrinth; + @NonNull + private final Random random; + @NonNull + private final Deque positions = new LinkedList<>(); + + public Generator(@NonNull final Labyrinth labyrinth) { + this.labyrinth = labyrinth; + this.random = new Random(labyrinth.getRandomSeed()); + } + + public void run() { + this.preDig(); + this.dig(); + this.postDig(); + } + + private void preDig() { + final Tile endTile = this.labyrinth.getEndTile(); + if (endTile.hasWallAt(Direction.BOTTOM)) { + endTile.enableDiggingToOrFrom(Direction.BOTTOM); + endTile.digFrom(Direction.BOTTOM); + } else if (endTile.hasWallAt(Direction.RIGHT)) { + endTile.enableDiggingToOrFrom(Direction.RIGHT); + endTile.digFrom(Direction.RIGHT); + } else if (endTile.hasWallAt(Direction.TOP)) { + endTile.enableDiggingToOrFrom(Direction.TOP); + endTile.digFrom(Direction.TOP); + } else if (endTile.hasWallAt(Direction.LEFT)) { + endTile.enableDiggingToOrFrom(Direction.LEFT); + endTile.digFrom(Direction.LEFT); + } + this.positions.push(this.labyrinth.getEnd()); + } + + private void dig() { + while (!this.positions.isEmpty()) { + final Position currentPosition = this.positions.peek(); + final Tile currentTile = this.labyrinth.getTileAt(currentPosition).get(); + final Option directionToDigTo = currentTile.getRandomAvailableDirection(this.random); + if (directionToDigTo.isDefined()) { + final Direction digTo = directionToDigTo.get(); + final Direction digFrom = digTo.invert(); + final Position neighborPosition = currentPosition.move(digTo); + final Tile neighborTile = this.labyrinth.getTileAt(neighborPosition).get(); + if (currentTile.digTo(digTo) && neighborTile.digFrom(digFrom)) { + // all ok! + this.positions.push(neighborPosition); + if (neighborPosition.equals(this.labyrinth.getStart())) { + this.markSolution(); + } + } else { + // Hm, didn't work. + currentTile.undigTo(digTo); + currentTile.preventDiggingToOrFrom(digTo); + } + } else { + this.positions.pop(); + } + } + } + + private void markSolution() { + this.positions.forEach(position -> this.labyrinth.getTileAt(position).get().setSolution()); + } + + private void postDig() { + final Tile startTile = this.labyrinth.getStartTile(); + if (startTile.hasWallAt(Direction.TOP)) { + startTile.enableDiggingToOrFrom(Direction.TOP); + startTile.digTo(Direction.TOP); + } else if (startTile.hasWallAt(Direction.LEFT)) { + startTile.enableDiggingToOrFrom(Direction.LEFT); + startTile.digTo(Direction.LEFT); + } else if (startTile.hasWallAt(Direction.BOTTOM)) { + startTile.enableDiggingToOrFrom(Direction.BOTTOM); + startTile.digTo(Direction.BOTTOM); + } else if (startTile.hasWallAt(Direction.RIGHT)) { + startTile.enableDiggingToOrFrom(Direction.RIGHT); + startTile.digTo(Direction.RIGHT); + } + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/Main.java b/src/main/java/ch/fritteli/labyrinth/generator/Main.java index 423db6a..79fc3fc 100644 --- a/src/main/java/ch/fritteli/labyrinth/generator/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/generator/Main.java @@ -22,6 +22,7 @@ public class Main { final int width = 20; final int height = 30; final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/); + new Generator(labyrinth).run(); final TextRenderer textRenderer = TextRenderer.newInstance(); final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); final JsonRenderer jsonRenderer = JsonRenderer.newInstance(); diff --git a/src/main/java/ch/fritteli/labyrinth/generator/model/Labyrinth.java b/src/main/java/ch/fritteli/labyrinth/generator/model/Labyrinth.java index 2ef7e3a..ddb93ca 100644 --- a/src/main/java/ch/fritteli/labyrinth/generator/model/Labyrinth.java +++ b/src/main/java/ch/fritteli/labyrinth/generator/model/Labyrinth.java @@ -6,13 +6,10 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; -import java.util.Deque; -import java.util.LinkedList; -import java.util.Random; - @EqualsAndHashCode @ToString public class Labyrinth { + private final Tile[][] field; @Getter private final int width; @@ -20,8 +17,6 @@ public class Labyrinth { private final int height; @Getter private final long randomSeed; - @EqualsAndHashCode.Exclude - private final Random random; @Getter private final Position start; @Getter @@ -31,35 +26,60 @@ public class Labyrinth { this(width, height, System.nanoTime()); } + public Labyrinth(final int width, final int height, @NonNull final Position start, @NonNull final Position end) { + this(width, height, System.nanoTime(), start, end); + } + public Labyrinth(final int width, final int height, final long randomSeed) { + this(width, height, randomSeed, new Position(0, 0), new Position(width - 1, height - 1)); + } + + public Labyrinth(final int width, final int height, final long randomSeed, @NonNull final Position start, @NonNull final Position end) { if (width <= 1 || height <= 1) { throw new IllegalArgumentException("width and height must be >1"); } + if (start.equals(end)) { + throw new IllegalArgumentException("'start' must not be equal to 'end'"); + } + if (start.getX() != 0 && start.getX() != width - 1 && start.getY() != 0 && start.getY() != height - 1) { + throw new IllegalArgumentException("'start' must be at the edge of the labyrinth"); + } + if (end.getX() != 0 && end.getX() != width - 1 && end.getY() != 0 && end.getY() != height - 1) { + throw new IllegalArgumentException("'start' must be at the edge of the labyrinth"); + } this.width = width; this.height = height; this.randomSeed = randomSeed; - this.random = new Random(randomSeed); this.field = new Tile[width][height]; - this.start = new Position(0, 0); - this.end = new Position(this.width - 1, this.height - 1); + this.start = start; + this.end = end; this.initField(); - this.generate(); } /** - * INTERNAL API. - * Exists only for deserialization. Not to be called from user code. + * INTERNAL API. Exists only for deserialization. Not to be called from user code. */ private Labyrinth(@NonNull final Tile[][] field, final int width, final int height, final long randomSeed) { this.field = field; this.width = width; this.height = height; this.randomSeed = randomSeed; - this.random = new Random(randomSeed); this.start = new Position(0, 0); this.end = new Position(this.width - 1, this.height - 1); } + /** + * INTERNAL API. Exists only for deserialization. Not to be called from user code. + */ + private Labyrinth(@NonNull final Tile[][] field, final int width, final int height, @NonNull final Position start, @NonNull final Position end, final long randomSeed) { + this.field = field; + this.width = width; + this.height = height; + this.randomSeed = randomSeed; + this.start = start; + this.end = end; + } + @NonNull public Option getTileAt(@NonNull final Position position) { return this.getTileAt(position.getX(), position.getY()); @@ -74,12 +94,12 @@ public class Labyrinth { } @NonNull - Tile getStartTile() { + public Tile getStartTile() { return this.getTileAt(this.start).get(); } @NonNull - Tile getEndTile() { + public Tile getEndTile() { return this.getTileAt(this.end).get(); } @@ -108,62 +128,4 @@ public class Labyrinth { tile.preventDiggingToOrFrom(Direction.BOTTOM); } } - - private void generate() { - new Generator().run(); - } - - private class Generator { - private final Deque positions = new LinkedList<>(); - - void run() { - this.preDig(); - this.dig(); - this.postDig(); - } - - private void preDig() { - final Tile endTile = Labyrinth.this.getEndTile(); - endTile.enableDiggingToOrFrom(Direction.BOTTOM); - endTile.digFrom(Direction.BOTTOM); - this.positions.push(Labyrinth.this.end); - } - - private void dig() { - while (!this.positions.isEmpty()) { - final Position currentPosition = this.positions.peek(); - final Tile currentTile = Labyrinth.this.getTileAt(currentPosition).get(); - final Option directionToDigTo = currentTile.getRandomAvailableDirection(Labyrinth.this.random); - if (directionToDigTo.isDefined()) { - final Direction digTo = directionToDigTo.get(); - final Direction digFrom = digTo.invert(); - final Position neighborPosition = currentPosition.move(digTo); - final Tile neighborTile = Labyrinth.this.getTileAt(neighborPosition).get(); - if (currentTile.digTo(digTo) && neighborTile.digFrom(digFrom)) { - // all ok! - this.positions.push(neighborPosition); - if (neighborPosition.equals(Labyrinth.this.start)) { - this.markSolution(); - } - } else { - // Hm, didn't work. - currentTile.undigTo(digTo); - currentTile.preventDiggingToOrFrom(digTo); - } - } else { - this.positions.pop(); - } - } - } - - private void markSolution() { - this.positions.forEach(position -> Labyrinth.this.getTileAt(position).get().setSolution()); - } - - private void postDig() { - final Tile startTile = Labyrinth.this.getStartTile(); - startTile.enableDiggingToOrFrom(Direction.TOP); - startTile.digTo(Direction.TOP); - } - } } diff --git a/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java b/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java index c0cb8cc..355342e 100644 --- a/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java +++ b/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java @@ -1,6 +1,7 @@ package ch.fritteli.labyrinth.generator.renderer.json; import ch.fritteli.labyrinth.generator.json.JsonCell; +import ch.fritteli.labyrinth.generator.json.JsonCoordinates; import ch.fritteli.labyrinth.generator.json.JsonLabyrinth; import ch.fritteli.labyrinth.generator.model.Direction; import ch.fritteli.labyrinth.generator.model.Labyrinth; @@ -43,6 +44,14 @@ class Generator { rows.add(row); } result.setGrid(rows); + final JsonCoordinates start = new JsonCoordinates(); + start.setX(this.labyrinth.getStart().getX()); + start.setY(this.labyrinth.getStart().getY()); + result.setStart(start); + final JsonCoordinates end = new JsonCoordinates(); + end.setX(this.labyrinth.getEnd().getX()); + end.setY(this.labyrinth.getEnd().getY()); + result.setEnd(end); return result; } } diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthInputStream.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthInputStream.java new file mode 100644 index 0000000..e2c1f10 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthInputStream.java @@ -0,0 +1,42 @@ +package ch.fritteli.labyrinth.generator.serialization; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import java.io.ByteArrayInputStream; +import lombok.NonNull; + +public abstract class AbstractLabyrinthInputStream extends ByteArrayInputStream { + + public AbstractLabyrinthInputStream(@NonNull final byte[] buf) { + super(buf); + } + + public abstract void checkHeader(); + + @NonNull + public abstract Labyrinth readLabyrinthData(); + + 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/AbstractLabyrinthOutputStream.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthOutputStream.java new file mode 100644 index 0000000..72e7a5a --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/AbstractLabyrinthOutputStream.java @@ -0,0 +1,28 @@ +package ch.fritteli.labyrinth.generator.serialization; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import java.io.ByteArrayOutputStream; +import lombok.NonNull; + +public abstract class AbstractLabyrinthOutputStream extends ByteArrayOutputStream { + + public abstract void writeHeader(); + + public abstract void writeLabyrinthData(@NonNull final Labyrinth labyrinth); + + 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/LabyrinthInputStream.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthInputStream.java deleted file mode 100644 index cf619c8..0000000 --- a/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthInputStream.java +++ /dev/null @@ -1,74 +0,0 @@ -package ch.fritteli.labyrinth.generator.serialization; - -import ch.fritteli.labyrinth.generator.model.Labyrinth; -import ch.fritteli.labyrinth.generator.model.Tile; -import lombok.NonNull; - -import java.io.ByteArrayInputStream; - -public class LabyrinthInputStream extends ByteArrayInputStream { - public LabyrinthInputStream(@NonNull final byte[] buf) { - super(buf); - } - - public long readLong() { - long result = 0; - result |= ((long) this.readInt()) << 32; - result |= 0xffffffffL & this.readInt(); - return result; - } - - 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 void checkHeader() { - final byte magic1 = this.readByte(); - if (magic1 != SerializerDeserializer.MAGIC_BYTE_1) { - throw new IllegalArgumentException("Invalid labyrinth data."); - } - final byte magic2 = this.readByte(); - if (magic2 != SerializerDeserializer.MAGIC_BYTE_2) { - throw new IllegalArgumentException("Invalid labyrinth data."); - } - final int version = this.readByte(); - if (version != SerializerDeserializer.VERSION_BYTE) { - throw new IllegalArgumentException("Unknown Labyrinth data version: " + version); - } - } - - 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; - } - - @NonNull - public Labyrinth readLabyrinthData() { - final long randomSeed = this.readLong(); - final int width = this.readInt(); - final int height = this.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 = this.readByte(); - tiles[x][y] = SerializerDeserializer.getTileForBitmask(bitmask); - } - } - - return SerializerDeserializer.createLabyrinth(tiles, width, height, randomSeed); - } -} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java deleted file mode 100644 index 363a52b..0000000 --- a/src/main/java/ch/fritteli/labyrinth/generator/serialization/LabyrinthOutputStream.java +++ /dev/null @@ -1,50 +0,0 @@ -package ch.fritteli.labyrinth.generator.serialization; - -import ch.fritteli.labyrinth.generator.model.Labyrinth; -import ch.fritteli.labyrinth.generator.model.Tile; -import lombok.NonNull; - -import java.io.ByteArrayOutputStream; - - -public class LabyrinthOutputStream extends ByteArrayOutputStream { - public void writeHeader() { - this.writeByte(SerializerDeserializer.MAGIC_BYTE_1); - this.writeByte(SerializerDeserializer.MAGIC_BYTE_2); - this.writeByte(SerializerDeserializer.VERSION_BYTE); - } - - public void writeByte(final byte value) { - this.write(value); - } - - public void writeLabyrinthData(@NonNull final Labyrinth labyrinth) { - final long randomSeed = labyrinth.getRandomSeed(); - final int width = labyrinth.getWidth(); - final int height = labyrinth.getHeight(); - this.writeLong(randomSeed); - this.writeInt(width); - this.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 = SerializerDeserializer.getBitmaskForTile(tile); - this.writeByte(bitmask); - } - } - } - - public void writeLong(final long value) { - this.writeInt((int) (value >> 32)); - this.writeInt((int) value); - } - - public void writeInt(final int value) { - this.write(value >> 24); - this.write(value >> 16); - this.write(value >> 8); - this.write(value); - } -} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthInputStreamV1.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthInputStreamV1.java new file mode 100644 index 0000000..d481907 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthInputStreamV1.java @@ -0,0 +1,51 @@ +package ch.fritteli.labyrinth.generator.serialization.v1; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Tile; +import ch.fritteli.labyrinth.generator.serialization.AbstractLabyrinthInputStream; +import lombok.NonNull; + +public class LabyrinthInputStreamV1 extends AbstractLabyrinthInputStream { + + public LabyrinthInputStreamV1(@NonNull final byte[] buf) { + super(buf); + } + + @Override + public void checkHeader() { + final byte magic1 = this.readByte(); + if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) { + throw new IllegalArgumentException("Invalid labyrinth data."); + } + final byte magic2 = this.readByte(); + if (magic2 != SerializerDeserializerV1.MAGIC_BYTE_2) { + throw new IllegalArgumentException("Invalid labyrinth data."); + } + final int version = this.readByte(); + if (version != SerializerDeserializerV1.VERSION_BYTE) { + throw new IllegalArgumentException("Unknown Labyrinth data version: " + version); + } + } + + @NonNull + @Override + public Labyrinth readLabyrinthData() { + final long randomSeed = this.readLong(); + final int width = this.readInt(); + final int height = this.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 = this.readByte(); + tiles[x][y] = SerializerDeserializerV1.getTileForBitmask(bitmask); + } + } + + return SerializerDeserializerV1.createLabyrinth(tiles, width, height, randomSeed); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthOutputStreamV1.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthOutputStreamV1.java new file mode 100644 index 0000000..20a9af4 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/LabyrinthOutputStreamV1.java @@ -0,0 +1,35 @@ +package ch.fritteli.labyrinth.generator.serialization.v1; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Tile; +import ch.fritteli.labyrinth.generator.serialization.AbstractLabyrinthOutputStream; +import lombok.NonNull; + +public class LabyrinthOutputStreamV1 extends AbstractLabyrinthOutputStream { + + @Override + public void writeHeader() { + this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1); + this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2); + this.writeByte(SerializerDeserializerV1.VERSION_BYTE); + } + + @Override + public void writeLabyrinthData(@NonNull final Labyrinth labyrinth) { + final long randomSeed = labyrinth.getRandomSeed(); + final int width = labyrinth.getWidth(); + final int height = labyrinth.getHeight(); + this.writeLong(randomSeed); + this.writeInt(width); + this.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 = SerializerDeserializerV1.getBitmaskForTile(tile); + this.writeByte(bitmask); + } + } + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializer.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1.java similarity index 95% rename from src/main/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializer.java rename to src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1.java index 244c3f6..44ce892 100644 --- a/src/main/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializer.java +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1.java @@ -1,4 +1,4 @@ -package ch.fritteli.labyrinth.generator.serialization; +package ch.fritteli.labyrinth.generator.serialization.v1; import ch.fritteli.labyrinth.generator.model.Direction; import ch.fritteli.labyrinth.generator.model.Labyrinth; @@ -45,7 +45,7 @@ import java.util.EnumSet; * Extraneous space (poss. last nibble) is ignored. */ @UtilityClass -public class SerializerDeserializer { +public class SerializerDeserializerV1 { final byte MAGIC_BYTE_1 = 0x1a; final byte MAGIC_BYTE_2 = (byte) 0xb1; final byte VERSION_BYTE = 0x01; @@ -64,7 +64,7 @@ public class SerializerDeserializer { */ @NonNull public byte[] serialize(@NonNull final Labyrinth labyrinth) { - final LabyrinthOutputStream stream = new LabyrinthOutputStream(); + final LabyrinthOutputStreamV1 stream = new LabyrinthOutputStreamV1(); stream.writeHeader(); stream.writeLabyrinthData(labyrinth); return stream.toByteArray(); @@ -78,7 +78,7 @@ public class SerializerDeserializer { */ @NonNull public Labyrinth deserialize(@NonNull final byte[] bytes) { - final LabyrinthInputStream stream = new LabyrinthInputStream(bytes); + final LabyrinthInputStreamV1 stream = new LabyrinthInputStreamV1(bytes); stream.checkHeader(); return stream.readLabyrinthData(); } diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthInputStreamV2.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthInputStreamV2.java new file mode 100644 index 0000000..6693f87 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthInputStreamV2.java @@ -0,0 +1,69 @@ +package ch.fritteli.labyrinth.generator.serialization.v2; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Position; +import ch.fritteli.labyrinth.generator.model.Tile; +import ch.fritteli.labyrinth.generator.serialization.AbstractLabyrinthInputStream; +import lombok.NonNull; + +public class LabyrinthInputStreamV2 extends AbstractLabyrinthInputStream { + + public LabyrinthInputStreamV2(@NonNull final byte[] buf) { + super(buf); + } + + @Override + public void checkHeader() { + // 00 0x1a magic + // 01 0xb1 magic + // 02 0x02 version + final byte magic1 = this.readByte(); + if (magic1 != SerializerDeserializerV2.MAGIC_BYTE_1) { + throw new IllegalArgumentException("Invalid labyrinth data."); + } + final byte magic2 = this.readByte(); + if (magic2 != SerializerDeserializerV2.MAGIC_BYTE_2) { + throw new IllegalArgumentException("Invalid labyrinth data."); + } + final int version = this.readByte(); + if (version != SerializerDeserializerV2.VERSION_BYTE) { + throw new IllegalArgumentException("Unknown Labyrinth data version: " + version); + } + } + + @NonNull + @Override + public Labyrinth readLabyrinthData() { + // 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.. 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 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] = SerializerDeserializerV2.getTileForBitmask(bitmask); + } + } + + final Position start = new Position(startX, startY); + final Position end = new Position(endX, endY); + return SerializerDeserializerV2.createLabyrinth(tiles, width, height, start, end, randomSeed); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthOutputStreamV2.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthOutputStreamV2.java new file mode 100644 index 0000000..76cb539 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/LabyrinthOutputStreamV2.java @@ -0,0 +1,53 @@ +package ch.fritteli.labyrinth.generator.serialization.v2; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Position; +import ch.fritteli.labyrinth.generator.model.Tile; +import ch.fritteli.labyrinth.generator.serialization.AbstractLabyrinthOutputStream; +import lombok.NonNull; + +public class LabyrinthOutputStreamV2 extends AbstractLabyrinthOutputStream { + + @Override + public void writeHeader() { + // 00 0x1a magic + // 01 0xb1 magic + // 02 0x02 version + this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_1); + this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_2); + this.writeByte(SerializerDeserializerV2.VERSION_BYTE); + } + + @Override + public void writeLabyrinthData(@NonNull final Labyrinth labyrinth) { + // 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.. tiles + final long randomSeed = labyrinth.getRandomSeed(); + final int width = labyrinth.getWidth(); + final int height = labyrinth.getHeight(); + final Position start = labyrinth.getStart(); + final Position end = labyrinth.getEnd(); + this.writeInt(width); + this.writeInt(height); + this.writeInt(start.getX()); + this.writeInt(start.getY()); + this.writeInt(end.getX()); + this.writeInt(end.getY()); + this.writeLong(randomSeed); + + 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 = SerializerDeserializerV2.getBitmaskForTile(tile); + this.writeByte(bitmask); + } + } + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/SerializerDeserializerV2.java b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/SerializerDeserializerV2.java new file mode 100644 index 0000000..efd1a99 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/serialization/v2/SerializerDeserializerV2.java @@ -0,0 +1,150 @@ +package ch.fritteli.labyrinth.generator.serialization.v2; + +import ch.fritteli.labyrinth.generator.model.Direction; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Position; +import ch.fritteli.labyrinth.generator.model.Tile; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.EnumSet; +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +/** + *
+ * 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 0x02 version (0x00 -> dev, 0x01 -> deprecated, 0x02 -> 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..    tiles
+ * 
+ * Extraneous space (poss. last nibble) is ignored. + */ +@UtilityClass +public class SerializerDeserializerV2 { + + final byte MAGIC_BYTE_1 = 0x1a; + final byte MAGIC_BYTE_2 = (byte) 0xb1; + final byte VERSION_BYTE = 0x02; + + private final byte TOP_BIT = 0b0000_0001; + private final byte RIGHT_BIT = 0b0000_0010; + private final byte BOTTOM_BIT = 0b0000_0100; + private final byte LEFT_BIT = 0b0000_1000; + private final byte SOLUTION_BIT = 0b0001_0000; + + /** + * Serializes the {@code 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 LabyrinthOutputStreamV2 stream = new LabyrinthOutputStreamV2(); + stream.writeHeader(); + stream.writeLabyrinthData(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 LabyrinthInputStreamV2 stream = new LabyrinthInputStreamV2(bytes); + stream.checkHeader(); + return stream.readLabyrinthData(); + } + + @NonNull + Labyrinth createLabyrinth(@NonNull final Tile[][] field, final int width, final int height, @NonNull final Position start, @NonNull final Position end, final long randomSeed) { + try { + final Constructor constructor = Labyrinth.class.getDeclaredConstructor(Tile[][].class, Integer.TYPE, Integer.TYPE, Position.class, Position.class, Long.TYPE); + constructor.setAccessible(true); + return constructor.newInstance(field, width, height, start, end, randomSeed); + } catch (@NonNull 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 { + final Constructor constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE); + constructor.setAccessible(true); + return constructor.newInstance(walls, solution); + } catch (@NonNull final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Can not deserialize Tile from labyrinth data.", e); + } + } + + 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 + 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/resources/labyrinth.schema.json b/src/main/resources/labyrinth.schema.json index e81ce01..ffcecab 100644 --- a/src/main/resources/labyrinth.schema.json +++ b/src/main/resources/labyrinth.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://manuel.friedli.info/labyrinth-1/labyrinth.schema.json", + "$id": "https://manuel.friedli.info/labyrinth-2/labyrinth.schema.json", "javaType": "ch.fritteli.labyrinth.generator.json.JsonLabyrinth", "type": "object", "additionalProperties": false, @@ -8,6 +8,8 @@ "id", "width", "height", + "start", + "end", "grid" ], "properties": { @@ -23,6 +25,12 @@ "type": "integer", "minimum": 1 }, + "start": { + "$ref": "#/$defs/coordinates" + }, + "end": { + "$ref": "#/$defs/coordinates" + }, "grid": { "$ref": "#/$defs/grid" } @@ -64,6 +72,25 @@ "type": "boolean" } } + }, + "coordinates": { + "type": "object", + "javaType": "ch.fritteli.labyrinth.generator.json.JsonCoordinates", + "additionalProperties": false, + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "integer", + "minimum": 0 + }, + "y": { + "type": "integer", + "minimum": 0 + } + } } } } diff --git a/src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java b/src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java deleted file mode 100644 index ed7c85a..0000000 --- a/src/test/java/ch/fritteli/labyrinth/generator/serialization/SerializerDeserializerTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.fritteli.labyrinth.generator.serialization; - -import ch.fritteli.labyrinth.generator.model.Labyrinth; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class SerializerDeserializerTest { - @Test - void testSerializeDeserializeTiny() { - final Labyrinth expected = new Labyrinth(2, 2, 255); - final byte[] bytes = SerializerDeserializer.serialize(expected); - final Labyrinth result = SerializerDeserializer.deserialize(bytes); - assertThat(result).isEqualTo(expected); - } - - @Test - void testSerializeDeserializeMedium() { - final Labyrinth expected = new Labyrinth(20, 20, -271828182846L); - final byte[] bytes = SerializerDeserializer.serialize(expected); - final Labyrinth result = SerializerDeserializer.deserialize(bytes); - assertThat(result).isEqualTo(expected); - } - - @Test - void testSerializeDeserializeLarge() { - final Labyrinth expected = new Labyrinth(200, 320, 3141592653589793238L); - final byte[] bytes = SerializerDeserializer.serialize(expected); - final Labyrinth result = SerializerDeserializer.deserialize(bytes); - assertThat(result).isEqualTo(expected); - } -} diff --git a/src/test/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1Test.java b/src/test/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1Test.java new file mode 100644 index 0000000..865a201 --- /dev/null +++ b/src/test/java/ch/fritteli/labyrinth/generator/serialization/v1/SerializerDeserializerV1Test.java @@ -0,0 +1,36 @@ +package ch.fritteli.labyrinth.generator.serialization.v1; + +import ch.fritteli.labyrinth.generator.Generator; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SerializerDeserializerV1Test { + @Test + void testSerializeDeserializeTiny() { + final Labyrinth expected = new Labyrinth(2, 2, 255); + new Generator(expected).run(); + final byte[] bytes = SerializerDeserializerV1.serialize(expected); + final Labyrinth result = SerializerDeserializerV1.deserialize(bytes); + assertThat(result).isEqualTo(expected); + } + + @Test + void testSerializeDeserializeMedium() { + final Labyrinth expected = new Labyrinth(20, 20, -271828182846L); + new Generator(expected).run(); + final byte[] bytes = SerializerDeserializerV1.serialize(expected); + final Labyrinth result = SerializerDeserializerV1.deserialize(bytes); + assertThat(result).isEqualTo(expected); + } + + @Test + void testSerializeDeserializeLarge() { + final Labyrinth expected = new Labyrinth(200, 320, 3141592653589793238L); + new Generator(expected).run(); + final byte[] bytes = SerializerDeserializerV1.serialize(expected); + final Labyrinth result = SerializerDeserializerV1.deserialize(bytes); + assertThat(result).isEqualTo(expected); + } +}