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 java.util.function.Predicate; /** * SwedishGenerator.java * * Usage: * javac SwedishGenerator.java * java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt] */ @SuppressWarnings("ALL") public record SwedishGenerator(int W, int H, int SIZE, int MAX_LEN) { record nbrs_8(int x, int y) { } class Data { static byte[] EXAMPLE = new byte[0]; } public SwedishGenerator { Data.EXAMPLE = new byte[SIZE]; Arrays.fill(Data.EXAMPLE, (byte) '#'); } public SwedishGenerator(int W, int H) { this(W, H, W * H, Math.min(W, H)); } public SwedishGenerator() { this(9, 8); } static final int CLUE_SIZE = 4, SIMPLICITY_DEFAULT_SCORE = 2; static final int MIN_LEN = 2; static final int MAX_TRIES_PER_SLOT = 2000; // Directions for '1'..'6' static final nbrs_8[] OFFSETS = new nbrs_8[7]; static final nbrs_8[] STEPS = new nbrs_8[7]; static { // 1: up OFFSETS[1] = new nbrs_8(-1, 0); STEPS[1] = new nbrs_8(-1, 0); // 2: right OFFSETS[2] = new nbrs_8(0, 1); STEPS[2] = new nbrs_8(0, 1); // 3: down OFFSETS[3] = new nbrs_8(1, 0); STEPS[3] = new nbrs_8(1, 0); // 4: left OFFSETS[4] = new nbrs_8(0, -1); STEPS[4] = new nbrs_8(0, -1); // 5: vertical down, clue is on the right of the first letter OFFSETS[5] = new nbrs_8(0, -1); STEPS[5] = new nbrs_8(1, 0); // 6: vertical down, clue is on the left of the first letter OFFSETS[6] = new nbrs_8(0, 1); STEPS[6] = new nbrs_8(1, 0); } final static nbrs_8[] nbrs8 = new nbrs_8[]{ new nbrs_8(-1, -1), new nbrs_8(-1, 0), new nbrs_8(-1, 1), new nbrs_8(0, -1), new nbrs_8(0, 1), new nbrs_8(1, -1), new nbrs_8(1, 0), new nbrs_8(1, 1) }; static final nbrs_8[] nbrs4 = new nbrs_8[]{ new nbrs_8(-1, 0), new nbrs_8(1, 0), new nbrs_8(0, -1), new nbrs_8(0, 1) }; static final char FIRST_ABC = 'A'; static final char LAST_ABC = 'Z'; static final char FIRST_ARROW = '1', LAST_ARROW = '6', HOR_ARROW_1 = '2', HOR_ARROW_2 = '4'; static boolean isDigit(char ch) { return ch >= FIRST_ARROW && ch <= LAST_ARROW; } static boolean isLetter(char ch) { return ch >= FIRST_ABC && ch <= LAST_ABC; } static final class Rng { private int x; Rng(int seed) { var s = seed; if (s == 0) s = 1; this.x = s; } int nextU32() { var y = x; y ^= (y << 13); y ^= (y >>> 17); y ^= (y << 5); x = y; return y; } int randint(int min, int max) { // inclusive var u = (nextU32() & 0xFFFFFFFFL); var range = (long) max - (long) min + 1L; return (int) (min + (u % range)); } double nextFloat() { return (nextU32() & 0xFFFFFFFFL) / 4294967295.0; } } static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); } static final record CandidateInfo(int[] indices, int count) { } record Grid(byte[] g, int H, int W) { Grid deepCopyGrid() { return new Grid(g.clone(), H, W); } int getOffset(int r, int c) { return r * W + c; } boolean isLettercell(int r, int c) { return !isDigitAt(r, c); } char getCharAt(int r, int c) { return (char) (g[getOffset(r, c)] & 0xFF); } void setCharAt(int r, int c, char ch) { g[getOffset(r, c)] = (byte) ch; } static final byte _1 = 49, _9 = 57, A = 65, Z = 90; boolean isLetterAt(int r, int c) { byte ch = g[getOffset(r, c)]; return ch >= A && ch <= Z; } boolean isDigitAt(int r, int c) { byte ch = g[getOffset(r, c)]; return ch >= _1 && ch <= _9; } } Grid makeEmptyGrid() { return new Grid(Data.EXAMPLE.clone(), H, W); } String gridToString(Grid g) { var sb = new StringBuilder(); for (var r = 0; r < H; r++) { if (r > 0) sb.append('\n'); for (var c = 0; c < W; c++) sb.append(g.getCharAt(r, c)); } return sb.toString(); } public String renderHuman(Grid g) { var sb = new StringBuilder(); for (var r = 0; r < H; r++) { if (r > 0) sb.append('\n'); for (var c = 0; c < W; c++) { var ch = g.getCharAt(r, c); sb.append(isDigit(ch) ? ' ' : ch); } } return sb.toString(); } static final class IntList { int[] a = new int[8]; int n = 0; void add(int v) { if (n >= a.length) a = Arrays.copyOf(a, a.length * 2); a[n++] = v; } int size() { return n; } int[] data() { return a; } } static record DictEntry(ArrayList words, IntList[][] pos) { 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 Lemma(int index, String word, int length, int difficulty, int simpel, int score, ArrayList clue) { static int LEMMA_COUNTER = 0; public Lemma(int index, String word, int simpel, int score, String clue) { var complex = 0 + ((8 - word.length()) * 30) + ((10 - score) * 15); var list = new ArrayList(10); list.add(clue); this(index, word, word.length(), complex, simpel, score, list); } public Lemma(String word, int simpel, int score, String clue) { this(LEMMA_COUNTER++, word, simpel, score, clue); } char charAt(int idx) { return word.charAt(idx); } @Override public int hashCode() { return index; } @Override public boolean equals(Object o) { return (o == this) || (o instanceof Lemma l && l.index == index); } } public static record Dict(Lemma[] wordz, DictEntry[] index, int[] lenCounts) { public Dict(Lemma[] wordz) { // Sort words by difficulty in ascending order Lemma[] lemmas = wordz.clone(); Arrays.sort(lemmas, Comparator.comparingInt(wd -> wd.simpel)); var lenCounts = new int[12]; var index = new DictEntry[12]; Arrays.setAll(index, i -> new DictEntry(i)); int maxLength = -1; for (var lemma : lemmas) { var L = lemma.length(); if (L > maxLength) maxLength = L; lenCounts[L]++; var entry = index[L]; var idx = entry.words.size(); entry.words.add(lemma); for (var i = 0; i < L; i++) { var letter = lemma.charAt(i) - 'A'; if (letter >= 0 && letter < 26) entry.pos[i][letter].add(idx); else throw new RuntimeException("Illegal letter: " + letter + " in word " + lemma); } } this(wordz, index, lenCounts); } } static Dict loadWords(String wordsPath) { String raw; try { raw = Files.readString(Path.of(wordsPath), StandardCharsets.UTF_8); } catch (IOException e) { e.printStackTrace(); 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 map = new HashMap(); boolean first = true; for (var line : raw.split("\\R")) { if (line.isBlank()) continue; var parts = line.split(",", 4); 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}$")) { // CSV has level 1-10. llmScores use 10-level. int score = 10 - Integer.parseInt(parts[1].trim()); int 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) { if (map.containsKey(s)) { map.get(s).clue.add(rawClue); } else { map.put(s, new Lemma(s, simpel, score, rawClue)); } } } else { System.err.println("Invalid word: " + line); } } return new Dict(map.values().toArray(Lemma[]::new)); } static int[] intersectSorted(int[] a, int aLen, int[] b, int bLen) { var out = new int[Math.min(aLen, bLen)]; int i = 0, j = 0, k = 0; while (i < aLen && j < bLen) { int x = a[i], y = b[j]; if (x == y) { out[k++] = x; i++; j++; } else if (x < y) i++; else j++; } return Arrays.copyOf(out, k); } static CandidateInfo candidateInfoForPattern(DictEntry entry, char[] pattern /* 0 means null */) { var lists = new ArrayList(); for (var i = 0; i < pattern.length; i++) { var ch = pattern[i]; if (ch != 0 && isLetter(ch)) { lists.add(entry.pos[i][ch - 'A']); } } if (lists.isEmpty()) { return new CandidateInfo(null, entry.words.size()); } var first = lists.get(0); var cur = Arrays.copyOf(first.data(), first.size()); var curLen = cur.length; for (var k = 1; k < lists.size(); k++) { var nxt = lists.get(k); var nextArr = nxt.data(); var nextLen = nxt.size(); cur = intersectSorted(cur, curLen, nextArr, nextLen); curLen = cur.length; if (curLen == 0) break; } return new CandidateInfo(cur, curLen); } static record Slot(String key, int clueR, int clueC, char dir, int[] rs, int[] cs, int len, boolean horiz) { public Slot(int clueR, int clueC, char dir, int[] rs, int[] cs) { this(clueR + "," + clueC + ":" + dir, clueR, clueC, dir, rs, cs, rs.length, dir == HOR_ARROW_1 || dir == HOR_ARROW_2); } } ArrayList extractSlots(Grid grid) { var slots = new ArrayList(); for (var r = 0; r < H; r++) { for (var c = 0; c < W; c++) { var d = grid.getCharAt(r, c); if (!isDigit(d)) continue; var dir = d - '0'; int or = OFFSETS[dir].x, oc = OFFSETS[dir].y; int dr = STEPS[dir].x, dc = STEPS[dir].y; int rr = r + or, cc = c + oc; if (rr < 0 || rr >= H || cc < 0 || cc >= W) continue; if (grid.isDigitAt(rr, cc)) continue; var rs = new int[MAX_LEN + 1]; var cs = new int[MAX_LEN + 1]; var n = 0; while (rr >= 0 && rr < H && cc >= 0 && cc < W) { if (grid.isDigitAt(rr, cc)) break; rs[n] = rr; cs[n] = cc; n++; rr += dr; cc += dc; if (n > MAX_LEN) break; } slots.add(new Slot(r, c, d, Arrays.copyOf(rs, n), Arrays.copyOf(cs, n))); } } return slots; } boolean hasRoomForClue(Grid grid, int r, int c, char d) { var di = d - '0'; int or = OFFSETS[di].x, oc = OFFSETS[di].y; int dr = STEPS[di].x, dc = STEPS[di].y; int rr = r + or, cc = c + oc; var run = 0; while (rr >= 0 && rr < H && cc >= 0 && cc < W && (grid.isLettercell(rr, cc)) && run < MAX_LEN) { run++; rr += dr; cc += dc; } return run >= MIN_LEN; } long maskFitness(Grid grid, int[] lenCounts) { long penalty = 0; var clueCount = 0; for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) if (grid.isDigitAt(r, c)) clueCount++; var targetClues = (int) Math.round(SIZE * 0.25); // ~18 penalty += 8L * Math.abs(clueCount - targetClues); var slots = extractSlots(grid); if (slots.isEmpty()) return 1_000_000_000L; var covH = new int[H][W]; var covV = new int[H][W]; for (var s : slots) { if (s.len < MIN_LEN) penalty += 8000; if (s.len > MAX_LEN) penalty += 8000 + (long) (s.len - MAX_LEN) * 500L; if (s.len >= MIN_LEN && s.len <= MAX_LEN) { if (lenCounts[s.len] <= 0) penalty += 12000; } for (var i = 0; i < s.len; i++) { int r = s.rs[i], c = s.cs[i]; if (s.horiz) covH[r][c] += 1; else covV[r][c] += 1; } } for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) { if (grid.isDigitAt(r, c)) continue; int h = covH[r][c], v = covV[r][c]; if (h == 0 && v == 0) penalty += 1500; else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty += 200; else penalty += 600; } // clue clustering (8-connected) var seen = new boolean[H][W]; var stack = new int[SIZE]; int sp; for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) { if (!grid.isDigitAt(r, c) || seen[r][c]) continue; sp = 0; stack[sp++] = r * W + c; seen[r][c] = true; var size = 0; while (sp > 0) { var p = stack[--sp]; int x = p / W, y = p % W; size++; for (var d : nbrs8) { int nx = x + d.x, ny = y + d.y; if (nx < 0 || nx >= H || ny < 0 || ny >= W) continue; if (seen[nx][ny]) continue; if (!grid.isDigitAt(nx, ny)) continue; seen[nx][ny] = true; stack[sp++] = nx * W + ny; } } if (size >= 2) penalty += (long) (size - 1) * 120L; } // dead-end-ish letter cell (3+ walls) for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) { if (grid.isDigitAt(r, c)) continue; var walls = 0; for (var d : nbrs4) { int rr = r + d.x, cc = c + d.y; if (rr < 0 || rr >= H || cc < 0 || cc >= W) { walls++; continue; } if (grid.isDigitAt(rr, cc)) walls++; } if (walls >= 3) penalty += 400; } return penalty; } // ---------------- Mask generation ---------------- Grid randomMask(Rng rng) { var g = makeEmptyGrid(); var targetClues = (int) Math.round(SIZE * 0.25); int placed = 0, guard = 0; while (placed < targetClues && guard++ < 4000) { var r = rng.randint(0, H - 1); var c = rng.randint(0, W - 1); if (g.isDigitAt(r, c)) continue; var d = (char) ('0' + rng.randint(1, c == 0 ? CLUE_SIZE : 4)); g.setCharAt(r, c, d); if (!hasRoomForClue(g, r, c, d)) { g.setCharAt(r, c, '#'); continue; } placed++; } return g; } Grid mutate(Rng rng, Grid grid) { var g = grid.deepCopyGrid(); var cx = rng.randint(0, H - 1); var cy = rng.randint(0, W - 1); var steps = 4; for (var k = 0; k < steps; k++) { var rr = clamp(cx + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, H - 1); var cc = clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, W - 1); if (g.isDigitAt(rr, cc)) { g.setCharAt(rr, cc, '#'); } else { var d = (char) ('0' + rng.randint(1, cc == 0 ? CLUE_SIZE : 4)); g.setCharAt(rr, cc, d); if (!hasRoomForClue(g, rr, cc, d)) g.setCharAt(rr, cc, '#'); } } return g; } Grid crossover(Rng rng, Grid a, Grid b) { var out = makeEmptyGrid(); var cx = (H - 1) / 2.0; var cy = (W - 1) / 2.0; var theta = rng.nextFloat() * Math.PI; var nx = Math.cos(theta); var ny = Math.sin(theta); for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) { double x = r - cx, y = c - cy; var side = x * nx + y * ny; out.setCharAt(r, c, (side >= 0) ? a.getCharAt(r, c) : b.getCharAt(r, c)); } for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) { var ch = out.getCharAt(r, c); if (isDigit(ch) && !hasRoomForClue(out, r, c, ch)) out.setCharAt(r, c, '#'); } return out; } Grid hillclimb(Rng rng, Grid start, int[] lenCounts, int limit) { var best = start.deepCopyGrid(); var bestF = maskFitness(best, lenCounts); var fails = 0; while (fails < limit) { var cand = mutate(rng, best); var f = maskFitness(cand, lenCounts); if (f < bestF) { best = cand; bestF = f; fails = 0; } else { fails++; } } return best; } double similarity(Grid a, Grid b) { var same = 0; for (var r = 0; r < H; r++) for (var c = 0; c < W; c++) if (a.getCharAt(r, c) == b.getCharAt(r, c)) same++; return same / (double) (W * H); } public Grid generateMask(Rng rng, int[] lenCounts, int popSize, int gens, boolean verbose) { if (verbose) System.out.println("generateMask init pop: " + popSize); var pop = new ArrayList(); for (var i = 0; i < popSize; i++) { var g = randomMask(rng); pop.add(hillclimb(rng, g, lenCounts, 180)); } for (var gen = 0; gen < gens; gen++) { if (Thread.currentThread().isInterrupted()) break; var children = new ArrayList(); var pairs = Math.max(popSize, (int) Math.floor(popSize * 1.5)); for (var k = 0; k < pairs; k++) { var p1 = pop.get(rng.randint(0, pop.size() - 1)); var p2 = pop.get(rng.randint(0, pop.size() - 1)); var child = crossover(rng, p1, p2); children.add(hillclimb(rng, child, lenCounts, 70)); } pop.addAll(children); pop.sort(Comparator.comparingLong(g -> maskFitness(g, lenCounts))); var next = new ArrayList(); for (var cand : pop) { if (next.size() >= popSize) break; var ok = true; for (var kept : next) { if (similarity(cand, kept) > 0.92) { ok = false; break; } } if (ok) next.add(cand); } pop = next; if (verbose && gen % 10 == 0) { var bestF = maskFitness(pop.get(0), lenCounts); System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + bestF); } } pop.sort(Comparator.comparingLong(g -> maskFitness(g, lenCounts))); return pop.get(0); } // ---------------- Fill (CSP) ---------------- record Undo(int[] rs, int[] cs, char[] prev, int n) { } public static final class FillStats { public long nodes; public long backtracks; public double seconds; public int lastMRV; } record Pick(Slot slot, CandidateInfo info, boolean done) { } public static final record FillResult(boolean ok, Grid grid, HashMap clueMap, FillStats stats, double simplicity) { public FillResult(boolean ok, Grid grid, HashMap assigned, FillStats stats) { double totalSimplicity = 0; if (ok) { for (var w : assigned.values()) totalSimplicity += w.difficulty; totalSimplicity = assigned.isEmpty() ? 0 : totalSimplicity / assigned.size(); } this(ok, grid, assigned, stats, totalSimplicity); } } static char[] patternForSlot(Grid grid, Slot s) { var pat = new char[s.len]; for (var i = 0; i < s.len; i++) { var ch = grid.getCharAt(s.rs[i], s.cs[i]); pat[i] = isLetter(ch) ? ch : 0; } return pat; } static int slotScore(int[][] cellCount, Slot s) { var cross = 0; for (var i = 0; i < s.len; i++) cross += (cellCount[s.rs[i]][s.cs[i]] - 1); return cross * 10 + s.len; } static Undo placeWord(Grid grid, Slot s, Lemma w) { var urs = new int[s.len]; var ucs = new int[s.len]; var up = new char[s.len]; var n = 0; for (var i = 0; i < s.len; i++) { int r = s.rs[i], c = s.cs[i]; char cur = grid.getCharAt(r, c); var ch = w.charAt(i); if (cur == '#') { urs[n] = r; ucs[n] = c; up[n] = '#'; n++; grid.setCharAt(r, c, ch); } else { if (cur != ch) { // rollback immediate changes for (var j = 0; j < n; j++) grid.setCharAt(urs[j], ucs[j], up[j]); return null; } } } return new Undo(urs, ucs, up, n); } static void undoPlace(Grid grid, Undo u) { for (var i = 0; i < u.n; i++) grid.setCharAt(u.rs[i], u.cs[i], u.prev[i]); } public FillResult fillMask(Rng rng, Grid mask, DictEntry[] dictIndex, int logEveryMs, int timeLimitMs, boolean verbose) { var grid = mask.deepCopyGrid(); var allSlots = extractSlots(grid); var slots = new ArrayList(); for (var s : allSlots) if (s.len >= MIN_LEN && s.len <= MAX_LEN) slots.add(s); var used = new BitSet(); var assigned = new HashMap(); var cellCount = new int[H][W]; for (var s : slots) for (var i = 0; i < s.len; i++) cellCount[s.rs[i]][s.cs[i]]++; var t0 = System.currentTimeMillis(); final var lastLog = new java.util.concurrent.atomic.AtomicLong(t0); var stats = new FillStats(); final var TOTAL = slots.size(); final var BAR_LEN = 22; Runnable renderProgress = () -> { if (!verbose) return; var now = System.currentTimeMillis(); if ((now - lastLog.get()) < logEveryMs) return; lastLog.set(now); var done = assigned.size(); var pct = (TOTAL == 0) ? 100 : (int) Math.floor((done / (double) TOTAL) * 100); var filled = Math.min(BAR_LEN, (int) Math.floor((pct / 100.0) * BAR_LEN)); var bar = "[" + "#".repeat(filled) + "-".repeat(BAR_LEN - filled) + "]"; var elapsed = String.format(Locale.ROOT, "%.1fs", (now - t0) / 1000.0); var msg = String.format( Locale.ROOT, "%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s", bar, done, TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, elapsed ); System.out.print("\r" + padRight(msg, 120)); System.out.flush(); }; java.util.function.Supplier chooseMRV = () -> { Slot best = null; CandidateInfo bestInfo = null; for (var s : slots) { var k = s.key(); if (assigned.containsKey(k)) continue; var entry = dictIndex[s.len]; if (entry == null) { return new Pick(null, null, false); } var pat = patternForSlot(grid, s); var info = candidateInfoForPattern(entry, pat); if (info.count == 0) { return new Pick(null, null, false); } if (best == null || info.count < bestInfo.count || (info.count == bestInfo.count && slotScore(cellCount, s) > slotScore(cellCount, best))) { best = s; bestInfo = info; if (info.count <= 1) break; } } if (best == null) { return new Pick(null, null, true); } else { return new Pick(best, bestInfo, false); } }; IO.println("hit"); class Solver { boolean backtrack() { if (Thread.currentThread().isInterrupted()) return false; stats.nodes++; if (timeLimitMs > 0 && (System.currentTimeMillis() - t0) > timeLimitMs) return false; var pick = chooseMRV.get(); if (pick.done) return true; if (pick.slot == null) { stats.backtracks++; return false; } stats.lastMRV = pick.info.count; renderProgress.run(); var s = pick.slot; var k = s.key(); var entry = dictIndex[s.len]; var pat = patternForSlot(grid, s); Predicate tryWord = (Lemma w) -> { if (w == null) { IO.println("tryword null Error"); return false; } if (used.get(w.index())) return false; for (var i = 0; i < pat.length; i++) { if (pat[i] != 0 && pat[i] != w.charAt(i)) return false; } var undo = placeWord(grid, s, w); if (undo == null) return false; used.set(w.index()); assigned.put(k, w); if (backtrack()) return true; assigned.remove(k); used.set(w.index, false); undoPlace(grid, undo); return false; }; if (pick.info.indices != null && pick.info.indices.length > 0) { var idxs = pick.info.indices; var L = idxs.length; var tries = Math.min(MAX_TRIES_PER_SLOT, L); // When picking words from sorted indices, we want to favor the beginning // (lower difficulty) but still have some randomness. for (var t = 0; t < tries; t++) { // Bias strongly towards lower indices (simpler words) using r^3 double r = rng.nextFloat(); int idxInArray = (int) (r * r * r * L); var idx = idxs[idxInArray]; var w = entry.words.get(idx); if (tryWord.test(w)) return true; } stats.backtracks++; return false; } var N = entry.words.size(); if (N == 0) { stats.backtracks++; return false; } var tries = Math.min(MAX_TRIES_PER_SLOT, N); for (var t = 0; t < tries; t++) { double r = rng.nextFloat(); int idxInArray = (int) (r * r * r * N); var w = entry.words.get(idxInArray); if (tryWord.test(w)) return true; } stats.backtracks++; return false; } } // initial render (same feel) renderProgress.run(); var ok = new Solver().backtrack(); // final progress line System.out.print("\r" + padRight("", 120) + "\r"); System.out.flush(); stats.seconds = (System.currentTimeMillis() - t0) / 1000.0; var res = new FillResult(ok, grid, assigned, stats); // print a final progress line if (verbose) { System.out.println( String.format(Locale.ROOT, "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs", assigned.size(), TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, stats.seconds ) ); } return res; } static String padRight(String s, int n) { if (s.length() >= n) return s; return s + " ".repeat(n - s.length()); } // ---------------- Top-level generatePuzzle ---------------- public record PuzzleResult(SwedishGenerator swe, Dict dict, Grid mask, FillResult filled) { } }