982 lines
35 KiB
Java
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());
|
|
}
|
|
|
|
}
|