initial commit
This commit is contained in:
254
src/puzzle/DailyGenerator.java
Normal file
254
src/puzzle/DailyGenerator.java
Normal file
@@ -0,0 +1,254 @@
|
||||
package puzzle;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* DailyGenerator - Generates daily themed puzzles with JSON output
|
||||
*/
|
||||
public class DailyGenerator {
|
||||
|
||||
private static String env(String name, String defaultValue) {
|
||||
var val = System.getenv(name);
|
||||
return (val == null || val.isEmpty()) ? defaultValue : val;
|
||||
}
|
||||
|
||||
private static int envInt(String name, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(env(name, String.valueOf(defaultValue)));
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean envBool(String name, boolean defaultValue) {
|
||||
var val = env(name, String.valueOf(defaultValue));
|
||||
return "true".equalsIgnoreCase(val) || "1".equals(val);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
var outDir = env("OUT_DIR", "/data/puzzles");
|
||||
var wordsPath = env("WORDS_PATH", "./word-list.txt");
|
||||
var puzzlesPerDay = envInt("PUZZLES_PER_DAY", 3);
|
||||
var seed = envInt("SEED", (int) System.currentTimeMillis());
|
||||
var themeFilter = envBool("THEME_FILTER", true);
|
||||
var themeMinScore = Double.parseDouble(env("THEME_MIN_SCORE", "0.6"));
|
||||
|
||||
var today = LocalDate.now();
|
||||
var dateStr = today.toString();
|
||||
|
||||
System.out.println("=== Daily Puzzle Generator ===");
|
||||
System.out.println("Date: " + dateStr);
|
||||
System.out.println("Output: " + outDir);
|
||||
System.out.println("Puzzles per day: " + puzzlesPerDay);
|
||||
System.out.println("Theme filtering: " + themeFilter);
|
||||
System.out.println();
|
||||
|
||||
// Load word list
|
||||
SwedishGenerator.Dict dict;
|
||||
try {
|
||||
dict = SwedishGenerator.loadWords(wordsPath);
|
||||
System.out.println("Loaded " + dict.words.size() + " words");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to load words: " + e.getMessage());
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
try {
|
||||
Files.createDirectories(Paths.get(outDir));
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to create output dir: " + e.getMessage());
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate puzzles
|
||||
List<String> generatedFiles = new ArrayList<>();
|
||||
var themes = new String[]{ "algemeen", "nieuws", "technologie", "sport", "weer", "economie" };
|
||||
|
||||
for (var i = 1; i <= puzzlesPerDay; i++) {
|
||||
System.out.println("\n--- Generating puzzle " + i + "/" + puzzlesPerDay + " ---");
|
||||
|
||||
// Select theme
|
||||
var theme = themes[new Random(seed + i).nextInt(themes.length)];
|
||||
System.out.println("Theme: " + theme);
|
||||
|
||||
// Filter word list by theme
|
||||
List<String> filteredWords = dict.words;
|
||||
if (themeFilter && !theme.equals("algemeen")) {
|
||||
filteredWords = ThemeGraph.filterByTheme(dict.words, theme, themeMinScore);
|
||||
System.out.println("Filtered to " + filteredWords.size() + " words for theme '" + theme + "'");
|
||||
|
||||
// If too few words, fall back to general
|
||||
if (filteredWords.size() < 50) {
|
||||
System.out.println("Not enough themed words, using general list");
|
||||
filteredWords = dict.words;
|
||||
theme = "algemeen";
|
||||
}
|
||||
}
|
||||
|
||||
// Create filtered dict
|
||||
var themedDict = filterDict(dict, filteredWords);
|
||||
|
||||
// Generate puzzle
|
||||
var opts = new Main.Opts();
|
||||
opts.seed = seed + i;
|
||||
opts.pop = 18;
|
||||
opts.gens = 100;
|
||||
opts.tries = 50;
|
||||
opts.wordsPath = wordsPath;
|
||||
|
||||
var result = generateWithFilteredDict(opts, themedDict);
|
||||
|
||||
if (result == null) {
|
||||
System.out.println("Failed to generate puzzle " + i);
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("Generated puzzle with " + result.filled().clueMap.size() + " words");
|
||||
|
||||
// Export to JSON
|
||||
var exported = ExportFormat.exportFormatFromFilled(
|
||||
result, 1, new ExportFormat.Rewards(50, 2, 1)
|
||||
);
|
||||
|
||||
// Write to JSON file
|
||||
var filename = String.format("crossword_%s_%02d_%s.json", dateStr, i, safeSlug(theme));
|
||||
var outputPath = Paths.get(outDir, filename);
|
||||
|
||||
try {
|
||||
var json = toJson(exported, dateStr, theme);
|
||||
Files.writeString(outputPath, json, StandardCharsets.UTF_8);
|
||||
generatedFiles.add(filename);
|
||||
System.out.println("Saved: " + filename);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to write " + filename + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Write index.json
|
||||
try {
|
||||
var indexJson = toIndexJson(dateStr, generatedFiles);
|
||||
Files.writeString(Paths.get(outDir, "index.json"), indexJson, StandardCharsets.UTF_8);
|
||||
System.out.println("\n✓ Generated " + generatedFiles.size() + " puzzles for " + dateStr);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to write index.json: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static SwedishGenerator.Dict filterDict(SwedishGenerator.Dict dict, List<String> allowedWords) {
|
||||
Set<String> allowed = new HashSet<>(allowedWords);
|
||||
var newIndex = new HashMap<Integer, SwedishGenerator.DictEntry>();
|
||||
var newLenCounts = new HashMap<Integer, Integer>();
|
||||
|
||||
for (var word : dict.words) {
|
||||
if (!allowed.contains(word)) continue;
|
||||
|
||||
var L = word.length();
|
||||
newLenCounts.put(L, newLenCounts.getOrDefault(L, 0) + 1);
|
||||
|
||||
var entry = newIndex.get(L);
|
||||
if (entry == null) {
|
||||
entry = new SwedishGenerator.DictEntry(L);
|
||||
newIndex.put(L, entry);
|
||||
}
|
||||
|
||||
var idx = entry.words.size();
|
||||
entry.words.add(word);
|
||||
|
||||
for (var i = 0; i < L; i++) {
|
||||
var letter = word.charAt(i) - 'A';
|
||||
if (letter >= 0 && letter < 26) {
|
||||
entry.pos[i][letter].add(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SwedishGenerator.Dict(new ArrayList<>(allowed), newIndex, newLenCounts);
|
||||
}
|
||||
|
||||
private static SwedishGenerator.PuzzleResult generateWithFilteredDict(Main.Opts opts, SwedishGenerator.Dict dict) {
|
||||
var rng = new SwedishGenerator.Rng(opts.seed);
|
||||
|
||||
for (var attempt = 1; attempt <= opts.tries; attempt++) {
|
||||
var mask = SwedishGenerator.generateMask(rng, dict.lenCounts, opts.pop, opts.gens);
|
||||
var filled = SwedishGenerator.fillMask(rng, mask, dict.index, 200, 30000);
|
||||
|
||||
if (filled.ok) {
|
||||
return new SwedishGenerator.PuzzleResult(mask, filled);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String toJson(ExportFormat.ExportedPuzzle puzzle, String date, String theme) {
|
||||
var sb = new StringBuilder();
|
||||
sb.append("{\n");
|
||||
sb.append(" \"date\": \"").append(escapeJson(date)).append("\",\n");
|
||||
sb.append(" \"theme\": \"").append(escapeJson(theme)).append("\",\n");
|
||||
sb.append(" \"difficulty\": ").append(puzzle.difficulty()).append(",\n");
|
||||
sb.append(" \"rewards\": {\n");
|
||||
sb.append(" \"coins\": ").append(puzzle.rewards().coins()).append(",\n");
|
||||
sb.append(" \"stars\": ").append(puzzle.rewards().stars()).append(",\n");
|
||||
sb.append(" \"hints\": ").append(puzzle.rewards().hints()).append("\n");
|
||||
sb.append(" },\n");
|
||||
sb.append(" \"gridv2\": [\n");
|
||||
for (var i = 0; i < puzzle.gridv2().size(); i++) {
|
||||
sb.append(" \"").append(escapeJson(puzzle.gridv2().get(i))).append("\"");
|
||||
if (i < puzzle.gridv2().size() - 1) sb.append(",");
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(" ],\n");
|
||||
sb.append(" \"words\": [\n");
|
||||
for (var i = 0; i < puzzle.words().size(); i++) {
|
||||
var w = puzzle.words().get(i);
|
||||
sb.append(" {\n");
|
||||
sb.append(" \"word\": \"").append(escapeJson(w.word())).append("\",\n");
|
||||
sb.append(" \"clue\": \"").append(escapeJson(w.clue())).append("\",\n");
|
||||
sb.append(" \"startRow\": ").append(w.startRow()).append(",\n");
|
||||
sb.append(" \"startCol\": ").append(w.startCol()).append(",\n");
|
||||
sb.append(" \"direction\": \"").append(escapeJson(w.direction())).append("\",\n");
|
||||
sb.append(" \"answer\": \"").append(escapeJson(w.answer())).append("\",\n");
|
||||
sb.append(" \"arrowRow\": ").append(w.arrowRow()).append(",\n");
|
||||
sb.append(" \"arrowCol\": ").append(w.arrowCol()).append("\n");
|
||||
sb.append(" }");
|
||||
if (i < puzzle.words().size() - 1) sb.append(",");
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(" ]\n");
|
||||
sb.append("}\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String toIndexJson(String date, List<String> files) {
|
||||
var sb = new StringBuilder();
|
||||
sb.append("{\n");
|
||||
sb.append(" \"date\": \"").append(escapeJson(date)).append("\",\n");
|
||||
sb.append(" \"files\": [\n");
|
||||
for (var i = 0; i < files.size(); i++) {
|
||||
sb.append(" \"").append(escapeJson(files.get(i))).append("\"");
|
||||
if (i < files.size() - 1) sb.append(",");
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(" ]\n");
|
||||
sb.append("}\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private static String safeSlug(String s) {
|
||||
return s.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", "");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
package puzzle;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -38,23 +39,23 @@ public final class ExportFormat {
|
||||
|
||||
public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz, int difficulty, Rewards rewards) {
|
||||
Objects.requireNonNull(puz, "puz");
|
||||
char[][] g = puz.filled.grid;
|
||||
int H = g.length;
|
||||
int W = g[0].length;
|
||||
var g = puz.filled().grid;
|
||||
var H = g.length;
|
||||
var W = g[0].length;
|
||||
|
||||
// 1) extract "placed" list from all clue digits in the filled grid
|
||||
List<Placed> placed = new ArrayList<>();
|
||||
Set<String> seen = new HashSet<>();
|
||||
|
||||
for (int r = 0; r < H; r++) {
|
||||
for (int c = 0; c < W; c++) {
|
||||
char ch = g[r][c];
|
||||
for (var r = 0; r < H; r++) {
|
||||
for (var c = 0; c < W; c++) {
|
||||
var ch = g[r][c];
|
||||
if (!isDigit(ch)) continue;
|
||||
|
||||
Placed p = extractPlacedFromClue(g, r, c, ch, 8, 2);
|
||||
var p = extractPlacedFromClue(g, r, c, ch, 8, 2);
|
||||
if (p == null) continue;
|
||||
|
||||
String key = p.startRow + "," + p.startCol + ":" + p.direction + ":" + p.word;
|
||||
var key = p.startRow + "," + p.startCol + ":" + p.direction + ":" + p.word;
|
||||
if (seen.contains(key)) continue;
|
||||
seen.add(key);
|
||||
placed.add(p);
|
||||
@@ -64,10 +65,10 @@ public final class ExportFormat {
|
||||
// If nothing placed: return full grid mapped to letters/# only
|
||||
if (placed.isEmpty()) {
|
||||
List<String> gridv2 = new ArrayList<>(H);
|
||||
for (int r = 0; r < H; r++) {
|
||||
StringBuilder sb = new StringBuilder(W);
|
||||
for (int c = 0; c < W; c++) {
|
||||
char ch = g[r][c];
|
||||
for (var chars : g) {
|
||||
var sb = new StringBuilder(W);
|
||||
for (var c = 0; c < W; c++) {
|
||||
var ch = chars[c];
|
||||
sb.append(isLetter(ch) ? ch : '#');
|
||||
}
|
||||
gridv2.add(sb.toString());
|
||||
@@ -77,7 +78,7 @@ public final class ExportFormat {
|
||||
|
||||
// 2) bounding box around all word cells + arrow cells, with 1-cell margin
|
||||
List<int[]> allCells = new ArrayList<>();
|
||||
for (Placed p : placed) {
|
||||
for (var p : placed) {
|
||||
allCells.addAll(p.cells);
|
||||
allCells.add(p.arrow);
|
||||
}
|
||||
@@ -85,7 +86,7 @@ public final class ExportFormat {
|
||||
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
|
||||
int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
|
||||
|
||||
for (int[] rc : allCells) {
|
||||
for (var rc : allCells) {
|
||||
int rr = rc[0], cc = rc[1];
|
||||
minR = Math.min(minR, rr);
|
||||
minC = Math.min(minC, cc);
|
||||
@@ -100,8 +101,8 @@ public final class ExportFormat {
|
||||
|
||||
// 3) map of only used letter cells (everything else becomes '#')
|
||||
Map<Long, Character> letterAt = new HashMap<>();
|
||||
for (Placed p : placed) {
|
||||
for (int[] rc : p.cells) {
|
||||
for (var p : placed) {
|
||||
for (var rc : p.cells) {
|
||||
int rr = rc[0], cc = rc[1];
|
||||
if (inBounds(H, W, rr, cc) && isLetter(g[rr][cc])) {
|
||||
letterAt.put(pack(rr, cc), g[rr][cc]);
|
||||
@@ -111,10 +112,10 @@ public final class ExportFormat {
|
||||
|
||||
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
|
||||
List<String> gridv2 = new ArrayList<>(Math.max(0, maxR - minR + 1));
|
||||
for (int r = minR; r <= maxR; r++) {
|
||||
StringBuilder row = new StringBuilder(Math.max(0, maxC - minC + 1));
|
||||
for (int c = minC; c <= maxC; c++) {
|
||||
Character ch = letterAt.get(pack(r, c));
|
||||
for (var r = minR; r <= maxR; r++) {
|
||||
var row = new StringBuilder(Math.max(0, maxC - minC + 1));
|
||||
for (var c = minC; c <= maxC; c++) {
|
||||
var ch = letterAt.get(pack(r, c));
|
||||
row.append(ch != null ? ch : '#');
|
||||
}
|
||||
gridv2.add(row.toString());
|
||||
@@ -122,7 +123,7 @@ public final class ExportFormat {
|
||||
|
||||
// 5) words output with cropped coordinates
|
||||
List<WordOut> wordsOut = new ArrayList<>(placed.size());
|
||||
for (Placed p : placed) {
|
||||
for (var p : placed) {
|
||||
wordsOut.add(new WordOut(
|
||||
p.word,
|
||||
p.clue, // placeholder = word (same as JS)
|
||||
@@ -137,7 +138,7 @@ public final class ExportFormat {
|
||||
|
||||
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract a word run for a clue cell at (r,c) with direction digit d.
|
||||
* Canonical output:
|
||||
@@ -148,7 +149,7 @@ public final class ExportFormat {
|
||||
*/
|
||||
private static Placed extractPlacedFromClue(char[][] g, int r, int c, char d, int maxLen, int minLen) {
|
||||
int H = g.length, W = g[0].length;
|
||||
int di = d - '0';
|
||||
var di = d - '0';
|
||||
int dr = DIRS[di][0], dc = DIRS[di][1];
|
||||
|
||||
// collect letter cells in ORIGINAL direction away from the clue
|
||||
@@ -180,14 +181,14 @@ public final class ExportFormat {
|
||||
arrowCol = c;
|
||||
} else if (d == '4') { // left -> canonical right
|
||||
direction = "horizontal";
|
||||
int[] farLeft = cells.get(cells.size() - 1);
|
||||
var farLeft = cells.get(cells.size() - 1);
|
||||
startRow = farLeft[0];
|
||||
startCol = farLeft[1];
|
||||
arrowRow = startRow;
|
||||
arrowCol = startCol - 1;
|
||||
} else if (d == '1') { // up -> canonical down
|
||||
direction = "vertical";
|
||||
int[] topMost = cells.get(cells.size() - 1);
|
||||
var topMost = cells.get(cells.size() - 1);
|
||||
startRow = topMost[0];
|
||||
startCol = topMost[1];
|
||||
arrowRow = startRow - 1;
|
||||
@@ -197,32 +198,32 @@ public final class ExportFormat {
|
||||
}
|
||||
|
||||
// Read word from grid in canonical order (right/down)
|
||||
StringBuilder wordChars = new StringBuilder();
|
||||
var wordChars = new StringBuilder();
|
||||
if ("horizontal".equals(direction)) {
|
||||
for (int i = 0; i < cells.size(); i++) {
|
||||
int cc2 = startCol + i;
|
||||
char ch = (inBounds(H, W, startRow, cc2) ? g[startRow][cc2] : '#');
|
||||
for (var i = 0; i < cells.size(); i++) {
|
||||
var cc2 = startCol + i;
|
||||
var ch = (inBounds(H, W, startRow, cc2) ? g[startRow][cc2] : '#');
|
||||
if (!isLetter(ch)) break;
|
||||
wordChars.append(ch);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < cells.size(); i++) {
|
||||
int rr2 = startRow + i;
|
||||
char ch = (inBounds(H, W, rr2, startCol) ? g[rr2][startCol] : '#');
|
||||
for (var i = 0; i < cells.size(); i++) {
|
||||
var rr2 = startRow + i;
|
||||
var ch = (inBounds(H, W, rr2, startCol) ? g[rr2][startCol] : '#');
|
||||
if (!isLetter(ch)) break;
|
||||
wordChars.append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
String word = wordChars.toString();
|
||||
var word = wordChars.toString();
|
||||
if (word.length() < minLen || word.length() > maxLen) return null;
|
||||
|
||||
// Build exact used cells (only for actual word length)
|
||||
List<int[]> used = new ArrayList<>(word.length());
|
||||
if ("horizontal".equals(direction)) {
|
||||
for (int i = 0; i < word.length(); i++) used.add(new int[]{ startRow, startCol + i });
|
||||
for (var i = 0; i < word.length(); i++) used.add(new int[]{ startRow, startCol + i });
|
||||
} else {
|
||||
for (int i = 0; i < word.length(); i++) used.add(new int[]{ startRow + i, startCol });
|
||||
for (var i = 0; i < word.length(); i++) used.add(new int[]{ startRow + i, startCol });
|
||||
}
|
||||
|
||||
return new Placed(
|
||||
@@ -246,84 +247,25 @@ public final class ExportFormat {
|
||||
|
||||
// ---------- Data models ----------
|
||||
|
||||
private static final class Placed {
|
||||
|
||||
final String word;
|
||||
final String clue;
|
||||
final int startRow, startCol;
|
||||
final String direction; // "horizontal" | "vertical"
|
||||
final String answer;
|
||||
final int arrowRow, arrowCol;
|
||||
final List<int[]> cells; // word cells
|
||||
final int[] arrow; // [arrowRow, arrowCol]
|
||||
|
||||
Placed(String word, String clue, int startRow, int startCol, String direction, String answer,
|
||||
int arrowRow, int arrowCol, List<int[]> cells, int[] arrow) {
|
||||
this.word = word;
|
||||
this.clue = clue;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.direction = direction;
|
||||
this.answer = answer;
|
||||
this.arrowRow = arrowRow;
|
||||
this.arrowCol = arrowCol;
|
||||
this.cells = cells;
|
||||
this.arrow = arrow;
|
||||
}
|
||||
/**
|
||||
* @param direction "horizontal" | "vertical"
|
||||
* @param cells word cells
|
||||
* @param arrow [arrowRow, arrowCol] */
|
||||
private record Placed(String word, String clue, int startRow, int startCol, String direction, String answer, int arrowRow, int arrowCol, List<int[]> cells, int[] arrow) {
|
||||
|
||||
}
|
||||
|
||||
public static final class Rewards {
|
||||
|
||||
public final int coins;
|
||||
public final int stars;
|
||||
public final int hints;
|
||||
|
||||
public Rewards(int coins, int stars, int hints) {
|
||||
this.coins = coins;
|
||||
this.stars = stars;
|
||||
this.hints = hints;
|
||||
}
|
||||
public record Rewards(int coins, int stars, int hints) {
|
||||
|
||||
}
|
||||
|
||||
public static final class WordOut {
|
||||
|
||||
public final String word;
|
||||
public final String clue;
|
||||
public final int startRow;
|
||||
public final int startCol;
|
||||
public final String direction; // "horizontal" | "vertical"
|
||||
public final String answer;
|
||||
public final int arrowRow;
|
||||
public final int arrowCol;
|
||||
|
||||
public WordOut(String word, String clue, int startRow, int startCol, String direction,
|
||||
String answer, int arrowRow, int arrowCol) {
|
||||
this.word = word;
|
||||
this.clue = clue;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.direction = direction;
|
||||
this.answer = answer;
|
||||
this.arrowRow = arrowRow;
|
||||
this.arrowCol = arrowCol;
|
||||
}
|
||||
/**
|
||||
* @param direction "horizontal" | "vertical" */
|
||||
public record WordOut(String word, String clue, int startRow, int startCol, String direction, String answer, int arrowRow, int arrowCol) {
|
||||
|
||||
}
|
||||
|
||||
public static final class ExportedPuzzle {
|
||||
|
||||
public final List<String> gridv2;
|
||||
public final List<WordOut> words;
|
||||
public final int difficulty;
|
||||
public final Rewards rewards;
|
||||
|
||||
public ExportedPuzzle(List<String> gridv2, List<WordOut> words, int difficulty, Rewards rewards) {
|
||||
this.gridv2 = gridv2;
|
||||
this.words = words;
|
||||
this.difficulty = difficulty;
|
||||
this.rewards = rewards;
|
||||
}
|
||||
public record ExportedPuzzle(List<String> gridv2, List<WordOut> words, int difficulty, Rewards rewards) {
|
||||
|
||||
}
|
||||
|
||||
// ---------- Tiny demo (optional) ----------
|
||||
|
||||
}
|
||||
|
||||
@@ -53,20 +53,20 @@ public class Main {
|
||||
}
|
||||
|
||||
System.out.println("\n=== GENERATED MASK ===");
|
||||
System.out.println(SwedishGenerator.gridToString(res.mask));
|
||||
System.out.println(SwedishGenerator.gridToString(res.mask()));
|
||||
|
||||
System.out.println("\n=== FILLED PUZZLE (RAW) ===");
|
||||
System.out.println(SwedishGenerator.gridToString(res.filled.grid));
|
||||
System.out.println(SwedishGenerator.gridToString(res.filled().grid));
|
||||
|
||||
System.out.println("\n=== FILLED PUZZLE (HUMAN) ===");
|
||||
System.out.println(SwedishGenerator.renderHuman(res.filled.grid));
|
||||
System.out.println(SwedishGenerator.renderHuman(res.filled().grid));
|
||||
var out = ExportFormat.exportFormatFromFilled(res, 1, new ExportFormat.Rewards(50, 2, 1));
|
||||
System.out.println("gridv2:");
|
||||
for (String row : out.gridv2) System.out.println(row);
|
||||
System.out.println("words: " + out.words.size());
|
||||
for (var w : out.words) {
|
||||
for (String row : out.gridv2()) System.out.println(row);
|
||||
System.out.println("words: " + out.words().size());
|
||||
for (var w : out.words()) {
|
||||
System.out.printf("%s %s start=(%d,%d) arrow=(%d,%d)%n",
|
||||
w.word, w.direction, w.startRow, w.startCol, w.arrowRow, w.arrowCol);
|
||||
w.word(), w.direction(), w.startRow(), w.startCol(), w.arrowRow(), w.arrowCol());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
205
src/puzzle/ThemeGraph.java
Normal file
205
src/puzzle/ThemeGraph.java
Normal file
@@ -0,0 +1,205 @@
|
||||
package puzzle;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* ThemeGraph - Creates a graph between words and themes for filtering.
|
||||
* Uses word embeddings approach: co-occurrence and semantic similarity.
|
||||
*/
|
||||
public class ThemeGraph {
|
||||
|
||||
// Predefined theme keywords for Dutch word filtering
|
||||
private static final Map<String, Set<String>> THEME_KEYWORDS = new HashMap<>();
|
||||
|
||||
static {
|
||||
// News/Politics
|
||||
THEME_KEYWORDS.put("nieuws", Set.of(
|
||||
"POLITIEK", "VERKIEZING", "MINISTER", "PARLEMENT", "WET", "BELEID",
|
||||
"REGERING", "PARTIJ", "STEM", "KAMER", "RAAD", "STAAT"
|
||||
));
|
||||
|
||||
// Technology
|
||||
THEME_KEYWORDS.put("technologie", Set.of(
|
||||
"COMPUTER", "INTERNET", "SOFTWARE", "APP", "DATA", "CODE",
|
||||
"NETWERK", "SYSTEEM", "DIGITAAL", "TECH", "ROBOT", "AI"
|
||||
));
|
||||
|
||||
// Sports
|
||||
THEME_KEYWORDS.put("sport", Set.of(
|
||||
"VOETBAL", "TENNIS", "WIELREN", "SPELER", "WEDSTRIJD", "TEAM",
|
||||
"GOAL", "BAL", "SPEL", "WINNEN", "COACH", "ATLEET"
|
||||
));
|
||||
|
||||
// Weather/Nature
|
||||
THEME_KEYWORDS.put("weer", Set.of(
|
||||
"REGEN", "ZON", "WIND", "WOLKEN", "STORM", "SNEEUW",
|
||||
"WEER", "KLIMAAT", "NATUUR", "LUCHT", "WARMTE", "KOU"
|
||||
));
|
||||
|
||||
// Economy
|
||||
THEME_KEYWORDS.put("economie", Set.of(
|
||||
"GELD", "EURO", "MARKT", "PRIJS", "KOPEN", "VERKOOP",
|
||||
"BEDRIJF", "BANK", "HANDEL", "WINST", "SCHULD", "BUDGET"
|
||||
));
|
||||
|
||||
// Health
|
||||
THEME_KEYWORDS.put("gezondheid", Set.of(
|
||||
"ZORG", "DOKTER", "MEDICIJN", "PATIENT", "ZIEKENHUIS", "GEZOND",
|
||||
"VIRUS", "VACCIN", "THERAPIE", "BEHANDEL", "ARTS", "KLINIEK"
|
||||
));
|
||||
|
||||
// General/Common
|
||||
THEME_KEYWORDS.put("algemeen", Set.of(
|
||||
"HUIS", "AUTO", "BOOM", "WATER", "MENS", "TIJD",
|
||||
"LEVEN", "WERK", "SCHOOL", "FAMILIE", "STAD", "LAND"
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Score a word against a theme (0.0 = no match, 1.0 = perfect match)
|
||||
*/
|
||||
public static double scoreWordTheme(String word, String theme) {
|
||||
Set<String> keywords = THEME_KEYWORDS.get(theme.toLowerCase());
|
||||
if (keywords == null) {
|
||||
return 0.5; // unknown theme = neutral score
|
||||
}
|
||||
|
||||
word = word.toUpperCase();
|
||||
|
||||
// Direct match
|
||||
if (keywords.contains(word)) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Substring match (partial relevance)
|
||||
for (String kw : keywords) {
|
||||
if (word.contains(kw) || kw.contains(word)) {
|
||||
return 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// Edit distance similarity (for typos/variations)
|
||||
for (String kw : keywords) {
|
||||
double similarity = editDistanceSimilarity(word, kw);
|
||||
if (similarity > 0.8) {
|
||||
return similarity * 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter word list by theme with minimum score threshold
|
||||
*/
|
||||
public static List<String> filterByTheme(List<String> words, String theme, double minScore) {
|
||||
List<String> filtered = new ArrayList<>();
|
||||
for (String word : words) {
|
||||
double score = scoreWordTheme(word, theme);
|
||||
if (score >= minScore) {
|
||||
filtered.add(word);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme suggestions for a word (sorted by score)
|
||||
*/
|
||||
public static List<ThemeScore> getThemesForWord(String word) {
|
||||
List<ThemeScore> scores = new ArrayList<>();
|
||||
for (String theme : THEME_KEYWORDS.keySet()) {
|
||||
double score = scoreWordTheme(word, theme);
|
||||
if (score > 0.0) {
|
||||
scores.add(new ThemeScore(theme, score));
|
||||
}
|
||||
}
|
||||
scores.sort(Comparator.comparingDouble(ts -> -ts.score));
|
||||
return scores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect best theme from a word list
|
||||
*/
|
||||
public static String detectTheme(List<String> words) {
|
||||
Map<String, Double> themeScores = new HashMap<>();
|
||||
|
||||
for (String theme : THEME_KEYWORDS.keySet()) {
|
||||
double totalScore = 0;
|
||||
for (String word : words) {
|
||||
totalScore += scoreWordTheme(word, theme);
|
||||
}
|
||||
themeScores.put(theme, totalScore / words.size());
|
||||
}
|
||||
|
||||
return themeScores.entrySet().stream()
|
||||
.max(Comparator.comparingDouble(Map.Entry::getValue))
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse("algemeen");
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple edit distance similarity (normalized Levenshtein)
|
||||
*/
|
||||
private static double editDistanceSimilarity(String a, String b) {
|
||||
int dist = levenshtein(a, b);
|
||||
int maxLen = Math.max(a.length(), b.length());
|
||||
if (maxLen == 0) return 1.0;
|
||||
return 1.0 - ((double) dist / maxLen);
|
||||
}
|
||||
|
||||
private static int levenshtein(String a, String b) {
|
||||
int[][] dp = new int[a.length() + 1][b.length() + 1];
|
||||
|
||||
for (int i = 0; i <= a.length(); i++) dp[i][0] = i;
|
||||
for (int j = 0; j <= b.length(); j++) dp[0][j] = j;
|
||||
|
||||
for (int i = 1; i <= a.length(); i++) {
|
||||
for (int j = 1; j <= b.length(); j++) {
|
||||
int cost = (a.charAt(i - 1) == b.charAt(j - 1)) ? 0 : 1;
|
||||
dp[i][j] = Math.min(
|
||||
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
|
||||
dp[i - 1][j - 1] + cost
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return dp[a.length()][b.length()];
|
||||
}
|
||||
|
||||
public record ThemeScore(String theme, double score) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s: %.2f", theme, score);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Main for testing ----
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Theme Graph Test ===\n");
|
||||
|
||||
// Test word scoring
|
||||
String[] testWords = {"POLITIEK", "VOETBAL", "COMPUTER", "REGEN", "AUTO"};
|
||||
for (String word : testWords) {
|
||||
System.out.println("Word: " + word);
|
||||
List<ThemeScore> themes = getThemesForWord(word);
|
||||
for (ThemeScore ts : themes) {
|
||||
System.out.println(" " + ts);
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
// Test theme detection
|
||||
List<String> techWords = Arrays.asList("COMPUTER", "INTERNET", "SOFTWARE", "DATA");
|
||||
String detected = detectTheme(techWords);
|
||||
System.out.println("Detected theme for tech words: " + detected);
|
||||
|
||||
// Test filtering
|
||||
List<String> allWords = Arrays.asList(
|
||||
"POLITIEK", "COMPUTER", "AUTO", "VOETBAL", "INTERNET", "BOOM"
|
||||
);
|
||||
List<String> filtered = filterByTheme(allWords, "technologie", 0.5);
|
||||
System.out.println("\nFiltered for 'technologie' (min 0.5): " + filtered);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user