diff --git a/dev-dist/sw.js b/dev-dist/sw.js index b5f7741..8d96cbc 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict'; */ workbox.precacheAndRoute([{ "url": "index.html", - "revision": "0.geiftdl7j9o" + "revision": "0.kkc80cp3p5o" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/utils/solver.js b/src/utils/solver.js index 90c2635..1e04010 100644 --- a/src/utils/solver.js +++ b/src/utils/solver.js @@ -18,7 +18,7 @@ const memo = new Map(); * @param {number[]} hints - Array of block lengths * @returns {number[]} - Updated line (or null if contradiction/impossible - though shouldn't happen for valid puzzles) */ -function solveLine(currentLine, hints) { +export function solveLine(currentLine, hints) { const length = currentLine.length; // If no hints, all must be empty @@ -57,6 +57,8 @@ function solveLine(currentLine, hints) { break; } } + // Cannot skip a filled cell - if we pass a '1', it becomes uncovered + if (currentLine[currentIdx] === 1) return null; currentIdx++; } if (leftPositions.length <= hIndex) return null; // Impossible @@ -81,6 +83,8 @@ function solveLine(currentLine, hints) { break; } } + // Cannot skip a filled cell + if (reversedLine[currentIdx] === 1) return null; currentIdx++; } if (rightPositionsReversed.length <= hIndex) return null; diff --git a/src/workers/solverWorker.js b/src/workers/solverWorker.js index 07c6f9b..e3e2362 100644 --- a/src/workers/solverWorker.js +++ b/src/workers/solverWorker.js @@ -1,4 +1,5 @@ import { calculateHints } from '../utils/puzzleUtils.js'; +import { solveLine } from '../utils/solver.js'; const messages = { pl: { @@ -40,195 +41,33 @@ const t = (locale, key, params) => { return typeof value === 'string' ? format(value, params) : key; }; -const buildPrefix = (lineState) => { - const n = lineState.length; - const filled = new Array(n + 1).fill(0); - const cross = new Array(n + 1).fill(0); - for (let i = 0; i < n; i++) { - filled[i + 1] = filled[i] + (lineState[i] === 1 ? 1 : 0); - cross[i + 1] = cross[i] + (lineState[i] === 2 ? 1 : 0); - } - return { filled, cross }; -}; - -const buildSuffixMin = (hints) => { - const m = hints.length; - const suffixMin = new Array(m + 1).fill(0); - let sumHints = 0; - for (let i = m - 1; i >= 0; i--) { - sumHints += hints[i]; - const separators = m - i - 1; - suffixMin[i] = sumHints + separators; - } - return suffixMin; -}; - const solveLineLogic = (lineState, hints) => { - const n = lineState.length; - const m = hints.length; - if (m === 0) { - for (let i = 0; i < n; i++) { - if (lineState[i] === 0) return { index: i, state: 2 }; - } - return { index: -1 }; - } + // Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty) + const solverLine = lineState.map(cell => { + if (cell === 0) return -1; // Unknown + if (cell === 1) return 1; // Filled + if (cell === 2) return 0; // Empty/Cross + return -1; + }); - const { filled, cross } = buildPrefix(lineState); - const suffixMin = buildSuffixMin(hints); + // Call robust solver + const resultLine = solveLine(solverLine, hints); - const hasFilled = (a, b) => filled[b] - filled[a] > 0; - const hasCross = (a, b) => cross[b] - cross[a] > 0; + // Check for new info + if (!resultLine) return { index: -1 }; // Contradiction or error - const memoSuffix = Array.from({ length: n + 1 }, () => Array(m + 1).fill(null)); - const memoPrefix = Array.from({ length: n + 1 }, () => Array(m + 1).fill(null)); - - const canPlaceSuffix = (pos, hintIndex) => { - const cached = memoSuffix[pos][hintIndex]; - if (cached !== null) return cached; - if (hintIndex === m) { - const result = !hasFilled(pos, n); - memoSuffix[pos][hintIndex] = result; - return result; - } - const len = hints[hintIndex]; - // maxStart logic: we need enough space for this block (len) + subsequent blocks/gaps (suffixMin[hintIndex+1]) - // suffixMin[hintIndex] = len + (m - hintIndex - 1) + suffixMin[hintIndex+1] - // Actually suffixMin[hintIndex] already includes everything needed from here to end. - // So if we place block at start, end is start + len. - // Total space needed is suffixMin[hintIndex]. - // So start can go up to n - suffixMin[hintIndex]. - const maxStart = n - suffixMin[hintIndex]; - - for (let start = pos; start <= maxStart; start++) { - if (hasFilled(pos, start)) continue; // Must be empty before this block - if (hasCross(start, start + len)) continue; // Block space must be free of crosses - - // If not the last block, we need a gap after - if (hintIndex < m - 1) { - if (start + len < n && lineState[start + len] === 1) continue; // Gap must not be filled - // We can assume gap is at start + len. Next block starts at least at start + len + 1 - const nextPos = start + len + 1; - if (canPlaceSuffix(nextPos, hintIndex + 1)) { - memoSuffix[pos][hintIndex] = true; - return true; - } - } else { - // Last block - // Check if we can fill the rest with empty - if (hasFilled(start + len, n)) continue; - memoSuffix[pos][hintIndex] = true; - return true; + for (let i = 0; i < lineState.length; i++) { + // We only care about cells that are currently 0 (Unknown) in Store + if (lineState[i] === 0) { + if (resultLine[i] === 1) { + return { index: i, state: 1 }; // Suggest Fill } - } - memoSuffix[pos][hintIndex] = false; - return false; - }; - - const canPlacePrefix = (pos, hintCount) => { - const cached = memoPrefix[pos][hintCount]; - if (cached !== null) return cached; - if (hintCount === 0) { - const result = !hasFilled(0, pos); - memoPrefix[pos][hintCount] = result; - return result; - } - const len = hints[hintCount - 1]; - - // Logic for prefix: - // We are placing the (hintCount-1)-th block ending at 'start + len' <= pos. - // So 'start' <= pos - len. - // But we also need to ensure there is space for previous blocks. - // However, the simple constraint is just iterating backwards. - - // maxStart: if this is the only block, maxStart = pos - len. - // If there are previous blocks, we need a gap before this block. - // So previous block ended at start - 1. - // Actually the recursive call will handle space check. - // But for the gap check: - // If we place block at 'start', we need lineState[start-1] != 1 (if start > 0). - // And we recursively check canPlacePrefix(start-1, count-1). - // But if start=0 and count > 1, impossible. - - const maxStart = pos - len; // Simplified, loop condition handles rest - - for (let start = maxStart; start >= 0; start--) { - if (hasCross(start, start + len)) continue; - if (hasFilled(start + len, pos)) continue; // Must be empty after this block up to pos - - // Check gap before - if (hintCount > 1) { - if (start === 0) continue; // No space for previous blocks - if (lineState[start - 1] === 1) continue; // Gap must not be filled - const prevPos = start - 1; - if (canPlacePrefix(prevPos, hintCount - 1)) { - memoPrefix[pos][hintCount] = true; - return true; - } - } else { - // First block - if (hasFilled(0, start)) continue; // Before first block must be empty - memoPrefix[pos][hintCount] = true; - return true; - } - } - memoPrefix[pos][hintCount] = false; - return false; - }; - - const possibleStarts = []; - for (let i = 0; i < m; i++) { - const len = hints[i]; - const starts = []; - for (let start = 0; start <= n - len; start++) { - if (i === 0) { - if (!canPlacePrefix(start, 0)) continue; - } else { - if (start === 0) continue; - if (lineState[start - 1] === 1) continue; - if (!canPlacePrefix(start - 1, i)) continue; - } - - if (hasCross(start, start + len)) continue; - if (start + len < n && lineState[start + len] === 1) continue; - const nextPos = start + len < n ? start + len + 1 : start + len; - if (!canPlaceSuffix(nextPos, i + 1)) continue; - starts.push(start); - } - possibleStarts.push(starts); - } - - const mustFill = new Array(n).fill(false); - const coverage = new Array(n).fill(false); - - for (let i = 0; i < m; i++) { - const starts = possibleStarts[i]; - const len = hints[i]; - if (starts.length === 0) return { index: -1 }; - let earliest = starts[0]; - let latest = starts[0]; - for (let j = 1; j < starts.length; j++) { - earliest = Math.min(earliest, starts[j]); - latest = Math.max(latest, starts[j]); - } - const startOverlap = Math.max(earliest, latest); - const endOverlap = Math.min(earliest + len - 1, latest + len - 1); - for (let k = startOverlap; k <= endOverlap; k++) { - if (k >= 0 && k < n) mustFill[k] = true; - } - for (let s = 0; s < starts.length; s++) { - const start = starts[s]; - for (let k = start; k < start + len; k++) { - coverage[k] = true; + if (resultLine[i] === 0) { + return { index: i, state: 2 }; // Suggest Cross } } } - for (let i = 0; i < n; i++) { - if (lineState[i] === 0 && mustFill[i]) return { index: i, state: 1 }; - } - for (let i = 0; i < n; i++) { - if (lineState[i] === 0 && !coverage[i]) return { index: i, state: 2 }; - } return { index: -1 }; };