package puzzle; import lombok.Data; import lombok.Getter; import puzzle.ExportFormat.Bit; import puzzle.ExportFormat.Bit1029; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Locale; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; /** * 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[] buff) { record CandidateInfo(int[] indices, int count) { } record nbrs_8(int r, int c) { } record nbrs_16(int r, int c, int dr, int dc) { } static final int C = Config.PUZZLE_COLS; static final double CROSS_Y = (C - 1) / 2.0; static final int R = Config.PUZZLE_ROWS; static final double CROSS_X = (R - 1) / 2.0; static final int SIZE = C * R;// ~18 static final int TARGET_CLUES = SIZE >> 2; static final int MAX_WORD_LENGTH = Math.min(C, R); static final int MIN_LEN = Config.MIN_LEN; static final int CLUE_SIZE = Config.CLUE_SIZE; static final int SIMPLICITY_DEFAULT_SCORE = 2; static final int MAX_TRIES_PER_SLOT = Config.MAX_TRIES_PER_SLOT; static final char C_DASH = '\0'; static final byte _1 = 49, _9 = 57, A = 65, Z = 90, DASH = (byte) C_DASH; static final ThreadLocal CTX = ThreadLocal.withInitial(Context::new); static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; } static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); } public SwedishGenerator() { this(new int[8124]); } // Directions for '1'..'6' static final nbrs_16[] OFFSETS = new nbrs_16[]{ null, new nbrs_16(-1, 0, -1, 0), // 1: up new nbrs_16(0, 1, 0, 1), // 2: right new nbrs_16(1, 0, 1, 0),// 3: down new nbrs_16(0, -1, 0, -1),// 4: left new nbrs_16(0, -1, 1, 0),// 5: vertical down, clue is on the right of the first letter new nbrs_16(0, 1, 1, 0)// 6: vertical down, clue is on the left of the first letter }; 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 record Context(int[] covH, int[] covV, int[] cellCount, int[] stack, Bit seen, char[] pattern, IntList[] intListBuffer, int[] undoBuffer) { public Context() { this(new int[SIZE], new int[SIZE], new int[SIZE], new int[SIZE], new Bit(), new char[MAX_WORD_LENGTH], new IntList[MAX_WORD_LENGTH], new int[2048]); } void setPatter(char[] chars) { System.arraycopy(chars, 0, this.pattern, 0, chars.length); } } static final class Rng { @Getter 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) { var u = (nextU32() & 0xFFFFFFFFL); var range = (long) max - (long) min + 1L; return (int) (min + (u % range)); } double nextFloat() { return (nextU32() & 0xFFFFFFFFL) / 4294967295.0; } } record Grid(byte[] g) { public static int r(int offset) { return offset & 7; } public static int c(int offset) { return offset >>> 3; } static int offset(int r, int c) { return r | (c << 3); } Grid deepCopyGrid() { return new Grid(g.clone()); } char getCharAt(int r, int c) { return (char) (g[offset(r, c)]); } char getCharAt(int pos) { return (char) (g[pos]); } int digitAt(int r, int c) { return g[offset(r, c)] - 48; } byte byteAt(int r, int c) { return g[offset(r, c)]; } void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) ch; } void setByteAt(int r, int c, byte ch) { g[offset(r, c)] = ch; } void setCharAt(int idx, char ch) { g[idx] = (byte) ch; } void setByteAt(int idx, byte ch) { g[idx] = ch; } void clear(int r, int c) { g[offset(r, c)] = 0; } void clear(int idx) { g[idx] = 0; } boolean isLetterAt(int r, int c) { return ((g[offset(r, c)] & 64) != 0); } boolean isDigitAt(int r, int c) { return (g[offset(r, c)] & 48) == 48; } boolean isDigitAt(int index) { return (g[index] & 48) == 48; } static boolean isDigit(byte b) { return (b & 48) == 48; } boolean isLettercell(int r, int c) { return (g[offset(r, c)] & 48) != 48; } boolean isLetterAt(int index) { return (g[index] & 48) != 48; } public double similarity(Grid b) { var same = 0; for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++; return same / (double) (SIZE); } } static Grid makeEmptyGrid() { return new Grid(new byte[SIZE]); } String gridToString(Grid g) { var sb = new StringBuilder(); for (var r = 0; r < R; r++) { if (r > 0) sb.append('\n'); for (var c = 0; c < C; c++) sb.append(g.getCharAt(r, c)); } return sb.toString(); } public String renderHuman(Grid g) { var sb = new StringBuilder(); for (var r = 0; r < R; r++) { if (r > 0) sb.append('\n'); for (var c = 0; c < C; c++) { sb.append(g.isDigitAt(r, c) ? ' ' : g.getCharAt(r, c)); } } 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, char[] word, int simpel, ArrayList clue) { static int LEMMA_COUNTER = 0; public Lemma(int index, String word, int simpel, String clu) { this(index, word.toCharArray(), simpel, new ArrayList(10)); clue.add(clu); } public Lemma(String word, int simpel, String clue) { this(LEMMA_COUNTER++, word, simpel, clue); } char charAt(int idx) { return word[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) { var lemmas = wordz.clone(); Arrays.sort(lemmas, Comparator.comparingInt(wd -> wd.simpel)); var lenCounts = new int[MAX_WORD_LENGTH + 1]; var index = new DictEntry[MAX_WORD_LENGTH + 1]; Arrays.setAll(index, i -> new DictEntry(i)); int maxLength = -1; for (var lemma : lemmas) { var L = lemma.word.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(); var first = true; for (var line : raw.split("\\R")) { if (line.isBlank()) { System.err.println("Empty line: " + line); 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, rawClue)); } } } else { System.err.println("Invalid word: " + line); } } return new Dict(map.values().toArray(Lemma[]::new)); } static int[] intersectSorted(int[] buff, int[] a, int aLen, int[] b, int bLen) { //var out = new int[Math.min(aLen, bLen)]; int i = 0, j = 0, k = 0, x = 0, y = 0; while (i < aLen && j < bLen) { x = a[i]; y = b[j]; if (x == y) { buff[k++] = x; i++; j++; } else if (x < y) i++; else j++; } return Arrays.copyOf(buff, k); } CandidateInfo candidateInfoForPattern(Context ctx, DictEntry entry, int len) { var pattern = ctx.pattern; var listBuffer = ctx.intListBuffer; var listCount = 0; for (var i = 0; i < len; i++) { var ch = pattern[i]; if (isLetter(ch)) { listBuffer[listCount++] = entry.pos[i][ch - 'A']; } } if (listCount == 0) { return new CandidateInfo(null, entry.words.size()); } // Sort constraints by size to optimize intersection for (int i = 0; i < listCount - 1; i++) { for (int j = i + 1; j < listCount; j++) { if (listBuffer[j].size() < listBuffer[i].size()) { var tmp = listBuffer[i]; listBuffer[i] = listBuffer[j]; listBuffer[j] = tmp; } } } var first = listBuffer[0]; var cur = first.data(); var curLen = first.size(); for (var k = 1; k < listCount; k++) { var nxt = listBuffer[k]; cur = intersectSorted(buff, cur, curLen, nxt.data(), nxt.size()); curLen = cur.length; if (curLen == 0) break; } return new CandidateInfo(cur, curLen); } static record Slot(int key, long packedPos) { static Slot from(int key, long packedPos, int len) { return new Slot(key, packedPos | ((long) len << 56)); } public int len() { return (int) (packedPos >>> 56); } public int clueR() { return (key >> 8) & 15; } public int clueC() { return (key >> 4) & 15; } public int dir() { return key & 15; } public boolean horiz() { return horiz(key); } public int pos(int i) { return offset(packedPos, i); } public static boolean horiz(int key) { return ((key & 15) & 1) == 0; } public static int offset(long packedPos, int i) { return (int) ((packedPos >> (i * 7)) & 127); } } static void undoPlace(Grid grid, Slot s, int mask) { for (var i = 0; i < s.len(); i++) { if ((mask & (1L << i)) != 0) { grid.clear(s.pos(i)); } } } @FunctionalInterface interface SlotVisitor { void visit(int key, long packedPos, int len); } void forEachSlot(Grid grid, SlotVisitor visitor) { for (var r = 0; r < R; r++) { for (var c = 0; c < C; c++) { if (!grid.isDigitAt(r, c)) continue; var d = grid.digitAt(r, c); var nbrs16 = OFFSETS[d]; int rr = r + nbrs16.r, cc = c + nbrs16.c; if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) continue; long packedPos = 0; var n = 0; while (rr >= 0 && rr < R && cc >= 0 && cc < C && grid.isLettercell(rr, cc) && n < MAX_WORD_LENGTH) { packedPos |= (long) Grid.offset(rr, cc) << (n * 7); n++; rr += nbrs16.dr; cc += nbrs16.dc; } if (n > 0) { visitor.visit((r << 8) | (c << 4) | d, packedPos, n); } } } } ArrayList extractSlots(Grid grid) { var slots = new ArrayList(64); forEachSlot(grid, (key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len))); return slots; } boolean hasRoomForClue(Grid grid, int r, int c, char d) { var nbrs16 = OFFSETS[d - '0']; int rr = r + nbrs16.r, cc = c + nbrs16.c; var run = 0; while (rr >= 0 && rr < R && cc >= 0 && cc < C && (grid.isLettercell(rr, cc)) && run < MAX_WORD_LENGTH) { run++; rr += nbrs16.dr; cc += nbrs16.dc; if (run >= MIN_LEN) return true; } return false; } long maskFitness(Grid grid, int[] lenCounts) { long penalty = 0; var clueCount = 0; for (var v : grid.g) if (Grid.isDigit(v)) clueCount++; penalty += 8L * Math.abs(clueCount - TARGET_CLUES); var ctx = CTX.get(); var covH = ctx.covH; var covV = ctx.covV; Arrays.fill(covH, 0, SIZE, 0); Arrays.fill(covV, 0, SIZE, 0); boolean hasSlots = false; for (var r = 0; r < R; r++) { for (var c = 0; c < C; c++) { if (!grid.isDigitAt(r, c)) continue; var d = grid.digitAt(r, c); var nbrs16 = OFFSETS[d]; int rr = r + nbrs16.r, cc = c + nbrs16.c; if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) continue; long packedPos = 0; var n = 0; while (rr >= 0 && rr < R && cc >= 0 && cc < C && n < MAX_WORD_LENGTH) { if (grid.isDigitAt(rr, cc)) break; packedPos |= (long) Grid.offset(rr, cc) << (n * 7); n++; rr += nbrs16.dr; cc += nbrs16.dc; } if (n == 0) continue; hasSlots = true; if (n < MIN_LEN) { penalty += 8000; } else { if (lenCounts[n] <= 0) penalty += 12000; } var horiz = Slot.horiz(d) ? covH : covV; for (var i = 0; i < n; i++) horiz[Slot.offset(packedPos, i)] += 1; } } if (!hasSlots) return 1_000_000_000L; int idx, h, v; for (var r = 0; r < R; r++) for (var c = 0; c < C; c++) { idx = Grid.offset(r, c); if (grid.isDigitAt(idx)) continue; h = covH[idx]; v = covV[idx]; 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 = ctx.seen; seen.clear(); var stack = ctx.stack; int sp, nr, nc; long size; for (var r = 0; r < R; r++) for (var c = 0; c < C; c++) { idx = Grid.offset(r, c); if (seen.get(idx) || grid.isLetterAt(idx)) continue; sp = 0; stack[sp++] = idx; seen.set(idx); size = 0; while (sp > 0) { var p = stack[--sp]; int rr = Grid.c(p), cc = Grid.r(p); size++; for (var d : nbrs8) { nr = rr + d.r; nc = cc + d.c; if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue; idx = Grid.offset(nr, nc); if (seen.get(idx) || grid.isLetterAt(idx)) continue; seen.set(idx); stack[sp++] = idx; } } if (size >= 2) penalty += (size - 1L) * 120L; } int walls, wr, wc; // dead-end-ish letter cell (3+ walls) for (var r = 0; r < R; r++) for (var c = 0; c < C; c++) { if (grid.isDigitAt(r, c)) continue; walls = 0; for (var d : nbrs4) { wr = r + d.r; wc = c + d.c; if (wr < 0 || wr >= R || wc < 0 || wc >= C || grid.isDigitAt(wr, wc)) 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, R - 1); var c = rng.randint(0, C - 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, C_DASH); continue; } placed++; } return g; } Grid mutate(Rng rng, Grid grid) { var g = grid.deepCopyGrid(); var cx = rng.randint(0, R - 1); var cy = rng.randint(0, C - 1); var steps = 4; for (var k = 0; k < steps; k++) { var rr = clamp(cx + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, R - 1); var cc = clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1); if (g.isDigitAt(rr, cc)) { g.setCharAt(rr, cc, C_DASH); } 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, C_DASH); } } return g; } Grid crossover(Rng rng, Grid a, Grid b) { var out = makeEmptyGrid(); var theta = rng.nextFloat() * Math.PI; var nx = Math.cos(theta); var ny = Math.sin(theta); for (var r = 0; r < R; r++) for (var c = 0; c < C; c++) { out.setByteAt(r, c, ((r - CROSS_X) * nx + (c - CROSS_Y) * ny >= 0) ? a.byteAt(r, c) : b.byteAt(r, c)); } for (var r = 0; r < R; r++) for (var c = 0; c < C; c++) { if (out.isDigitAt(r, c) && !hasRoomForClue(out, r, c, out.getCharAt(r, c))) out.clear(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; } public Grid generateMask(Rng rng, int[] lenCounts, int popSize, int gens, boolean verbose) { class GridAndFit { Grid grid; Long fite; GridAndFit(Grid grid) { this.grid = grid; } long fit() { if (fite == null) this.fite = maskFitness(grid, lenCounts); return this.fite; } } if (verbose) System.out.println("generateMask init pop: " + popSize); var pop = new ArrayList(); for (var i = 0; i < popSize; i++) { pop.add(new GridAndFit(hillclimb(rng, randomMask(rng), 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.grid, p2.grid); children.add(new GridAndFit(hillclimb(rng, child, lenCounts, 70))); } pop.addAll(children); pop.sort(Comparator.comparingLong(GridAndFit::fit)); var next = new ArrayList(); for (var cand : pop) { if (next.size() >= popSize) break; var ok = true; for (var kept : next) { if (cand.grid.similarity(kept.grid) > 0.92) { ok = false; break; } } if (ok) next.add(cand); } pop = next; if (verbose && gen % 10 == 0) { var bestF = pop.get(0).fit(); System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + bestF); } } pop.sort(Comparator.comparingLong(GridAndFit::fit)); return pop.get(0).grid; } public Grid generateMask2(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 (cand.similarity(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) ---------------- @Data 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 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.simpel; totalSimplicity = assigned.isEmpty() ? 0 : totalSimplicity / assigned.size(); } this(ok, grid, assigned, stats, totalSimplicity); } } static void patternForSlot(Grid grid, Slot s, char[] pat) { for (var i = 0; i < s.len(); i++) { var ch = grid.getCharAt(s.pos(i)); pat[i] = isLetter(ch) ? ch : C_DASH; } } static int slotScore(int[] cellCount, Slot s, Grid grid) { var cross = 0; for (var i = 0; i < s.len(); i++) cross += (cellCount[s.pos(i)] - 1); return cross * 10 + s.len(); } static int placeWord(Grid grid, Slot s, Lemma w, int[] undoBuffer, int offset) { int mask = 0; for (var i = 0; i < s.len(); i++) { int idx = s.pos(i); char cur = grid.getCharAt(idx); var ch = w.charAt(i); if (cur == C_DASH) { mask |= (1 << i); grid.setCharAt(idx, ch); } else if (cur != ch) { for (var j = 0; j < i; j++) { if ((mask & (1 << j)) != 0) { grid.clear(s.pos(j)); } } return -1; } } undoBuffer[offset] = mask; return 1; } public FillResult fillMask(Rng rng, Grid mask, DictEntry[] dictIndex, int logEveryMs, int timeLimitMs, boolean verbose) { boolean multiThreaded = Thread.currentThread().getName().contains("pool"); var grid = mask.deepCopyGrid(); var slots = extractSlots(grid); var used = new Bit1029(); var assigned = new HashMap(); var ctx = CTX.get(); var cellCount = ctx.cellCount; Arrays.fill(cellCount, 0, SIZE, 0); for (var s : slots) for (var i = 0; i < s.len(); i++) cellCount[s.pos(i)]++; var t0 = System.currentTimeMillis(); final var lastLog = new AtomicLong(t0); var stats = new FillStats(); final var TOTAL = slots.size(); final var BAR_LEN = 22; Runnable renderProgress = () -> { if (!verbose || multiThreaded) 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(); }; 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 patLen = s.len(); patternForSlot(grid, s, ctx.pattern); var info = candidateInfoForPattern(ctx, entry, patLen); if (info.count == 0) { return new Pick(null, null, false); } if (best == null || info.count < bestInfo.count || (info.count == bestInfo.count && slotScore(cellCount, s, grid) > slotScore(cellCount, best, grid))) { 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); } }; class Solver { boolean backtrack(int depth) { 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(); int patLen = s.len(); var entry = dictIndex[patLen]; var pat = new char[patLen]; patternForSlot(grid, s, pat); 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); for (var t = 0; t < tries; t++) { double r = rng.nextFloat(); int idxInArray = (int) (r * r * r * L); var idx = idxs[idxInArray]; var w = entry.words.get(idx); if (used.get(w.index())) continue; boolean match = true; for (var i = 0; i < patLen; i++) { if (pat[i] != C_DASH && pat[i] != w.charAt(i)) { match = false; break; } } if (!match) continue; int nPlaced = placeWord(grid, s, w, ctx.undoBuffer, depth); if (nPlaced < 0) continue; used.set(w.index()); assigned.put(k, w); if (backtrack(depth + 1)) return true; assigned.remove(k); used.clear(w.index); undoPlace(grid, s, ctx.undoBuffer[depth]); } 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 (used.get(w.index())) continue; boolean match = true; for (var i = 0; i < patLen; i++) { if (pat[i] != C_DASH && pat[i] != w.charAt(i)) { match = false; break; } } if (!match) continue; int nPlaced = placeWord(grid, s, w, ctx.undoBuffer, depth); if (nPlaced < 0) continue; used.set(w.index()); assigned.put(k, w); if (backtrack(depth + 1)) return true; assigned.remove(k); used.clear(w.index); undoPlace(grid, s, ctx.undoBuffer[depth]); } stats.backtracks++; return false; } } // initial render (same feel) renderProgress.run(); var ok = new Solver().backtrack(0); // final progress line if (!multiThreaded) { 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 && !multiThreaded) { 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()); } }