clean
This commit is contained in:
@@ -2,4 +2,5 @@ paper/
|
|||||||
.git/
|
.git/
|
||||||
data/
|
data/
|
||||||
target/
|
target/
|
||||||
.idea/
|
.idea/
|
||||||
|
.env
|
||||||
2
.env
2
.env
@@ -1,2 +0,0 @@
|
|||||||
PUZZLE_ROOT_DIR=/home/mike/dev/puzzle-generator
|
|
||||||
OUT_DIR=/home/mike/dev/puzzle-generator/data
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.idea/
|
.idea/
|
||||||
/vocab/.custom/
|
/vocab/.custom/
|
||||||
**/.custom/
|
**/.custom/
|
||||||
target/
|
target/
|
||||||
|
.env
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
// export_format.js
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const DIRS = {
|
|
||||||
"1": [-1, 0], // up
|
|
||||||
"2": [0, 1], // right
|
|
||||||
"3": [1, 0], // down
|
|
||||||
"4": [0, -1], // left
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDigit = (ch) => ch >= "1" && ch <= "4";
|
|
||||||
const isLetter = (ch) => ch >= "A" && ch <= "Z";
|
|
||||||
|
|
||||||
function toGrid2D(grid) {
|
|
||||||
if (Array.isArray(grid) && typeof grid[0] === "string") return grid.map(r => r.split(""));
|
|
||||||
return grid; // assume 2D char array
|
|
||||||
}
|
|
||||||
|
|
||||||
function inBounds(H, W, r, c) {
|
|
||||||
return r >= 0 && r < H && c >= 0 && c < W;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a word run for a clue cell at (r,c) with direction digit d.
|
|
||||||
* Returns canonical representation where:
|
|
||||||
* - direction is only "horizontal"(right) or "vertical"(down)
|
|
||||||
* - startRow/startCol is the first letter cell in that canonical direction
|
|
||||||
* - arrowRow/arrowCol is immediately before the start (left or above)
|
|
||||||
* - word is read from grid in canonical order (start -> end)
|
|
||||||
*/
|
|
||||||
function extractPlacedFromClue(g, r, c, d, maxLen = 8, minLen = 2) {
|
|
||||||
const H = g.length, W = g[0].length;
|
|
||||||
const [dr, dc] = DIRS[d];
|
|
||||||
|
|
||||||
// collect letter cells in the ORIGINAL direction away from the clue
|
|
||||||
const cells = [];
|
|
||||||
let rr = r + dr, cc = c + dc;
|
|
||||||
while (inBounds(H, W, rr, cc) && isLetter(g[rr][cc]) && cells.length < maxLen) {
|
|
||||||
cells.push([rr, cc]);
|
|
||||||
rr += dr;
|
|
||||||
cc += dc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cells.length < minLen) return null;
|
|
||||||
|
|
||||||
// Canonicalize so we always output right/down runs
|
|
||||||
// If original was right (2) or down (3): start is first cell, arrow is clue cell
|
|
||||||
// If original was left (4): start is the farthest-left cell, arrow is one cell left of start
|
|
||||||
// If original was up (1): start is the topmost cell, arrow is one cell above start
|
|
||||||
let startRow, startCol, arrowRow, arrowCol, direction;
|
|
||||||
|
|
||||||
if (d === "2") { // right
|
|
||||||
direction = "horizontal";
|
|
||||||
[startRow, startCol] = cells[0];
|
|
||||||
arrowRow = r;
|
|
||||||
arrowCol = c; // clue cell is before start
|
|
||||||
} else if (d === "3") { // down
|
|
||||||
direction = "vertical";
|
|
||||||
[startRow, startCol] = cells[0];
|
|
||||||
arrowRow = r;
|
|
||||||
arrowCol = c;
|
|
||||||
} else if (d === "4") { // left => canonical right
|
|
||||||
direction = "horizontal";
|
|
||||||
// farthest left is last in cells list (because we walked left)
|
|
||||||
[startRow, startCol] = cells[cells.length - 1];
|
|
||||||
arrowRow = startRow;
|
|
||||||
arrowCol = startCol - 1;
|
|
||||||
} else if (d === "1") { // up => canonical down
|
|
||||||
direction = "vertical";
|
|
||||||
[startRow, startCol] = cells[cells.length - 1]; // topmost
|
|
||||||
arrowRow = startRow - 1;
|
|
||||||
arrowCol = startCol;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the word from the grid in canonical order (right or down)
|
|
||||||
const wordChars = [];
|
|
||||||
if (direction === "horizontal") {
|
|
||||||
for (let i = 0; i < cells.length; i++) {
|
|
||||||
const ch = (inBounds(H, W, startRow, startCol + i) ? g[startRow][startCol + i] : "#");
|
|
||||||
if (!isLetter(ch)) break;
|
|
||||||
wordChars.push(ch);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < cells.length; i++) {
|
|
||||||
const ch = (inBounds(H, W, startRow + i, startCol) ? g[startRow + i][startCol] : "#");
|
|
||||||
if (!isLetter(ch)) break;
|
|
||||||
wordChars.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const word = wordChars.join("");
|
|
||||||
if (word.length < minLen || word.length > maxLen) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
word,
|
|
||||||
clue: word, // placeholder; you’ll replace later
|
|
||||||
startRow,
|
|
||||||
startCol,
|
|
||||||
direction,
|
|
||||||
answer: word,
|
|
||||||
arrowRow,
|
|
||||||
arrowCol,
|
|
||||||
// For cropping:
|
|
||||||
_cells: direction === "horizontal"
|
|
||||||
? Array.from({length: word.length}, (_, i) => [startRow, startCol + i])
|
|
||||||
: Array.from({length: word.length}, (_, i) => [startRow + i, startCol]),
|
|
||||||
_arrow: [arrowRow, arrowCol],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform your generator output into the JSON format you showed.
|
|
||||||
* @param {Object} puz - { grid: string[]|char[][], clueMap?: object }
|
|
||||||
*/
|
|
||||||
function exportFormatFromFilled(puz, difficulty = 1, rewards = {coins: 50, stars: 2, hints: 1}) {
|
|
||||||
const g = toGrid2D(puz.grid);
|
|
||||||
const H = g.length, W = g[0].length;
|
|
||||||
|
|
||||||
// 1) extract "placed" list from all clue digits in the filled grid
|
|
||||||
const placed = [];
|
|
||||||
const seen = new Set(); // avoid duplicates by start+dir
|
|
||||||
for (let r = 0; r < H; r++) {
|
|
||||||
for (let c = 0; c < W; c++) {
|
|
||||||
const ch = g[r][c];
|
|
||||||
if (!isDigit(ch)) continue;
|
|
||||||
|
|
||||||
const p = extractPlacedFromClue(g, r, c, ch, 8, 2);
|
|
||||||
if (!p) continue;
|
|
||||||
|
|
||||||
const key = `${p.startRow},${p.startCol}:${p.direction}:${p.word}`;
|
|
||||||
if (seen.has(key)) continue;
|
|
||||||
seen.add(key);
|
|
||||||
placed.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (placed.length === 0) {
|
|
||||||
return {gridv2: g.map(row => row.map(ch => (isLetter(ch) ? ch : "#")).join("")), words: [], difficulty, rewards};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) compute bounding box around all word cells + arrow cells, with 1-cell margin
|
|
||||||
const allCells = [];
|
|
||||||
for (const p of placed) {
|
|
||||||
for (const [rr, cc] of p._cells) allCells.push([rr, cc]);
|
|
||||||
allCells.push(p._arrow);
|
|
||||||
}
|
|
||||||
|
|
||||||
let minR = Math.min(...allCells.map(([r]) => r)) - 1;
|
|
||||||
let minC = Math.min(...allCells.map(([, c]) => c)) - 1;
|
|
||||||
let maxR = Math.max(...allCells.map(([r]) => r)) + 1;
|
|
||||||
let maxC = Math.max(...allCells.map(([, c]) => c)) + 1;
|
|
||||||
|
|
||||||
// 3) build a map of only the used letter cells (so everything else becomes '#')
|
|
||||||
const letterAt = new Map();
|
|
||||||
for (const p of placed) {
|
|
||||||
for (const [rr, cc] of p._cells) {
|
|
||||||
if (inBounds(H, W, rr, cc) && isLetter(g[rr][cc])) {
|
|
||||||
letterAt.set(`${rr},${cc}`, g[rr][cc]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) render gridv2
|
|
||||||
const gridv2 = [];
|
|
||||||
for (let r = minR; r <= maxR; r++) {
|
|
||||||
let row = "";
|
|
||||||
for (let c = minC; c <= maxC; c++) {
|
|
||||||
const ch = letterAt.get(`${r},${c}`);
|
|
||||||
row += ch ? ch : "#";
|
|
||||||
}
|
|
||||||
gridv2.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5) words output with cropped coordinates
|
|
||||||
const words_out = placed.map(p => ({
|
|
||||||
word: p.word,
|
|
||||||
clue: p.clue, // currently word itself
|
|
||||||
startRow: p.startRow - minR,
|
|
||||||
startCol: p.startCol - minC,
|
|
||||||
direction: p.direction,
|
|
||||||
answer: p.word,
|
|
||||||
arrowRow: p.arrowRow - minR,
|
|
||||||
arrowCol: p.arrowCol - minC,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {gridv2, words: words_out, difficulty, rewards};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {exportFormatFromFilled};
|
|
||||||
26
node/main.js
26
node/main.js
@@ -1,26 +0,0 @@
|
|||||||
const {parseArgs, generatePuzzle, gridToString} = require("./swedish_generator");
|
|
||||||
const {exportFormatFromFilled} = require("./export_format");
|
|
||||||
|
|
||||||
// ---- main ----
|
|
||||||
|
|
||||||
(function main() {
|
|
||||||
const opts = parseArgs(process.argv);
|
|
||||||
console.log(opts);
|
|
||||||
|
|
||||||
const res = generatePuzzle(opts);
|
|
||||||
if (!res) {
|
|
||||||
console.error("Failed to generate a fillable puzzle.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Existing logs...
|
|
||||||
console.log("\n=== FILLED PUZZLE (RAW) ===");
|
|
||||||
console.log(gridToString(res.filled.grid));
|
|
||||||
|
|
||||||
// ✅ Transform to your JSON format
|
|
||||||
const puz = {grid: res.filled.grid, clueMap: res.filled.clueMap};
|
|
||||||
const json = exportFormatFromFilled(puz, 1);
|
|
||||||
|
|
||||||
console.log("\n=== EXPORTED JSON ===");
|
|
||||||
console.log(JSON.stringify(json, null, 2));
|
|
||||||
})();
|
|
||||||
@@ -1,654 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const W = 9, H = 8;
|
|
||||||
const MIN_LEN = 2, MAX_LEN = 8;
|
|
||||||
|
|
||||||
const DIRS = {
|
|
||||||
"1": [-1, 0], // up
|
|
||||||
"2": [0, 1], // right
|
|
||||||
"3": [1, 0], // down
|
|
||||||
"4": [0, -1], // left
|
|
||||||
};
|
|
||||||
|
|
||||||
const IS_DIGIT = (ch) => ch >= "1" && ch <= "4";
|
|
||||||
const IS_LETTER = (ch) => ch >= "A" && ch <= "Z";
|
|
||||||
const IS_LETTER_CELL = (ch) => ch === "#" || IS_LETTER(ch);
|
|
||||||
|
|
||||||
function usage() {
|
|
||||||
console.log(`Usage:
|
|
||||||
node swedish_generator.js [--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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const out = {seed: 1, pop: 18, gens: 100, tries: 50, wordsPath: "./word-list.txt"};
|
|
||||||
for (let i = 2; i < argv.length; i++) {
|
|
||||||
const a = argv[i];
|
|
||||||
const v = argv[i + 1];
|
|
||||||
if (a === "--help" || a === "-h") {
|
|
||||||
usage();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
if (a === "--seed") out.seed = parseInt(v, 10), i++;
|
|
||||||
else if (a === "--pop") out.pop = parseInt(v, 10), i++;
|
|
||||||
else if (a === "--gens") out.gens = parseInt(v, 10), i++;
|
|
||||||
else if (a === "--tries") out.tries = parseInt(v, 10), i++;
|
|
||||||
else if (a === "--words") out.wordsPath = v, i++;
|
|
||||||
else throw new Error(`Unknown arg: ${a}`);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Seeded RNG (xorshift32) */
|
|
||||||
function makeRng(seed) {
|
|
||||||
let x = (seed >>> 0) || 1;
|
|
||||||
return {
|
|
||||||
nextU32() {
|
|
||||||
x ^= x << 13;
|
|
||||||
x >>>= 0;
|
|
||||||
x ^= x >>> 17;
|
|
||||||
x >>>= 0;
|
|
||||||
x ^= x << 5;
|
|
||||||
x >>>= 0;
|
|
||||||
return x >>> 0;
|
|
||||||
},
|
|
||||||
int(min, max) {
|
|
||||||
const r = this.nextU32();
|
|
||||||
return min + (r % (max - min + 1));
|
|
||||||
},
|
|
||||||
float() {
|
|
||||||
return this.nextU32() / 0xFFFFFFFF;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
|
|
||||||
|
|
||||||
function makeEmptyGrid() {
|
|
||||||
return Array.from({length: H}, () => Array.from({length: W}, () => "#"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deepCopyGrid(g) { return g.map(r => r.slice()); }
|
|
||||||
|
|
||||||
function gridToString(g) { return g.map(r => r.join("")).join("\n"); }
|
|
||||||
|
|
||||||
function renderHuman(g) {
|
|
||||||
return g.map(row => row.map(ch => IS_DIGIT(ch) ? " " : ch).join("")).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --- Words / index --- */
|
|
||||||
function loadWords(wordsPath) {
|
|
||||||
let raw = "";
|
|
||||||
try {
|
|
||||||
raw = fs.readFileSync(wordsPath, "utf8");
|
|
||||||
} catch {
|
|
||||||
raw = "EU\nUUR\nAUTO\nBOOM\nHUIS\nKAT\nZEE\nRODE\nDRAAD\nKENNIS\nNETWERK\nPAKTE\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
const words = raw
|
|
||||||
.split(/\r?\n/g)
|
|
||||||
.map(s => s.trim().toUpperCase())
|
|
||||||
.filter(s => /^[A-Z]{2,8}$/.test(s));
|
|
||||||
|
|
||||||
// index[len] = { words: string[], pos: Array(len) of [26 arrays of indices] }
|
|
||||||
const index = new Map();
|
|
||||||
const lenCounts = new Map();
|
|
||||||
|
|
||||||
for (const w of words) {
|
|
||||||
const L = w.length;
|
|
||||||
lenCounts.set(L, (lenCounts.get(L) || 0) + 1);
|
|
||||||
|
|
||||||
if (!index.has(L)) {
|
|
||||||
const pos = Array.from({length: L}, () =>
|
|
||||||
Array.from({length: 26}, () => [])
|
|
||||||
);
|
|
||||||
index.set(L, {words: [], pos});
|
|
||||||
}
|
|
||||||
const entry = index.get(L);
|
|
||||||
const idx = entry.words.length;
|
|
||||||
entry.words.push(w);
|
|
||||||
for (let i = 0; i < L; i++) {
|
|
||||||
entry.pos[i][w.charCodeAt(i) - 65].push(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {words, index, lenCounts};
|
|
||||||
}
|
|
||||||
|
|
||||||
function intersectSorted(a, b) {
|
|
||||||
const out = [];
|
|
||||||
let i = 0, j = 0;
|
|
||||||
while (i < a.length && j < b.length) {
|
|
||||||
const x = a[i], y = b[j];
|
|
||||||
if (x === y) {
|
|
||||||
out.push(x);
|
|
||||||
i++;
|
|
||||||
j++;
|
|
||||||
} else if (x < y) i++;
|
|
||||||
else j++;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** returns {indices?: number[], count: number} WITHOUT allocating huge arrays */
|
|
||||||
function candidateInfoForPattern(entry, pattern /* array char|null */) {
|
|
||||||
const lists = [];
|
|
||||||
for (let i = 0; i < pattern.length; i++) {
|
|
||||||
const ch = pattern[i];
|
|
||||||
if (ch && IS_LETTER(ch)) {
|
|
||||||
lists.push(entry.pos[i][ch.charCodeAt(0) - 65]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lists.length === 0) {
|
|
||||||
return {indices: null, count: entry.words.length}; // unconstrained
|
|
||||||
}
|
|
||||||
lists.sort((a, b) => a.length - b.length);
|
|
||||||
let cur = lists[0];
|
|
||||||
for (let k = 1; k < lists.length; k++) {
|
|
||||||
cur = intersectSorted(cur, lists[k]);
|
|
||||||
if (cur.length === 0) break;
|
|
||||||
}
|
|
||||||
return {indices: cur, count: cur.length};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --- Slots --- */
|
|
||||||
function extractSlots(grid) {
|
|
||||||
const slots = [];
|
|
||||||
for (let r = 0; r < H; r++) {
|
|
||||||
for (let c = 0; c < W; c++) {
|
|
||||||
const d = grid[r][c];
|
|
||||||
if (!IS_DIGIT(d)) continue;
|
|
||||||
|
|
||||||
const [dr, dc] = DIRS[d];
|
|
||||||
let rr = r + dr, cc = c + dc;
|
|
||||||
if (rr < 0 || rr >= H || cc < 0 || cc >= W) continue;
|
|
||||||
if (!IS_LETTER_CELL(grid[rr][cc])) continue;
|
|
||||||
|
|
||||||
const cells = [];
|
|
||||||
while (rr >= 0 && rr < H && cc >= 0 && cc < W) {
|
|
||||||
const ch = grid[rr][cc];
|
|
||||||
if (!IS_LETTER_CELL(ch)) break;
|
|
||||||
cells.push([rr, cc]);
|
|
||||||
rr += dr;
|
|
||||||
cc += dc;
|
|
||||||
if (cells.length > MAX_LEN) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
slots.push({clue: [r, c, d], dir: d, cells, len: cells.length});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return slots;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRoomForClue(grid, r, c, d) {
|
|
||||||
const [dr, dc] = DIRS[d];
|
|
||||||
let rr = r + dr, cc = c + dc;
|
|
||||||
let run = 0;
|
|
||||||
while (rr >= 0 && rr < H && cc >= 0 && cc < W && IS_LETTER_CELL(grid[rr][cc]) && run < MAX_LEN) {
|
|
||||||
run++;
|
|
||||||
rr += dr;
|
|
||||||
cc += dc;
|
|
||||||
}
|
|
||||||
return run >= MIN_LEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --- FAST mask fitness (structural only) --- */
|
|
||||||
function maskFitness(grid, lenCounts) {
|
|
||||||
let penalty = 0;
|
|
||||||
|
|
||||||
// clue density (avoid all digits)
|
|
||||||
let clueCount = 0;
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
if (IS_DIGIT(grid[r][c])) clueCount++;
|
|
||||||
}
|
|
||||||
const targetClues = Math.round(W * H * 0.25); // ~18
|
|
||||||
penalty += 8 * Math.abs(clueCount - targetClues);
|
|
||||||
|
|
||||||
const slots = extractSlots(grid);
|
|
||||||
if (slots.length === 0) return 1e9;
|
|
||||||
|
|
||||||
// coverage counts per letter cell: horiz vs vert
|
|
||||||
const covH = Array.from({length: H}, () => Array(W).fill(0));
|
|
||||||
const covV = Array.from({length: H}, () => Array(W).fill(0));
|
|
||||||
|
|
||||||
for (const s of slots) {
|
|
||||||
const horiz = (s.dir === "2" || s.dir === "4");
|
|
||||||
|
|
||||||
if (s.len < MIN_LEN) penalty += 8000;
|
|
||||||
if (s.len > MAX_LEN) penalty += 8000 + (s.len - MAX_LEN) * 500;
|
|
||||||
|
|
||||||
// dictionary availability only (cheap)
|
|
||||||
if (s.len >= MIN_LEN && s.len <= MAX_LEN) {
|
|
||||||
if (!lenCounts.get(s.len)) penalty += 12000;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [r, c] of s.cells) {
|
|
||||||
if (horiz) covH[r][c] += 1;
|
|
||||||
else covV[r][c] += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coverage penalties per letter cell
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
if (!IS_LETTER_CELL(grid[r][c])) continue;
|
|
||||||
const h = covH[r][c], v = covV[r][c];
|
|
||||||
if (h === 0 && v === 0) penalty += 1500;
|
|
||||||
else if (h > 0 && v > 0) penalty += 0;
|
|
||||||
else if (h + v === 1) penalty += 200;
|
|
||||||
else penalty += 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clue clustering (8-connected)
|
|
||||||
const seen = Array.from({length: H}, () => Array(W).fill(false));
|
|
||||||
const nbrs8 = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
if (!IS_DIGIT(grid[r][c]) || seen[r][c]) continue;
|
|
||||||
const stack = [[r, c]];
|
|
||||||
seen[r][c] = true;
|
|
||||||
let size = 0;
|
|
||||||
while (stack.length) {
|
|
||||||
const [x, y] = stack.pop();
|
|
||||||
size++;
|
|
||||||
for (const [dr, dc] of nbrs8) {
|
|
||||||
const nx = x + dr, ny = y + dc;
|
|
||||||
if (nx < 0 || nx >= H || ny < 0 || ny >= W) continue;
|
|
||||||
if (seen[nx][ny]) continue;
|
|
||||||
if (!IS_DIGIT(grid[nx][ny])) continue;
|
|
||||||
seen[nx][ny] = true;
|
|
||||||
stack.push([nx, ny]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (size >= 2) penalty += (size - 1) * 120;
|
|
||||||
}
|
|
||||||
|
|
||||||
// dead-end-ish letter cell (3+ walls)
|
|
||||||
const nbrs4 = [[-1, 0], [1, 0], [0, -1], [0, 1]];
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
if (!IS_LETTER_CELL(grid[r][c])) continue;
|
|
||||||
let walls = 0;
|
|
||||||
for (const [dr, dc] of nbrs4) {
|
|
||||||
const rr = r + dr, cc = c + dc;
|
|
||||||
if (rr < 0 || rr >= H || cc < 0 || cc >= W) {
|
|
||||||
walls++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!IS_LETTER_CELL(grid[rr][cc])) walls++;
|
|
||||||
}
|
|
||||||
if (walls >= 3) penalty += 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
return penalty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --- Mask generation (memetic-ish + hillclimb) --- */
|
|
||||||
function randomMask(rng) {
|
|
||||||
const g = makeEmptyGrid();
|
|
||||||
const targetClues = Math.round(W * H * 0.25); // ~18
|
|
||||||
let placed = 0, guard = 0;
|
|
||||||
|
|
||||||
while (placed < targetClues && guard++ < 4000) {
|
|
||||||
const r = rng.int(0, H - 1);
|
|
||||||
const c = rng.int(0, W - 1);
|
|
||||||
if (IS_DIGIT(g[r][c])) continue;
|
|
||||||
|
|
||||||
const d = String(rng.int(1, 4));
|
|
||||||
g[r][c] = d;
|
|
||||||
if (!hasRoomForClue(g, r, c, d)) {
|
|
||||||
g[r][c] = "#";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
placed++;
|
|
||||||
}
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mutate(rng, grid) {
|
|
||||||
const g = deepCopyGrid(grid);
|
|
||||||
const cx = rng.int(0, H - 1);
|
|
||||||
const cy = rng.int(0, W - 1);
|
|
||||||
|
|
||||||
const steps = 4;
|
|
||||||
for (let k = 0; k < steps; k++) {
|
|
||||||
const rr = clamp(cx + (rng.int(-2, 2) + rng.int(-2, 2)), 0, H - 1);
|
|
||||||
const cc = clamp(cy + (rng.int(-2, 2) + rng.int(-2, 2)), 0, W - 1);
|
|
||||||
|
|
||||||
const cur = g[rr][cc];
|
|
||||||
if (IS_DIGIT(cur)) {
|
|
||||||
g[rr][cc] = "#";
|
|
||||||
} else {
|
|
||||||
const d = String(rng.int(1, 4));
|
|
||||||
g[rr][cc] = d;
|
|
||||||
if (!hasRoomForClue(g, rr, cc, d)) g[rr][cc] = "#";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
function crossover(rng, a, b) {
|
|
||||||
const out = makeEmptyGrid();
|
|
||||||
const cx = (H - 1) / 2;
|
|
||||||
const cy = (W - 1) / 2;
|
|
||||||
const theta = rng.float() * Math.PI;
|
|
||||||
const nx = Math.cos(theta);
|
|
||||||
const ny = Math.sin(theta);
|
|
||||||
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
const x = r - cx, y = c - cy;
|
|
||||||
const side = x * nx + y * ny;
|
|
||||||
out[r][c] = (side >= 0) ? a[r][c] : b[r][c];
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup invalid clues
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
const ch = out[r][c];
|
|
||||||
if (IS_DIGIT(ch) && !hasRoomForClue(out, r, c, ch)) out[r][c] = "#";
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hillclimb(rng, start, lenCounts, limit) {
|
|
||||||
let best = deepCopyGrid(start);
|
|
||||||
let bestF = maskFitness(best, lenCounts);
|
|
||||||
let fails = 0;
|
|
||||||
|
|
||||||
while (fails < limit) {
|
|
||||||
const cand = mutate(rng, best);
|
|
||||||
const f = maskFitness(cand, lenCounts);
|
|
||||||
if (f < bestF) {
|
|
||||||
best = cand;
|
|
||||||
bestF = f;
|
|
||||||
fails = 0;
|
|
||||||
} else {
|
|
||||||
fails++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
function similarity(a, b) {
|
|
||||||
let same = 0;
|
|
||||||
for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
|
|
||||||
if (a[r][c] === b[r][c]) same++;
|
|
||||||
}
|
|
||||||
return same / (W * H);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateMask(rng, lenCounts, popSize, gens) {
|
|
||||||
console.log(`generateMask init pop: ${popSize}`);
|
|
||||||
let pop = [];
|
|
||||||
for (let i = 0; i < popSize; i++) {
|
|
||||||
const g = randomMask(rng);
|
|
||||||
pop.push(hillclimb(rng, g, lenCounts, 180)); // faster init
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let gen = 0; gen < gens; gen++) {
|
|
||||||
const children = [];
|
|
||||||
const pairs = Math.max(popSize, Math.floor(popSize * 1.5));
|
|
||||||
for (let k = 0; k < pairs; k++) {
|
|
||||||
const p1 = pop[rng.int(0, pop.length - 1)];
|
|
||||||
const p2 = pop[rng.int(0, pop.length - 1)];
|
|
||||||
const child = crossover(rng, p1, p2);
|
|
||||||
children.push(hillclimb(rng, child, lenCounts, 70)); // light repair
|
|
||||||
}
|
|
||||||
|
|
||||||
pop = pop.concat(children);
|
|
||||||
pop.sort((x, y) => maskFitness(x, lenCounts) - maskFitness(y, lenCounts));
|
|
||||||
|
|
||||||
// similarity cull
|
|
||||||
const next = [];
|
|
||||||
for (const cand of pop) {
|
|
||||||
if (next.length >= popSize) break;
|
|
||||||
let ok = true;
|
|
||||||
for (const kept of next) {
|
|
||||||
if (similarity(cand, kept) > 0.92) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ok) next.push(cand);
|
|
||||||
}
|
|
||||||
pop = next;
|
|
||||||
|
|
||||||
if ((gen % 10) === 0) {
|
|
||||||
const bestF = maskFitness(pop[0], lenCounts);
|
|
||||||
console.log(` gen ${gen}/${gens} bestFitness=${bestF}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pop.sort((x, y) => maskFitness(x, lenCounts) - maskFitness(y, lenCounts));
|
|
||||||
return pop[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --- Fill (CSP) with NO huge candidate arrays --- */
|
|
||||||
function fillMask(rng, mask, dictIndex, opts = {}) {
|
|
||||||
const grid = deepCopyGrid(mask);
|
|
||||||
const slots = extractSlots(grid).filter(s => s.len >= MIN_LEN && s.len <= MAX_LEN);
|
|
||||||
|
|
||||||
const used = new Set();
|
|
||||||
const assigned = new Map();
|
|
||||||
|
|
||||||
// progress options
|
|
||||||
const logEveryMs = opts.logEveryMs ?? 250;
|
|
||||||
const timeLimitMs = opts.timeLimitMs ?? 0; // 0 = no limit
|
|
||||||
|
|
||||||
// crossing weight precompute
|
|
||||||
const cellCount = Array.from({length: H}, () => Array(W).fill(0));
|
|
||||||
for (const s of slots) for (const [r, c] of s.cells) cellCount[r][c]++;
|
|
||||||
|
|
||||||
function slotKey(s) { return `${s.clue[0]},${s.clue[1]}:${s.clue[2]}`; }
|
|
||||||
|
|
||||||
function patternForSlot(s) {
|
|
||||||
return s.cells.map(([r, c]) => {
|
|
||||||
const ch = grid[r][c];
|
|
||||||
return IS_LETTER(ch) ? ch : null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function slotScore(s) {
|
|
||||||
let cross = 0;
|
|
||||||
for (const [r, c] of s.cells) cross += (cellCount[r][c] - 1);
|
|
||||||
return cross * 10 + s.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
function placeWord(s, w) {
|
|
||||||
const undo = [];
|
|
||||||
for (let i = 0; i < s.cells.length; i++) {
|
|
||||||
const [r, c] = s.cells[i];
|
|
||||||
const prev = grid[r][c];
|
|
||||||
const ch = w[i];
|
|
||||||
if (prev === "#") {
|
|
||||||
undo.push([r, c, prev]);
|
|
||||||
grid[r][c] = ch;
|
|
||||||
} else if (prev !== ch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undo;
|
|
||||||
}
|
|
||||||
|
|
||||||
function undoPlace(undo) { for (const [r, c, prev] of undo) grid[r][c] = prev; }
|
|
||||||
|
|
||||||
// ---- progress bar ----
|
|
||||||
const t0 = Date.now();
|
|
||||||
let lastLog = t0;
|
|
||||||
let nodes = 0;
|
|
||||||
let backtracks = 0;
|
|
||||||
let lastMRV = 0;
|
|
||||||
|
|
||||||
function renderProgress(final = false) {
|
|
||||||
const now = Date.now();
|
|
||||||
if (!final && (now - lastLog) < logEveryMs) return;
|
|
||||||
lastLog = now;
|
|
||||||
|
|
||||||
const done = assigned.size;
|
|
||||||
const total = slots.length;
|
|
||||||
const pct = total ? Math.floor((done / total) * 100) : 100;
|
|
||||||
const barLen = 22;
|
|
||||||
const filled = Math.min(barLen, Math.floor((pct / 100) * barLen));
|
|
||||||
const bar = `[${"#".repeat(filled)}${"-".repeat(barLen - filled)}]`;
|
|
||||||
|
|
||||||
const elapsed = ((now - t0) / 1000).toFixed(1);
|
|
||||||
const msg =
|
|
||||||
`${bar} ${done}/${total} slots | nodes=${nodes} | backtracks=${backtracks} | mrv=${lastMRV} | ${elapsed}s`;
|
|
||||||
|
|
||||||
process.stdout.write("\r" + msg.padEnd(120));
|
|
||||||
if (final) process.stdout.write("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseMRV() {
|
|
||||||
let best = null;
|
|
||||||
let bestInfo = null;
|
|
||||||
|
|
||||||
for (const s of slots) {
|
|
||||||
const k = slotKey(s);
|
|
||||||
if (assigned.has(k)) continue;
|
|
||||||
|
|
||||||
const entry = dictIndex.get(s.len);
|
|
||||||
if (!entry) return {slot: null, info: null};
|
|
||||||
|
|
||||||
const pat = patternForSlot(s);
|
|
||||||
const info = candidateInfoForPattern(entry, pat);
|
|
||||||
|
|
||||||
if (info.count === 0) return {slot: null, info: null};
|
|
||||||
|
|
||||||
if (
|
|
||||||
!best ||
|
|
||||||
info.count < bestInfo.count ||
|
|
||||||
(info.count === bestInfo.count && slotScore(s) > slotScore(best))
|
|
||||||
) {
|
|
||||||
best = s;
|
|
||||||
bestInfo = info;
|
|
||||||
if (info.count <= 1) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!best) return {slot: null, info: {done: true}};
|
|
||||||
return {slot: best, info: bestInfo};
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_TRIES_PER_SLOT = 500;
|
|
||||||
|
|
||||||
function backtrack() {
|
|
||||||
nodes++;
|
|
||||||
|
|
||||||
if (timeLimitMs && (Date.now() - t0) > timeLimitMs) return false;
|
|
||||||
|
|
||||||
const pick = chooseMRV();
|
|
||||||
if (!pick.slot && pick.info && pick.info.done) return true;
|
|
||||||
if (!pick.slot) {
|
|
||||||
backtracks++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMRV = pick.info.count;
|
|
||||||
renderProgress(false);
|
|
||||||
|
|
||||||
const s = pick.slot;
|
|
||||||
const k = slotKey(s);
|
|
||||||
const entry = dictIndex.get(s.len);
|
|
||||||
const pat = patternForSlot(s);
|
|
||||||
|
|
||||||
const tryWord = (w) => {
|
|
||||||
if (!w) return false;
|
|
||||||
if (used.has(w)) return false;
|
|
||||||
|
|
||||||
for (let i = 0; i < pat.length; i++) {
|
|
||||||
if (pat[i] && pat[i] !== w[i]) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const undo = placeWord(s, w);
|
|
||||||
if (!undo) return false;
|
|
||||||
|
|
||||||
used.add(w);
|
|
||||||
assigned.set(k, w);
|
|
||||||
|
|
||||||
if (backtrack()) return true;
|
|
||||||
|
|
||||||
assigned.delete(k);
|
|
||||||
used.delete(w);
|
|
||||||
undoPlace(undo);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// constrained: iterate indices (bounded)
|
|
||||||
if (pick.info.indices && pick.info.indices.length) {
|
|
||||||
const idxs = pick.info.indices;
|
|
||||||
const L = idxs.length;
|
|
||||||
const tries = Math.min(MAX_TRIES_PER_SLOT, L);
|
|
||||||
|
|
||||||
// safe stepping even for L=1
|
|
||||||
const start = (L === 1) ? 0 : rng.int(0, L - 1);
|
|
||||||
const step = (L <= 1) ? 1 : rng.int(1, L - 1);
|
|
||||||
|
|
||||||
for (let t = 0; t < tries; t++) {
|
|
||||||
const idx = idxs[(start + t * step) % L];
|
|
||||||
const w = entry.words[idx];
|
|
||||||
if (tryWord(w)) return true;
|
|
||||||
}
|
|
||||||
backtracks++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unconstrained: sample without building arrays
|
|
||||||
const N = entry.words.length;
|
|
||||||
if (N === 0) {
|
|
||||||
backtracks++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tries = Math.min(MAX_TRIES_PER_SLOT, N);
|
|
||||||
const start = (N === 1) ? 0 : rng.int(0, N - 1);
|
|
||||||
const step = (N <= 1) ? 1 : rng.int(1, N - 1);
|
|
||||||
|
|
||||||
for (let t = 0; t < tries; t++) {
|
|
||||||
const idx = (start + t * step) % N;
|
|
||||||
const w = entry.words[idx];
|
|
||||||
if (tryWord(w)) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
backtracks++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderProgress(false);
|
|
||||||
const ok = backtrack();
|
|
||||||
renderProgress(true);
|
|
||||||
|
|
||||||
const clueMap = {};
|
|
||||||
for (const [k, v] of assigned.entries()) clueMap[k] = v;
|
|
||||||
return {ok, grid, clueMap, stats: {nodes, backtracks, seconds: (Date.now() - t0) / 1000}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --- Top-level: try mask+fill until success --- */
|
|
||||||
function generatePuzzle(opts) {
|
|
||||||
const rng = makeRng(opts.seed);
|
|
||||||
console.time("LOAD_WORDS");
|
|
||||||
const dict = loadWords(opts.wordsPath);
|
|
||||||
console.timeEnd("LOAD_WORDS");
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= opts.tries; attempt++) {
|
|
||||||
console.log(`\nAttempt ${attempt}/${opts.tries}`);
|
|
||||||
console.time("MASK");
|
|
||||||
const mask = generateMask(rng, dict.lenCounts, opts.pop, opts.gens);
|
|
||||||
console.timeEnd("MASK");
|
|
||||||
|
|
||||||
console.time("FILL");
|
|
||||||
const filled = fillMask(rng, mask, dict.index, {logEveryMs: 200, timeLimitMs: 30000});
|
|
||||||
console.timeEnd("FILL");
|
|
||||||
|
|
||||||
if (filled.ok) return {mask, filled};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {parseArgs, generatePuzzle, gridToString};
|
|
||||||
@@ -59,7 +59,7 @@ public class ThemeGraph {
|
|||||||
* Score a word against a theme (0.0 = no match, 1.0 = perfect match)
|
* Score a word against a theme (0.0 = no match, 1.0 = perfect match)
|
||||||
*/
|
*/
|
||||||
public static double scoreWordTheme(String word, String theme) {
|
public static double scoreWordTheme(String word, String theme) {
|
||||||
Set<String> keywords = THEME_KEYWORDS.get(theme.toLowerCase());
|
var keywords = THEME_KEYWORDS.get(theme.toLowerCase());
|
||||||
if (keywords == null) {
|
if (keywords == null) {
|
||||||
return 0.5; // unknown theme = neutral score
|
return 0.5; // unknown theme = neutral score
|
||||||
}
|
}
|
||||||
@@ -72,15 +72,15 @@ public class ThemeGraph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Substring match (partial relevance)
|
// Substring match (partial relevance)
|
||||||
for (String kw : keywords) {
|
for (var kw : keywords) {
|
||||||
if (word.contains(kw) || kw.contains(word)) {
|
if (word.contains(kw) || kw.contains(word)) {
|
||||||
return 0.7;
|
return 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit distance similarity (for typos/variations)
|
// Edit distance similarity (for typos/variations)
|
||||||
for (String kw : keywords) {
|
for (var kw : keywords) {
|
||||||
double similarity = editDistanceSimilarity(word, kw);
|
var similarity = editDistanceSimilarity(word, kw);
|
||||||
if (similarity > 0.8) {
|
if (similarity > 0.8) {
|
||||||
return similarity * 0.9;
|
return similarity * 0.9;
|
||||||
}
|
}
|
||||||
@@ -94,8 +94,8 @@ public class ThemeGraph {
|
|||||||
*/
|
*/
|
||||||
public static List<String> filterByTheme(List<String> words, String theme, double minScore) {
|
public static List<String> filterByTheme(List<String> words, String theme, double minScore) {
|
||||||
List<String> filtered = new ArrayList<>();
|
List<String> filtered = new ArrayList<>();
|
||||||
for (String word : words) {
|
for (var word : words) {
|
||||||
double score = scoreWordTheme(word, theme);
|
var score = scoreWordTheme(word, theme);
|
||||||
if (score >= minScore) {
|
if (score >= minScore) {
|
||||||
filtered.add(word);
|
filtered.add(word);
|
||||||
}
|
}
|
||||||
@@ -108,8 +108,8 @@ public class ThemeGraph {
|
|||||||
*/
|
*/
|
||||||
public static List<ThemeScore> getThemesForWord(String word) {
|
public static List<ThemeScore> getThemesForWord(String word) {
|
||||||
List<ThemeScore> scores = new ArrayList<>();
|
List<ThemeScore> scores = new ArrayList<>();
|
||||||
for (String theme : THEME_KEYWORDS.keySet()) {
|
for (var theme : THEME_KEYWORDS.keySet()) {
|
||||||
double score = scoreWordTheme(word, theme);
|
var score = scoreWordTheme(word, theme);
|
||||||
if (score > 0.0) {
|
if (score > 0.0) {
|
||||||
scores.add(new ThemeScore(theme, score));
|
scores.add(new ThemeScore(theme, score));
|
||||||
}
|
}
|
||||||
@@ -124,9 +124,9 @@ public class ThemeGraph {
|
|||||||
public static String detectTheme(List<String> words) {
|
public static String detectTheme(List<String> words) {
|
||||||
Map<String, Double> themeScores = new HashMap<>();
|
Map<String, Double> themeScores = new HashMap<>();
|
||||||
|
|
||||||
for (String theme : THEME_KEYWORDS.keySet()) {
|
for (var theme : THEME_KEYWORDS.keySet()) {
|
||||||
double totalScore = 0;
|
double totalScore = 0;
|
||||||
for (String word : words) {
|
for (var word : words) {
|
||||||
totalScore += scoreWordTheme(word, theme);
|
totalScore += scoreWordTheme(word, theme);
|
||||||
}
|
}
|
||||||
themeScores.put(theme, totalScore / words.size());
|
themeScores.put(theme, totalScore / words.size());
|
||||||
@@ -142,21 +142,21 @@ public class ThemeGraph {
|
|||||||
* Simple edit distance similarity (normalized Levenshtein)
|
* Simple edit distance similarity (normalized Levenshtein)
|
||||||
*/
|
*/
|
||||||
private static double editDistanceSimilarity(String a, String b) {
|
private static double editDistanceSimilarity(String a, String b) {
|
||||||
int dist = levenshtein(a, b);
|
var dist = levenshtein(a, b);
|
||||||
int maxLen = Math.max(a.length(), b.length());
|
var maxLen = Math.max(a.length(), b.length());
|
||||||
if (maxLen == 0) return 1.0;
|
if (maxLen == 0) return 1.0;
|
||||||
return 1.0 - ((double) dist / maxLen);
|
return 1.0 - ((double) dist / maxLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int levenshtein(String a, String b) {
|
private static int levenshtein(String a, String b) {
|
||||||
int[][] dp = new int[a.length() + 1][b.length() + 1];
|
var dp = new int[a.length() + 1][b.length() + 1];
|
||||||
|
|
||||||
for (int i = 0; i <= a.length(); i++) dp[i][0] = i;
|
for (var i = 0; i <= a.length(); i++) dp[i][0] = i;
|
||||||
for (int j = 0; j <= b.length(); j++) dp[0][j] = j;
|
for (var j = 0; j <= b.length(); j++) dp[0][j] = j;
|
||||||
|
|
||||||
for (int i = 1; i <= a.length(); i++) {
|
for (var i = 1; i <= a.length(); i++) {
|
||||||
for (int j = 1; j <= b.length(); j++) {
|
for (var j = 1; j <= b.length(); j++) {
|
||||||
int cost = (a.charAt(i - 1) == b.charAt(j - 1)) ? 0 : 1;
|
var cost = (a.charAt(i - 1) == b.charAt(j - 1)) ? 0 : 1;
|
||||||
dp[i][j] = Math.min(
|
dp[i][j] = Math.min(
|
||||||
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
|
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
|
||||||
dp[i - 1][j - 1] + cost
|
dp[i - 1][j - 1] + cost
|
||||||
@@ -180,26 +180,26 @@ public class ThemeGraph {
|
|||||||
System.out.println("=== Theme Graph Test ===\n");
|
System.out.println("=== Theme Graph Test ===\n");
|
||||||
|
|
||||||
// Test word scoring
|
// Test word scoring
|
||||||
String[] testWords = {"POLITIEK", "VOETBAL", "COMPUTER", "REGEN", "AUTO"};
|
var testWords = new String[]{ "POLITIEK", "VOETBAL", "COMPUTER", "REGEN", "AUTO" };
|
||||||
for (String word : testWords) {
|
for (var word : testWords) {
|
||||||
System.out.println("Word: " + word);
|
System.out.println("Word: " + word);
|
||||||
List<ThemeScore> themes = getThemesForWord(word);
|
var themes = getThemesForWord(word);
|
||||||
for (ThemeScore ts : themes) {
|
for (var ts : themes) {
|
||||||
System.out.println(" " + ts);
|
System.out.println(" " + ts);
|
||||||
}
|
}
|
||||||
System.out.println();
|
System.out.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test theme detection
|
// Test theme detection
|
||||||
List<String> techWords = Arrays.asList("COMPUTER", "INTERNET", "SOFTWARE", "DATA");
|
var techWords = Arrays.asList("COMPUTER", "INTERNET", "SOFTWARE", "DATA");
|
||||||
String detected = detectTheme(techWords);
|
var detected = detectTheme(techWords);
|
||||||
System.out.println("Detected theme for tech words: " + detected);
|
System.out.println("Detected theme for tech words: " + detected);
|
||||||
|
|
||||||
// Test filtering
|
// Test filtering
|
||||||
List<String> allWords = Arrays.asList(
|
var allWords = Arrays.asList(
|
||||||
"POLITIEK", "COMPUTER", "AUTO", "VOETBAL", "INTERNET", "BOOM"
|
"POLITIEK", "COMPUTER", "AUTO", "VOETBAL", "INTERNET", "BOOM"
|
||||||
);
|
);
|
||||||
List<String> filtered = filterByTheme(allWords, "technologie", 0.5);
|
var filtered = filterByTheme(allWords, "technologie", 0.5);
|
||||||
System.out.println("\nFiltered for 'technologie' (min 0.5): " + filtered);
|
System.out.println("\nFiltered for 'technologie' (min 0.5): " + filtered);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user