192 lines
5.6 KiB
JavaScript
192 lines
5.6 KiB
JavaScript
// 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};
|