Gather data
This commit is contained in:
@@ -5,216 +5,218 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import static puzzle.ExportFormat.*;
|
||||
|
||||
public class ClueGenerator {
|
||||
|
||||
private static final String OLLAMA_URL = "http://localhost:11434/api/chat";
|
||||
private static final String MODEL = "qwen2.5:14b";
|
||||
private static final String HINTS_FILE = "/data/puzzle/export_with_hints.csv";
|
||||
private static Map<String, String> prebuiltClues = null;
|
||||
|
||||
private static synchronized void ensurePrebuiltCluesLoaded() {
|
||||
if (prebuiltClues != null) return;
|
||||
prebuiltClues = new HashMap<>();
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(Path.of(HINTS_FILE), StandardCharsets.UTF_8);
|
||||
for (String line : lines) {
|
||||
String[] parts = line.split(",", 3);
|
||||
if (parts.length >= 3) {
|
||||
String word = parts[0].trim().toUpperCase(Locale.ROOT);
|
||||
String rawClue = parts[2].trim();
|
||||
if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) {
|
||||
rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\"");
|
||||
}
|
||||
if (!word.isEmpty() && !rawClue.isEmpty()) {
|
||||
prebuiltClues.put(word, rawClue);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String OLLAMA_URL = "http://localhost:11434/api/chat";
|
||||
private static final String MODEL = "qwen2.5:14b";
|
||||
private static final String HINTS_FILE = "/home/mike/dev/puzzle-generator/nl_score_hints.csv";
|
||||
private static Map<String, String> prebuiltClues = null;
|
||||
|
||||
private static synchronized void ensurePrebuiltCluesLoaded() {
|
||||
if (prebuiltClues != null) return;
|
||||
prebuiltClues = new HashMap<>();
|
||||
try {
|
||||
var lines = Files.readAllLines(Path.of(HINTS_FILE), StandardCharsets.UTF_8);
|
||||
for (var line : lines) {
|
||||
var parts = line.split(",", 3);
|
||||
if (parts.length >= 3) {
|
||||
var word = parts[0].trim().toUpperCase(Locale.ROOT);
|
||||
var rawClue = parts[2].trim();
|
||||
if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) {
|
||||
rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\"");
|
||||
}
|
||||
if (!word.isEmpty() && !rawClue.isEmpty()) {
|
||||
prebuiltClues.put(word, rawClue);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Warning: " + HINTS_FILE + " not found or could not be read.");
|
||||
}
|
||||
}
|
||||
|
||||
public static ExportFormat.ExportedPuzzle applyClues(ExportFormat.ExportedPuzzle puzzle) {
|
||||
if (puzzle == null || puzzle.words().isEmpty()) {
|
||||
return puzzle;
|
||||
}
|
||||
|
||||
ensurePrebuiltCluesLoaded();
|
||||
|
||||
Map<String, String> finalClueMap = new HashMap<>();
|
||||
List<String> wordsMissingClues = new ArrayList<>();
|
||||
|
||||
for (var w : puzzle.words()) {
|
||||
String wordUpper = w.word().toUpperCase(Locale.ROOT);
|
||||
if (prebuiltClues.containsKey(wordUpper)) {
|
||||
finalClueMap.put(w.word(), prebuiltClues.get(wordUpper));
|
||||
} else {
|
||||
wordsMissingClues.add(w.word());
|
||||
}
|
||||
}
|
||||
|
||||
if (!wordsMissingClues.isEmpty()) {
|
||||
Map<String, String> generatedClues = generateClues(wordsMissingClues);
|
||||
finalClueMap.putAll(generatedClues);
|
||||
}
|
||||
|
||||
List<ExportFormat.WordOut> wordsWithClues = new ArrayList<>();
|
||||
for (var w : puzzle.words()) {
|
||||
String clue = finalClueMap.getOrDefault(w.word(), w.word());
|
||||
wordsWithClues.add(new ExportFormat.WordOut(
|
||||
w.word(),
|
||||
clue,
|
||||
w.startRow(),
|
||||
w.startCol(),
|
||||
w.direction(),
|
||||
w.answer(),
|
||||
w.arrowRow(),
|
||||
w.arrowCol(),
|
||||
w.isReversed()
|
||||
));
|
||||
}
|
||||
|
||||
return new ExportFormat.ExportedPuzzle(puzzle.gridv2(), wordsWithClues, puzzle.difficulty(), puzzle.rewards());
|
||||
}
|
||||
|
||||
public static Map<String, String> generateClues(List<String> words) {
|
||||
if (words == null || words.isEmpty()) {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Warning: " + HINTS_FILE + " not found or could not be read.");
|
||||
}
|
||||
}
|
||||
|
||||
public static ExportedPuzzle applyClues(ExportedPuzzle puzzle) {
|
||||
if (puzzle == null || puzzle.words().isEmpty()) {
|
||||
return puzzle;
|
||||
}
|
||||
|
||||
ensurePrebuiltCluesLoaded();
|
||||
|
||||
Map<String, String> finalClueMap = new HashMap<>();
|
||||
List<String> wordsMissingClues = new ArrayList<>();
|
||||
|
||||
for (var w : puzzle.words()) {
|
||||
var wordUpper = w.word().toUpperCase(Locale.ROOT);
|
||||
if (prebuiltClues.containsKey(wordUpper)) {
|
||||
finalClueMap.put(w.word(), prebuiltClues.get(wordUpper));
|
||||
} else {
|
||||
wordsMissingClues.add(w.word());
|
||||
}
|
||||
}
|
||||
|
||||
if (!wordsMissingClues.isEmpty()) {
|
||||
var generatedClues = generateClues(wordsMissingClues);
|
||||
finalClueMap.putAll(generatedClues);
|
||||
}
|
||||
|
||||
List<WordOut> wordsWithClues = new ArrayList<>();
|
||||
for (var w : puzzle.words()) {
|
||||
var clue = finalClueMap.getOrDefault(w.word(), w.word());
|
||||
wordsWithClues.add(new WordOut(
|
||||
w.word(),
|
||||
clue,
|
||||
w.startRow(),
|
||||
w.startCol(),
|
||||
w.direction(),
|
||||
w.answer(),
|
||||
w.arrowRow(),
|
||||
w.arrowCol(),
|
||||
w.isReversed(),
|
||||
w.complex()
|
||||
));
|
||||
}
|
||||
|
||||
return new ExportedPuzzle(puzzle.gridv2(), wordsWithClues, puzzle.difficulty(), puzzle.rewards());
|
||||
}
|
||||
|
||||
public static Map<String, String> generateClues(List<String> words) {
|
||||
if (words == null || words.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
var prompt = createCluePrompt(words);
|
||||
try {
|
||||
var jsonRequest = String.format(
|
||||
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],\"stream\":false,\"temperature\":0.7}",
|
||||
MODEL, escapeJson(prompt)
|
||||
);
|
||||
|
||||
var responseBody = curlPostJson(OLLAMA_URL, jsonRequest, 120);
|
||||
var content = extractChatContent(responseBody);
|
||||
|
||||
if (content == null || content.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
String prompt = createCluePrompt(words);
|
||||
try {
|
||||
String jsonRequest = String.format(
|
||||
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],\"stream\":false,\"temperature\":0.7}",
|
||||
MODEL, escapeJson(prompt)
|
||||
);
|
||||
|
||||
String responseBody = curlPostJson(OLLAMA_URL, jsonRequest, 120);
|
||||
String content = extractChatContent(responseBody);
|
||||
|
||||
if (content == null || content.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return parseCluesFromReply(words, content);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to generate clues: " + e.getMessage());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
private static String createCluePrompt(List<String> words) {
|
||||
return "Je bent een expert in het maken van kruiswoordpuzzels. Geef voor elk van de onderstaande woorden een korte, uitdagende maar duidelijke cryptische of beschrijvende aanwijzing in het Nederlands.\n\n" +
|
||||
"Output ALLEEN in dit formaat:\n" +
|
||||
"woord1:aanwijzing\n" +
|
||||
"woord2:aanwijzing\n\n" +
|
||||
"GEEN andere tekst of uitleg. Sla GEEN woorden over.\n\n" +
|
||||
"Lijst:\n" +
|
||||
String.join("\n", words);
|
||||
}
|
||||
|
||||
private static Map<String, String> parseCluesFromReply(List<String> expectedWords, String reply) {
|
||||
Map<String, String> wordClueMap = new HashMap<>();
|
||||
var lines = reply.split("\n");
|
||||
|
||||
for (var line : lines) {
|
||||
line = line.trim();
|
||||
if (line.contains(":")) {
|
||||
var parts = line.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
var wordPart = parts[0].trim().replaceAll("^[\\d+.)*\\-\\s]+", "").toLowerCase();
|
||||
var clue = parts[1].trim();
|
||||
if (!clue.isEmpty()) {
|
||||
wordClueMap.put(wordPart, clue);
|
||||
}
|
||||
}
|
||||
|
||||
return parseCluesFromReply(words, content);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to generate clues: " + e.getMessage());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
private static String createCluePrompt(List<String> words) {
|
||||
return "Je bent een expert in het maken van kruiswoordpuzzels. Geef voor elk van de onderstaande woorden een korte, uitdagende maar duidelijke cryptische of beschrijvende aanwijzing in het Nederlands.\n\n" +
|
||||
"Output ALLEEN in dit formaat:\n" +
|
||||
"woord1:aanwijzing\n" +
|
||||
"woord2:aanwijzing\n\n" +
|
||||
"GEEN andere tekst of uitleg. Sla GEEN woorden over.\n\n" +
|
||||
"Lijst:\n" +
|
||||
String.join("\n", words);
|
||||
}
|
||||
|
||||
private static Map<String, String> parseCluesFromReply(List<String> expectedWords, String reply) {
|
||||
Map<String, String> wordClueMap = new HashMap<>();
|
||||
String[] lines = reply.split("\n");
|
||||
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.contains(":")) {
|
||||
String[] parts = line.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
String wordPart = parts[0].trim().replaceAll("^[\\d+.)*\\-\\s]+", "").toLowerCase();
|
||||
String clue = parts[1].trim();
|
||||
if (!clue.isEmpty()) {
|
||||
wordClueMap.put(wordPart, clue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> results = new HashMap<>();
|
||||
for (String word : expectedWords) {
|
||||
String clue = wordClueMap.get(word.toLowerCase());
|
||||
if (clue != null) {
|
||||
results.put(word, clue);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static String curlPostJson(String url, String jsonBody, int timeoutSeconds) throws Exception {
|
||||
var tempFile = Files.createTempFile("clue-request-", ".json");
|
||||
try {
|
||||
Files.writeString(tempFile, jsonBody, StandardCharsets.UTF_8);
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add("curl");
|
||||
cmd.add("-fsSL");
|
||||
cmd.add("--connect-timeout");
|
||||
cmd.add("10");
|
||||
cmd.add("--max-time");
|
||||
cmd.add(String.valueOf(timeoutSeconds));
|
||||
cmd.add("-H");
|
||||
cmd.add("Content-Type: application/json");
|
||||
cmd.add("-d");
|
||||
cmd.add("@" + tempFile);
|
||||
cmd.add(url);
|
||||
|
||||
var p = new ProcessBuilder(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
var bytes = p.getInputStream().readAllBytes();
|
||||
var code = p.waitFor();
|
||||
|
||||
if (code != 0) {
|
||||
throw new IOException("curl POST failed (" + code + ") url=" + url + "\nOutput:\n" +
|
||||
new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} finally {
|
||||
Files.deleteIfExists(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractChatContent(String json) {
|
||||
if (json == null) return null;
|
||||
var choices = json.indexOf("\"choices\"");
|
||||
var p = (choices >= 0) ? choices : 0;
|
||||
var i = json.indexOf("\"content\"", p);
|
||||
if (i < 0) {
|
||||
// Fallback for Ollama non-chat format if needed, but we used /api/chat
|
||||
// Ollama /api/chat returns {"model":"...","message":{"role":"assistant","content":"..."}}
|
||||
i = json.indexOf("\"content\"");
|
||||
if (i < 0) return null;
|
||||
}
|
||||
var colon = json.indexOf(':', i);
|
||||
if (colon < 0) return null;
|
||||
var q = json.indexOf('"', colon + 1);
|
||||
if (q < 0) return null;
|
||||
var sb = new StringBuilder();
|
||||
var esc = false;
|
||||
for (var k = q + 1; k < json.length(); k++) {
|
||||
var ch = json.charAt(k);
|
||||
if (esc) {
|
||||
if (ch == 'n') sb.append('\n');
|
||||
else if (ch == 't') sb.append('\t');
|
||||
else if (ch == 'r') sb.append('\r');
|
||||
else sb.append(ch);
|
||||
esc = false;
|
||||
} else {
|
||||
if (ch == '\\') esc = true;
|
||||
else if (ch == '"') break;
|
||||
else sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String escapeJson(String str) {
|
||||
return str.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> results = new HashMap<>();
|
||||
for (var word : expectedWords) {
|
||||
var clue = wordClueMap.get(word.toLowerCase());
|
||||
if (clue != null) {
|
||||
results.put(word, clue);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static String curlPostJson(String url, String jsonBody, int timeoutSeconds) throws Exception {
|
||||
var tempFile = Files.createTempFile("clue-request-", ".json");
|
||||
try {
|
||||
Files.writeString(tempFile, jsonBody, StandardCharsets.UTF_8);
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add("curl");
|
||||
cmd.add("-fsSL");
|
||||
cmd.add("--connect-timeout");
|
||||
cmd.add("10");
|
||||
cmd.add("--max-time");
|
||||
cmd.add(String.valueOf(timeoutSeconds));
|
||||
cmd.add("-H");
|
||||
cmd.add("Content-Type: application/json");
|
||||
cmd.add("-d");
|
||||
cmd.add("@" + tempFile);
|
||||
cmd.add(url);
|
||||
|
||||
var p = new ProcessBuilder(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
var bytes = p.getInputStream().readAllBytes();
|
||||
var code = p.waitFor();
|
||||
|
||||
if (code != 0) {
|
||||
throw new IOException("curl POST failed (" + code + ") url=" + url + "\nOutput:\n" +
|
||||
new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} finally {
|
||||
Files.deleteIfExists(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractChatContent(String json) {
|
||||
if (json == null) return null;
|
||||
var choices = json.indexOf("\"choices\"");
|
||||
var p = (choices >= 0) ? choices : 0;
|
||||
var i = json.indexOf("\"content\"", p);
|
||||
if (i < 0) {
|
||||
// Fallback for Ollama non-chat format if needed, but we used /api/chat
|
||||
// Ollama /api/chat returns {"model":"...","message":{"role":"assistant","content":"..."}}
|
||||
i = json.indexOf("\"content\"");
|
||||
if (i < 0) return null;
|
||||
}
|
||||
var colon = json.indexOf(':', i);
|
||||
if (colon < 0) return null;
|
||||
var q = json.indexOf('"', colon + 1);
|
||||
if (q < 0) return null;
|
||||
var sb = new StringBuilder();
|
||||
var esc = false;
|
||||
for (var k = q + 1; k < json.length(); k++) {
|
||||
var ch = json.charAt(k);
|
||||
if (esc) {
|
||||
if (ch == 'n') sb.append('\n');
|
||||
else if (ch == 't') sb.append('\t');
|
||||
else if (ch == 'r') sb.append('\r');
|
||||
else sb.append(ch);
|
||||
esc = false;
|
||||
} else {
|
||||
if (ch == '\\') esc = true;
|
||||
else if (ch == '"') break;
|
||||
else sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String escapeJson(String str) {
|
||||
return str.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
static void main(String[] args) {
|
||||
var outDir = env("OUT_DIR", "/home/mike/dev/puzzle-generator/data/");
|
||||
var wordsPath = env("WORDS_PATH", "/data/puzzle/export_with_hints.csv");
|
||||
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")) {
|
||||
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, true);
|
||||
var filled = SwedishGenerator.fillMask(rng, mask, dict.index, llmScores, 200, 30000, true);
|
||||
|
||||
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("^-|-$", "");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package puzzle;
|
||||
|
||||
import java.util.*;
|
||||
import static puzzle.SwedishGenerator.*;
|
||||
|
||||
/**
|
||||
* ExportFormat.java
|
||||
@@ -13,7 +14,7 @@ import java.util.*;
|
||||
*/
|
||||
public final class ExportFormat {
|
||||
|
||||
private ExportFormat() { }
|
||||
private ExportFormat() { }
|
||||
|
||||
private static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; }
|
||||
|
||||
@@ -23,20 +24,16 @@ public final class ExportFormat {
|
||||
|
||||
// ---------- Public API ----------
|
||||
|
||||
public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz) {
|
||||
return exportFormatFromFilled(puz, 1, new Rewards(50, 2, 1));
|
||||
}
|
||||
|
||||
public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz, int difficulty, Rewards rewards) {
|
||||
public static ExportedPuzzle exportFormatFromFilled(PuzzleResult puz, int difficulty, Rewards rewards) {
|
||||
Objects.requireNonNull(puz, "puz");
|
||||
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<>();
|
||||
var allSlots = SwedishGenerator.extractSlots(g);
|
||||
var clueMap = puz.filled().clueMap;
|
||||
List<Placed> placed = new ArrayList<>();
|
||||
var allSlots = extractSlots(g);
|
||||
var clueMap = puz.filled().clueMap;
|
||||
|
||||
for (var s : allSlots) {
|
||||
var word = clueMap.get(s.key());
|
||||
@@ -113,24 +110,25 @@ public final class ExportFormat {
|
||||
p.word, // answer
|
||||
p.arrowRow - minR,
|
||||
p.arrowCol - minC,
|
||||
p.isReversed
|
||||
p.isReversed,
|
||||
puz.dict().words().get(p.word).difficulty()
|
||||
));
|
||||
}
|
||||
|
||||
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
|
||||
}
|
||||
|
||||
static final String HORIZONTAL = "h", VERTICAL = "v";
|
||||
/**
|
||||
* Convert a generator Slot + assigned word into a Placed object for export.
|
||||
*/
|
||||
private static Placed extractPlacedFromSlot(SwedishGenerator.Slot s, String word) {
|
||||
int r = s.clueR;
|
||||
int c = s.clueC;
|
||||
char d = s.dir;
|
||||
private static Placed extractPlacedFromSlot(Slot s, String word) {
|
||||
int r = s.clueR();
|
||||
int c = s.clueC();
|
||||
char d = s.dir();
|
||||
|
||||
List<int[]> cells = new ArrayList<>();
|
||||
for (int i = 0; i < s.len; i++) {
|
||||
cells.add(new int[]{ s.rs[i], s.cs[i] });
|
||||
for (int i = 0; i < s.len(); i++) {
|
||||
cells.add(new int[]{ s.rs()[i], s.cs()[i] });
|
||||
}
|
||||
|
||||
// Canonicalize: always output right/down
|
||||
@@ -139,26 +137,26 @@ public final class ExportFormat {
|
||||
boolean isReversed = false;
|
||||
|
||||
if (d == '2') { // right -> horizontal
|
||||
direction = "horizontal";
|
||||
direction = HORIZONTAL;
|
||||
startRow = cells.get(0)[0];
|
||||
startCol = cells.get(0)[1];
|
||||
arrowRow = r;
|
||||
arrowCol = c;
|
||||
} else if (d == '3' || d == '5') { // down or down-bent -> vertical
|
||||
direction = "vertical";
|
||||
direction = VERTICAL;
|
||||
startRow = cells.get(0)[0];
|
||||
startCol = cells.get(0)[1];
|
||||
arrowRow = r;
|
||||
arrowCol = c;
|
||||
} else if (d == '4') { // left -> horizontal (REVERSED)
|
||||
direction = "horizontal";
|
||||
direction = HORIZONTAL;
|
||||
isReversed = true;
|
||||
startRow = cells.get(0)[0];
|
||||
startCol = cells.get(0)[1];
|
||||
arrowRow = r;
|
||||
arrowCol = c;
|
||||
} else if (d == '1') { // up -> vertical (REVERSED)
|
||||
direction = "vertical";
|
||||
direction = VERTICAL;
|
||||
isReversed = true;
|
||||
startRow = cells.get(0)[0];
|
||||
startCol = cells.get(0)[1];
|
||||
@@ -195,21 +193,13 @@ public final class ExportFormat {
|
||||
* @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,
|
||||
boolean isReversed) {
|
||||
|
||||
}
|
||||
boolean isReversed) { }
|
||||
|
||||
public record Rewards(int coins, int stars, int hints) {
|
||||
|
||||
}
|
||||
public record Rewards(int coins, int stars, int hints) { }
|
||||
|
||||
/**
|
||||
* @param direction "horizontal" | "vertical" */
|
||||
public record WordOut(String word, String clue, int startRow, int startCol, String direction, String answer, int arrowRow, int arrowCol, boolean isReversed) {
|
||||
|
||||
}
|
||||
public record WordOut(String word, String clue, int startRow, int startCol, String direction, String answer, int arrowRow, int arrowCol, boolean isReversed, int complex) { }
|
||||
|
||||
public record ExportedPuzzle(List<String> gridv2, List<WordOut> words, int difficulty, Rewards rewards) {
|
||||
|
||||
}
|
||||
public record ExportedPuzzle(List<String> gridv2, List<WordOut> words, int difficulty, Rewards rewards) { }
|
||||
}
|
||||
|
||||
@@ -2,195 +2,317 @@ package puzzle;
|
||||
|
||||
import puzzle.SwedishGenerator.PuzzleResult;
|
||||
import puzzle.SwedishGenerator.Rng;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.*;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static puzzle.SwedishGenerator.fillMask;
|
||||
import static puzzle.SwedishGenerator.generateMask;
|
||||
import static puzzle.SwedishGenerator.loadScores;
|
||||
import static puzzle.SwedishGenerator.loadWords;
|
||||
|
||||
public class Main {
|
||||
// ---------------- CLI ----------------
|
||||
|
||||
|
||||
final static String OUT_DIR = envOrDefault("OUT_DIR", "/data/puzzle");
|
||||
final static Path PUZZLE_DIR = Paths.get(OUT_DIR, "puzzles");
|
||||
static final Path INDEX_FILE = PUZZLE_DIR.resolve("index.json");
|
||||
static final OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
static final String CREATED_AT = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
||||
static final String FILE_ID = CREATED_AT.replace(":", "-") + "_" + (System.currentTimeMillis() / 1000);
|
||||
static final String FILE_NAME = FILE_ID + ".json";
|
||||
static final Path OUTPUT_PATH = PUZZLE_DIR.resolve(FILE_NAME);
|
||||
static final String DATE_STRING = now.toLocalDate().toString();
|
||||
|
||||
public static class Opts {
|
||||
public int seed = 1234;
|
||||
public int pop = 18;
|
||||
public int gens = 300;
|
||||
public String wordsPath = "/data/puzzle/pool.txt";
|
||||
public double minSimplicity = 0; // 0 means no limit
|
||||
public int threads = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
public int tries = threads;
|
||||
|
||||
public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis());
|
||||
public int pop = 18;
|
||||
public int gens = 500;
|
||||
public String wordsPath = "nl_score_hints.csv";
|
||||
public double minSimplicity = 0; // 0 means no limit
|
||||
public int threads = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
public int tries = threads;
|
||||
public boolean reindex = false;
|
||||
}
|
||||
|
||||
public void main(String[] args) {
|
||||
var opts = parseArgs(args);
|
||||
|
||||
if (opts.reindex) {
|
||||
|
||||
section("Reindex");
|
||||
info("OutputDir : " + OUT_DIR);
|
||||
rebuildIndex();
|
||||
return;
|
||||
}
|
||||
|
||||
section("Puzzle Generator");
|
||||
info("OutputDir : " + OUT_DIR);
|
||||
info("WordsFile : " + opts.wordsPath);
|
||||
|
||||
section("Settings");
|
||||
printSettings(opts);
|
||||
|
||||
var res = generatePuzzle(opts);
|
||||
if (res == null) {
|
||||
err("Search status : UNSOLVED");
|
||||
err("Reason : No solution found within tries.");
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
section("Result");
|
||||
info(String.format(Locale.ROOT, "simplicity : %.2f", res.filled().simplicity));
|
||||
|
||||
section("Mask");
|
||||
System.out.print(indentLines(SwedishGenerator.gridToString(res.mask()), " "));
|
||||
|
||||
section("Grid (raw)");
|
||||
System.out.print(indentLines(SwedishGenerator.gridToString(res.filled().grid), " "));
|
||||
|
||||
section("Grid (human)");
|
||||
System.out.print(indentLines(SwedishGenerator.renderHuman(res.filled().grid), " "));
|
||||
|
||||
var exported = ExportFormat.exportFormatFromFilled(res, 1, new ExportFormat.Rewards(50, 2, 1));
|
||||
|
||||
section("Clues");
|
||||
info("status : generating...");
|
||||
info("generatedFor : " + exported.words().size());
|
||||
exported = ClueGenerator.applyClues(exported);
|
||||
info("status : done");
|
||||
|
||||
section("Words");
|
||||
printWordsTable(exported.words());
|
||||
|
||||
section("Gridv2");
|
||||
for (var row : exported.gridv2()) System.out.println(" " + row);
|
||||
|
||||
// Export to JSON file
|
||||
|
||||
var theme = "algemeen";
|
||||
|
||||
section("Export");
|
||||
info("file : " + OUTPUT_PATH);
|
||||
|
||||
try {
|
||||
Files.createDirectories(PUZZLE_DIR);
|
||||
var json = toJson(exported, DATE_STRING, theme);
|
||||
Files.writeString(OUTPUT_PATH, json, StandardCharsets.UTF_8);
|
||||
|
||||
// Update index.json
|
||||
var pathInIndex = "/puzzles/" + FILE_NAME;
|
||||
var indexRecord = toIndexRecordJson(FILE_ID, pathInIndex, DATE_STRING, theme, exported.difficulty(), CREATED_AT);
|
||||
if (1 != 1) updateIndex(PUZZLE_DIR.toString(), indexRecord);
|
||||
else rebuildIndex();
|
||||
info("indexUpdated : " + INDEX_FILE);
|
||||
} catch (IOException e) {
|
||||
err("Failed to write: " + FILE_NAME);
|
||||
err("Reason : " + e.getMessage());
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Output helpers ----------------
|
||||
|
||||
private static void info(String msg) { System.out.println("[INFO ] " + msg); }
|
||||
private static void warn(String msg) { System.out.println("[WARN ] " + msg); }
|
||||
private static void err(String msg) { System.err.println("[ERROR] " + msg); }
|
||||
|
||||
private static void section(String title) {
|
||||
System.out.println();
|
||||
System.out.println(title);
|
||||
}
|
||||
|
||||
private static String envOrDefault(String key, String def) {
|
||||
var v = System.getenv(key);
|
||||
return (v == null || v.isBlank()) ? def : v;
|
||||
}
|
||||
|
||||
private static void printSettings(Opts o) {
|
||||
System.out.printf(Locale.ROOT, " %-14s: %d%n", "seed", o.seed);
|
||||
System.out.printf(Locale.ROOT, " %-14s: %d%n", "population", o.pop);
|
||||
System.out.printf(Locale.ROOT, " %-14s: %d%n", "generations", o.gens);
|
||||
System.out.printf(Locale.ROOT, " %-14s: %s%n", "wordsPath", o.wordsPath);
|
||||
System.out.printf(Locale.ROOT, " %-14s: %.2f%n", "minSimplicity", o.minSimplicity);
|
||||
System.out.printf(Locale.ROOT, " %-14s: %d%n", "threads", o.threads);
|
||||
System.out.printf(Locale.ROOT, " %-14s: %d%n", "maxTries", o.tries);
|
||||
}
|
||||
|
||||
private static String fmtPoint(int r, int c) { return String.format(Locale.ROOT, "(%d,%d)", r, c); }
|
||||
|
||||
private static void printWordsTable(List<ExportFormat.WordOut> words) {
|
||||
System.out.println(" # WORD CX DIR START ARROW CLUE");
|
||||
var i = 1;
|
||||
for (var w : words) {
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
" %-2d %-12s %-3s %-3s %-9s %-9s %s%n",
|
||||
i++,
|
||||
safe(w.word(), 12),
|
||||
safe("" + w.complex(), 3),
|
||||
safe(w.direction(), 3),
|
||||
fmtPoint(w.startRow(), w.startCol()),
|
||||
fmtPoint(w.arrowRow(), w.arrowCol()),
|
||||
w.clue() == null ? "" : w.clue()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String s, int max) {
|
||||
if (s == null) return "";
|
||||
if (s.length() <= max) return s;
|
||||
return s.substring(0, Math.max(0, max - 1)) + "…";
|
||||
}
|
||||
|
||||
private static String indentLines(String s, String indent) {
|
||||
if (s == null || s.isEmpty()) return "";
|
||||
var lines = s.split("\\R", -1);
|
||||
var sb = new StringBuilder();
|
||||
for (var line : lines) sb.append(indent).append(line).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static void usage() {
|
||||
System.out.println("""
|
||||
Usage:
|
||||
java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt] [--min-simplicity N.N] [--threads N]
|
||||
|
||||
Defaults:
|
||||
--seed 1234
|
||||
--pop 18
|
||||
--gens 1000
|
||||
--tries 5
|
||||
--words /data/puzzle/pool.txt
|
||||
--min-simplicity 0 (no limit)
|
||||
--threads %d
|
||||
""".formatted(Math.max(1, Runtime.getRuntime().availableProcessors())));
|
||||
Usage:
|
||||
java puzzle.Main [--seed N] [--pop N] [--gens N] [--tries N] [--words FILE] [--min-simplicity N.N] [--threads N] [--reindex]
|
||||
|
||||
Defaults:
|
||||
--pop 18
|
||||
--gens 600
|
||||
--tries = threads
|
||||
--words nl_score_hints.csv
|
||||
--min-simplicity 0 (no limit)
|
||||
--threads %d
|
||||
""".formatted(Math.max(1, Runtime.getRuntime().availableProcessors())));
|
||||
}
|
||||
|
||||
static Opts parseArgs(String[] argv) {
|
||||
var out = new Opts();
|
||||
for (int i = 0; i < argv.length; i++) {
|
||||
String a = argv[i];
|
||||
String v = (i + 1 < argv.length) ? argv[i + 1] : null;
|
||||
for (var i = 0; i < argv.length; i++) {
|
||||
var a = argv[i];
|
||||
var v = (i + 1 < argv.length) ? argv[i + 1] : null;
|
||||
|
||||
if (a.equals("--help") || a.equals("-h")) {
|
||||
usage();
|
||||
System.exit(0);
|
||||
}
|
||||
if (a.equals("--seed")) { out.seed = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--pop")) { out.pop = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--gens")) { out.gens = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--tries")) { out.tries = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--words")) { out.wordsPath = v; i++; }
|
||||
else if (a.equals("--min-simplicity")) { out.minSimplicity = Double.parseDouble(v); i++; }
|
||||
else if (a.equals("--threads")) { out.threads = Integer.parseInt(v); i++; }
|
||||
else throw new IllegalArgumentException("Unknown arg: " + a);
|
||||
|
||||
if (a.equals("--seed")) {
|
||||
out.seed = Integer.parseInt(v);
|
||||
i++;
|
||||
} else if (a.equals("--pop")) {
|
||||
out.pop = Integer.parseInt(v);
|
||||
i++;
|
||||
} else if (a.equals("--gens")) {
|
||||
out.gens = Integer.parseInt(v);
|
||||
i++;
|
||||
} else if (a.equals("--tries")) {
|
||||
out.tries = Integer.parseInt(v);
|
||||
i++;
|
||||
} else if (a.equals("--words")) {
|
||||
out.wordsPath = v;
|
||||
i++;
|
||||
} else if (a.equals("--min-simplicity")) {
|
||||
out.minSimplicity = Double.parseDouble(v);
|
||||
i++;
|
||||
} else if (a.equals("--threads")) {
|
||||
out.threads = Integer.parseInt(v);
|
||||
i++;
|
||||
} else if (a.equals("--reindex")) {
|
||||
out.reindex = true;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown arg: " + a);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
var opts = parseArgs(args);
|
||||
var res = generatePuzzle(opts);
|
||||
if (res == null) {
|
||||
System.out.println("No solution found within tries.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println("\n=== GENERATED 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("\n=== FILLED PUZZLE (HUMAN) ===");
|
||||
System.out.println(SwedishGenerator.renderHuman(res.filled().grid));
|
||||
System.out.printf(Locale.ROOT, "Puzzle Simplicity: %.2f%n", res.filled().simplicity);
|
||||
var out = ExportFormat.exportFormatFromFilled(res, 1, new ExportFormat.Rewards(50, 2, 1));
|
||||
|
||||
// Generate clues via LLM
|
||||
System.out.println("Generating clues for " + out.words().size() + " words...");
|
||||
out = ClueGenerator.applyClues(out);
|
||||
|
||||
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()) {
|
||||
var simplicityOfWord =
|
||||
System.out.printf("%s %s start=(%d,%d) arrow=(%d,%d)%n",
|
||||
w.word(), w.direction(), w.startRow(), w.startCol(), w.arrowRow(), w.arrowCol());
|
||||
}
|
||||
|
||||
// Export to JSON file
|
||||
var now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
var createdAt = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
||||
var dateStr = now.toLocalDate().toString();
|
||||
var id = createdAt.replace(":", "-") + "_" + (System.currentTimeMillis() / 1000);
|
||||
var theme = "algemeen";
|
||||
var filename = id + ".json";
|
||||
var outDir = "/data/puzzle/puzzles";
|
||||
var outputPath = Paths.get(outDir, filename);
|
||||
|
||||
try {
|
||||
Files.createDirectories(Paths.get(outDir));
|
||||
var json = toJson(out, dateStr, theme);
|
||||
Files.writeString(outputPath, json, StandardCharsets.UTF_8);
|
||||
System.out.println("\nSaved to: " + outputPath);
|
||||
|
||||
// Update index.json
|
||||
var pathInIndex = "/puzzles/" + filename;
|
||||
var indexRecord = toIndexRecordJson(id, pathInIndex, dateStr, theme, out.difficulty(), createdAt);
|
||||
updateIndex(outDir, indexRecord);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to write " + filename + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Generation ----------------
|
||||
|
||||
// Package-private method for testing
|
||||
static PuzzleResult generatePuzzle(Opts opts) {
|
||||
PuzzleResult generatePuzzle(Opts opts) {
|
||||
var llmScores = loadScores();
|
||||
var tLoad0 = System.nanoTime();
|
||||
var dict = loadWords(opts.wordsPath, llmScores);
|
||||
var tLoad1 = System.nanoTime();
|
||||
System.out.printf(Locale.ROOT, "LOAD_WORDS: %.3fs%n %s words%n", (tLoad1 - tLoad0) / 1e9, dict.words.size());
|
||||
|
||||
var tLoad0 = System.nanoTime();
|
||||
var dict = loadWords(opts.wordsPath, llmScores);
|
||||
var tLoad1 = System.nanoTime();
|
||||
|
||||
section("Load");
|
||||
info(String.format(Locale.ROOT, "words : %,d", dict.words().size()));
|
||||
info(String.format(Locale.ROOT, "loadTime : %.3f s", (tLoad1 - tLoad0) / 1e9));
|
||||
|
||||
section("Search");
|
||||
|
||||
if (opts.threads > 1) {
|
||||
System.out.println("Running in multi-threaded mode with " + opts.threads + " threads...");
|
||||
info("mode : multi-threaded (" + opts.threads + ")");
|
||||
var executor = Executors.newFixedThreadPool(opts.threads);
|
||||
try {
|
||||
var tasks = new ArrayList<Callable<PuzzleResult>>();
|
||||
for (int i = 1; i <= opts.tries; i++) {
|
||||
final int attempt = i;
|
||||
for (var i = 1; i <= opts.tries; i++) {
|
||||
final var attempt = i;
|
||||
tasks.add(() -> {
|
||||
var threadRng = new Rng(opts.seed + attempt);
|
||||
var mask = generateMask(threadRng, dict.lenCounts, opts.pop, opts.gens, false);
|
||||
var filled = fillMask(threadRng, mask, dict.index, llmScores, 200, 60000, false);
|
||||
var mask = generateMask(threadRng, dict.lenCounts(), opts.pop, opts.gens, false);
|
||||
var filled = fillMask(threadRng, mask, dict.index(), llmScores, 200, 60000, false);
|
||||
|
||||
if (filled.ok && (opts.minSimplicity <= 0 || filled.simplicity >= opts.minSimplicity)) {
|
||||
System.out.println("\nSolution found on attempt " + attempt);
|
||||
return new PuzzleResult(mask, filled);
|
||||
info("status : SOLVED");
|
||||
info("foundAtTry : " + attempt);
|
||||
return new PuzzleResult(dict, mask, filled);
|
||||
}
|
||||
throw new RuntimeException("No solution found in attempt " + attempt);
|
||||
throw new RuntimeException("No solution in try " + attempt);
|
||||
});
|
||||
}
|
||||
return executor.invokeAny(tasks);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
warn("status : INTERRUPTED");
|
||||
} catch (ExecutionException e) {
|
||||
// all failed
|
||||
warn("status : UNSOLVED");
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
return null;
|
||||
|
||||
} else {
|
||||
info("mode : single-threaded");
|
||||
var rng = new Rng(opts.seed);
|
||||
|
||||
for (var attempt = 1; attempt <= opts.tries; attempt++) {
|
||||
System.out.println("\nAttempt " + attempt + "/" + opts.tries);
|
||||
info("try : " + attempt + "/" + opts.tries);
|
||||
|
||||
var tMask0 = System.nanoTime();
|
||||
var mask = generateMask(rng, dict.lenCounts, opts.pop, opts.gens, true);
|
||||
var tMask1 = System.nanoTime();
|
||||
System.out.printf(Locale.ROOT, "MASK: %.3fs%n", (tMask1 - tMask0) / 1e9);
|
||||
|
||||
var tFill0 = System.nanoTime();
|
||||
var filled = fillMask(rng, mask, dict.index, llmScores, 200, 60000, true);
|
||||
var tFill1 = System.nanoTime();
|
||||
System.out.printf(Locale.ROOT, "FILL: %.3fms | Simplicity: %.2f%n", (tFill1 - tFill0) / 1e6, filled.simplicity);
|
||||
var mask = generateMask(rng, dict.lenCounts(), opts.pop, opts.gens, true);
|
||||
var filled = fillMask(rng, mask, dict.index(), llmScores, 200, 60000, true);
|
||||
|
||||
if (filled.ok && (opts.minSimplicity <= 0 || filled.simplicity >= opts.minSimplicity)) {
|
||||
return new PuzzleResult(mask, filled);
|
||||
info("status : SOLVED");
|
||||
info("foundAtTry : " + attempt);
|
||||
return new PuzzleResult(dict, mask, filled);
|
||||
}
|
||||
|
||||
if (filled.ok) {
|
||||
System.out.printf(Locale.ROOT, "Puzzle simplicity %.2f is below min %.2f, retrying...%n",
|
||||
filled.simplicity, opts.minSimplicity);
|
||||
warn(String.format(Locale.ROOT,
|
||||
"simplicity : %.2f (below min %.2f)",
|
||||
filled.simplicity, opts.minSimplicity
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
info("status : UNSOLVED");
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// ---------------- Export (unchanged logic) ----------------
|
||||
|
||||
private static String toJson(ExportFormat.ExportedPuzzle puzzle, String date, String theme) {
|
||||
var sb = new StringBuilder();
|
||||
sb.append("{\n");
|
||||
@@ -230,7 +352,7 @@ public class Main {
|
||||
sb.append("}\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
@@ -238,42 +360,96 @@ public class Main {
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private static String safeSlug(String s) {
|
||||
return s.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", "");
|
||||
}
|
||||
|
||||
|
||||
private static String toIndexRecordJson(String id, String path, String date, String theme, int difficulty, String createdAt) {
|
||||
return String.format(
|
||||
"{\"id\":\"%s\",\"path\":\"%s\",\"date\":\"%s\",\"theme\":\"%s\",\"difficulty\":%d,\"createdAt\":\"%s\"}",
|
||||
escapeJson(id), escapeJson(path), escapeJson(date), escapeJson(theme), difficulty, escapeJson(createdAt)
|
||||
);
|
||||
Locale.ROOT,
|
||||
"{\"id\":\"%s\",\"path\":\"%s\",\"date\":\"%s\",\"theme\":\"%s\",\"difficulty\":%d,\"createdAt\":\"%s\"}",
|
||||
escapeJson(id), escapeJson(path), escapeJson(date), escapeJson(theme), difficulty, escapeJson(createdAt)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void updateIndex(String outDir, String newRecordJson) {
|
||||
Path indexPath = Paths.get(outDir, "index.json");
|
||||
var indexPath = Paths.get(outDir, "index.json");
|
||||
try {
|
||||
String content;
|
||||
if (Files.exists(indexPath)) {
|
||||
content = Files.readString(indexPath, StandardCharsets.UTF_8).trim();
|
||||
} else {
|
||||
content = "";
|
||||
}
|
||||
|
||||
var content = Files.exists(indexPath) ? Files.readString(indexPath, StandardCharsets.UTF_8).trim() : "";
|
||||
|
||||
if (content.isEmpty() || content.equals("[]")) {
|
||||
content = "[\n " + newRecordJson + "\n]";
|
||||
} else {
|
||||
int firstBracket = content.indexOf('[');
|
||||
if (firstBracket != -1) {
|
||||
content = content.substring(0, firstBracket + 1) + "\n " + newRecordJson + "," + content.substring(firstBracket + 1);
|
||||
} else {
|
||||
content = "[\n " + newRecordJson + "\n]";
|
||||
}
|
||||
var firstBracket = content.indexOf('[');
|
||||
if (firstBracket != -1) {
|
||||
content = content.substring(0, firstBracket + 1) + "\n " + newRecordJson + "," + content.substring(firstBracket + 1);
|
||||
} else {
|
||||
content = "[\n " + newRecordJson + "\n]";
|
||||
}
|
||||
}
|
||||
|
||||
Files.writeString(indexPath, content, StandardCharsets.UTF_8);
|
||||
System.out.println("Updated index.json at: " + indexPath);
|
||||
info("indexUpdated : " + indexPath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to update index.json: " + e.getMessage());
|
||||
err("Failed to update index.json: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildIndex() {
|
||||
|
||||
if (!Files.exists(PUZZLE_DIR)) {
|
||||
err("Puzzles directory does not exist: " + PUZZLE_DIR);
|
||||
return;
|
||||
}
|
||||
|
||||
info("Rebuilding index from: " + PUZZLE_DIR);
|
||||
|
||||
List<String> records = new ArrayList<>();
|
||||
try (var stream = Files.list(PUZZLE_DIR)) {
|
||||
stream.filter(p -> p.toString().endsWith(".json") && !p.getFileName().toString().equals("index.json"))
|
||||
.sorted(Comparator.comparing(Path::getFileName).reversed())
|
||||
.forEach(path -> {
|
||||
try {
|
||||
var filename = path.getFileName().toString();
|
||||
var id = filename.substring(0, filename.length() - 5);
|
||||
var content = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
var date = extractValue(content, "date");
|
||||
var theme = extractValue(content, "theme");
|
||||
var difficulty = 1;
|
||||
try { difficulty = Integer.parseInt(extractValue(content, "difficulty")); } catch (Exception ignored) { }
|
||||
|
||||
var createdAt = id;
|
||||
if (id.length() >= 20 && id.charAt(10) == 'T') {
|
||||
var parts = id.split("_");
|
||||
var dtPart = parts[0]; // 2025-12-24T04-25-06Z
|
||||
if (dtPart.length() >= 19) {
|
||||
createdAt = dtPart.substring(0, 13) + ":" + dtPart.substring(14, 16) + ":" + dtPart.substring(17);
|
||||
}
|
||||
}
|
||||
|
||||
var pathInIndex = "/puzzles/" + filename;
|
||||
records.add(toIndexRecordJson(id, pathInIndex, date, theme, difficulty, createdAt));
|
||||
} catch (IOException e) {
|
||||
err("Failed to read " + path + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
err("Failed to list puzzles: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
var indexPath = PUZZLE_DIR.resolve("index.json");
|
||||
var content = "[\n " + String.join(",\n ", records) + "\n]";
|
||||
try {
|
||||
Files.writeString(indexPath, content, StandardCharsets.UTF_8);
|
||||
info("Successfully rebuilt index.json with " + records.size() + " records.");
|
||||
} catch (IOException e) {
|
||||
err("Failed to write index.json: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractValue(String json, String key) {
|
||||
var pattern = java.util.regex.Pattern.compile("\"" + key + "\":\\s*\"?([^\",\\n\\r}]*)\"?");
|
||||
var matcher = pattern.matcher(json);
|
||||
if (matcher.find()) return matcher.group(1).trim();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class MainTest {
|
||||
opts.tries = 1;
|
||||
|
||||
// Act
|
||||
var result = Main.generatePuzzle(opts);
|
||||
var result = new Main().generatePuzzle(opts);
|
||||
|
||||
// Assert
|
||||
/* assertNotNull(result);
|
||||
|
||||
@@ -6,8 +6,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* SwedishGenerator.java
|
||||
@@ -145,35 +144,31 @@ public class SwedishGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
static class WordDifficulty {
|
||||
|
||||
final String word;
|
||||
final int difficulty;
|
||||
final int score;
|
||||
static record WordDifficulty(String word, int difficulty, int score) {
|
||||
|
||||
public WordDifficulty(String word, int score) {
|
||||
this.word = word;
|
||||
this.score = score;
|
||||
var difficulty1 = 0 + ((8 - word.length()) * 30) + ((10 - score) * 15);
|
||||
this(word, difficulty1, score);
|
||||
// We want LONGER and SIMPLER words to be tried earlier (lower difficulty value).
|
||||
// word.length() is 2 to 8.
|
||||
// score is 1 to 10.
|
||||
// Base difficulty starts high and decreases with length and score.
|
||||
// Length impact: up to 8 * 10 = 80
|
||||
// Score impact: up to 10 * 15 = 150
|
||||
var difficulty1 = 0 + ((8 - word.length()) * 30) + ((10 - score) * 15);
|
||||
this.difficulty = difficulty1;
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, Integer> loadScores() {
|
||||
var scores = new HashMap<String, Integer>();
|
||||
try {
|
||||
var lines = Files.readAllLines(Path.of("/data/puzzle/export_with_hints.csv"), StandardCharsets.UTF_8);
|
||||
var scoresPath = System.getenv("SCORES_PATH");
|
||||
if (scoresPath == null || scoresPath.isBlank()) scoresPath = "export_real_words_with_hints.csv";
|
||||
var lines = Files.readAllLines(Path.of(scoresPath), StandardCharsets.UTF_8);
|
||||
var first = true;
|
||||
for (var line : lines) {
|
||||
if (first) {
|
||||
first = false;
|
||||
continue;
|
||||
if (line.startsWith("WOORD")) continue;
|
||||
}
|
||||
var parts = line.split(",", 3);
|
||||
if (parts.length >= 2) {
|
||||
@@ -182,10 +177,7 @@ public class SwedishGenerator {
|
||||
var score = 10 - Integer.parseInt(parts[1].trim());
|
||||
scores.put(word, score);
|
||||
} catch (NumberFormatException ignored) {
|
||||
System.err.println("Illegal number format: " + line);
|
||||
}
|
||||
} else {
|
||||
System.err.println("Illegal word: " + line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -194,48 +186,55 @@ public class SwedishGenerator {
|
||||
return scores;
|
||||
}
|
||||
|
||||
static final class Dict {
|
||||
|
||||
final ArrayList<String> words;
|
||||
final HashMap<Integer, DictEntry> index; // len -> DictEntry
|
||||
final HashMap<Integer, Integer> lenCounts; // len -> count
|
||||
Dict(ArrayList<String> words, HashMap<Integer, DictEntry> index, HashMap<Integer, Integer> lenCounts) {
|
||||
this.words = words;
|
||||
this.index = index;
|
||||
this.lenCounts = lenCounts;
|
||||
}
|
||||
}
|
||||
public static record Dict(Map<String, WordDifficulty> words,
|
||||
HashMap<Integer, DictEntry> index,
|
||||
HashMap<Integer, Integer> lenCounts) { }
|
||||
static Dict loadWords(String wordsPath, Map<String, Integer> llmScores) {
|
||||
String raw;
|
||||
try {
|
||||
raw = Files.readString(Path.of(wordsPath), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
raw = "EU\nUUR\nAUTO\nBOOM\nHUIS\nKAT\nZEE\nRODE\nDRAAD\nKENNIS\nNETWERK\nPAKTE\n";
|
||||
raw = "WOORD,level_1_to_10,hint\nEU,2,hint\nUUR,2,hint\nAUTO,2,hint\nBOOM,2,hint\nHUIS,2,hint\nKAT,2,hint\nZEE,2,hint\nRODE,2,hint\nDRAAD,2,hint\nKENNIS,2,hint\nNETWERK,2,hint\nPAKTE,2,hint\n";
|
||||
}
|
||||
|
||||
var words = new ArrayList<WordDifficulty>();
|
||||
var map = new HashMap<String, WordDifficulty>();
|
||||
boolean first = true;
|
||||
for (var line : raw.split("\\R")) {
|
||||
var word = line.split(",", 3)[0].trim();
|
||||
var s = word.trim().toUpperCase(Locale.ROOT);
|
||||
if (line.isBlank()) continue;
|
||||
var parts = line.split(",", 3);
|
||||
var word = parts[0].trim();
|
||||
if (first && word.equalsIgnoreCase("WOORD")) {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
first = false;
|
||||
var s = word.toUpperCase(Locale.ROOT);
|
||||
if (s.matches("^[A-Z]{2,8}$")) {
|
||||
var score = llmScores.getOrDefault(s, SIMPLICITY_DEFAULT_SCORE); // Default to middle
|
||||
words.add(new WordDifficulty(s, score));
|
||||
int score = SIMPLICITY_DEFAULT_SCORE;
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
// CSV has level 1-10. llmScores use 10-level.
|
||||
score = 10 - Integer.parseInt(parts[1].trim());
|
||||
} catch (NumberFormatException e) {
|
||||
score = llmScores.getOrDefault(s, SIMPLICITY_DEFAULT_SCORE);
|
||||
System.err.println("Warning: " + word + " csv not found, using default scores.");
|
||||
}
|
||||
} else {
|
||||
score = llmScores.getOrDefault(s, SIMPLICITY_DEFAULT_SCORE);
|
||||
System.err.println("Warning: " + word + " csv not found, using default scores.");
|
||||
}
|
||||
map.put(s, new WordDifficulty(s, score));
|
||||
}
|
||||
}
|
||||
|
||||
var words = map.values().stream().collect(Collectors.toCollection(ArrayList::new));
|
||||
// Sort words by difficulty in ascending order
|
||||
words.sort(Comparator.comparingInt(wd -> wd.difficulty));
|
||||
|
||||
var dictWords = new ArrayList<String>();
|
||||
for (var wd : words) {
|
||||
dictWords.add(wd.word);
|
||||
}
|
||||
|
||||
var index = new HashMap<Integer, DictEntry>();
|
||||
var lenCounts = new HashMap<Integer, Integer>();
|
||||
|
||||
for (var w : dictWords) {
|
||||
var L = w.length();
|
||||
for (var w : words) {
|
||||
var L = w.word.length();
|
||||
lenCounts.put(L, lenCounts.getOrDefault(L, 0) + 1);
|
||||
|
||||
var entry = index.get(L);
|
||||
@@ -245,15 +244,15 @@ public class SwedishGenerator {
|
||||
}
|
||||
|
||||
var idx = entry.words.size();
|
||||
entry.words.add(w);
|
||||
entry.words.add(w.word);
|
||||
|
||||
for (var i = 0; i < L; i++) {
|
||||
var letter = w.charAt(i) - 'A';
|
||||
var letter = w.word.charAt(i) - 'A';
|
||||
if (letter >= 0 && letter < 26) entry.pos[i][letter].add(idx);
|
||||
}
|
||||
}
|
||||
|
||||
return new Dict(dictWords, index, lenCounts);
|
||||
return new Dict(map, index, lenCounts);
|
||||
}
|
||||
|
||||
static int[] intersectSorted(int[] a, int aLen, int[] b, int bLen) {
|
||||
@@ -271,10 +270,8 @@ public class SwedishGenerator {
|
||||
return Arrays.copyOf(out, k);
|
||||
}
|
||||
|
||||
static final class CandidateInfo {
|
||||
static final record CandidateInfo(int[] indices, int count) {
|
||||
|
||||
int[] indices; // null => unconstrained
|
||||
int count;
|
||||
}
|
||||
static CandidateInfo candidateInfoForPattern(DictEntry entry, char[] pattern /* 0 means null */) {
|
||||
var lists = new ArrayList<IntList>();
|
||||
@@ -285,11 +282,8 @@ public class SwedishGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
var ci = new CandidateInfo();
|
||||
if (lists.isEmpty()) {
|
||||
ci.indices = null;
|
||||
ci.count = entry.words.size();
|
||||
return ci;
|
||||
return new CandidateInfo(null, entry.words.size());
|
||||
}
|
||||
|
||||
var first = lists.get(0);
|
||||
@@ -305,9 +299,7 @@ public class SwedishGenerator {
|
||||
if (curLen == 0) break;
|
||||
}
|
||||
|
||||
ci.indices = cur;
|
||||
ci.count = curLen;
|
||||
return ci;
|
||||
return new CandidateInfo(cur, curLen);
|
||||
}
|
||||
static int indexToDifficulty(DictEntry entry, int index, Map<String, Integer> llmScores) {
|
||||
var word = entry.words.get(index);
|
||||
@@ -317,19 +309,10 @@ public class SwedishGenerator {
|
||||
|
||||
// ---------------- Slots ----------------
|
||||
|
||||
static final class Slot {
|
||||
static record Slot(int clueR, int clueC, char dir, int[] rs, int[] cs, int len) {
|
||||
|
||||
final int clueR, clueC;
|
||||
final char dir; // '1'..'5'
|
||||
final int[] rs, cs; // cells
|
||||
final int len;
|
||||
Slot(int clueR, int clueC, char dir, int[] rs, int[] cs) {
|
||||
this.clueR = clueR;
|
||||
this.clueC = clueC;
|
||||
this.dir = dir;
|
||||
this.rs = rs;
|
||||
this.cs = cs;
|
||||
this.len = rs.length;
|
||||
public Slot(int clueR, int clueC, char dir, int[] rs, int[] cs) {
|
||||
this(clueR, clueC, dir, rs, cs, rs.length);
|
||||
}
|
||||
String key() { return clueR + "," + clueC + ":" + dir; }
|
||||
}
|
||||
@@ -917,7 +900,7 @@ public class SwedishGenerator {
|
||||
}
|
||||
|
||||
// ---------------- Top-level generatePuzzle ----------------
|
||||
public record PuzzleResult(char[][] mask, FillResult filled) { }
|
||||
public record PuzzleResult(Dict dict, char[][] mask, FillResult filled) { }
|
||||
|
||||
public static PuzzleResult generatePuzzle(Main.Opts opts) {
|
||||
var llmScores = loadScores();
|
||||
@@ -940,7 +923,7 @@ public class SwedishGenerator {
|
||||
|
||||
if (filled.ok && (opts.minSimplicity <= 0 || filled.simplicity >= opts.minSimplicity)) {
|
||||
System.out.println("\nSolution found on attempt " + attempt);
|
||||
return new PuzzleResult(mask, filled);
|
||||
return new PuzzleResult(dict, mask, filled);
|
||||
}
|
||||
throw new RuntimeException("No solution found in attempt " + attempt);
|
||||
});
|
||||
@@ -970,7 +953,7 @@ public class SwedishGenerator {
|
||||
System.out.printf(Locale.ROOT, "FILL: %.3fms | Simplicity: %.2f%n", (tFill1 - tFill0) / 1e6, filled.simplicity);
|
||||
|
||||
if (filled.ok && (opts.minSimplicity <= 0 || filled.simplicity >= opts.minSimplicity)) {
|
||||
return new PuzzleResult(mask, filled);
|
||||
return new PuzzleResult(dict, mask, filled);
|
||||
}
|
||||
if (filled.ok) {
|
||||
System.out.printf(Locale.ROOT, "Puzzle simplicity %.2f is below min %.2f, retrying...%n",
|
||||
|
||||
@@ -65,7 +65,7 @@ public class ThemePoolBuilderLength {
|
||||
|
||||
String endpoint = "https://jarvis-lan.appmodel.nl/api/ollama/";
|
||||
List<String> feeds = new ArrayList<>(DEFAULT_FEEDS);
|
||||
String outDir = "/data/puzzle";
|
||||
String outDir = System.getenv("OUT_DIR") != null ? System.getenv("OUT_DIR") : "/data/puzzle";
|
||||
int bridgeN = 30000;
|
||||
int themeN = 800;
|
||||
int relatedN = 2200;
|
||||
|
||||
Reference in New Issue
Block a user