initial commit

This commit is contained in:
mike
2025-12-19 13:12:14 +01:00
parent 5ee765997e
commit 1960109601
71 changed files with 977009 additions and 386992 deletions

191
export_format.js Normal file
View File

@@ -0,0 +1,191 @@
// 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; youll 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};