Merge pull request 'feature/serialize' (#1) from feature/serialize into master
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Reviewed-on: java/labyrinth-generator#1
This commit is contained in:
		
						commit
						d320771614
					
				
					 24 changed files with 544 additions and 112 deletions
				
			
		|  | @ -20,7 +20,9 @@ steps: | ||||||
|         from_secret: repo-token |         from_secret: repo-token | ||||||
|     commands: |     commands: | ||||||
|       - mvn -s maven-settings.xml deploy -DskipTests=true |       - mvn -s maven-settings.xml deploy -DskipTests=true | ||||||
|     when: |     trigger: | ||||||
|       branch: |       branch: | ||||||
|         include: |  | ||||||
|         - master |         - master | ||||||
|  |       event: | ||||||
|  |         exclude: | ||||||
|  |         - pull_request | ||||||
|  |  | ||||||
|  | @ -16,8 +16,8 @@ | ||||||
|     <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" /> |     <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> |     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> |     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" /> |     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.25" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" /> |     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.25" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> |     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> | ||||||
|     <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" /> |     <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" /> | ||||||
|     <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" /> |     <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" /> | ||||||
|  |  | ||||||
							
								
								
									
										37
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										37
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -8,9 +8,17 @@ | ||||||
| 		<artifactId>fritteli-build-parent</artifactId> | 		<artifactId>fritteli-build-parent</artifactId> | ||||||
| 		<version>2.0.4</version> | 		<version>2.0.4</version> | ||||||
| 	</parent> | 	</parent> | ||||||
|  | 
 | ||||||
| 	<groupId>ch.fritteli.labyrinth</groupId> | 	<groupId>ch.fritteli.labyrinth</groupId> | ||||||
| 	<artifactId>labyrinth-generator</artifactId> | 	<artifactId>labyrinth-generator</artifactId> | ||||||
| 	<version>0.0.2-SNAPSHOT</version> | 	<version>0.0.2-SNAPSHOT</version> | ||||||
|  | 
 | ||||||
|  | 	<properties> | ||||||
|  | 		<logback.version>1.2.10</logback.version> | ||||||
|  | 		<pdfbox.version>2.0.25</pdfbox.version> | ||||||
|  | 		<slf4j.version>1.7.35</slf4j.version> | ||||||
|  | 	</properties> | ||||||
|  | 
 | ||||||
| 	<dependencies> | 	<dependencies> | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>org.projectlombok</groupId> | 			<groupId>org.projectlombok</groupId> | ||||||
|  | @ -27,12 +35,21 @@ | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>org.apache.pdfbox</groupId> | 			<groupId>org.apache.pdfbox</groupId> | ||||||
| 			<artifactId>pdfbox</artifactId> | 			<artifactId>pdfbox</artifactId> | ||||||
| 			<version>2.0.20</version> | 			<version>${pdfbox.version}</version> | ||||||
|  | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>org.slf4j</groupId> | ||||||
|  | 			<artifactId>slf4j-api</artifactId> | ||||||
|  | 			<version>${slf4j.version}</version> | ||||||
|  | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>ch.qos.logback</groupId> | ||||||
|  | 			<artifactId>logback-classic</artifactId> | ||||||
|  | 			<version>${logback.version}</version> | ||||||
| 		</dependency> | 		</dependency> | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>org.junit.jupiter</groupId> | 			<groupId>org.junit.jupiter</groupId> | ||||||
| 			<artifactId>junit-jupiter-api</artifactId> | 			<artifactId>junit-jupiter-api</artifactId> | ||||||
| 			<scope>test</scope> |  | ||||||
| 		</dependency> | 		</dependency> | ||||||
| 	</dependencies> | 	</dependencies> | ||||||
| 	<build> | 	<build> | ||||||
|  | @ -81,12 +98,24 @@ | ||||||
| 	</distributionManagement> | 	</distributionManagement> | ||||||
| 	<repositories> | 	<repositories> | ||||||
| 		<repository> | 		<repository> | ||||||
| 			<id>repo.gittr.ch</id> | 			<id>repo.gittr.ch.releases</id> | ||||||
| 			<url>https://repo.gittr.ch/</url> | 			<url>https://repo.gittr.ch/releases/</url> | ||||||
| 			<releases> | 			<releases> | ||||||
| 				<enabled>true</enabled> | 				<enabled>true</enabled> | ||||||
| 				<updatePolicy>never</updatePolicy> | 				<updatePolicy>never</updatePolicy> | ||||||
| 			</releases> | 			</releases> | ||||||
|  | 			<snapshots> | ||||||
|  | 				<enabled>false</enabled> | ||||||
|  | 				<updatePolicy>never</updatePolicy> | ||||||
|  | 			</snapshots> | ||||||
|  | 		</repository> | ||||||
|  | 		<repository> | ||||||
|  | 			<id>repo.gittr.ch.snapshots</id> | ||||||
|  | 			<url>https://repo.gittr.ch/snapshots/</url> | ||||||
|  | 			<releases> | ||||||
|  | 				<enabled>false</enabled> | ||||||
|  | 				<updatePolicy>never</updatePolicy> | ||||||
|  | 			</releases> | ||||||
| 			<snapshots> | 			<snapshots> | ||||||
| 				<enabled>true</enabled> | 				<enabled>true</enabled> | ||||||
| 				<updatePolicy>always</updatePolicy> | 				<updatePolicy>always</updatePolicy> | ||||||
|  |  | ||||||
|  | @ -7,10 +7,12 @@ import ch.fritteli.labyrinth.generator.renderer.pdffile.PDFFileRenderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.textfile.TextFileRenderer; | import ch.fritteli.labyrinth.generator.renderer.textfile.TextFileRenderer; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
| 
 | 
 | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
| import java.nio.file.Paths; | import java.nio.file.Paths; | ||||||
| 
 | 
 | ||||||
|  | @Slf4j | ||||||
| public class Main { | public class Main { | ||||||
|     public static void main(@NonNull final String[] args) { |     public static void main(@NonNull final String[] args) { | ||||||
|         int width = 100; |         int width = 100; | ||||||
|  | @ -28,19 +30,19 @@ public class Main { | ||||||
|         final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() |         final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() | ||||||
|                 .setTargetFile(userHome.resolve(baseFilename + ".pdf")); |                 .setTargetFile(userHome.resolve(baseFilename + ".pdf")); | ||||||
| 
 | 
 | ||||||
|         System.out.println("Labyrinth-ID: " + labyrinth.getRandomSeed()); |         log.info("Labyrinth-ID: {}", labyrinth.getRandomSeed()); | ||||||
|         // Render Labyrinth to stdout |         // Render Labyrinth to stdout | ||||||
|         System.out.println(textRenderer.render(labyrinth)); |         log.info("Text rendering:\n{}", textRenderer.render(labyrinth)); | ||||||
|         // Render Labyrinth solution to stdout |         // Render Labyrinth solution to stdout | ||||||
|         System.out.println(textRenderer.setRenderSolution(true).render(labyrinth)); |         log.info("Text rendering with solution:\n{}", textRenderer.setRenderSolution(true).render(labyrinth)); | ||||||
|         // Render HTML to stdout |         // Render HTML to stdout | ||||||
|         System.out.println(htmlRenderer.render(labyrinth)); |         log.info("HTML rendering:\n{}", htmlRenderer.render(labyrinth)); | ||||||
|         // Render Labyrinth and solution to (separate) files |         // Render Labyrinth and solution to (separate) files | ||||||
|         System.out.println(textFileRenderer.render(labyrinth)); |         log.info("Text rendering to file:\n{}", textFileRenderer.render(labyrinth)); | ||||||
|         // Render HTML to file |         // Render HTML to file | ||||||
|         System.out.println(htmlFileRenderer.render(labyrinth)); |         log.info("HTML rendering to file:\n{}", htmlFileRenderer.render(labyrinth)); | ||||||
|         // Render PDF to file |         // Render PDF to file | ||||||
|         System.out.println(pdfFileRenderer.render(labyrinth)); |         log.info("PDF rendering to file:\n{}", pdfFileRenderer.render(labyrinth)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static String getBaseFilename(@NonNull final Labyrinth labyrinth) { |     private static String getBaseFilename(@NonNull final Labyrinth labyrinth) { | ||||||
|  |  | ||||||
|  | @ -1,13 +1,17 @@ | ||||||
| package ch.fritteli.labyrinth.generator.model; | package ch.fritteli.labyrinth.generator.model; | ||||||
| 
 | 
 | ||||||
| import io.vavr.control.Option; | import io.vavr.control.Option; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.Getter; | import lombok.Getter; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.ToString; | ||||||
| 
 | 
 | ||||||
| import java.util.Deque; | import java.util.Deque; | ||||||
| import java.util.LinkedList; | import java.util.LinkedList; | ||||||
| import java.util.Random; | import java.util.Random; | ||||||
| 
 | 
 | ||||||
|  | @EqualsAndHashCode | ||||||
|  | @ToString | ||||||
| public class Labyrinth { | public class Labyrinth { | ||||||
|     private final Tile[][] field; |     private final Tile[][] field; | ||||||
|     @Getter |     @Getter | ||||||
|  | @ -16,6 +20,7 @@ public class Labyrinth { | ||||||
|     private final int height; |     private final int height; | ||||||
|     @Getter |     @Getter | ||||||
|     private final long randomSeed; |     private final long randomSeed; | ||||||
|  |     @EqualsAndHashCode.Exclude | ||||||
|     private final Random random; |     private final Random random; | ||||||
|     @Getter |     @Getter | ||||||
|     private final Position start; |     private final Position start; | ||||||
|  | @ -41,6 +46,20 @@ public class Labyrinth { | ||||||
|         this.generate(); |         this.generate(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * INTERNAL API. | ||||||
|  |      * Exists only for deserialization. Not to be called from user code. | ||||||
|  |      */ | ||||||
|  |     private Labyrinth(@NonNull final Tile[][] field, final int width, final int height, final long randomSeed) { | ||||||
|  |         this.field = field; | ||||||
|  |         this.width = width; | ||||||
|  |         this.height = height; | ||||||
|  |         this.randomSeed = randomSeed; | ||||||
|  |         this.random = new Random(randomSeed); | ||||||
|  |         this.start = new Position(0, 0); | ||||||
|  |         this.end = new Position(this.width - 1, this.height - 1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public Option<Tile> getTileAt(@NonNull final Position position) { |     public Option<Tile> getTileAt(@NonNull final Position position) { | ||||||
|         return this.getTileAt(position.getX(), position.getY()); |         return this.getTileAt(position.getX(), position.getY()); | ||||||
|  |  | ||||||
|  | @ -3,13 +3,18 @@ package ch.fritteli.labyrinth.generator.model; | ||||||
| import io.vavr.collection.Stream; | import io.vavr.collection.Stream; | ||||||
| import io.vavr.control.Option; | import io.vavr.control.Option; | ||||||
| import lombok.AccessLevel; | import lombok.AccessLevel; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.Getter; | import lombok.Getter; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.ToString; | ||||||
| import lombok.experimental.FieldDefaults; | import lombok.experimental.FieldDefaults; | ||||||
| 
 | 
 | ||||||
|  | import java.util.EnumSet; | ||||||
| import java.util.Random; | import java.util.Random; | ||||||
| 
 | 
 | ||||||
| @FieldDefaults(level = AccessLevel.PRIVATE) | @FieldDefaults(level = AccessLevel.PRIVATE) | ||||||
|  | @EqualsAndHashCode | ||||||
|  | @ToString | ||||||
| public class Tile { | public class Tile { | ||||||
|     final Walls walls = new Walls(); |     final Walls walls = new Walls(); | ||||||
|     boolean visited = false; |     boolean visited = false; | ||||||
|  | @ -20,6 +25,22 @@ public class Tile { | ||||||
|         this.walls.setAll(); |         this.walls.setAll(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * INTERNAL API. | ||||||
|  |      * Exists only for deserialization. Not to be called from user code. | ||||||
|  |      * | ||||||
|  |      * @param walls | ||||||
|  |      * @param solution | ||||||
|  |      */ | ||||||
|  |     private Tile(@NonNull final EnumSet<Direction> walls, final boolean solution) { | ||||||
|  |         for (final Direction direction : walls) { | ||||||
|  |             this.walls.set(direction); | ||||||
|  |             this.walls.harden(direction); | ||||||
|  |         } | ||||||
|  |         this.visited = true; | ||||||
|  |         this.solution = solution; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public void preventDiggingToOrFrom(@NonNull final Direction direction) { |     public void preventDiggingToOrFrom(@NonNull final Direction direction) { | ||||||
|         this.walls.harden(direction); |         this.walls.harden(direction); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| package ch.fritteli.labyrinth.generator.model; | package ch.fritteli.labyrinth.generator.model; | ||||||
| 
 | 
 | ||||||
| import io.vavr.collection.Stream; | import io.vavr.collection.Stream; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.ToString; | ||||||
| 
 | 
 | ||||||
| import java.util.EnumSet; | import java.util.EnumSet; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
|  | @ -9,6 +11,8 @@ import java.util.Set; | ||||||
| import java.util.SortedSet; | import java.util.SortedSet; | ||||||
| import java.util.TreeSet; | import java.util.TreeSet; | ||||||
| 
 | 
 | ||||||
|  | @EqualsAndHashCode | ||||||
|  | @ToString | ||||||
| public class Walls { | public class Walls { | ||||||
|     private final SortedSet<Direction> directions = new TreeSet<>(); |     private final SortedSet<Direction> directions = new TreeSet<>(); | ||||||
|     private final Set<Direction> hardened = new HashSet<>(); |     private final Set<Direction> hardened = new HashSet<>(); | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ public class HTMLRenderer implements Renderer<String> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  |     @Override | ||||||
|     public String render(@NonNull final Labyrinth labyrinth) { |     public String render(@NonNull final Labyrinth labyrinth) { | ||||||
|         if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { |         if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { | ||||||
|             return this.getPreamble(labyrinth) + POSTAMBLE; |             return this.getPreamble(labyrinth) + POSTAMBLE; | ||||||
|  |  | ||||||
|  | @ -3,25 +3,30 @@ package ch.fritteli.labyrinth.generator.renderer.htmlfile; | ||||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.Renderer; | import ch.fritteli.labyrinth.generator.renderer.Renderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; | import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; | ||||||
|  | import io.vavr.control.Option; | ||||||
|  | import io.vavr.control.Try; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
| 
 | 
 | ||||||
|  | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
| import java.nio.file.Files; | import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
|  | import java.util.NoSuchElementException; | ||||||
| 
 | 
 | ||||||
|  | @Slf4j | ||||||
| public class HTMLFileRenderer implements Renderer<Path> { | public class HTMLFileRenderer implements Renderer<Path> { | ||||||
|     @NonNull |     @NonNull | ||||||
|     private static final HTMLRenderer HTML_RENDERER = HTMLRenderer.newInstance(); |     private static final HTMLRenderer HTML_RENDERER = HTMLRenderer.newInstance(); | ||||||
|     private Path targetFile; |     @NonNull | ||||||
|  |     private Option<Path> targetFile; | ||||||
| 
 | 
 | ||||||
|     private HTMLFileRenderer() { |     private HTMLFileRenderer() { | ||||||
|         try { |         this.targetFile = Try | ||||||
|             this.targetFile = Files.createTempFile("labyrinth_", ".html"); |                 .of(() -> Files.createTempFile("labyrinth_", ".html")) | ||||||
|         } catch (IOException e) { |                 .onFailure(ex -> log.error("Unable to set default target file.", ex)) | ||||||
|             System.err.println("Unable to set default target file."); |                 .toOption(); | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  | @ -30,30 +35,33 @@ public class HTMLFileRenderer implements Renderer<Path> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean isTargetFileDefinedAndWritable() { |     public boolean isTargetFileDefinedAndWritable() { | ||||||
|         return this.targetFile != null && this.targetFile.toFile().canWrite(); |         return this.targetFile | ||||||
|  |                 .map(Path::toFile) | ||||||
|  |                 .exists(File::canWrite); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public HTMLFileRenderer setTargetFile(@NonNull final Path targetFile) { |     public HTMLFileRenderer setTargetFile(@NonNull final Path targetFile) { | ||||||
|         this.targetFile = targetFile; |         this.targetFile = Option.of(targetFile); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public Path render(@NonNull final Labyrinth labyrinth) { |     public Path render(@NonNull final Labyrinth labyrinth) { | ||||||
|         if (!this.isTargetFileDefinedAndWritable()) { |         if (!this.isTargetFileDefinedAndWritable()) { | ||||||
|             try { |             try { | ||||||
|                 Files.createFile(this.targetFile); |                 Files.createFile(this.targetFile.get()); | ||||||
|             } catch (IOException e) { |             } catch (IOException | NoSuchElementException e) { | ||||||
|                 throw new IllegalArgumentException("Cannot write to target file.", e); |                 throw new IllegalArgumentException("Cannot write to target file.", e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         final String html = HTML_RENDERER.render(labyrinth); |         final String html = HTML_RENDERER.render(labyrinth); | ||||||
|  |         final Path targetFile = this.targetFile.get(); | ||||||
|         try { |         try { | ||||||
|             Files.writeString(this.targetFile, html, StandardCharsets.UTF_8); |             Files.writeString(targetFile, html, StandardCharsets.UTF_8); | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             System.err.println("Failed writing to file " + this.targetFile.normalize().toString()); |             log.error("Failed writing to file " + targetFile.normalize(), e); | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |         } | ||||||
|         return targetFile; |         return targetFile; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import io.vavr.control.Option; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import lombok.Value; | import lombok.Value; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
| import org.apache.pdfbox.pdmodel.PDDocument; | import org.apache.pdfbox.pdmodel.PDDocument; | ||||||
| import org.apache.pdfbox.pdmodel.PDDocumentInformation; | import org.apache.pdfbox.pdmodel.PDDocumentInformation; | ||||||
| import org.apache.pdfbox.pdmodel.PDPage; | import org.apache.pdfbox.pdmodel.PDPage; | ||||||
|  | @ -20,14 +21,12 @@ import java.io.ByteArrayOutputStream; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| 
 | 
 | ||||||
| @RequiredArgsConstructor | @RequiredArgsConstructor | ||||||
|  | @Slf4j | ||||||
| class Generator { | class Generator { | ||||||
|     @NonNull |     @NonNull | ||||||
|     private final Labyrinth labyrinth; |     private final Labyrinth labyrinth; | ||||||
| 
 | 
 | ||||||
|     private static boolean isValid(@NonNull final Position position) { |     @NonNull | ||||||
|         return position.getX() >= 0 && position.getY() >= 0; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public byte[] generate() { |     public byte[] generate() { | ||||||
|         final float pageWidth = this.labyrinth.getWidth() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN; |         final float pageWidth = this.labyrinth.getWidth() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN; | ||||||
|         final float pageHeight = this.labyrinth.getHeight() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN; |         final float pageHeight = this.labyrinth.getHeight() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN; | ||||||
|  | @ -36,26 +35,26 @@ class Generator { | ||||||
|         final PDDocumentInformation info = new PDDocumentInformation(); |         final PDDocumentInformation info = new PDDocumentInformation(); | ||||||
|         info.setTitle("Labyrinth " + this.labyrinth.getWidth() + "x" + this.labyrinth.getHeight() + ", ID " + this.labyrinth.getRandomSeed()); |         info.setTitle("Labyrinth " + this.labyrinth.getWidth() + "x" + this.labyrinth.getHeight() + ", ID " + this.labyrinth.getRandomSeed()); | ||||||
|         pdDocument.setDocumentInformation(info); |         pdDocument.setDocumentInformation(info); | ||||||
|         final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight)); |         final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight)); | ||||||
|         final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight)); |         final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight)); | ||||||
|         pdDocument.addPage(page); |         pdDocument.addPage(puzzlePage); | ||||||
|         pdDocument.addPage(solution); |         pdDocument.addPage(solutionPage); | ||||||
|         try (final PDPageContentStream labyrinthPageContentStream = new PDPageContentStream(pdDocument, page); |         try (final PDPageContentStream puzzlePageContentStream = new PDPageContentStream(pdDocument, puzzlePage); | ||||||
|              final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solution)) { |              final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solutionPage)) { | ||||||
|             setUpPageContentStream(labyrinthPageContentStream); |             setUpPageContentStream(puzzlePageContentStream); | ||||||
|             setUpPageContentStream(solutionPageContentStream); |             setUpPageContentStream(solutionPageContentStream); | ||||||
|             this.drawHorizonzalLines(labyrinthPageContentStream, solutionPageContentStream); |             this.drawHorizontalLines(puzzlePageContentStream, solutionPageContentStream); | ||||||
|             this.drawVerticalLines(labyrinthPageContentStream, solutionPageContentStream); |             this.drawVerticalLines(puzzlePageContentStream, solutionPageContentStream); | ||||||
|             this.drawSolution(solutionPageContentStream); |             this.drawSolution(solutionPageContentStream); | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             e.printStackTrace(); |             log.error("Error while rendering PDF document", e); | ||||||
|         } |         } | ||||||
|         final ByteArrayOutputStream output = new ByteArrayOutputStream(); |         final ByteArrayOutputStream output = new ByteArrayOutputStream(); | ||||||
|         try { |         try { | ||||||
|             pdDocument.save(output); |             pdDocument.save(output); | ||||||
|             pdDocument.close(); |             pdDocument.close(); | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             e.printStackTrace(); |             log.error("Error while writing PDF data", e); | ||||||
|         } |         } | ||||||
|         return output.toByteArray(); |         return output.toByteArray(); | ||||||
|     } |     } | ||||||
|  | @ -68,9 +67,10 @@ class Generator { | ||||||
|         pageContentStream.setNonStrokingColor(Color.BLACK); |         pageContentStream.setNonStrokingColor(Color.BLACK); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void drawHorizonzalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException { |     private void drawHorizontalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException { | ||||||
|         // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required. |         // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required. | ||||||
|         Coordinate coordinate = new Coordinate(0f, 0f); |         Coordinate coordinate = new Coordinate(0f, 0f); | ||||||
|  |         // Draw the TOP borders of all tiles. | ||||||
|         for (int y = 0; y < this.labyrinth.getHeight(); y++) { |         for (int y = 0; y < this.labyrinth.getHeight(); y++) { | ||||||
|             boolean isPainting = false; |             boolean isPainting = false; | ||||||
|             coordinate = coordinate.withY(y); |             coordinate = coordinate.withY(y); | ||||||
|  | @ -102,6 +102,7 @@ class Generator { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         // Draw the BOTTOM border of the last row of tiles. | ||||||
|         boolean isPainting = false; |         boolean isPainting = false; | ||||||
|         int y = this.labyrinth.getHeight(); |         int y = this.labyrinth.getHeight(); | ||||||
|         coordinate = coordinate.withY(this.labyrinth.getHeight()); |         coordinate = coordinate.withY(this.labyrinth.getHeight()); | ||||||
|  | @ -137,6 +138,7 @@ class Generator { | ||||||
|     private void drawVerticalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException { |     private void drawVerticalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException { | ||||||
|         // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required. |         // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required. | ||||||
|         Coordinate coordinate = new Coordinate(0f, 0f); |         Coordinate coordinate = new Coordinate(0f, 0f); | ||||||
|  |         // Draw the LEFT borders of all tiles. | ||||||
|         for (int x = 0; x < this.labyrinth.getWidth(); x++) { |         for (int x = 0; x < this.labyrinth.getWidth(); x++) { | ||||||
|             boolean isPainting = false; |             boolean isPainting = false; | ||||||
|             coordinate = coordinate.withX(x); |             coordinate = coordinate.withX(x); | ||||||
|  | @ -168,6 +170,7 @@ class Generator { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         // Draw the RIGHT border of the last column of tiles. | ||||||
|         boolean isPainting = false; |         boolean isPainting = false; | ||||||
|         int x = this.labyrinth.getWidth(); |         int x = this.labyrinth.getWidth(); | ||||||
|         coordinate = coordinate.withX(this.labyrinth.getWidth()); |         coordinate = coordinate.withX(this.labyrinth.getWidth()); | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ public class PDFRenderer implements Renderer<byte[]> { | ||||||
|     static final float SCALE = 10; |     static final float SCALE = 10; | ||||||
| 
 | 
 | ||||||
|     private PDFRenderer() { |     private PDFRenderer() { | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  | @ -17,12 +16,10 @@ public class PDFRenderer implements Renderer<byte[]> { | ||||||
|         return new PDFRenderer(); |         return new PDFRenderer(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     @NonNull |     @NonNull | ||||||
|  |     @Override | ||||||
|     public byte[] render(@NonNull final Labyrinth labyrinth) { |     public byte[] render(@NonNull final Labyrinth labyrinth) { | ||||||
|         final Generator generator = new Generator(labyrinth); |         final Generator generator = new Generator(labyrinth); | ||||||
|         return generator.generate(); |         return generator.generate(); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,24 +3,29 @@ package ch.fritteli.labyrinth.generator.renderer.pdffile; | ||||||
| import ch.fritteli.labyrinth.generator.model.Labyrinth; | import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.Renderer; | import ch.fritteli.labyrinth.generator.renderer.Renderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; | import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer; | ||||||
|  | import io.vavr.control.Option; | ||||||
|  | import io.vavr.control.Try; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
| 
 | 
 | ||||||
|  | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.nio.file.Files; | import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
|  | import java.util.NoSuchElementException; | ||||||
| 
 | 
 | ||||||
|  | @Slf4j | ||||||
| public class PDFFileRenderer implements Renderer<Path> { | public class PDFFileRenderer implements Renderer<Path> { | ||||||
|     @NonNull |     @NonNull | ||||||
|     private static final PDFRenderer PDF_RENDERER = PDFRenderer.newInstance(); |     private static final PDFRenderer PDF_RENDERER = PDFRenderer.newInstance(); | ||||||
|     private Path targetFile; |     @NonNull | ||||||
|  |     private Option<Path> targetFile; | ||||||
| 
 | 
 | ||||||
|     private PDFFileRenderer() { |     private PDFFileRenderer() { | ||||||
|         try { |         this.targetFile = Try | ||||||
|             this.targetFile = Files.createTempFile("labyrinth_", ".pdf"); |                 .of(() -> Files.createTempFile("labyrinth_", ".pdf")) | ||||||
|         } catch (IOException e) { |                 .onFailure(ex -> log.error("Unable to set default target file.", ex)) | ||||||
|             System.err.println("Unable to set default target file."); |                 .toOption(); | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  | @ -29,31 +34,34 @@ public class PDFFileRenderer implements Renderer<Path> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean isTargetFileDefinedAndWritable() { |     public boolean isTargetFileDefinedAndWritable() { | ||||||
|         return this.targetFile != null && this.targetFile.toFile().canWrite(); |         return this.targetFile | ||||||
|  |                 .map(Path::toFile) | ||||||
|  |                 .exists(File::canWrite); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public PDFFileRenderer setTargetFile(@NonNull final Path targetFile) { |     public PDFFileRenderer setTargetFile(@NonNull final Path targetFile) { | ||||||
|         this.targetFile = targetFile; |         this.targetFile = Option.of(targetFile); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public Path render(@NonNull final Labyrinth labyrinth) { |     public Path render(@NonNull final Labyrinth labyrinth) { | ||||||
|         if (!this.isTargetFileDefinedAndWritable()) { |         if (!this.isTargetFileDefinedAndWritable()) { | ||||||
|             try { |             try { | ||||||
|                 Files.createFile(this.targetFile); |                 Files.createFile(this.targetFile.get()); | ||||||
|             } catch (IOException e) { |             } catch (IOException | NoSuchElementException e) { | ||||||
|                 throw new IllegalArgumentException("Cannot write to target file.", e); |                 throw new IllegalArgumentException("Cannot write to target file.", e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         final byte[] bytes = PDF_RENDERER.render(labyrinth); |         final byte[] bytes = PDF_RENDERER.render(labyrinth); | ||||||
|  |         final Path targetFile = this.targetFile.get(); | ||||||
|         try { |         try { | ||||||
|             Files.write(this.targetFile, bytes); |             Files.write(targetFile, bytes); | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             System.err.println("Failed writing to file " + this.targetFile.normalize().toString()); |             log.error("Failed writing to file " + targetFile.normalize(), e); | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |         } | ||||||
|         return this.targetFile; |         return targetFile; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,12 +2,14 @@ package ch.fritteli.labyrinth.generator.renderer.text; | ||||||
| 
 | 
 | ||||||
| import lombok.AccessLevel; | import lombok.AccessLevel; | ||||||
| import lombok.AllArgsConstructor; | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.EqualsAndHashCode; | ||||||
| import lombok.NoArgsConstructor; | import lombok.NoArgsConstructor; | ||||||
| import lombok.experimental.FieldDefaults; | import lombok.experimental.FieldDefaults; | ||||||
| 
 | 
 | ||||||
|  | @AllArgsConstructor | ||||||
|  | @EqualsAndHashCode | ||||||
| @FieldDefaults(level = AccessLevel.PRIVATE) | @FieldDefaults(level = AccessLevel.PRIVATE) | ||||||
| @NoArgsConstructor | @NoArgsConstructor | ||||||
| @AllArgsConstructor |  | ||||||
| class CharDefinition { | class CharDefinition { | ||||||
|     // ─ |     // ─ | ||||||
|     static final String HORIZONTAL = "\u2500"; |     static final String HORIZONTAL = "\u2500"; | ||||||
|  |  | ||||||
|  | @ -15,6 +15,13 @@ class Generator { | ||||||
|     private final boolean renderSolution; |     private final boolean renderSolution; | ||||||
|     private int x = 0; |     private int x = 0; | ||||||
|     private int y = 0; |     private int y = 0; | ||||||
|  |     // Each row has three lines: | ||||||
|  |     // - The top border | ||||||
|  |     // - The central pathway | ||||||
|  |     // - The bottom border | ||||||
|  |     // The bottom border of one row is identical to the top border of the following row. We use this variable here in | ||||||
|  |     // order to be able to render the bottom border of the last row, which obviously does not have a following row with | ||||||
|  |     // a top border. | ||||||
|     private int line = 0; |     private int line = 0; | ||||||
| 
 | 
 | ||||||
|     boolean hasNext() { |     boolean hasNext() { | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ public class TextRenderer implements Renderer<String> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  |     @Override | ||||||
|     public String render(@NonNull final Labyrinth labyrinth) { |     public String render(@NonNull final Labyrinth labyrinth) { | ||||||
|         if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { |         if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { | ||||||
|             return ""; |             return ""; | ||||||
|  |  | ||||||
|  | @ -4,29 +4,42 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.Renderer; | import ch.fritteli.labyrinth.generator.renderer.Renderer; | ||||||
| import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; | ||||||
| import io.vavr.collection.List; | import io.vavr.collection.List; | ||||||
|  | import io.vavr.control.Option; | ||||||
|  | import io.vavr.control.Try; | ||||||
| import lombok.NonNull; | import lombok.NonNull; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
| 
 | 
 | ||||||
|  | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
| import java.nio.file.Files; | import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
|  | import java.util.NoSuchElementException; | ||||||
| 
 | 
 | ||||||
|  | @Slf4j | ||||||
| public class TextFileRenderer implements Renderer<List<Path>> { | public class TextFileRenderer implements Renderer<List<Path>> { | ||||||
|     @NonNull |     @NonNull | ||||||
|     private static final TextRenderer TEXT_RENDERER = TextRenderer.newInstance(); |     private static final TextRenderer TEXT_RENDERER = TextRenderer.newInstance(); | ||||||
|     private Path targetLabyrinthFile; |     @NonNull | ||||||
|     private Path targetSolutionFile; |     private Option<Path> targetLabyrinthFile; | ||||||
|  |     @NonNull | ||||||
|  |     private Option<Path> targetSolutionFile; | ||||||
| 
 | 
 | ||||||
|     private TextFileRenderer() { |     private TextFileRenderer() { | ||||||
|         try { |         this.targetLabyrinthFile = Try | ||||||
|             this.targetLabyrinthFile = Files.createTempFile("labyrinth_", ".txt"); |                 .of(() -> Files.createTempFile("labyrinth_", ".txt")) | ||||||
|             this.targetSolutionFile = this.targetLabyrinthFile.getParent().resolve( |                 .onFailure(ex -> log.error("Unable to set default target file", ex)) | ||||||
|                     this.targetLabyrinthFile.getFileName().toString().replace(".txt", "-solution.txt") |                 .toOption(); | ||||||
|             ); | 
 | ||||||
|         } catch (IOException e) { |         this.targetSolutionFile = this.targetLabyrinthFile.toTry() | ||||||
|             System.err.println("Unable to set default target file."); |                 .map(Path::getParent) | ||||||
|             e.printStackTrace(); |                 .flatMap(parent -> this.targetLabyrinthFile.toTry() | ||||||
|         } |                         .map(Path::getFileName) | ||||||
|  |                         .map(Path::toString) | ||||||
|  |                         .map(a -> a.replace(".txt", "-solution.txt")) | ||||||
|  |                         .map(parent::resolve)) | ||||||
|  |                 .onFailure(ex -> log.error("Unable to set default solution target file", ex)) | ||||||
|  |                 .toOption(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  | @ -35,38 +48,44 @@ public class TextFileRenderer implements Renderer<List<Path>> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean isTargetLabyrinthFileDefinedAndWritable() { |     public boolean isTargetLabyrinthFileDefinedAndWritable() { | ||||||
|         return this.targetLabyrinthFile != null && this.targetLabyrinthFile.toFile().canWrite(); |         return this.targetLabyrinthFile | ||||||
|  |                 .map(Path::toFile) | ||||||
|  |                 .exists(File::canWrite); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean isTargetSolutionFileDefinedAndWritable() { |     public boolean isTargetSolutionFileDefinedAndWritable() { | ||||||
|         return this.targetSolutionFile != null && this.targetSolutionFile.toFile().canWrite(); |         return this.targetSolutionFile | ||||||
|  |                 .map(Path::toFile) | ||||||
|  |                 .exists(File::canWrite); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public TextFileRenderer setTargetLabyrinthFile(@NonNull final Path targetLabyrinthFile) { |     public TextFileRenderer setTargetLabyrinthFile(@NonNull final Path targetLabyrinthFile) { | ||||||
|         this.targetLabyrinthFile = targetLabyrinthFile; |         this.targetLabyrinthFile = Option.of(targetLabyrinthFile); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public TextFileRenderer setTargetSolutionFile(@NonNull final Path targetSolutionFile) { |     public TextFileRenderer setTargetSolutionFile(@NonNull final Path targetSolutionFile) { | ||||||
|         this.targetSolutionFile = targetSolutionFile; |         this.targetSolutionFile = Option.of(targetSolutionFile); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public List<Path> render(@NonNull final Labyrinth labyrinth) { |     public List<Path> render(@NonNull final Labyrinth labyrinth) { | ||||||
|         if (!this.isTargetLabyrinthFileDefinedAndWritable()) { |         if (!this.isTargetLabyrinthFileDefinedAndWritable()) { | ||||||
|             try { |             try { | ||||||
|                 Files.createFile(this.targetLabyrinthFile); |                 Files.createFile(this.targetLabyrinthFile.get()); | ||||||
|             } catch (IOException e) { |             } catch (IOException | NoSuchElementException e) { | ||||||
|  |                 log.error("Cannot write to target labyrinth file.", e); | ||||||
|                 throw new IllegalArgumentException("Cannot write to target labyrinth file.", e); |                 throw new IllegalArgumentException("Cannot write to target labyrinth file.", e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (!this.isTargetSolutionFileDefinedAndWritable()) { |         if (!this.isTargetSolutionFileDefinedAndWritable()) { | ||||||
|             try { |             try { | ||||||
|                 Files.createFile(this.targetSolutionFile); |                 Files.createFile(this.targetSolutionFile.get()); | ||||||
|             } catch (IOException e) { |             } catch (IOException | NoSuchElementException e) { | ||||||
|                 throw new IllegalArgumentException("Cannot write to target solution file.", e); |                 throw new IllegalArgumentException("Cannot write to target solution file.", e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -76,13 +95,18 @@ public class TextFileRenderer implements Renderer<List<Path>> { | ||||||
|             text = TEXT_RENDERER.setRenderSolution(false).render(labyrinth).strip(); |             text = TEXT_RENDERER.setRenderSolution(false).render(labyrinth).strip(); | ||||||
|             solution = TEXT_RENDERER.setRenderSolution(true).render(labyrinth).strip(); |             solution = TEXT_RENDERER.setRenderSolution(true).render(labyrinth).strip(); | ||||||
|         } |         } | ||||||
|  |         final Path targetLabyrinthFile = this.targetLabyrinthFile.get(); | ||||||
|  |         final Path targetSolutionFile = this.targetSolutionFile.get(); | ||||||
|         try { |         try { | ||||||
|             Files.write(this.targetLabyrinthFile, text.getBytes(StandardCharsets.UTF_8)); |             Files.write(targetLabyrinthFile, text.getBytes(StandardCharsets.UTF_8)); | ||||||
|             Files.write(this.targetSolutionFile, solution.getBytes(StandardCharsets.UTF_8)); |  | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             System.err.println("Failed writing to file " + this.targetLabyrinthFile.normalize().toString()); |             log.error("Failed writing to file " + targetLabyrinthFile.normalize(), e); | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |         } | ||||||
|         return List.of(this.targetLabyrinthFile, this.targetSolutionFile); |         try { | ||||||
|  |             Files.write(targetSolutionFile, solution.getBytes(StandardCharsets.UTF_8)); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             log.error("Failed writing to file " + targetSolutionFile.normalize()); | ||||||
|  |         } | ||||||
|  |         return List.of(targetLabyrinthFile, targetSolutionFile); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | package ch.fritteli.labyrinth.generator.serialization; | ||||||
|  | 
 | ||||||
|  | import lombok.NonNull; | ||||||
|  | 
 | ||||||
|  | import java.io.ByteArrayInputStream; | ||||||
|  | 
 | ||||||
|  | public class LabyrinthInputStream extends ByteArrayInputStream { | ||||||
|  |     public LabyrinthInputStream(@NonNull final byte[] buf) { | ||||||
|  |         super(buf); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public byte readByte() { | ||||||
|  |         final int read = this.read(); | ||||||
|  |         if (read == -1) { | ||||||
|  |             // end of stream reached | ||||||
|  |             throw new ArrayIndexOutOfBoundsException("End of stream reached. Cannot read more bytes."); | ||||||
|  |         } | ||||||
|  |         return (byte) read; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int readInt() { | ||||||
|  |         int result = 0; | ||||||
|  |         result |= (0xff & this.readByte()) << 24; | ||||||
|  |         result |= (0xff & this.readByte()) << 16; | ||||||
|  |         result |= (0xff & this.readByte()) << 8; | ||||||
|  |         result |= 0xff & this.readByte(); | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public long readLong() { | ||||||
|  |         long result = 0; | ||||||
|  |         result |= ((long) this.readInt()) << 32; | ||||||
|  |         result |= 0xffffffffL & this.readInt(); | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | package ch.fritteli.labyrinth.generator.serialization; | ||||||
|  | 
 | ||||||
|  | import java.io.ByteArrayOutputStream; | ||||||
|  | 
 | ||||||
|  | public class LabyrinthOutputStream extends ByteArrayOutputStream { | ||||||
|  |     public void writeByte(final byte value) { | ||||||
|  |         this.write(value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void writeInt(final int value) { | ||||||
|  |         this.write(value >> 24); | ||||||
|  |         this.write(value >> 16); | ||||||
|  |         this.write(value >> 8); | ||||||
|  |         this.write(value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void writeLong(final long value) { | ||||||
|  |         this.writeInt((int) (value >> 32)); | ||||||
|  |         this.writeInt((int) value); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,208 @@ | ||||||
|  | package ch.fritteli.labyrinth.generator.serialization; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.labyrinth.generator.model.Direction; | ||||||
|  | import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||||
|  | import ch.fritteli.labyrinth.generator.model.Tile; | ||||||
|  | import lombok.NonNull; | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
|  | 
 | ||||||
|  | import java.lang.reflect.Constructor; | ||||||
|  | import java.lang.reflect.InvocationTargetException; | ||||||
|  | import java.util.EnumSet; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * <pre> | ||||||
|  |  * decimal hex bin  border | ||||||
|  |  *       0   0 0000 no border | ||||||
|  |  *       1   1 0001 top | ||||||
|  |  *       2   2 0010 right | ||||||
|  |  *       3   3 0011 top+right | ||||||
|  |  *       4   4 0100 bottom | ||||||
|  |  *       5   5 0101 top+bottom | ||||||
|  |  *       6   6 0110 right+bottom | ||||||
|  |  *       7   7 0111 top+right+bottom | ||||||
|  |  *       8   8 1000 left | ||||||
|  |  *       9   9 1001 top+left | ||||||
|  |  *      10   a 1010 right+left | ||||||
|  |  *      11   b 1011 top+right+left | ||||||
|  |  *      12   c 1100 bottom+left | ||||||
|  |  *      13   d 1101 top+bottom+left | ||||||
|  |  *      14   e 1110 right+bottom+left | ||||||
|  |  *      15   f 1111 top+right+bottom+left | ||||||
|  |  * </pre> | ||||||
|  |  * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls | ||||||
|  |  * ==> first bytes are: | ||||||
|  |  * <pre> | ||||||
|  |  *   byte  hex meaning | ||||||
|  |  *     00 0x1a magic | ||||||
|  |  *     01 0xb1 magic | ||||||
|  |  *     02 0x00 version (0x00 -> dev / unstable; will be bumped to 0x01 once stabilized) | ||||||
|  |  * 03..06      width (int) | ||||||
|  |  * 07..10      height (int) | ||||||
|  |  * 11..18      random seed number (long) | ||||||
|  |  * 19..        tiles | ||||||
|  |  * </pre> | ||||||
|  |  * exteaneous space (poss. last nibble) is ignored. | ||||||
|  |  */ | ||||||
|  | @UtilityClass | ||||||
|  | public class SerializerDeserializer { | ||||||
|  |     private final byte MAGIC_BYTE_1 = 0x1a; | ||||||
|  |     private final byte MAGIC_BYTE_2 = (byte) 0xb1; | ||||||
|  |     private final byte VERSION_BYTE = 0x01; | ||||||
|  | 
 | ||||||
|  |     private final byte TOP_BIT = 0b0000_0001; | ||||||
|  |     private final byte RIGHT_BIT = 0b0000_0010; | ||||||
|  |     private final byte BOTTOM_BIT = 0b0000_0100; | ||||||
|  |     private final byte LEFT_BIT = 0b0000_1000; | ||||||
|  |     private final byte SOLUTION_BIT = 0b0001_0000; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Serializes the {@code labyrinth} into a byte array. | ||||||
|  |      * | ||||||
|  |      * @param labyrinth The labyrinth to be serialized. | ||||||
|  |      * @return The resulting byte array. | ||||||
|  |      */ | ||||||
|  |     @NonNull | ||||||
|  |     public byte[] serialize(@NonNull final Labyrinth labyrinth) { | ||||||
|  |         final LabyrinthOutputStream stream = new LabyrinthOutputStream(); | ||||||
|  |         writeHeader(stream); | ||||||
|  |         writeLabyrinthData(stream, labyrinth); | ||||||
|  |         return stream.toByteArray(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Deserializes the byte array into an instance of {@link Labyrinth}. | ||||||
|  |      * | ||||||
|  |      * @param bytes The byte array to be deserialized. | ||||||
|  |      * @return An instance of {@link Labyrinth}. | ||||||
|  |      */ | ||||||
|  |     @NonNull | ||||||
|  |     public Labyrinth deserialize(@NonNull final byte[] bytes) { | ||||||
|  |         final LabyrinthInputStream stream = new LabyrinthInputStream(bytes); | ||||||
|  |         checkHeader(stream); | ||||||
|  |         return readLabyrinthData(stream); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void writeHeader(@NonNull final LabyrinthOutputStream stream) { | ||||||
|  |         stream.writeByte(MAGIC_BYTE_1); | ||||||
|  |         stream.writeByte(MAGIC_BYTE_2); | ||||||
|  |         stream.writeByte(VERSION_BYTE); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void checkHeader(@NonNull final LabyrinthInputStream stream) { | ||||||
|  |         final byte magic1 = stream.readByte(); | ||||||
|  |         if (magic1 != MAGIC_BYTE_1) { | ||||||
|  |             throw new IllegalArgumentException("Invalid labyrinth data."); | ||||||
|  |         } | ||||||
|  |         final byte magic2 = stream.readByte(); | ||||||
|  |         if (magic2 != MAGIC_BYTE_2) { | ||||||
|  |             throw new IllegalArgumentException("Invalid labyrinth data."); | ||||||
|  |         } | ||||||
|  |         final int version = stream.readByte(); | ||||||
|  |         if (version != VERSION_BYTE) { | ||||||
|  |             throw new IllegalArgumentException("Unknown Labyrinth data version: " + version); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void writeLabyrinthData(@NonNull final LabyrinthOutputStream stream, @NonNull final Labyrinth labyrinth) { | ||||||
|  |         final long randomSeed = labyrinth.getRandomSeed(); | ||||||
|  |         final int width = labyrinth.getWidth(); | ||||||
|  |         final int height = labyrinth.getHeight(); | ||||||
|  |         stream.writeLong(randomSeed); | ||||||
|  |         stream.writeInt(width); | ||||||
|  |         stream.writeInt(height); | ||||||
|  | 
 | ||||||
|  |         for (int y = 0; y < height; y++) { | ||||||
|  |             for (int x = 0; x < width; x++) { | ||||||
|  |                 // We .get() it, because we want to crash hard if it is not available. | ||||||
|  |                 final Tile tile = labyrinth.getTileAt(x, y).get(); | ||||||
|  |                 final byte bitmask = getBitmaskForTile(tile); | ||||||
|  |                 stream.writeByte(bitmask); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static Labyrinth readLabyrinthData(@NonNull final LabyrinthInputStream stream) { | ||||||
|  |         final long randomSeed = stream.readLong(); | ||||||
|  |         final int width = stream.readInt(); | ||||||
|  |         final int height = stream.readInt(); | ||||||
|  | 
 | ||||||
|  |         final Tile[][] tiles = new Tile[width][height]; | ||||||
|  |         for (int x = 0; x < width; x++) { | ||||||
|  |             tiles[x] = new Tile[height]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (int y = 0; y < height; y++) { | ||||||
|  |             for (int x = 0; x < width; x++) { | ||||||
|  |                 final byte bitmask = stream.readByte(); | ||||||
|  |                 tiles[x][y] = getTileForBitmask(bitmask); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return createLabyrinth(tiles, width, height, randomSeed); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private Labyrinth createLabyrinth(@NonNull final Tile[][] field, final int width, final int height, final long randomSeed) { | ||||||
|  |         try { | ||||||
|  |             @NonNull final Constructor<Labyrinth> constructor = Labyrinth.class.getDeclaredConstructor(Tile[][].class, Integer.TYPE, Integer.TYPE, Long.TYPE); | ||||||
|  |             constructor.setAccessible(true); | ||||||
|  |             return constructor.newInstance(field, width, height, randomSeed); | ||||||
|  |         } catch (final NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { | ||||||
|  |             throw new RuntimeException("Can not deserialize Labyrinth from labyrinth data.", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private Tile createTile(@NonNull final EnumSet<Direction> walls, boolean solution) { | ||||||
|  |         try { | ||||||
|  |             @NonNull final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE); | ||||||
|  |             constructor.setAccessible(true); | ||||||
|  |             return constructor.newInstance(walls, solution); | ||||||
|  |         } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { | ||||||
|  |             throw new RuntimeException("Can not deserialize Tile from labyrinth data.", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private byte getBitmaskForTile(@NonNull final Tile tile) { | ||||||
|  |         byte bitmask = 0; | ||||||
|  |         if (tile.hasWallAt(Direction.TOP)) { | ||||||
|  |             bitmask |= TOP_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.hasWallAt(Direction.RIGHT)) { | ||||||
|  |             bitmask |= RIGHT_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.hasWallAt(Direction.BOTTOM)) { | ||||||
|  |             bitmask |= BOTTOM_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.hasWallAt((Direction.LEFT))) { | ||||||
|  |             bitmask |= LEFT_BIT; | ||||||
|  |         } | ||||||
|  |         if (tile.isSolution()) { | ||||||
|  |             bitmask |= SOLUTION_BIT; | ||||||
|  |         } | ||||||
|  |         return bitmask; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private Tile getTileForBitmask(final byte bitmask) { | ||||||
|  |         final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class); | ||||||
|  |         if ((bitmask & TOP_BIT) == TOP_BIT) { | ||||||
|  |             walls.add(Direction.TOP); | ||||||
|  |         } | ||||||
|  |         if ((bitmask & RIGHT_BIT) == RIGHT_BIT) { | ||||||
|  |             walls.add(Direction.RIGHT); | ||||||
|  |         } | ||||||
|  |         if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) { | ||||||
|  |             walls.add(Direction.BOTTOM); | ||||||
|  |         } | ||||||
|  |         if ((bitmask & LEFT_BIT) == LEFT_BIT) { | ||||||
|  |             walls.add(Direction.LEFT); | ||||||
|  |         } | ||||||
|  |         final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT; | ||||||
|  |         return createTile(walls, solution); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <module type="JAVA_MODULE" version="4"> |  | ||||||
|   <component name="NewModuleRootManager" inherit-compiler-output="true"> |  | ||||||
|     <exclude-output /> |  | ||||||
|     <content url="file://$MODULE_DIR$"> |  | ||||||
|       <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="false" /> |  | ||||||
|     </content> |  | ||||||
|     <orderEntry type="inheritedJdk" /> |  | ||||||
|     <orderEntry type="sourceFolder" forTests="false" /> |  | ||||||
|   </component> |  | ||||||
| </module> |  | ||||||
							
								
								
									
										16
									
								
								src/main/resources/logback.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/main/resources/logback.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8" ?> | ||||||
|  | <configuration> | ||||||
|  |     <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> | ||||||
|  | 
 | ||||||
|  |     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||||||
|  |         <!-- encoders are by default assigned the type | ||||||
|  |         ch.qos.logback.classic.encoder.PatternLayoutEncoder --> | ||||||
|  |         <encoder> | ||||||
|  |             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||||||
|  |         </encoder> | ||||||
|  |     </appender> | ||||||
|  | 
 | ||||||
|  |     <root level="info"> | ||||||
|  |         <appender-ref ref="STDOUT"/> | ||||||
|  |     </root> | ||||||
|  | </configuration> | ||||||
|  | @ -44,4 +44,16 @@ class CharDefinitionTest { | ||||||
|         assertEquals("┤", new CharDefinition(true, true, true, false, true).toString()); |         assertEquals("┤", new CharDefinition(true, true, true, false, true).toString()); | ||||||
|         assertEquals("┼", new CharDefinition(true, true, true, true, true).toString()); |         assertEquals("┼", new CharDefinition(true, true, true, true, true).toString()); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testBuilderMethods() { | ||||||
|  |         assertEquals(new CharDefinition(true, false, false, false, false), new CharDefinition().up()); | ||||||
|  |         assertEquals(new CharDefinition(false, true, false, false, false), new CharDefinition().down()); | ||||||
|  |         assertEquals(new CharDefinition(false, false, true, false, false), new CharDefinition().left()); | ||||||
|  |         assertEquals(new CharDefinition(false, false, false, true, false), new CharDefinition().right()); | ||||||
|  |         assertEquals(new CharDefinition(false, false, false, false, true), new CharDefinition().solution()); | ||||||
|  |         assertEquals(new CharDefinition(true, true, false, false, false), new CharDefinition().vertical()); | ||||||
|  |         assertEquals(new CharDefinition(false, false, true, true, false), new CharDefinition().horizontal()); | ||||||
|  |         assertEquals(new CharDefinition(true, true, true, true, true), new CharDefinition().vertical().horizontal().solution()); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,33 @@ | ||||||
|  | package ch.fritteli.labyrinth.generator.serialization; | ||||||
|  | 
 | ||||||
|  | import ch.fritteli.labyrinth.generator.model.Labyrinth; | ||||||
|  | import lombok.NonNull; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
|  | 
 | ||||||
|  | class SerializerDeserializerTest { | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeTiny() { | ||||||
|  |         @NonNull final Labyrinth expected = new Labyrinth(2, 2, 255); | ||||||
|  |         @NonNull final byte[] bytes = SerializerDeserializer.serialize(expected); | ||||||
|  |         @NonNull final Labyrinth result = SerializerDeserializer.deserialize(bytes); | ||||||
|  |         assertEquals(expected, result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeMedium() { | ||||||
|  |         @NonNull final Labyrinth expected = new Labyrinth(20, 20, -271828182846L); | ||||||
|  |         @NonNull final byte[] bytes = SerializerDeserializer.serialize(expected); | ||||||
|  |         @NonNull final Labyrinth result = SerializerDeserializer.deserialize(bytes); | ||||||
|  |         assertEquals(expected, result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testSerializeDeserializeLarge() { | ||||||
|  |         @NonNull final Labyrinth expected = new Labyrinth(200, 320, 3141592653589793238L); | ||||||
|  |         @NonNull final byte[] bytes = SerializerDeserializer.serialize(expected); | ||||||
|  |         @NonNull final Labyrinth result = SerializerDeserializer.deserialize(bytes); | ||||||
|  |         assertEquals(expected, result); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <module type="JAVA_MODULE" version="4"> |  | ||||||
|   <component name="NewModuleRootManager" inherit-compiler-output="true"> |  | ||||||
|     <exclude-output /> |  | ||||||
|     <content url="file://$MODULE_DIR$"> |  | ||||||
|       <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="true" /> |  | ||||||
|     </content> |  | ||||||
|     <orderEntry type="inheritedJdk" /> |  | ||||||
|     <orderEntry type="sourceFolder" forTests="false" /> |  | ||||||
|   </component> |  | ||||||
| </module> |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue