Refactoring
This commit is contained in:
parent
661452d747
commit
472106ba3a
10 changed files with 429 additions and 106 deletions
|
@ -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
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
|
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);
|
this.words.add(word);
|
||||||
System.out.println("Word added: " + word.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 { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
} while (!canPlace(placedWord));
|
|
||||||
|
|
||||||
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;
|
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;
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canPlace(PlacedWord word) {
|
System.out.println(" No solution");
|
||||||
int x = word.x();
|
// no placement at this depth
|
||||||
int y = word.y();
|
return null;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
93
src/main/java/eu/m724/crossword/Grid.java
Normal file
93
src/main/java/eu/m724/crossword/Grid.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
6
src/main/java/eu/m724/crossword/Solution.java
Normal file
6
src/main/java/eu/m724/crossword/Solution.java
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package eu.m724.crossword;
|
||||||
|
|
||||||
|
public record Solution(
|
||||||
|
String text,
|
||||||
|
Vec[] coordinates
|
||||||
|
) { }
|
24
src/main/java/eu/m724/crossword/Vec.java
Normal file
24
src/main/java/eu/m724/crossword/Vec.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue