feature/make-start-and-end-configurable #7
17 changed files with 644 additions and 234 deletions
103
src/main/java/ch/fritteli/labyrinth/generator/Generator.java
Normal file
103
src/main/java/ch/fritteli/labyrinth/generator/Generator.java
Normal file
|
@ -0,0 +1,103 @@
|
|||
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 Position end = this.labyrinth.getEnd();
|
||||
final Tile endTile = this.labyrinth.getEndTile();
|
||||
|
||||
if (end.getY() == 0) {
|
||||
endTile.enableDiggingToOrFrom(Direction.TOP);
|
||||
endTile.digFrom(Direction.TOP);
|
||||
} else if (end.getX() == 0) {
|
||||
endTile.enableDiggingToOrFrom(Direction.LEFT);
|
||||
endTile.digFrom(Direction.LEFT);
|
||||
} else if (end.getY() == this.labyrinth.getHeight() - 1) {
|
||||
endTile.enableDiggingToOrFrom(Direction.BOTTOM);
|
||||
endTile.digFrom(Direction.BOTTOM);
|
||||
} else if (end.getX() == this.labyrinth.getWidth() - 1) {
|
||||
endTile.enableDiggingToOrFrom(Direction.RIGHT);
|
||||
endTile.digFrom(Direction.RIGHT);
|
||||
}
|
||||
|
||||
this.positions.push(end);
|
||||
}
|
||||
|
||||
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 Position start = this.labyrinth.getStart();
|
||||
final Tile startTile = this.labyrinth.getStartTile();
|
||||
|
||||
if (start.getY() == 0) {
|
||||
startTile.enableDiggingToOrFrom(Direction.TOP);
|
||||
startTile.digTo(Direction.TOP);
|
||||
} else if (start.getX() == 0) {
|
||||
startTile.enableDiggingToOrFrom(Direction.LEFT);
|
||||
startTile.digTo(Direction.LEFT);
|
||||
} else if (start.getY() == this.labyrinth.getHeight() - 1) {
|
||||
startTile.enableDiggingToOrFrom(Direction.BOTTOM);
|
||||
startTile.digTo(Direction.BOTTOM);
|
||||
} else if (start.getX() == this.labyrinth.getWidth() - 1) {
|
||||
startTile.enableDiggingToOrFrom(Direction.RIGHT);
|
||||
startTile.digTo(Direction.RIGHT);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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("'end' 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
* ==> 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 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue