Gather data

This commit is contained in:
mike
2026-01-04 01:37:42 +01:00
parent 795067472f
commit 3e25ce3e1f
22 changed files with 233 additions and 1414 deletions

View File

@@ -1,222 +0,0 @@
package puzzle;
import java.io.IOException;
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 = "/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(",", 4);
if (parts.length >= 4) {
var word = parts[0].trim().toUpperCase(Locale.ROOT);
var rawClue = parts[3].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 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();
}
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);
}
}
}
}
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");
}
}

View File

@@ -1,532 +0,0 @@
package puzzle;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.io.*;
import java.time.*;
import java.util.concurrent.atomic.*;
/**
* CONCURRENT MULTI-ENDPOINT Dutch Wordlist Scorer
* Distributes batches across Ollama, LM-Studio, and a third endpoint simultaneously
*/
public class ConcurrentWordScorer {
// ===== CONFIGURATION =====
private static final String INPUT_WORDLIST = "word-list.txt";
private static final String OUTPUT_SCORES = "word_scores.csv";
private static final int BATCH_SIZE = 10; // Even smaller for the difficult remaining words
private static final int MAX_RETRIES = 3;
// Define all three endpoints
private static final LLMEndpoint[] ENDPOINTS = {
new OllamaEndpoint(),
new LMStudioEndpoint(),
new LMStudioEndpoint("LM-Studio", "http://192.168.1.74:1234/v1/chat/completions",
"mistralai/mistral-nemo-instruct-2407", 1)
// new CustomEndpoint()
};
// ===== ENDPOINT CLASSES =====
abstract static class LLMEndpoint {
String name;
String baseUrl;
String model;
Semaphore rateLimiter; // Per-endpoint rate limiting
int maxConcurrent;
LLMEndpoint(String name, String baseUrl, String model, int maxConcurrent) {
this.name = name;
this.baseUrl = baseUrl;
this.model = model;
this.maxConcurrent = maxConcurrent;
this.rateLimiter = new Semaphore(maxConcurrent);
}
abstract String buildRequestJson(String prompt);
abstract String extractResponseContent(String responseBody);
// Rate-limited request execution
List<WordScore> execute(List<String> batch) throws Exception {
rateLimiter.acquire(); // Wait for slot
try {
return executeInternal(batch);
} finally {
rateLimiter.release();
}
}
private List<WordScore> executeInternal(List<String> batch) throws Exception {
var prompt = createScoringPrompt(batch);
var jsonRequest = buildRequestJson(prompt);
var responseBody = curlPostJson(baseUrl, jsonRequest, 120);
var content = extractResponseContent(responseBody);
if (content == null || content.isEmpty()) {
throw new IOException("[" + name + "] Empty response content");
}
return parseScoresFromReply(batch, content, name);
}
}
static class OllamaEndpoint
extends LLMEndpoint {
OllamaEndpoint() {
super("Ollama", "http://localhost:11434/api/chat",
"qwen2.5:14b", 1); // 2 concurrent requests
}
@Override String buildRequestJson(String prompt) {
return String.format("{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],\"stream\":false,\"temperature\":0.1}",
model, escapeJson(prompt));
}
@Override String extractResponseContent(String responseBody) {
// Ollama uses "message" -> "content"
var start = responseBody.indexOf("\"content\":\"") + 11;
var end = responseBody.indexOf("\"", start);
if (start < 11 || end < 0) return "";
return responseBody.substring(start, end).replace("\\n", "\n");
}
}
static class LMStudioEndpoint
extends LLMEndpoint {
LMStudioEndpoint() {
super("LM-Studio", "http://192.168.1.159:1234/v1/chat/completions",
"mistralai/mistral-nemo-instruct-2407", 1); // LM-Studio can handle more
}
public LMStudioEndpoint(String s, String url, String s1, int i) {
super(
s, url, s1, i
);
}
@Override String buildRequestJson(String prompt) {
return String.format("{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],\"temperature\":0.1,\"max_tokens\":2048}",
model, escapeJson(prompt));
}
@Override String extractResponseContent(String responseBody) {
return extractChatContent(responseBody);
}
}
static class CustomEndpoint
extends LLMEndpoint {
CustomEndpoint() {
super("Custom", "http://192.168.1.74:1234/v1/chat/completions",
"qwen2.5-vl-7b-abliterated-caption-it_gguf", 2);
}
@Override String buildRequestJson(String prompt) {
// Adapt to your third endpoint's format
return new LMStudioEndpoint().buildRequestJson(prompt);
}
@Override String extractResponseContent(String responseBody) {
return new LMStudioEndpoint().extractResponseContent(responseBody);
}
}
// ===== MAIN COORDINATOR =====
static void main(String[] args) throws Exception {
System.out.println("=== CONCURRENT 3-Endpoint Scorer ===");
for (var ep : ENDPOINTS) {
System.out.printf("- %s: %s%n", ep.name, ep.baseUrl);
}
System.out.println();
cleanupOutputFile();
// Load work queue
var allWords = Files.readAllLines(Paths.get(INPUT_WORDLIST));
var scoredWords = loadAlreadyScoredWords();
var workQueue = createWorkQueue(allWords, scoredWords);
System.out.printf("Total words: %d | Already scored: %d | Remaining: %d%n%n",
allWords.size(), scoredWords.size(), workQueue.size());
if (workQueue.isEmpty()) {
System.out.println("All done!");
return;
}
// Start result writer thread
BlockingQueue<List<WordScore>> resultQueue = new LinkedBlockingQueue<>();
var writerThread = startResultWriter(resultQueue);
// Start worker threads
var totalThreads = 0;
for (var ep : ENDPOINTS) totalThreads += ep.maxConcurrent;
var executor = Executors.newFixedThreadPool(totalThreads);
var totalProcessed = new AtomicInteger(scoredWords.size());
for (var endpoint : ENDPOINTS) {
for (var i = 0; i < endpoint.maxConcurrent; i++) {
executor.submit(() -> {
processBatches(endpoint, workQueue, resultQueue, totalProcessed, allWords.size());
});
}
}
// Wait for completion
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
// Signal writer to stop
resultQueue.put(Collections.singletonList(new WordScore(null, 0, "STOP")));
writerThread.join();
// Update hints in the database
System.out.println("\n✓ All endpoints finished!");
}
// ===== WORKER THREAD LOGIC =====
private static void processBatches(LLMEndpoint endpoint,
BlockingQueue<WorkItem> workQueue,
BlockingQueue<List<WordScore>> resultQueue,
AtomicInteger totalProcessed,
int totalWords) {
System.out.printf("[%s] Worker started%n", endpoint.name);
while (!Thread.currentThread().isInterrupted()) {
try {
var work = workQueue.poll(1, TimeUnit.SECONDS);
if (work == null) {
if (workQueue.isEmpty()) break; // No more work in queue
continue;
}
var scores = processWithRetry(endpoint, work.batch);
// Add metadata
scores.forEach(s -> {
s.endpoint = endpoint.name;
s.batchId = work.batchId;
});
resultQueue.put(scores);
// Progress update
var processed = totalProcessed.addAndGet(scores.size());
if (processed % 100 < BATCH_SIZE) { // Reduce console spam
System.out.printf("Progress: %d/%d (%.1f%%)%n",
processed, totalWords, (processed * 100.0 / totalWords));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
System.err.printf("[%s] Fatal error: %s%n", endpoint.name, e.getMessage());
break;
}
}
System.out.printf("[%s] Worker stopped%n", endpoint.name);
}
private static List<WordScore> processWithRetry(LLMEndpoint endpoint, List<String> batch) {
var retries = 0;
while (retries < MAX_RETRIES) {
try {
return endpoint.execute(batch);
} catch (Exception e) {
retries++;
System.err.printf("[%s] Attempt %d/%d failed: %s%n",
endpoint.name, retries, MAX_RETRIES, e.getMessage());
if (retries >= MAX_RETRIES) {
return createFailedScores(batch, endpoint.name);
}
try {
Thread.sleep(2000L * retries);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return createFailedScores(batch, endpoint.name);
}
}
}
return createFailedScores(batch, endpoint.name);
}
// ===== RESULT WRITER THREAD =====
private static Thread startResultWriter(BlockingQueue<List<WordScore>> resultQueue) throws Exception {
var writer = new BufferedWriter(new FileWriter(OUTPUT_SCORES, true));
var isNew = Files.size(Paths.get(OUTPUT_SCORES)) == 0;
if (isNew) {
writer.write("word,score,status,endpoint,batch_id,timestamp\n");
writer.flush();
}
var thread = new Thread(() -> {
try {
while (true) {
var scores = resultQueue.take();
// Stop signal
if (scores.size() == 1 && scores.get(0).status.equals("STOP")) {
break;
}
writeBatch(writer, scores);
}
writer.close();
} catch (Exception e) {
System.err.println("Writer thread error: " + e.getMessage());
}
});
thread.start();
return thread;
}
private static synchronized void writeBatch(BufferedWriter writer, List<WordScore> scores) throws Exception {
var timestamp = Instant.now().toString();
for (var ws : scores) {
writer.write(String.format("%s,%d,%s,%s,%d,%s\n",
ws.word, ws.score, ws.status, ws.endpoint, ws.batchId, timestamp));
}
writer.flush();
}
// ===== QUEUE & DATA STRUCTURES =====
record WorkItem(int batchId, List<String> batch) {
}
private static BlockingQueue<WorkItem> createWorkQueue(List<String> allWords, Set<String> scored) {
BlockingQueue<WorkItem> queue = new LinkedBlockingQueue<>();
var batchId = 0;
for (var i = 0; i < allWords.size(); i += BATCH_SIZE) {
List<String> batch = new ArrayList<>();
for (var j = i; j < Math.min(i + BATCH_SIZE, allWords.size()); j++) {
var word = allWords.get(j);
if (!scored.contains(word.toLowerCase())) {
batch.add(word);
}
}
if (!batch.isEmpty()) {
queue.add(new WorkItem(batchId++, batch));
}
}
return queue;
}
// ===== LOADING & PARSING =====
private static Set<String> loadAlreadyScoredWords() throws Exception {
Set<String> scored = new HashSet<>();
var file = new File(OUTPUT_SCORES);
if (!file.exists()) return scored;
var lines = Files.readAllLines(file.toPath());
var first = true;
for (var line : lines) {
if (first) {
first = false;
continue;
}
var parts = line.split(",");
if (parts.length >= 3) {
var word = parts[0].trim().toLowerCase();
var status = parts[2].trim();
if ("OK".equalsIgnoreCase(status)) {
scored.add(word);
}
}
}
return scored;
}
private static void cleanupOutputFile() throws IOException {
var path = Paths.get(OUTPUT_SCORES);
if (!Files.exists(path)) return;
System.out.println("Cleaning up " + OUTPUT_SCORES + "...");
var lines = Files.readAllLines(path);
if (lines.isEmpty()) return;
var header = lines.get(0);
Map<String, String> latestOkEntries = new LinkedHashMap<>();
for (int i = 1; i < lines.size(); i++) {
var line = lines.get(i);
var parts = line.split(",");
if (parts.length >= 3) {
var word = parts[0].trim().toLowerCase();
var status = parts[2].trim();
if ("OK".equalsIgnoreCase(status)) {
latestOkEntries.put(word, line);
}
}
}
var cleanedLines = new ArrayList<String>();
cleanedLines.add(header);
cleanedLines.addAll(latestOkEntries.values());
Files.write(path, cleanedLines, StandardCharsets.UTF_8);
System.out.printf("Cleanup complete. Kept %d unique OK entries. Removed %d non-OK or duplicate entries.%n",
latestOkEntries.size(), lines.size() - cleanedLines.size());
}
private static List<WordScore> createFailedScores(List<String> words, String endpoint) {
List<WordScore> failed = new ArrayList<>();
for (var word : words) {
failed.add(new WordScore(word, -1, "FAILED", endpoint, -1));
}
return failed;
}
// Parsing logic
private static List<WordScore> parseScoresFromReply(List<String> expectedWords, String reply, String endpointName) {
Map<String, Integer> wordScoreMap = new HashMap<>();
var lines = reply.split("\n");
for (var line : lines) {
line = line.trim();
// Handle formats like "1. word:score", "word: score", "word - score"
String sep = null;
if (line.contains(":")) sep = ":";
else if (line.contains("-")) sep = "-";
if (sep != null) {
var parts = line.split(sep, 2);
if (parts.length == 2) {
var wordPart = parts[0].trim();
// Remove leading numbering like "1. " or bullets like "* ", "- "
wordPart = wordPart.replaceAll("^[\\d+.)*\\-\\s]+", "");
var word = wordPart.toLowerCase();
try {
var scoreStr = parts[1].trim();
// Handle potential non-numeric junk after the number
scoreStr = scoreStr.replaceAll("[^0-9].*", "");
if (!scoreStr.isEmpty()) {
var score = Integer.parseInt(scoreStr);
wordScoreMap.put(word, Math.max(1, Math.min(10, score)));
}
} catch (NumberFormatException e) {
// Skip invalid lines
}
}
}
}
// Match scores to original words (maintaining order)
List<WordScore> results = new ArrayList<>();
for (var word : expectedWords) {
var score = wordScoreMap.get(word.toLowerCase());
if (score != null) {
results.add(new WordScore(word, score, "OK"));
} else {
results.add(new WordScore(word, -1, "MISSING"));
}
}
return results;
}
// Prompt creation
private static String createScoringPrompt(List<String> words) {
return "Je bent een Nederlandse taalexpert. Geef elk van de " + words.size() + " onderstaande woorden een populariteitsscore van 1 (zeer zeldzaam) tot 10 (zeer algemeen).\n\n" +
"Output ALLEEN in dit formaat:\n" +
"woord1:score\n" +
"woord2:score\n\n" +
"GEEN andere tekst of uitleg. Sla GEEN woorden over.\n\n" +
"Lijst:\n" +
String.join("\n", words);
}
// Utility methods
private static String escapeJson(String str) {
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n");
}
private static String curlPostJson(String url, String jsonBody, int timeoutSeconds) throws Exception {
// Write JSON body to temp file to avoid shell escaping issues
var tempFile = Files.createTempFile("lm-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) 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();
}
}

View File

@@ -39,7 +39,7 @@ public final class ExportFormat {
var word = clueMap.get(s.key());
if (word == null) continue;
var p = extractPlacedFromSlot(s, word);
var p = extractPlacedFromSlot(puz.dict(),s, word);
if (p == null) continue;
placed.add(p);
}
@@ -121,7 +121,7 @@ public final class ExportFormat {
/**
* Convert a generator Slot + assigned word into a Placed object for export.
*/
private static Placed extractPlacedFromSlot(Slot s, String word) {
private static Placed extractPlacedFromSlot(Dict dict,Slot s, String word) {
int r = s.clueR();
int c = s.clueC();
char d = s.dir();
@@ -168,7 +168,7 @@ public final class ExportFormat {
return new Placed(
word,
word, // clue placeholder
dict.words().get(word).clue(), // clue placeholder
startRow,
startCol,
direction,
@@ -182,14 +182,9 @@ public final class ExportFormat {
}
// pack (r,c) into one long key (handles negatives too)
private static long pack(int r, int c) {
return (((long) r) << 32) ^ (c & 0xFFFFFFFFL);
}
// ---------- Data models ----------
private static long pack(int r, int c) { return (((long) r) << 32) ^ (c & 0xFFFFFFFFL); }
/**
* @param direction "horizontal" | "vertical"
* @param direction "h" | "v"
* @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,
@@ -197,8 +192,7 @@ public final class ExportFormat {
public record Rewards(int coins, int stars, int hints) { }
/**
* @param direction "horizontal" | "vertical" */
/// @param direction "h" | "v"
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) { }

View File

@@ -83,7 +83,7 @@ public class Main {
section("Clues");
info("status : generating...");
info("generatedFor : " + exported.words().size());
exported = ClueGenerator.applyClues(exported);
//exported = ClueGenerator.applyClues(exported);
info("status : done");
section("Words");

View File

@@ -132,24 +132,22 @@ public class SwedishGenerator {
int[] data() { return a; } // note: may have extra capacity
}
static final class DictEntry {
static record DictEntry(ArrayList<String> words, IntList[][] pos) {
final ArrayList<String> words = new ArrayList<>();
final IntList[][] pos; // pos[i][letter] -> indices (sorted by insertion)
DictEntry(int L) {
pos = new IntList[L][26];
public DictEntry(int L) {
this(new ArrayList<>(), new IntList[L][26]);
for (var i = 0; i < L; i++) {
for (var j = 0; j < 26; j++) pos[i][j] = new IntList();
}
}
}
static record WordDifficulty(String word, int difficulty, int simpel, int score, int cross) {
static record WordDifficulty(String word, int difficulty, int simpel, int score, int cross, String clue) {
public WordDifficulty(String word, int simpel, int score) {
public WordDifficulty(String word, int simpel, int score, String clue) {
var difficulty1 = 0 + ((8 - word.length()) * 30) + ((10 - score) * 15);
var crossScore = ThemePoolBuilderLength.crossabilityScore(word);
this(word, difficulty1, simpel, score, (crossScore * 7) + ((score) * 30) + ((word.length()) * 15));
this(word, difficulty1, simpel, score, (crossScore * 7) + ((score) * 30) + ((word.length()) * 15), clue);
// Prioritize simple words (high lScore) and long words.
// lScore (1-10) adds up to 1000 points (weight 100).
@@ -163,7 +161,6 @@ public class SwedishGenerator {
}
}
public static record Dict(Map<String, WordDifficulty> words,
HashMap<Integer, DictEntry> index,
HashMap<Integer, Integer> lenCounts) { }
@@ -193,8 +190,12 @@ public class SwedishGenerator {
// CSV has level 1-10. llmScores use 10-level.
score = 10 - Integer.parseInt(parts[1].trim());
simpel = Integer.parseInt(parts[2].trim());
var rawClue = parts[3].trim();
if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) {
rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\"");
}
if (score >= 1)
map.put(s, new WordDifficulty(s, simpel, score));
map.put(s, new WordDifficulty(s, simpel, score, rawClue));
}
}
var words = map.values().stream().collect(Collectors.toCollection(ArrayList::new));
@@ -682,11 +683,10 @@ public class SwedishGenerator {
System.out.flush();
};
class Pick {
record Pick(Slot slot,
CandidateInfo info,
boolean done) {
Slot slot;
CandidateInfo info;
boolean done;
}
java.util.function.Supplier<Pick> chooseMRV = () -> {
@@ -699,22 +699,14 @@ public class SwedishGenerator {
var entry = dictIndex.get(s.len);
if (entry == null) {
var p = new Pick();
p.slot = null;
p.info = null;
p.done = false;
return p;
return new Pick(null, null, false);
}
var pat = patternForSlot(grid, s);
var info = candidateInfoForPattern(entry, pat);
if (info.count == 0) {
var p = new Pick();
p.slot = null;
p.info = null;
p.done = false;
return p;
return new Pick(null, null, false);
}
if (best == null
@@ -726,17 +718,11 @@ public class SwedishGenerator {
}
}
var p = new Pick();
if (best == null) {
p.slot = null;
p.info = null;
p.done = true;
return new Pick(null, null, true);
} else {
p.slot = best;
p.info = bestInfo;
p.done = false;
return new Pick(best, bestInfo, false);
}
return p;
};
final var MAX_TRIES_PER_SLOT = 2000;
@@ -868,9 +854,9 @@ public class SwedishGenerator {
public record PuzzleResult(Dict dict, char[][] mask, FillResult filled) { }
public static PuzzleResult generatePuzzle(Main.Opts opts) {
var tLoad0 = System.nanoTime();
var dict = loadWords(opts.wordsPath);
var tLoad1 = System.nanoTime();
var tLoad0 = System.nanoTime();
var dict = loadWords(opts.wordsPath);
var tLoad1 = System.nanoTime();
System.out.printf(Locale.ROOT, "LOAD_WORDS: %.3fs%n %s words%n", (tLoad1 - tLoad0) / 1e9, dict.words.size());
if (opts.threads > 1) {

View File

@@ -1,24 +0,0 @@
package puzzle;
// ===== DATA CLASS =====
class WordScore {
String word;
int score;
String status;
String endpoint;
int batchId;
WordScore(String word, int score, String status, String endpoint, int batchId) {
this.word = word;
this.score = score;
this.status = status;
this.endpoint = endpoint;
this.batchId = batchId;
}
WordScore(String word, int score, String status) {
this.word = word;
this.score = score;
this.status = status;
}
}