Make the generation of a labyrinth reproducible and refactor the

PDFRenderer a bit.
This commit is contained in:
Manuel Friedli 2020-10-04 21:57:11 +02:00
parent fd38b141a1
commit 6060a08573
6 changed files with 114 additions and 74 deletions

View file

@ -6,33 +6,6 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
public class HTMLRenderer implements Renderer<String> { public class HTMLRenderer implements Renderer<String> {
private static final String PREAMBLE = "<!DOCTYPE html><html lang=\"en\">" +
"<head>" +
"<title>Labyrinth</title>" +
"<meta charset=\"utf-8\">" +
"<style>" +
"table{border-collapse:collapse;}" +
"td{border:0 solid black;height:1em;width:1em;}" +
"td.top{border-top-width:1px;}" +
"td.right{border-right-width:1px;}" +
"td.bottom{border-bottom-width:1px;}" +
"td.left{border-left-width:1px;}" +
"</style>" +
"<script>" +
"let solution = false;" +
"function toggleSolution() {" +
"let stylesheet = document.styleSheets[0];" +
"if(solution){" +
"stylesheet.deleteRule(0);" +
"}else{" +
"stylesheet.insertRule(\"td.solution{background-color:lightgray;}\", 0);" +
"}" +
"solution = !solution;" +
"}" +
"</script>" +
"</head>" +
"<body>" +
"<input type=\"checkbox\" onclick=\"toggleSolution()\">show solution</input>";
private static final String POSTAMBLE = "</body></html>"; private static final String POSTAMBLE = "</body></html>";
private HTMLRenderer() { private HTMLRenderer() {
@ -46,10 +19,10 @@ public class HTMLRenderer implements Renderer<String> {
@NonNull @NonNull
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 PREAMBLE + POSTAMBLE; return this.getPreamble(labyrinth) + POSTAMBLE;
} }
final Generator generator = new Generator(labyrinth); final Generator generator = new Generator(labyrinth);
final StringBuilder sb = new StringBuilder(PREAMBLE); final StringBuilder sb = new StringBuilder(this.getPreamble(labyrinth));
sb.append("<table>"); sb.append("<table>");
while (generator.hasNext()) { while (generator.hasNext()) {
sb.append(generator.next()); sb.append(generator.next());
@ -59,6 +32,36 @@ public class HTMLRenderer implements Renderer<String> {
return sb.toString(); return sb.toString();
} }
private String getPreamble(@NonNull final Labyrinth labyrinth) {
return "<!DOCTYPE html><html lang=\"en\">" +
"<head>" +
"<title>Labyrinth " + labyrinth.getWidth() + "x" + labyrinth.getHeight() + ", ID " + labyrinth.getRandomSeed() + "</title>" +
"<meta charset=\"utf-8\">" +
"<style>" +
"table{border-collapse:collapse;}" +
"td{border:0 solid black;height:1em;width:1em;}" +
"td.top{border-top-width:1px;}" +
"td.right{border-right-width:1px;}" +
"td.bottom{border-bottom-width:1px;}" +
"td.left{border-left-width:1px;}" +
"</style>" +
"<script>" +
"let solution = false;" +
"function toggleSolution() {" +
"let stylesheet = document.styleSheets[0];" +
"if(solution){" +
"stylesheet.deleteRule(0);" +
"}else{" +
"stylesheet.insertRule(\"td.solution{background-color:lightgray;}\", 0);" +
"}" +
"solution = !solution;" +
"}" +
"</script>" +
"</head>" +
"<body>" +
"<input type=\"checkbox\" onclick=\"toggleSolution()\">show solution</input>";
}
@RequiredArgsConstructor @RequiredArgsConstructor
private static class Generator { private static class Generator {
private final Labyrinth labyrinth; private final Labyrinth labyrinth;

View file

@ -6,6 +6,7 @@ import lombok.NonNull;
import java.util.Deque; import java.util.Deque;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Random;
public class Labyrinth { public class Labyrinth {
private final Tile[][] field; private final Tile[][] field;
@ -14,13 +15,22 @@ public class Labyrinth {
@Getter @Getter
private final int height; private final int height;
@Getter @Getter
private final long randomSeed;
private final Random random;
@Getter
private final Position start; private final Position start;
@Getter @Getter
private final Position end; private final Position end;
public Labyrinth(final int width, final int height) { public Labyrinth(final int width, final int height) {
this(width, height, System.nanoTime());
}
public Labyrinth(final int width, final int height, final long randomSeed) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.randomSeed = randomSeed;
this.random = new Random(this.randomSeed);
this.field = new Tile[width][height]; this.field = new Tile[width][height];
this.start = new Position(0, 0); this.start = new Position(0, 0);
this.end = new Position(this.width - 1, this.height - 1); this.end = new Position(this.width - 1, this.height - 1);
@ -94,7 +104,7 @@ public class Labyrinth {
while (!this.positions.isEmpty()) { while (!this.positions.isEmpty()) {
final Position currentPosition = this.positions.peek(); final Position currentPosition = this.positions.peek();
final Tile currentTile = Labyrinth.this.getTileAt(currentPosition); final Tile currentTile = Labyrinth.this.getTileAt(currentPosition);
final Option<Direction> directionToDigTo = currentTile.getRandomAvailableDirection(); final Option<Direction> directionToDigTo = currentTile.getRandomAvailableDirection(Labyrinth.this.random);
if (directionToDigTo.isDefined()) { if (directionToDigTo.isDefined()) {
final Direction digTo = directionToDigTo.get(); final Direction digTo = directionToDigTo.get();
final Direction digFrom = digTo.invert(); final Direction digFrom = digTo.invert();

View file

@ -9,17 +9,20 @@ 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;
int height = 100; int height = 100;
final Labyrinth labyrinth = new Labyrinth(width, height); final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/);
final TextRenderer textRenderer = TextRenderer.newInstance(); final TextRenderer textRenderer = TextRenderer.newInstance();
final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance();
final Path userHome = Paths.get(System.getProperty("user.home")); final Path userHome = Paths.get(System.getProperty("user.home"));
final String baseFilename = getBaseFilename(labyrinth);
final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance() final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance()
.setTargetLabyrinthFile(userHome.resolve("labyrinth.txt")) .setTargetLabyrinthFile(userHome.resolve(baseFilename + ".txt"))
.setTargetSolutionFile(userHome.resolve("labyrinth-solution.txt")); .setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt"));
final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance() final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance()
.setTargetFile(userHome.resolve("labyrinth.html")); .setTargetFile(userHome.resolve(baseFilename + ".html"));
final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance().setTargetFile(userHome.resolve("labyrinth.pdf")); final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance()
.setTargetFile(userHome.resolve(baseFilename + ".pdf"));
System.out.println("Labyrinth-ID: " + labyrinth.getRandomSeed());
// Render Labyrinth to stdout // Render Labyrinth to stdout
System.out.println(textRenderer.render(labyrinth)); System.out.println(textRenderer.render(labyrinth));
// Render Labyrinth solution to stdout // Render Labyrinth solution to stdout
@ -33,4 +36,8 @@ public class Main {
// Render PDF to file // Render PDF to file
System.out.println(pdfFileRenderer.render(labyrinth)); System.out.println(pdfFileRenderer.render(labyrinth));
} }
private static String getBaseFilename(@NonNull final Labyrinth labyrinth) {
return "labyrinth-" + labyrinth.getWidth() + "x" + labyrinth.getHeight() + "-" + labyrinth.getRandomSeed();
}
} }

View file

@ -2,6 +2,7 @@ package ch.fritteli.labyrinth;
import lombok.NonNull; import lombok.NonNull;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
@ -35,30 +36,22 @@ public class PDFRenderer implements Renderer<byte[]> {
final float pageHeight = labyrinth.getHeight() * SCALE + 2 * MARGIN; final float pageHeight = labyrinth.getHeight() * SCALE + 2 * MARGIN;
final PDDocument pdDocument = new PDDocument(); final PDDocument pdDocument = new PDDocument();
final PDDocumentInformation info = new PDDocumentInformation();
info.setTitle("Labyrinth " + labyrinth.getWidth() + "x" + labyrinth.getHeight() + ", ID " + labyrinth.getRandomSeed());
pdDocument.setDocumentInformation(info);
final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight)); final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight));
pdDocument.addPage(page);
try (PDPageContentStream pdPageContentStream = new PDPageContentStream(pdDocument, page)) {
pdPageContentStream.setLineCapStyle(BasicStroke.CAP_ROUND);
pdPageContentStream.setLineJoinStyle(BasicStroke.JOIN_MITER);
pdPageContentStream.setLineWidth(1.0f);
pdPageContentStream.setStrokingColor(Color.BLACK);
pdPageContentStream.setNonStrokingColor(Color.BLACK);
this.drawHorizonzalLines(labyrinth, pdPageContentStream);
this.drawVerticalLines(labyrinth, pdPageContentStream);
} catch (IOException e) {
e.printStackTrace();
}
final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight)); final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight));
pdDocument.addPage(page);
pdDocument.addPage(solution); pdDocument.addPage(solution);
try (PDPageContentStream pdPageContentStream = new PDPageContentStream(pdDocument, solution)) { try (final PDPageContentStream labyrinthPageContentStream = new PDPageContentStream(pdDocument, page);
pdPageContentStream.setLineCapStyle(BasicStroke.CAP_ROUND); final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solution)) {
pdPageContentStream.setLineJoinStyle(BasicStroke.JOIN_MITER); setUpPageContentStream(labyrinthPageContentStream);
pdPageContentStream.setLineWidth(1.0f); setUpPageContentStream(solutionPageContentStream);
pdPageContentStream.setStrokingColor(Color.BLACK); this.drawHorizonzalLines(labyrinth, labyrinthPageContentStream);
pdPageContentStream.setNonStrokingColor(Color.BLACK); this.drawVerticalLines(labyrinth, labyrinthPageContentStream);
this.drawHorizonzalLines(labyrinth, pdPageContentStream); this.drawHorizonzalLines(labyrinth, solutionPageContentStream);
this.drawVerticalLines(labyrinth, pdPageContentStream); this.drawVerticalLines(labyrinth, solutionPageContentStream);
this.drawSolution(labyrinth, pdPageContentStream); this.drawSolution(labyrinth, solutionPageContentStream);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -72,51 +65,65 @@ public class PDFRenderer implements Renderer<byte[]> {
return output.toByteArray(); return output.toByteArray();
} }
private void setUpPageContentStream(@NonNull final PDPageContentStream pageContentStream) throws IOException {
pageContentStream.setLineCapStyle(BasicStroke.CAP_ROUND);
pageContentStream.setLineJoinStyle(BasicStroke.JOIN_ROUND);
pageContentStream.setLineWidth(1.0f);
pageContentStream.setStrokingColor(Color.BLACK);
pageContentStream.setNonStrokingColor(Color.BLACK);
}
private void drawHorizonzalLines(@NonNull final Labyrinth labyrinth, private void drawHorizonzalLines(@NonNull final Labyrinth labyrinth,
@NonNull final PDPageContentStream pdPageContentStream) throws IOException { @NonNull final PDPageContentStream pdPageContentStream) 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.
final float labyrinthHeight = labyrinth.getHeight() * SCALE; final float labyrinthHeight = labyrinth.getHeight() * SCALE;
for (int y = 0; y < labyrinth.getHeight(); y++) { for (int y = 0; y < labyrinth.getHeight(); y++) {
boolean isPainting = false; boolean isPainting = false;
final float yCoordinate = labyrinthHeight - y * SCALE + MARGIN;
for (int x = 0; x < labyrinth.getWidth(); x++) { for (int x = 0; x < labyrinth.getWidth(); x++) {
final Tile currentTile = labyrinth.getTileAt(x, y); final Tile currentTile = labyrinth.getTileAt(x, y);
final float xCoordinate = x * SCALE + MARGIN;
if (currentTile.hasWallAt(Direction.TOP)) { if (currentTile.hasWallAt(Direction.TOP)) {
if (!isPainting) { if (!isPainting) {
pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.moveTo(xCoordinate, yCoordinate);
isPainting = true; isPainting = true;
} }
} else { } else {
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, yCoordinate);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
isPainting = false; isPainting = false;
} }
} }
} }
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); final float xCoordinate = labyrinth.getWidth() * SCALE + MARGIN;
pdPageContentStream.lineTo(xCoordinate, yCoordinate);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
} }
} }
boolean isPainting = false; boolean isPainting = false;
int y = labyrinth.getHeight(); int y = labyrinth.getHeight();
final float yCoordinate = /*labyrinthHeight - y * SCALE +*/ MARGIN;
for (int x = 0; x < labyrinth.getWidth(); x++) { for (int x = 0; x < labyrinth.getWidth(); x++) {
final Tile currentTile = labyrinth.getTileAt(x, y - 1); final Tile currentTile = labyrinth.getTileAt(x, y - 1);
final float xCoordinate = x * SCALE + MARGIN;
if (currentTile.hasWallAt(Direction.BOTTOM)) { if (currentTile.hasWallAt(Direction.BOTTOM)) {
if (!isPainting) { if (!isPainting) {
pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.moveTo(xCoordinate, yCoordinate);
isPainting = true; isPainting = true;
} }
} else { } else {
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, yCoordinate);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
isPainting = false; isPainting = false;
} }
} }
} }
final float xCoordinate = labyrinth.getWidth() * SCALE + MARGIN;
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, yCoordinate);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
} }
} }
@ -127,45 +134,49 @@ public class PDFRenderer implements Renderer<byte[]> {
final float labyrinthHeight = labyrinth.getHeight() * SCALE; final float labyrinthHeight = labyrinth.getHeight() * SCALE;
for (int x = 0; x < labyrinth.getWidth(); x++) { for (int x = 0; x < labyrinth.getWidth(); x++) {
boolean isPainting = false; boolean isPainting = false;
final float xCoordinate = x * SCALE + MARGIN;
for (int y = 0; y < labyrinth.getHeight(); y++) { for (int y = 0; y < labyrinth.getHeight(); y++) {
final Tile currentTile = labyrinth.getTileAt(x, y); final Tile currentTile = labyrinth.getTileAt(x, y);
final float yCoordinate = labyrinthHeight - y * SCALE + MARGIN;
if (currentTile.hasWallAt(Direction.LEFT)) { if (currentTile.hasWallAt(Direction.LEFT)) {
if (!isPainting) { if (!isPainting) {
pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.moveTo(xCoordinate, yCoordinate);
isPainting = true; isPainting = true;
} }
} else { } else {
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, yCoordinate);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
isPainting = false; isPainting = false;
} }
} }
} }
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, MARGIN);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
} }
} }
boolean isPainting = false; boolean isPainting = false;
int x = labyrinth.getWidth(); int x = labyrinth.getWidth();
final float xCoordinate = x * SCALE + MARGIN;
for (int y = 0; y < labyrinth.getHeight(); y++) { for (int y = 0; y < labyrinth.getHeight(); y++) {
final Tile currentTile = labyrinth.getTileAt(x - 1, y); final Tile currentTile = labyrinth.getTileAt(x - 1, y);
final float yCoordinate = labyrinthHeight - y * SCALE + MARGIN;
if (currentTile.hasWallAt(Direction.RIGHT)) { if (currentTile.hasWallAt(Direction.RIGHT)) {
if (!isPainting) { if (!isPainting) {
pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.moveTo(xCoordinate, yCoordinate);
isPainting = true; isPainting = true;
} }
} else { } else {
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, yCoordinate);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
isPainting = false; isPainting = false;
} }
} }
} }
if (isPainting) { if (isPainting) {
pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN); pdPageContentStream.lineTo(xCoordinate, MARGIN);
pdPageContentStream.stroke(); pdPageContentStream.stroke();
} }
} }
@ -184,11 +195,14 @@ public class PDFRenderer implements Renderer<byte[]> {
Position newCurrent = this.findNextSolutionPosition(labyrinth, previousPosition, currentPosition); Position newCurrent = this.findNextSolutionPosition(labyrinth, previousPosition, currentPosition);
previousPosition = currentPosition; previousPosition = currentPosition;
currentPosition = newCurrent; currentPosition = newCurrent;
pdPageContentStream.lineTo(MARGIN + currentPosition.getX() * SCALE + SCALE / 2, MARGIN + labyrinthHeight - (currentPosition.getY() * SCALE + SCALE / 2)); final float xCoordinate = MARGIN + currentPosition.getX() * SCALE + SCALE / 2;
final float yCoordinate = MARGIN + labyrinthHeight - (currentPosition.getY() * SCALE + SCALE / 2);
pdPageContentStream.lineTo(xCoordinate, yCoordinate);
} while (!currentPosition.equals(end)); } while (!currentPosition.equals(end));
pdPageContentStream.stroke(); pdPageContentStream.stroke();
} }
@NonNull
private Position findNextSolutionPosition(@NonNull final Labyrinth labyrinth, @Nullable final Position previousPosition, @NonNull final Position currentPosition) { private Position findNextSolutionPosition(@NonNull final Labyrinth labyrinth, @Nullable final Position previousPosition, @NonNull final Position currentPosition) {
final Tile currentTile = labyrinth.getTileAt(currentPosition); final Tile currentTile = labyrinth.getTileAt(currentPosition);
for (final Direction direction : Direction.values()) { for (final Direction direction : Direction.values()) {
@ -202,7 +216,6 @@ public class PDFRenderer implements Renderer<byte[]> {
} }
} }
} }
// We *SHOULD* never get here. ... famous last words ... throw new IllegalStateException("We *SHOULD* never have gotten here. ... famous last words ...");
return null;
} }
} }

View file

@ -7,6 +7,8 @@ import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import java.util.Random;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Tile { public class Tile {
final Walls walls = new Walls(); final Walls walls = new Walls();
@ -42,8 +44,13 @@ public class Tile {
this.walls.set(direction); this.walls.set(direction);
} }
public Option<Direction> getRandomAvailableDirection() { public Option<Direction> getRandomAvailableDirection(@NonNull final Random random) {
return Stream.ofAll(this.walls.getUnhardenedSet()).shuffle().headOption(); final Stream<Direction> availableDirections = this.walls.getUnhardenedSet();
if (availableDirections.isEmpty()) {
return Option.none();
}
final int index = random.nextInt(availableDirections.length());
return Option.of(availableDirections.get(index));
} }
public boolean hasWallAt(@NonNull final Direction direction) { public boolean hasWallAt(@NonNull final Direction direction) {

View file

@ -30,7 +30,7 @@ public class Walls {
return this.directions.contains(direction); return this.directions.contains(direction);
} }
public Iterable<Direction> getUnhardenedSet() { public Stream<Direction> getUnhardenedSet() {
return Stream.ofAll(this.directions) return Stream.ofAll(this.directions)
.removeAll(this.hardened); .removeAll(this.hardened);
} }