From cf84dfd7f297aa2adca78a8ace3f66b9027e7a0a Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Sat, 21 Feb 2026 20:41:19 +0000 Subject: [PATCH] Refactor solver worker to return translation keys and centralize i18n --- src/composables/useI18n.js | 20 ++- src/composables/useSolver.js | 16 +- src/workers/solver.worker.js | 277 ++++++++++++++++++++++++++++------- src/workers/solverWorker.js | 220 ---------------------------- 4 files changed, 259 insertions(+), 274 deletions(-) delete mode 100644 src/workers/solverWorker.js diff --git a/src/composables/useI18n.js b/src/composables/useI18n.js index 12e5b5c..ea47538 100644 --- a/src/composables/useI18n.js +++ b/src/composables/useI18n.js @@ -199,7 +199,15 @@ const messages = { 'simulation.table.solved': 'Rozwiązano (Logika)', 'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo', 'difficultyMap.size': 'Rozmiar', - 'difficultyMap.density': 'Gęstość' + 'difficultyMap.density': 'Gęstość', + 'worker.solved': 'Rozwiązane!', + 'worker.logicRow': 'Logika: Wiersz {row}, Kolumna {col} -> {state}', + 'worker.logicCol': 'Logika: Kolumna {col}, Wiersz {row} -> {state}', + 'worker.stuck': 'Brak logicznego ruchu. Spróbuj zgadnąć lub cofnąć.', + 'worker.boosted': 'Boost (DFS): Wiersz {row}, Kolumna {col} -> {state}', + 'worker.done': 'Koniec!', + 'worker.state.filled': 'Pełne', + 'worker.state.empty': 'Puste' }, en: { 'app.title': 'Nonograms', @@ -399,7 +407,15 @@ const messages = { 'custom.hideMap': 'Hide difficulty map', 'custom.showMap': 'Show difficulty map', 'difficultyMap.size': 'Size', - 'difficultyMap.density': 'Density' + 'difficultyMap.density': 'Density', + 'worker.solved': 'Solved!', + 'worker.logicRow': 'Logic: Row {row}, Column {col} -> {state}', + 'worker.logicCol': 'Logic: Column {col}, Row {row} -> {state}', + 'worker.stuck': 'No logical move found. Try guessing or undoing.', + 'worker.boosted': 'Boost (DFS): Row {row}, Column {col} -> {state}', + 'worker.done': 'Done!', + 'worker.state.filled': 'Filled', + 'worker.state.empty': 'Empty' }, zh: { 'app.title': 'Nonograms', diff --git a/src/composables/useSolver.js b/src/composables/useSolver.js index 7851acb..aacc4dc 100644 --- a/src/composables/useSolver.js +++ b/src/composables/useSolver.js @@ -97,10 +97,20 @@ export function useSolver() { function ensureWorker() { if (worker) return; - worker = new Worker(new URL('../workers/solverWorker.js', import.meta.url), { type: 'module' }); + worker = new Worker(new URL('../workers/solver.worker.js', import.meta.url), { type: 'module' }); worker.onmessage = (event) => { - const { type, r, c, state, statusText: text } = event.data; - if (text) statusText.value = text; + const { type, r, c, state, status } = event.data; + if (status) { + const params = status.params ? { ...status.params } : {}; + if (params.stateKey) { + params.state = t(params.stateKey); + } + statusText.value = t(status.key, params); + } else if (event.data.statusText) { + // Fallback for legacy messages if any + statusText.value = event.data.statusText; + } + if (type === 'move') { store.setCell(r, c, state); isProcessing.value = false; diff --git a/src/workers/solver.worker.js b/src/workers/solver.worker.js index c58d0d5..55aecf7 100644 --- a/src/workers/solver.worker.js +++ b/src/workers/solver.worker.js @@ -1,60 +1,239 @@ -import { calculateHints } from '../utils/puzzleUtils'; -import { solvePuzzle } from '../utils/solver'; +import { calculateHints } from '../utils/puzzleUtils.js'; +import { solveLine, solvePuzzle } from '../utils/solver.js'; +// --- Logic Helpers --- +const solveLineLogic = (lineState, hints) => { + // 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; + }); + + // Call robust solver + const resultLine = solveLine(solverLine, hints); + + // Check for new info + if (!resultLine) return { index: -1 }; // Contradiction or error + + 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 + } + if (resultLine[i] === 0) { + return { index: i, state: 2 }; // Suggest Cross + } + } + } + + return { index: -1 }; +}; + +const isSolved = (grid, solution) => { + const size = grid.length; + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + const playerCell = grid[r][c]; + const solutionCell = solution[r][c]; + const isFilled = playerCell === 1; + const shouldBeFilled = solutionCell === 1; + + // Check correctness + if (isFilled !== shouldBeFilled) return false; + + // Check completeness (must be fully resolved to 1 or 2) + if (playerCell === 0) return false; + } + } + return true; +}; + +const handleStep = (playerGrid, solution) => { + if (isSolved(playerGrid, solution)) { + return { type: 'done', status: { key: 'worker.solved' } }; + } + + const size = solution.length; + const { rowHints, colHints } = calculateHints(solution); + + for (let r = 0; r < size; r++) { + const rowLine = playerGrid[r]; + const hints = rowHints[r]; + const result = solveLineLogic(rowLine, hints); + if (result.index !== -1) { + const stateKey = result.state === 1 ? 'worker.state.filled' : 'worker.state.empty'; + return { + type: 'move', + r, + c: result.index, + state: result.state, + status: { + key: 'worker.logicRow', + params: { row: r + 1, col: result.index + 1, stateKey } + } + }; + } + } + + for (let c = 0; c < size; c++) { + const colLine = []; + for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]); + const hints = colHints[c]; + const result = solveLineLogic(colLine, hints); + if (result.index !== -1) { + const stateKey = result.state === 1 ? 'worker.state.filled' : 'worker.state.empty'; + return { + type: 'move', + r: result.index, + c, + state: result.state, + status: { + key: 'worker.logicCol', + params: { row: result.index + 1, col: c + 1, stateKey } + } + }; + } + } + + return { type: 'stuck', status: { key: 'worker.stuck' } }; +}; + +const handleBoost = (playerGrid, solution) => { + const size = solution.length; + + // 1. Try to use the Solver (DFS) to find a logical move + try { + const { rowHints, colHints } = calculateHints(solution); + + // Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty) + const solverGrid = playerGrid.map(row => row.map(cell => { + if (cell === 0) return -1; + if (cell === 1) return 1; + if (cell === 2) return 0; + return -1; + })); + + // Run full solver (logicOnly=false allows DFS/guessing) + const result = solvePuzzle(rowHints, colHints, null, solverGrid, false); + + if (result && result.solution) { + const solvedGrid = result.solution; + + // Find the first cell that is Unknown in playerGrid but Known in solvedGrid + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + if (playerGrid[r][c] === 0) { // Unknown in Player + const solvedVal = solvedGrid[r][c]; // -1=Unk, 0=Empty, 1=Filled + + if (solvedVal !== -1) { + // Found a logical deduction! + const newState = solvedVal === 1 ? 1 : 2; // 1->Filled, 0->Cross + const stateKey = newState === 1 ? 'worker.state.filled' : 'worker.state.empty'; + return { + type: 'move', + r, + c, + state: newState, + status: { + key: 'worker.boosted', + params: { row: r + 1, col: c + 1, stateKey } + } + }; + } + } + } + } + } + } catch (e) { + console.warn('Boost Solver failed, falling back to simple reveal:', e); + } + + // 2. Fallback: Cheat + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + if (playerGrid[r][c] === 0) { + const correctState = solution[r][c] === 1 ? 1 : 2; + const stateKey = correctState === 1 ? 'worker.state.filled' : 'worker.state.empty'; + return { + type: 'move', + r, + c, + state: correctState, + status: { + key: 'worker.boosted', + params: { row: r + 1, col: c + 1, stateKey } + } + }; + } + } + } + return { type: 'done', status: { key: 'worker.solved' } }; +}; + +// --- Main Worker Handler --- self.onmessage = (e) => { - const { id, grid, initialGrid } = e.data; + const { id, grid, initialGrid, playerGrid, solution, action } = e.data; try { - if (!grid || grid.length === 0) { - self.postMessage({ id, error: 'Empty grid' }); + // Mode 1: Analysis (Batch) - from ImageImportModal / workerPool + if (grid) { + if (grid.length === 0) { + self.postMessage({ id, error: 'Empty grid' }); + return; + } + + const rows = grid.length; + const cols = grid[0].length; + const { rowHints, colHints } = calculateHints(grid); + + const onProgress = (percent) => { + self.postMessage({ + id, + type: 'progress', + percent + }); + }; + + const { percentSolved, difficultyScore } = solvePuzzle(rowHints, colHints, onProgress, initialGrid); + + let value = difficultyScore; + let level; + + if (percentSolved < 100) { + level = 'extreme'; + } else { + if (value < 25) level = 'easy'; + else if (value < 50) level = 'medium'; + else if (value < 75) level = 'hard'; + else level = 'extreme'; + } + + self.postMessage({ + id, + solvability: Math.floor(percentSolved), + difficulty: Math.round(value), + difficultyLabel: level, + rows, + cols + }); return; } - const rows = grid.length; - const cols = grid[0].length; - // Use initialGrid if provided, otherwise assume we are starting fresh - // BUT wait, 'grid' passed here is usually the 0/1 grid from Image Import (target pattern). - // 'initialGrid' would be the partial solution state (-1/0/1). - - // 1. Calculate Hints from the TARGET grid (the image) - const { rowHints, colHints } = calculateHints(grid); - - // 2. Run Solver (Logic + Lookahead) - const onProgress = (percent) => { - self.postMessage({ - id, - type: 'progress', - percent - }); - }; - - const { percentSolved, difficultyScore, lookaheadUsed } = solvePuzzle(rowHints, colHints, onProgress, initialGrid); - - // 3. Determine Level - let value = difficultyScore; - let level; - - if (percentSolved < 100) { - level = 'extreme'; // Unsolvable by logic+lookahead - } else { - if (value < 25) level = 'easy'; - else if (value < 50) level = 'medium'; - else if (value < 75) level = 'hard'; - else level = 'extreme'; + // Mode 2: Assistant (Step/Boost) - from useSolver + if (playerGrid) { + let result; + if (action === 'boost') { + result = handleBoost(playerGrid, solution); + } else { + result = handleStep(playerGrid, solution); + } + + self.postMessage({ id, ...result }); + return; } - - // Add specific note if lookahead was needed? - // UI doesn't have a field for that, but we can encode it in difficultyLabel if needed. - // For now, standard levels are fine. - - self.postMessage({ - id, - solvability: Math.floor(percentSolved), - difficulty: Math.round(value), - difficultyLabel: level, - rows, - cols - }); } catch (err) { self.postMessage({ id, error: err.message }); diff --git a/src/workers/solverWorker.js b/src/workers/solverWorker.js deleted file mode 100644 index 454e1ed..0000000 --- a/src/workers/solverWorker.js +++ /dev/null @@ -1,220 +0,0 @@ -import { calculateHints } from '../utils/puzzleUtils.js'; -import { solveLine, solvePuzzle } from '../utils/solver.js'; - -const messages = { - pl: { - 'worker.solved': 'Rozwiązane!', - 'worker.logicRow': 'Logika: Wiersz {row}, Kolumna {col} -> {state}', - 'worker.logicCol': 'Logika: Kolumna {col}, Wiersz {row} -> {state}', - 'worker.stuck': 'Brak logicznego ruchu. Spróbuj zgadnąć lub cofnąć.', - 'worker.boosted': 'Boost (DFS): Wiersz {row}, Kolumna {col} -> {state}', - 'worker.done': 'Koniec!', - 'worker.state.filled': 'Pełne', - 'worker.state.empty': 'Puste' - }, - en: { - 'worker.solved': 'Solved!', - 'worker.logicRow': 'Logic: Row {row}, Column {col} -> {state}', - 'worker.logicCol': 'Logic: Column {col}, Row {row} -> {state}', - 'worker.stuck': 'No logical move found. Try guessing or undoing.', - 'worker.boosted': 'Boost (DFS): Row {row}, Column {col} -> {state}', - 'worker.done': 'Done!', - 'worker.state.filled': 'Filled', - 'worker.state.empty': 'Empty' - } -}; - -const resolveLocale = (value) => { - if (!value) return 'en'; - const short = String(value).toLowerCase().split('-')[0]; - return short === 'pl' ? 'pl' : 'en'; -}; - -const format = (text, params = {}) => { - return text.replace(/\{(\w+)\}/g, (_, key) => { - const value = params[key]; - return value === undefined ? `{${key}}` : String(value); - }); -}; - -const t = (locale, key, params) => { - const lang = messages[locale] || messages.en; - const value = lang[key] || messages.en[key] || key; - return typeof value === 'string' ? format(value, params) : key; -}; - -const solveLineLogic = (lineState, hints) => { - // 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; - }); - - // Call robust solver - const resultLine = solveLine(solverLine, hints); - - // Check for new info - if (!resultLine) return { index: -1 }; // Contradiction or error - - 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 - } - if (resultLine[i] === 0) { - return { index: i, state: 2 }; // Suggest Cross - } - } - } - - return { index: -1 }; -}; - -const isSolved = (grid, solution) => { - const size = grid.length; - for (let r = 0; r < size; r++) { - for (let c = 0; c < size; c++) { - const playerCell = grid[r][c]; - const solutionCell = solution[r][c]; - const isFilled = playerCell === 1; - const shouldBeFilled = solutionCell === 1; - - // Check correctness - if (isFilled !== shouldBeFilled) return false; - - // Check completeness (must be fully resolved to 1 or 2) - if (playerCell === 0) return false; - } - } - return true; -}; - -const handleStep = (playerGrid, solution, locale) => { - if (isSolved(playerGrid, solution)) { - return { type: 'done', statusText: t(locale, 'worker.solved') }; - } - - const size = solution.length; - const { rowHints, colHints } = calculateHints(solution); - - for (let r = 0; r < size; r++) { - const rowLine = playerGrid[r]; - const hints = rowHints[r]; - const result = solveLineLogic(rowLine, hints); - if (result.index !== -1) { - const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty'); - return { - type: 'move', - r, - c: result.index, - state: result.state, - statusText: t(locale, 'worker.logicRow', { row: r + 1, col: result.index + 1, state: stateLabel }) - }; - } - } - - for (let c = 0; c < size; c++) { - const colLine = []; - for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]); - const hints = colHints[c]; - const result = solveLineLogic(colLine, hints); - if (result.index !== -1) { - const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty'); - return { - type: 'move', - r: result.index, - c, - state: result.state, - statusText: t(locale, 'worker.logicCol', { row: result.index + 1, col: c + 1, state: stateLabel }) - }; - } - } - - // Check for guess logic - we want to avoid this unless strictly necessary - // If no logic move found, return 'stuck' instead of cheating - return { type: 'stuck', statusText: t(locale, 'worker.stuck') }; -}; - -const handleBoost = (playerGrid, solution, locale) => { - const size = solution.length; - - // 1. Try to use the Solver (DFS) to find a logical move - try { - const { rowHints, colHints } = calculateHints(solution); - - // Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty) - const solverGrid = playerGrid.map(row => row.map(cell => { - if (cell === 0) return -1; - if (cell === 1) return 1; - if (cell === 2) return 0; - return -1; - })); - - // Run full solver (logicOnly=false allows DFS/guessing) - // We pass solverGrid as initial state to respect user's moves - const result = solvePuzzle(rowHints, colHints, null, solverGrid, false); - - if (result && result.solution) { - const solvedGrid = result.solution; - - // Find the first cell that is Unknown in playerGrid but Known in solvedGrid - for (let r = 0; r < size; r++) { - for (let c = 0; c < size; c++) { - if (playerGrid[r][c] === 0) { // Unknown in Player - const solvedVal = solvedGrid[r][c]; // -1=Unk, 0=Empty, 1=Filled - - if (solvedVal !== -1) { - // Found a logical deduction! - const newState = solvedVal === 1 ? 1 : 2; // 1->Filled, 0->Cross - const stateLabel = t(locale, newState === 1 ? 'worker.state.filled' : 'worker.state.empty'); - return { - type: 'move', - r, - c, - state: newState, - statusText: t(locale, 'worker.boosted', { row: r + 1, col: c + 1, state: stateLabel }) - }; - } - } - } - } - } - } catch (e) { - console.warn('Boost Solver failed, falling back to simple reveal:', e); - } - - // 2. Fallback: If solver failed (e.g. contradiction due to user error), - // or no new info found, use the "Cheat" method (reveal from true solution). - for (let r = 0; r < size; r++) { - for (let c = 0; c < size; c++) { - if (playerGrid[r][c] === 0) { - const correctState = solution[r][c] === 1 ? 1 : 2; - const stateLabel = t(locale, correctState === 1 ? 'worker.state.filled' : 'worker.state.empty'); - return { - type: 'move', - r, - c, - state: correctState, - statusText: t(locale, 'worker.boosted', { row: r + 1, col: c + 1, state: stateLabel }) - }; - } - } - } - return { type: 'done', statusText: t(locale, 'worker.solved') }; -}; - -self.onmessage = (event) => { - const { id, playerGrid, solution, locale, action } = event.data; - const resolved = resolveLocale(locale); - - if (action === 'boost') { - const result = handleBoost(playerGrid, solution, resolved); - self.postMessage({ id, ...result }); - } else { - const result = handleStep(playerGrid, solution, resolved); - self.postMessage({ id, ...result }); - } -};