Feat: Make start and end of a labyrinth configurable.
All checks were successful
continuous-integration/drone/push Build is passing

Also, small refactoring.
This commit is contained in:
Manuel Friedli 2023-04-16 23:38:01 +02:00
parent 14e4e497ac
commit 6302aaa5e8
17 changed files with 639 additions and 234 deletions

View file

@ -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<Position> 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<Direction> 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);
}
}
}

View file

@ -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();

View file

@ -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<Tile> 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<Position> 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<Direction> 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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}
}

View file

@ -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;
/**
* <pre>
* 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
* </pre>
* ==&gt; bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==&gt; first bytes are:
* <pre>
* 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
* </pre>
* 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<Labyrinth> 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<Direction> walls, boolean solution) {
try {
final Constructor<Tile> 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<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);
}
}

View file

@ -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
}
}
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}