diff --git a/src/main/java/eu/m724/crossword/Crossword.java b/src/main/java/eu/m724/crossword/Crossword.java index 15179a9..a0fbea4 100644 --- a/src/main/java/eu/m724/crossword/Crossword.java +++ b/src/main/java/eu/m724/crossword/Crossword.java @@ -11,7 +11,7 @@ package eu.m724.crossword; */ public record Crossword( int width, int height, - String solution, + Solution solution, PlacedWord[] words ) { /** diff --git a/src/main/java/eu/m724/crossword/CrosswordBuilder.java b/src/main/java/eu/m724/crossword/CrosswordBuilder.java index 0fc3ca4..1295073 100644 --- a/src/main/java/eu/m724/crossword/CrosswordBuilder.java +++ b/src/main/java/eu/m724/crossword/CrosswordBuilder.java @@ -10,22 +10,25 @@ import java.util.Set; */ public class CrosswordBuilder { private final int width, height; - private final String solution; + private final String solutionText; + private final Set words = new HashSet<>(); private final Set placedWords = new HashSet<>(); + private Solution solution = null; + /** * Creates a new {@link CrosswordBuilder} * * @param width The width of the crossword * @param height The height of the crossword - * @param solution The solution + * @param solutionText The solution text */ - public CrosswordBuilder(int width, int height, String solution) { + public CrosswordBuilder(int width, int height, String solutionText) { this.width = width; this.height = height; - this.solution = solution.toLowerCase(); - System.out.printf("Initialized builder of %dx%d with solution \"%s\"\n", width, height, solution); + this.solutionText = solutionText.toLowerCase(); + System.out.printf("Initialized builder of %dx%d with solution \"%s\"\n", width, height, solutionText); } /** @@ -46,8 +49,11 @@ public class CrosswordBuilder { * @return This exact {@link CrosswordBuilder} */ public CrosswordBuilder addWord(Word word) { - this.words.add(word); - System.out.println("Word added: " + word.word()); + System.out.println("Adding word: " + word.text()); + if (word.length() >= Math.max(width, height)) + System.out.printf("A word is too long and can't be added. Increase crossword size or shorten this word. (%d > %d) %s\n", word.length(), Math.min(width, height) - 1, word.text()); + else + this.words.add(word); return this; } @@ -58,15 +64,43 @@ public class CrosswordBuilder { * @return This exact {@link CrosswordBuilder} */ public CrosswordBuilder addWords(Word... words) { - this.words.addAll(Set.of(words)); - System.out.println("Multiple words added: " + String.join(", ", Arrays.stream(words).map(Word::word).toList())); + System.out.println("Adding multiple words: " + String.join(", ", Arrays.stream(words).map(Word::text).toList())); + for (Word word : words) { + if (word.length() >= Math.min(width, height)) + System.out.println("A word is too long and can't be added. Increase crossword size or shorten this word. " + word.text()); + else + this.words.add(word); + } return this; } + private Generator getGenerator() { + return new Generator(width, height, solutionText, words.toArray(Word[]::new)); + } + + public CrosswordBuilder generate(long seed) { + return generate(getGenerator().seed(seed)); + } + public CrosswordBuilder generate() { + return generate(getGenerator()); + } + + private CrosswordBuilder generate(Generator generator) { System.out.println("Generator invoked"); + if (generator == null) + generator = getGenerator(); + // TODO perhaps making this an assignment and making placedWords not final is better - placedWords.addAll(Generator.generate(width, height, solution, words.toArray(Word[]::new))); + Set g = generator.generate(); + if (g == null) { + throw new RuntimeException("Unable to find solution. Perhaps the crossword is too small for the words?"); + } + + placedWords.addAll(g); + // TODO improve this too + this.solution = generator.solutionFinder(placedWords); + System.out.println("Generator done"); return this; } @@ -75,23 +109,35 @@ public class CrosswordBuilder { * Builds a {@link Crossword} * * @throws SizeMismatchException When not all words have been placed. Or too many, for some reason. + * @throws NoSolutionException if no solution * @return The {@link Crossword} */ - public Crossword build() throws SizeMismatchException { // TODO maybe it should be unchecked + public Crossword build() { System.out.println("Building"); // TODO + if (words.size() != placedWords.size()) { throw new SizeMismatchException("Words: %d, placed: %d".formatted(words.size(), placedWords.size())); } + + if (solution == null) { + throw new NoSolutionException(); + } + return new Crossword(width, height, solution, placedWords.toArray(PlacedWord[]::new)); } /** * When not all words have been placed. Or too many, for some reason. */ - public static class SizeMismatchException extends Exception { + public static class SizeMismatchException extends RuntimeException { public SizeMismatchException(String message) { super(message); } } + + /** + * If a solution hasn't been found; + */ + public static class NoSolutionException extends RuntimeException { } } diff --git a/src/main/java/eu/m724/crossword/Generator.java b/src/main/java/eu/m724/crossword/Generator.java index 4985798..995e7ba 100644 --- a/src/main/java/eu/m724/crossword/Generator.java +++ b/src/main/java/eu/m724/crossword/Generator.java @@ -1,88 +1,202 @@ package eu.m724.crossword; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import java.util.concurrent.ThreadLocalRandom; public class Generator { private final int width, height; - private final String solution; + private final String solutionText; private final Word[] words; - private final char[][] charArray; - private final Set placedWords; + //private final char[][] charArray; + //private final Set placedWords; - private final ThreadLocalRandom random = ThreadLocalRandom.current(); + //private final ThreadLocalRandom random = ThreadLocalRandom.current(); + private long seed = ThreadLocalRandom.current().nextLong(); + private final Random random = new Random(seed); - private Generator(int width, int height, String solution, Word[] words) { + private final List possiblePlacements = new ArrayList<>(); // TODO does this belong here + private int peakDepth; // TODO useless? + + public Generator(int width, int height, String solutionText, Word[] words) { this.width = width; this.height = height; - this.solution = solution; + this.solutionText = solutionText; this.words = words; - this.charArray = new char[width][height]; - this.placedWords = new HashSet<>(words.length); + //this.charArray = new char[width][height]; + //this.placedWords = new HashSet<>(words.length); } - public static Set generate(int width, int height, String solution, Word[] words) { - return new Generator(width, height, solution, words).generate(); + public Generator seed(long seed) { + this.seed = seed; + random.setSeed(seed); + + return this; } - private Set generate() { + public Solution solutionFinder(Set placedWords) { + System.out.printf("Solution finder running with %d placed words\n", placedWords.size()); + long start = System.nanoTime(); + + List coordinatesList = new ArrayList<>(solutionText.length()); + + List placedWordList; + + charLoop: + for (int i=0; i(placedWords); + shuffle(placedWordList); + + for (PlacedWord placedWord : placedWordList) { + String wordText = placedWord.word().text(); + int charIndex = wordText.indexOf(wantedChar); + Vec coordinates = placedWord.coordinatesOf(charIndex); + + if (charIndex != -1 && !coordinatesList.contains(coordinates)) { + coordinatesList.add(coordinates); + continue charLoop; + } + } + + throw new IllegalStateException("No word has character %c".formatted(wantedChar)); + } + + System.out.printf("Solution found in %fms\n", (System.nanoTime() - start) / 1000000.0); + + return new Solution(solutionText, coordinatesList.toArray(Vec[]::new)); + } + + public Set generate() { System.out.printf("Generator running with %d words\n", words.length); long start = System.nanoTime(); - boolean vertical = random.nextBoolean(); - for (Word word : words) { - PlacedWord placedWord; + for (int x=0; x currentPossiblePlacements = new ArrayList<>(possiblePlacements); + Collections.shuffle(currentPossiblePlacements); + + pickLoop: + for (int i=0; i<2; i++) { // repeating twice to pass horizontal and vertical + vertical = !vertical; + + for (Vec placement : currentPossiblePlacements) { + + if (vertical) { + int y = placement.y(); + if (y == 0 || y + word.length() >= height) continue; + } else { + int x = placement.x(); + if (x == 0 || x + word.length() >= width) continue; + } + + PlacedWord candidate = new PlacedWord(placement, vertical, word); + if (canPlace(candidate)) { + placedWord = candidate; + break pickLoop; + } + } + } + + if (placedWord == null) + return null; // TODO remove problems placeWord(placedWord); // TODO rename + }*/ - vertical = !vertical; + Set placedWords = follow(new Grid(width, height), new HashSet<>(), 0); + System.out.println("Explored depth: " + peakDepth + " / " + words.length); + + System.out.println("Seed: " + seed); + + double ms = (System.nanoTime() - start) / 1000000.0; + System.out.printf("Generation took %fms", ms); + + if (ms > 30000) { + System.out.println(". That's a very long time!"); + } else { + System.out.println(); } - System.out.printf("Generation took %fms\n", (System.nanoTime() - start) / 1000000.0); - return placedWords; } - private void placeWord(PlacedWord word) { - int x = word.x(); - int y = word.y(); + private Set follow(Grid grid, Set placedChain, int wordIndex) { + System.out.println("Depth " + wordIndex); - placedWords.add(word); + if (wordIndex > peakDepth) + peakDepth = wordIndex; - for (int i=0; i currentPossiblePlacements = new ArrayList<>(possiblePlacements); + shuffle(currentPossiblePlacements); + //Collections.shuffle(currentPossiblePlacements); - if (charArray[x][y] == 0 || charArray[x][y] == c) { - charArray[x][y] = c; + boolean vertical = random.nextBoolean(); + Word word = words[wordIndex]; + + for (int i=0; i<2; i++) { // repeating twice to pass horizontal and vertical + vertical = !vertical; + + for (Vec placement : currentPossiblePlacements) { + if (vertical) { + int y = placement.y(); + if (y == 0 || y + word.length() >= height) continue; + } else { + int x = placement.x(); + if (x == 0 || x + word.length() >= width) continue; + } + + PlacedWord candidate = new PlacedWord(placement, vertical, word); + + if (grid.canPlace(candidate)) { + System.out.println(" Found candidate"); + Set potentialChain = new HashSet<>(placedChain); + potentialChain.add(candidate); + + Grid newGrid = grid.clone(); + newGrid.placeWord(candidate); + + Set newChain = follow(newGrid, potentialChain, wordIndex + 1); + System.out.println("Back to depth " + wordIndex); + + // if it's null it means there's no good placement, and we should continue searching at this depth + if (newChain != null) { + System.out.println(" Unfolding"); // is that a correct word? + return newChain; + } else { + System.out.println(" Looking further"); + } + } } } + + System.out.println(" No solution"); + // no placement at this depth + return null; } - private boolean canPlace(PlacedWord word) { - int x = word.x(); - int y = word.y(); - - for (int i=0; i void shuffle(List list) { + for (int i = list.size() - 1; i > 0; i--) { + int index = random.nextInt(i + 1); + // Swap elements + T temp = list.get(index); + list.set(index, list.get(i)); + list.set(i, temp); } - - return true; } } diff --git a/src/main/java/eu/m724/crossword/Grid.java b/src/main/java/eu/m724/crossword/Grid.java new file mode 100644 index 0000000..6fc228b --- /dev/null +++ b/src/main/java/eu/m724/crossword/Grid.java @@ -0,0 +1,93 @@ +package eu.m724.crossword; + +public class Grid implements Cloneable { + private char[][] charArray; + + public Grid(int width, int height) { + this.charArray = new char[width][height]; + } + + public char getCharAt(int x, int y) { + return charArray[x][y]; + } + + /** + * Place a word.
+ * Whether it can be placed is not checked. + * + * @param placedWord The word to place + */ + public void placeWord(PlacedWord placedWord) { + int x = placedWord.pos().x(); + int y = placedWord.pos().y(); + + int _x = x; + int _y = y; + + for (int i=-1; i< placedWord.length(); i++) { // -1 for hint + if (placedWord.vertical()) { + _y = y + i; + } else { + _x = x + i; + } + + char c = Character.MAX_VALUE - 1; // it would be a bad idea to assign a different id to every hint + if (i != -1) { + c = placedWord.word().text().charAt(i); + } + + if (charArray[_x][_y] == 0 || charArray[_x][_y] == c) { + charArray[_x][_y] = c; + } + } + } + + /** + * Can a word be placed, will it not collide + * + * @param word The word + * @return can it be placed + */ + public boolean canPlace(PlacedWord word) { + int x = word.pos().x(); + int y = word.pos().y(); + + int _x = x; + int _y = y; + + for (int i=-1; i solutionCoordinates = Arrays.asList(crossword.solution().coordinates()); + for (PlacedWord word : crossword.words()) { - int x = word.x(); - int y = word.y(); + int x = word.pos().x(); + int y = word.pos().y(); String hint = word.word().hint(); - if (word.vertical()) { + if (word.vertical()) { // TODO squash that drawStringFit(g2, x * tileSize, (y - 1) * tileSize, hint); + + int[] xPoints = new int[] { + toPos(x + 0.25), + toPos(x + 0.75), + toPos(x + 0.5) + }; + int[] yPoints = new int[] { + toPos(y - 1 + 0.7), + toPos(y - 1 + 0.7), + toPos(y - 1 + 0.9) + }; + g2.drawPolygon(xPoints, yPoints, 3); for (int i=0; i * The word should be lowercase. TODO make that not a requirement * - * @param word the word + * @param text the word text * @param hint the hint / clue */ public record Word( - String word, + String text, String hint ) { public int length() { - return word.length(); + return text.length(); } }