diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cd0f37d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+.idea/
+### Maven template
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..8afd225
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+
+ ch.fritteli
+ fritteli-build-parent
+ 2.0.3
+
+ org.example
+ labyrinth
+ 0.0.1-SNAPSHOT
+
+
+ org.projectlombok
+ lombok
+
+
+ org.jetbrains
+ annotations
+
+
+ io.vavr
+ vavr
+
+
+
diff --git a/src/main/java/labyrinth/Direction.java b/src/main/java/labyrinth/Direction.java
new file mode 100644
index 0000000..0ab2434
--- /dev/null
+++ b/src/main/java/labyrinth/Direction.java
@@ -0,0 +1,57 @@
+package labyrinth;
+
+import io.vavr.collection.Stream;
+import io.vavr.control.Option;
+import lombok.NonNull;
+
+import java.util.EnumSet;
+import java.util.Random;
+import java.util.function.UnaryOperator;
+
+public enum Direction {
+ TOP(position -> position.withY(position.getY() - 1)),
+ BOTTOM(position -> position.withY(position.getY() + 1)),
+ LEFT(position -> position.withX(position.getX() - 1)),
+ RIGHT(position -> position.withX(position.getX() + 1));
+ private static final Random random = new Random();
+ @NonNull
+ private final UnaryOperator translation;
+
+ Direction(@NonNull final UnaryOperator translation) {
+ this.translation = translation;
+ }
+
+ public static Direction getRandom() {
+ return values()[random.nextInt(4)];
+ }
+
+ public static Option getRandomExcluding(@NonNull final EnumSet directions) {
+ final EnumSet allowedDirections = EnumSet.complementOf(directions);
+ return Stream.ofAll(allowedDirections).shuffle().headOption();
+ }
+
+ public static Option getRandomExcluding(@NonNull final Direction... directions) {
+ return Stream.of(values())
+ .removeAll(Stream.of(directions))
+ .shuffle()
+ .headOption();
+ }
+
+ public Direction getOpposite() {
+ switch (this) {
+ case TOP:
+ return BOTTOM;
+ case LEFT:
+ return RIGHT;
+ case RIGHT:
+ return LEFT;
+ case BOTTOM:
+ return TOP;
+ }
+ throw new IllegalStateException("Programming error: Not all enum values covered in enum Direction#getOpposite()!");
+ }
+
+ public Position translate(@NonNull final Position position) {
+ return this.translation.apply(position);
+ }
+}
diff --git a/src/main/java/labyrinth/Directions.java b/src/main/java/labyrinth/Directions.java
new file mode 100644
index 0000000..12e2ea6
--- /dev/null
+++ b/src/main/java/labyrinth/Directions.java
@@ -0,0 +1,63 @@
+package labyrinth;
+
+import io.vavr.collection.Stream;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Directions {
+ private final Set directions = new HashSet<>();
+ // FIXME remove getter, only for debugging
+ @Getter
+ private final Set hardened = new HashSet<>();
+
+ public boolean set(@NonNull final Direction direction) {
+ return this.directions.add(direction);
+ }
+
+ public void setAll() {
+ this.directions.addAll(EnumSet.allOf(Direction.class));
+ }
+
+ public boolean clear(@NonNull final Direction direction) {
+ if (this.hardened.contains(direction)) {
+ return false;
+ }
+ return this.directions.remove(direction);
+ }
+
+ public void clearAll() {
+ this.directions.clear();
+ this.directions.addAll(this.hardened);
+ }
+
+ public boolean isSet(@NonNull final Direction direction) {
+ return this.directions.contains(direction);
+ }
+
+ public Iterable getSet() {
+ return this.getSet(false);
+ }
+
+ public Iterable getSet(final boolean onlyBrittleWalls) {
+ if (onlyBrittleWalls) {
+ return Stream.ofAll(this.directions)
+ .removeAll(this.hardened);
+ }
+ return Stream.ofAll(this.directions);
+ }
+
+ public void harden(@NonNull final Direction direction) {
+ if (!this.directions.contains(direction)) {
+ throw new IllegalStateException("Trying to harden cleared Direction: " + direction);
+ }
+ this.hardened.add(direction);
+ }
+
+ public void unharden(@NonNull final Direction direction) {
+ this.hardened.remove(direction);
+ }
+}
diff --git a/src/main/java/labyrinth/Labyrinth.java b/src/main/java/labyrinth/Labyrinth.java
new file mode 100644
index 0000000..bb23734
--- /dev/null
+++ b/src/main/java/labyrinth/Labyrinth.java
@@ -0,0 +1,194 @@
+package labyrinth;
+
+import io.vavr.control.Option;
+import lombok.NonNull;
+
+import java.util.Deque;
+import java.util.LinkedList;
+
+public class Labyrinth {
+ private final Tile[][] field;
+ private final int width;
+ private final int height;
+
+ public Labyrinth(final int width, final int height) {
+ this.width = width;
+ this.height = height;
+ this.field = new Tile[width][height];
+ this.initField();
+ System.out.println(this);
+ this.generate();
+ }
+ private static final char TOP_LEFT = '\u250c';
+ private static final char TOP_RIGHT = '\u2510';
+ private static final char MIDDLE_LEFT = '\u251c';
+ private static final char MIDDLE_RIGHT = '\u2524';
+ private static final char BOTTOM_LEFT = '\u2514';
+ private static final char BOTTOM_RIGHT = '\u2518';
+ private static final char HORIZONTAL = '\u2500';
+ private static final char VERTICAL = '\u2502';
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (int y = 0; y < this.height; y++) {
+ // TOP WALL
+ for (int x = 0; x < this.width; x++) {
+ final Tile tile = this.getTileAt(new Position(x, y));
+ final boolean top = tile.getWalls().isSet(Direction.TOP);
+ final boolean topHard = tile.getWalls().getHardened().contains(Direction.TOP);
+ sb.append(TOP_LEFT);
+ if (topHard) {
+ if (top) {
+ sb.append(HORIZONTAL);
+ } else {
+ sb.append("?");
+ }
+ } else {
+ if (top) {
+ sb.append("-");
+ } else {
+ sb.append(" ");
+ }
+ }
+ sb.append(TOP_RIGHT);
+ }
+ sb.append("\n");
+ // LEFT WALL, CENTER, RIGHT WALL
+ for (int x = 0; x < this.width; x++) {
+ final Tile tile = this.getTileAt(new Position(x, y));
+ // left
+ final boolean left = tile.getWalls().isSet(Direction.LEFT);
+ final boolean leftHard = tile.getWalls().getHardened().contains(Direction.LEFT);
+ if (leftHard) {
+ if (left) {
+ sb.append(VERTICAL);
+ } else {
+ sb.append("?");
+ }
+ } else {
+ if (left) {
+ sb.append(":");
+ } else {
+ sb.append(" ");
+ }
+ }
+ // center
+ sb.append(" ");
+ // right
+ final boolean right = tile.getWalls().isSet(Direction.RIGHT);
+ final boolean rightHard = tile.getWalls().getHardened().contains(Direction.RIGHT);
+ if (rightHard) {
+ if (right) {
+ sb.append(VERTICAL);
+ } else {
+ sb.append("?");
+ }
+ } else {
+ if (right) {
+ sb.append(":");
+ } else {
+ sb.append(" ");
+ }
+ }
+ }
+ sb.append("\n");
+ // BOTTOM WALL
+ for (int x = 0; x < this.width; x++) {
+ final Tile tile = this.getTileAt(new Position(x, y));
+ final boolean bottom = tile.getWalls().isSet(Direction.BOTTOM);
+ final boolean bottomHard = tile.getWalls().getHardened().contains(Direction.BOTTOM);
+ sb.append(BOTTOM_LEFT);
+ if (bottomHard) {
+ if (bottom) {
+ sb.append(HORIZONTAL);
+ } else {
+ sb.append("?");
+ }
+ } else {
+ if (bottom) {
+ sb.append("-");
+ } else {
+ sb.append(" ");
+ }
+ }
+ sb.append(BOTTOM_RIGHT);
+ }
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+
+ private Tile getTileAt(@NonNull final Position position) {
+ return this.field[position.getX()][position.getY()];
+ }
+
+ private void initField() {
+ for (int x = 0; x < this.width; x++) {
+ this.field[x] = new Tile[this.height];
+ for (int y = 0; y < this.height; y++) {
+ final Tile tile = new Tile();
+ this.hardenWalls(tile, x, y);
+ this.field[x][y] = tile;
+ }
+ }
+ }
+
+ private void hardenWalls(@NonNull final Tile tile, final int x, final int y) {
+ if (x == 0) {
+ tile.preventDiggingToOrFrom(Direction.LEFT);
+ } else if (x == width - 1) {
+ tile.preventDiggingToOrFrom(Direction.RIGHT);
+ }
+ if (y == 0) {
+ if (x != 0) {
+ tile.preventDiggingToOrFrom(Direction.TOP);
+ }
+ } else if (y == height - 1) {
+ tile.preventDiggingToOrFrom(Direction.BOTTOM);
+ }
+ }
+
+ private void generate() {
+ new Generator();
+ }
+
+ public class Generator {
+ private final Deque positions = new LinkedList<>();
+
+ Generator() {
+ final Position topLeft = new Position(0, 0);
+ Labyrinth.this.getTileAt(topLeft).digFrom(Direction.TOP);
+ this.positions.push(topLeft);
+ this.dig();
+ final Position bottomRight = new Position(Labyrinth.this.width - 1, Labyrinth.this.height - 1);
+ final Tile bottomRightTile = Labyrinth.this.getTileAt(bottomRight);
+ bottomRightTile.enableDiggingToOrFrom(Direction.BOTTOM);
+ bottomRightTile.digTo(Direction.BOTTOM);
+ }
+
+ private void dig() {
+ while (!this.positions.isEmpty()) {
+ final Position currentPosition = this.positions.peek();
+ final Tile currentTile = Labyrinth.this.getTileAt(currentPosition);
+ final Option directionToDigTo = currentTile.getRandomAvailableDirection();
+ if (directionToDigTo.isDefined()) {
+ final Direction digTo = directionToDigTo.get();
+ final Direction digFrom = digTo.getOpposite();
+ final Position neighborPosition = digTo.translate(currentPosition);
+ final Tile neighborTile = Labyrinth.this.getTileAt(neighborPosition);
+ if (currentTile.digTo(digTo) && neighborTile.digFrom(digFrom)) {
+ // all ok!
+ this.positions.push(neighborPosition);
+ } else {
+ // Hm, didn't work.
+ currentTile.undigTo(digTo);
+ currentTile.preventDiggingToOrFrom(digTo);
+ }
+ } else {
+ this.positions.pop();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/labyrinth/Main.java b/src/main/java/labyrinth/Main.java
new file mode 100644
index 0000000..c478990
--- /dev/null
+++ b/src/main/java/labyrinth/Main.java
@@ -0,0 +1,12 @@
+package labyrinth;
+
+import lombok.NonNull;
+
+public class Main {
+ public static void main(@NonNull final String[] args) {
+ int width = 5;
+ int height = 8;
+ final Labyrinth labyrinth = new Labyrinth(width, height);
+ System.out.println(labyrinth);
+ }
+}
diff --git a/src/main/java/labyrinth/Position.java b/src/main/java/labyrinth/Position.java
new file mode 100644
index 0000000..90f33e5
--- /dev/null
+++ b/src/main/java/labyrinth/Position.java
@@ -0,0 +1,11 @@
+package labyrinth;
+
+import lombok.Value;
+import lombok.With;
+
+@Value
+@With
+public class Position {
+ int x;
+ int y;
+}
diff --git a/src/main/java/labyrinth/Tile.java b/src/main/java/labyrinth/Tile.java
new file mode 100644
index 0000000..7ad5135
--- /dev/null
+++ b/src/main/java/labyrinth/Tile.java
@@ -0,0 +1,49 @@
+package labyrinth;
+
+import io.vavr.collection.Stream;
+import io.vavr.control.Option;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class Tile {
+ // FIXME remove me; only for debugging
+ @Getter
+ final Directions walls = new Directions();
+ @Getter
+ boolean visited = false;
+
+ public Tile() {
+ this.walls.setAll();
+ }
+
+ public void preventDiggingToOrFrom(@NonNull final Direction direction) {
+ this.walls.harden(direction);
+ }
+
+ public void enableDiggingToOrFrom(@NonNull final Direction direction) {
+ this.walls.unharden(direction);
+ }
+
+ public boolean digFrom(@NonNull final Direction direction) {
+ if (this.visited) {
+ return false;
+ }
+ this.visited = true;
+ return this.walls.clear(direction);
+ }
+
+ public boolean digTo(@NonNull final Direction direction) {
+ return this.walls.clear(direction);
+ }
+
+ public void undigTo(@NonNull final Direction direction) {
+ this.walls.set(direction);
+ }
+
+ public Option getRandomAvailableDirection() {
+ return Stream.ofAll(this.walls.getSet(true)).shuffle().headOption();
+ }
+}