From afd05f6871339f2577078dc8f7c294679f2bf8e1 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Sun, 4 Oct 2020 23:03:05 +0200 Subject: [PATCH] Refactor package structure. --- .../java/ch/fritteli/labyrinth/Labyrinth.java | 22 +- src/main/java/ch/fritteli/labyrinth/Main.java | 5 + .../ch/fritteli/labyrinth/PDFRenderer.java | 301 -------------- .../ch/fritteli/labyrinth/TextRenderer.java | 381 ------------------ .../labyrinth/{ => renderer}/Renderer.java | 3 +- .../labyrinth/renderer/html/Generator.java | 53 +++ .../{ => renderer/html}/HTMLRenderer.java | 49 +-- .../htmlfile}/HTMLFileRenderer.java | 5 +- .../labyrinth/renderer/pdf/Generator.java | 284 +++++++++++++ .../labyrinth/renderer/pdf/PDFRenderer.java | 28 ++ .../pdffile}/PDFFileRenderer.java | 5 +- .../renderer/text/CharDefinition.java | 152 +++++++ .../labyrinth/renderer/text/Generator.java | 196 +++++++++ .../labyrinth/renderer/text/TextRenderer.java | 38 ++ .../textfile}/TextFileRenderer.java | 5 +- .../fritteli/labyrinth/TextRendererTest.java | 52 --- .../renderer/text/CharDefinitionTest.java | 47 +++ 17 files changed, 840 insertions(+), 786 deletions(-) delete mode 100644 src/main/java/ch/fritteli/labyrinth/PDFRenderer.java delete mode 100644 src/main/java/ch/fritteli/labyrinth/TextRenderer.java rename src/main/java/ch/fritteli/labyrinth/{ => renderer}/Renderer.java (60%) create mode 100644 src/main/java/ch/fritteli/labyrinth/renderer/html/Generator.java rename src/main/java/ch/fritteli/labyrinth/{ => renderer/html}/HTMLRenderer.java (58%) rename src/main/java/ch/fritteli/labyrinth/{ => renderer/htmlfile}/HTMLFileRenderer.java (89%) create mode 100644 src/main/java/ch/fritteli/labyrinth/renderer/pdf/Generator.java create mode 100644 src/main/java/ch/fritteli/labyrinth/renderer/pdf/PDFRenderer.java rename src/main/java/ch/fritteli/labyrinth/{ => renderer/pdffile}/PDFFileRenderer.java (89%) create mode 100644 src/main/java/ch/fritteli/labyrinth/renderer/text/CharDefinition.java create mode 100644 src/main/java/ch/fritteli/labyrinth/renderer/text/Generator.java create mode 100644 src/main/java/ch/fritteli/labyrinth/renderer/text/TextRenderer.java rename src/main/java/ch/fritteli/labyrinth/{ => renderer/textfile}/TextFileRenderer.java (94%) delete mode 100644 src/test/java/ch/fritteli/labyrinth/TextRendererTest.java create mode 100644 src/test/java/ch/fritteli/labyrinth/renderer/text/CharDefinitionTest.java diff --git a/src/main/java/ch/fritteli/labyrinth/Labyrinth.java b/src/main/java/ch/fritteli/labyrinth/Labyrinth.java index 02838ee..c3c780c 100644 --- a/src/main/java/ch/fritteli/labyrinth/Labyrinth.java +++ b/src/main/java/ch/fritteli/labyrinth/Labyrinth.java @@ -3,6 +3,7 @@ package ch.fritteli.labyrinth; import io.vavr.control.Option; import lombok.Getter; import lombok.NonNull; +import org.jetbrains.annotations.Nullable; import java.util.Deque; import java.util.LinkedList; @@ -38,18 +39,35 @@ public class Labyrinth { this.generate(); } - Tile getTileAt(@NonNull final Position position) { + @NonNull + public Tile getTileAt(@NonNull final Position position) { return this.getTileAt(position.getX(), position.getY()); } - Tile getTileAt(final int x, final int y) { + @NonNull + public Tile getTileAtOrNull(@NonNull final Position position) { + return this.getTileAtOrNull(position.getX(), position.getY()); + } + + @NonNull + public Tile getTileAt(final int x, final int y) { return this.field[x][y]; } + @Nullable + public Tile getTileAtOrNull(final int x, final int y) { + if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + return null; + } + return this.getTileAt(x, y); + } + + @NonNull Tile getStartTile() { return this.getTileAt(this.start); } + @NonNull Tile getEndTile() { return this.getTileAt(this.end); } diff --git a/src/main/java/ch/fritteli/labyrinth/Main.java b/src/main/java/ch/fritteli/labyrinth/Main.java index cf8f2f1..0a34a46 100644 --- a/src/main/java/ch/fritteli/labyrinth/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/Main.java @@ -1,5 +1,10 @@ package ch.fritteli.labyrinth; +import ch.fritteli.labyrinth.renderer.html.HTMLRenderer; +import ch.fritteli.labyrinth.renderer.htmlfile.HTMLFileRenderer; +import ch.fritteli.labyrinth.renderer.pdffile.PDFFileRenderer; +import ch.fritteli.labyrinth.renderer.textfile.TextFileRenderer; +import ch.fritteli.labyrinth.renderer.text.TextRenderer; import lombok.NonNull; import java.nio.file.Path; diff --git a/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java b/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java deleted file mode 100644 index e0474a0..0000000 --- a/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java +++ /dev/null @@ -1,301 +0,0 @@ -package ch.fritteli.labyrinth; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.Value; -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; -import org.jetbrains.annotations.Nullable; - -import java.awt.*; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -public class PDFRenderer implements Renderer { - private static final float MARGIN = 10; - private static final float SCALE = 10; - - private PDFRenderer() { - - } - - @NonNull - public static PDFRenderer newInstance() { - return new PDFRenderer(); - } - - @Override - @NonNull - public byte[] render(@NonNull final Labyrinth labyrinth) { - final Generator generator = new Generator(labyrinth); - return generator.generate(); - } - - - @RequiredArgsConstructor - private static class Generator { - @NonNull - private final Labyrinth labyrinth; - - private static boolean isValid(@NonNull final Position position) { - return position.getX() >= 0 && position.getY() >= 0; - } - - public byte[] generate() { - final float pageWidth = this.labyrinth.getWidth() * SCALE + 2 * MARGIN; - final float pageHeight = this.labyrinth.getHeight() * SCALE + 2 * MARGIN; - - final PDDocument pdDocument = new PDDocument(); - final PDDocumentInformation info = new PDDocumentInformation(); - info.setTitle("Labyrinth " + this.labyrinth.getWidth() + "x" + this.labyrinth.getHeight() + ", ID " + this.labyrinth.getRandomSeed()); - pdDocument.setDocumentInformation(info); - final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight)); - final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight)); - pdDocument.addPage(page); - pdDocument.addPage(solution); - try (final PDPageContentStream labyrinthPageContentStream = new PDPageContentStream(pdDocument, page); - final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solution)) { - setUpPageContentStream(labyrinthPageContentStream); - setUpPageContentStream(solutionPageContentStream); - this.drawHorizonzalLines(labyrinthPageContentStream, solutionPageContentStream); - this.drawVerticalLines(labyrinthPageContentStream, solutionPageContentStream); - this.drawSolution(solutionPageContentStream); - } catch (IOException e) { - e.printStackTrace(); - } - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - try { - pdDocument.save(output); - pdDocument.close(); - } catch (IOException e) { - e.printStackTrace(); - } - 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 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. - Coordinate coordinate = new Coordinate(0f, 0f); - for (int y = 0; y < this.labyrinth.getHeight(); y++) { - boolean isPainting = false; - coordinate = coordinate.withY(y); - for (int x = 0; x < this.labyrinth.getWidth(); x++) { - final Tile currentTile = this.labyrinth.getTileAt(x, y); - coordinate = coordinate.withX(x); - if (currentTile.hasWallAt(Direction.TOP)) { - if (!isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.moveTo(coordinate.getX(), coordinate.getY()); - } - isPainting = true; - } - } else { - if (isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - isPainting = false; - } - } - } - if (isPainting) { - coordinate = coordinate.withX(this.labyrinth.getWidth()); - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - } - } - boolean isPainting = false; - int y = this.labyrinth.getHeight(); - coordinate = coordinate.withY(this.labyrinth.getHeight()); - for (int x = 0; x < this.labyrinth.getWidth(); x++) { - final Tile currentTile = this.labyrinth.getTileAt(x, y - 1); - coordinate = coordinate.withX(x); - if (currentTile.hasWallAt(Direction.BOTTOM)) { - if (!isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.moveTo(coordinate.getX(), coordinate.getY()); - } - isPainting = true; - } - } else { - if (isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - isPainting = false; - } - } - } - if (isPainting) { - coordinate = coordinate.withX(this.labyrinth.getWidth()); - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - } - } - - 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. - Coordinate coordinate = new Coordinate(0f, 0f); - for (int x = 0; x < this.labyrinth.getWidth(); x++) { - boolean isPainting = false; - coordinate = coordinate.withX(x); - for (int y = 0; y < this.labyrinth.getHeight(); y++) { - final Tile currentTile = this.labyrinth.getTileAt(x, y); - coordinate = coordinate.withY(y); - if (currentTile.hasWallAt(Direction.LEFT)) { - if (!isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.moveTo(coordinate.getX(), coordinate.getY()); - } - isPainting = true; - } - } else { - if (isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - isPainting = false; - } - } - } - if (isPainting) { - coordinate = coordinate.withY(this.labyrinth.getHeight()); - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - } - } - boolean isPainting = false; - int x = this.labyrinth.getWidth(); - coordinate = coordinate.withX(this.labyrinth.getWidth()); - for (int y = 0; y < this.labyrinth.getHeight(); y++) { - final Tile currentTile = this.labyrinth.getTileAt(x - 1, y); - coordinate = coordinate.withY(y); - if (currentTile.hasWallAt(Direction.RIGHT)) { - if (!isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.moveTo(coordinate.getX(), coordinate.getY()); - } - isPainting = true; - } - } else { - if (isPainting) { - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - isPainting = false; - } - } - } - if (isPainting) { - coordinate = coordinate.withY(this.labyrinth.getHeight()); - for (final PDPageContentStream contentStream : contentStreams) { - contentStream.lineTo(coordinate.getX(), coordinate.getY()); - contentStream.stroke(); - } - } - } - - private void drawSolution(@NonNull final PDPageContentStream pageContentStream) throws IOException { - // Draw the solution in red - pageContentStream.setStrokingColor(Color.RED); - // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required. - final Position end = this.labyrinth.getEnd(); - Position currentPosition = this.labyrinth.getStart(); - Position previousPosition = null; - SolutionCoordinate coordinate = new SolutionCoordinate(currentPosition.getX(), currentPosition.getY()); - pageContentStream.moveTo(coordinate.getX(), coordinate.getY()); - do { - Position newCurrent = this.findNextSolutionPosition(previousPosition, currentPosition); - previousPosition = currentPosition; - currentPosition = newCurrent; - coordinate = new SolutionCoordinate(currentPosition.getX(), currentPosition.getY()); - pageContentStream.lineTo(coordinate.getX(), coordinate.getY()); - } while (!currentPosition.equals(end)); - pageContentStream.stroke(); - } - - @NonNull - private Position findNextSolutionPosition(@Nullable final Position previousPosition, @NonNull final Position currentPosition) { - final Tile currentTile = this.labyrinth.getTileAt(currentPosition); - for (final Direction direction : Direction.values()) { - if (!currentTile.hasWallAt(direction)) { - final Position position = direction.translate(currentPosition); - if (position.equals(previousPosition) || !isValid(position)) { - continue; - } - if (this.labyrinth.getTileAt(position).isSolution()) { - return position; - } - } - } - throw new IllegalStateException("We *SHOULD* never have gotten here. ... famous last words ..."); - } - - @Value - private class Coordinate { - float x; - float y; - - public Coordinate(final float x, final float y) { - this.x = x; - this.y = y; - } - - private float calcX(final int x) { - return x * SCALE + MARGIN; - } - - private float calcY(final int y) { - return (Generator.this.labyrinth.getHeight() - y) * SCALE + MARGIN; - } - - public Coordinate withX(final int x) { - return new Coordinate(calcX(x), this.y); - } - - public Coordinate withY(final int y) { - return new Coordinate(this.x, calcY(y)); - } - } - - @Value - private class SolutionCoordinate { - float x; - float y; - - public SolutionCoordinate(final int x, final int y) { - this.x = calcX(x); - this.y = calcY(y); - } - - private float calcX(final int x) { - return x * SCALE + SCALE / 2 + MARGIN; - } - - private float calcY(final int y) { - return (Generator.this.labyrinth.getHeight() - y) * SCALE - SCALE / 2 + MARGIN; - } - } - } -} diff --git a/src/main/java/ch/fritteli/labyrinth/TextRenderer.java b/src/main/java/ch/fritteli/labyrinth/TextRenderer.java deleted file mode 100644 index 1cfb405..0000000 --- a/src/main/java/ch/fritteli/labyrinth/TextRenderer.java +++ /dev/null @@ -1,381 +0,0 @@ -package ch.fritteli.labyrinth; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.experimental.FieldDefaults; -import org.jetbrains.annotations.Nullable; - -public class TextRenderer implements Renderer { - private boolean renderSolution; - - private TextRenderer() { - this.renderSolution = false; - } - - @NonNull - public static TextRenderer newInstance() { - return new TextRenderer(); - } - - @NonNull - public TextRenderer setRenderSolution(final boolean renderSolution) { - this.renderSolution = renderSolution; - return this; - } - - @NonNull - public String render(@NonNull final Labyrinth labyrinth) { - if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { - return ""; - } - final Generator generator = new Generator(labyrinth, this.renderSolution); - final StringBuilder sb = new StringBuilder(); - while (generator.hasNext()) { - sb.append(generator.next()); - } - return sb.toString(); - } - - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) - static class Generator { - @NonNull - private final Labyrinth labyrinth; - private final boolean renderSolution; - private int x = 0; - private int y = 0; - private int line = 0; - - private boolean hasNext() { - return this.y < this.labyrinth.getHeight(); - } - - private String next() { - final Tile currentTile = this.labyrinth.getTileAt(this.x, this.y); - final Tile topTile = this.getTileOrNull(this.x, this.y - 1); - final Tile rightTile = this.getTileOrNull(this.x + 1, this.y); - final Tile bottomTile = this.getTileOrNull(this.x, this.y + 1); - final Tile leftTile = this.getTileOrNull(this.x - 1, this.y); - final String s; - switch (this.line) { - case 0: - s = this.renderTopLine(currentTile, leftTile, topTile); - break; - case 1: - s = this.renderCenterLine(currentTile, topTile, rightTile, bottomTile, leftTile); - break; - case 2: - s = this.renderBottomLine(currentTile, leftTile); - break; - default: - s = ""; - break; - } - this.prepareNextStep(); - return s; - } - - private void prepareNextStep() { - // do some magic ... - this.x++; - if (this.x == this.labyrinth.getWidth()) { - // Reached the end of the row? - // On to the next line then! - this.x = 0; - this.line++; - } - if (this.line == 2 && this.y < this.labyrinth.getHeight() - 1) { - // Finished rendering the center line, and more rows available? - // On to the next row then! - this.line = 0; - this.y++; - } - if (this.line == 3) { - // Finished rendering the bottom line (of the last row)? - // Increment row counter one more time => this is the exit condition. - this.line = 0; - this.y++; - } - } - - private Tile getTileOrNull(final int x, final int y) { - if (x < 0 || y < 0 || x >= this.labyrinth.getWidth() || y >= this.labyrinth.getHeight()) { - return null; - } - return this.labyrinth.getTileAt(x, y); - } - - private String renderTopLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile, @Nullable final Tile topTile) { - final CharDefinition charDef1 = new CharDefinition(); - final CharDefinition charDef2 = new CharDefinition(); - final CharDefinition charDef3 = new CharDefinition(); - String result; - if (currentTile.hasWallAt(Direction.LEFT)) { - charDef1.down(); - } - if (currentTile.hasWallAt(Direction.TOP)) { - charDef1.right(); - charDef2.horizontal(); - charDef3.left(); - } else { - if (this.isSolution(currentTile) && (this.isSolution(topTile) || topTile == null)) { - charDef2.solution().vertical(); - } - } - if (currentTile.hasWallAt(Direction.RIGHT)) { - charDef3.down(); - } - if (this.hasWallAt(leftTile, Direction.TOP)) { - charDef1.left(); - } - if (this.hasWallAt(topTile, Direction.LEFT)) { - charDef1.up(); - } - if (this.hasWallAt(topTile, Direction.RIGHT)) { - charDef3.up(); - } - result = charDef1.toString() + charDef2; - if (this.x == this.labyrinth.getWidth() - 1) { - result += charDef3 + "\n"; - } - return result; - } - - private String renderCenterLine(@NonNull final Tile currentTile, - @Nullable final Tile topTile, - @Nullable final Tile rightTile, - @Nullable final Tile bottomTile, - @Nullable final Tile leftTile) { - final CharDefinition charDef1 = new CharDefinition(); - final CharDefinition charDef2 = new CharDefinition(); - final CharDefinition charDef3 = new CharDefinition(); - String result; - if (currentTile.hasWallAt(Direction.LEFT)) { - charDef1.vertical(); - } else { - if (this.isSolution(currentTile) && this.isSolution(leftTile)) { - charDef1.solution().horizontal(); - } - } - if (this.isSolution(currentTile)) { - charDef2.solution(); - if (!currentTile.hasWallAt(Direction.LEFT) && (this.isSolution(leftTile) || leftTile == null)) { - charDef2.left(); - } - if (!currentTile.hasWallAt(Direction.TOP) && (this.isSolution(topTile) || topTile == null)) { - charDef2.up(); - } - if (!currentTile.hasWallAt(Direction.RIGHT) && (this.isSolution(rightTile) || rightTile == null)) { - charDef2.right(); - } - if (!currentTile.hasWallAt(Direction.BOTTOM) && (this.isSolution(bottomTile) || bottomTile == null)) { - charDef2.down(); - } - } - if (currentTile.hasWallAt(Direction.RIGHT)) { - charDef3.vertical(); - } else { - if (this.isSolution(currentTile) && this.isSolution(rightTile)) { - charDef3.solution().horizontal(); - } - } - - result = charDef1.toString() + charDef2; - - if (this.x == this.labyrinth.getWidth() - 1) { - result += charDef3 + "\n"; - } - - return result; - } - - private String renderBottomLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile) { - String result; - final CharDefinition charDef1 = new CharDefinition(); - final CharDefinition charDef2 = new CharDefinition(); - final CharDefinition charDef3 = new CharDefinition(); - - if (currentTile.hasWallAt(Direction.LEFT)) { - charDef1.up(); - } - if (currentTile.hasWallAt(Direction.BOTTOM)) { - charDef1.right(); - charDef2.horizontal(); - charDef3.left(); - } else { - if (this.isSolution(currentTile)) { - charDef2.solution().vertical(); - } - } - if (currentTile.hasWallAt(Direction.RIGHT)) { - charDef3.up(); - } - if (this.hasWallAt(leftTile, Direction.BOTTOM)) { - charDef1.left(); - } - - result = charDef1.toString() + charDef2; - - if (this.x == this.labyrinth.getWidth() - 1) { - result += charDef3; - } - return result; - } - - private boolean hasWallAt(@Nullable final Tile tile, @NonNull final Direction direction) { - return tile != null && tile.hasWallAt(direction); - } - - private boolean isSolution(@Nullable final Tile tile) { - return this.renderSolution && tile != null && tile.isSolution(); - } - - @FieldDefaults(level = AccessLevel.PRIVATE) - @NoArgsConstructor - @AllArgsConstructor - static class CharDefinition { - // ─ - static final String HORIZONTAL = "\u2500"; - // │ - static final String VERTICAL = "\u2502"; - // ╴ - static final String LEFT = "\u2574"; - // ╵ - static final String UP = "\u2575"; - // ╶ - static final String RIGHT = "\u2576"; - // ╷ - static final String DOWN = "\u2577"; - // ┌ - static final String DOWN_RIGHT = "\u250c"; - // ┐ - static final String DOWN_LEFT = "\u2510"; - // └ - static final String UP_RIGHT = "\u2514"; - // ┘ - static final String UP_LEFT = "\u2518"; - // ├ - static final String VERTICAL_RIGHT = "\u251c"; - // ┤ - static final String VERTICAL_LEFT = "\u2524"; - // ┬ - static final String HORIZONTAL_DOWN = "\u252c"; - // ┴ - static final String HORIZONTAL_UP = "\u2534"; - // ┼ - static final String CROSS = "\u253c"; - // ╭ - static final String SOLUTION_DOWN_RIGHT = "\u256d"; - // ╮ - static final String SOLUTION_DOWN_LEFT = "\u256e"; - // ╯ - static final String SOLUTION_UP_LEFT = "\u256f"; - // ╰ - static final String SOLUTION_UP_RIGHT = "\u2570"; - boolean up = false; - boolean down = false; - boolean left = false; - boolean right = false; - boolean solution = false; - - CharDefinition solution() { - this.solution = true; - return this; - } - - CharDefinition up() { - this.up = true; - return this; - } - - CharDefinition down() { - this.down = true; - return this; - } - - CharDefinition vertical() { - return this.up().down(); - } - - CharDefinition left() { - this.left = true; - return this; - } - - CharDefinition right() { - this.right = true; - return this; - } - - CharDefinition horizontal() { - return this.left().right(); - } - - public String toString() { - if (this.up) { - if (this.down) { - if (this.left) { - if (this.right) { - return CROSS; - } else { - return VERTICAL_LEFT; - } - } else { - if (this.right) { - return VERTICAL_RIGHT; - } else { - return VERTICAL; - } - } - } else { - if (this.left) { - if (this.right) { - return HORIZONTAL_UP; - } else { - return this.solution ? SOLUTION_UP_LEFT : UP_LEFT; - } - } else { - if (this.right) { - return this.solution ? SOLUTION_UP_RIGHT : UP_RIGHT; - } else { - return UP; - } - } - } - } else { - if (this.down) { - if (this.left) { - if (this.right) { - return HORIZONTAL_DOWN; - } else { - return this.solution ? SOLUTION_DOWN_LEFT : DOWN_LEFT; - } - } else { - if (this.right) { - return this.solution ? SOLUTION_DOWN_RIGHT : DOWN_RIGHT; - } else { - return DOWN; - } - } - } else { - if (this.left) { - if (this.right) { - return HORIZONTAL; - } else { - return LEFT; - } - } else { - if (this.right) { - return RIGHT; - } else { - return " "; - } - } - } - } - } - } - } -} diff --git a/src/main/java/ch/fritteli/labyrinth/Renderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/Renderer.java similarity index 60% rename from src/main/java/ch/fritteli/labyrinth/Renderer.java rename to src/main/java/ch/fritteli/labyrinth/renderer/Renderer.java index 52a4798..865522b 100644 --- a/src/main/java/ch/fritteli/labyrinth/Renderer.java +++ b/src/main/java/ch/fritteli/labyrinth/renderer/Renderer.java @@ -1,5 +1,6 @@ -package ch.fritteli.labyrinth; +package ch.fritteli.labyrinth.renderer; +import ch.fritteli.labyrinth.Labyrinth; import lombok.NonNull; public interface Renderer { diff --git a/src/main/java/ch/fritteli/labyrinth/renderer/html/Generator.java b/src/main/java/ch/fritteli/labyrinth/renderer/html/Generator.java new file mode 100644 index 0000000..fb21d59 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/renderer/html/Generator.java @@ -0,0 +1,53 @@ +package ch.fritteli.labyrinth.renderer.html; + +import ch.fritteli.labyrinth.Direction; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.Tile; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +class Generator { + private final Labyrinth labyrinth; + private int y = 0; + + boolean hasNext() { + return this.y < this.labyrinth.getHeight(); + } + + String next() { + StringBuilder sb = new StringBuilder(""); + for (int x = 0; x < this.labyrinth.getWidth(); x++) { + final Tile currentTile = this.labyrinth.getTileAt(x, this.y); + sb.append(" "); + } + sb.append(""); + this.y++; + return sb.toString(); + } + + private Set getClasses(@NonNull final Tile tile) { + Set result = HashSet.empty(); + if (tile.hasWallAt(Direction.TOP)) { + result = result.add("top"); + } + if (tile.hasWallAt(Direction.RIGHT)) { + result = result.add("right"); + } + if (tile.hasWallAt(Direction.BOTTOM)) { + result = result.add("bottom"); + } + if (tile.hasWallAt(Direction.LEFT)) { + result = result.add("left"); + } + if (tile.isSolution()) { + result = result.add("solution"); + } + return result; + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/html/HTMLRenderer.java similarity index 58% rename from src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java rename to src/main/java/ch/fritteli/labyrinth/renderer/html/HTMLRenderer.java index fa3e072..173a4f1 100644 --- a/src/main/java/ch/fritteli/labyrinth/HTMLRenderer.java +++ b/src/main/java/ch/fritteli/labyrinth/renderer/html/HTMLRenderer.java @@ -1,9 +1,8 @@ -package ch.fritteli.labyrinth; +package ch.fritteli.labyrinth.renderer.html; -import io.vavr.collection.HashSet; -import io.vavr.collection.Set; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.renderer.Renderer; import lombok.NonNull; -import lombok.RequiredArgsConstructor; public class HTMLRenderer implements Renderer { private static final String POSTAMBLE = ""; @@ -62,46 +61,4 @@ public class HTMLRenderer implements Renderer { "show solution"; } - @RequiredArgsConstructor - private static class Generator { - private final Labyrinth labyrinth; - private int y = 0; - - private boolean hasNext() { - return this.y < this.labyrinth.getHeight(); - } - - private String next() { - StringBuilder sb = new StringBuilder(""); - for (int x = 0; x < this.labyrinth.getWidth(); x++) { - final Tile currentTile = this.labyrinth.getTileAt(x, this.y); - sb.append(" "); - } - sb.append(""); - this.y++; - return sb.toString(); - } - - private Set getClasses(@NonNull final Tile tile) { - Set result = HashSet.empty(); - if (tile.hasWallAt(Direction.TOP)) { - result = result.add("top"); - } - if (tile.hasWallAt(Direction.RIGHT)) { - result = result.add("right"); - } - if (tile.hasWallAt(Direction.BOTTOM)) { - result = result.add("bottom"); - } - if (tile.hasWallAt(Direction.LEFT)) { - result = result.add("left"); - } - if (tile.isSolution()) { - result = result.add("solution"); - } - return result; - } - } } diff --git a/src/main/java/ch/fritteli/labyrinth/HTMLFileRenderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/htmlfile/HTMLFileRenderer.java similarity index 89% rename from src/main/java/ch/fritteli/labyrinth/HTMLFileRenderer.java rename to src/main/java/ch/fritteli/labyrinth/renderer/htmlfile/HTMLFileRenderer.java index 04d7d79..c7b2629 100644 --- a/src/main/java/ch/fritteli/labyrinth/HTMLFileRenderer.java +++ b/src/main/java/ch/fritteli/labyrinth/renderer/htmlfile/HTMLFileRenderer.java @@ -1,5 +1,8 @@ -package ch.fritteli.labyrinth; +package ch.fritteli.labyrinth.renderer.htmlfile; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.renderer.Renderer; +import ch.fritteli.labyrinth.renderer.html.HTMLRenderer; import lombok.NonNull; import java.io.IOException; diff --git a/src/main/java/ch/fritteli/labyrinth/renderer/pdf/Generator.java b/src/main/java/ch/fritteli/labyrinth/renderer/pdf/Generator.java new file mode 100644 index 0000000..62b0922 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/renderer/pdf/Generator.java @@ -0,0 +1,284 @@ +package ch.fritteli.labyrinth.renderer.pdf; + +import ch.fritteli.labyrinth.Direction; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.Position; +import ch.fritteli.labyrinth.Tile; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; +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; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@RequiredArgsConstructor +class Generator { + @NonNull + private final Labyrinth labyrinth; + + private static boolean isValid(@NonNull final Position position) { + return position.getX() >= 0 && position.getY() >= 0; + } + + public byte[] generate() { + final float pageWidth = this.labyrinth.getWidth() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN; + final float pageHeight = this.labyrinth.getHeight() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN; + + final PDDocument pdDocument = new PDDocument(); + final PDDocumentInformation info = new PDDocumentInformation(); + info.setTitle("Labyrinth " + this.labyrinth.getWidth() + "x" + this.labyrinth.getHeight() + ", ID " + this.labyrinth.getRandomSeed()); + pdDocument.setDocumentInformation(info); + final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight)); + final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight)); + pdDocument.addPage(page); + pdDocument.addPage(solution); + try (final PDPageContentStream labyrinthPageContentStream = new PDPageContentStream(pdDocument, page); + final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solution)) { + setUpPageContentStream(labyrinthPageContentStream); + setUpPageContentStream(solutionPageContentStream); + this.drawHorizonzalLines(labyrinthPageContentStream, solutionPageContentStream); + this.drawVerticalLines(labyrinthPageContentStream, solutionPageContentStream); + this.drawSolution(solutionPageContentStream); + } catch (IOException e) { + e.printStackTrace(); + } + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + pdDocument.save(output); + pdDocument.close(); + } catch (IOException e) { + e.printStackTrace(); + } + 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 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. + Coordinate coordinate = new Coordinate(0f, 0f); + for (int y = 0; y < this.labyrinth.getHeight(); y++) { + boolean isPainting = false; + coordinate = coordinate.withY(y); + for (int x = 0; x < this.labyrinth.getWidth(); x++) { + final Tile currentTile = this.labyrinth.getTileAt(x, y); + coordinate = coordinate.withX(x); + if (currentTile.hasWallAt(Direction.TOP)) { + if (!isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.moveTo(coordinate.getX(), coordinate.getY()); + } + isPainting = true; + } + } else { + if (isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + isPainting = false; + } + } + } + if (isPainting) { + coordinate = coordinate.withX(this.labyrinth.getWidth()); + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + } + } + boolean isPainting = false; + int y = this.labyrinth.getHeight(); + coordinate = coordinate.withY(this.labyrinth.getHeight()); + for (int x = 0; x < this.labyrinth.getWidth(); x++) { + final Tile currentTile = this.labyrinth.getTileAt(x, y - 1); + coordinate = coordinate.withX(x); + if (currentTile.hasWallAt(Direction.BOTTOM)) { + if (!isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.moveTo(coordinate.getX(), coordinate.getY()); + } + isPainting = true; + } + } else { + if (isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + isPainting = false; + } + } + } + if (isPainting) { + coordinate = coordinate.withX(this.labyrinth.getWidth()); + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + } + } + + 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. + Coordinate coordinate = new Coordinate(0f, 0f); + for (int x = 0; x < this.labyrinth.getWidth(); x++) { + boolean isPainting = false; + coordinate = coordinate.withX(x); + for (int y = 0; y < this.labyrinth.getHeight(); y++) { + final Tile currentTile = this.labyrinth.getTileAt(x, y); + coordinate = coordinate.withY(y); + if (currentTile.hasWallAt(Direction.LEFT)) { + if (!isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.moveTo(coordinate.getX(), coordinate.getY()); + } + isPainting = true; + } + } else { + if (isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + isPainting = false; + } + } + } + if (isPainting) { + coordinate = coordinate.withY(this.labyrinth.getHeight()); + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + } + } + boolean isPainting = false; + int x = this.labyrinth.getWidth(); + coordinate = coordinate.withX(this.labyrinth.getWidth()); + for (int y = 0; y < this.labyrinth.getHeight(); y++) { + final Tile currentTile = this.labyrinth.getTileAt(x - 1, y); + coordinate = coordinate.withY(y); + if (currentTile.hasWallAt(Direction.RIGHT)) { + if (!isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.moveTo(coordinate.getX(), coordinate.getY()); + } + isPainting = true; + } + } else { + if (isPainting) { + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + isPainting = false; + } + } + } + if (isPainting) { + coordinate = coordinate.withY(this.labyrinth.getHeight()); + for (final PDPageContentStream contentStream : contentStreams) { + contentStream.lineTo(coordinate.getX(), coordinate.getY()); + contentStream.stroke(); + } + } + } + + private void drawSolution(@NonNull final PDPageContentStream pageContentStream) throws IOException { + // Draw the solution in red + pageContentStream.setStrokingColor(Color.RED); + // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required. + final Position end = this.labyrinth.getEnd(); + Position currentPosition = this.labyrinth.getStart(); + Position previousPosition = null; + SolutionCoordinate coordinate = new SolutionCoordinate(currentPosition.getX(), currentPosition.getY()); + pageContentStream.moveTo(coordinate.getX(), coordinate.getY()); + do { + Position newCurrent = this.findNextSolutionPosition(previousPosition, currentPosition); + previousPosition = currentPosition; + currentPosition = newCurrent; + coordinate = new SolutionCoordinate(currentPosition.getX(), currentPosition.getY()); + pageContentStream.lineTo(coordinate.getX(), coordinate.getY()); + } while (!currentPosition.equals(end)); + pageContentStream.stroke(); + } + + @NonNull + private Position findNextSolutionPosition(@Nullable final Position previousPosition, @NonNull final Position currentPosition) { + final Tile currentTile = this.labyrinth.getTileAt(currentPosition); + for (final Direction direction : Direction.values()) { + if (!currentTile.hasWallAt(direction)) { + final Position position = direction.translate(currentPosition); + final Tile tileAtPosition = this.labyrinth.getTileAtOrNull(position); + if (position.equals(previousPosition) || tileAtPosition == null) { + continue; + } + if (tileAtPosition.isSolution()) { + return position; + } + } + } + throw new IllegalStateException("We *SHOULD* never have gotten here. ... famous last words ..."); + } + + @Value + private class Coordinate { + float x; + float y; + + public Coordinate(final float x, final float y) { + this.x = x; + this.y = y; + } + + private float calcX(final int x) { + return x * PDFRenderer.SCALE + PDFRenderer.MARGIN; + } + + private float calcY(final int y) { + return (Generator.this.labyrinth.getHeight() - y) * PDFRenderer.SCALE + PDFRenderer.MARGIN; + } + + public Coordinate withX(final int x) { + return new Coordinate(calcX(x), this.y); + } + + public Coordinate withY(final int y) { + return new Coordinate(this.x, calcY(y)); + } + } + + @Value + private class SolutionCoordinate { + float x; + float y; + + public SolutionCoordinate(final int x, final int y) { + this.x = calcX(x); + this.y = calcY(y); + } + + private float calcX(final int x) { + return x * PDFRenderer.SCALE + PDFRenderer.SCALE / 2 + PDFRenderer.MARGIN; + } + + private float calcY(final int y) { + return (Generator.this.labyrinth.getHeight() - y) * PDFRenderer.SCALE - PDFRenderer.SCALE / 2 + PDFRenderer.MARGIN; + } + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/renderer/pdf/PDFRenderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/pdf/PDFRenderer.java new file mode 100644 index 0000000..84f2e68 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/renderer/pdf/PDFRenderer.java @@ -0,0 +1,28 @@ +package ch.fritteli.labyrinth.renderer.pdf; + +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.renderer.Renderer; +import lombok.NonNull; + +public class PDFRenderer implements Renderer { + static final float MARGIN = 10; + static final float SCALE = 10; + + private PDFRenderer() { + + } + + @NonNull + public static PDFRenderer newInstance() { + return new PDFRenderer(); + } + + @Override + @NonNull + public byte[] render(@NonNull final Labyrinth labyrinth) { + final Generator generator = new Generator(labyrinth); + return generator.generate(); + } + + +} diff --git a/src/main/java/ch/fritteli/labyrinth/PDFFileRenderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/pdffile/PDFFileRenderer.java similarity index 89% rename from src/main/java/ch/fritteli/labyrinth/PDFFileRenderer.java rename to src/main/java/ch/fritteli/labyrinth/renderer/pdffile/PDFFileRenderer.java index 7772f9b..617dcf1 100644 --- a/src/main/java/ch/fritteli/labyrinth/PDFFileRenderer.java +++ b/src/main/java/ch/fritteli/labyrinth/renderer/pdffile/PDFFileRenderer.java @@ -1,5 +1,8 @@ -package ch.fritteli.labyrinth; +package ch.fritteli.labyrinth.renderer.pdffile; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.renderer.pdf.PDFRenderer; +import ch.fritteli.labyrinth.renderer.Renderer; import lombok.NonNull; import java.io.IOException; diff --git a/src/main/java/ch/fritteli/labyrinth/renderer/text/CharDefinition.java b/src/main/java/ch/fritteli/labyrinth/renderer/text/CharDefinition.java new file mode 100644 index 0000000..4f7e0a9 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/renderer/text/CharDefinition.java @@ -0,0 +1,152 @@ +package ch.fritteli.labyrinth.renderer.text; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +class CharDefinition { + // ─ + static final String HORIZONTAL = "\u2500"; + // │ + static final String VERTICAL = "\u2502"; + // ╴ + static final String LEFT = "\u2574"; + // ╵ + static final String UP = "\u2575"; + // ╶ + static final String RIGHT = "\u2576"; + // ╷ + static final String DOWN = "\u2577"; + // ┌ + static final String DOWN_RIGHT = "\u250c"; + // ┐ + static final String DOWN_LEFT = "\u2510"; + // └ + static final String UP_RIGHT = "\u2514"; + // ┘ + static final String UP_LEFT = "\u2518"; + // ├ + static final String VERTICAL_RIGHT = "\u251c"; + // ┤ + static final String VERTICAL_LEFT = "\u2524"; + // ┬ + static final String HORIZONTAL_DOWN = "\u252c"; + // ┴ + static final String HORIZONTAL_UP = "\u2534"; + // ┼ + static final String CROSS = "\u253c"; + // ╭ + static final String SOLUTION_DOWN_RIGHT = "\u256d"; + // ╮ + static final String SOLUTION_DOWN_LEFT = "\u256e"; + // ╯ + static final String SOLUTION_UP_LEFT = "\u256f"; + // ╰ + static final String SOLUTION_UP_RIGHT = "\u2570"; + boolean up = false; + boolean down = false; + boolean left = false; + boolean right = false; + boolean solution = false; + + CharDefinition solution() { + this.solution = true; + return this; + } + + CharDefinition up() { + this.up = true; + return this; + } + + CharDefinition down() { + this.down = true; + return this; + } + + CharDefinition vertical() { + return this.up().down(); + } + + CharDefinition left() { + this.left = true; + return this; + } + + CharDefinition right() { + this.right = true; + return this; + } + + CharDefinition horizontal() { + return this.left().right(); + } + + public String toString() { + if (this.up) { + if (this.down) { + if (this.left) { + if (this.right) { + return CROSS; + } else { + return VERTICAL_LEFT; + } + } else { + if (this.right) { + return VERTICAL_RIGHT; + } else { + return VERTICAL; + } + } + } else { + if (this.left) { + if (this.right) { + return HORIZONTAL_UP; + } else { + return this.solution ? SOLUTION_UP_LEFT : UP_LEFT; + } + } else { + if (this.right) { + return this.solution ? SOLUTION_UP_RIGHT : UP_RIGHT; + } else { + return UP; + } + } + } + } else { + if (this.down) { + if (this.left) { + if (this.right) { + return HORIZONTAL_DOWN; + } else { + return this.solution ? SOLUTION_DOWN_LEFT : DOWN_LEFT; + } + } else { + if (this.right) { + return this.solution ? SOLUTION_DOWN_RIGHT : DOWN_RIGHT; + } else { + return DOWN; + } + } + } else { + if (this.left) { + if (this.right) { + return HORIZONTAL; + } else { + return LEFT; + } + } else { + if (this.right) { + return RIGHT; + } else { + return " "; + } + } + } + } + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/renderer/text/Generator.java b/src/main/java/ch/fritteli/labyrinth/renderer/text/Generator.java new file mode 100644 index 0000000..4294f60 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/renderer/text/Generator.java @@ -0,0 +1,196 @@ +package ch.fritteli.labyrinth.renderer.text; + +import ch.fritteli.labyrinth.Direction; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.Tile; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +class Generator { + @NonNull + private final Labyrinth labyrinth; + private final boolean renderSolution; + private int x = 0; + private int y = 0; + private int line = 0; + + boolean hasNext() { + return this.y < this.labyrinth.getHeight(); + } + + String next() { + final Tile currentTile = this.labyrinth.getTileAt(this.x, this.y); + final Tile topTile = this.labyrinth.getTileAtOrNull(this.x, this.y - 1); + final Tile rightTile = this.labyrinth.getTileAtOrNull(this.x + 1, this.y); + final Tile bottomTile = this.labyrinth.getTileAtOrNull(this.x, this.y + 1); + final Tile leftTile = this.labyrinth.getTileAtOrNull(this.x - 1, this.y); + final String s; + switch (this.line) { + case 0: + s = this.renderTopLine(currentTile, leftTile, topTile); + break; + case 1: + s = this.renderCenterLine(currentTile, topTile, rightTile, bottomTile, leftTile); + break; + case 2: + s = this.renderBottomLine(currentTile, leftTile); + break; + default: + s = ""; + break; + } + this.prepareNextStep(); + return s; + } + + private void prepareNextStep() { + // do some magic ... + this.x++; + if (this.x == this.labyrinth.getWidth()) { + // Reached the end of the row? + // On to the next line then! + this.x = 0; + this.line++; + } + if (this.line == 2 && this.y < this.labyrinth.getHeight() - 1) { + // Finished rendering the center line, and more rows available? + // On to the next row then! + this.line = 0; + this.y++; + } + if (this.line == 3) { + // Finished rendering the bottom line (of the last row)? + // Increment row counter one more time => this is the exit condition. + this.line = 0; + this.y++; + } + } + + private String renderTopLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile, @Nullable final Tile topTile) { + final CharDefinition charDef1 = new CharDefinition(); + final CharDefinition charDef2 = new CharDefinition(); + final CharDefinition charDef3 = new CharDefinition(); + String result; + if (currentTile.hasWallAt(Direction.LEFT)) { + charDef1.down(); + } + if (currentTile.hasWallAt(Direction.TOP)) { + charDef1.right(); + charDef2.horizontal(); + charDef3.left(); + } else { + if (this.isSolution(currentTile) && (this.isSolution(topTile) || topTile == null)) { + charDef2.solution().vertical(); + } + } + if (currentTile.hasWallAt(Direction.RIGHT)) { + charDef3.down(); + } + if (this.hasWallAt(leftTile, Direction.TOP)) { + charDef1.left(); + } + if (this.hasWallAt(topTile, Direction.LEFT)) { + charDef1.up(); + } + if (this.hasWallAt(topTile, Direction.RIGHT)) { + charDef3.up(); + } + result = charDef1.toString() + charDef2; + if (this.x == this.labyrinth.getWidth() - 1) { + result += charDef3 + "\n"; + } + return result; + } + + private String renderCenterLine(@NonNull final Tile currentTile, + @Nullable final Tile topTile, + @Nullable final Tile rightTile, + @Nullable final Tile bottomTile, + @Nullable final Tile leftTile) { + final CharDefinition charDef1 = new CharDefinition(); + final CharDefinition charDef2 = new CharDefinition(); + final CharDefinition charDef3 = new CharDefinition(); + String result; + if (currentTile.hasWallAt(Direction.LEFT)) { + charDef1.vertical(); + } else { + if (this.isSolution(currentTile) && this.isSolution(leftTile)) { + charDef1.solution().horizontal(); + } + } + if (this.isSolution(currentTile)) { + charDef2.solution(); + if (!currentTile.hasWallAt(Direction.LEFT) && (this.isSolution(leftTile) || leftTile == null)) { + charDef2.left(); + } + if (!currentTile.hasWallAt(Direction.TOP) && (this.isSolution(topTile) || topTile == null)) { + charDef2.up(); + } + if (!currentTile.hasWallAt(Direction.RIGHT) && (this.isSolution(rightTile) || rightTile == null)) { + charDef2.right(); + } + if (!currentTile.hasWallAt(Direction.BOTTOM) && (this.isSolution(bottomTile) || bottomTile == null)) { + charDef2.down(); + } + } + if (currentTile.hasWallAt(Direction.RIGHT)) { + charDef3.vertical(); + } else { + if (this.isSolution(currentTile) && this.isSolution(rightTile)) { + charDef3.solution().horizontal(); + } + } + + result = charDef1.toString() + charDef2; + + if (this.x == this.labyrinth.getWidth() - 1) { + result += charDef3 + "\n"; + } + + return result; + } + + private String renderBottomLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile) { + String result; + final CharDefinition charDef1 = new CharDefinition(); + final CharDefinition charDef2 = new CharDefinition(); + final CharDefinition charDef3 = new CharDefinition(); + + if (currentTile.hasWallAt(Direction.LEFT)) { + charDef1.up(); + } + if (currentTile.hasWallAt(Direction.BOTTOM)) { + charDef1.right(); + charDef2.horizontal(); + charDef3.left(); + } else { + if (this.isSolution(currentTile)) { + charDef2.solution().vertical(); + } + } + if (currentTile.hasWallAt(Direction.RIGHT)) { + charDef3.up(); + } + if (this.hasWallAt(leftTile, Direction.BOTTOM)) { + charDef1.left(); + } + + result = charDef1.toString() + charDef2; + + if (this.x == this.labyrinth.getWidth() - 1) { + result += charDef3; + } + return result; + } + + private boolean hasWallAt(@Nullable final Tile tile, @NonNull final Direction direction) { + return tile != null && tile.hasWallAt(direction); + } + + private boolean isSolution(@Nullable final Tile tile) { + return this.renderSolution && tile != null && tile.isSolution(); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/renderer/text/TextRenderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/text/TextRenderer.java new file mode 100644 index 0000000..d184085 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/renderer/text/TextRenderer.java @@ -0,0 +1,38 @@ +package ch.fritteli.labyrinth.renderer.text; + +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.renderer.Renderer; +import lombok.NonNull; + +public class TextRenderer implements Renderer { + private boolean renderSolution; + + private TextRenderer() { + this.renderSolution = false; + } + + @NonNull + public static TextRenderer newInstance() { + return new TextRenderer(); + } + + @NonNull + public TextRenderer setRenderSolution(final boolean renderSolution) { + this.renderSolution = renderSolution; + return this; + } + + @NonNull + public String render(@NonNull final Labyrinth labyrinth) { + if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { + return ""; + } + final Generator generator = new Generator(labyrinth, this.renderSolution); + final StringBuilder sb = new StringBuilder(); + while (generator.hasNext()) { + sb.append(generator.next()); + } + return sb.toString(); + } + +} diff --git a/src/main/java/ch/fritteli/labyrinth/TextFileRenderer.java b/src/main/java/ch/fritteli/labyrinth/renderer/textfile/TextFileRenderer.java similarity index 94% rename from src/main/java/ch/fritteli/labyrinth/TextFileRenderer.java rename to src/main/java/ch/fritteli/labyrinth/renderer/textfile/TextFileRenderer.java index 64191bf..416cd3a 100644 --- a/src/main/java/ch/fritteli/labyrinth/TextFileRenderer.java +++ b/src/main/java/ch/fritteli/labyrinth/renderer/textfile/TextFileRenderer.java @@ -1,5 +1,8 @@ -package ch.fritteli.labyrinth; +package ch.fritteli.labyrinth.renderer.textfile; +import ch.fritteli.labyrinth.Labyrinth; +import ch.fritteli.labyrinth.renderer.Renderer; +import ch.fritteli.labyrinth.renderer.text.TextRenderer; import io.vavr.collection.List; import lombok.NonNull; diff --git a/src/test/java/ch/fritteli/labyrinth/TextRendererTest.java b/src/test/java/ch/fritteli/labyrinth/TextRendererTest.java deleted file mode 100644 index 58b36e4..0000000 --- a/src/test/java/ch/fritteli/labyrinth/TextRendererTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package ch.fritteli.labyrinth; - -import ch.fritteli.labyrinth.TextRenderer.Generator.CharDefinition; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class TextRendererTest { - @Nested - class CharDefinitionTest { - @Test - void testRenderingWall() { - assertEquals(" ", new CharDefinition(false, false, false, false, false).toString()); - assertEquals("╶", new CharDefinition(false, false, false, true, false).toString()); - assertEquals("╴", new CharDefinition(false, false, true, false, false).toString()); - assertEquals("─", new CharDefinition(false, false, true, true, false).toString()); - assertEquals("╷", new CharDefinition(false, true, false, false, false).toString()); - assertEquals("┌", new CharDefinition(false, true, false, true, false).toString()); - assertEquals("┐", new CharDefinition(false, true, true, false, false).toString()); - assertEquals("┬", new CharDefinition(false, true, true, true, false).toString()); - assertEquals("╵", new CharDefinition(true, false, false, false, false).toString()); - assertEquals("└", new CharDefinition(true, false, false, true, false).toString()); - assertEquals("┘", new CharDefinition(true, false, true, false, false).toString()); - assertEquals("┴", new CharDefinition(true, false, true, true, false).toString()); - assertEquals("│", new CharDefinition(true, true, false, false, false).toString()); - assertEquals("├", new CharDefinition(true, true, false, true, false).toString()); - assertEquals("┤", new CharDefinition(true, true, true, false, false).toString()); - assertEquals("┼", new CharDefinition(true, true, true, true, false).toString()); - } - - @Test - void testRenderingSolution() { - assertEquals(" ", new CharDefinition(false, false, false, false, true).toString()); - assertEquals("╶", new CharDefinition(false, false, false, true, true).toString()); - assertEquals("╴", new CharDefinition(false, false, true, false, true).toString()); - assertEquals("─", new CharDefinition(false, false, true, true, true).toString()); - assertEquals("╷", new CharDefinition(false, true, false, false, true).toString()); - assertEquals("╭", new CharDefinition(false, true, false, true, true).toString()); - assertEquals("╮", new CharDefinition(false, true, true, false, true).toString()); - assertEquals("┬", new CharDefinition(false, true, true, true, true).toString()); - assertEquals("╵", new CharDefinition(true, false, false, false, true).toString()); - assertEquals("╰", new CharDefinition(true, false, false, true, true).toString()); - assertEquals("╯", new CharDefinition(true, false, true, false, true).toString()); - assertEquals("┴", new CharDefinition(true, false, true, true, true).toString()); - assertEquals("│", new CharDefinition(true, true, false, false, true).toString()); - assertEquals("├", new CharDefinition(true, true, false, true, true).toString()); - assertEquals("┤", new CharDefinition(true, true, true, false, true).toString()); - assertEquals("┼", new CharDefinition(true, true, true, true, true).toString()); - } - } -} diff --git a/src/test/java/ch/fritteli/labyrinth/renderer/text/CharDefinitionTest.java b/src/test/java/ch/fritteli/labyrinth/renderer/text/CharDefinitionTest.java new file mode 100644 index 0000000..a6b8b45 --- /dev/null +++ b/src/test/java/ch/fritteli/labyrinth/renderer/text/CharDefinitionTest.java @@ -0,0 +1,47 @@ +package ch.fritteli.labyrinth.renderer.text; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CharDefinitionTest { + @Test + void testRenderingWall() { + assertEquals(" ", new CharDefinition(false, false, false, false, false).toString()); + assertEquals("╶", new CharDefinition(false, false, false, true, false).toString()); + assertEquals("╴", new CharDefinition(false, false, true, false, false).toString()); + assertEquals("─", new CharDefinition(false, false, true, true, false).toString()); + assertEquals("╷", new CharDefinition(false, true, false, false, false).toString()); + assertEquals("┌", new CharDefinition(false, true, false, true, false).toString()); + assertEquals("┐", new CharDefinition(false, true, true, false, false).toString()); + assertEquals("┬", new CharDefinition(false, true, true, true, false).toString()); + assertEquals("╵", new CharDefinition(true, false, false, false, false).toString()); + assertEquals("└", new CharDefinition(true, false, false, true, false).toString()); + assertEquals("┘", new CharDefinition(true, false, true, false, false).toString()); + assertEquals("┴", new CharDefinition(true, false, true, true, false).toString()); + assertEquals("│", new CharDefinition(true, true, false, false, false).toString()); + assertEquals("├", new CharDefinition(true, true, false, true, false).toString()); + assertEquals("┤", new CharDefinition(true, true, true, false, false).toString()); + assertEquals("┼", new CharDefinition(true, true, true, true, false).toString()); + } + + @Test + void testRenderingSolution() { + assertEquals(" ", new CharDefinition(false, false, false, false, true).toString()); + assertEquals("╶", new CharDefinition(false, false, false, true, true).toString()); + assertEquals("╴", new CharDefinition(false, false, true, false, true).toString()); + assertEquals("─", new CharDefinition(false, false, true, true, true).toString()); + assertEquals("╷", new CharDefinition(false, true, false, false, true).toString()); + assertEquals("╭", new CharDefinition(false, true, false, true, true).toString()); + assertEquals("╮", new CharDefinition(false, true, true, false, true).toString()); + assertEquals("┬", new CharDefinition(false, true, true, true, true).toString()); + assertEquals("╵", new CharDefinition(true, false, false, false, true).toString()); + assertEquals("╰", new CharDefinition(true, false, false, true, true).toString()); + assertEquals("╯", new CharDefinition(true, false, true, false, true).toString()); + assertEquals("┴", new CharDefinition(true, false, true, true, true).toString()); + assertEquals("│", new CharDefinition(true, true, false, false, true).toString()); + assertEquals("├", new CharDefinition(true, true, false, true, true).toString()); + assertEquals("┤", new CharDefinition(true, true, true, false, true).toString()); + assertEquals("┼", new CharDefinition(true, true, true, true, true).toString()); + } +}