feature/serialize #1
					 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue