942 lines
34 KiB
Java
942 lines
34 KiB
Java
package puzzle;
|
|
|
|
import com.google.gson.Gson;
|
|
import lombok.Getter;
|
|
import lombok.val;
|
|
import puzzle.ExportFormat.Bit;
|
|
import puzzle.ExportFormat.Bit1029;
|
|
import puzzle.ExportFormat.Gridded;
|
|
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, int d, byte dbyte) {
|
|
|
|
public nbrs_16(int r, int c, int dr, int dc, int d) {
|
|
this(r, c, dr, dc, d, (byte) (48 + d));
|
|
}
|
|
}
|
|
|
|
record rci(int r, int c, int i) { }
|
|
|
|
static final rci[] IT = new rci[]{
|
|
new rci(0, 0, 0), new rci(0, 1, 8), new rci(0, 2, 16), new rci(0, 3, 24), new rci(0, 4, 32), new rci(0, 5, 40), new rci(0, 6, 48), new rci(0, 7, 56), new rci(0, 8, 64),
|
|
new rci(1, 0, 1), new rci(1, 1, 9), new rci(1, 2, 17), new rci(1, 3, 25), new rci(1, 4, 33), new rci(1, 5, 41), new rci(1, 6, 49), new rci(1, 7, 57), new rci(1, 8, 65),
|
|
new rci(2, 0, 2), new rci(2, 1, 10), new rci(2, 2, 18), new rci(2, 3, 26), new rci(2, 4, 34), new rci(2, 5, 42), new rci(2, 6, 50), new rci(2, 7, 58), new rci(2, 8, 66),
|
|
new rci(3, 0, 3), new rci(3, 1, 11), new rci(3, 2, 19), new rci(3, 3, 27), new rci(3, 4, 35), new rci(3, 5, 43), new rci(3, 6, 51), new rci(3, 7, 59), new rci(3, 8, 67),
|
|
new rci(4, 0, 4), new rci(4, 1, 12), new rci(4, 2, 20), new rci(4, 3, 28), new rci(4, 4, 36), new rci(4, 5, 44), new rci(4, 6, 52), new rci(4, 7, 60), new rci(4, 8, 68),
|
|
new rci(5, 0, 5), new rci(5, 1, 13), new rci(5, 2, 21), new rci(5, 3, 29), new rci(5, 4, 37), new rci(5, 5, 45), new rci(5, 6, 53), new rci(5, 7, 61), new rci(5, 8, 69),
|
|
new rci(6, 0, 6), new rci(6, 1, 14), new rci(6, 2, 22), new rci(6, 3, 30), new rci(6, 4, 38), new rci(6, 5, 46), new rci(6, 6, 54), new rci(6, 7, 62), new rci(6, 8, 70),
|
|
new rci(7, 0, 7), new rci(7, 1, 15), new rci(7, 2, 23), new rci(7, 3, 31), new rci(7, 4, 39), new rci(7, 5, 47), new rci(7, 6, 55), new rci(7, 7, 63), new rci(7, 8, 71),
|
|
};
|
|
static final int BAR_LEN = 22;
|
|
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 double SIZED = (double) SIZE;// ~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(byte b) { return (b & 64) != 0; }
|
|
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), // 1: up
|
|
new nbrs_16(0, 1, 0, 1, 2), // 2: right
|
|
new nbrs_16(1, 0, 1, 0, 3),// 3: down
|
|
new nbrs_16(0, -1, 0, -1, 4),// 4: left
|
|
new nbrs_16(0, -1, 1, 0, 5),// 5: vertical down, clue is on the right of the first letter
|
|
new nbrs_16(0, 1, 1, 0, 6)// 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,
|
|
byte[] pattern,
|
|
IntList[] intListBuffer,
|
|
int[] undo) {
|
|
|
|
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]);
|
|
}
|
|
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; }
|
|
}
|
|
|
|
static final byte _48 = 48;
|
|
|
|
record Grid(byte[] g, long[] bo) {
|
|
|
|
public Grid(byte[] g) { this(g, new long[2]); }
|
|
int digitAt(int r, int c) { return g[offset(r, c)] - 48; }
|
|
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 r, int c) { return g[offset(r, c)]; }
|
|
public byte byteAt(int pos) { return g[pos]; }
|
|
void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) ch; }
|
|
void setByteAt(int idx, byte ch) { g[idx] = ch; }
|
|
void setAt(int idx, byte ch) {
|
|
if (isDigit(ch)) setClue(idx, ch);
|
|
else 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));
|
|
}
|
|
public 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 / SIZED;
|
|
}
|
|
int clueCount() {
|
|
return Long.bitCount(bo[0]) + Long.bitCount(bo[1]);
|
|
}
|
|
void forEachSetBit71(java.util.function.IntConsumer consumer) {
|
|
long lo = bo[0], hi = bo[1];
|
|
// low 64 bits
|
|
while (lo != 0L) {
|
|
int bit = Long.numberOfTrailingZeros(lo);
|
|
consumer.accept(bit); // 0..63
|
|
lo &= (lo - 1); // clear lowest set bit
|
|
}
|
|
|
|
// high 7 bits (positions 64..70)
|
|
// hi &= 0x7FL;
|
|
while (hi != 0L) {
|
|
int bit = Long.numberOfTrailingZeros(hi);
|
|
consumer.accept(64 + bit); // 64..70
|
|
hi &= (hi - 1L);
|
|
}
|
|
}
|
|
}
|
|
static Grid makeEmptyGrid() { return new Grid(new byte[SIZE], new long[2]); }
|
|
|
|
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, 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 }); }
|
|
public Lemma(String word, int simpel, String[] clue) { this(LEMMA_COUNTER++, word, simpel, 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(Lemma[] wordz,
|
|
DictEntry[] index,
|
|
int[] lenCounts) {
|
|
|
|
public Dict(Lemma[] wordz) {
|
|
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 : wordz) {
|
|
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.byteAt(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 final Gson GSON = new Gson();
|
|
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 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(",", 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) {
|
|
map.add(new Lemma(s, simpel, GSON.fromJson(rawClue, String[].class)));
|
|
}
|
|
} else {
|
|
System.err.println("Invalid word: " + line);
|
|
}
|
|
}
|
|
|
|
return new Dict(map .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 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); }
|
|
}
|
|
static void undoPlace(Grid grid, Slot s, int mask) {
|
|
for (int i = 0, len = s.len(); i < 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) {
|
|
grid.forEachSetBit71(idx -> {
|
|
var d = grid.digitAt(idx);
|
|
var nbrs16 = OFFSETS[d];
|
|
int r = Grid.r(idx), c = Grid.c(idx), rr = r + nbrs16.r, cc = c + nbrs16.c;
|
|
if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) return;
|
|
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((idx << 4) | d, packedPos, n);
|
|
}
|
|
});
|
|
}
|
|
|
|
ArrayList<Slot> extractSlots(Grid grid) {
|
|
var slots = new ArrayList<Slot>(32);
|
|
forEachSlot(grid, (key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len)));
|
|
return slots;
|
|
}
|
|
boolean hasRoomForClue(Grid grid, int idx, nbrs_16 nbrs16) {
|
|
int rr = Grid.r(idx) + nbrs16.r, cc = Grid.c(idx) + 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) {
|
|
final long[] penalty = { 0 };
|
|
|
|
var clueCount = grid.clueCount();
|
|
penalty[0] += 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 };
|
|
grid.forEachSetBit71(clueIdx -> {
|
|
var d = grid.digitAt(clueIdx);
|
|
var nbrs16 = OFFSETS[d];
|
|
int rr = Grid.r(clueIdx) + nbrs16.r, cc = Grid.c(clueIdx) + nbrs16.c;
|
|
if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) return;
|
|
|
|
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) return;
|
|
hasSlots[0] = true;
|
|
|
|
if (n < MIN_LEN) {
|
|
penalty[0] += 8000;
|
|
} else {
|
|
if (lenCounts[n] <= 0) penalty[0] += 12000;
|
|
}
|
|
|
|
var horiz = Slot.horiz(d) ? covH : covV;
|
|
for (var i = 0; i < n; i++) horiz[Slot.offset(packedPos, i)] += 1;
|
|
});
|
|
|
|
if (!hasSlots[0]) return 1_000_000_000L;
|
|
|
|
/* grid.forEachSetBit71(clueIdx -> {
|
|
var h = covH[clueIdx];
|
|
var v = covV[clueIdx];
|
|
if (h == 0 && v == 0) penalty[0] += 1500;
|
|
else if (h > 0 && v > 0) { *//* ok *//* } else if (h + v == 1) penalty[0] += 200;
|
|
else penalty[0] += 600;
|
|
});*/
|
|
int idx, h, v;
|
|
for (idx = 0; idx < SIZE; idx++) {
|
|
if (grid.isDigitAt(idx)) continue;
|
|
h = covH[idx];
|
|
v = covV[idx];
|
|
if (h == 0 && v == 0) penalty[0] += 1500;
|
|
else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty[0] += 200;
|
|
else penalty[0] += 600;
|
|
}
|
|
|
|
// clue clustering (8-connected)
|
|
var seen = ctx.seen;
|
|
seen.clear();
|
|
var stack = ctx.stack;
|
|
|
|
grid.forEachSetBit71(clueIdx -> {
|
|
if (seen.get(clueIdx)) return;
|
|
var sp = 0;
|
|
stack[sp++] = clueIdx;
|
|
seen.set(clueIdx);
|
|
var size = 0;
|
|
|
|
while (sp > 0) {
|
|
var p = stack[--sp];
|
|
int rr = Grid.c(p), cc = Grid.r(p);
|
|
size++;
|
|
|
|
for (var d : nbrs8) {
|
|
var nr = rr + d.r;
|
|
var nc = cc + d.c;
|
|
if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
|
|
var nidx = Grid.offset(nr, nc);
|
|
if (seen.get(nidx) || grid.isLetterAt(nidx)) continue;
|
|
seen.set(nidx);
|
|
stack[sp++] = nidx;
|
|
}
|
|
}
|
|
|
|
if (size >= 2) penalty[0] += (size - 1L) * 120L;
|
|
});
|
|
int sp, nr, nc;
|
|
long size;
|
|
int walls, wr, wc;
|
|
// dead-end-ish letter cell (3+ walls)
|
|
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(wr, wc)) walls++;
|
|
}
|
|
if (walls >= 3) penalty[0] += 400;
|
|
}
|
|
|
|
return penalty[0];
|
|
}
|
|
|
|
Grid randomMask(Rng rng) {
|
|
var g = makeEmptyGrid();
|
|
int placed = 0, guard = 0;
|
|
|
|
while (placed < TARGET_CLUES && guard++ < 4000) {
|
|
var idx = Grid.offset(rng.randint(0, R - 1),
|
|
rng.randint(0, C - 1));
|
|
if (g.isDigitAt(idx)) continue;
|
|
var d = OFFSETS[rng.randbyte(1, 4)];
|
|
|
|
if (hasRoomForClue(g, idx, d)) {
|
|
g.setClue(idx, d.dbyte);
|
|
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 ri = Grid.offset(
|
|
clamp(cx + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, R - 1),
|
|
clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1));
|
|
|
|
if (g.isDigitAt(ri)) {
|
|
g.clearClue(ri);
|
|
} else {
|
|
var d = OFFSETS[rng.randint(1, 4)];
|
|
if (hasRoomForClue(g, ri, d)) g.setClue(ri, d.dbyte);
|
|
}
|
|
}
|
|
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 rci : IT) {
|
|
out.setAt(rci.i, ((rci.r - CROSS_X) * nx + (rci.c - CROSS_Y) * ny >= 0) ? a.byteAt(rci.i) : b.byteAt(rci.i));
|
|
}
|
|
for (var rci : IT) if (out.isDigitAt(rci.i) && !hasRoomForClue(out, rci.i, OFFSETS[out.digitAt(rci.i)])) out.clearClue(rci.i);
|
|
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;
|
|
}
|
|
|
|
// ---------------- Fill (CSP) ----------------
|
|
|
|
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) { }
|
|
|
|
static final Pick PICK_DONE = new Pick(null, null, true);
|
|
static final Pick PICK_NOT_DONE = new Pick(null, null, false);
|
|
|
|
public static record FillResult(boolean ok,
|
|
Gridded grid,
|
|
HashMap<Integer, Lemma> clueMap,
|
|
FillStats stats,
|
|
double simplicity) {
|
|
|
|
public FillResult(boolean ok, Gridded 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 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;
|
|
}
|
|
|
|
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 count = ctx.cellCount;
|
|
Arrays.fill(count, 0, SIZE, 0);
|
|
for (var s : slots) for (int i = 0, len = s.len(); i < len; i++) count[s.pos(i)]++;
|
|
|
|
var t0 = System.currentTimeMillis();
|
|
final var lastLog = new AtomicLong(t0);
|
|
|
|
var stats = new FillStats();
|
|
final var TOTAL = slots.size();
|
|
|
|
Runnable renderProgress = () -> {
|
|
if (!verbose || multiThreaded) return;
|
|
var now = System.currentTimeMillis();
|
|
if ((now - lastLog.get()) < logEveryMs) return;
|
|
lastLog.set(now);
|
|
|
|
var done = assigned.size();
|
|
// if (done!=grid.clueCount())throw new RuntimeException();
|
|
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;
|
|
int bestSlot = -1;
|
|
for (var s : slots) {
|
|
if (assigned.containsKey(s.key())) continue;
|
|
/* if (assigned.size()!= grid.clueCount())
|
|
throw new RuntimeException();*/
|
|
|
|
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);
|
|
bestInfo = info;
|
|
if (info.count <= 1) break;
|
|
}
|
|
}
|
|
|
|
if (best == null) {
|
|
return PICK_DONE;
|
|
} 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;
|
|
}
|
|
val info = pick.info;
|
|
stats.lastMRV = info.count;
|
|
renderProgress.run();
|
|
|
|
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);
|
|
undoPlace(grid, s, 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);
|
|
undoPlace(grid, s, ctx.undo[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, new Gridded(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());
|
|
}
|
|
|
|
}
|