Refactoring

This commit is contained in:
Minecon724 2024-10-05 16:44:02 +02:00
parent 661452d747
commit 472106ba3a
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
10 changed files with 429 additions and 106 deletions

View file

@ -11,7 +11,7 @@ package eu.m724.crossword;
*/ */
public record Crossword( public record Crossword(
int width, int height, int width, int height,
String solution, Solution solution,
PlacedWord[] words PlacedWord[] words
) { ) {
/** /**

View file

@ -10,22 +10,25 @@ import java.util.Set;
*/ */
public class CrosswordBuilder { public class CrosswordBuilder {
private final int width, height; private final int width, height;
private final String solution; private final String solutionText;
private final Set<Word> words = new HashSet<>(); private final Set<Word> words = new HashSet<>();
private final Set<PlacedWord> placedWords = new HashSet<>(); private final Set<PlacedWord> placedWords = new HashSet<>();
private Solution solution = null;
/** /**
* Creates a new {@link CrosswordBuilder} * Creates a new {@link CrosswordBuilder}
* *
* @param width The width of the crossword * @param width The width of the crossword
* @param height The height 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.width = width;
this.height = height; this.height = height;
this.solution = solution.toLowerCase(); this.solutionText = solutionText.toLowerCase();
System.out.printf("Initialized builder of %dx%d with solution \"%s\"\n", width, height, solution); 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} * @return This exact {@link CrosswordBuilder}
*/ */
public CrosswordBuilder addWord(Word word) { public CrosswordBuilder addWord(Word word) {
this.words.add(word); System.out.println("Adding word: " + word.text());
System.out.println("Word added: " + word.word()); 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; return this;
} }
@ -58,15 +64,43 @@ public class CrosswordBuilder {
* @return This exact {@link CrosswordBuilder} * @return This exact {@link CrosswordBuilder}
*/ */
public CrosswordBuilder addWords(Word... words) { public CrosswordBuilder addWords(Word... words) {
this.words.addAll(Set.of(words)); System.out.println("Adding multiple words: " + String.join(", ", Arrays.stream(words).map(Word::text).toList()));
System.out.println("Multiple words added: " + String.join(", ", Arrays.stream(words).map(Word::word).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; 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() { public CrosswordBuilder generate() {
return generate(getGenerator());
}
private CrosswordBuilder generate(Generator generator) {
System.out.println("Generator invoked"); System.out.println("Generator invoked");
if (generator == null)
generator = getGenerator();
// TODO perhaps making this an assignment and making placedWords not final is better // 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<PlacedWord> 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"); System.out.println("Generator done");
return this; return this;
} }
@ -75,23 +109,35 @@ public class CrosswordBuilder {
* Builds a {@link Crossword} * Builds a {@link Crossword}
* *
* @throws SizeMismatchException When not all words have been placed. Or too many, for some reason. * @throws SizeMismatchException When not all words have been placed. Or too many, for some reason.
* @throws NoSolutionException if no solution
* @return The {@link Crossword} * @return The {@link Crossword}
*/ */
public Crossword build() throws SizeMismatchException { // TODO maybe it should be unchecked public Crossword build() {
System.out.println("Building"); System.out.println("Building");
// TODO // TODO
if (words.size() != placedWords.size()) { if (words.size() != placedWords.size()) {
throw new SizeMismatchException("Words: %d, placed: %d".formatted(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)); return new Crossword(width, height, solution, placedWords.toArray(PlacedWord[]::new));
} }
/** /**
* When not all words have been placed. Or too many, for some reason. * 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) { public SizeMismatchException(String message) {
super(message); super(message);
} }
} }
/**
* If a solution hasn't been found;
*/
public static class NoSolutionException extends RuntimeException { }
} }

View file

@ -1,88 +1,202 @@
package eu.m724.crossword; package eu.m724.crossword;
import java.util.HashSet; import java.util.*;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
public class Generator { public class Generator {
private final int width, height; private final int width, height;
private final String solution; private final String solutionText;
private final Word[] words; private final Word[] words;
private final char[][] charArray; //private final char[][] charArray;
private final Set<PlacedWord> placedWords; //private final Set<PlacedWord> 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<Vec> 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.width = width;
this.height = height; this.height = height;
this.solution = solution; this.solutionText = solutionText;
this.words = words; this.words = words;
this.charArray = new char[width][height]; //this.charArray = new char[width][height];
this.placedWords = new HashSet<>(words.length); //this.placedWords = new HashSet<>(words.length);
} }
public static Set<PlacedWord> generate(int width, int height, String solution, Word[] words) { public Generator seed(long seed) {
return new Generator(width, height, solution, words).generate(); this.seed = seed;
random.setSeed(seed);
return this;
} }
private Set<PlacedWord> generate() { public Solution solutionFinder(Set<PlacedWord> placedWords) {
System.out.printf("Solution finder running with %d placed words\n", placedWords.size());
long start = System.nanoTime();
List<Vec> coordinatesList = new ArrayList<>(solutionText.length());
List<PlacedWord> placedWordList;
charLoop:
for (int i=0; i<solutionText.length(); i++) {
char wantedChar = solutionText.charAt(i);
placedWordList = new ArrayList<>(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<PlacedWord> generate() {
System.out.printf("Generator running with %d words\n", words.length); System.out.printf("Generator running with %d words\n", words.length);
long start = System.nanoTime(); long start = System.nanoTime();
boolean vertical = random.nextBoolean(); for (int x=0; x<width; x++) {
for (Word word : words) { for (int y=0; y<height; y++) {
PlacedWord placedWord; possiblePlacements.add(new Vec(x, y));
}
}
System.out.println("Possible placements: " + possiblePlacements.size() + " * 2");
do { /*for (Word word : words) {
int x = random.nextInt(1, width - (!vertical ? word.length() : 0)); PlacedWord placedWord = null;
int y = random.nextInt(1, height - (vertical ? word.length() : 0));
placedWord = new PlacedWord(x, y, vertical, word); List<Vec> currentPossiblePlacements = new ArrayList<>(possiblePlacements);
} while (!canPlace(placedWord)); 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 placeWord(placedWord); // TODO rename
}*/
vertical = !vertical; Set<PlacedWord> 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; return placedWords;
} }
private void placeWord(PlacedWord word) { private Set<PlacedWord> follow(Grid grid, Set<PlacedWord> placedChain, int wordIndex) {
int x = word.x(); System.out.println("Depth " + wordIndex);
int y = word.y();
placedWords.add(word); if (wordIndex > peakDepth)
peakDepth = wordIndex;
for (int i=0; i<word.length(); i++) { if (wordIndex == words.length) {
if (word.vertical()) { y++; } else { x++; } System.out.println(" Completed");
return placedChain;
}
char c = word.word().word().charAt(i); // TODO List<Vec> currentPossiblePlacements = new ArrayList<>(possiblePlacements);
shuffle(currentPossiblePlacements);
//Collections.shuffle(currentPossiblePlacements);
if (charArray[x][y] == 0 || charArray[x][y] == c) { boolean vertical = random.nextBoolean();
charArray[x][y] = c; 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<PlacedWord> potentialChain = new HashSet<>(placedChain);
potentialChain.add(candidate);
Grid newGrid = grid.clone();
newGrid.placeWord(candidate);
Set<PlacedWord> 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) { private <T> void shuffle(List<T> list) {
int x = word.x(); for (int i = list.size() - 1; i > 0; i--) {
int y = word.y(); int index = random.nextInt(i + 1);
// Swap elements
for (int i=0; i<word.length(); i++) { T temp = list.get(index);
if (word.vertical()) { y++; } else { x++; } list.set(index, list.get(i));
list.set(i, temp);
char c = word.word().word().charAt(i); // TODO
if (charArray[x][y] != 0 && charArray[x][y] != c) {
return false;
}
} }
return true;
} }
} }

View file

@ -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.<br>
* 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<word.length(); i++) { // -1 for hint
if (word.vertical()) {
_y = y + i;
} else {
_x = x + i;
}
char c = Character.MAX_VALUE; // hint
if (i != -1) {
c = word.word().text().charAt(i);
}
if (charArray[_x][_y] != 0 && charArray[_x][_y] != c) {
return false;
}
}
return true;
}
@Override
public Grid clone() {
try {
Grid grid = (Grid) super.clone();
grid.charArray = charArray.clone();
for (int i=0; i<charArray.length; i++) {
grid.charArray[i] = charArray[i].clone();
} // so when I .clone() a 2d array the objects inside it are not cloned so that's a problem. I struggled with it for several hours. Claude found the solution.
return grid;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
}

View file

@ -1,39 +1,24 @@
package eu.m724.crossword; package eu.m724.crossword;
public class Main { public class Main {
public static void main(String[] args) throws CrosswordBuilder.SizeMismatchException { public static void main(String[] args) {
CrosswordBuilder builder = new CrosswordBuilder(20, 20, "ardour") CrosswordBuilder builder = new CrosswordBuilder(12, 12, "crossword")
.addWord("cat", "Furry feline pet") .addWord("cat", "Furry feline pet")
.addWord("sun", "Bright star in our sky") .addWord("owl", "Nocturnal bird of prey")
.addWord("book", "Bound pages with a story") .addWord("kite", "Flying toy on a string")
.addWord("tree", "Woody plant with branches") .addWord("moon", "Earth's natural satellite")
.addWord("blue", "Color of a clear sky") .addWord("train", "Rail transport vehicle")
.addWord("door", "Entrance to a room") .addWord("cloud", "Visible mass of water droplets")
.addWord("ocean", "Vast body of saltwater") .addWord("camera", "Device for taking photos")
.addWord("mountain", "Large natural elevation of Earth's surface") .addWord("locket", "Small ornamental case")
.addWord("computer", "Electronic device for processing data") .addWord("rainbow", "Multicolored arc in the sky")
.addWord("music", "Art of arranging sounds in time") .addWord("volcano", "Erupting mountain")
.addWord("bicycle", "Two-wheeled vehicle powered by pedaling") .addWord("elevator", "Vertical transport device")
.addWord("flower", "Reproductive structure of flowering plants") .addWord("pineapple", "Tropical fruit with spiky skin")
.addWord("rain", "Water droplets falling from the sky") .addWord("submarine", "Underwater military vessel")
.addWord("camera", "Device for capturing images or videos") .addWord("sunflower", "Tall yellow-petaled plant")
.addWord("pizza", "Flat bread topped with sauce and cheese") .addWord("helicopter", "Rotary-wing aircraft")
.addWord("telescope", "Optical instrument for viewing distant objects") .addWord("rhinoceros", "Large horned African mammal")
.addWord("volcano", "Mountain that erupts molten rock")
.addWord("galaxy", "Vast collection of stars and cosmic matter")
.addWord("umbrella", "Device for protection against rain or sun")
.addWord("microscope", "Instrument for viewing tiny objects")
.addWord("butterfly", "Insect with large, colorful wings")
.addWord("saxophone", "Brass musical instrument with a reed")
.addWord("democracy", "System of government by the people")
.addWord("calendar", "System for organizing days and dates")
.addWord("tornado", "Violently rotating column of air")
.addWord("origami", "Japanese art of paper folding")
.addWord("kangaroo", "Large hopping marsupial from Australia")
.addWord("lighthouse", "Tower with light to guide ships")
.addWord("cactus", "Desert plant with spines")
.addWord("ballet", "Classical form of dance")
.addWord("pyramid", "Ancient structure with triangular sides")
.generate(); .generate();
Crossword crossword = builder.build(); Crossword crossword = builder.build();

View file

@ -3,18 +3,30 @@ package eu.m724.crossword;
/** /**
* Represents a placement of a word * Represents a placement of a word
* *
* @param x x of the first letter * @param pos Position of the first letter
* @param y y of the first letter * @param vertical Is the word, from the first letter, going downwards (true), or rightwards (false)
* @param vertical is the word, from the first letter, downwards (true), or rightwards (false) * @param word The word
* @param word the word
*/ */
public record PlacedWord( public record PlacedWord(
int x, Vec pos,
int y,
boolean vertical, boolean vertical,
Word word Word word
) { ) {
public int length() { public int length() {
return word.word().length(); return word.length();
}
/**
* Coordinates of nth character
*
* @param index character index, starting from 0
* @return the coordinates of the character
*/
public Vec coordinatesOf(int index) {
if (vertical) {
return pos.plus(0, index);
} else {
return pos.plus(index, 0);
}
} }
} }

View file

@ -5,6 +5,8 @@ import org.jfree.svg.SVGGraphics2D;
import java.awt.*; import java.awt.*;
import java.awt.font.FontRenderContext; import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List;
public class Renderer { public class Renderer {
private final int tileSize; private final int tileSize;
@ -13,26 +15,63 @@ public class Renderer {
this.tileSize = tileSize; this.tileSize = tileSize;
} }
public String render(Crossword crossword) { public String render(Crossword crossword) { // TODO split this
SVGGraphics2D g2 = new SVGGraphics2D(crossword.width() * tileSize, crossword.height() * tileSize); SVGGraphics2D g2 = new SVGGraphics2D(crossword.width() * tileSize, crossword.height() * tileSize);
g2.setPaint(Color.BLACK); g2.setPaint(Color.BLACK);
List<Vec> solutionCoordinates = Arrays.asList(crossword.solution().coordinates());
for (PlacedWord word : crossword.words()) { for (PlacedWord word : crossword.words()) {
int x = word.x(); int x = word.pos().x();
int y = word.y(); int y = word.pos().y();
String hint = word.word().hint(); String hint = word.word().hint();
if (word.vertical()) { if (word.vertical()) { // TODO squash that
drawStringFit(g2, x * tileSize, (y - 1) * tileSize, hint); 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<word.length(); i++) { for (int i=0; i<word.length(); i++) {
int solIndex;
if ((solIndex = solutionCoordinates.indexOf(new Vec(x, y + i))) != -1) {
solutionCoordinates.set(solIndex, null);
g2.drawString(Integer.toString(solIndex + 1), x * tileSize + 5, (y + i) * tileSize + 15); // TODO align
}
g2.draw(new Rectangle(x * tileSize, (y + i) * tileSize, tileSize, tileSize)); g2.draw(new Rectangle(x * tileSize, (y + i) * tileSize, tileSize, tileSize));
} }
} else { } else {
drawStringFit(g2, (x - 1) * tileSize, y * tileSize, hint); drawStringFit(g2, (x - 1) * tileSize, y * tileSize, hint);
int[] xPoints = new int[] {
toPos(x - 1 + 0.7),
toPos(x - 1 + 0.7),
toPos(x - 1 + 0.9)
};
int[] yPoints = new int[] {
toPos(y + 0.25),
toPos(y + 0.75),
toPos(y + 0.5)
};
g2.drawPolygon(xPoints, yPoints, 3);
for (int i=0; i<word.length(); i++) { for (int i=0; i<word.length(); i++) {
int solIndex;
if ((solIndex = solutionCoordinates.indexOf(new Vec(x + i, y))) != -1) {
solutionCoordinates.set(solIndex, null);
g2.drawString(Integer.toString(solIndex + 1), (x + i) * tileSize + 5, y * tileSize + 15);
}
g2.draw(new Rectangle((x + i) * tileSize, y * tileSize, tileSize, tileSize)); g2.draw(new Rectangle((x + i) * tileSize, y * tileSize, tileSize, tileSize));
} }
} }
@ -86,4 +125,8 @@ public class Renderer {
graphics2D.drawString(line, x + (int)padding, y + (textHeight * i)); graphics2D.drawString(line, x + (int)padding, y + (textHeight * i));
} }
} }
private int toPos(double u) {
return (int) (u * tileSize);
}
} }

View file

@ -0,0 +1,6 @@
package eu.m724.crossword;
public record Solution(
String text,
Vec[] coordinates
) { }

View file

@ -0,0 +1,24 @@
package eu.m724.crossword;
import java.util.Objects;
public record Vec( // TODO use this
int x,
int y
) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Vec vec)) return false;
return x == vec.x && y == vec.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
public Vec plus(int x, int y) {
return new Vec(this.x + x, this.y + y);
}
}

View file

@ -4,14 +4,14 @@ package eu.m724.crossword;
* Represents a word and a hint.<br> * Represents a word and a hint.<br>
* The word should be lowercase. TODO make that not a requirement * 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 * @param hint the hint / clue
*/ */
public record Word( public record Word(
String word, String text,
String hint String hint
) { ) {
public int length() { public int length() {
return word.length(); return text.length();
} }
} }