initial commit
This commit is contained in:
329
src/puzzle/ExportFormat.java
Normal file
329
src/puzzle/ExportFormat.java
Normal file
@@ -0,0 +1,329 @@
|
||||
package puzzle;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* ExportFormat.java
|
||||
*
|
||||
* Direct port of export_format.js:
|
||||
* - scans filled grid for clue digits '1'..'4'
|
||||
* - extracts placed words in canonical direction (horizontal=right, vertical=down)
|
||||
* - crops to bounding box (words + arrow cells) with 1-cell margin
|
||||
* - outputs gridv2 + words[] (+ difficulty, rewards)
|
||||
*/
|
||||
public final class ExportFormat {
|
||||
|
||||
private ExportFormat() { }
|
||||
|
||||
// Directions for digits '1'..'4'
|
||||
private static final int[][] DIRS = new int[5][2];
|
||||
static {
|
||||
DIRS[1] = new int[]{ -1, 0 }; // up
|
||||
DIRS[2] = new int[]{ 0, 1 }; // right
|
||||
DIRS[3] = new int[]{ 1, 0 }; // down
|
||||
DIRS[4] = new int[]{ 0, -1 }; // left
|
||||
}
|
||||
|
||||
private static boolean isDigit(char ch) { return ch >= '1' && ch <= '4'; }
|
||||
private static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; }
|
||||
|
||||
private static boolean inBounds(int H, int W, int r, int c) {
|
||||
return r >= 0 && r < H && c >= 0 && c < W;
|
||||
}
|
||||
|
||||
// ---------- Public API ----------
|
||||
|
||||
public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz) {
|
||||
return exportFormatFromFilled(puz, 1, new Rewards(50, 2, 1));
|
||||
}
|
||||
|
||||
public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz, int difficulty, Rewards rewards) {
|
||||
Objects.requireNonNull(puz, "puz");
|
||||
char[][] g = puz.filled.grid;
|
||||
int H = g.length;
|
||||
int W = g[0].length;
|
||||
|
||||
// 1) extract "placed" list from all clue digits in the filled grid
|
||||
List<Placed> placed = new ArrayList<>();
|
||||
Set<String> seen = new HashSet<>();
|
||||
|
||||
for (int r = 0; r < H; r++) {
|
||||
for (int c = 0; c < W; c++) {
|
||||
char ch = g[r][c];
|
||||
if (!isDigit(ch)) continue;
|
||||
|
||||
Placed p = extractPlacedFromClue(g, r, c, ch, 8, 2);
|
||||
if (p == null) continue;
|
||||
|
||||
String key = p.startRow + "," + p.startCol + ":" + p.direction + ":" + p.word;
|
||||
if (seen.contains(key)) continue;
|
||||
seen.add(key);
|
||||
placed.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing placed: return full grid mapped to letters/# only
|
||||
if (placed.isEmpty()) {
|
||||
List<String> gridv2 = new ArrayList<>(H);
|
||||
for (int r = 0; r < H; r++) {
|
||||
StringBuilder sb = new StringBuilder(W);
|
||||
for (int c = 0; c < W; c++) {
|
||||
char ch = g[r][c];
|
||||
sb.append(isLetter(ch) ? ch : '#');
|
||||
}
|
||||
gridv2.add(sb.toString());
|
||||
}
|
||||
return new ExportedPuzzle(gridv2, List.of(), difficulty, rewards);
|
||||
}
|
||||
|
||||
// 2) bounding box around all word cells + arrow cells, with 1-cell margin
|
||||
List<int[]> allCells = new ArrayList<>();
|
||||
for (Placed p : placed) {
|
||||
allCells.addAll(p.cells);
|
||||
allCells.add(p.arrow);
|
||||
}
|
||||
|
||||
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
|
||||
int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
|
||||
|
||||
for (int[] rc : allCells) {
|
||||
int rr = rc[0], cc = rc[1];
|
||||
minR = Math.min(minR, rr);
|
||||
minC = Math.min(minC, cc);
|
||||
maxR = Math.max(maxR, rr);
|
||||
maxC = Math.max(maxC, cc);
|
||||
}
|
||||
|
||||
minR -= 1;
|
||||
minC -= 1;
|
||||
maxR += 1;
|
||||
maxC += 1;
|
||||
|
||||
// 3) map of only used letter cells (everything else becomes '#')
|
||||
Map<Long, Character> letterAt = new HashMap<>();
|
||||
for (Placed p : placed) {
|
||||
for (int[] rc : p.cells) {
|
||||
int rr = rc[0], cc = rc[1];
|
||||
if (inBounds(H, W, rr, cc) && isLetter(g[rr][cc])) {
|
||||
letterAt.put(pack(rr, cc), g[rr][cc]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
|
||||
List<String> gridv2 = new ArrayList<>(Math.max(0, maxR - minR + 1));
|
||||
for (int r = minR; r <= maxR; r++) {
|
||||
StringBuilder row = new StringBuilder(Math.max(0, maxC - minC + 1));
|
||||
for (int c = minC; c <= maxC; c++) {
|
||||
Character ch = letterAt.get(pack(r, c));
|
||||
row.append(ch != null ? ch : '#');
|
||||
}
|
||||
gridv2.add(row.toString());
|
||||
}
|
||||
|
||||
// 5) words output with cropped coordinates
|
||||
List<WordOut> wordsOut = new ArrayList<>(placed.size());
|
||||
for (Placed p : placed) {
|
||||
wordsOut.add(new WordOut(
|
||||
p.word,
|
||||
p.clue, // placeholder = word (same as JS)
|
||||
p.startRow - minR,
|
||||
p.startCol - minC,
|
||||
p.direction,
|
||||
p.word, // answer
|
||||
p.arrowRow - minR,
|
||||
p.arrowCol - minC
|
||||
));
|
||||
}
|
||||
|
||||
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a word run for a clue cell at (r,c) with direction digit d.
|
||||
* Canonical output:
|
||||
* - direction: "horizontal" (right) or "vertical" (down)
|
||||
* - startRow/startCol: first letter cell in canonical direction
|
||||
* - arrowRow/arrowCol: immediately before the start (left or above)
|
||||
* - word read from grid in canonical order
|
||||
*/
|
||||
private static Placed extractPlacedFromClue(char[][] g, int r, int c, char d, int maxLen, int minLen) {
|
||||
int H = g.length, W = g[0].length;
|
||||
int di = d - '0';
|
||||
int dr = DIRS[di][0], dc = DIRS[di][1];
|
||||
|
||||
// collect letter cells in ORIGINAL direction away from the clue
|
||||
List<int[]> cells = new ArrayList<>();
|
||||
int rr = r + dr, cc = c + dc;
|
||||
while (inBounds(H, W, rr, cc) && isLetter(g[rr][cc]) && cells.size() < maxLen) {
|
||||
cells.add(new int[]{ rr, cc });
|
||||
rr += dr;
|
||||
cc += dc;
|
||||
}
|
||||
|
||||
if (cells.size() < minLen) return null;
|
||||
|
||||
// Canonicalize: always output right/down
|
||||
int startRow, startCol, arrowRow, arrowCol;
|
||||
String direction;
|
||||
|
||||
if (d == '2') { // right -> horizontal
|
||||
direction = "horizontal";
|
||||
startRow = cells.get(0)[0];
|
||||
startCol = cells.get(0)[1];
|
||||
arrowRow = r;
|
||||
arrowCol = c;
|
||||
} else if (d == '3') { // down -> vertical
|
||||
direction = "vertical";
|
||||
startRow = cells.get(0)[0];
|
||||
startCol = cells.get(0)[1];
|
||||
arrowRow = r;
|
||||
arrowCol = c;
|
||||
} else if (d == '4') { // left -> canonical right
|
||||
direction = "horizontal";
|
||||
int[] farLeft = cells.get(cells.size() - 1);
|
||||
startRow = farLeft[0];
|
||||
startCol = farLeft[1];
|
||||
arrowRow = startRow;
|
||||
arrowCol = startCol - 1;
|
||||
} else if (d == '1') { // up -> canonical down
|
||||
direction = "vertical";
|
||||
int[] topMost = cells.get(cells.size() - 1);
|
||||
startRow = topMost[0];
|
||||
startCol = topMost[1];
|
||||
arrowRow = startRow - 1;
|
||||
arrowCol = startCol;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read word from grid in canonical order (right/down)
|
||||
StringBuilder wordChars = new StringBuilder();
|
||||
if ("horizontal".equals(direction)) {
|
||||
for (int i = 0; i < cells.size(); i++) {
|
||||
int cc2 = startCol + i;
|
||||
char ch = (inBounds(H, W, startRow, cc2) ? g[startRow][cc2] : '#');
|
||||
if (!isLetter(ch)) break;
|
||||
wordChars.append(ch);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < cells.size(); i++) {
|
||||
int rr2 = startRow + i;
|
||||
char ch = (inBounds(H, W, rr2, startCol) ? g[rr2][startCol] : '#');
|
||||
if (!isLetter(ch)) break;
|
||||
wordChars.append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
String word = wordChars.toString();
|
||||
if (word.length() < minLen || word.length() > maxLen) return null;
|
||||
|
||||
// Build exact used cells (only for actual word length)
|
||||
List<int[]> used = new ArrayList<>(word.length());
|
||||
if ("horizontal".equals(direction)) {
|
||||
for (int i = 0; i < word.length(); i++) used.add(new int[]{ startRow, startCol + i });
|
||||
} else {
|
||||
for (int i = 0; i < word.length(); i++) used.add(new int[]{ startRow + i, startCol });
|
||||
}
|
||||
|
||||
return new Placed(
|
||||
word,
|
||||
word, // clue placeholder (same as JS)
|
||||
startRow,
|
||||
startCol,
|
||||
direction,
|
||||
word, // answer
|
||||
arrowRow,
|
||||
arrowCol,
|
||||
used,
|
||||
new int[]{ arrowRow, arrowCol }
|
||||
);
|
||||
}
|
||||
|
||||
// pack (r,c) into one long key (handles negatives too)
|
||||
private static long pack(int r, int c) {
|
||||
return (((long) r) << 32) ^ (c & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
// ---------- Data models ----------
|
||||
|
||||
private static final class Placed {
|
||||
|
||||
final String word;
|
||||
final String clue;
|
||||
final int startRow, startCol;
|
||||
final String direction; // "horizontal" | "vertical"
|
||||
final String answer;
|
||||
final int arrowRow, arrowCol;
|
||||
final List<int[]> cells; // word cells
|
||||
final int[] arrow; // [arrowRow, arrowCol]
|
||||
|
||||
Placed(String word, String clue, int startRow, int startCol, String direction, String answer,
|
||||
int arrowRow, int arrowCol, List<int[]> cells, int[] arrow) {
|
||||
this.word = word;
|
||||
this.clue = clue;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.direction = direction;
|
||||
this.answer = answer;
|
||||
this.arrowRow = arrowRow;
|
||||
this.arrowCol = arrowCol;
|
||||
this.cells = cells;
|
||||
this.arrow = arrow;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Rewards {
|
||||
|
||||
public final int coins;
|
||||
public final int stars;
|
||||
public final int hints;
|
||||
|
||||
public Rewards(int coins, int stars, int hints) {
|
||||
this.coins = coins;
|
||||
this.stars = stars;
|
||||
this.hints = hints;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WordOut {
|
||||
|
||||
public final String word;
|
||||
public final String clue;
|
||||
public final int startRow;
|
||||
public final int startCol;
|
||||
public final String direction; // "horizontal" | "vertical"
|
||||
public final String answer;
|
||||
public final int arrowRow;
|
||||
public final int arrowCol;
|
||||
|
||||
public WordOut(String word, String clue, int startRow, int startCol, String direction,
|
||||
String answer, int arrowRow, int arrowCol) {
|
||||
this.word = word;
|
||||
this.clue = clue;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.direction = direction;
|
||||
this.answer = answer;
|
||||
this.arrowRow = arrowRow;
|
||||
this.arrowCol = arrowCol;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ExportedPuzzle {
|
||||
|
||||
public final List<String> gridv2;
|
||||
public final List<WordOut> words;
|
||||
public final int difficulty;
|
||||
public final Rewards rewards;
|
||||
|
||||
public ExportedPuzzle(List<String> gridv2, List<WordOut> words, int difficulty, Rewards rewards) {
|
||||
this.gridv2 = gridv2;
|
||||
this.words = words;
|
||||
this.difficulty = difficulty;
|
||||
this.rewards = rewards;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Tiny demo (optional) ----------
|
||||
|
||||
}
|
||||
72
src/puzzle/Main.java
Normal file
72
src/puzzle/Main.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package puzzle;
|
||||
|
||||
public class Main {
|
||||
// ---------------- CLI ----------------
|
||||
|
||||
public static class Opts {
|
||||
public int seed = 1;
|
||||
public int pop = 18;
|
||||
public int gens = 100;
|
||||
public int tries = 50;
|
||||
public String wordsPath = "./word-list.txt";
|
||||
}
|
||||
|
||||
static void usage() {
|
||||
System.out.println("""
|
||||
Usage:
|
||||
java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt]
|
||||
|
||||
Defaults:
|
||||
--seed 1
|
||||
--pop 18
|
||||
--gens 100
|
||||
--tries 50
|
||||
--words ./word-list.txt
|
||||
""");
|
||||
}
|
||||
|
||||
static Opts parseArgs(String[] argv) {
|
||||
var out = new Opts();
|
||||
for (int i = 0; i < argv.length; i++) {
|
||||
String a = argv[i];
|
||||
String v = (i + 1 < argv.length) ? argv[i + 1] : null;
|
||||
if (a.equals("--help") || a.equals("-h")) {
|
||||
usage();
|
||||
System.exit(0);
|
||||
}
|
||||
if (a.equals("--seed")) { out.seed = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--pop")) { out.pop = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--gens")) { out.gens = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--tries")) { out.tries = Integer.parseInt(v); i++; }
|
||||
else if (a.equals("--words")) { out.wordsPath = v; i++; }
|
||||
else throw new IllegalArgumentException("Unknown arg: " + a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
var opts = parseArgs(args);
|
||||
var res = SwedishGenerator.generatePuzzle(opts);
|
||||
if (res == null) {
|
||||
System.out.println("No solution found within tries.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println("\n=== GENERATED MASK ===");
|
||||
System.out.println(SwedishGenerator.gridToString(res.mask));
|
||||
|
||||
System.out.println("\n=== FILLED PUZZLE (RAW) ===");
|
||||
System.out.println(SwedishGenerator.gridToString(res.filled.grid));
|
||||
|
||||
System.out.println("\n=== FILLED PUZZLE (HUMAN) ===");
|
||||
System.out.println(SwedishGenerator.renderHuman(res.filled.grid));
|
||||
var out = ExportFormat.exportFormatFromFilled(res, 1, new ExportFormat.Rewards(50, 2, 1));
|
||||
System.out.println("gridv2:");
|
||||
for (String row : out.gridv2) System.out.println(row);
|
||||
System.out.println("words: " + out.words.size());
|
||||
for (var w : out.words) {
|
||||
System.out.printf("%s %s start=(%d,%d) arrow=(%d,%d)%n",
|
||||
w.word, w.direction, w.startRow, w.startCol, w.arrowRow, w.arrowCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
843
src/puzzle/SwedishGenerator.java
Normal file
843
src/puzzle/SwedishGenerator.java
Normal file
@@ -0,0 +1,843 @@
|
||||
package puzzle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* SwedishGenerator.java
|
||||
*
|
||||
* Usage:
|
||||
* javac SwedishGenerator.java
|
||||
* java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt]
|
||||
*/
|
||||
public class SwedishGenerator {
|
||||
|
||||
static final int W = 9, H = 8;
|
||||
static final int MIN_LEN = 2, MAX_LEN = 8;
|
||||
|
||||
// Directions for '1'..'4'
|
||||
static final int[][] DIRS = new int[5][2];
|
||||
static {
|
||||
DIRS[1] = new int[]{-1, 0}; // up
|
||||
DIRS[2] = new int[]{0, 1}; // right
|
||||
DIRS[3] = new int[]{1, 0}; // down
|
||||
DIRS[4] = new int[]{0, -1}; // left
|
||||
}
|
||||
|
||||
static boolean isDigit(char ch) { return ch >= '1' && ch <= '4'; }
|
||||
static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; }
|
||||
static boolean isLetterCell(char ch) { return ch == '#' || isLetter(ch); }
|
||||
|
||||
|
||||
// ---------------- RNG (xorshift32) ----------------
|
||||
|
||||
static final class Rng {
|
||||
private int x;
|
||||
Rng(int seed) {
|
||||
int s = seed;
|
||||
if (s == 0) s = 1;
|
||||
this.x = s;
|
||||
}
|
||||
int nextU32() {
|
||||
int y = x;
|
||||
y ^= (y << 13);
|
||||
y ^= (y >>> 17);
|
||||
y ^= (y << 5);
|
||||
x = y;
|
||||
return y;
|
||||
}
|
||||
int randint(int min, int max) { // inclusive
|
||||
int r = nextU32();
|
||||
long u = (r & 0xFFFFFFFFL);
|
||||
long range = (long) max - (long) min + 1L;
|
||||
return (int) (min + (u % range));
|
||||
}
|
||||
double nextFloat() {
|
||||
long u = nextU32() & 0xFFFFFFFFL;
|
||||
return u / 4294967295.0; // 0xFFFFFFFF
|
||||
}
|
||||
}
|
||||
|
||||
static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
|
||||
|
||||
// ---------------- Grid helpers ----------------
|
||||
|
||||
static char[][] makeEmptyGrid() {
|
||||
char[][] g = new char[H][W];
|
||||
for (int r = 0; r < H; r++) Arrays.fill(g[r], '#');
|
||||
return g;
|
||||
}
|
||||
|
||||
static char[][] deepCopyGrid(char[][] g) {
|
||||
char[][] out = new char[H][W];
|
||||
for (int r = 0; r < H; r++) out[r] = Arrays.copyOf(g[r], W);
|
||||
return out;
|
||||
}
|
||||
|
||||
static String gridToString(char[][] g) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int r = 0; r < H; r++) {
|
||||
if (r > 0) sb.append('\n');
|
||||
sb.append(g[r]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static String renderHuman(char[][] g) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int r = 0; r < H; r++) {
|
||||
if (r > 0) sb.append('\n');
|
||||
for (int c = 0; c < W; c++) {
|
||||
char ch = g[r][c];
|
||||
sb.append(isDigit(ch) ? ' ' : ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ---------------- Words / index ----------------
|
||||
|
||||
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; } // note: may have extra capacity
|
||||
}
|
||||
|
||||
static final class DictEntry {
|
||||
final ArrayList<String> words = new ArrayList<>();
|
||||
final IntList[][] pos; // pos[i][letter] -> indices (sorted by insertion)
|
||||
DictEntry(int L) {
|
||||
pos = new IntList[L][26];
|
||||
for (int i = 0; i < L; i++) {
|
||||
for (int j = 0; j < 26; j++) pos[i][j] = new IntList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class Dict {
|
||||
final ArrayList<String> words;
|
||||
final HashMap<Integer, DictEntry> index; // len -> DictEntry
|
||||
final HashMap<Integer, Integer> lenCounts; // len -> count
|
||||
Dict(ArrayList<String> words, HashMap<Integer, DictEntry> index, HashMap<Integer, Integer> lenCounts) {
|
||||
this.words = words;
|
||||
this.index = index;
|
||||
this.lenCounts = lenCounts;
|
||||
}
|
||||
}
|
||||
|
||||
static Dict loadWords(String wordsPath) {
|
||||
String raw;
|
||||
try {
|
||||
raw = Files.readString(Path.of(wordsPath), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
raw = "EU\nUUR\nAUTO\nBOOM\nHUIS\nKAT\nZEE\nRODE\nDRAAD\nKENNIS\nNETWERK\nPAKTE\n";
|
||||
}
|
||||
|
||||
ArrayList<String> words = new ArrayList<>();
|
||||
for (String line : raw.split("\\R")) {
|
||||
String s = line.trim().toUpperCase(Locale.ROOT);
|
||||
if (s.matches("^[A-Z]{2,8}$")) words.add(s);
|
||||
}
|
||||
|
||||
HashMap<Integer, DictEntry> index = new HashMap<>();
|
||||
HashMap<Integer, Integer> lenCounts = new HashMap<>();
|
||||
|
||||
for (String w : words) {
|
||||
int L = w.length();
|
||||
lenCounts.put(L, lenCounts.getOrDefault(L, 0) + 1);
|
||||
|
||||
DictEntry entry = index.get(L);
|
||||
if (entry == null) {
|
||||
entry = new DictEntry(L);
|
||||
index.put(L, entry);
|
||||
}
|
||||
|
||||
int idx = entry.words.size();
|
||||
entry.words.add(w);
|
||||
|
||||
for (int i = 0; i < L; i++) {
|
||||
int letter = w.charAt(i) - 'A';
|
||||
if (letter >= 0 && letter < 26) entry.pos[i][letter].add(idx);
|
||||
}
|
||||
}
|
||||
|
||||
return new Dict(words, index, lenCounts);
|
||||
}
|
||||
|
||||
static int[] intersectSorted(int[] a, int aLen, int[] b, int bLen) {
|
||||
int[] out = new int[Math.min(aLen, bLen)];
|
||||
int i = 0, j = 0, k = 0;
|
||||
while (i < aLen && j < bLen) {
|
||||
int x = a[i], y = b[j];
|
||||
if (x == y) { out[k++] = x; i++; j++; }
|
||||
else if (x < y) i++;
|
||||
else j++;
|
||||
}
|
||||
return Arrays.copyOf(out, k);
|
||||
}
|
||||
|
||||
static final class CandidateInfo {
|
||||
int[] indices; // null => unconstrained
|
||||
int count;
|
||||
}
|
||||
|
||||
static CandidateInfo candidateInfoForPattern(DictEntry entry, char[] pattern /* 0 means null */) {
|
||||
ArrayList<IntList> lists = new ArrayList<>();
|
||||
for (int i = 0; i < pattern.length; i++) {
|
||||
char ch = pattern[i];
|
||||
if (ch != 0 && isLetter(ch)) {
|
||||
lists.add(entry.pos[i][ch - 'A']);
|
||||
}
|
||||
}
|
||||
CandidateInfo ci = new CandidateInfo();
|
||||
if (lists.isEmpty()) {
|
||||
ci.indices = null;
|
||||
ci.count = entry.words.size();
|
||||
return ci;
|
||||
}
|
||||
|
||||
lists.sort(Comparator.comparingInt(IntList::size));
|
||||
|
||||
IntList first = lists.get(0);
|
||||
int[] cur = Arrays.copyOf(first.data(), first.size());
|
||||
int curLen = cur.length;
|
||||
|
||||
for (int k = 1; k < lists.size(); k++) {
|
||||
IntList nxt = lists.get(k);
|
||||
int[] nextArr = nxt.data();
|
||||
int nextLen = nxt.size();
|
||||
cur = intersectSorted(cur, curLen, nextArr, nextLen);
|
||||
curLen = cur.length;
|
||||
if (curLen == 0) break;
|
||||
}
|
||||
|
||||
ci.indices = cur;
|
||||
ci.count = curLen;
|
||||
return ci;
|
||||
}
|
||||
|
||||
// ---------------- Slots ----------------
|
||||
|
||||
static final class Slot {
|
||||
final int clueR, clueC;
|
||||
final char dir; // '1'..'4'
|
||||
final int[] rs, cs; // cells
|
||||
final int len;
|
||||
Slot(int clueR, int clueC, char dir, int[] rs, int[] cs) {
|
||||
this.clueR = clueR; this.clueC = clueC; this.dir = dir;
|
||||
this.rs = rs; this.cs = cs;
|
||||
this.len = rs.length;
|
||||
}
|
||||
String key() { return clueR + "," + clueC + ":" + dir; }
|
||||
}
|
||||
|
||||
static ArrayList<Slot> extractSlots(char[][] grid) {
|
||||
ArrayList<Slot> slots = new ArrayList<>();
|
||||
for (int r = 0; r < H; r++) {
|
||||
for (int c = 0; c < W; c++) {
|
||||
char d = grid[r][c];
|
||||
if (!isDigit(d)) continue;
|
||||
|
||||
int di = d - '0';
|
||||
int dr = DIRS[di][0], dc = DIRS[di][1];
|
||||
|
||||
int rr = r + dr, cc = c + dc;
|
||||
if (rr < 0 || rr >= H || cc < 0 || cc >= W) continue;
|
||||
if (!isLetterCell(grid[rr][cc])) continue;
|
||||
|
||||
int[] rs = new int[MAX_LEN + 1]; // allow MAX_LEN+1 like JS loop
|
||||
int[] cs = new int[MAX_LEN + 1];
|
||||
int n = 0;
|
||||
|
||||
while (rr >= 0 && rr < H && cc >= 0 && cc < W) {
|
||||
char ch = grid[rr][cc];
|
||||
if (!isLetterCell(ch)) break;
|
||||
rs[n] = rr;
|
||||
cs[n] = cc;
|
||||
n++;
|
||||
rr += dr;
|
||||
cc += dc;
|
||||
if (n > MAX_LEN) break; // allow n==MAX_LEN+1
|
||||
}
|
||||
|
||||
slots.add(new Slot(r, c, d, Arrays.copyOf(rs, n), Arrays.copyOf(cs, n)));
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
static boolean hasRoomForClue(char[][] grid, int r, int c, char d) {
|
||||
int di = d - '0';
|
||||
int dr = DIRS[di][0], dc = DIRS[di][1];
|
||||
int rr = r + dr, cc = c + dc;
|
||||
int run = 0;
|
||||
while (rr >= 0 && rr < H && cc >= 0 && cc < W && isLetterCell(grid[rr][cc]) && run < MAX_LEN) {
|
||||
run++;
|
||||
rr += dr;
|
||||
cc += dc;
|
||||
}
|
||||
return run >= MIN_LEN;
|
||||
}
|
||||
|
||||
// ---------------- FAST mask fitness ----------------
|
||||
|
||||
static long maskFitness(char[][] grid, HashMap<Integer, Integer> lenCounts) {
|
||||
long penalty = 0;
|
||||
|
||||
int clueCount = 0;
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) if (isDigit(grid[r][c])) clueCount++;
|
||||
|
||||
int targetClues = (int)Math.round(W * H * 0.25); // ~18
|
||||
penalty += 8L * Math.abs(clueCount - targetClues);
|
||||
|
||||
ArrayList<Slot> slots = extractSlots(grid);
|
||||
if (slots.isEmpty()) return 1_000_000_000L;
|
||||
|
||||
int[][] covH = new int[H][W];
|
||||
int[][] covV = new int[H][W];
|
||||
|
||||
for (Slot s : slots) {
|
||||
boolean horiz = (s.dir == '2' || s.dir == '4');
|
||||
|
||||
if (s.len < MIN_LEN) penalty += 8000;
|
||||
if (s.len > MAX_LEN) penalty += 8000 + (long)(s.len - MAX_LEN) * 500L;
|
||||
|
||||
if (s.len >= MIN_LEN && s.len <= MAX_LEN) {
|
||||
if (!lenCounts.containsKey(s.len)) penalty += 12000;
|
||||
}
|
||||
|
||||
for (int i = 0; i < s.len; i++) {
|
||||
int r = s.rs[i], c = s.cs[i];
|
||||
if (horiz) covH[r][c] += 1;
|
||||
else covV[r][c] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) {
|
||||
if (!isLetterCell(grid[r][c])) continue;
|
||||
int h = covH[r][c], v = covV[r][c];
|
||||
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)
|
||||
boolean[][] seen = new boolean[H][W];
|
||||
int[] stack = new int[W * H];
|
||||
int sp;
|
||||
int[][] nbrs8 = {
|
||||
{-1,-1},{-1,0},{-1,1},
|
||||
{0,-1}, {0,1},
|
||||
{1,-1},{1,0},{1,1}
|
||||
};
|
||||
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) {
|
||||
if (!isDigit(grid[r][c]) || seen[r][c]) continue;
|
||||
sp = 0;
|
||||
stack[sp++] = r * W + c;
|
||||
seen[r][c] = true;
|
||||
int size = 0;
|
||||
|
||||
while (sp > 0) {
|
||||
int p = stack[--sp];
|
||||
int x = p / W, y = p % W;
|
||||
size++;
|
||||
|
||||
for (int[] d : nbrs8) {
|
||||
int nx = x + d[0], ny = y + d[1];
|
||||
if (nx < 0 || nx >= H || ny < 0 || ny >= W) continue;
|
||||
if (seen[nx][ny]) continue;
|
||||
if (!isDigit(grid[nx][ny])) continue;
|
||||
seen[nx][ny] = true;
|
||||
stack[sp++] = nx * W + ny;
|
||||
}
|
||||
}
|
||||
|
||||
if (size >= 2) penalty += (long)(size - 1) * 120L;
|
||||
}
|
||||
|
||||
// dead-end-ish letter cell (3+ walls)
|
||||
int[][] nbrs4 = {{-1,0},{1,0},{0,-1},{0,1}};
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) {
|
||||
if (!isLetterCell(grid[r][c])) continue;
|
||||
int walls = 0;
|
||||
for (int[] d : nbrs4) {
|
||||
int rr = r + d[0], cc = c + d[1];
|
||||
if (rr < 0 || rr >= H || cc < 0 || cc >= W) { walls++; continue; }
|
||||
if (!isLetterCell(grid[rr][cc])) walls++;
|
||||
}
|
||||
if (walls >= 3) penalty += 400;
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
// ---------------- Mask generation ----------------
|
||||
|
||||
static char[][] randomMask(Rng rng) {
|
||||
char[][] g = makeEmptyGrid();
|
||||
int targetClues = (int)Math.round(W * H * 0.25);
|
||||
int placed = 0, guard = 0;
|
||||
|
||||
while (placed < targetClues && guard++ < 4000) {
|
||||
int r = rng.randint(0, H - 1);
|
||||
int c = rng.randint(0, W - 1);
|
||||
if (isDigit(g[r][c])) continue;
|
||||
|
||||
char d = (char)('0' + rng.randint(1, 4));
|
||||
g[r][c] = d;
|
||||
if (!hasRoomForClue(g, r, c, d)) {
|
||||
g[r][c] = '#';
|
||||
continue;
|
||||
}
|
||||
placed++;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
static char[][] mutate(Rng rng, char[][] grid) {
|
||||
char[][] g = deepCopyGrid(grid);
|
||||
int cx = rng.randint(0, H - 1);
|
||||
int cy = rng.randint(0, W - 1);
|
||||
|
||||
int steps = 4;
|
||||
for (int k = 0; k < steps; k++) {
|
||||
int rr = clamp(cx + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, H - 1);
|
||||
int cc = clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, W - 1);
|
||||
|
||||
char cur = g[rr][cc];
|
||||
if (isDigit(cur)) {
|
||||
g[rr][cc] = '#';
|
||||
} else {
|
||||
char d = (char)('0' + rng.randint(1, 4));
|
||||
g[rr][cc] = d;
|
||||
if (!hasRoomForClue(g, rr, cc, d)) g[rr][cc] = '#';
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
static char[][] crossover(Rng rng, char[][] a, char[][] b) {
|
||||
char[][] out = makeEmptyGrid();
|
||||
double cx = (H - 1) / 2.0;
|
||||
double cy = (W - 1) / 2.0;
|
||||
double theta = rng.nextFloat() * Math.PI;
|
||||
double nx = Math.cos(theta);
|
||||
double ny = Math.sin(theta);
|
||||
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) {
|
||||
double x = r - cx, y = c - cy;
|
||||
double side = x * nx + y * ny;
|
||||
out[r][c] = (side >= 0) ? a[r][c] : b[r][c];
|
||||
}
|
||||
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) {
|
||||
char ch = out[r][c];
|
||||
if (isDigit(ch) && !hasRoomForClue(out, r, c, ch)) out[r][c] = '#';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static char[][] hillclimb(Rng rng, char[][] start, HashMap<Integer, Integer> lenCounts, int limit) {
|
||||
char[][] best = deepCopyGrid(start);
|
||||
long bestF = maskFitness(best, lenCounts);
|
||||
int fails = 0;
|
||||
|
||||
while (fails < limit) {
|
||||
char[][] cand = mutate(rng, best);
|
||||
long f = maskFitness(cand, lenCounts);
|
||||
if (f < bestF) {
|
||||
best = cand;
|
||||
bestF = f;
|
||||
fails = 0;
|
||||
} else {
|
||||
fails++;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static double similarity(char[][] a, char[][] b) {
|
||||
int same = 0;
|
||||
for (int r = 0; r < H; r++) for (int c = 0; c < W; c++) if (a[r][c] == b[r][c]) same++;
|
||||
return same / (double)(W * H);
|
||||
}
|
||||
|
||||
static char[][] generateMask(Rng rng, HashMap<Integer, Integer> lenCounts, int popSize, int gens) {
|
||||
System.out.println("generateMask init pop: " + popSize);
|
||||
ArrayList<char[][]> pop = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < popSize; i++) {
|
||||
char[][] g = randomMask(rng);
|
||||
pop.add(hillclimb(rng, g, lenCounts, 180));
|
||||
}
|
||||
|
||||
for (int gen = 0; gen < gens; gen++) {
|
||||
ArrayList<char[][]> children = new ArrayList<>();
|
||||
int pairs = Math.max(popSize, (int)Math.floor(popSize * 1.5));
|
||||
|
||||
for (int k = 0; k < pairs; k++) {
|
||||
char[][] p1 = pop.get(rng.randint(0, pop.size() - 1));
|
||||
char[][] p2 = pop.get(rng.randint(0, pop.size() - 1));
|
||||
char[][] child = crossover(rng, p1, p2);
|
||||
children.add(hillclimb(rng, child, lenCounts, 70));
|
||||
}
|
||||
|
||||
pop.addAll(children);
|
||||
pop.sort(Comparator.comparingLong(g -> maskFitness(g, lenCounts)));
|
||||
|
||||
ArrayList<char[][]> next = new ArrayList<>();
|
||||
for (char[][] cand : pop) {
|
||||
if (next.size() >= popSize) break;
|
||||
boolean ok = true;
|
||||
for (char[][] kept : next) {
|
||||
if (similarity(cand, kept) > 0.92) { ok = false; break; }
|
||||
}
|
||||
if (ok) next.add(cand);
|
||||
}
|
||||
pop = next;
|
||||
|
||||
if (gen % 10 == 0) {
|
||||
long 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) ----------------
|
||||
|
||||
public static final class FillStats {
|
||||
public long nodes;
|
||||
public long backtracks;
|
||||
public double seconds;
|
||||
public int lastMRV;
|
||||
}
|
||||
|
||||
public static final class FillResult {
|
||||
public boolean ok;
|
||||
public char[][] grid;
|
||||
public HashMap<String, String> clueMap;
|
||||
public FillStats stats;
|
||||
}
|
||||
|
||||
static final class Undo {
|
||||
final int[] rs, cs;
|
||||
final char[] prev;
|
||||
final int n;
|
||||
Undo(int[] rs, int[] cs, char[] prev, int n) {
|
||||
this.rs = rs; this.cs = cs; this.prev = prev; this.n = n;
|
||||
}
|
||||
}
|
||||
|
||||
static char[] patternForSlot(char[][] grid, Slot s) {
|
||||
char[] pat = new char[s.len];
|
||||
for (int i = 0; i < s.len; i++) {
|
||||
char ch = grid[s.rs[i]][s.cs[i]];
|
||||
pat[i] = isLetter(ch) ? ch : 0;
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
static int slotScore(int[][] cellCount, Slot s) {
|
||||
int cross = 0;
|
||||
for (int i = 0; i < s.len; i++) cross += (cellCount[s.rs[i]][s.cs[i]] - 1);
|
||||
return cross * 10 + s.len;
|
||||
}
|
||||
|
||||
static Undo placeWord(char[][] grid, Slot s, String w) {
|
||||
int[] urs = new int[s.len];
|
||||
int[] ucs = new int[s.len];
|
||||
char[] up = new char[s.len];
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < s.len; i++) {
|
||||
int r = s.rs[i], c = s.cs[i];
|
||||
char prev = grid[r][c];
|
||||
char ch = w.charAt(i);
|
||||
if (prev == '#') {
|
||||
urs[n] = r; ucs[n] = c; up[n] = prev;
|
||||
n++;
|
||||
grid[r][c] = ch;
|
||||
} else if (prev != ch) {
|
||||
// rollback immediate changes
|
||||
for (int j = 0; j < n; j++) grid[urs[j]][ucs[j]] = up[j];
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return new Undo(urs, ucs, up, n);
|
||||
}
|
||||
|
||||
static void undoPlace(char[][] grid, Undo u) {
|
||||
for (int i = 0; i < u.n; i++) grid[u.rs[i]][u.cs[i]] = u.prev[i];
|
||||
}
|
||||
|
||||
static FillResult fillMask(Rng rng, char[][] mask, HashMap<Integer, DictEntry> dictIndex,
|
||||
int logEveryMs, int timeLimitMs) {
|
||||
|
||||
char[][] grid = deepCopyGrid(mask);
|
||||
ArrayList<Slot> allSlots = extractSlots(grid);
|
||||
ArrayList<Slot> slots = new ArrayList<>();
|
||||
for (Slot s : allSlots) if (s.len >= MIN_LEN && s.len <= MAX_LEN) slots.add(s);
|
||||
|
||||
HashSet<String> used = new HashSet<>();
|
||||
HashMap<String, String> assigned = new HashMap<>();
|
||||
|
||||
int[][] cellCount = new int[H][W];
|
||||
for (Slot s : slots) for (int i = 0; i < s.len; i++) cellCount[s.rs[i]][s.cs[i]]++;
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
final java.util.concurrent.atomic.AtomicLong lastLog = new java.util.concurrent.atomic.AtomicLong(t0);
|
||||
|
||||
FillStats stats = new FillStats();
|
||||
final int TOTAL = slots.size();
|
||||
final int BAR_LEN = 22;
|
||||
|
||||
Runnable renderProgress = () -> {
|
||||
long now = System.currentTimeMillis();
|
||||
if ((now - lastLog.get()) < logEveryMs) return;
|
||||
lastLog.set(now);
|
||||
|
||||
int done = assigned.size();
|
||||
int pct = (TOTAL == 0) ? 100 : (int)Math.floor((done / (double)TOTAL) * 100);
|
||||
int filled = Math.min(BAR_LEN, (int)Math.floor((pct / 100.0) * BAR_LEN));
|
||||
String bar = "[" + "#".repeat(filled) + "-".repeat(BAR_LEN - filled) + "]";
|
||||
String elapsed = String.format(Locale.ROOT, "%.1fs", (now - t0) / 1000.0);
|
||||
|
||||
String 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();
|
||||
};
|
||||
|
||||
class Pick {
|
||||
Slot slot;
|
||||
CandidateInfo info;
|
||||
boolean done;
|
||||
}
|
||||
|
||||
java.util.function.Supplier<Pick> chooseMRV = () -> {
|
||||
Slot best = null;
|
||||
CandidateInfo bestInfo = null;
|
||||
|
||||
for (Slot s : slots) {
|
||||
String k = s.key();
|
||||
if (assigned.containsKey(k)) continue;
|
||||
|
||||
DictEntry entry = dictIndex.get(s.len);
|
||||
if (entry == null) {
|
||||
Pick p = new Pick();
|
||||
p.slot = null; p.info = null; p.done = false;
|
||||
return p;
|
||||
}
|
||||
|
||||
char[] pat = patternForSlot(grid, s);
|
||||
CandidateInfo info = candidateInfoForPattern(entry, pat);
|
||||
|
||||
if (info.count == 0) {
|
||||
Pick p = new Pick();
|
||||
p.slot = null; p.info = null; p.done = false;
|
||||
return p;
|
||||
}
|
||||
|
||||
if (best == null
|
||||
|| info.count < bestInfo.count
|
||||
|| (info.count == bestInfo.count && slotScore(cellCount, s) > slotScore(cellCount, best))) {
|
||||
best = s;
|
||||
bestInfo = info;
|
||||
if (info.count <= 1) break;
|
||||
}
|
||||
}
|
||||
|
||||
Pick p = new Pick();
|
||||
if (best == null) {
|
||||
p.slot = null;
|
||||
p.info = null;
|
||||
p.done = true;
|
||||
} else {
|
||||
p.slot = best;
|
||||
p.info = bestInfo;
|
||||
p.done = false;
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
final int MAX_TRIES_PER_SLOT = 500;
|
||||
|
||||
class Solver {
|
||||
boolean backtrack() {
|
||||
stats.nodes++;
|
||||
|
||||
if (timeLimitMs > 0 && (System.currentTimeMillis() - t0) > timeLimitMs) return false;
|
||||
|
||||
Pick pick = chooseMRV.get();
|
||||
if (pick.done) return true;
|
||||
if (pick.slot == null) { stats.backtracks++; return false; }
|
||||
|
||||
stats.lastMRV = pick.info.count;
|
||||
renderProgress.run();
|
||||
|
||||
Slot s = pick.slot;
|
||||
String k = s.key();
|
||||
DictEntry entry = dictIndex.get(s.len);
|
||||
char[] pat = patternForSlot(grid, s);
|
||||
|
||||
java.util.function.Function<String, Boolean> tryWord = (String w) -> {
|
||||
if (w == null) return false;
|
||||
if (used.contains(w)) return false;
|
||||
|
||||
for (int i = 0; i < pat.length; i++) {
|
||||
if (pat[i] != 0 && pat[i] != w.charAt(i)) return false;
|
||||
}
|
||||
|
||||
Undo undo = placeWord(grid, s, w);
|
||||
if (undo == null) return false;
|
||||
|
||||
used.add(w);
|
||||
assigned.put(k, w);
|
||||
|
||||
if (backtrack()) return true;
|
||||
|
||||
assigned.remove(k);
|
||||
used.remove(w);
|
||||
undoPlace(grid, undo);
|
||||
return false;
|
||||
};
|
||||
|
||||
if (pick.info.indices != null && pick.info.indices.length > 0) {
|
||||
int[] idxs = pick.info.indices;
|
||||
int L = idxs.length;
|
||||
int tries = Math.min(MAX_TRIES_PER_SLOT, L);
|
||||
|
||||
int start = (L == 1) ? 0 : rng.randint(0, L - 1);
|
||||
int step = (L <= 1) ? 1 : rng.randint(1, L - 1);
|
||||
|
||||
for (int t = 0; t < tries; t++) {
|
||||
int idx = idxs[(start + t * step) % L];
|
||||
String w = entry.words.get(idx);
|
||||
if (tryWord.apply(w)) return true;
|
||||
}
|
||||
stats.backtracks++;
|
||||
return false;
|
||||
}
|
||||
|
||||
int N = entry.words.size();
|
||||
if (N == 0) { stats.backtracks++; return false; }
|
||||
|
||||
int tries = Math.min(MAX_TRIES_PER_SLOT, N);
|
||||
int start = (N == 1) ? 0 : rng.randint(0, N - 1);
|
||||
int step = (N <= 1) ? 1 : rng.randint(1, N - 1);
|
||||
|
||||
for (int t = 0; t < tries; t++) {
|
||||
int idx = (start + t * step) % N;
|
||||
String w = entry.words.get(idx);
|
||||
if (tryWord.apply(w)) return true;
|
||||
}
|
||||
|
||||
stats.backtracks++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// initial render (same feel)
|
||||
renderProgress.run();
|
||||
boolean ok = new Solver().backtrack();
|
||||
// final progress line
|
||||
System.out.print("\r" + padRight("", 120) + "\r");
|
||||
System.out.flush();
|
||||
|
||||
FillResult res = new FillResult();
|
||||
res.ok = ok;
|
||||
res.grid = grid;
|
||||
res.clueMap = assigned;
|
||||
stats.seconds = (System.currentTimeMillis() - t0) / 1000.0;
|
||||
res.stats = stats;
|
||||
|
||||
// print a final progress line
|
||||
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());
|
||||
}
|
||||
|
||||
// ---------------- Top-level generatePuzzle ----------------
|
||||
|
||||
public static final class PuzzleResult {
|
||||
public char[][] mask;
|
||||
public FillResult filled;
|
||||
}
|
||||
|
||||
public static PuzzleResult generatePuzzle(Main.Opts opts) {
|
||||
var rng = new Rng(opts.seed);
|
||||
|
||||
var tLoad0 = System.nanoTime();
|
||||
var dict = loadWords(opts.wordsPath);
|
||||
var tLoad1 = System.nanoTime();
|
||||
System.out.printf(Locale.ROOT, "LOAD_WORDS: %.3fs%n", (tLoad1 - tLoad0) / 1e9);
|
||||
|
||||
for (int attempt = 1; attempt <= opts.tries; attempt++) {
|
||||
System.out.println("\nAttempt " + attempt + "/" + opts.tries);
|
||||
|
||||
long tMask0 = System.nanoTime();
|
||||
char[][] mask = generateMask(rng, dict.lenCounts, opts.pop, opts.gens);
|
||||
long tMask1 = System.nanoTime();
|
||||
System.out.printf(Locale.ROOT, "MASK: %.3fs%n", (tMask1 - tMask0) / 1e9);
|
||||
|
||||
long tFill0 = System.nanoTime();
|
||||
var filled = fillMask(rng, mask, dict.index, 200, 30000);
|
||||
long tFill1 = System.nanoTime();
|
||||
System.out.printf(Locale.ROOT, "FILL: %.3fms%n", (tFill1 - tFill0) / 1e6);
|
||||
|
||||
if (filled.ok) {
|
||||
var pr = new PuzzleResult();
|
||||
pr.mask = mask;
|
||||
pr.filled = filled;
|
||||
return pr;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------- main ----------------
|
||||
|
||||
public static void convert(Main.Opts opts) {
|
||||
|
||||
var res = generatePuzzle(opts);
|
||||
if (res == null) {
|
||||
System.out.println("No solution found within tries.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println("\n=== GENERATED MASK ===");
|
||||
System.out.println(gridToString(res.mask));
|
||||
|
||||
System.out.println("\n=== FILLED PUZZLE (RAW) ===");
|
||||
System.out.println(gridToString(res.filled.grid));
|
||||
|
||||
System.out.println("\n=== FILLED PUZZLE (HUMAN) ===");
|
||||
System.out.println(renderHuman(res.filled.grid));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user