From 6060a0857354a2994a6de6fdcfcb04a300d46d97 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 4 Oct 2020 21:57:11 +0200 Subject: [PATCH] Make the generation of a labyrinth reproducible and refactor the PDFRenderer a bit. --- .../ch/fritteli/labyrinth/HTMLRenderer.java | 61 ++++++------- .../java/ch/fritteli/labyrinth/Labyrinth.java | 12 ++- src/main/java/ch/fritteli/labyrinth/Main.java | 17 ++-- .../ch/fritteli/labyrinth/PDFRenderer.java | 85 +++++++++++-------- src/main/java/ch/fritteli/labyrinth/Tile.java | 11 ++- .../java/ch/fritteli/labyrinth/Walls.java | 2 +- 6 files changed, 114 insertions(+), 74 deletions(-) diff --git a/src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java b/src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java index 73983ec..fa3e072 100644 --- a/src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java +++ b/src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java @@ -6,33 +6,6 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; public class HTMLRenderer implements Renderer { - private static final String PREAMBLE = "" + - "" + - "Labyrinth" + - "" + - "" + - "" + - "" + - "" + - "show solution"; private static final String POSTAMBLE = ""; private HTMLRenderer() { @@ -46,10 +19,10 @@ public class HTMLRenderer implements Renderer { @NonNull public String render(@NonNull final Labyrinth labyrinth) { if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { - return PREAMBLE + POSTAMBLE; + return this.getPreamble(labyrinth) + POSTAMBLE; } final Generator generator = new Generator(labyrinth); - final StringBuilder sb = new StringBuilder(PREAMBLE); + final StringBuilder sb = new StringBuilder(this.getPreamble(labyrinth)); sb.append(""); while (generator.hasNext()) { sb.append(generator.next()); @@ -59,6 +32,36 @@ public class HTMLRenderer implements Renderer { return sb.toString(); } + private String getPreamble(@NonNull final Labyrinth labyrinth) { + return "" + + "" + + "Labyrinth " + labyrinth.getWidth() + "x" + labyrinth.getHeight() + ", ID " + labyrinth.getRandomSeed() + "" + + "" + + "" + + "" + + "" + + "" + + "show solution"; + } + @RequiredArgsConstructor private static class Generator { private final Labyrinth labyrinth; diff --git a/src/main/java/ch/fritteli/labyrinth/Labyrinth.java b/src/main/java/ch/fritteli/labyrinth/Labyrinth.java index b5c6244..02838ee 100644 --- a/src/main/java/ch/fritteli/labyrinth/Labyrinth.java +++ b/src/main/java/ch/fritteli/labyrinth/Labyrinth.java @@ -6,6 +6,7 @@ import lombok.NonNull; import java.util.Deque; import java.util.LinkedList; +import java.util.Random; public class Labyrinth { private final Tile[][] field; @@ -14,13 +15,22 @@ public class Labyrinth { @Getter private final int height; @Getter + private final long randomSeed; + private final Random random; + @Getter private final Position start; @Getter private final Position end; 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.height = height; + this.randomSeed = randomSeed; + this.random = new Random(this.randomSeed); this.field = new Tile[width][height]; this.start = new Position(0, 0); this.end = new Position(this.width - 1, this.height - 1); @@ -94,7 +104,7 @@ public class Labyrinth { while (!this.positions.isEmpty()) { final Position currentPosition = this.positions.peek(); final Tile currentTile = Labyrinth.this.getTileAt(currentPosition); - final Option directionToDigTo = currentTile.getRandomAvailableDirection(); + final Option directionToDigTo = currentTile.getRandomAvailableDirection(Labyrinth.this.random); if (directionToDigTo.isDefined()) { final Direction digTo = directionToDigTo.get(); final Direction digFrom = digTo.invert(); diff --git a/src/main/java/ch/fritteli/labyrinth/Main.java b/src/main/java/ch/fritteli/labyrinth/Main.java index 084a784..cf8f2f1 100644 --- a/src/main/java/ch/fritteli/labyrinth/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/Main.java @@ -9,17 +9,20 @@ public class Main { public static void main(@NonNull final String[] args) { int width = 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 HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); final Path userHome = Paths.get(System.getProperty("user.home")); + final String baseFilename = getBaseFilename(labyrinth); final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance() - .setTargetLabyrinthFile(userHome.resolve("labyrinth.txt")) - .setTargetSolutionFile(userHome.resolve("labyrinth-solution.txt")); + .setTargetLabyrinthFile(userHome.resolve(baseFilename + ".txt")) + .setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt")); final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance() - .setTargetFile(userHome.resolve("labyrinth.html")); - final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance().setTargetFile(userHome.resolve("labyrinth.pdf")); + .setTargetFile(userHome.resolve(baseFilename + ".html")); + final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() + .setTargetFile(userHome.resolve(baseFilename + ".pdf")); + System.out.println("Labyrinth-ID: " + labyrinth.getRandomSeed()); // Render Labyrinth to stdout System.out.println(textRenderer.render(labyrinth)); // Render Labyrinth solution to stdout @@ -33,4 +36,8 @@ public class Main { // Render PDF to file System.out.println(pdfFileRenderer.render(labyrinth)); } + + private static String getBaseFilename(@NonNull final Labyrinth labyrinth) { + return "labyrinth-" + labyrinth.getWidth() + "x" + labyrinth.getHeight() + "-" + labyrinth.getRandomSeed(); + } } diff --git a/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java b/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java index 7b2d6ae..dd403ff 100644 --- a/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java +++ b/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java @@ -2,6 +2,7 @@ package ch.fritteli.labyrinth; import lombok.NonNull; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; @@ -35,30 +36,22 @@ public class PDFRenderer implements Renderer { final float pageHeight = labyrinth.getHeight() * SCALE + 2 * MARGIN; 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)); - 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)); + pdDocument.addPage(page); pdDocument.addPage(solution); - try (PDPageContentStream pdPageContentStream = new PDPageContentStream(pdDocument, solution)) { - 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); - this.drawSolution(labyrinth, pdPageContentStream); + try (final PDPageContentStream labyrinthPageContentStream = new PDPageContentStream(pdDocument, page); + final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solution)) { + setUpPageContentStream(labyrinthPageContentStream); + setUpPageContentStream(solutionPageContentStream); + this.drawHorizonzalLines(labyrinth, labyrinthPageContentStream); + this.drawVerticalLines(labyrinth, labyrinthPageContentStream); + this.drawHorizonzalLines(labyrinth, solutionPageContentStream); + this.drawVerticalLines(labyrinth, solutionPageContentStream); + this.drawSolution(labyrinth, solutionPageContentStream); } catch (IOException e) { e.printStackTrace(); } @@ -72,51 +65,65 @@ public class PDFRenderer implements Renderer { 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, @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. final float labyrinthHeight = labyrinth.getHeight() * SCALE; for (int y = 0; y < labyrinth.getHeight(); y++) { boolean isPainting = false; + final float yCoordinate = labyrinthHeight - y * SCALE + MARGIN; for (int x = 0; x < labyrinth.getWidth(); x++) { final Tile currentTile = labyrinth.getTileAt(x, y); + final float xCoordinate = x * SCALE + MARGIN; if (currentTile.hasWallAt(Direction.TOP)) { if (!isPainting) { - pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.moveTo(xCoordinate, yCoordinate); isPainting = true; } } else { if (isPainting) { - pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, yCoordinate); pdPageContentStream.stroke(); isPainting = false; } } } 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(); } } boolean isPainting = false; int y = labyrinth.getHeight(); + final float yCoordinate = /*labyrinthHeight - y * SCALE +*/ MARGIN; for (int x = 0; x < labyrinth.getWidth(); x++) { final Tile currentTile = labyrinth.getTileAt(x, y - 1); + final float xCoordinate = x * SCALE + MARGIN; if (currentTile.hasWallAt(Direction.BOTTOM)) { if (!isPainting) { - pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.moveTo(xCoordinate, yCoordinate); isPainting = true; } } else { if (isPainting) { - pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, yCoordinate); pdPageContentStream.stroke(); isPainting = false; } } } + final float xCoordinate = labyrinth.getWidth() * SCALE + MARGIN; if (isPainting) { - pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, yCoordinate); pdPageContentStream.stroke(); } } @@ -127,45 +134,49 @@ public class PDFRenderer implements Renderer { final float labyrinthHeight = labyrinth.getHeight() * SCALE; for (int x = 0; x < labyrinth.getWidth(); x++) { boolean isPainting = false; + final float xCoordinate = x * SCALE + MARGIN; for (int y = 0; y < labyrinth.getHeight(); y++) { final Tile currentTile = labyrinth.getTileAt(x, y); + final float yCoordinate = labyrinthHeight - y * SCALE + MARGIN; if (currentTile.hasWallAt(Direction.LEFT)) { if (!isPainting) { - pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.moveTo(xCoordinate, yCoordinate); isPainting = true; } } else { if (isPainting) { - pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, yCoordinate); pdPageContentStream.stroke(); isPainting = false; } } } if (isPainting) { - pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, MARGIN); pdPageContentStream.stroke(); } } boolean isPainting = false; int x = labyrinth.getWidth(); + final float xCoordinate = x * SCALE + MARGIN; for (int y = 0; y < labyrinth.getHeight(); y++) { final Tile currentTile = labyrinth.getTileAt(x - 1, y); + final float yCoordinate = labyrinthHeight - y * SCALE + MARGIN; if (currentTile.hasWallAt(Direction.RIGHT)) { if (!isPainting) { - pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.moveTo(xCoordinate, yCoordinate); isPainting = true; } } else { if (isPainting) { - pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, yCoordinate); pdPageContentStream.stroke(); isPainting = false; } } } if (isPainting) { - pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN); + pdPageContentStream.lineTo(xCoordinate, MARGIN); pdPageContentStream.stroke(); } } @@ -184,11 +195,14 @@ public class PDFRenderer implements Renderer { Position newCurrent = this.findNextSolutionPosition(labyrinth, previousPosition, currentPosition); previousPosition = currentPosition; 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)); pdPageContentStream.stroke(); } + @NonNull private Position findNextSolutionPosition(@NonNull final Labyrinth labyrinth, @Nullable final Position previousPosition, @NonNull final Position currentPosition) { final Tile currentTile = labyrinth.getTileAt(currentPosition); for (final Direction direction : Direction.values()) { @@ -202,7 +216,6 @@ public class PDFRenderer implements Renderer { } } } - // We *SHOULD* never get here. ... famous last words ... - return null; + throw new IllegalStateException("We *SHOULD* never have gotten here. ... famous last words ..."); } } diff --git a/src/main/java/ch/fritteli/labyrinth/Tile.java b/src/main/java/ch/fritteli/labyrinth/Tile.java index 4db471b..5516b86 100644 --- a/src/main/java/ch/fritteli/labyrinth/Tile.java +++ b/src/main/java/ch/fritteli/labyrinth/Tile.java @@ -7,6 +7,8 @@ import lombok.Getter; import lombok.NonNull; import lombok.experimental.FieldDefaults; +import java.util.Random; + @FieldDefaults(level = AccessLevel.PRIVATE) public class Tile { final Walls walls = new Walls(); @@ -42,8 +44,13 @@ public class Tile { this.walls.set(direction); } - public Option getRandomAvailableDirection() { - return Stream.ofAll(this.walls.getUnhardenedSet()).shuffle().headOption(); + public Option getRandomAvailableDirection(@NonNull final Random random) { + final Stream 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) { diff --git a/src/main/java/ch/fritteli/labyrinth/Walls.java b/src/main/java/ch/fritteli/labyrinth/Walls.java index fdf7c23..15705c4 100644 --- a/src/main/java/ch/fritteli/labyrinth/Walls.java +++ b/src/main/java/ch/fritteli/labyrinth/Walls.java @@ -30,7 +30,7 @@ public class Walls { return this.directions.contains(direction); } - public Iterable getUnhardenedSet() { + public Stream getUnhardenedSet() { return Stream.ofAll(this.directions) .removeAll(this.hardened); }