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(
int width, int height,
String solution,
Solution solution,
PlacedWord[] words
) {
/**

View file

@ -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<Word> words = new HashSet<>();
private final Set<PlacedWord> 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) {
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);
System.out.println("Word added: " + word.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<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");
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 { }
}

View file

@ -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<PlacedWord> placedWords;
//private final char[][] charArray;
//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.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<PlacedWord> 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<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);
long start = System.nanoTime();
boolean vertical = random.nextBoolean();
for (Word word : words) {
PlacedWord placedWord;
for (int x=0; x<width; x++) {
for (int y=0; y<height; y++) {
possiblePlacements.add(new Vec(x, y));
}
}
System.out.println("Possible placements: " + possiblePlacements.size() + " * 2");
do {
int x = random.nextInt(1, width - (!vertical ? word.length() : 0));
int y = random.nextInt(1, height - (vertical ? word.length() : 0));
placedWord = new PlacedWord(x, y, vertical, word);
} while (!canPlace(placedWord));
/*for (Word word : words) {
PlacedWord placedWord = null;
placeWord(placedWord); // TODO rename
List<Vec> 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;
}
System.out.printf("Generation took %fms\n", (System.nanoTime() - start) / 1000000.0);
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
}*/
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();
}
return placedWords;
}
private void placeWord(PlacedWord word) {
int x = word.x();
int y = word.y();
private Set<PlacedWord> follow(Grid grid, Set<PlacedWord> placedChain, int wordIndex) {
System.out.println("Depth " + wordIndex);
placedWords.add(word);
if (wordIndex > peakDepth)
peakDepth = wordIndex;
for (int i=0; i<word.length(); i++) {
if (word.vertical()) { y++; } else { x++; }
if (wordIndex == words.length) {
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) {
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<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");
}
}
}
}
private boolean canPlace(PlacedWord word) {
int x = word.x();
int y = word.y();
for (int i=0; i<word.length(); i++) {
if (word.vertical()) { y++; } else { x++; }
char c = word.word().word().charAt(i); // TODO
if (charArray[x][y] != 0 && charArray[x][y] != c) {
return false;
}
System.out.println(" No solution");
// no placement at this depth
return null;
}
return true;
private <T> void shuffle(List<T> 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);
}
}
}

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;
public class Main {
public static void main(String[] args) throws CrosswordBuilder.SizeMismatchException {
CrosswordBuilder builder = new CrosswordBuilder(20, 20, "ardour")
public static void main(String[] args) {
CrosswordBuilder builder = new CrosswordBuilder(12, 12, "crossword")
.addWord("cat", "Furry feline pet")
.addWord("sun", "Bright star in our sky")
.addWord("book", "Bound pages with a story")
.addWord("tree", "Woody plant with branches")
.addWord("blue", "Color of a clear sky")
.addWord("door", "Entrance to a room")
.addWord("ocean", "Vast body of saltwater")
.addWord("mountain", "Large natural elevation of Earth's surface")
.addWord("computer", "Electronic device for processing data")
.addWord("music", "Art of arranging sounds in time")
.addWord("bicycle", "Two-wheeled vehicle powered by pedaling")
.addWord("flower", "Reproductive structure of flowering plants")
.addWord("rain", "Water droplets falling from the sky")
.addWord("camera", "Device for capturing images or videos")
.addWord("pizza", "Flat bread topped with sauce and cheese")
.addWord("telescope", "Optical instrument for viewing distant objects")
.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")
.addWord("owl", "Nocturnal bird of prey")
.addWord("kite", "Flying toy on a string")
.addWord("moon", "Earth's natural satellite")
.addWord("train", "Rail transport vehicle")
.addWord("cloud", "Visible mass of water droplets")
.addWord("camera", "Device for taking photos")
.addWord("locket", "Small ornamental case")
.addWord("rainbow", "Multicolored arc in the sky")
.addWord("volcano", "Erupting mountain")
.addWord("elevator", "Vertical transport device")
.addWord("pineapple", "Tropical fruit with spiky skin")
.addWord("submarine", "Underwater military vessel")
.addWord("sunflower", "Tall yellow-petaled plant")
.addWord("helicopter", "Rotary-wing aircraft")
.addWord("rhinoceros", "Large horned African mammal")
.generate();
Crossword crossword = builder.build();

View file

@ -3,18 +3,30 @@ package eu.m724.crossword;
/**
* Represents a placement of a word
*
* @param x x of the first letter
* @param y y of the first letter
* @param vertical is the word, from the first letter, downwards (true), or rightwards (false)
* @param word the word
* @param pos Position of the first letter
* @param vertical Is the word, from the first letter, going downwards (true), or rightwards (false)
* @param word The word
*/
public record PlacedWord(
int x,
int y,
Vec pos,
boolean vertical,
Word word
) {
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.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List;
public class Renderer {
private final int tileSize;
@ -13,26 +15,63 @@ public class Renderer {
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);
g2.setPaint(Color.BLACK);
List<Vec> 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<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));
}
} else {
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++) {
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));
}
}
@ -86,4 +125,8 @@ public class Renderer {
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>
* 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();
}
}