262 lines
10 KiB
Java
262 lines
10 KiB
Java
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", "/home/mike/dev/puzzle-generator/data/");
|
|
var wordsPath = env("WORDS_PATH", "./export_words_only.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.0"));
|
|
|
|
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;
|
|
var llmScores = SwedishGenerator.loadScores();
|
|
try {
|
|
dict = SwedishGenerator.loadWords(wordsPath, llmScores);
|
|
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;
|
|
opts.minSimplicity = 0; // default
|
|
|
|
var result = generateWithFilteredDict(opts, themedDict, llmScores);
|
|
|
|
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)
|
|
);
|
|
|
|
// Generate clues via LLM
|
|
System.out.println("Generating clues for " + exported.words().size() + " words...");
|
|
exported = ClueGenerator.applyClues(exported);
|
|
|
|
// 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, Map<String, Integer> llmScores) {
|
|
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, llmScores, 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(" \"isReversed\": ").append(w.isReversed()).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("^-|-$", "");
|
|
}
|
|
}
|