Files
puzzle-generator/src/main/java/puzzle/SwedishGenerator.java
2026-01-08 22:54:14 +01:00

982 lines
35 KiB
Java

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<Context> 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,
long[] 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 long[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)]); }
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; }
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<Lemma> 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<String> clue) {
static int LEMMA_COUNTER = 0;
public Lemma(int index, String word, int simpel, String clu) {
this(index, word.toCharArray(), simpel, new ArrayList<String>(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<String, Lemma>();
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 rs, long cs, int len) {
//perhaps just put len into key and use hash-code and derrive index from key. or just put both ints into tail of the two longs.
static Slot from(int key, long rs, long cs, int len) {
/* if ((Long.highestOneBit(rs | cs) >> 2) != (len - 1)) throw new RuntimeException();
if ((Long.highestOneBit(cs) >> 2) != (len - 1)) throw new RuntimeException();
if ((Long.highestOneBit(rs) >> 2) != (len - 1)) throw new RuntimeException();*/
return new Slot(key, rs, cs, len);
}
//public int len() { return (int) (Long.highestOneBit(rs | cs) >> 2); }
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 r(int i) { return r(rs, i); }
public int c(int i) { return c(cs, i); }
public static boolean horiz(int key) { return ((key & 15) & 1) == 0; }
public static int r(long rs, int i) { return (int) ((rs >> (i << 2)) & 15); }
public static int c(long cs, int i) { return (int) ((cs >> (i << 2)) & 15); }
}
static void undoPlace(Grid grid, long[] undoBuffer, int offset, int n) {
for (var i = 0; i < n; i++) {
long v = undoBuffer[offset + i];
grid.setCharAt((int) (v >> 16) & 0xFF, (int) (v >> 8) & 0xFF, (char) (v & 0xFF));
}
}
@FunctionalInterface
interface SlotVisitor {
void visit(int key, long rs, long cs, 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 packedRs = 0;
long packedCs = 0;
var n = 0;
while (rr >= 0 && rr < R && cc >= 0 && cc < C && grid.isLettercell(rr, cc) && n < MAX_WORD_LENGTH) {
packedRs |= (long) rr << (n << 2);
packedCs |= (long) cc << (n << 2);
n++;
rr += nbrs16.dr;
cc += nbrs16.dc;
}
if (n > 0) {
visitor.visit((r << 8) | (c << 4) | d, packedRs, packedCs, n);
}
}
}
}
ArrayList<Slot> extractSlots(Grid grid) {
var slots = new ArrayList<Slot>(64);
forEachSlot(grid, (key, rs, cs, len) -> slots.add(Slot.from(key, rs, cs, 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 packedRs = 0;
long packedCs = 0;
var n = 0;
while (rr >= 0 && rr < R && cc >= 0 && cc < C && n < MAX_WORD_LENGTH) {
if (grid.isDigitAt(rr, cc)) break;
packedRs |= (long) rr << (n << 2);
packedCs |= (long) cc << (n << 2);
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[Grid.offset(Slot.r(packedRs, i), Slot.c(packedCs, 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.setCharAt(r, c, ((r - CROSS_X) * nx + (c - CROSS_Y) * ny >= 0) ? a.getCharAt(r, c) : b.getCharAt(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.setCharAt(r, c, C_DASH);
}
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<GridAndFit>();
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<GridAndFit>();
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<GridAndFit>();
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<Grid>();
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<Grid>();
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<Grid>();
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<Integer, Lemma> clueMap,
FillStats stats,
double simplicity) {
public FillResult(boolean ok, Grid grid, HashMap<Integer, Lemma> 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 int patternForSlot(Grid grid, Slot s, char[] pat) {
for (var i = 0; i < s.len(); i++) {
var ch = grid.getCharAt(s.r(i), s.c(i));
pat[i] = isLetter(ch) ? ch : C_DASH;
}
return s.len();
}
static int slotScore(int[] cellCount, Slot s, Grid grid) {
var cross = 0;
for (var i = 0; i < s.len(); i++) cross += (cellCount[Grid.offset(s.r(i), s.c(i))] - 1);
return cross * 10 + s.len();
}
static int placeWord(Grid grid, Slot s, Lemma w, long[] undoBuffer, int offset) {
int n = 0;
for (var i = 0; i < s.len(); i++) {
int r = s.r(i), c = s.c(i);
char cur = grid.getCharAt(r, c);
var ch = w.charAt(i);
if (cur == C_DASH) {
undoBuffer[offset + n] = ((long) r << 16) | ((long) c << 8) | (long) C_DASH;
n++;
grid.setCharAt(r, c, ch);
} else {
if (cur != ch) {
for (var j = 0; j < n; j++) {
long v = undoBuffer[offset + j];
grid.setCharAt((int) (v >> 16) & 0xFF, (int) (v >> 8) & 0xFF, (char) (v & 0xFF));
}
return -1;
}
}
}
return n;
}
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<Integer, Lemma>();
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[Grid.offset(s.r(i), s.c(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<Pick> 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 = 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();
var entry = dictIndex[s.len()];
var pat = new char[s.len()];
int patLen = patternForSlot(grid, s, pat);
int undoOffset = depth * SIZE;
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, undoOffset);
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, ctx.undoBuffer, undoOffset, nPlaced);
}
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, undoOffset);
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, ctx.undoBuffer, undoOffset, nPlaced);
}
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());
}
}