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; | package ch.fritteli.labyrinth.generator.model; | ||||||
| 
 | 
 | ||||||
| import io.vavr.control.Option; | import io.vavr.control.Option; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.Getter; | import lombok.Getter; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.ToString; | ||||||
| 
 | 
 | ||||||
| import java.util.Deque; | import java.util.Deque; | ||||||
| import java.util.LinkedList; | import java.util.LinkedList; | ||||||
| import java.util.Random; | import java.util.Random; | ||||||
| 
 | 
 | ||||||
|  | @EqualsAndHashCode | ||||||
|  | @ToString | ||||||
| public class Labyrinth { | public class Labyrinth { | ||||||
|     private final Tile[][] field; |     private final Tile[][] field; | ||||||
|     @Getter |     @Getter | ||||||
|  | @ -16,6 +20,7 @@ public class Labyrinth { | ||||||
|     private final int height; |     private final int height; | ||||||
|     @Getter |     @Getter | ||||||
|     private final long randomSeed; |     private final long randomSeed; | ||||||
|  |     @EqualsAndHashCode.Exclude | ||||||
|     private final Random random; |     private final Random random; | ||||||
|     @Getter |     @Getter | ||||||
|     private final Position start; |     private final Position start; | ||||||
|  | @ -41,6 +46,25 @@ public class Labyrinth { | ||||||
|         this.generate(); |         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 |     @NonNull | ||||||
|     public Option<Tile> getTileAt(@NonNull final Position position) { |     public Option<Tile> getTileAt(@NonNull final Position position) { | ||||||
|         return this.getTileAt(position.getX(), position.getY()); |         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.collection.Stream; | ||||||
| import io.vavr.control.Option; | import io.vavr.control.Option; | ||||||
| import lombok.AccessLevel; | import lombok.AccessLevel; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.Getter; | import lombok.Getter; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.ToString; | ||||||
| import lombok.experimental.FieldDefaults; | import lombok.experimental.FieldDefaults; | ||||||
| 
 | 
 | ||||||
|  | import java.util.EnumSet; | ||||||
| import java.util.Random; | import java.util.Random; | ||||||
| 
 | 
 | ||||||
| @FieldDefaults(level = AccessLevel.PRIVATE) | @FieldDefaults(level = AccessLevel.PRIVATE) | ||||||
|  | @EqualsAndHashCode | ||||||
|  | @ToString | ||||||
| public class Tile { | public class Tile { | ||||||
|     final Walls walls = new Walls(); |     final Walls walls = new Walls(); | ||||||
|     boolean visited = false; |     boolean visited = false; | ||||||
|  | @ -20,6 +25,22 @@ public class Tile { | ||||||
|         this.walls.setAll(); |         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) { |     public void preventDiggingToOrFrom(@NonNull final Direction direction) { | ||||||
|         this.walls.harden(direction); |         this.walls.harden(direction); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| package ch.fritteli.labyrinth.generator.model; | package ch.fritteli.labyrinth.generator.model; | ||||||
| 
 | 
 | ||||||
| import io.vavr.collection.Stream; | import io.vavr.collection.Stream; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.ToString; | ||||||
| 
 | 
 | ||||||
| import java.util.EnumSet; | import java.util.EnumSet; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
|  | @ -9,6 +11,8 @@ import java.util.Set; | ||||||
| import java.util.SortedSet; | import java.util.SortedSet; | ||||||
| import java.util.TreeSet; | import java.util.TreeSet; | ||||||
| 
 | 
 | ||||||
|  | @EqualsAndHashCode | ||||||
|  | @ToString | ||||||
| public class Walls { | public class Walls { | ||||||
|     private final SortedSet<Direction> directions = new TreeSet<>(); |     private final SortedSet<Direction> directions = new TreeSet<>(); | ||||||
|     private final Set<Direction> hardened = new HashSet<>(); |     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