Add serialization and deserialization to a compact byte-based format.
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
41330de4f4
commit
185114bcf4
7 changed files with 336 additions and 0 deletions
|
@ -1,13 +1,17 @@
|
|||
package ch.fritteli.labyrinth.generator.model;
|
||||
|
||||
import io.vavr.control.Option;
|
||||
import lombok.EqualsAndHashCode;
|
||||
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
|
||||
|
@ -16,6 +20,7 @@ public class Labyrinth {
|
|||
private final int height;
|
||||
@Getter
|
||||
private final long randomSeed;
|
||||
@EqualsAndHashCode.Exclude
|
||||
private final Random random;
|
||||
@Getter
|
||||
private final Position start;
|
||||
|
@ -41,6 +46,25 @@ public class Labyrinth {
|
|||
this.generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API.
|
||||
* Exists only for deserialization. Not to be called from user code.
|
||||
*
|
||||
* @param field
|
||||
* @param width
|
||||
* @param height
|
||||
* @param randomSeed
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Option<Tile> getTileAt(@NonNull final Position position) {
|
||||
return this.getTileAt(position.getX(), position.getY());
|
||||
|
|
|
@ -3,13 +3,18 @@ package ch.fritteli.labyrinth.generator.model;
|
|||
import io.vavr.collection.Stream;
|
||||
import io.vavr.control.Option;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Random;
|
||||
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public class Tile {
|
||||
final Walls walls = new Walls();
|
||||
boolean visited = false;
|
||||
|
@ -20,6 +25,22 @@ public class Tile {
|
|||
this.walls.setAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API.
|
||||
* Exists only for deserialization. Not to be called from user code.
|
||||
*
|
||||
* @param walls
|
||||
* @param solution
|
||||
*/
|
||||
private Tile(@NonNull final EnumSet<Direction> walls, final boolean solution) {
|
||||
for (final Direction direction : walls) {
|
||||
this.walls.set(direction);
|
||||
this.walls.harden(direction);
|
||||
}
|
||||
this.visited = true;
|
||||
this.solution = solution;
|
||||
}
|
||||
|
||||
public void preventDiggingToOrFrom(@NonNull final Direction direction) {
|
||||
this.walls.harden(direction);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package ch.fritteli.labyrinth.generator.model;
|
||||
|
||||
import io.vavr.collection.Stream;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
|
@ -9,6 +11,8 @@ import java.util.Set;
|
|||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public class Walls {
|
||||
private final SortedSet<Direction> directions = new TreeSet<>();
|
||||
private final Set<Direction> hardened = new HashSet<>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
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;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* decimal hex border
|
||||
* 0 0 no border
|
||||
* 1 1 top
|
||||
* 2 2 right
|
||||
* 3 3 top+right
|
||||
* 4 4 bottom
|
||||
* 5 5 top+bottom
|
||||
* 6 6 right+bottom
|
||||
* 7 7 top+right+bottom
|
||||
* 8 8 left
|
||||
* 9 9 top+left
|
||||
* 10 a right+left
|
||||
* 11 b top+right+left
|
||||
* 12 c bottom+left
|
||||
* 13 d top+bottom+left
|
||||
* 14 e right+bottom+left
|
||||
* 15 f 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 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
|
||||
* </pre>
|
||||
* 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) {
|
||||
@NonNull final LabyrinthOutputStream stream = new LabyrinthOutputStream();
|
||||
final int width = labyrinth.getWidth();
|
||||
final int height = labyrinth.getHeight();
|
||||
final long randomSeed = labyrinth.getRandomSeed();
|
||||
stream.writeByte(MAGIC_BYTE_1);
|
||||
stream.writeByte(MAGIC_BYTE_2);
|
||||
stream.writeByte(VERSION_BYTE);
|
||||
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.
|
||||
@NonNull final Tile tile = labyrinth.getTileAt(x, y).get();
|
||||
final byte bitmask = getBitmaskForTile(tile);
|
||||
stream.writeByte(bitmask);
|
||||
}
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Labyrinth readLabyrinthData(@NonNull final LabyrinthInputStream stream) {
|
||||
final long randomSeed = stream.readLong();
|
||||
final int width = stream.readInt();
|
||||
final int height = stream.readInt();
|
||||
|
||||
@NonNull 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<Labyrinth> 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<Direction> walls, boolean solution) {
|
||||
try {
|
||||
@NonNull final Constructor<Tile> 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<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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue