Files
puzzle-generator/src/puzzle/DailyGenerator.java
2025-12-25 00:21:58 +01:00

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("^-|-$", "");
}
}