Merge pull request 'feature/serialize' (#1) from feature/serialize into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: java/labyrinth-generator#1
This commit is contained in:
commit
d320771614
24 changed files with 544 additions and 112 deletions
|
@ -20,7 +20,9 @@ steps:
|
||||||
from_secret: repo-token
|
from_secret: repo-token
|
||||||
commands:
|
commands:
|
||||||
- mvn -s maven-settings.xml deploy -DskipTests=true
|
- mvn -s maven-settings.xml deploy -DskipTests=true
|
||||||
when:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
include:
|
- master
|
||||||
- master
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
<orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" />
|
<orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" />
|
||||||
<orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" />
|
<orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" />
|
||||||
<orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" />
|
<orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" />
|
||||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" />
|
<orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.25" level="project" />
|
||||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" />
|
<orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.25" level="project" />
|
||||||
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
|
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
|
||||||
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" />
|
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" />
|
||||||
<orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
|
<orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
|
||||||
|
|
37
pom.xml
37
pom.xml
|
@ -8,9 +8,17 @@
|
||||||
<artifactId>fritteli-build-parent</artifactId>
|
<artifactId>fritteli-build-parent</artifactId>
|
||||||
<version>2.0.4</version>
|
<version>2.0.4</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>ch.fritteli.labyrinth</groupId>
|
<groupId>ch.fritteli.labyrinth</groupId>
|
||||||
<artifactId>labyrinth-generator</artifactId>
|
<artifactId>labyrinth-generator</artifactId>
|
||||||
<version>0.0.2-SNAPSHOT</version>
|
<version>0.0.2-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<logback.version>1.2.10</logback.version>
|
||||||
|
<pdfbox.version>2.0.25</pdfbox.version>
|
||||||
|
<slf4j.version>1.7.35</slf4j.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
|
@ -27,12 +35,21 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.pdfbox</groupId>
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
<artifactId>pdfbox</artifactId>
|
<artifactId>pdfbox</artifactId>
|
||||||
<version>2.0.20</version>
|
<version>${pdfbox.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<version>${logback.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
|
@ -81,12 +98,24 @@
|
||||||
</distributionManagement>
|
</distributionManagement>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>repo.gittr.ch</id>
|
<id>repo.gittr.ch.releases</id>
|
||||||
<url>https://repo.gittr.ch/</url>
|
<url>https://repo.gittr.ch/releases/</url>
|
||||||
<releases>
|
<releases>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
<updatePolicy>never</updatePolicy>
|
<updatePolicy>never</updatePolicy>
|
||||||
</releases>
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
<updatePolicy>never</updatePolicy>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>repo.gittr.ch.snapshots</id>
|
||||||
|
<url>https://repo.gittr.ch/snapshots/</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
<updatePolicy>never</updatePolicy>
|
||||||
|
</releases>
|
||||||
<snapshots>
|
<snapshots>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
<updatePolicy>always</updatePolicy>
|
<updatePolicy>always</updatePolicy>
|
||||||
|
|
|
@ -7,10 +7,12 @@ import ch.fritteli.labyrinth.generator.renderer.pdffile.PDFFileRenderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
|
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.textfile.TextFileRenderer;
|
import ch.fritteli.labyrinth.generator.renderer.textfile.TextFileRenderer;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(@NonNull final String[] args) {
|
public static void main(@NonNull final String[] args) {
|
||||||
int width = 100;
|
int width = 100;
|
||||||
|
@ -28,19 +30,19 @@ public class Main {
|
||||||
final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance()
|
final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance()
|
||||||
.setTargetFile(userHome.resolve(baseFilename + ".pdf"));
|
.setTargetFile(userHome.resolve(baseFilename + ".pdf"));
|
||||||
|
|
||||||
System.out.println("Labyrinth-ID: " + labyrinth.getRandomSeed());
|
log.info("Labyrinth-ID: {}", labyrinth.getRandomSeed());
|
||||||
// Render Labyrinth to stdout
|
// Render Labyrinth to stdout
|
||||||
System.out.println(textRenderer.render(labyrinth));
|
log.info("Text rendering:\n{}", textRenderer.render(labyrinth));
|
||||||
// Render Labyrinth solution to stdout
|
// Render Labyrinth solution to stdout
|
||||||
System.out.println(textRenderer.setRenderSolution(true).render(labyrinth));
|
log.info("Text rendering with solution:\n{}", textRenderer.setRenderSolution(true).render(labyrinth));
|
||||||
// Render HTML to stdout
|
// Render HTML to stdout
|
||||||
System.out.println(htmlRenderer.render(labyrinth));
|
log.info("HTML rendering:\n{}", htmlRenderer.render(labyrinth));
|
||||||
// Render Labyrinth and solution to (separate) files
|
// Render Labyrinth and solution to (separate) files
|
||||||
System.out.println(textFileRenderer.render(labyrinth));
|
log.info("Text rendering to file:\n{}", textFileRenderer.render(labyrinth));
|
||||||
// Render HTML to file
|
// Render HTML to file
|
||||||
System.out.println(htmlFileRenderer.render(labyrinth));
|
log.info("HTML rendering to file:\n{}", htmlFileRenderer.render(labyrinth));
|
||||||
// Render PDF to file
|
// Render PDF to file
|
||||||
System.out.println(pdfFileRenderer.render(labyrinth));
|
log.info("PDF rendering to file:\n{}", pdfFileRenderer.render(labyrinth));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getBaseFilename(@NonNull final Labyrinth labyrinth) {
|
private static String getBaseFilename(@NonNull final Labyrinth labyrinth) {
|
||||||
|
|
|
@ -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,20 @@ public class Labyrinth {
|
||||||
this.generate();
|
this.generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
@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<>();
|
||||||
|
|
|
@ -16,6 +16,7 @@ public class HTMLRenderer implements Renderer<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@Override
|
||||||
public String render(@NonNull final Labyrinth labyrinth) {
|
public String render(@NonNull final Labyrinth labyrinth) {
|
||||||
if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) {
|
if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) {
|
||||||
return this.getPreamble(labyrinth) + POSTAMBLE;
|
return this.getPreamble(labyrinth) + POSTAMBLE;
|
||||||
|
|
|
@ -3,25 +3,30 @@ package ch.fritteli.labyrinth.generator.renderer.htmlfile;
|
||||||
import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.Renderer;
|
import ch.fritteli.labyrinth.generator.renderer.Renderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
|
import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import io.vavr.control.Try;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class HTMLFileRenderer implements Renderer<Path> {
|
public class HTMLFileRenderer implements Renderer<Path> {
|
||||||
@NonNull
|
@NonNull
|
||||||
private static final HTMLRenderer HTML_RENDERER = HTMLRenderer.newInstance();
|
private static final HTMLRenderer HTML_RENDERER = HTMLRenderer.newInstance();
|
||||||
private Path targetFile;
|
@NonNull
|
||||||
|
private Option<Path> targetFile;
|
||||||
|
|
||||||
private HTMLFileRenderer() {
|
private HTMLFileRenderer() {
|
||||||
try {
|
this.targetFile = Try
|
||||||
this.targetFile = Files.createTempFile("labyrinth_", ".html");
|
.of(() -> Files.createTempFile("labyrinth_", ".html"))
|
||||||
} catch (IOException e) {
|
.onFailure(ex -> log.error("Unable to set default target file.", ex))
|
||||||
System.err.println("Unable to set default target file.");
|
.toOption();
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -30,30 +35,33 @@ public class HTMLFileRenderer implements Renderer<Path> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTargetFileDefinedAndWritable() {
|
public boolean isTargetFileDefinedAndWritable() {
|
||||||
return this.targetFile != null && this.targetFile.toFile().canWrite();
|
return this.targetFile
|
||||||
|
.map(Path::toFile)
|
||||||
|
.exists(File::canWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public HTMLFileRenderer setTargetFile(@NonNull final Path targetFile) {
|
public HTMLFileRenderer setTargetFile(@NonNull final Path targetFile) {
|
||||||
this.targetFile = targetFile;
|
this.targetFile = Option.of(targetFile);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Path render(@NonNull final Labyrinth labyrinth) {
|
public Path render(@NonNull final Labyrinth labyrinth) {
|
||||||
if (!this.isTargetFileDefinedAndWritable()) {
|
if (!this.isTargetFileDefinedAndWritable()) {
|
||||||
try {
|
try {
|
||||||
Files.createFile(this.targetFile);
|
Files.createFile(this.targetFile.get());
|
||||||
} catch (IOException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
throw new IllegalArgumentException("Cannot write to target file.", e);
|
throw new IllegalArgumentException("Cannot write to target file.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final String html = HTML_RENDERER.render(labyrinth);
|
final String html = HTML_RENDERER.render(labyrinth);
|
||||||
|
final Path targetFile = this.targetFile.get();
|
||||||
try {
|
try {
|
||||||
Files.writeString(this.targetFile, html, StandardCharsets.UTF_8);
|
Files.writeString(targetFile, html, StandardCharsets.UTF_8);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Failed writing to file " + this.targetFile.normalize().toString());
|
log.error("Failed writing to file " + targetFile.normalize(), e);
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
return targetFile;
|
return targetFile;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import io.vavr.control.Option;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
@ -20,14 +21,12 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
class Generator {
|
class Generator {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Labyrinth labyrinth;
|
private final Labyrinth labyrinth;
|
||||||
|
|
||||||
private static boolean isValid(@NonNull final Position position) {
|
@NonNull
|
||||||
return position.getX() >= 0 && position.getY() >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] generate() {
|
public byte[] generate() {
|
||||||
final float pageWidth = this.labyrinth.getWidth() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN;
|
final float pageWidth = this.labyrinth.getWidth() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN;
|
||||||
final float pageHeight = this.labyrinth.getHeight() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN;
|
final float pageHeight = this.labyrinth.getHeight() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN;
|
||||||
|
@ -36,26 +35,26 @@ class Generator {
|
||||||
final PDDocumentInformation info = new PDDocumentInformation();
|
final PDDocumentInformation info = new PDDocumentInformation();
|
||||||
info.setTitle("Labyrinth " + this.labyrinth.getWidth() + "x" + this.labyrinth.getHeight() + ", ID " + this.labyrinth.getRandomSeed());
|
info.setTitle("Labyrinth " + this.labyrinth.getWidth() + "x" + this.labyrinth.getHeight() + ", ID " + this.labyrinth.getRandomSeed());
|
||||||
pdDocument.setDocumentInformation(info);
|
pdDocument.setDocumentInformation(info);
|
||||||
final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
||||||
final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
||||||
pdDocument.addPage(page);
|
pdDocument.addPage(puzzlePage);
|
||||||
pdDocument.addPage(solution);
|
pdDocument.addPage(solutionPage);
|
||||||
try (final PDPageContentStream labyrinthPageContentStream = new PDPageContentStream(pdDocument, page);
|
try (final PDPageContentStream puzzlePageContentStream = new PDPageContentStream(pdDocument, puzzlePage);
|
||||||
final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solution)) {
|
final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solutionPage)) {
|
||||||
setUpPageContentStream(labyrinthPageContentStream);
|
setUpPageContentStream(puzzlePageContentStream);
|
||||||
setUpPageContentStream(solutionPageContentStream);
|
setUpPageContentStream(solutionPageContentStream);
|
||||||
this.drawHorizonzalLines(labyrinthPageContentStream, solutionPageContentStream);
|
this.drawHorizontalLines(puzzlePageContentStream, solutionPageContentStream);
|
||||||
this.drawVerticalLines(labyrinthPageContentStream, solutionPageContentStream);
|
this.drawVerticalLines(puzzlePageContentStream, solutionPageContentStream);
|
||||||
this.drawSolution(solutionPageContentStream);
|
this.drawSolution(solutionPageContentStream);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
log.error("Error while rendering PDF document", e);
|
||||||
}
|
}
|
||||||
final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
try {
|
try {
|
||||||
pdDocument.save(output);
|
pdDocument.save(output);
|
||||||
pdDocument.close();
|
pdDocument.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
log.error("Error while writing PDF data", e);
|
||||||
}
|
}
|
||||||
return output.toByteArray();
|
return output.toByteArray();
|
||||||
}
|
}
|
||||||
|
@ -68,9 +67,10 @@ class Generator {
|
||||||
pageContentStream.setNonStrokingColor(Color.BLACK);
|
pageContentStream.setNonStrokingColor(Color.BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawHorizonzalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException {
|
private void drawHorizontalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException {
|
||||||
// PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
|
// PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
|
||||||
Coordinate coordinate = new Coordinate(0f, 0f);
|
Coordinate coordinate = new Coordinate(0f, 0f);
|
||||||
|
// Draw the TOP borders of all tiles.
|
||||||
for (int y = 0; y < this.labyrinth.getHeight(); y++) {
|
for (int y = 0; y < this.labyrinth.getHeight(); y++) {
|
||||||
boolean isPainting = false;
|
boolean isPainting = false;
|
||||||
coordinate = coordinate.withY(y);
|
coordinate = coordinate.withY(y);
|
||||||
|
@ -102,6 +102,7 @@ class Generator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Draw the BOTTOM border of the last row of tiles.
|
||||||
boolean isPainting = false;
|
boolean isPainting = false;
|
||||||
int y = this.labyrinth.getHeight();
|
int y = this.labyrinth.getHeight();
|
||||||
coordinate = coordinate.withY(this.labyrinth.getHeight());
|
coordinate = coordinate.withY(this.labyrinth.getHeight());
|
||||||
|
@ -137,6 +138,7 @@ class Generator {
|
||||||
private void drawVerticalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException {
|
private void drawVerticalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException {
|
||||||
// PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
|
// PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
|
||||||
Coordinate coordinate = new Coordinate(0f, 0f);
|
Coordinate coordinate = new Coordinate(0f, 0f);
|
||||||
|
// Draw the LEFT borders of all tiles.
|
||||||
for (int x = 0; x < this.labyrinth.getWidth(); x++) {
|
for (int x = 0; x < this.labyrinth.getWidth(); x++) {
|
||||||
boolean isPainting = false;
|
boolean isPainting = false;
|
||||||
coordinate = coordinate.withX(x);
|
coordinate = coordinate.withX(x);
|
||||||
|
@ -168,6 +170,7 @@ class Generator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Draw the RIGHT border of the last column of tiles.
|
||||||
boolean isPainting = false;
|
boolean isPainting = false;
|
||||||
int x = this.labyrinth.getWidth();
|
int x = this.labyrinth.getWidth();
|
||||||
coordinate = coordinate.withX(this.labyrinth.getWidth());
|
coordinate = coordinate.withX(this.labyrinth.getWidth());
|
||||||
|
|
|
@ -9,7 +9,6 @@ public class PDFRenderer implements Renderer<byte[]> {
|
||||||
static final float SCALE = 10;
|
static final float SCALE = 10;
|
||||||
|
|
||||||
private PDFRenderer() {
|
private PDFRenderer() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -17,12 +16,10 @@ public class PDFRenderer implements Renderer<byte[]> {
|
||||||
return new PDFRenderer();
|
return new PDFRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@Override
|
||||||
public byte[] render(@NonNull final Labyrinth labyrinth) {
|
public byte[] render(@NonNull final Labyrinth labyrinth) {
|
||||||
final Generator generator = new Generator(labyrinth);
|
final Generator generator = new Generator(labyrinth);
|
||||||
return generator.generate();
|
return generator.generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,24 +3,29 @@ package ch.fritteli.labyrinth.generator.renderer.pdffile;
|
||||||
import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.Renderer;
|
import ch.fritteli.labyrinth.generator.renderer.Renderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
|
import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import io.vavr.control.Try;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class PDFFileRenderer implements Renderer<Path> {
|
public class PDFFileRenderer implements Renderer<Path> {
|
||||||
@NonNull
|
@NonNull
|
||||||
private static final PDFRenderer PDF_RENDERER = PDFRenderer.newInstance();
|
private static final PDFRenderer PDF_RENDERER = PDFRenderer.newInstance();
|
||||||
private Path targetFile;
|
@NonNull
|
||||||
|
private Option<Path> targetFile;
|
||||||
|
|
||||||
private PDFFileRenderer() {
|
private PDFFileRenderer() {
|
||||||
try {
|
this.targetFile = Try
|
||||||
this.targetFile = Files.createTempFile("labyrinth_", ".pdf");
|
.of(() -> Files.createTempFile("labyrinth_", ".pdf"))
|
||||||
} catch (IOException e) {
|
.onFailure(ex -> log.error("Unable to set default target file.", ex))
|
||||||
System.err.println("Unable to set default target file.");
|
.toOption();
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -29,31 +34,34 @@ public class PDFFileRenderer implements Renderer<Path> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTargetFileDefinedAndWritable() {
|
public boolean isTargetFileDefinedAndWritable() {
|
||||||
return this.targetFile != null && this.targetFile.toFile().canWrite();
|
return this.targetFile
|
||||||
|
.map(Path::toFile)
|
||||||
|
.exists(File::canWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public PDFFileRenderer setTargetFile(@NonNull final Path targetFile) {
|
public PDFFileRenderer setTargetFile(@NonNull final Path targetFile) {
|
||||||
this.targetFile = targetFile;
|
this.targetFile = Option.of(targetFile);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Path render(@NonNull final Labyrinth labyrinth) {
|
public Path render(@NonNull final Labyrinth labyrinth) {
|
||||||
if (!this.isTargetFileDefinedAndWritable()) {
|
if (!this.isTargetFileDefinedAndWritable()) {
|
||||||
try {
|
try {
|
||||||
Files.createFile(this.targetFile);
|
Files.createFile(this.targetFile.get());
|
||||||
} catch (IOException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
throw new IllegalArgumentException("Cannot write to target file.", e);
|
throw new IllegalArgumentException("Cannot write to target file.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final byte[] bytes = PDF_RENDERER.render(labyrinth);
|
final byte[] bytes = PDF_RENDERER.render(labyrinth);
|
||||||
|
final Path targetFile = this.targetFile.get();
|
||||||
try {
|
try {
|
||||||
Files.write(this.targetFile, bytes);
|
Files.write(targetFile, bytes);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Failed writing to file " + this.targetFile.normalize().toString());
|
log.error("Failed writing to file " + targetFile.normalize(), e);
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
return this.targetFile;
|
return targetFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@ package ch.fritteli.labyrinth.generator.renderer.text;
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.FieldDefaults;
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
class CharDefinition {
|
class CharDefinition {
|
||||||
// ─
|
// ─
|
||||||
static final String HORIZONTAL = "\u2500";
|
static final String HORIZONTAL = "\u2500";
|
||||||
|
|
|
@ -15,6 +15,13 @@ class Generator {
|
||||||
private final boolean renderSolution;
|
private final boolean renderSolution;
|
||||||
private int x = 0;
|
private int x = 0;
|
||||||
private int y = 0;
|
private int y = 0;
|
||||||
|
// Each row has three lines:
|
||||||
|
// - The top border
|
||||||
|
// - The central pathway
|
||||||
|
// - The bottom border
|
||||||
|
// The bottom border of one row is identical to the top border of the following row. We use this variable here in
|
||||||
|
// order to be able to render the bottom border of the last row, which obviously does not have a following row with
|
||||||
|
// a top border.
|
||||||
private int line = 0;
|
private int line = 0;
|
||||||
|
|
||||||
boolean hasNext() {
|
boolean hasNext() {
|
||||||
|
|
|
@ -23,6 +23,7 @@ public class TextRenderer implements Renderer<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@Override
|
||||||
public String render(@NonNull final Labyrinth labyrinth) {
|
public String render(@NonNull final Labyrinth labyrinth) {
|
||||||
if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) {
|
if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) {
|
||||||
return "";
|
return "";
|
||||||
|
|
|
@ -4,29 +4,42 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.Renderer;
|
import ch.fritteli.labyrinth.generator.renderer.Renderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
|
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
|
||||||
import io.vavr.collection.List;
|
import io.vavr.collection.List;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import io.vavr.control.Try;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class TextFileRenderer implements Renderer<List<Path>> {
|
public class TextFileRenderer implements Renderer<List<Path>> {
|
||||||
@NonNull
|
@NonNull
|
||||||
private static final TextRenderer TEXT_RENDERER = TextRenderer.newInstance();
|
private static final TextRenderer TEXT_RENDERER = TextRenderer.newInstance();
|
||||||
private Path targetLabyrinthFile;
|
@NonNull
|
||||||
private Path targetSolutionFile;
|
private Option<Path> targetLabyrinthFile;
|
||||||
|
@NonNull
|
||||||
|
private Option<Path> targetSolutionFile;
|
||||||
|
|
||||||
private TextFileRenderer() {
|
private TextFileRenderer() {
|
||||||
try {
|
this.targetLabyrinthFile = Try
|
||||||
this.targetLabyrinthFile = Files.createTempFile("labyrinth_", ".txt");
|
.of(() -> Files.createTempFile("labyrinth_", ".txt"))
|
||||||
this.targetSolutionFile = this.targetLabyrinthFile.getParent().resolve(
|
.onFailure(ex -> log.error("Unable to set default target file", ex))
|
||||||
this.targetLabyrinthFile.getFileName().toString().replace(".txt", "-solution.txt")
|
.toOption();
|
||||||
);
|
|
||||||
} catch (IOException e) {
|
this.targetSolutionFile = this.targetLabyrinthFile.toTry()
|
||||||
System.err.println("Unable to set default target file.");
|
.map(Path::getParent)
|
||||||
e.printStackTrace();
|
.flatMap(parent -> this.targetLabyrinthFile.toTry()
|
||||||
}
|
.map(Path::getFileName)
|
||||||
|
.map(Path::toString)
|
||||||
|
.map(a -> a.replace(".txt", "-solution.txt"))
|
||||||
|
.map(parent::resolve))
|
||||||
|
.onFailure(ex -> log.error("Unable to set default solution target file", ex))
|
||||||
|
.toOption();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -35,38 +48,44 @@ public class TextFileRenderer implements Renderer<List<Path>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTargetLabyrinthFileDefinedAndWritable() {
|
public boolean isTargetLabyrinthFileDefinedAndWritable() {
|
||||||
return this.targetLabyrinthFile != null && this.targetLabyrinthFile.toFile().canWrite();
|
return this.targetLabyrinthFile
|
||||||
|
.map(Path::toFile)
|
||||||
|
.exists(File::canWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTargetSolutionFileDefinedAndWritable() {
|
public boolean isTargetSolutionFileDefinedAndWritable() {
|
||||||
return this.targetSolutionFile != null && this.targetSolutionFile.toFile().canWrite();
|
return this.targetSolutionFile
|
||||||
|
.map(Path::toFile)
|
||||||
|
.exists(File::canWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public TextFileRenderer setTargetLabyrinthFile(@NonNull final Path targetLabyrinthFile) {
|
public TextFileRenderer setTargetLabyrinthFile(@NonNull final Path targetLabyrinthFile) {
|
||||||
this.targetLabyrinthFile = targetLabyrinthFile;
|
this.targetLabyrinthFile = Option.of(targetLabyrinthFile);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public TextFileRenderer setTargetSolutionFile(@NonNull final Path targetSolutionFile) {
|
public TextFileRenderer setTargetSolutionFile(@NonNull final Path targetSolutionFile) {
|
||||||
this.targetSolutionFile = targetSolutionFile;
|
this.targetSolutionFile = Option.of(targetSolutionFile);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public List<Path> render(@NonNull final Labyrinth labyrinth) {
|
public List<Path> render(@NonNull final Labyrinth labyrinth) {
|
||||||
if (!this.isTargetLabyrinthFileDefinedAndWritable()) {
|
if (!this.isTargetLabyrinthFileDefinedAndWritable()) {
|
||||||
try {
|
try {
|
||||||
Files.createFile(this.targetLabyrinthFile);
|
Files.createFile(this.targetLabyrinthFile.get());
|
||||||
} catch (IOException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
|
log.error("Cannot write to target labyrinth file.", e);
|
||||||
throw new IllegalArgumentException("Cannot write to target labyrinth file.", e);
|
throw new IllegalArgumentException("Cannot write to target labyrinth file.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.isTargetSolutionFileDefinedAndWritable()) {
|
if (!this.isTargetSolutionFileDefinedAndWritable()) {
|
||||||
try {
|
try {
|
||||||
Files.createFile(this.targetSolutionFile);
|
Files.createFile(this.targetSolutionFile.get());
|
||||||
} catch (IOException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
throw new IllegalArgumentException("Cannot write to target solution file.", e);
|
throw new IllegalArgumentException("Cannot write to target solution file.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,13 +95,18 @@ public class TextFileRenderer implements Renderer<List<Path>> {
|
||||||
text = TEXT_RENDERER.setRenderSolution(false).render(labyrinth).strip();
|
text = TEXT_RENDERER.setRenderSolution(false).render(labyrinth).strip();
|
||||||
solution = TEXT_RENDERER.setRenderSolution(true).render(labyrinth).strip();
|
solution = TEXT_RENDERER.setRenderSolution(true).render(labyrinth).strip();
|
||||||
}
|
}
|
||||||
|
final Path targetLabyrinthFile = this.targetLabyrinthFile.get();
|
||||||
|
final Path targetSolutionFile = this.targetSolutionFile.get();
|
||||||
try {
|
try {
|
||||||
Files.write(this.targetLabyrinthFile, text.getBytes(StandardCharsets.UTF_8));
|
Files.write(targetLabyrinthFile, text.getBytes(StandardCharsets.UTF_8));
|
||||||
Files.write(this.targetSolutionFile, solution.getBytes(StandardCharsets.UTF_8));
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Failed writing to file " + this.targetLabyrinthFile.normalize().toString());
|
log.error("Failed writing to file " + targetLabyrinthFile.normalize(), e);
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
return List.of(this.targetLabyrinthFile, this.targetSolutionFile);
|
try {
|
||||||
|
Files.write(targetSolutionFile, solution.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed writing to file " + targetSolutionFile.normalize());
|
||||||
|
}
|
||||||
|
return List.of(targetLabyrinthFile, targetSolutionFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,208 @@
|
||||||
|
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 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 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) {
|
||||||
|
final LabyrinthOutputStream stream = new LabyrinthOutputStream();
|
||||||
|
writeHeader(stream);
|
||||||
|
writeLabyrinthData(stream, 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 LabyrinthInputStream stream = new LabyrinthInputStream(bytes);
|
||||||
|
checkHeader(stream);
|
||||||
|
return readLabyrinthData(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeHeader(@NonNull final LabyrinthOutputStream stream) {
|
||||||
|
stream.writeByte(MAGIC_BYTE_1);
|
||||||
|
stream.writeByte(MAGIC_BYTE_2);
|
||||||
|
stream.writeByte(VERSION_BYTE);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeLabyrinthData(@NonNull final LabyrinthOutputStream stream, @NonNull final Labyrinth labyrinth) {
|
||||||
|
final long randomSeed = labyrinth.getRandomSeed();
|
||||||
|
final int width = labyrinth.getWidth();
|
||||||
|
final int height = labyrinth.getHeight();
|
||||||
|
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.
|
||||||
|
final Tile tile = labyrinth.getTileAt(x, y).get();
|
||||||
|
final byte bitmask = getBitmaskForTile(tile);
|
||||||
|
stream.writeByte(bitmask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static Labyrinth readLabyrinthData(@NonNull final LabyrinthInputStream stream) {
|
||||||
|
final long randomSeed = stream.readLong();
|
||||||
|
final int width = stream.readInt();
|
||||||
|
final int height = stream.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 = 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/java" isTestSource="false" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
16
src/main/resources/logback.xml
Normal file
16
src/main/resources/logback.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<configuration>
|
||||||
|
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
|
||||||
|
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<!-- encoders are by default assigned the type
|
||||||
|
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
|
@ -44,4 +44,16 @@ class CharDefinitionTest {
|
||||||
assertEquals("┤", new CharDefinition(true, true, true, false, true).toString());
|
assertEquals("┤", new CharDefinition(true, true, true, false, true).toString());
|
||||||
assertEquals("┼", new CharDefinition(true, true, true, true, true).toString());
|
assertEquals("┼", new CharDefinition(true, true, true, true, true).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuilderMethods() {
|
||||||
|
assertEquals(new CharDefinition(true, false, false, false, false), new CharDefinition().up());
|
||||||
|
assertEquals(new CharDefinition(false, true, false, false, false), new CharDefinition().down());
|
||||||
|
assertEquals(new CharDefinition(false, false, true, false, false), new CharDefinition().left());
|
||||||
|
assertEquals(new CharDefinition(false, false, false, true, false), new CharDefinition().right());
|
||||||
|
assertEquals(new CharDefinition(false, false, false, false, true), new CharDefinition().solution());
|
||||||
|
assertEquals(new CharDefinition(true, true, false, false, false), new CharDefinition().vertical());
|
||||||
|
assertEquals(new CharDefinition(false, false, true, true, false), new CharDefinition().horizontal());
|
||||||
|
assertEquals(new CharDefinition(true, true, true, true, true), new CharDefinition().vertical().horizontal().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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/java" isTestSource="true" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
Loading…
Reference in a new issue