Files
puzzle-generator/src/main/java/puzzle/SwedishGenerator.java
2026-01-10 08:32:27 +01:00

907 lines
33 KiB
Java

package puzzle;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.val;
import precomp.Neighbors9x8;
import precomp.Neighbors9x8.nbrs_16;
import precomp.Neighbors9x8.nbrs_8;
import precomp.Neighbors9x8.rci;
import puzzle.Export.Bit;
import puzzle.Export.Bit1029;
import puzzle.Export.Gridded;
import puzzle.Export.Strings;
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.function.IntConsumer;
/**
* 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(Rng rng) {
record CandidateInfo(int[] indices, int count) { }
//@formatter:off
@FunctionalInterface interface SlotVisitor { void visit(int key, long packedPos, int len); }
//@formatter:on
static final int LOG_EVERY_MS = 200;
static final int BAR_LEN = 22;
static final int C = Config.PUZZLE_COLS;
static final double CROSS_R = (C - 1) / 2.0;
static final int R = Config.PUZZLE_ROWS;
static final double CROSS_C = (R - 1) / 2.0;
static final int SIZE = C * R;// ~18
static final double SIZED = (double) SIZE;// ~18
static final int TARGET_CLUES = SIZE >> 2;
static final int MAX_WORD_LENGTH = C <= R ? C : R;
static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1;
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(byte b) { return (b & 64) != 0; }
static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
record Pick(Slot slot, CandidateInfo info, boolean done) { }
// Directions for '1'..'6'
static final nbrs_16[] OFFSETS = Neighbors9x8.OFFSETS;
static final nbrs_8[] nbrs8 = Neighbors9x8.nbrs8;
static final nbrs_8[] nbrs4 = Neighbors9x8.nbrs4;
static final rci[] IT = Neighbors9x8.IT;
static final long[] INBR8_PACKEDT = Neighbors9x8.NBR8_PACKED;
static final int[][] MUTATE_RI = new int[SIZE][625];
static {
for (int i = 0; i < SIZE; i++) {
int k = 0;
for (int dr1 = -2; dr1 <= 2; dr1++)
for (int dr2 = -2; dr2 <= 2; dr2++)
for (int dc1 = -2; dc1 <= 2; dc1++)
for (int dc2 = -2; dc2 <= 2; dc2++)
MUTATE_RI[i][k++] = Grid.offset(clamp(Grid.r(i) + dr1 + dr2, 0, R - 1),
clamp(Grid.c(i) + dc1 + dc2, 0, C - 1));
}
}
static final Pick PICK_DONE = new Pick(null, null, true);
static final Pick PICK_NOT_DONE = new Pick(null, null, false);
public static final class FillStats {
public long nodes;
public long backtracks;
public double seconds;
public int lastMRV;
public double simplicity;
}
public static record FillResult(boolean ok,
Gridded grid,
HashMap<Integer, Lemma> clueMap,
FillStats stats) {
public FillResult {
if (ok) {
clueMap.forEach((k, v) -> stats.simplicity += v.simpel);
stats.simplicity = clueMap.isEmpty() ? 0 : stats.simplicity / clueMap.size();
}
}
}
static record Context(int[] covH,
int[] covV,
int[] cellCount,
int[] stack,
Bit seen,
byte[] pattern,
IntList[] intListBuffer,
int[] undo,
int[] inter1,
int[] inter2) {
public Context() {
this(new int[SIZE], new int[SIZE], new int[SIZE], new int[SIZE], new Bit(), new byte[MAX_WORD_LENGTH], new IntList[MAX_WORD_LENGTH],
new int[2048], new int[160000], new int[160000]);
}
void setPatter(byte[] 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;
}
byte randbyte(int min, int max) {
var u = (nextU32() & 0xFFFFFFFFL);
var range = (long) max - (long) min + 1L;
return (byte) (min + (u % range));
}
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, long[] bo) {
public Grid(byte[] g) { this(g, new long[2]); }
static Grid createEmpty() { return new Grid(new byte[SIZE], new long[2]); }
int digitAt(int index) { return g[index] - 48; }
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(), bo.clone()); }
public byte byteAt(int pos) { return g[pos]; }
void setByteAt(int idx, byte ch) { g[idx] = ch; }
void setClue(int idx, byte ch) {
g[idx] = ch;
if (idx < 64) bo[0] |= (1L << idx);
else bo[1] |= (1L << (idx & 63));
}
void clear(int idx) { g[idx] = DASH; }
void clearClue(int idx) {
g[idx] = DASH;
if (idx < 64) bo[0] &= ~(1L << idx);
else bo[1] &= ~(1L << (idx & 63));
}
static boolean isDigit(byte b) { return (b & 48) == 48; }
boolean isDigitAt(int index) { return isDigit(g[index]); }
boolean isClue(int index) {
if (index < 64) {
return ((bo[0] >> index) & 1L) != 0;
}
return ((bo[1] >> (index & 63)) & 1L) != 0;
}
boolean notClue(int index) {
if (index < 64) {
return ((bo[0] >> index) & 1L) == 0L;
}
return ((bo[1] >> (index & 63)) & 1L) == 0L;
}
boolean clueless(int idx) {
if (idx < 64) {
val test = (1L << idx);
if ((test & bo[0]) != 0L) {
g[idx] = DASH;
bo[0] &= ~test;
return true;
}
return false;
} else {
val test = (1L << (idx & 63));
if ((test & bo[1]) != 0L) {
g[idx] = DASH;
bo[1] &= ~test;
return true;
}
return false;
}
}
static boolean isLetter(byte b) { return (b & 64) != 0; }
public boolean isLetterSet(int idx) { return isLetter(g[idx]); }
static boolean notDigit(byte b) { return (b & 48) != 48; }
public boolean isLetterAt(int index) { return notDigit(g[index]); }
public double similarity(Grid b) {
var same = 0;
for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++;
return same / SIZED;
}
int clueCount() { return Long.bitCount(bo[0]) + Long.bitCount(bo[1]); }
void forEachSetBit71(IntConsumer consumer) {
for (var lo = bo[0]; lo != 0; lo &= lo - 1) consumer.accept(Long.numberOfTrailingZeros(lo));
for (var hi = bo[1]; hi != 0; hi &= hi - 1) consumer.accept(64 + Long.numberOfTrailingZeros(hi));
}
void forEachSlot(SlotVisitor visitor) {
for (var lo = bo[0]; lo != 0; lo &= lo - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(lo));
for (var hi = bo[1]; hi != 0; hi &= hi - 1) processSlot(this, visitor, 64 + Long.numberOfTrailingZeros(hi));
}
}
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();
}
}
public static record Lemma(int index, byte[] word, int simpel, String[] clue) {
static int LEMMA_COUNTER = 0;
public Lemma(int index, String word, int simpel, String[] clu) { this(index, word.getBytes(StandardCharsets.US_ASCII), simpel, clu); }
public Lemma(String word, int simpel, String clue) { this(LEMMA_COUNTER++, word, simpel, new String[]{ clue }); }
byte byteAt(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(
DictEntry[] index,
int length) {
static final Gson GSON = new Gson();
public Dict(Lemma[] wordz) {
var index = new DictEntry[MAX_WORD_LENGTH_PLUS_ONE];
Arrays.setAll(index, i -> new DictEntry(i));
for (var lemma : wordz) {
var L = lemma.word.length;
var entry = index[L];
var idx = entry.words.size();
entry.words.add(lemma);
for (var i = 0; i < L; i++) {
var letter = lemma.byteAt(i) - 'A';
if (letter < 0 || letter >= 26) throw new RuntimeException("Illegal letter: " + letter + " in word " + lemma);
entry.pos[i][letter].add(idx);
}
}
for (int i = MIN_LEN; i < index.length; i++) {
var len = index[i].words.size();
if (len <= 0) {
throw new RuntimeException("No words for length " + i);
}
}
this(index, Arrays.stream(index).mapToInt(i -> i.words.size()).sum());
}
static Dict loadDict(String wordsPath) {
String raw;
try {
raw = Files.readString(Path.of(wordsPath), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("Failed to load dictionary from " + wordsPath, e);
}
var map = new ArrayList<Lemma>();
var first = true;
for (var line : raw.split("\\R")) {
if (line.isBlank()) {
System.err.println("Empty line: " + line);
continue;
}
var parts = line.split(",", 5);
var id = Integer.parseInt(parts[0].trim());
var word = parts[1].trim();
if (first && word.equalsIgnoreCase("WOORD")) {
first = false;
continue;
}
first = false;
var s = word.toUpperCase(Locale.ROOT);
if (!s.matches("^[A-Z]{2,8}$")) {
System.err.println("Invalid word: " + line);
continue;
}
// CSV has level 1-10. llmScores use 10-level.
int score = Integer.parseInt(parts[2].trim());
if (score < 1) {
if (Main.VERBOSE) System.err.println("Word too complex: " + line);
continue;
}
int simpel = Integer.parseInt(parts[3].trim());
var rawClue = parts[4].trim();
if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) {
rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\"");
}
map.add(new Lemma(id, s, simpel, GSON.fromJson(rawClue, String[].class)));
}
return new Dict(map.toArray(Lemma[]::new));
}
}
static int intersectSorted(int[] a, int aLen, int[] b, int bLen, int[] out) {
int i = 0, j = 0, k = 0, x, y;
while (i < aLen && j < bLen) {
x = a[i];
y = b[j];
if (x == y) {
out[k++] = x;
i++;
j++;
} else if (x < y) i++;
else j++;
}
return k;
}
static record Slot(int key, long packedPos) {
static Slot from(int key, long packedPos, int len) { return new Slot(key, packedPos | ((long) len << 56)); }
void undoPlace(Grid grid, int mask) { for (int i = 0, len = len(); i < len; i++) if ((mask & (1L << i)) != 0) grid.clear(pos(i)); }
public int len() { return (int) (packedPos >>> 56); }
public int clueR() { return Grid.r((key >>> 4)); }
public int clueIndex() { return key >>> 4; }
public int clueC() { return Grid.c((key >>> 4)); }
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); }
}
private static void processSlot(Grid grid, SlotVisitor visitor, int idx) {
var d = grid.digitAt(idx);
var packed = OFFSETS[d].path()[idx];
long packedPos = 0;
int k = 0;
for (int n = (int) (packed >>> 56), iidx; k < n && k < MAX_WORD_LENGTH; k++) {
iidx = (int) ((packed >>> (k * 7)) & 0x7F);
if (grid.isClue(iidx)) break;
packedPos |= (long) iidx << (k * 7);
}
if (k > 0) {
visitor.visit((idx << 4) | d, packedPos, k);
}
}
static ArrayList<Slot> extractSlots(Grid grid) {
var slots = new ArrayList<Slot>(32);
grid.forEachSlot((key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len)));
return slots;
}
static boolean hasRoomForClue(Grid grid, long packed) {
for (int n = (int) (packed >>> 56), k = 0; k < n && k < MAX_WORD_LENGTH; ) {
if (grid.isClue((int) ((packed >>> (k * 7)) & 0x7F))) break;
if (++k >= MIN_LEN) return true;
}
return false;
}
long maskFitness(Grid grid) {
var clueCount = grid.clueCount();
long penalty = ((long) Math.abs(clueCount - TARGET_CLUES)) << 3;
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;
long lo_cl = grid.bo[0], hi_cl = grid.bo[1];
while (lo_cl != 0L) {
int clueIdx = Long.numberOfTrailingZeros(lo_cl);
lo_cl &= (lo_cl - 1);
var d = grid.digitAt(clueIdx);
var nbrs16 = OFFSETS[d];
long packed = nbrs16.path()[clueIdx];
int n = (int) (packed >>> 56), k, idx;
var horiz = Slot.horiz(d) ? covH : covV;
for (k = 0; k < n && k < MAX_WORD_LENGTH; k++) {
idx = (int) ((packed >>> (k * 7)) & 0x7F);
if (grid.isClue(idx)) break;
horiz[idx] += 1;
}
if (k == 0) continue;
hasSlots = true;
if (k < MIN_LEN) penalty += 8000;
}
while (hi_cl != 0L) {
int clueIdx = 64 + Long.numberOfTrailingZeros(hi_cl);
hi_cl &= (hi_cl - 1);
var d = grid.digitAt(clueIdx);
var nbrs16 = OFFSETS[d];
long packed = nbrs16.path()[clueIdx];
int n = (int) (packed >>> 56), k, idx;
var horiz = Slot.horiz(d) ? covH : covV;
for (k = 0; k < n && k < MAX_WORD_LENGTH; k++) {
idx = (int) ((packed >>> (k * 7)) & 0x7F);
if (grid.isClue(idx)) break;
horiz[idx] += 1;
}
if (k == 0) continue;
hasSlots = true;
if (k < MIN_LEN) penalty += 8000;
}
if (!hasSlots) return 1_000_000_000L;
// clue clustering (8-connected)
var seen = ctx.seen;
seen.clear();
var stack = ctx.stack;
for (var lo = grid.bo[0]; lo != 0; lo &= lo - 1) penalty += clueStackPenalty(seen, stack, grid, Long.numberOfTrailingZeros(lo));
for (var hi = grid.bo[1]; hi != 0; hi &= hi - 1) penalty += clueStackPenalty(seen, stack, grid, 64 + Long.numberOfTrailingZeros(hi));
// dead-end-ish letter cell (3+ walls)
int walls, wc, wr;
/* for (var rci : IT) {
if (grid.isDigitAt(rci.i())) continue;
walls = 0;
for (var d : nbrs4) {
wr = rci.r() + d.r();
wc = rci.c() + d.c();
if (wr < 0 || wr >= R || wc < 0 || wc >= C || grid.isDigitAt(Grid.offset(wr, wc))) walls++;
}
if (walls >= 3) penalty[0] += 400;
}*/
int h, v;
for (var rci : IT) {
if (grid.isClue(rci.i())) continue;
if ((4 - rci.nbrCount()) + Long.bitCount(rci.n1() & grid.bo[0]) + Long.bitCount(rci.n2() & grid.bo[1]) >= 3) penalty += 400;
h = covH[rci.i()];
v = covV[rci.i()];
if (h == 0 && v == 0) penalty += 1500;
else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty += 200;
else penalty += 600;
}
return penalty;
}
static long clueStackPenalty(Bit seen, int[] stack, Grid grid, int clueIdx) {
if (seen.get(clueIdx)) return 0;
var sp = 0;
stack[sp++] = clueIdx;
seen.set(clueIdx);
var size = 0;
while (sp > 0) {
var p = stack[--sp];
size++;
long packed = Neighbors9x8.NBR8_PACKED[p];
int n = (int) (packed >>> 56);
for (int k = 0; k < n; k++) {
int nidx = (int) ((packed >>> (k * 7)) & 0x7F);
if (seen.get(nidx) || grid.notClue(nidx)) continue;
seen.set(nidx);
stack[sp++] = nidx;
}
}
return (size >= 2) ? (size - 1L) * 120L : 0;
}
Grid randomMask() {
var g = Grid.createEmpty();
for (int placed = 0, guard = 0, idx; placed < TARGET_CLUES && guard < 4000; guard++) {
idx = rng.randint(0, SIZE - 1);
if (g.isClue(idx)) continue;
var d = OFFSETS[rng.randbyte(1, 4)];
if (hasRoomForClue(g, d.path()[idx])) {
g.setClue(idx, d.dbyte());
placed++;
}
}
return g;
}
Grid mutate(Grid grid) {
var g = grid.deepCopyGrid();
var centerIdx = rng.randint(0, SIZE - 1);
var steps = 4;
for (var k = 0; k < steps; k++) {
var ri = MUTATE_RI[centerIdx][rng.randint(0, 624)];
if (!g.clueless(ri)) {
var d = OFFSETS[rng.randint(1, 4)];
if (hasRoomForClue(g, d.path()[ri])) g.setClue(ri, d.dbyte());
}
}
return g;
}
Grid crossover(Grid a, Grid b) {
var out = a.deepCopyGrid();
var theta = rng.nextFloat() * Math.PI;
var nc = Math.cos(theta);
var nr = Math.sin(theta);
long bo0 = out.bo[0], bo1 = out.bo[1];
for (var rci : IT) {
int i = rci.i();
if ((rci.r() - CROSS_C) * nc + (rci.c() - CROSS_R) * nr < 0) {
byte ch = b.g[i];
if (out.g[i] != ch) {
out.g[i] = ch;
if (Grid.isDigit(ch)) {
if (i < 64) bo0 |= (1L << i);
else bo1 |= (1L << (i & 63));
} else {
if (i < 64) bo0 &= ~(1L << i);
else bo1 &= ~(1L << (i & 63));
}
}
}
}
out.bo[0] = bo0;
out.bo[1] = bo1;
for (var lo = out.bo[0]; lo != 0L; lo &= lo - 1L) clearClues(out, Long.numberOfTrailingZeros(lo));
for (var hi = out.bo[1]; hi != 0L; hi &= hi - 1L) clearClues(out, 64 + Long.numberOfTrailingZeros(hi));
return out;
}
public static void clearClues(Grid out, int idx) { if (!hasRoomForClue(out, OFFSETS[out.digitAt(idx)].path()[idx])) out.clearClue(idx); }
Grid hillclimb(Grid start, int limit) {
var best = start;
var bestF = maskFitness(best);
var fails = 0;
while (fails < limit) {
var cand = mutate(best);
var f = maskFitness(cand);
if (f < bestF) {
best = cand;
bestF = f;
fails = 0;
} else {
fails++;
}
}
return best;
}
public Grid generateMask(int popSize, int gens) {
class GridAndFit {
Grid grid;
Long fite;
GridAndFit(Grid grid) { this.grid = grid; }
long fit() {
if (fite == null) this.fite = maskFitness(grid);
return this.fite;
}
}
if (Main.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(randomMask(), 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(p1.grid, p2.grid);
children.add(new GridAndFit(hillclimb(child, 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 (Main.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;
}
static void patternForSlot(Grid grid, Slot s, byte[] pat) {
for (int i = 0, len = s.len(); i < len; i++) {
var ch = grid.byteAt(s.pos(i));
pat[i] = isLetter(ch) ? ch : DASH;
}
}
static int slotScore(int[] count, Slot s) {
int cross = 0, len = s.len();
for (int i = 0; i < len; i++) cross += (count[s.pos(i)] - 1);
return cross * 10 + len;
}
static boolean placeWord(Grid grid, Slot s, Lemma w, int[] undoBuffer, int offset) {
int mask = 0;
byte cur, ch;
for (int i = 0, leng = s.len(), idx; i < leng; i++) {
idx = s.pos(i);
cur = grid.byteAt(idx);
ch = w.byteAt(i);
if (cur == DASH) {
mask |= (1 << i);
grid.setByteAt(idx, ch);
} else if (cur != ch) {
for (var j = 0; j < i; j++) {
if ((mask & (1 << j)) != 0) {
grid.clear(s.pos(j));
}
}
return false;
}
}
undoBuffer[offset] = mask;
return true;
}
static 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 cur = listBuffer[0].data();
var curLen = listBuffer[0].size();
if (listCount == 1) return new CandidateInfo(cur, curLen);
int[] b1 = ctx.inter1;
int[] b2 = ctx.inter2;
int[] in = cur;
int[] out = b1;
for (var k = 1; k < listCount; k++) {
var nxt = listBuffer[k];
curLen = intersectSorted(in, curLen, nxt.data(), nxt.size(), out);
in = out;
out = (out == b1) ? b2 : b1;
if (curLen == 0) break;
}
return new CandidateInfo(in, curLen);
}
public FillResult fillMask(Grid mask, DictEntry[] dictIndex,
int timeLimitMs) {
val multiThreaded = Thread.currentThread().getName().contains("pool");
val grid = mask.deepCopyGrid();
val used = new Bit1029();
val assigned = new HashMap<Integer, Lemma>();
val ctx = CTX.get();
val count = ctx.cellCount;
Arrays.fill(count, 0, SIZE, 0);
val slots = extractSlots(grid);
for (var s : slots) for (int i = 0, len = s.len(); i < len; i++) count[s.pos(i)]++;
val t0 = System.currentTimeMillis();
val stats = new FillStats();
val TOTAL = slots.size();
class Solver {
long lastLog = t0;
void renderProgress() {
if (!Main.VERBOSE || multiThreaded) return;
var now = System.currentTimeMillis();
if ((now - lastLog) < LOG_EVERY_MS) return;
lastLog = (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" + Strings.padRight(msg, 120));
System.out.flush();
}
Pick chooseMRV() {
Slot best = null;
CandidateInfo bestInfo = null;
int bestSlot = -1;
for (var s : slots) {
if (assigned.containsKey(s.key())) continue;
var entry = dictIndex[s.len()];
if (entry == null) return PICK_NOT_DONE;
patternForSlot(grid, s, ctx.pattern);
var info = candidateInfoForPattern(ctx, entry, s.len());
if (info.count == 0) return PICK_NOT_DONE;
var slotScore = -1;
if (best == null
|| info.count < bestInfo.count
|| (info.count == bestInfo.count && (slotScore = slotScore(count, s)) > bestSlot)) {
best = s;
bestSlot = (slotScore != -1) ? slotScore : slotScore(count, s);
if (info.indices != null && (info.indices == ctx.inter1 || info.indices == ctx.inter2)) {
bestInfo = new CandidateInfo(Arrays.copyOf(info.indices, info.count), info.count);
} else {
bestInfo = info;
}
if (info.count <= 1) break;
}
}
if (best == null) {
return PICK_DONE;
} else {
return new Pick(best, bestInfo, false);
}
}
boolean backtrack(int depth) {
if (Thread.currentThread().isInterrupted()) return false;
stats.nodes++;
if (timeLimitMs > 0 && (System.currentTimeMillis() - t0) > timeLimitMs) return false;
var pick = chooseMRV();
if (pick.done) return true;
if (pick.slot == null) {
stats.backtracks++;
return false;
}
val info = pick.info;
stats.lastMRV = info.count;
renderProgress();
var s = pick.slot;
var k = s.key();
int patLen = s.len();
var entry = dictIndex[patLen];
var pat = new byte[patLen];
patternForSlot(grid, s, pat);
if (info.indices != null && info.indices.length > 0) {
var idxs = 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] != DASH && pat[i] != w.byteAt(i)) {
match = false;
break;
}
}
if (!match || !placeWord(grid, s, w, ctx.undo, depth)) continue;
used.set(w.index());
assigned.put(k, w);
if (backtrack(depth + 1)) return true;
assigned.remove(k);
used.clear(w.index);
s.undoPlace(grid, ctx.undo[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] != DASH && pat[i] != w.byteAt(i)) {
match = false;
break;
}
}
if (!match || !placeWord(grid, s, w, ctx.undo, depth)) continue;
used.set(w.index());
assigned.put(k, w);
if (backtrack(depth + 1)) return true;
assigned.remove(k);
used.clear(w.index);
s.undoPlace(grid, ctx.undo[depth]);
}
stats.backtracks++;
return false;
}
}
// initial render (same feel)
var solver = new Solver();
solver.renderProgress();
var ok = solver.backtrack(0);
// final progress line
if (!multiThreaded) {
System.out.print("\r" + Strings.padRight("", 120) + "\r");
System.out.flush();
}
stats.seconds = (System.currentTimeMillis() - t0) / 1000.0;
var res = new FillResult(ok, new Gridded(grid), assigned, stats);
// print a final progress line
if (Main.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;
}
}