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 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 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 allowedWords) { Set allowed = new HashSet<>(allowedWords); var newIndex = new HashMap(); var newLenCounts = new HashMap(); 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 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 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("^-|-$", ""); } }